Files
badminton-scoreboard/src/pages/RoomSpectatorPage.tsx

223 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { loadLiveRoom, subscribeLiveRoom } from '../lib/api'
import type { LiveRoomDetail } from '../types'
type RoomSpectatorPageProps = {
onConfirmFinished: () => void
}
const ROOM_POLL_MS = 1500
type RoomClosedDialog = {
message: string
title: string
} | null
export function RoomSpectatorPage({ onConfirmFinished }: RoomSpectatorPageProps) {
const { roomId = '' } = useParams()
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [room, setRoom] = useState<LiveRoomDetail | null>(null)
const [showFinishedDialog, setShowFinishedDialog] = useState(false)
const [roomClosedDialog, setRoomClosedDialog] = useState<RoomClosedDialog>(null)
const previousStatusRef = useRef<string | null>(null)
const hasRoomRef = useRef(false)
useEffect(() => {
if (!roomId) {
return
}
let active = true
hasRoomRef.current = false
previousStatusRef.current = null
const applyRoomUpdate = (nextRoom: LiveRoomDetail) => {
if (!active) {
return
}
if (nextRoom.status === 'finished' && previousStatusRef.current !== 'finished') {
setShowFinishedDialog(true)
}
previousStatusRef.current = nextRoom.status
hasRoomRef.current = true
setRoom(nextRoom)
setError('')
setLoading(false)
}
const load = async (showLoadError = true) => {
try {
const nextRoom = await loadLiveRoom(roomId)
applyRoomUpdate(nextRoom)
} catch (loadError) {
if (!active) {
return
}
if (showLoadError || !hasRoomRef.current) {
setError(loadError instanceof Error ? loadError.message : '載入觀戰房間失敗。')
setLoading(false)
}
}
}
void load()
const unsubscribe = subscribeLiveRoom(
roomId,
(nextRoom) => {
applyRoomUpdate(nextRoom)
},
() => {
if (!active) {
return
}
if (!hasRoomRef.current) {
setError('觀戰連線中斷,請稍後重試。')
setLoading(false)
}
},
(payload) => {
if (!active) {
return
}
if (payload.status === 'released') {
setRoomClosedDialog({
title: '房間已關閉',
message: '房主已重整頁面、離開記分板或重新選隊伍,本房間已結束觀戰。',
})
setLoading(false)
}
},
)
const timer = window.setInterval(() => {
void load(false)
}, ROOM_POLL_MS)
return () => {
active = false
window.clearInterval(timer)
unsubscribe()
}
}, [roomId])
if (loading) {
return (
<section className="page-grid">
<article className="panel full-span">
<p>...</p>
</article>
</section>
)
}
if (!room && error) {
return (
<section className="page-grid">
<article className="panel panel-hero full-span">
<p className="panel-kicker">Spectator</p>
<h2></h2>
<p className="panel-copy">{error}</p>
<Link className="primary-button inline-link" to="/rooms">
</Link>
</article>
</section>
)
}
return (
<>
<section className="page-grid">
<article className="panel panel-hero full-span">
<p className="panel-kicker">Spectator</p>
<h2> {room?.roomId ?? roomId}</h2>
<p className="panel-copy"></p>
</article>
{room ? (
<article className="panel full-span room-watch-panel">
<div className="room-watch-scoreboard">
<div className="room-watch-team">
<small>{room.leftTeamName}</small>
<strong>{room.scoreState.scoreLeft}</strong>
</div>
<div className="room-watch-divider">:</div>
<div className="room-watch-team">
<small>{room.rightTeamName}</small>
<strong>{room.scoreState.scoreRight}</strong>
</div>
</div>
<div className="room-watch-meta">
<span> {room.scoreState.targetScore}</span>
<span> {room.status === 'finished' ? '已結束' : '進行中'}</span>
<span>
{new Date(room.updatedAt).toLocaleTimeString('zh-TW', { hour12: false })}
</span>
</div>
</article>
) : (
<article className="panel full-span">
<p className="history-empty"></p>
</article>
)}
</section>
{showFinishedDialog && room ? (
<div className="finish-dialog-overlay" role="presentation">
<div aria-modal="true" className="finish-dialog" role="dialog">
<p className="panel-kicker"></p>
<h3>{room.winnerTeamName ?? '已有獲勝隊伍'}</h3>
<p className="finish-dialog-copy">
{room.winnerTeamName
? `${room.winnerTeamName} 已獲勝,按下確定後返回房間列表。`
: '比賽已結束,按下確定後返回房間列表。'}
</p>
<div className="finish-dialog-actions">
<button
className="team-picker-confirm"
type="button"
onClick={() => {
setShowFinishedDialog(false)
onConfirmFinished()
}}
>
</button>
</div>
</div>
</div>
) : null}
{roomClosedDialog ? (
<div className="finish-dialog-overlay" role="presentation">
<div aria-modal="true" className="finish-dialog" role="dialog">
<p className="panel-kicker"></p>
<h3>{roomClosedDialog.title}</h3>
<p className="finish-dialog-copy">{roomClosedDialog.message}</p>
<div className="finish-dialog-actions">
<button
className="team-picker-confirm"
type="button"
onClick={() => {
setRoomClosedDialog(null)
onConfirmFinished()
}}
>
</button>
</div>
</div>
</div>
) : null}
</>
)
}