diff options
Diffstat (limited to 'frontend/app/components')
15 files changed, 354 insertions, 1066 deletions
diff --git a/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx index b611c5d..a717a48 100644 --- a/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx +++ b/frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx @@ -6,7 +6,7 @@ import { faRotate, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import type { ExecResultStatus } from "../../models/ExecResult"; +import type { ExecResultStatus } from "../../types/ExecResult"; type Props = { status: ExecResultStatus; diff --git a/frontend/app/components/Gaming/SubmitResult.tsx b/frontend/app/components/Gaming/SubmitResult.tsx index 93e08a7..c626910 100644 --- a/frontend/app/components/Gaming/SubmitResult.tsx +++ b/frontend/app/components/Gaming/SubmitResult.tsx @@ -1,5 +1,5 @@ import React from "react"; -import type { SubmitResult } from "../../models/SubmitResult"; +import type { SubmitResult } from "../../types/SubmitResult"; import BorderedContainer from "../BorderedContainer"; import SubmitStatusLabel from "../SubmitStatusLabel"; import ExecStatusIndicatorIcon from "./ExecStatusIndicatorIcon"; diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx index 42f0250..0230426 100644 --- a/frontend/app/components/GolfPlayApp.client.tsx +++ b/frontend/app/components/GolfPlayApp.client.tsx @@ -1,8 +1,20 @@ -import { useEffect, useState } from "react"; +import { useAtomValue, useSetAtom } from "jotai"; +import { useEffect } from "react"; +import { useTimer } from "react-use-precision-timer"; import { useDebouncedCallback } from "use-debounce"; import type { components } from "../.server/api/schema"; import useWebSocket, { ReadyState } from "../hooks/useWebSocket"; -import type { PlayerInfo } from "../models/PlayerInfo"; +import { + gameStartAtom, + gameStateKindAtom, + handleSubmitCodeAtom, + handleWsConnectionClosedAtom, + handleWsExecResultMessageAtom, + handleWsSubmitResultMessageAtom, + setCurrentTimestampAtom, + setGameStateConnectingAtom, + setGameStateWaitingAtom, +} from "../states/play"; import GolfPlayAppConnecting from "./GolfPlayApps/GolfPlayAppConnecting"; import GolfPlayAppFinished from "./GolfPlayApps/GolfPlayAppFinished"; import GolfPlayAppGaming from "./GolfPlayApps/GolfPlayAppGaming"; @@ -15,78 +27,47 @@ type GamePlayerMessageC2S = components["schemas"]["GamePlayerMessageC2S"]; type Game = components["schemas"]["Game"]; type User = components["schemas"]["User"]; -type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished"; +type Props = { + game: Game; + player: User; + initialCode: string; + sockToken: string; +}; export default function GolfPlayApp({ game, player, + initialCode, sockToken, -}: { - game: Game; - player: User; - sockToken: string; -}) { +}: Props) { const socketUrl = process.env.NODE_ENV === "development" ? `ws://localhost:8002/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/play?token=${sockToken}` : `wss://t.nil.ninja/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/play?token=${sockToken}`; + const gameStateKind = useAtomValue(gameStateKindAtom); + const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom); + const gameStart = useSetAtom(gameStartAtom); + const setGameStateConnecting = useSetAtom(setGameStateConnectingAtom); + const setGameStateWaiting = useSetAtom(setGameStateWaitingAtom); + const handleWsConnectionClosed = useSetAtom(handleWsConnectionClosedAtom); + const handleWsExecResultMessage = useSetAtom(handleWsExecResultMessageAtom); + const handleWsSubmitResultMessage = useSetAtom( + handleWsSubmitResultMessageAtom, + ); + const handleSubmitCode = useSetAtom(handleSubmitCodeAtom); + + useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp); + const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket< GamePlayerMessageS2C, GamePlayerMessageC2S >(socketUrl); - const [gameState, setGameState] = useState<GameState>("connecting"); - - const [startedAt, setStartedAt] = useState<number | null>(null); - - const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null); - - useEffect(() => { - if ( - (gameState === "starting" || gameState === "gaming") && - startedAt !== null - ) { - const timer = setInterval(() => { - setLeftTimeSeconds((prev) => { - if (prev === null) { - return null; - } - if (prev <= 1) { - const nowSec = Math.floor(Date.now() / 1000); - const finishedAt = startedAt + game.duration_seconds; - if (nowSec >= finishedAt) { - clearInterval(timer); - setGameState("finished"); - } else { - setGameState("gaming"); - } - } - return prev - 1; - }); - }, 1000); - - return () => { - clearInterval(timer); - }; - } - }, [gameState, startedAt, game.duration_seconds]); - - const [playerInfo, setPlayerInfo] = useState<Omit<PlayerInfo, "code">>({ + const playerProfile = { displayName: player.display_name, iconPath: player.icon_path ?? null, - score: null, - submitResult: { - status: "waiting_submission", - execResults: game.exec_steps.map((r) => ({ - testcase_id: r.testcase_id, - status: "waiting_submission", - label: r.label, - stdout: "", - stderr: "", - })), - }, - }); + }; const onCodeChange = useDebouncedCallback((code: string) => { console.log("player:c2s:code"); @@ -94,6 +75,8 @@ export default function GolfPlayApp({ type: "player:c2s:code", data: { code }, }); + const baseKey = `playerState:${game.game_id}:${player.user_id}`; + window.localStorage.setItem(`${baseKey}:code`, code); }, 1000); const onCodeSubmit = useDebouncedCallback((code: string) => { @@ -105,18 +88,7 @@ export default function GolfPlayApp({ type: "player:c2s:submit", data: { code }, }); - setPlayerInfo((prev) => ({ - ...prev, - submitResult: { - status: "running", - execResults: prev.submitResult.execResults.map((r) => ({ - ...r, - status: "running", - stdout: "", - stderr: "", - })), - }, - })); + handleSubmitCode(); }, 1000); if (readyState === ReadyState.UNINSTANTIATED) { @@ -125,133 +97,89 @@ export default function GolfPlayApp({ useEffect(() => { if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) { - if (gameState !== "finished") { - setGameState("connecting"); - } + handleWsConnectionClosed(); } else if (readyState === ReadyState.CONNECTING) { - setGameState("connecting"); + setGameStateConnecting(); } else if (readyState === ReadyState.OPEN) { if (lastJsonMessage !== null) { console.log(lastJsonMessage.type); if (lastJsonMessage.type === "player:s2c:start") { - if ( - gameState !== "starting" && - gameState !== "gaming" && - gameState !== "finished" - ) { - const { start_at } = lastJsonMessage.data; - setStartedAt(start_at); - const nowSec = Math.floor(Date.now() / 1000); - setLeftTimeSeconds(start_at - nowSec); - setGameState("starting"); - } + const { start_at } = lastJsonMessage.data; + gameStart(start_at); } else if (lastJsonMessage.type === "player:s2c:execresult") { - const { testcase_id, status, stdout, stderr } = lastJsonMessage.data; - setPlayerInfo((prev) => { - const ret = { ...prev }; - ret.submitResult = { - ...prev.submitResult, - execResults: prev.submitResult.execResults.map((r) => - r.testcase_id === testcase_id && r.status === "running" - ? { - ...r, - status, - stdout, - stderr, - } - : r, - ), - }; - return ret; - }); + handleWsExecResultMessage( + lastJsonMessage.data, + (submissionResult) => { + const baseKey = `playerState:${game.game_id}:${player.user_id}`; + window.localStorage.setItem( + `${baseKey}:submissionResult`, + JSON.stringify(submissionResult), + ); + }, + ); } else if (lastJsonMessage.type === "player:s2c:submitresult") { - const { status, score } = lastJsonMessage.data; - setPlayerInfo((prev) => { - const ret = { ...prev }; - ret.submitResult = { - ...prev.submitResult, - status, - }; - if (status === "success") { - if (score) { - if (ret.score === null || score < ret.score) { - ret.score = score; - } - } - } else { - ret.submitResult.execResults = prev.submitResult.execResults.map( - (r) => - r.status === "running" ? { ...r, status: "canceled" } : r, + handleWsSubmitResultMessage( + lastJsonMessage.data, + (submissionResult, score) => { + const baseKey = `playerState:${game.game_id}:${player.user_id}`; + window.localStorage.setItem( + `${baseKey}:submissionResult`, + JSON.stringify(submissionResult), ); - } - return ret; - }); + window.localStorage.setItem( + `${baseKey}:score`, + score === null ? "" : score.toString(), + ); + }, + ); } } else { if (game.started_at) { - const nowSec = Math.floor(Date.now() / 1000); - if (game.started_at <= nowSec) { - // The game has already started. - if (gameState !== "gaming" && gameState !== "finished") { - setStartedAt(game.started_at); - setLeftTimeSeconds(game.started_at - nowSec); - setGameState("gaming"); - } - } else { - // The game is starting. - if ( - gameState !== "starting" && - gameState !== "gaming" && - gameState !== "finished" - ) { - setStartedAt(game.started_at); - setLeftTimeSeconds(game.started_at - nowSec); - setGameState("starting"); - } - } + gameStart(game.started_at); } else { - setGameState("waiting"); + setGameStateWaiting(); } } } }, [ + game.game_id, game.started_at, + player.user_id, sendJsonMessage, lastJsonMessage, readyState, - gameState, + gameStart, + handleWsConnectionClosed, + handleWsExecResultMessage, + handleWsSubmitResultMessage, + setGameStateConnecting, + setGameStateWaiting, ]); - if (gameState === "connecting") { + if (gameStateKind === "connecting") { return <GolfPlayAppConnecting />; - } else if (gameState === "waiting") { + } else if (gameStateKind === "waiting") { return ( <GolfPlayAppWaiting gameDisplayName={game.display_name} - playerInfo={playerInfo} - /> - ); - } else if (gameState === "starting") { - return ( - <GolfPlayAppStarting - gameDisplayName={game.display_name} - leftTimeSeconds={leftTimeSeconds!} + playerProfile={playerProfile} /> ); - } else if (gameState === "gaming") { + } else if (gameStateKind === "starting") { + return <GolfPlayAppStarting gameDisplayName={game.display_name} />; + } else if (gameStateKind === "gaming") { return ( <GolfPlayAppGaming gameDisplayName={game.display_name} - gameDurationSeconds={game.duration_seconds} - leftTimeSeconds={leftTimeSeconds!} - playerInfo={playerInfo} + playerProfile={playerProfile} problemTitle={game.problem.title} problemDescription={game.problem.description} + initialCode={initialCode} onCodeChange={onCodeChange} onCodeSubmit={onCodeSubmit} /> ); - } else if (gameState === "finished") { + } else if (gameStateKind === "finished") { return <GolfPlayAppFinished />; } else { return null; diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx index c3ef2d4..c218414 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx @@ -2,7 +2,7 @@ export default function GolfPlayAppFinished() { 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">Finished</h1> + <div className="text-6xl font-bold text-black">終了</div> </div> </div> ); diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx index e6cb7e9..0aa6b3d 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx @@ -1,32 +1,40 @@ import { Link } from "@remix-run/react"; +import { useAtomValue } from "jotai"; import React, { useRef } from "react"; import SubmitButton from "../../components/SubmitButton"; -import type { PlayerInfo } from "../../models/PlayerInfo"; +import { + gamingLeftTimeSecondsAtom, + scoreAtom, + submitResultAtom, +} from "../../states/play"; +import type { PlayerProfile } from "../../types/PlayerProfile"; import BorderedContainer from "../BorderedContainer"; import SubmitResult from "../Gaming/SubmitResult"; import UserIcon from "../UserIcon"; type Props = { gameDisplayName: string; - gameDurationSeconds: number; - leftTimeSeconds: number; - playerInfo: Omit<PlayerInfo, "code">; + playerProfile: PlayerProfile; problemTitle: string; problemDescription: string; + initialCode: string; onCodeChange: (code: string) => void; onCodeSubmit: (code: string) => void; }; export default function GolfPlayAppGaming({ gameDisplayName, - gameDurationSeconds, - leftTimeSeconds, - playerInfo, + playerProfile, problemTitle, problemDescription, + initialCode, onCodeChange, onCodeSubmit, }: Props) { + const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!; + const score = useAtomValue(scoreAtom); + const submitResult = useAtomValue(submitResultAtom); + const textareaRef = useRef<HTMLTextAreaElement>(null); const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { @@ -40,9 +48,8 @@ export default function GolfPlayAppGaming({ }; const leftTime = (() => { - const k = gameDurationSeconds + leftTimeSeconds; - const m = Math.floor(k / 60); - const s = k % 60; + const m = Math.floor(leftTimeSeconds / 60); + const s = leftTimeSeconds % 60; return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; })(); @@ -55,15 +62,15 @@ export default function GolfPlayAppGaming({ </div> <Link to={"/dashboard"}> <div className="flex gap-4 my-auto font-bold"> - <div className="text-6xl">{playerInfo.score}</div> + <div className="text-6xl">{score}</div> <div className="text-end"> <div className="text-gray-100">Player 1</div> - <div className="text-2xl">{playerInfo.displayName}</div> + <div className="text-2xl">{playerProfile.displayName}</div> </div> - {playerInfo.iconPath && ( + {playerProfile.iconPath && ( <UserIcon - iconPath={playerInfo.iconPath} - displayName={playerInfo.displayName!} + iconPath={playerProfile.iconPath} + displayName={playerProfile.displayName} className="w-12 h-12 my-auto" /> )} @@ -82,13 +89,14 @@ export default function GolfPlayAppGaming({ <div className="p-4"> <textarea ref={textareaRef} + defaultValue={initialCode} onChange={handleTextChange} className="resize-none h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-gray-400 transition duration-300" - ></textarea> + /> </div> <div className="p-4"> <SubmitResult - result={playerInfo.submitResult} + result={submitResult} submitButton={ <SubmitButton onClick={handleSubmitButtonClick}> 提出 diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx index a42c883..b41dfed 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx @@ -1,18 +1,19 @@ +import { useAtomValue } from "jotai"; +import { startingLeftTimeSecondsAtom } from "../../states/play"; + type Props = { gameDisplayName: string; - leftTimeSeconds: number; }; -export default function GolfPlayAppStarting({ - gameDisplayName, - leftTimeSeconds, -}: Props) { +export default function GolfPlayAppStarting({ gameDisplayName }: Props) { + const leftTimeSeconds = useAtomValue(startingLeftTimeSecondsAtom)!; + return ( <div className="min-h-screen bg-gray-100 flex flex-col"> <div className="text-white bg-iosdc-japan p-10 text-center"> <div className="text-4xl font-bold">{gameDisplayName}</div> </div> - <div className="text-center text-black font-black text-10xl animate-ping"> + <div className="text-center text-black font-black text-10xl"> {leftTimeSeconds} </div> </div> diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx index bbef43e..706dc8f 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx @@ -1,598 +1,23 @@ -import { PlayerInfo } from "../../models/PlayerInfo"; -import PlayerProfile from "../PlayerProfile"; +import type { PlayerProfile } from "../../types/PlayerProfile"; +import PlayerNameAndIcon from "../PlayerNameAndIcon"; type Props = { gameDisplayName: string; - playerInfo: Omit<PlayerInfo, "code">; + playerProfile: PlayerProfile; }; export default function GolfPlayAppWaiting({ gameDisplayName, - playerInfo, + playerProfile, }: Props) { return ( - <> - <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center"> - <div className="text-white bg-iosdc-japan p-10"> - <div className="text-4xl">{gameDisplayName}</div> - </div> - <div className="grow grid mx-auto text-black"> - <PlayerProfile playerInfo={playerInfo} label="You" /> - </div> + <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center"> + <div className="text-white bg-iosdc-japan p-10"> + <div className="text-4xl">{gameDisplayName}</div> </div> - <style> - {` - @keyframes changeHeight { - 0% { height: 20%; } - 50% { height: 100%; } - 100% { height: 20%; } - } - `} - </style> - <div - style={{ - position: "fixed", - bottom: 0, - width: "100%", - display: "flex", - justifyContent: "center", - alignItems: "flex-end", - height: "100px", - margin: "0 2px", - }} - > - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "2.0s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.9s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.8s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.7s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.6s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.5s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.4s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.3s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.2s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.1s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.0s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.9s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.8s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.7s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.6s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.5s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.4s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.3s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.2s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.1s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.5s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.4s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.3s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.2s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.1s", - }} - ></div> - - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.1s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.2s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.3s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.4s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.5s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.1s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.2s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.3s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.4s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.5s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.6s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.7s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.8s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "0.9s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.0s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.1s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.2s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.3s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.4s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.5s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.6s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.7s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.8s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "1.9s", - }} - ></div> - <div - style={{ - width: "2%", - margin: "0 2px", - background: - "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)", - display: "inline-block", - animation: "changeHeight 1s infinite ease-in-out", - animationDelay: "2.0s", - }} - ></div> + <div className="grow grid mx-auto text-black"> + <PlayerNameAndIcon label="You" profile={playerProfile} /> </div> - </> + </div> ); } diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx index d09a4ae..c8b1d53 100644 --- a/frontend/app/components/GolfWatchApp.client.tsx +++ b/frontend/app/components/GolfWatchApp.client.tsx @@ -1,8 +1,20 @@ -import { useEffect, useState } from "react"; -import { AudioController } from "../.client/audio/AudioController"; +import { useAtomValue, useSetAtom } from "jotai"; +import { useCallback, useEffect } from "react"; +import { useTimer } from "react-use-precision-timer"; import type { components } from "../.server/api/schema"; import useWebSocket, { ReadyState } from "../hooks/useWebSocket"; -import type { PlayerInfo } from "../models/PlayerInfo"; +import { + gameStartAtom, + gameStateKindAtom, + handleWsCodeMessageAtom, + handleWsConnectionClosedAtom, + handleWsExecResultMessageAtom, + handleWsSubmitMessageAtom, + handleWsSubmitResultMessageAtom, + setCurrentTimestampAtom, + setGameStateConnectingAtom, + setGameStateWaitingAtom, +} from "../states/watch"; import GolfWatchAppConnecting from "./GolfWatchApps/GolfWatchAppConnecting"; import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming"; import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting"; @@ -13,101 +25,58 @@ type GameWatcherMessageC2S = never; type Game = components["schemas"]["Game"]; -type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished"; - export type Props = { game: Game; sockToken: string; - audioController: AudioController; }; -export default function GolfWatchApp({ - game, - sockToken, - audioController, -}: Props) { +export default function GolfWatchApp({ game, sockToken }: Props) { const socketUrl = process.env.NODE_ENV === "development" ? `ws://localhost:8002/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/watch?token=${sockToken}` : `wss://t.nil.ninja/iosdc-japan/2024/code-battle/sock/golf/${game.game_id}/watch?token=${sockToken}`; + const gameStateKind = useAtomValue(gameStateKindAtom); + const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom); + const gameStart = useSetAtom(gameStartAtom); + const setGameStateConnecting = useSetAtom(setGameStateConnectingAtom); + const setGameStateWaiting = useSetAtom(setGameStateWaitingAtom); + const handleWsConnectionClosed = useSetAtom(handleWsConnectionClosedAtom); + const handleWsCodeMessage = useSetAtom(handleWsCodeMessageAtom); + const handleWsSubmitMessage = useSetAtom(handleWsSubmitMessageAtom); + const handleWsExecResultMessage = useSetAtom(handleWsExecResultMessageAtom); + const handleWsSubmitResultMessage = useSetAtom( + handleWsSubmitResultMessageAtom, + ); + + useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp); + const { lastJsonMessage, readyState } = useWebSocket< GameWatcherMessageS2C, GameWatcherMessageC2S >(socketUrl); - const [gameState, setGameState] = useState<GameState>("connecting"); - - const [startedAt, setStartedAt] = useState<number | null>(null); - - const [leftTimeSeconds, setLeftTimeSeconds] = useState<number | null>(null); - - useEffect(() => { - if ( - (gameState === "starting" || gameState === "gaming") && - startedAt !== null - ) { - const timer = setInterval(() => { - setLeftTimeSeconds((prev) => { - if (prev === null) { - return null; - } - if (prev <= 1) { - const nowSec = Math.floor(Date.now() / 1000); - const finishedAt = startedAt + game.duration_seconds; - if (nowSec >= finishedAt) { - clearInterval(timer); - setGameState("finished"); - audioController.playSoundEffectFinish(); - } else { - setGameState("gaming"); - } - } - return prev - 1; - }); - }, 1000); - - return () => { - clearInterval(timer); - }; - } - }, [gameState, startedAt, game.duration_seconds, audioController]); - - 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: "", - submitResult: { - status: "waiting_submission", - execResults: game.exec_steps.map((r) => ({ - testcase_id: r.testcase_id, - status: "waiting_submission", - label: r.label, - stdout: "", - stderr: "", - })), - }, - }); - const [playerInfoB, setPlayerInfoB] = useState<PlayerInfo>({ - displayName: playerB?.display_name ?? null, - iconPath: playerB?.icon_path ?? null, - score: null, - code: "", - submitResult: { - status: "waiting_submission", - execResults: game.exec_steps.map((r) => ({ - testcase_id: r.testcase_id, - status: "waiting_submission", - label: r.label, - stdout: "", - stderr: "", - })), - }, - }); + const playerA = game.players[0]!; + const playerB = game.players[1]!; + + const getTargetAtomByPlayerId: <T>( + player_id: number, + atomA: T, + atomB: T, + ) => T = useCallback( + (player_id, atomA, atomB) => + player_id === playerA.user_id ? atomA : atomB, + [playerA.user_id], + ); + + const playerProfileA = { + displayName: playerA.display_name, + iconPath: playerA.icon_path ?? null, + }; + const playerProfileB = { + displayName: playerB.display_name, + iconPath: playerB.icon_path ?? null, + }; if (readyState === ReadyState.UNINSTANTIATED) { throw new Error("WebSocket is not connected"); @@ -115,155 +84,107 @@ export default function GolfWatchApp({ useEffect(() => { if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) { - if (gameState !== "finished") { - setGameState("connecting"); - } + handleWsConnectionClosed(); } else if (readyState === ReadyState.CONNECTING) { - setGameState("connecting"); + setGameStateConnecting(); } else if (readyState === ReadyState.OPEN) { if (lastJsonMessage !== null) { console.log(lastJsonMessage.type); if (lastJsonMessage.type === "watcher:s2c:start") { - if ( - gameState !== "starting" && - gameState !== "gaming" && - gameState !== "finished" - ) { - const { start_at } = lastJsonMessage.data; - setStartedAt(start_at); - const nowSec = Math.floor(Date.now() / 1000); - setLeftTimeSeconds(start_at - nowSec); - setGameState("starting"); - } + const { start_at } = lastJsonMessage.data; + gameStart(start_at); } else if (lastJsonMessage.type === "watcher:s2c:code") { - const { player_id, code } = lastJsonMessage.data; - const setter = - player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; - setter((prev) => ({ ...prev, code })); + handleWsCodeMessage( + lastJsonMessage.data, + getTargetAtomByPlayerId, + (player_id, code) => { + const baseKey = `watcherState:${game.game_id}:${player_id}`; + window.localStorage.setItem(`${baseKey}:code`, code); + }, + ); } else if (lastJsonMessage.type === "watcher:s2c:submit") { - const { player_id } = lastJsonMessage.data; - const setter = - player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; - setter((prev) => ({ - ...prev, - submitResult: { - status: "running", - execResults: prev.submitResult.execResults.map((r) => ({ - ...r, - status: "running", - stdout: "", - stderr: "", - })), + handleWsSubmitMessage( + lastJsonMessage.data, + getTargetAtomByPlayerId, + (player_id, submissionResult) => { + const baseKey = `watcherState:${game.game_id}:${player_id}`; + window.localStorage.setItem( + `${baseKey}:submissionResult`, + JSON.stringify(submissionResult), + ); }, - })); + ); } else if (lastJsonMessage.type === "watcher:s2c:execresult") { - const { player_id, testcase_id, status, stdout, stderr } = - lastJsonMessage.data; - const setter = - player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; - setter((prev) => { - const ret = { ...prev }; - ret.submitResult = { - ...prev.submitResult, - execResults: prev.submitResult.execResults.map((r) => - r.testcase_id === testcase_id && r.status === "running" - ? { - ...r, - status, - stdout, - stderr, - } - : r, - ), - }; - return ret; - }); + handleWsExecResultMessage( + lastJsonMessage.data, + getTargetAtomByPlayerId, + (player_id, submissionResult) => { + const baseKey = `watcherState:${game.game_id}:${player_id}`; + window.localStorage.setItem( + `${baseKey}:submissionResult`, + JSON.stringify(submissionResult), + ); + }, + ); } else if (lastJsonMessage.type === "watcher:s2c:submitresult") { - const { player_id, status, score } = lastJsonMessage.data; - const setter = - player_id === playerA?.user_id ? setPlayerInfoA : setPlayerInfoB; - setter((prev) => { - const ret = { ...prev }; - ret.submitResult = { - ...prev.submitResult, - status, - }; - if (status === "success") { - if (score) { - if (ret.score === null || score < ret.score) { - ret.score = score; - } - } - } else { - ret.submitResult.execResults = prev.submitResult.execResults.map( - (r) => - r.status === "running" ? { ...r, status: "canceled" } : r, + handleWsSubmitResultMessage( + lastJsonMessage.data, + getTargetAtomByPlayerId, + (player_id, submissionResult, score) => { + const baseKey = `watcherState:${game.game_id}:${player_id}`; + window.localStorage.setItem( + `${baseKey}:submissionResult`, + JSON.stringify(submissionResult), + ); + window.localStorage.setItem( + `${baseKey}:score`, + score === null ? "" : score.toString(), ); - } - return ret; - }); + }, + ); } } else { if (game.started_at) { - const nowSec = Math.floor(Date.now() / 1000); - if (game.started_at <= nowSec) { - // The game has already started. - if (gameState !== "gaming" && gameState !== "finished") { - setStartedAt(game.started_at); - setLeftTimeSeconds(game.started_at - nowSec); - setGameState("gaming"); - } - } else { - // The game is starting. - if ( - gameState !== "starting" && - gameState !== "gaming" && - gameState !== "finished" - ) { - setStartedAt(game.started_at); - setLeftTimeSeconds(game.started_at - nowSec); - setGameState("starting"); - } - } + gameStart(game.started_at); } else { - setGameState("waiting"); + setGameStateWaiting(); } } } }, [ game.started_at, + game.game_id, lastJsonMessage, readyState, - gameState, - playerA?.user_id, - playerB?.user_id, + gameStart, + getTargetAtomByPlayerId, + handleWsCodeMessage, + handleWsConnectionClosed, + handleWsExecResultMessage, + handleWsSubmitMessage, + handleWsSubmitResultMessage, + setGameStateConnecting, + setGameStateWaiting, ]); - if (gameState === "connecting") { + if (gameStateKind === "connecting") { return <GolfWatchAppConnecting />; - } else if (gameState === "waiting") { + } else if (gameStateKind === "waiting") { return ( <GolfWatchAppWaiting gameDisplayName={game.display_name} - playerInfoA={playerInfoA} - playerInfoB={playerInfoB} - /> - ); - } else if (gameState === "starting") { - return ( - <GolfWatchAppStarting - gameDisplayName={game.display_name} - leftTimeSeconds={leftTimeSeconds!} + playerProfileA={playerProfileA} + playerProfileB={playerProfileB} /> ); - } else if (gameState === "gaming" || gameState === "finished") { + } else if (gameStateKind === "starting") { + return <GolfWatchAppStarting gameDisplayName={game.display_name} />; + } else if (gameStateKind === "gaming" || gameStateKind === "finished") { return ( <GolfWatchAppGaming gameDisplayName={game.display_name} - gameDurationSeconds={game.duration_seconds} - leftTimeSeconds={leftTimeSeconds!} - playerInfoA={playerInfoA} - playerInfoB={playerInfoB} + playerProfileA={playerProfileA} + playerProfileB={playerProfileB} problemTitle={game.problem.title} problemDescription={game.problem.description} gameResult={null /* TODO */} diff --git a/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx b/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx index e299f4b..ce5a59c 100644 --- a/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx +++ b/frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx @@ -1,35 +1,33 @@ -import { useState } from "react"; +import { useAtom } from "jotai"; import { AudioController } from "../.client/audio/AudioController"; +import { audioControllerAtom } from "../states/watch"; import GolfWatchApp, { type Props } from "./GolfWatchApp.client"; +import SubmitButton from "./SubmitButton"; export default function GolfWatchAppWithAudioPlayRequest({ game, sockToken, }: Omit<Props, "audioController">) { - const [audioController, setAudioController] = - useState<AudioController | null>(null); + const [audioController, setAudioController] = useAtom(audioControllerAtom); const audioPlayPermitted = audioController !== null; if (audioPlayPermitted) { - return ( - <GolfWatchApp - game={game} - sockToken={sockToken} - audioController={audioController} - /> - ); + return <GolfWatchApp game={game} sockToken={sockToken} />; } else { return ( - <div> - <button - onClick={async () => { - const audioController = new AudioController(); - await audioController.loadAll(); - setAudioController(audioController); - }} - > - Enable Audio Play - </button> + <div className="min-h-screen bg-gray-100 flex items-center justify-center"> + <div className="text-center"> + <SubmitButton + onClick={async () => { + const audioController = new AudioController(); + await audioController.loadAll(); + await audioController.playDummySoundEffect(); + setAudioController(audioController); + }} + > + 開始 + </SubmitButton> + </div> </div> ); } diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx index 28babff..2907f5a 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx @@ -1,4 +1,14 @@ -import { PlayerInfo } from "../../models/PlayerInfo"; +import { useAtomValue } from "jotai"; +import { + codeAAtom, + codeBAtom, + gamingLeftTimeSecondsAtom, + scoreAAtom, + scoreBAtom, + submitResultAAtom, + submitResultBAtom, +} from "../../states/watch"; +import type { PlayerProfile } from "../../types/PlayerProfile"; import BorderedContainer from "../BorderedContainer"; import CodeBlock from "../Gaming/CodeBlock"; import ScoreBar from "../Gaming/ScoreBar"; @@ -7,10 +17,8 @@ import UserIcon from "../UserIcon"; type Props = { gameDisplayName: string; - gameDurationSeconds: number; - leftTimeSeconds: number; - playerInfoA: PlayerInfo; - playerInfoB: PlayerInfo; + playerProfileA: PlayerProfile; + playerProfileB: PlayerProfile; problemTitle: string; problemDescription: string; gameResult: "winA" | "winB" | "draw" | null; @@ -18,21 +26,23 @@ type Props = { export default function GolfWatchAppGaming({ gameDisplayName, - gameDurationSeconds, - leftTimeSeconds, - playerInfoA, - playerInfoB, + playerProfileA, + playerProfileB, problemTitle, problemDescription, gameResult, }: Props) { + const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!; + const codeA = useAtomValue(codeAAtom); + const codeB = useAtomValue(codeBAtom); + const scoreA = useAtomValue(scoreAAtom); + const scoreB = useAtomValue(scoreBAtom); + const submitResultA = useAtomValue(submitResultAAtom); + const submitResultB = useAtomValue(submitResultBAtom); + const leftTime = (() => { - const k = gameDurationSeconds + leftTimeSeconds; - if (k <= 0) { - return "00:00"; - } - const m = Math.floor(k / 60); - const s = k % 60; + const m = Math.floor(leftTimeSeconds / 60); + const s = leftTimeSeconds % 60; return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; })(); @@ -49,43 +59,43 @@ export default function GolfWatchAppGaming({ <div className={`text-white ${topBg} grid grid-cols-3 px-4 py-2`}> <div className="font-bold flex justify-between my-auto"> <div className="flex gap-6"> - {playerInfoA.iconPath && ( + {playerProfileA.iconPath && ( <UserIcon - iconPath={playerInfoA.iconPath} - displayName={playerInfoA.displayName!} + iconPath={playerProfileA.iconPath} + displayName={playerProfileA.displayName} className="w-12 h-12 my-auto" /> )} <div> <div className="text-gray-100">Player 1</div> - <div className="text-2xl">{playerInfoA.displayName}</div> + <div className="text-2xl">{playerProfileA.displayName}</div> </div> </div> - <div className="text-6xl">{playerInfoA.score}</div> + <div className="text-6xl">{scoreA}</div> </div> <div className="font-bold text-center"> <div className="text-gray-100">{gameDisplayName}</div> <div className="text-3xl"> {gameResult ? gameResult === "winA" - ? `勝者 ${playerInfoA.displayName}` + ? `勝者 ${playerProfileA.displayName}` : gameResult === "winB" - ? `勝者 ${playerInfoB.displayName}` + ? `勝者 ${playerProfileB.displayName}` : "引き分け" : leftTime} </div> </div> <div className="font-bold flex justify-between my-auto"> - <div className="text-6xl">{playerInfoB.score}</div> + <div className="text-6xl">{scoreB}</div> <div className="flex gap-6 text-end"> <div> <div className="text-gray-100">Player 2</div> - <div className="text-2xl">{playerInfoB.displayName}</div> + <div className="text-2xl">{playerProfileB.displayName}</div> </div> - {playerInfoB.iconPath && ( + {playerProfileB.iconPath && ( <UserIcon - iconPath={playerInfoB.iconPath} - displayName={playerInfoB.displayName!} + iconPath={playerProfileB.iconPath} + displayName={playerProfileB.displayName} className="w-12 h-12 my-auto" /> )} @@ -93,17 +103,17 @@ export default function GolfWatchAppGaming({ </div> </div> <ScoreBar - scoreA={playerInfoA.score} - scoreB={playerInfoB.score} + scoreA={scoreA} + scoreB={scoreB} bgA="bg-orange-400" bgB="bg-purple-400" /> <div className="grow grid grid-cols-3 p-4 gap-4"> - <CodeBlock code={playerInfoA.code ?? ""} language="swift" /> + <CodeBlock code={codeA} language="swift" /> <div className="flex flex-col gap-4"> <div className="grid grid-cols-2 gap-4"> - <SubmitResult result={playerInfoA.submitResult} /> - <SubmitResult result={playerInfoB.submitResult} /> + <SubmitResult result={submitResultA} /> + <SubmitResult result={submitResultB} /> </div> <div> <div className="mb-2 text-center text-xl font-bold"> @@ -112,7 +122,7 @@ export default function GolfWatchAppGaming({ <BorderedContainer>{problemDescription}</BorderedContainer> </div> </div> - <CodeBlock code={playerInfoB.code ?? ""} language="swift" /> + <CodeBlock code={codeB} language="swift" /> </div> </div> ); diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx index cd4195d..684d2af 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx @@ -1,18 +1,19 @@ +import { useAtomValue } from "jotai"; +import { startingLeftTimeSecondsAtom } from "../../states/watch"; + type Props = { gameDisplayName: string; - leftTimeSeconds: number; }; -export default function GolfWatchAppStarting({ - gameDisplayName, - leftTimeSeconds, -}: Props) { +export default function GolfWatchAppStarting({ gameDisplayName }: Props) { + const leftTimeSeconds = useAtomValue(startingLeftTimeSecondsAtom)!; + return ( <div className="min-h-screen bg-gray-100 flex flex-col"> <div className="text-white bg-iosdc-japan p-10 text-center"> <div className="text-4xl font-bold">{gameDisplayName}</div> </div> - <div className="text-center text-black font-black text-10xl animate-ping"> + <div className="text-center text-black font-black text-10xl"> {leftTimeSeconds} </div> </div> diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx index faa9485..0e964e3 100644 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx @@ -1,18 +1,16 @@ -import { PlayerInfo as FullPlayerInfo } from "../../models/PlayerInfo"; -import PlayerProfile from "../PlayerProfile"; - -type PlayerInfo = Pick<FullPlayerInfo, "displayName" | "iconPath">; +import type { PlayerProfile } from "../../types/PlayerProfile"; +import PlayerNameAndIcon from "../PlayerNameAndIcon"; type Props = { gameDisplayName: string; - playerInfoA: PlayerInfo; - playerInfoB: PlayerInfo; + playerProfileA: PlayerProfile; + playerProfileB: PlayerProfile; }; export default function GolfWatchAppWaiting({ gameDisplayName, - playerInfoA, - playerInfoB, + playerProfileA, + playerProfileB, }: Props) { return ( <div className="min-h-screen bg-gray-100 flex flex-col font-bold text-center"> @@ -20,9 +18,9 @@ export default function GolfWatchAppWaiting({ <div className="text-4xl">{gameDisplayName}</div> </div> <div className="grow grid grid-cols-3 gap-10 mx-auto text-black"> - <PlayerProfile playerInfo={playerInfoA} label="Player 1" /> + <PlayerNameAndIcon label="Player 1" profile={playerProfileA} /> <div className="text-8xl my-auto">vs.</div> - <PlayerProfile playerInfo={playerInfoB} label="Player 2" /> + <PlayerNameAndIcon label="Player 2" profile={playerProfileB} /> </div> </div> ); diff --git a/frontend/app/components/PlayerNameAndIcon.tsx b/frontend/app/components/PlayerNameAndIcon.tsx new file mode 100644 index 0000000..e9536e3 --- /dev/null +++ b/frontend/app/components/PlayerNameAndIcon.tsx @@ -0,0 +1,25 @@ +import { PlayerProfile } from "../types/PlayerProfile"; +import UserIcon from "./UserIcon"; + +type Props = { + label: string; + profile: PlayerProfile; +}; + +export default function PlayerNameAndIcon({ label, profile }: Props) { + return ( + <div className="flex flex-col gap-6 my-auto"> + <div className="flex flex-col gap-2"> + <div className="text-4xl">{label}</div> + <div className="text-6xl">{profile.displayName}</div> + </div> + {profile.iconPath && ( + <UserIcon + iconPath={profile.iconPath} + displayName={profile.displayName} + className="w-48 h-48" + /> + )} + </div> + ); +} diff --git a/frontend/app/components/PlayerProfile.tsx b/frontend/app/components/PlayerProfile.tsx deleted file mode 100644 index 675d77b..0000000 --- a/frontend/app/components/PlayerProfile.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { PlayerInfo as FullPlayerInfo } from "../models/PlayerInfo"; -import UserIcon from "./UserIcon"; - -type PlayerInfo = Pick<FullPlayerInfo, "displayName" | "iconPath">; - -type Props = { - playerInfo: PlayerInfo; - label: string; -}; - -export default function PlayerProfile({ playerInfo, label }: Props) { - return ( - <div className="flex flex-col gap-6 my-auto"> - <div className="flex flex-col gap-2"> - <div className="text-4xl">{label}</div> - <div className="text-6xl">{playerInfo.displayName}</div> - </div> - {playerInfo.iconPath && ( - <UserIcon - iconPath={playerInfo.iconPath} - displayName={playerInfo.displayName!} - className="w-48 h-48" - /> - )} - </div> - ); -} diff --git a/frontend/app/components/SubmitStatusLabel.tsx b/frontend/app/components/SubmitStatusLabel.tsx index 0e13c1e..d1dc89c 100644 --- a/frontend/app/components/SubmitStatusLabel.tsx +++ b/frontend/app/components/SubmitStatusLabel.tsx @@ -1,4 +1,4 @@ -import type { SubmitResultStatus } from "../models/SubmitResult"; +import type { SubmitResultStatus } from "../types/SubmitResult"; type Props = { status: SubmitResultStatus; |
