refactor(game): 重构游戏组件和数据结构

- 移除中心模态框的背景模糊效果并调整透明度
- 为桌面游戏历史组件添加空状态显示组件
- 重构头部时钟显示逻辑,提取为独立组件并优化时间同步
- 移除用户ID遮罩功能,直接使用昵称显示中奖信息
- 调整入口页面的模态框渲染结构和认证状态检查逻辑
- 更新奖池广播数据结构,替换用户ID为昵称字段
- 优化实时同步中的数据验证和映射逻辑
- 调整移动端头部时钟组件的实现方式
This commit is contained in:
JiaJun
2026-06-02 10:02:08 +08:00
parent 72b6de499e
commit 522b8a1f28
12 changed files with 180 additions and 121 deletions

View File

@@ -71,7 +71,7 @@ export function CenterModal({
return createPortal( return createPortal(
<div <div
className={cn( 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, backdropClassName,
)} )}
> >

View File

@@ -214,10 +214,10 @@ export interface GamePeriodTickDto {
} }
export interface JackpotHitItemDto { export interface JackpotHitItemDto {
nickname: string
period_no: string period_no: string
result_number: number result_number: number
total_win: string total_win: string
user_id: number
} }
export interface JackpotHitEventDataDto { export interface JackpotHitEventDataDto {

View File

@@ -1,3 +1,4 @@
import { History } from 'lucide-react'
import { useCallback, useRef } from 'react' import { useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import historyBg from '@/assets/system/history-bg.png' 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() { export function DesktopGameHistory() {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
@@ -99,13 +119,7 @@ export function DesktopGameHistory() {
className="min-h-full flex-1" className="min-h-full flex-1"
/> />
) : isEmpty ? ( ) : isEmpty ? (
<div <HistoryEmptyState label={emptyText} />
className={
'flex w-full flex-1 items-center justify-center text-design-18 text-[#84A2A2]'
}
>
{emptyText}
</div>
) : ( ) : (
<> <>
{items.map((item) => { {items.map((item) => {

View File

@@ -14,7 +14,16 @@ import avatar from '@/assets/system/avatar.webp'
import diamond from '@/assets/system/diamond.webp' import diamond from '@/assets/system/diamond.webp'
import logo from '@/assets/system/logo.webp' import logo from '@/assets/system/logo.webp'
import { SmartImage } from '@/components/smart-image.tsx' 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({ function SignalBars({
activeBars, activeBars,
@@ -66,12 +75,11 @@ export function DesktopHeader() {
onOpenRules, onOpenRules,
onOpenUserInfo, onOpenUserInfo,
signalPresentation, signalPresentation,
systemTimeLabel,
toggleSoundEnabled, toggleSoundEnabled,
} = useHeaderVm() } = useHeaderVm()
return ( 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-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"> <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 <SmartImage
@@ -99,7 +107,7 @@ export function DesktopHeader() {
<div className={'text-[#B4E4E9]'}> <div className={'text-[#B4E4E9]'}>
{t('gameDesktop.header.systemTime')} {t('gameDesktop.header.systemTime')}
</div> </div>
<div className={'text-[#D2FCFF] font-bold'}>{systemTimeLabel}</div> <HeaderClock />
</div> </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"> <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">

View File

@@ -6,20 +6,6 @@ const winAmountFormatter = new Intl.NumberFormat('en-US', {
maximumFractionDigits: 6, 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) { function formatWinAmount(value: string) {
const amount = Number(value) const amount = Number(value)
@@ -34,9 +20,9 @@ export function DesktopTitle() {
jackpotBroadcasts.length > 0 jackpotBroadcasts.length > 0
? jackpotBroadcasts.map((broadcast) => ({ ? jackpotBroadcasts.map((broadcast) => ({
id: broadcast.id, id: broadcast.id,
message: `Player ${maskParticipantLabel( message: `Player ${broadcast.nickname} won ${formatWinAmount(
broadcast.userId, broadcast.totalWin,
)} won ${formatWinAmount(broadcast.totalWin)}`, )}`,
})) }))
: [{ id: 'empty', message: '' }] : [{ id: 'empty', message: '' }]
const marqueeTitles = const marqueeTitles =

View File

@@ -12,7 +12,20 @@ import avatar from '@/assets/system/avatar.webp'
import diamond from '@/assets/system/diamond.webp' import diamond from '@/assets/system/diamond.webp'
import logo from '@/assets/system/logo.webp' import logo from '@/assets/system/logo.webp'
import { SmartImage } from '@/components/smart-image.tsx' 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() { export function MobileHeader() {
const { t } = useTranslation() const { t } = useTranslation()
@@ -30,7 +43,6 @@ export function MobileHeader() {
onOpenRules, onOpenRules,
onOpenUserInfo, onOpenUserInfo,
signalPresentation, signalPresentation,
systemTimeLabel,
toggleSoundEnabled, toggleSoundEnabled,
} = useHeaderVm() } = useHeaderVm()
@@ -155,9 +167,7 @@ export function MobileHeader() {
<div className="text-[#B4E4E9]"> <div className="text-[#B4E4E9]">
{t('gameDesktop.header.systemTime')} {t('gameDesktop.header.systemTime')}
</div> </div>
<div className="mt-design-3 whitespace-nowrap text-design-7 font-bold text-[#D2FCFF]"> <MobileHeaderClock />
{systemTimeLabel}
</div>
</div> </div>
<button <button

View File

@@ -69,8 +69,8 @@ export function EntryNoticeGateModal() {
setHasEntered(false) setHasEntered(false)
setHasAgreed(false) setHasAgreed(false)
if (!hasStoredLoginInfo) { if (!hasStoredLoginInfo || shouldSuppressEntryGate) {
setShouldGateEntry(!shouldSuppressEntryGate) setShouldGateEntry(false)
return return
} }

View File

@@ -2,14 +2,54 @@ import { startTransition, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getGameLobbyInit } from '@/features/game' import { getGameLobbyInit } from '@/features/game'
import { EntryNoticeGateModal } from '@/features/game/components'
import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx' import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx'
import { PcEntry } from '@/features/game/entry/pc-entry.tsx' import { PcEntry } from '@/features/game/entry/pc-entry.tsx'
import { useGameRealtimeSync } from '@/features/game/hooks/use-game-realtime-sync.ts' 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 { useDocumentMetadata } from '@/lib/head/document-metadata'
import { notify } from '@/lib/notify' import { notify } from '@/lib/notify'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
import { useGameRoundStore, useGameSessionStore } from '@/store/game' import { useGameRoundStore, useGameSessionStore } from '@/store/game'
function EntryModalHost() {
return (
<>
{/* 桌面端登录弹窗:用于未登录用户进入登录流程 */}
<DesktopLoginModal />
{/* 桌面端注册弹窗:用于新用户注册账号 */}
<DesktopRegisterModal />
{/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */}
<DesktopLanguageModal />
{/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */}
<DesktopRulesModal />
{/* 桌面端用户信息弹窗:展示个人资料与站内消息 */}
<DesktopUserInfoModal />
{/* 桌面端公告弹窗:展示活动公告或运营通知内容 */}
<DesktopNoticeModal />
{/* 桌面端自动托管弹窗:配置自动托管相关条件 */}
<DesktopAutoSettingModal />
{/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */}
<DesktopProceduresModal />
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
<DesktopWithdrawTopupModal />
{/* 强制弹窗 */}
<EntryNoticeGateModal />
{/* 历史开奖信息弹窗 */}
<DesktopPeriodHistoryDrawer />
</>
)
}
export function EntryPage() { export function EntryPage() {
const { t } = useTranslation() const { t } = useTranslation()
useGameRealtimeSync() useGameRealtimeSync()
@@ -19,6 +59,8 @@ export function EntryPage() {
const syncConnection = useGameSessionStore((state) => state.syncConnection) const syncConnection = useGameSessionStore((state) => state.syncConnection)
const setCurrentUser = useAuthStore((state) => state.setCurrentUser) const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
const authStatus = useAuthStore((state) => state.status) 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 lastUnauthorizedAt = useAuthStore((state) => state.lastUnauthorizedAt)
const isReloginRequired = const isReloginRequired =
authStatus === 'anonymous' && Boolean(lastUnauthorizedAt) authStatus === 'anonymous' && Boolean(lastUnauthorizedAt)
@@ -38,7 +80,13 @@ export function EntryPage() {
}) })
useEffect(() => { useEffect(() => {
if (isReloginRequired) { if (!authIsHydrated) {
setIsHydrating(true)
return
}
if (isReloginRequired || authStatus !== 'authenticated' || !accessToken) {
setIsHydrating(false) setIsHydrating(false)
return return
@@ -121,6 +169,8 @@ export function EntryPage() {
cancelled = true cancelled = true
} }
}, [ }, [
accessToken,
authIsHydrated,
authStatus, authStatus,
hydrateRound, hydrateRound,
hydrateSession, hydrateSession,
@@ -156,6 +206,7 @@ export function EntryPage() {
className="flex min-h-0 flex-1 flex-col" className="flex min-h-0 flex-1 flex-col"
> >
{isMobile ? <MobileEntry /> : <PcEntry />} {isMobile ? <MobileEntry /> : <PcEntry />}
<EntryModalHost />
</section> </section>
) )
} }

View File

@@ -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 { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx'
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx' import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx' import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx' import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts' 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() { export function PcEntry() {
useAutoHostingRunner() useAutoHostingRunner()
@@ -50,28 +40,6 @@ export function PcEntry() {
> >
<DesktopControl /> <DesktopControl />
</div> </div>
{/* 桌面端登录弹窗:用于未登录用户进入登录流程 */}
<DesktopLoginModal />
{/* 桌面端注册弹窗:用于新用户注册账号 */}
<DesktopRegisterModal />
{/* 桌面端语言切换弹窗:用于选择当前站点展示语言 */}
<DesktopLanguageModal />
{/* 桌面端规则弹窗:展示当前游戏玩法、下注与结算规则 */}
<DesktopRulesModal />
{/* 桌面端用户信息弹窗:展示个人资料与站内消息 */}
<DesktopUserInfoModal />
{/* 桌面端公告弹窗:展示活动公告或运营通知内容 */}
<DesktopNoticeModal />
{/* 桌面端自动托管弹窗:配置自动托管相关条件 */}
<DesktopAutoSettingModal />
{/* 桌面端充值/提现前置选择弹窗:先选择进入充值还是提现 */}
<DesktopProceduresModal />
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
<DesktopWithdrawTopupModal />
{/* 强制弹窗 */}
<EntryNoticeGateModal />
{/* 历史开奖信息弹窗 */}
<DesktopPeriodHistoryDrawer />
</> </>
) )
} }

View File

@@ -256,7 +256,7 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
const source = value as Record<string, unknown> const source = value as Record<string, unknown>
if ( if (
typeof source.user_id !== 'number' || typeof source.nickname !== 'string' ||
typeof source.period_no !== 'string' || typeof source.period_no !== 'string' ||
typeof source.total_win !== 'string' typeof source.total_win !== 'string'
) { ) {
@@ -270,10 +270,10 @@ function extractJackpotHitItem(value: unknown): JackpotHitItemDto | null {
} }
return { return {
nickname: source.nickname,
period_no: source.period_no, period_no: source.period_no,
result_number: resultNumber, result_number: resultNumber,
total_win: source.total_win, total_win: source.total_win,
user_id: source.user_id,
} }
} }
@@ -286,18 +286,31 @@ function extractJackpotHitData(
return null 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') { if (typeof serverTime !== 'number') {
return null 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 { return {
hits, hits,
period_id: period_id:
@@ -589,11 +602,11 @@ function applyJackpotHitMessage(message: GameSocketMessage) {
if (jackpotHitData?.hits.length) { if (jackpotHitData?.hits.length) {
useGameSessionStore.getState().pushJackpotBroadcasts( useGameSessionStore.getState().pushJackpotBroadcasts(
jackpotHitData.hits.map((hit) => ({ jackpotHitData.hits.map((hit) => ({
id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.user_id}:${hit.total_win}`, id: `${jackpotHitData.period_no}:${hit.result_number}:${hit.nickname}:${hit.total_win}`,
message: `恭喜${hit.user_id} 用户中奖,获得${hit.total_win}`, message: `恭喜${hit.nickname} 用户中奖,获得${hit.total_win}`,
nickname: hit.nickname,
periodNo: hit.period_no, periodNo: hit.period_no,
totalWin: hit.total_win, totalWin: hit.total_win,
userId: hit.user_id,
})), })),
) )
} }

View File

@@ -107,7 +107,6 @@ function resolveSignalPresentation(input: {
export function useHeaderVm() { export function useHeaderVm() {
const [isFullscreen, setIsFullscreen] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false)
const [clockNow, setClockNow] = useState(() => Date.now())
const [isOnline, setIsOnline] = useState(() => const [isOnline, setIsOnline] = useState(() =>
typeof navigator === 'undefined' ? true : navigator.onLine, typeof navigator === 'undefined' ? true : navigator.onLine,
) )
@@ -128,31 +127,6 @@ export function useHeaderVm() {
const setModalOpen = useModalStore((state) => state.setModalOpen) const setModalOpen = useModalStore((state) => state.setModalOpen)
const { currentLanguageLabel, currentLanguageOption } = useAppLanguage() 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(() => { const signalLatencyMs = useMemo(() => {
if ( if (
typeof connection.latencyMs === 'number' && typeof connection.latencyMs === 'number' &&
@@ -184,16 +158,6 @@ export function useHeaderVm() {
return subscribeDesktopFullscreenChange(syncFullscreenState) return subscribeDesktopFullscreenChange(syncFullscreenState)
}, []) }, [])
useEffect(() => {
const timer = window.setInterval(() => {
setClockNow(Date.now())
}, 1000)
return () => {
window.clearInterval(timer)
}
}, [])
useEffect(() => { useEffect(() => {
const syncBrowserNetworkState = () => { const syncBrowserNetworkState = () => {
setIsOnline(navigator.onLine) setIsOnline(navigator.onLine)
@@ -238,7 +202,52 @@ export function useHeaderVm() {
onOpenRules: () => setModalOpen('desktopRules', true), onOpenRules: () => setModalOpen('desktopRules', true),
onOpenUserInfo: () => setModalOpen('desktopUserInfo', true), onOpenUserInfo: () => setModalOpen('desktopUserInfo', true),
signalPresentation, signalPresentation,
systemTimeLabel,
toggleSoundEnabled, 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))
}

View File

@@ -23,10 +23,10 @@ const MAX_JACKPOT_BROADCAST_COUNT = 20
export interface JackpotBroadcastItem { export interface JackpotBroadcastItem {
id: string id: string
message: string message: string
nickname: string
periodNo: string periodNo: string
receivedAt: string receivedAt: string
totalWin: string totalWin: string
userId: number
} }
type JackpotBroadcastInput = Omit<JackpotBroadcastItem, 'receivedAt'> type JackpotBroadcastInput = Omit<JackpotBroadcastItem, 'receivedAt'>