From 1e6df136d8202c8adf65948527f4c3e7583b338c Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 4 Mar 2025 22:55:01 +0900 Subject: websocket to polling --- frontend/app/components/GolfWatchApp.tsx | 127 +++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 frontend/app/components/GolfWatchApp.tsx (limited to 'frontend/app/components/GolfWatchApp.tsx') diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx new file mode 100644 index 0000000..fe71932 --- /dev/null +++ b/frontend/app/components/GolfWatchApp.tsx @@ -0,0 +1,127 @@ +import { useAtomValue, useSetAtom } from "jotai"; +import { useContext, useEffect, useState } from "react"; +import { useTimer } from "react-use-precision-timer"; +import { + ApiAuthTokenContext, + apiGetGame, + apiGetGameWatchLatestStates, + apiGetGameWatchRanking, +} from "../api/client"; +import type { components } from "../api/schema"; +import { + gameStateKindAtom, + setCurrentTimestampAtom, + setGameStartedAtAtom, + setLatestGameStatesAtom, + setRankingAtom, +} from "../states/watch"; +import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming"; +import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting"; +import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting"; + +type Game = components["schemas"]["Game"]; + +export type Props = { + game: Game; +}; + +export default function GolfWatchApp({ game }: Props) { + const apiAuthToken = useContext(ApiAuthTokenContext); + + const gameStateKind = useAtomValue(gameStateKindAtom); + const setGameStartedAt = useSetAtom(setGameStartedAtAtom); + const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom); + const setLatestGameStates = useSetAtom(setLatestGameStatesAtom); + const setRanking = useSetAtom(setRankingAtom); + + useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp); + + const playerA = game.main_players[0]!; + const playerB = game.main_players[1]!; + + const playerProfileA = { + id: playerA.user_id, + displayName: playerA.display_name, + iconPath: playerA.icon_path ?? null, + }; + const playerProfileB = { + id: playerB.user_id, + displayName: playerB.display_name, + iconPath: playerB.icon_path ?? null, + }; + + const [isDataPolling, setIsDataPolling] = useState(false); + + useEffect(() => { + if (isDataPolling) { + return; + } + const timerId = setInterval(async () => { + if (isDataPolling) { + return; + } + setIsDataPolling(true); + + try { + if (gameStateKind === "waiting") { + const { game: g } = await apiGetGame(apiAuthToken, game.game_id); + if (g.started_at != null) { + setGameStartedAt(g.started_at); + } + } else if (gameStateKind === "gaming") { + const { states } = await apiGetGameWatchLatestStates( + apiAuthToken, + game.game_id, + ); + setLatestGameStates(states); + const { ranking } = await apiGetGameWatchRanking( + apiAuthToken, + game.game_id, + ); + setRanking(ranking); + } + } catch (error) { + console.error(error); + } finally { + setIsDataPolling(false); + } + }, 1000); + + return () => { + clearInterval(timerId); + }; + }, [ + isDataPolling, + apiAuthToken, + game.game_id, + gameStateKind, + setGameStartedAt, + setLatestGameStates, + setRanking, + ]); + + if (gameStateKind === "waiting") { + return ( + + ); + } else if (gameStateKind === "starting") { + return ; + } else if (gameStateKind === "gaming" || gameStateKind === "finished") { + return ( + + ); + } else { + return null; + } +} -- cgit v1.2.3-70-g09d2 From c889a9ad33374eae03cec5b0358d79016d6fd97e Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 8 Mar 2025 10:32:05 +0900 Subject: show ranking --- frontend/app/components/GolfWatchApp.tsx | 66 +++++++---- .../GolfWatchApps/GolfWatchAppGaming.tsx | 128 --------------------- .../GolfWatchApps/GolfWatchAppGaming1v1.tsx | 128 +++++++++++++++++++++ .../GolfWatchAppGamingMultiplayer.tsx | 102 ++++++++++++++++ .../GolfWatchApps/GolfWatchAppWaiting.tsx | 27 ----- .../GolfWatchApps/GolfWatchAppWaiting1v1.tsx | 27 +++++ .../GolfWatchAppWaitingMultiplayer.tsx | 15 +++ frontend/app/routes/golf.$gameId.watch.tsx | 4 +- frontend/app/states/watch.ts | 5 +- 9 files changed, 316 insertions(+), 186 deletions(-) delete mode 100644 frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx create mode 100644 frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx create mode 100644 frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx delete mode 100644 frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx create mode 100644 frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx create mode 100644 frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx (limited to 'frontend/app/components/GolfWatchApp.tsx') diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx index fe71932..402884f 100644 --- a/frontend/app/components/GolfWatchApp.tsx +++ b/frontend/app/components/GolfWatchApp.tsx @@ -1,4 +1,4 @@ -import { useAtomValue, useSetAtom } from "jotai"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useContext, useEffect, useState } from "react"; import { useTimer } from "react-use-precision-timer"; import { @@ -10,14 +10,16 @@ import { import type { components } from "../api/schema"; import { gameStateKindAtom, + rankingAtom, setCurrentTimestampAtom, setGameStartedAtAtom, setLatestGameStatesAtom, - setRankingAtom, } from "../states/watch"; -import GolfWatchAppGaming from "./GolfWatchApps/GolfWatchAppGaming"; +import GolfWatchAppGaming1v1 from "./GolfWatchApps/GolfWatchAppGaming1v1"; +import GolfWatchAppGamingMultiplayer from "./GolfWatchApps/GolfWatchAppGamingMultiplayer"; import GolfWatchAppStarting from "./GolfWatchApps/GolfWatchAppStarting"; -import GolfWatchAppWaiting from "./GolfWatchApps/GolfWatchAppWaiting"; +import GolfWatchAppWaiting1v1 from "./GolfWatchApps/GolfWatchAppWaiting1v1"; +import GolfWatchAppWaitingMultiplayer from "./GolfWatchApps/GolfWatchAppWaitingMultiplayer"; type Game = components["schemas"]["Game"]; @@ -32,23 +34,27 @@ export default function GolfWatchApp({ game }: Props) { const setGameStartedAt = useSetAtom(setGameStartedAtAtom); const setCurrentTimestamp = useSetAtom(setCurrentTimestampAtom); const setLatestGameStates = useSetAtom(setLatestGameStatesAtom); - const setRanking = useSetAtom(setRankingAtom); + const [ranking, setRanking] = useAtom(rankingAtom); useTimer({ delay: 1000, startImmediately: true }, setCurrentTimestamp); - const playerA = game.main_players[0]!; - const playerB = game.main_players[1]!; + const playerA = game.main_players[0]; + const playerB = game.main_players[1]; - const playerProfileA = { - id: playerA.user_id, - displayName: playerA.display_name, - iconPath: playerA.icon_path ?? null, - }; - const playerProfileB = { - id: playerB.user_id, - displayName: playerB.display_name, - iconPath: playerB.icon_path ?? null, - }; + const playerProfileA = playerA + ? { + id: playerA.user_id, + displayName: playerA.display_name, + iconPath: playerA.icon_path ?? null, + } + : null; + const playerProfileB = playerB + ? { + id: playerB.user_id, + displayName: playerB.display_name, + iconPath: playerB.icon_path ?? null, + } + : null; const [isDataPolling, setIsDataPolling] = useState(false); @@ -101,21 +107,31 @@ export default function GolfWatchApp({ game }: Props) { ]); if (gameStateKind === "waiting") { - return ( - + ) : ( + ); } else if (gameStateKind === "starting") { return ; } else if (gameStateKind === "gaming" || gameStateKind === "finished") { - return ( - + ) : ( + { - const m = Math.floor(leftTimeSeconds / 60); - const s = leftTimeSeconds % 60; - return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; - })(); - - const topBg = gameResult - ? gameResult === "winA" - ? "bg-orange-400" - : gameResult === "winB" - ? "bg-purple-400" - : "bg-pink-500" - : "bg-sky-600"; - - return ( -
-
-
-
- {playerProfileA.iconPath && ( - - )} -
-
Player 1
-
{playerProfileA.displayName}
-
-
-
{scoreA}
-
-
-
{gameDisplayName}
-
- {gameResult - ? gameResult === "winA" - ? `勝者 ${playerProfileA.displayName}` - : gameResult === "winB" - ? `勝者 ${playerProfileB.displayName}` - : "引き分け" - : leftTime} -
-
-
-
{scoreB}
-
-
-
Player 2
-
{playerProfileB.displayName}
-
- {playerProfileB.iconPath && ( - - )} -
-
-
- -
- -
-
- - -
-
-
- {problemTitle} -
- {problemDescription} -
-
- -
-
- ); -} diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx new file mode 100644 index 0000000..033186c --- /dev/null +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming1v1.tsx @@ -0,0 +1,128 @@ +import { useAtomValue } from "jotai"; +import { + gamingLeftTimeSecondsAtom, + latestGameStatesAtom, +} from "../../states/watch"; +import type { PlayerProfile } from "../../types/PlayerProfile"; +import BorderedContainer from "../BorderedContainer"; +import CodeBlock from "../Gaming/CodeBlock"; +import ScoreBar from "../Gaming/ScoreBar"; +import SubmitResult from "../Gaming/SubmitResult"; +import UserIcon from "../UserIcon"; + +type Props = { + gameDisplayName: string; + playerProfileA: PlayerProfile; + playerProfileB: PlayerProfile; + problemTitle: string; + problemDescription: string; + gameResult: "winA" | "winB" | "draw" | null; +}; + +export default function GolfWatchAppGaming1v1({ + gameDisplayName, + playerProfileA, + playerProfileB, + problemTitle, + problemDescription, + gameResult, +}: Props) { + const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!; + const latestGameStates = useAtomValue(latestGameStatesAtom); + + const stateA = latestGameStates[playerProfileA.id]; + const codeA = stateA?.code ?? ""; + const scoreA = stateA?.score ?? null; + const statusA = stateA?.status ?? "none"; + const stateB = latestGameStates[playerProfileB.id]; + const codeB = stateB?.code ?? ""; + const scoreB = stateB?.score ?? null; + const statusB = stateB?.status ?? "none"; + + const leftTime = (() => { + const m = Math.floor(leftTimeSeconds / 60); + const s = leftTimeSeconds % 60; + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + })(); + + const topBg = gameResult + ? gameResult === "winA" + ? "bg-orange-400" + : gameResult === "winB" + ? "bg-purple-400" + : "bg-pink-500" + : "bg-sky-600"; + + return ( +
+
+
+
+ {playerProfileA.iconPath && ( + + )} +
+
Player 1
+
{playerProfileA.displayName}
+
+
+
{scoreA}
+
+
+
{gameDisplayName}
+
+ {gameResult + ? gameResult === "winA" + ? `勝者 ${playerProfileA.displayName}` + : gameResult === "winB" + ? `勝者 ${playerProfileB.displayName}` + : "引き分け" + : leftTime} +
+
+
+
{scoreB}
+
+
+
Player 2
+
{playerProfileB.displayName}
+
+ {playerProfileB.iconPath && ( + + )} +
+
+
+ +
+ +
+
+ + +
+
+
+ {problemTitle} +
+ {problemDescription} +
+
+ +
+
+ ); +} diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx new file mode 100644 index 0000000..b6d2ac3 --- /dev/null +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGamingMultiplayer.tsx @@ -0,0 +1,102 @@ +import { useAtomValue } from "jotai"; +import type { components } from "../../api/schema"; +import { gamingLeftTimeSecondsAtom } from "../../states/watch"; +import BorderedContainer from "../BorderedContainer"; + +type RankingEntry = components["schemas"]["RankingEntry"]; + +type Props = { + gameDisplayName: string; + ranking: RankingEntry[]; + problemTitle: string; + problemDescription: string; + gameResult: "winA" | "winB" | "draw" | null; +}; + +export default function GolfWatchAppGamingMultiplayer({ + gameDisplayName, + ranking, + problemTitle, + problemDescription, + gameResult, +}: Props) { + const leftTimeSeconds = useAtomValue(gamingLeftTimeSecondsAtom)!; + + const leftTime = (() => { + const m = Math.floor(leftTimeSeconds / 60); + const s = leftTimeSeconds % 60; + return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; + })(); + + const topBg = gameResult + ? gameResult === "winA" + ? "bg-orange-400" + : gameResult === "winB" + ? "bg-purple-400" + : "bg-pink-500" + : "bg-sky-600"; + + return ( +
+
+
+
+
{gameDisplayName}
+
{leftTime}
+
+
+
+
+
+
+
+ {problemTitle} +
+ {problemDescription} +
+
+
+ + + + + + + + + + {ranking.map((entry, index) => ( + + + + + + ))} + +
+ 順位 + + 名前 + + スコア +
+ {index + 1} + + {entry.player.display_name} + + {entry.score} +
+
+
+
+ ); +} diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx deleted file mode 100644 index c2a5431..0000000 --- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { PlayerProfile } from "../../types/PlayerProfile"; -import PlayerNameAndIcon from "../PlayerNameAndIcon"; - -type Props = { - gameDisplayName: string; - playerProfileA: PlayerProfile; - playerProfileB: PlayerProfile; -}; - -export default function GolfWatchAppWaiting({ - gameDisplayName, - playerProfileA, - playerProfileB, -}: Props) { - return ( -
-
-
{gameDisplayName}
-
-
- -
vs.
- -
-
- ); -} diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx new file mode 100644 index 0000000..fb315ff --- /dev/null +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting1v1.tsx @@ -0,0 +1,27 @@ +import type { PlayerProfile } from "../../types/PlayerProfile"; +import PlayerNameAndIcon from "../PlayerNameAndIcon"; + +type Props = { + gameDisplayName: string; + playerProfileA: PlayerProfile; + playerProfileB: PlayerProfile; +}; + +export default function GolfWatchAppWaiting1v1({ + gameDisplayName, + playerProfileA, + playerProfileB, +}: Props) { + return ( +
+
+
{gameDisplayName}
+
+
+ +
vs.
+ +
+
+ ); +} diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx new file mode 100644 index 0000000..13bcc10 --- /dev/null +++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaitingMultiplayer.tsx @@ -0,0 +1,15 @@ +type Props = { + gameDisplayName: string; +}; + +export default function GolfWatchAppWaitingMultiplayer({ + gameDisplayName, +}: Props) { + return ( +
+
+
{gameDisplayName}
+
+
+ ); +} diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx index 0c07633..fed06aa 100644 --- a/frontend/app/routes/golf.$gameId.watch.tsx +++ b/frontend/app/routes/golf.$gameId.watch.tsx @@ -10,11 +10,11 @@ import { } from "../api/client"; import GolfWatchApp from "../components/GolfWatchApp"; import { + rankingAtom, setCurrentTimestampAtom, setDurationSecondsAtom, setGameStartedAtAtom, setLatestGameStatesAtom, - setRankingAtom, } from "../states/watch"; export const meta: MetaFunction = ({ data }) => [ @@ -59,10 +59,10 @@ export default function GolfWatch() { useLoaderData(); useHydrateAtoms([ + [rankingAtom, ranking], [setCurrentTimestampAtom, undefined], [setDurationSecondsAtom, game.duration_seconds], [setGameStartedAtAtom, game.started_at ?? null], - [setRankingAtom, ranking], [setLatestGameStatesAtom, gameStates], ]); diff --git a/frontend/app/states/watch.ts b/frontend/app/states/watch.ts index ad95e0a..d3cc723 100644 --- a/frontend/app/states/watch.ts +++ b/frontend/app/states/watch.ts @@ -58,10 +58,7 @@ export const gamingLeftTimeSecondsAtom = atom((get) => { return Math.min(durationSeconds, Math.max(0, finishedAt - currentTimestamp)); }); -const rankingAtom = atom([]); -export const setRankingAtom = atom(null, (_, set, value: RankingEntry[]) => { - set(rankingAtom, value); -}); +export const rankingAtom = atom([]); const rawLatestGameStatesAtom = atom<{ [key: string]: LatestGameState | undefined; -- cgit v1.2.3-70-g09d2