diff options
Diffstat (limited to 'frontend/app/routes')
| -rw-r--r-- | frontend/app/routes/golf.$gameId.play.tsx | 97 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.watch.tsx | 162 |
2 files changed, 241 insertions, 18 deletions
diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx index ea1b8fd..a2860dd 100644 --- a/frontend/app/routes/golf.$gameId.play.tsx +++ b/frontend/app/routes/golf.$gameId.play.tsx @@ -1,10 +1,17 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; -import { ClientOnly } from "remix-utils/client-only"; +import { ClientLoaderFunctionArgs, useLoaderData } from "@remix-run/react"; +import { useHydrateAtoms } from "jotai/utils"; import { apiGetGame, apiGetToken } from "../.server/api/client"; import { ensureUserLoggedIn } from "../.server/auth"; import GolfPlayApp from "../components/GolfPlayApp.client"; import GolfPlayAppConnecting from "../components/GolfPlayApps/GolfPlayAppConnecting"; +import { + scoreAtom, + setCurrentTimestampAtom, + setDurationSecondsAtom, + submitResultAtom, +} from "../states/play"; +import { PlayerState } from "../types/PlayerState"; export const meta: MetaFunction<typeof loader> = ({ data }) => [ { @@ -25,19 +32,97 @@ export async function loader({ params, request }: LoaderFunctionArgs) { }; const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]); + + const playerState: PlayerState = { + code: "", + 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: "", + })), + }, + }; + return { game, player: user, sockToken, + playerState, }; } +export async function clientLoader({ serverLoader }: ClientLoaderFunctionArgs) { + const data = await serverLoader<typeof loader>(); + const baseKey = `playerState:${data.game.game_id}:${data.player.user_id}`; + + const localCode = (() => { + const rawValue = window.localStorage.getItem(`${baseKey}:code`); + if (rawValue === null) { + return null; + } + return rawValue; + })(); + + const localScore = (() => { + const rawValue = window.localStorage.getItem(`${baseKey}:score`); + if (rawValue === null || rawValue === "") { + return null; + } + return Number(rawValue); + })(); + + const localSubmissionResult = (() => { + const rawValue = window.localStorage.getItem(`${baseKey}:submissionResult`); + if (rawValue === null) { + return null; + } + const parsed = JSON.parse(rawValue); + if (typeof parsed !== "object") { + return null; + } + return parsed; + })(); + + if (localCode !== null) { + data.playerState.code = localCode; + } + if (localScore !== null) { + data.playerState.score = localScore; + } + if (localSubmissionResult !== null) { + data.playerState.submitResult = localSubmissionResult; + } + + return data; +} +clientLoader.hydrate = true; + +export function HydrateFallback() { + return <GolfPlayAppConnecting />; +} + export default function GolfPlay() { - const { game, player, sockToken } = useLoaderData<typeof loader>(); + const { game, player, sockToken, playerState } = + useLoaderData<typeof loader>(); + + useHydrateAtoms([ + [setCurrentTimestampAtom, undefined], + [setDurationSecondsAtom, game.duration_seconds], + [scoreAtom, playerState.score], + [submitResultAtom, playerState.submitResult], + ]); return ( - <ClientOnly fallback={<GolfPlayAppConnecting />}> - {() => <GolfPlayApp game={game} player={player} sockToken={sockToken} />} - </ClientOnly> + <GolfPlayApp + game={game} + player={player} + initialCode={playerState.code} + sockToken={sockToken} + /> ); } diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx index 7e90b2d..f04f6b0 100644 --- a/frontend/app/routes/golf.$gameId.watch.tsx +++ b/frontend/app/routes/golf.$gameId.watch.tsx @@ -1,10 +1,21 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; -import { ClientOnly } from "remix-utils/client-only"; +import { ClientLoaderFunctionArgs, useLoaderData } from "@remix-run/react"; +import { useHydrateAtoms } from "jotai/utils"; import { apiGetGame, apiGetToken } from "../.server/api/client"; import { ensureUserLoggedIn } from "../.server/auth"; import GolfWatchAppWithAudioPlayRequest from "../components/GolfWatchAppWithAudioPlayRequest.client"; import GolfWatchAppConnecting from "../components/GolfWatchApps/GolfWatchAppConnecting"; +import { + codeAAtom, + codeBAtom, + scoreAAtom, + scoreBAtom, + setCurrentTimestampAtom, + setDurationSecondsAtom, + submitResultAAtom, + submitResultBAtom, +} from "../states/watch"; +import { PlayerState } from "../types/PlayerState"; export const meta: MetaFunction<typeof loader> = ({ data }) => [ { @@ -27,23 +38,150 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]); if (game.game_type !== "1v1") { - return new Response("Not Found", { status: 404 }); + throw new Response("Not Found", { status: 404 }); } + const playerStateA: PlayerState = { + code: "", + 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 playerStateB = structuredClone(playerStateA); + return { game, sockToken, + playerStateA, + playerStateB, }; } +export async function clientLoader({ serverLoader }: ClientLoaderFunctionArgs) { + const data = await serverLoader<typeof loader>(); + + const playerIdA = data.game.players[0]?.user_id; + const playerIdB = data.game.players[1]?.user_id; + + if (playerIdA !== null) { + const baseKeyA = `watcherState:${data.game.game_id}:${playerIdA}`; + + const localCodeA = (() => { + const rawValue = window.localStorage.getItem(`${baseKeyA}:code`); + + if (rawValue === null) { + return null; + } + return rawValue; + })(); + + const localScoreA = (() => { + const rawValue = window.localStorage.getItem(`${baseKeyA}:score`); + if (rawValue === null || rawValue === "") { + return null; + } + return Number(rawValue); + })(); + + const localSubmissionResultA = (() => { + const rawValue = window.localStorage.getItem( + `${baseKeyA}:submissionResult`, + ); + if (rawValue === null) { + return null; + } + const parsed = JSON.parse(rawValue); + if (typeof parsed !== "object") { + return null; + } + return parsed; + })(); + + if (localCodeA !== null) { + data.playerStateA.code = localCodeA; + } + if (localScoreA !== null) { + data.playerStateA.score = localScoreA; + } + if (localSubmissionResultA !== null) { + data.playerStateA.submitResult = localSubmissionResultA; + } + } + + if (playerIdB !== null) { + const baseKeyB = `watcherState:${data.game.game_id}:${playerIdB}`; + + const localCodeB = (() => { + const rawValue = window.localStorage.getItem(`${baseKeyB}:code`); + if (rawValue === null) { + return null; + } + return rawValue; + })(); + + const localScoreB = (() => { + const rawValue = window.localStorage.getItem(`${baseKeyB}:score`); + if (rawValue === null || rawValue === "") { + return null; + } + return Number(rawValue); + })(); + + const localSubmissionResultB = (() => { + const rawValue = window.localStorage.getItem( + `${baseKeyB}:submissionResult`, + ); + if (rawValue === null) { + return null; + } + const parsed = JSON.parse(rawValue); + if (typeof parsed !== "object") { + return null; + } + return parsed; + })(); + + if (localCodeB !== null) { + data.playerStateB.code = localCodeB; + } + if (localScoreB !== null) { + data.playerStateB.score = localScoreB; + } + if (localSubmissionResultB !== null) { + data.playerStateB.submitResult = localSubmissionResultB; + } + } + + return data; +} +clientLoader.hydrate = true; + +export function HydrateFallback() { + return <GolfWatchAppConnecting />; +} + export default function GolfWatch() { - const { game, sockToken } = useLoaderData<typeof loader>(); - - return ( - <ClientOnly fallback={<GolfWatchAppConnecting />}> - {() => ( - <GolfWatchAppWithAudioPlayRequest game={game} sockToken={sockToken} /> - )} - </ClientOnly> - ); + const { game, sockToken, playerStateA, playerStateB } = + useLoaderData<typeof loader>(); + + useHydrateAtoms([ + [setCurrentTimestampAtom, undefined], + [setDurationSecondsAtom, game.duration_seconds], + [codeAAtom, playerStateA.code], + [codeBAtom, playerStateB.code], + [scoreAAtom, playerStateA.score], + [scoreBAtom, playerStateB.score], + [submitResultAAtom, playerStateA.submitResult], + [submitResultBAtom, playerStateB.submitResult], + ]); + + return <GolfWatchAppWithAudioPlayRequest game={game} sockToken={sockToken} />; } |
