refactor(game): 重构游戏组件和数据结构
- 移除中心模态框的背景模糊效果并调整透明度 - 为桌面游戏历史组件添加空状态显示组件 - 重构头部时钟显示逻辑,提取为独立组件并优化时间同步 - 移除用户ID遮罩功能,直接使用昵称显示中奖信息 - 调整入口页面的模态框渲染结构和认证状态检查逻辑 - 更新奖池广播数据结构,替换用户ID为昵称字段 - 优化实时同步中的数据验证和映射逻辑 - 调整移动端头部时钟组件的实现方式
This commit is contained in:
@@ -71,7 +71,7 @@ export function CenterModal({
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 flex items-center justify-center bg-slate-950/72 backdrop-blur-sm',
|
||||
'fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80',
|
||||
backdropClassName,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -214,10 +214,10 @@ export interface GamePeriodTickDto {
|
||||
}
|
||||
|
||||
export interface JackpotHitItemDto {
|
||||
nickname: string
|
||||
period_no: string
|
||||
result_number: number
|
||||
total_win: string
|
||||
user_id: number
|
||||
}
|
||||
|
||||
export interface JackpotHitEventDataDto {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { History } from 'lucide-react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import historyBg from '@/assets/system/history-bg.png'
|
||||
@@ -43,6 +44,25 @@ function HistoryRewardNumber({
|
||||
)
|
||||
}
|
||||
|
||||
function HistoryEmptyState({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="flex min-h-full w-full flex-1 items-center justify-center px-design-10 py-design-10">
|
||||
<div className="flex w-fit max-w-full flex-col items-center rounded-[calc(var(--design-unit)*8)] border border-[rgba(94,212,230,0.14)] bg-[rgba(5,23,33,0.3)] px-design-14 py-design-12 text-center">
|
||||
<div className="mb-design-8 flex h-design-42 w-design-42 items-center justify-center rounded-full border border-[#56EFFF]/18 bg-[#061D29]/55 shadow-[0_0_calc(var(--design-unit)*10)_rgba(86,239,255,0.08)]">
|
||||
<History
|
||||
aria-hidden="true"
|
||||
className="h-design-20 w-design-20 text-[#A8EAF1]"
|
||||
strokeWidth={1.8}
|
||||
/>
|
||||
</div>
|
||||
<div className="whitespace-nowrap text-design-16 font-semibold text-[#9CCFD4]">
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DesktopGameHistory() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
@@ -99,13 +119,7 @@ export function DesktopGameHistory() {
|
||||
className="min-h-full flex-1"
|
||||
/>
|
||||
) : isEmpty ? (
|
||||
<div
|
||||
className={
|
||||
'flex w-full flex-1 items-center justify-center text-design-18 text-[#84A2A2]'
|
||||
}
|
||||
>
|
||||
{emptyText}
|
||||
</div>
|
||||
<HistoryEmptyState label={emptyText} />
|
||||
) : (
|
||||
<>
|
||||
{items.map((item) => {
|
||||
|
||||
@@ -14,7 +14,16 @@ import avatar from '@/assets/system/avatar.webp'
|
||||
import diamond from '@/assets/system/diamond.webp'
|
||||
import logo from '@/assets/system/logo.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { useHeaderVm } from '@/features/game/hooks/use-header-vm'
|
||||
import {
|
||||
useHeaderClockLabel,
|
||||
useHeaderVm,
|
||||
} from '@/features/game/hooks/use-header-vm'
|
||||
|
||||
function HeaderClock() {
|
||||
const systemTimeLabel = useHeaderClockLabel()
|
||||
|
||||
return <div className={'text-[#D2FCFF] font-bold'}>{systemTimeLabel}</div>
|
||||
}
|
||||
|
||||
function SignalBars({
|
||||
activeBars,
|
||||
@@ -66,12 +75,11 @@ export function DesktopHeader() {
|
||||
onOpenRules,
|
||||
onOpenUserInfo,
|
||||
signalPresentation,
|
||||
systemTimeLabel,
|
||||
toggleSoundEnabled,
|
||||
} = useHeaderVm()
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 border-b border-white/8 bg-slate-950/70 backdrop-blur-xl">
|
||||
<header className="sticky top-0 z-30 border-b border-white/8 bg-[#020B14]/95 ">
|
||||
<div className="flex h-design-70 w-full items-center px-design-12">
|
||||
<div className="flex h-full w-design-375 items-center justify-center border-r border-[rgba(128,223,231,0.65)] px-design-10">
|
||||
<SmartImage
|
||||
@@ -99,7 +107,7 @@ export function DesktopHeader() {
|
||||
<div className={'text-[#B4E4E9]'}>
|
||||
{t('gameDesktop.header.systemTime')}
|
||||
</div>
|
||||
<div className={'text-[#D2FCFF] font-bold'}>{systemTimeLabel}</div>
|
||||
<HeaderClock />
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-1 items-center justify-around gap-design-10 border-r border-[rgba(128,223,231,0.65)] px-design-20">
|
||||
|
||||
@@ -6,20 +6,6 @@ const winAmountFormatter = new Intl.NumberFormat('en-US', {
|
||||
maximumFractionDigits: 6,
|
||||
})
|
||||
|
||||
function maskParticipantLabel(value: number | string) {
|
||||
const label = String(value).trim()
|
||||
|
||||
if (label.length <= 1) {
|
||||
return '*'
|
||||
}
|
||||
|
||||
const visibleLength = Math.ceil(label.length / 2)
|
||||
|
||||
return `${label.slice(0, visibleLength)}${'*'.repeat(
|
||||
label.length - visibleLength,
|
||||
)}`
|
||||
}
|
||||
|
||||
function formatWinAmount(value: string) {
|
||||
const amount = Number(value)
|
||||
|
||||
@@ -34,9 +20,9 @@ export function DesktopTitle() {
|
||||
jackpotBroadcasts.length > 0
|
||||
? jackpotBroadcasts.map((broadcast) => ({
|
||||
id: broadcast.id,
|
||||
message: `Player ${maskParticipantLabel(
|
||||
broadcast.userId,
|
||||
)} won ${formatWinAmount(broadcast.totalWin)}`,
|
||||
message: `Player ${broadcast.nickname} won ${formatWinAmount(
|
||||
broadcast.totalWin,
|
||||
)}`,
|
||||
}))
|
||||
: [{ id: 'empty', message: '' }]
|
||||
const marqueeTitles =
|
||||
|
||||
@@ -12,7 +12,20 @@ import avatar from '@/assets/system/avatar.webp'
|
||||
import diamond from '@/assets/system/diamond.webp'
|
||||
import logo from '@/assets/system/logo.webp'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
import { useHeaderVm } from '@/features/game/hooks/use-header-vm'
|
||||
import {
|
||||
useHeaderClockLabel,
|
||||
useHeaderVm,
|
||||
} from '@/features/game/hooks/use-header-vm'
|
||||
|
||||
function MobileHeaderClock() {
|
||||
const systemTimeLabel = useHeaderClockLabel()
|
||||
|
||||
return (
|
||||
<div className="mt-design-3 whitespace-nowrap text-design-7 font-bold text-[#D2FCFF]">
|
||||
{systemTimeLabel}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobileHeader() {
|
||||
const { t } = useTranslation()
|
||||
@@ -30,7 +43,6 @@ export function MobileHeader() {
|
||||
onOpenRules,
|
||||
onOpenUserInfo,
|
||||
signalPresentation,
|
||||
systemTimeLabel,
|
||||
toggleSoundEnabled,
|
||||
} = useHeaderVm()
|
||||
|
||||
@@ -155,9 +167,7 @@ export function MobileHeader() {
|
||||
<div className="text-[#B4E4E9]">
|
||||
{t('gameDesktop.header.systemTime')}
|
||||
</div>
|
||||
<div className="mt-design-3 whitespace-nowrap text-design-7 font-bold text-[#D2FCFF]">
|
||||
{systemTimeLabel}
|
||||
</div>
|
||||
<MobileHeaderClock />
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -69,8 +69,8 @@ export function EntryNoticeGateModal() {
|
||||
setHasEntered(false)
|
||||
setHasAgreed(false)
|
||||
|
||||
if (!hasStoredLoginInfo) {
|
||||
setShouldGateEntry(!shouldSuppressEntryGate)
|
||||
if (!hasStoredLoginInfo || shouldSuppressEntryGate) {
|
||||
setShouldGateEntry(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,14 +2,54 @@ import { startTransition, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { getGameLobbyInit } from '@/features/game'
|
||||
import { EntryNoticeGateModal } from '@/features/game/components'
|
||||
import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx'
|
||||
import { PcEntry } from '@/features/game/entry/pc-entry.tsx'
|
||||
import { useGameRealtimeSync } from '@/features/game/hooks/use-game-realtime-sync.ts'
|
||||
import DesktopAutoSettingModal from '@/features/game/modal/desktop/desktop-auto-setting-modal.tsx'
|
||||
import DesktopLanguageModal from '@/features/game/modal/desktop/desktop-language-modal.tsx'
|
||||
import DesktopLoginModal from '@/features/game/modal/desktop/desktop-login-modal.tsx'
|
||||
import DesktopNoticeModal from '@/features/game/modal/desktop/desktop-notice-modal.tsx'
|
||||
import { DesktopPeriodHistoryDrawer } from '@/features/game/modal/desktop/desktop-period-history-drawer.tsx'
|
||||
import DesktopProceduresModal from '@/features/game/modal/desktop/desktop-procedures-modal.tsx'
|
||||
import DesktopRegisterModal from '@/features/game/modal/desktop/desktop-register-modal.tsx'
|
||||
import DesktopRulesModal from '@/features/game/modal/desktop/desktop-rules-modal.tsx'
|
||||
import DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx'
|
||||
import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx'
|
||||
import { useDocumentMetadata } from '@/lib/head/document-metadata'
|
||||
import { notify } from '@/lib/notify'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
||||
|
||||
function EntryModalHost() {
|
||||
return (
|
||||
<>
|
||||
{/* 桌面端登录弹窗:用于未登录用户进入登录流程 */}
|
||||
<DesktopLoginModal />
|
||||
{/* 桌面端注册弹窗:用于新用户注册账号 */}
|
||||
<DesktopRegisterModal />
|
||||
{/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */}
|
||||
<DesktopLanguageModal />
|
||||
{/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */}
|
||||
<DesktopRulesModal />
|
||||
{/* 桌面端用户信息弹窗:展示个人资料与站内消息 */}
|
||||
<DesktopUserInfoModal />
|
||||
{/* 桌面端公告弹窗:展示活动公告或运营通知内容 */}
|
||||
<DesktopNoticeModal />
|
||||
{/* 桌面端自动托管弹窗:配置自动托管相关条件 */}
|
||||
<DesktopAutoSettingModal />
|
||||
{/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */}
|
||||
<DesktopProceduresModal />
|
||||
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
||||
<DesktopWithdrawTopupModal />
|
||||
{/* 强制弹窗 */}
|
||||
<EntryNoticeGateModal />
|
||||
{/* 历史开奖信息弹窗 */}
|
||||
<DesktopPeriodHistoryDrawer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function EntryPage() {
|
||||
const { t } = useTranslation()
|
||||
useGameRealtimeSync()
|
||||
@@ -19,6 +59,8 @@ export function EntryPage() {
|
||||
const syncConnection = useGameSessionStore((state) => state.syncConnection)
|
||||
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
||||
const authStatus = useAuthStore((state) => state.status)
|
||||
const authIsHydrated = useAuthStore((state) => state.isHydrated)
|
||||
const accessToken = useAuthStore((state) => state.accessToken)
|
||||
const lastUnauthorizedAt = useAuthStore((state) => state.lastUnauthorizedAt)
|
||||
const isReloginRequired =
|
||||
authStatus === 'anonymous' && Boolean(lastUnauthorizedAt)
|
||||
@@ -38,7 +80,13 @@ export function EntryPage() {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (isReloginRequired) {
|
||||
if (!authIsHydrated) {
|
||||
setIsHydrating(true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (isReloginRequired || authStatus !== 'authenticated' || !accessToken) {
|
||||
setIsHydrating(false)
|
||||
|
||||
return
|
||||
@@ -121,6 +169,8 @@ export function EntryPage() {
|
||||
cancelled = true
|
||||
}
|
||||
}, [
|
||||
accessToken,
|
||||
authIsHydrated,
|
||||
authStatus,
|
||||
hydrateRound,
|
||||
hydrateSession,
|
||||
@@ -156,6 +206,7 @@ export function EntryPage() {
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
>
|
||||
{isMobile ? <MobileEntry /> : <PcEntry />}
|
||||
<EntryModalHost />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { DesktopHeader, EntryNoticeGateModal } from '@/features/game/components'
|
||||
import { DesktopHeader } from '@/features/game/components'
|
||||
import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
|
||||
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
||||
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
||||
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
|
||||
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts'
|
||||
import DesktopAutoSettingModal from '@/features/game/modal/desktop/desktop-auto-setting-modal.tsx'
|
||||
import DesktopLanguageModal from '@/features/game/modal/desktop/desktop-language-modal.tsx'
|
||||
import DesktopLoginModal from '@/features/game/modal/desktop/desktop-login-modal.tsx'
|
||||
import DesktopNoticeModal from '@/features/game/modal/desktop/desktop-notice-modal.tsx'
|
||||
import { DesktopPeriodHistoryDrawer } from '@/features/game/modal/desktop/desktop-period-history-drawer.tsx'
|
||||
import DesktopProceduresModal from '@/features/game/modal/desktop/desktop-procedures-modal.tsx'
|
||||
import DesktopRegisterModal from '@/features/game/modal/desktop/desktop-register-modal.tsx'
|
||||
import DesktopRulesModal from '@/features/game/modal/desktop/desktop-rules-modal.tsx'
|
||||
import DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx'
|
||||
import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx'
|
||||
|
||||
export function PcEntry() {
|
||||
useAutoHostingRunner()
|
||||
@@ -50,28 +40,6 @@ export function PcEntry() {
|
||||
>
|
||||
<DesktopControl />
|
||||
</div>
|
||||
{/* 桌面端登录弹窗:用于未登录用户进入登录流程 */}
|
||||
<DesktopLoginModal />
|
||||
{/* 桌面端注册弹窗:用于新用户注册账号 */}
|
||||
<DesktopRegisterModal />
|
||||
{/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */}
|
||||
<DesktopLanguageModal />
|
||||
{/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */}
|
||||
<DesktopRulesModal />
|
||||
{/* 桌面端用户信息弹窗:展示个人资料与站内消息 */}
|
||||
<DesktopUserInfoModal />
|
||||
{/* 桌面端公告弹窗:展示活动公告或运营通知内容 */}
|
||||
<DesktopNoticeModal />
|
||||
{/* 桌面端自动托管弹窗:配置自动托管相关条件 */}
|
||||
<DesktopAutoSettingModal />
|
||||
{/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */}
|
||||
<DesktopProceduresModal />
|
||||
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
||||
<DesktopWithdrawTopupModal />
|
||||
{/* 强制弹窗 */}
|
||||
<EntryNoticeGateModal />
|
||||
{/* 历史开奖信息弹窗 */}
|
||||
<DesktopPeriodHistoryDrawer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
|
||||
const source = value as Record<string, unknown>
|
||||
|
||||
if (
|
||||
typeof source.user_id !== 'number' ||
|
||||
typeof source.nickname !== 'string' ||
|
||||
typeof source.period_no !== 'string' ||
|
||||
typeof source.total_win !== 'string'
|
||||
) {
|
||||
@@ -270,10 +270,10 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
|
||||
}
|
||||
|
||||
return {
|
||||
nickname: source.nickname,
|
||||
period_no: source.period_no,
|
||||
result_number: resultNumber,
|
||||
total_win: source.total_win,
|
||||
user_id: source.user_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,18 +286,31 @@ function extractJackpotHitData(
|
||||
return null
|
||||
}
|
||||
|
||||
const serverTime = toOptionalNumber(data.server_time)
|
||||
const nestedHits = data.hits
|
||||
const sourceHits = Array.isArray(nestedHits)
|
||||
? nestedHits
|
||||
: nestedHits && typeof nestedHits === 'object'
|
||||
? [nestedHits]
|
||||
: [data]
|
||||
const firstHitSource = sourceHits.find(
|
||||
(item): item is Record<string, unknown> =>
|
||||
Boolean(item) && typeof item === 'object',
|
||||
)
|
||||
const hits = sourceHits
|
||||
.map((item) => extractJackpotHitItem(item))
|
||||
.filter((item): item is JackpotHitItemDto => item !== null)
|
||||
const root = message as Record<string, unknown>
|
||||
const serverTime = toOptionalNumber(
|
||||
data.server_time ?? firstHitSource?.server_time ?? root.server_time,
|
||||
)
|
||||
const resultNumber = toOptionalNumber(
|
||||
data.result_number ?? data['result number'],
|
||||
)
|
||||
|
||||
if (typeof serverTime !== 'number') {
|
||||
return null
|
||||
}
|
||||
|
||||
const sourceHits = Array.isArray(data.hits) ? data.hits : [data]
|
||||
const hits = sourceHits
|
||||
.map((item) => extractJackpotHitItem(item))
|
||||
.filter((item): item is JackpotHitItemDto => item !== null)
|
||||
const resultNumber = toOptionalNumber(data.result_number)
|
||||
|
||||
return {
|
||||
hits,
|
||||
period_id:
|
||||
@@ -589,11 +602,11 @@ function applyJackpotHitMessage(message: GameSocketMessage) {
|
||||
if (jackpotHitData?.hits.length) {
|
||||
useGameSessionStore.getState().pushJackpotBroadcasts(
|
||||
jackpotHitData.hits.map((hit) => ({
|
||||
id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.user_id}:${hit.total_win}`,
|
||||
message: `恭喜${hit.user_id} 用户中奖,获得${hit.total_win}`,
|
||||
id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.nickname}:${hit.total_win}`,
|
||||
message: `恭喜${hit.nickname} 用户中奖,获得${hit.total_win}`,
|
||||
nickname: hit.nickname,
|
||||
periodNo: hit.period_no,
|
||||
totalWin: hit.total_win,
|
||||
userId: hit.user_id,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@ function resolveSignalPresentation(input: {
|
||||
|
||||
export function useHeaderVm() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const [clockNow, setClockNow] = useState(() => Date.now())
|
||||
const [isOnline, setIsOnline] = useState(() =>
|
||||
typeof navigator === 'undefined' ? true : navigator.onLine,
|
||||
)
|
||||
@@ -128,31 +127,6 @@ export function useHeaderVm() {
|
||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||
const { currentLanguageLabel, currentLanguageOption } = useAppLanguage()
|
||||
|
||||
const serverClockOffsetMs = useMemo(() => {
|
||||
if (
|
||||
connection.status !== 'connected' ||
|
||||
connection.transport !== 'websocket' ||
|
||||
!connection.lastMessageAt
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const serverTimestamp = Date.parse(connection.lastMessageAt)
|
||||
|
||||
if (Number.isNaN(serverTimestamp)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return serverTimestamp - Date.now()
|
||||
}, [connection.lastMessageAt, connection.status, connection.transport])
|
||||
|
||||
const systemTimeLabel = useMemo(() => {
|
||||
const activeTimestamp =
|
||||
serverClockOffsetMs === null ? clockNow : clockNow + serverClockOffsetMs
|
||||
|
||||
return formatHeaderTime(new Date(activeTimestamp))
|
||||
}, [clockNow, serverClockOffsetMs])
|
||||
|
||||
const signalLatencyMs = useMemo(() => {
|
||||
if (
|
||||
typeof connection.latencyMs === 'number' &&
|
||||
@@ -184,16 +158,6 @@ export function useHeaderVm() {
|
||||
return subscribeDesktopFullscreenChange(syncFullscreenState)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
setClockNow(Date.now())
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const syncBrowserNetworkState = () => {
|
||||
setIsOnline(navigator.onLine)
|
||||
@@ -238,7 +202,52 @@ export function useHeaderVm() {
|
||||
onOpenRules: () => setModalOpen('desktopRules', true),
|
||||
onOpenUserInfo: () => setModalOpen('desktopUserInfo', true),
|
||||
signalPresentation,
|
||||
systemTimeLabel,
|
||||
toggleSoundEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
export function useHeaderClockLabel() {
|
||||
const [clockNow, setClockNow] = useState(() => Date.now())
|
||||
const lastMessageAt = useGameSessionStore(
|
||||
(state) => state.connection.lastMessageAt,
|
||||
)
|
||||
const connectionStatus = useGameSessionStore(
|
||||
(state) => state.connection.status,
|
||||
)
|
||||
const connectionTransport = useGameSessionStore(
|
||||
(state) => state.connection.transport,
|
||||
)
|
||||
|
||||
const serverClockOffsetMs = useMemo(() => {
|
||||
if (
|
||||
connectionStatus !== 'connected' ||
|
||||
connectionTransport !== 'websocket' ||
|
||||
!lastMessageAt
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const serverTimestamp = Date.parse(lastMessageAt)
|
||||
|
||||
if (Number.isNaN(serverTimestamp)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return serverTimestamp - Date.now()
|
||||
}, [connectionStatus, connectionTransport, lastMessageAt])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
setClockNow(Date.now())
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const activeTimestamp =
|
||||
serverClockOffsetMs === null ? clockNow : clockNow + serverClockOffsetMs
|
||||
|
||||
return formatHeaderTime(new Date(activeTimestamp))
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ const MAX_JACKPOT_BROADCAST_COUNT = 20
|
||||
export interface JackpotBroadcastItem {
|
||||
id: string
|
||||
message: string
|
||||
nickname: string
|
||||
periodNo: string
|
||||
receivedAt: string
|
||||
totalWin: string
|
||||
userId: number
|
||||
}
|
||||
|
||||
type JackpotBroadcastInput = Omit<JackpotBroadcastItem, 'receivedAt'>
|
||||
|
||||
Reference in New Issue
Block a user