diff options
| author | nsfisis <nsfisis@gmail.com> | 2024-08-10 13:31:52 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2024-08-10 19:55:50 +0900 |
| commit | 9477788709127ffd5611caed0fa7ee191326ce00 (patch) | |
| tree | 8053dfed468d525ce471eabdff2471a325f4ce97 | |
| parent | 47f95342097e8828059053c168e06cd44e0ab43b (diff) | |
| download | iosdc-japan-2024-albatross-9477788709127ffd5611caed0fa7ee191326ce00.tar.gz iosdc-japan-2024-albatross-9477788709127ffd5611caed0fa7ee191326ce00.tar.zst iosdc-japan-2024-albatross-9477788709127ffd5611caed0fa7ee191326ce00.zip | |
feat(frontend): partially implement watch page
4 files changed, 171 insertions, 46 deletions
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx index 829f709..355f7e3 100644 --- a/frontend/app/components/GolfWatchApp.client.tsx +++ b/frontend/app/components/GolfWatchApp.client.tsx @@ -3,7 +3,9 @@ import useWebSocket, { ReadyState } from "react-use-websocket"; import type { components } from "../.server/api/schema"; import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting"; import GolfWatchAppFinished from "./GolfWatchApps/GolfWatchAppFinished"; -import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming"; +import GolfWatchAppGaming, { + PlayerInfo, +} from "./GolfWatchApps/GolfWatchAppGaming"; import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting"; import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting"; @@ -34,12 +36,12 @@ export default function GolfWatchApp({ const [startedAt, setStartedAt] = useState<number | null>(null); - const [timeLeftSeconds, setTimeLeftSeconds] = useState<number | null>(null); + const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null); useEffect(() => { if (gameState === "starting" && startedAt !== null) { const timer1 = setInterval(() => { - setTimeLeftSeconds((prev) => { + setLeftTimeSeconds((prev) => { if (prev === null) { return null; } @@ -68,10 +70,23 @@ export default function GolfWatchApp({ } }, [gameState, startedAt, game.duration_seconds]); - const [scoreA, setScoreA] = useState<number | null>(null); - const [scoreB, setScoreB] = useState<number | null>(null); - const [codeA, setCodeA] = useState<string>(""); - const [codeB, setCodeB] = useState<string>(""); + const playerA = game.players[0]; + const playerB = game.players[1]; + + const [playerInfoA, setPlayerInfoA] = useState<PlayerInfo>({ + displayName: playerA?.display_name ?? null, + iconPath: playerA?.icon_path ?? null, + score: null, + code: "", + submissionResult: undefined, + }); + const [playerInfoB, setPlayerInfoB] = useState<PlayerInfo>({ + displayName: playerB?.display_name ?? null, + iconPath: playerB?.icon_path ?? null, + score: null, + code: "", + submissionResult: undefined, + }); if (readyState === ReadyState.UNINSTANTIATED) { throw new Error("WebSocket is not connected"); @@ -96,38 +111,51 @@ export default function GolfWatchApp({ const { start_at } = lastJsonMessage.data; setStartedAt(start_at); const nowSec = Math.floor(Date.now() / 1000); - setTimeLeftSeconds(start_at - nowSec); + setLeftTimeSeconds(start_at - nowSec); setGameState("starting"); } } else if (lastJsonMessage.type === "watcher:s2c:code") { const { player_id, code } = lastJsonMessage.data; - setCodeA(code); - } else if (lastJsonMessage.type === "watcher:s2c:execresult") { - const { score } = lastJsonMessage.data; - if (score !== null && (scoreA === null || score < scoreA)) { - setScoreA(score); + if (player_id === playerA?.user_id) { + setPlayerInfoA((prev) => ({ ...prev, code })); + } else if (player_id === playerB?.user_id) { + setPlayerInfoB((prev) => ({ ...prev, code })); + } else { + throw new Error("Unknown player_id"); } + } else if (lastJsonMessage.type === "watcher:s2c:execresult") { + // const { score } = lastJsonMessage.data; + // if (score !== null && (scoreA === null || score < scoreA)) { + // setScoreA(score); + // } } } else { setGameState("waiting"); } } - }, [lastJsonMessage, readyState, gameState, scoreA]); + }, [ + lastJsonMessage, + readyState, + gameState, + playerInfoA, + playerInfoB, + playerA?.user_id, + playerB?.user_id, + ]); if (gameState === "connecting") { return <GolfWatchAppConnecting />; } else if (gameState === "waiting") { return <GolfWatchAppWaiting />; } else if (gameState === "starting") { - return <GolfWatchAppStarting timeLeft={timeLeftSeconds!} />; + return <GolfWatchAppStarting leftTimeSeconds={leftTimeSeconds!} />; } else if (gameState === "gaming") { return ( <GolfWatchAppGaming problem={game.problem!.description} - codeA={codeA} - scoreA={scoreA} - codeB={codeB} - scoreB={scoreB} + playerInfoA={playerInfoA} + playerInfoB={playerInfoB} + leftTimeSeconds={leftTimeSeconds!} /> ); } else if (gameState === "finished") { diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx index 22277f8..470a00c 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx @@ -1,41 +1,130 @@ type Props = { problem: string; - codeA: string; - scoreA: number | null; - codeB: string; - scoreB: number | null; + playerInfoA: PlayerInfo; + playerInfoB: PlayerInfo; + leftTimeSeconds: number; +}; + +export type PlayerInfo = { + displayName: string | null; + iconPath: string | null; + score: number | null; + code: string | null; + submissionResult?: SubmissionResult; +}; + +type SubmissionResult = { + status: string; + nextScore: number; + executionResults: ExecutionResult[]; +}; + +type ExecutionResult = { + status: string; + label: string; + output: string; }; export default function GolfWatchAppGaming({ problem, - codeA, - scoreA, - codeB, - scoreB, + playerInfoA, + playerInfoB, + leftTimeSeconds, }: Props) { + const leftTime = (() => { + const m = Math.floor(leftTimeSeconds / 60); + const s = leftTimeSeconds % 60; + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + })(); + const scoreRatio = (() => { + const scoreA = playerInfoA.score ?? 0; + const scoreB = playerInfoB.score ?? 0; + const totalScore = scoreA + scoreB; + return totalScore === 0 ? 50 : (scoreA / totalScore) * 100; + })(); + return ( - <div style={{ display: "flex", flexDirection: "column" }}> - <div style={{ display: "flex", flex: 1, justifyContent: "center" }}> - <div>{problem}</div> + <div className="grid h-full w-full grid-rows-[auto_auto_1fr_auto]"> + <div className="grid grid-cols-[1fr_auto_1fr]"> + <div className="grid justify-start bg-red-500 p-2 text-white"> + {playerInfoA.displayName} + </div> + <div className="grid justify-center p-2">{leftTime}</div> + <div className="grid justify-end bg-blue-500 p-2 text-white"> + {playerInfoB.displayName} + </div> </div> - <div style={{ display: "flex", flex: 3 }}> - <div style={{ display: "flex", flex: 3, flexDirection: "column" }}> - <div style={{ flex: 1, justifyContent: "center" }}>{scoreA}</div> - <div style={{ flex: 3 }}> - <pre> - <code>{codeA}</code> - </pre> + <div className="grid grid-cols-[auto_1fr_auto]"> + <div className="grid justify-start bg-red-500 p-2 text-lg font-bold text-white"> + {playerInfoA.score ?? "-"} + </div> + <div className="w-full bg-blue-500"> + <div + className="h-full bg-red-500" + style={{ width: `${scoreRatio}%` }} + ></div> + </div> + <div className="grid justify-end bg-blue-500 p-2 text-lg font-bold text-white"> + {playerInfoB.score ?? "-"} + </div> + </div> + <div className="grid grid-cols-[2fr_1fr_2fr_1fr] p-2"> + <div> + <pre> + <code>{playerInfoA.code}</code> + </pre> + </div> + <div> + <div> + {playerInfoA.submissionResult?.status}( + {playerInfoA.submissionResult?.nextScore}) + </div> + <div> + <ol> + {playerInfoA.submissionResult?.executionResults.map( + (result, idx) => ( + <li key={idx}> + <div> + <div> + {result.status} {result.label} + </div> + <div>{result.output}</div> + </div> + </li> + ), + )} + </ol> </div> </div> - <div style={{ display: "flex", flex: 3, flexDirection: "column" }}> - <div style={{ flex: 1, justifyContent: "center" }}>{scoreB}</div> - <div style={{ flex: 3 }}> - <pre> - <code>{codeB}</code> - </pre> + <div> + <pre> + <code>{playerInfoB.code}</code> + </pre> + </div> + <div> + <div> + {playerInfoB.submissionResult?.status}( + {playerInfoB.submissionResult?.nextScore}) + </div> + <div> + <ol> + {playerInfoB.submissionResult?.executionResults.map( + (result, idx) => ( + <li key={idx}> + <div> + <div> + {result.status} {result.label} + </div> + <div>{result.output}</div> + </div> + </li> + ), + )} + </ol> </div> </div> </div> + <div className="grid justify-center p-2 bg-slate-300">{problem}</div> </div> ); } diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx index ef72cec..8282fb4 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx @@ -1,8 +1,10 @@ type Props = { - timeLeft: number; + leftTimeSeconds: number; }; -export default function GolfWatchAppStarting({ timeLeft }: Props) { +export default function GolfWatchAppStarting({ + leftTimeSeconds: timeLeft, +}: Props) { return ( <div className="min-h-screen bg-gray-100 flex items-center justify-center"> <div className="text-center"> diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx index d58ec19..17ef2b9 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx @@ -1,3 +1,9 @@ export default function GolfWatchAppWaiting() { - return <div>Waiting...</div>; + return ( + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <div className="text-center"> + <h1 className="text-4xl font-bold text-black-600 mb-4">Waiting...</h1> + </div> + </div> + ); } |
