aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/components/GolfWatchApp.client.tsx
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-22 12:09:04 +0900
committernsfisis <nsfisis@gmail.com>2024-08-22 13:15:59 +0900
commitc6ab1db6688f26880503e59b165cba4849f924be (patch)
tree24e52810b9a06527ac4a500b51e2df8e52cee055 /frontend/app/components/GolfWatchApp.client.tsx
parenta2b6ed9cd67f1406a6656bce9b3d51b55378ac1e (diff)
downloadiosdc-japan-2025-albatross-c6ab1db6688f26880503e59b165cba4849f924be.tar.gz
iosdc-japan-2025-albatross-c6ab1db6688f26880503e59b165cba4849f924be.tar.zst
iosdc-japan-2025-albatross-c6ab1db6688f26880503e59b165cba4849f924be.zip
feat(frontend): jotai for watch app
Diffstat (limited to 'frontend/app/components/GolfWatchApp.client.tsx')
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx295
1 files changed, 106 insertions, 189 deletions
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
index 9eacb2d..7ea2ced 100644
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ b/frontend/app/components/GolfWatchApp.client.tsx
@@ -1,8 +1,21 @@
-import { useEffect, useState } from "react";
+import { useAtomValue, useSetAtom } from "jotai";
+import { useCallback, useEffect } from "react";
+import { useTimer } from "react-use-precision-timer";
import { AudioController } from "../.client/audio/AudioController";
import type { components } from "../.server/api/schema";
import useWebSocket, { ReadyState } from "../hooks/useWebSocket";
-import type { PlayerState } from "../types/PlayerState";
+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,8 +26,6 @@ type GameWatcherMessageC2S = never;
type Game = components["schemas"]["Game"];
-type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished";
-
export type Props = {
game: Game;
sockToken: string;
@@ -31,87 +42,47 @@ export default function GolfWatchApp({
? `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 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 [playerStateA, setPlayerStateA] = useState<PlayerState>({
- 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 playerProfileB = {
displayName: playerB.display_name,
iconPath: playerB.icon_path ?? null,
};
- const [playerStateB, setPlayerStateB] = useState<PlayerState>({
- 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: "",
- })),
- },
- });
if (readyState === ReadyState.UNINSTANTIATED) {
throw new Error("WebSocket is not connected");
@@ -119,133 +90,92 @@ 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 ? setPlayerStateA : setPlayerStateB;
- 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 ? setPlayerStateA : setPlayerStateB;
- 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 ? setPlayerStateA : setPlayerStateB;
- 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 ? setPlayerStateA : setPlayerStateB;
- 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),
);
- }
- 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.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}
@@ -253,27 +183,14 @@ export default function GolfWatchApp({
playerProfileB={playerProfileB}
/>
);
- } else if (gameState === "starting") {
- return (
- <GolfWatchAppStarting
- gameDisplayName={game.display_name}
- leftTimeSeconds={leftTimeSeconds!}
- />
- );
- } 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={{
- profile: playerProfileA,
- state: playerStateA,
- }}
- playerInfoB={{
- profile: playerProfileB,
- state: playerStateB,
- }}
+ playerProfileA={playerProfileA}
+ playerProfileB={playerProfileB}
problemTitle={game.problem.title}
problemDescription={game.problem.description}
gameResult={null /* TODO */}