aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/components')
-rw-r--r--frontend/app/components/Gaming/ExecStatusIndicatorIcon.tsx2
-rw-r--r--frontend/app/components/Gaming/SubmitResult.tsx2
-rw-r--r--frontend/app/components/GolfPlayApp.client.tsx246
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx2
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx42
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx13
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx595
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx319
-rw-r--r--frontend/app/components/GolfWatchAppWithAudioPlayRequest.client.tsx38
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx76
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx13
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx18
-rw-r--r--frontend/app/components/PlayerNameAndIcon.tsx25
-rw-r--r--frontend/app/components/PlayerProfile.tsx27
-rw-r--r--frontend/app/components/SubmitStatusLabel.tsx2
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;