diff options
Diffstat (limited to 'frontend/app/routes')
| -rw-r--r-- | frontend/app/routes/_index.tsx | 38 | ||||
| -rw-r--r-- | frontend/app/routes/dashboard.tsx | 27 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.play.tsx | 114 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.watch.tsx | 189 | ||||
| -rw-r--r-- | frontend/app/routes/login.tsx | 15 |
5 files changed, 92 insertions, 291 deletions
diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx index 808302d..06cca78 100644 --- a/frontend/app/routes/_index.tsx +++ b/frontend/app/routes/_index.tsx @@ -1,10 +1,10 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link } from "@remix-run/react"; import { ensureUserNotLoggedIn } from "../.server/auth"; import BorderedContainer from "../components/BorderedContainer"; +import NavigateLink from "../components/NavigateLink"; export const meta: MetaFunction = () => [ - { title: "iOSDC Japan 2024 Albatross.swift" }, + { title: "PHPerKaigi 2025 Albatross" }, ]; export async function loader({ request }: LoaderFunctionArgs) { @@ -14,41 +14,31 @@ export async function loader({ request }: LoaderFunctionArgs) { export default function Index() { return ( - <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center gap-y-6"> + <div className="min-h-screen bg-sky-600 flex flex-col items-center justify-center gap-y-6"> <img - src="/iosdc-japan/2024/code-battle/favicon.svg" - alt="iOSDC Japan 2024" - className="w-24 h-24" + src="/phperkaigi/2025/code-battle/logo.svg" + alt="PHPerKaigi 2025" + className="w-64 h-64" /> <div className="text-center"> - <div className="font-bold text-transparent bg-clip-text bg-iosdc-japan flex flex-col gap-y-2"> - <div className="text-3xl">iOSDC Japan 2024</div> - <div className="text-6xl">Swift Code Battle</div> + <div className="font-bold text-sky-50 flex flex-col gap-y-2"> + <div className="text-5xl">PHPER CODE BATTLE</div> </div> </div> <div className="mx-2"> <BorderedContainer> <p className="text-gray-900 max-w-prose"> - Swift コードバトルは指示された動作をする Swift + PHPer コードバトルは指示された動作をする PHP コードをより短く書けた方が勝ち、という 1 対 1 - の対戦コンテンツです。8/22(木)day0 前夜祭では 8/12 + の対戦コンテンツです。3/21(金)day0 前夜祭では 3/8 に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での - Swift - コードバトルを実施します。ここでは短いコードが正義です!可読性も保守性も放り投げた、イベントならではのコードをお楽しみください! + PHPer コードバトルを実施します。 + ここでは短いコードが正義です!可読性も保守性も放り投げた、イベントならではのコードをお楽しみください! </p> </BorderedContainer> </div> - <div className="mt-4"> - <p className="mb-2"> - 追記: - イベントは終了しました。ご参加いただいたみなさま、ありがとうございました! - </p> - <Link - to="https://blog.iosdc.jp/2024/08/30/iosdc-japan-2024-swift-code-battle/" - className="underline underline-offset-4 text-pink-600 hover:text-pink-500 transition duration-300" - > - 試合結果はこちらのスタッフブログで公開しています。 - </Link> + <div> + <NavigateLink to="/login">ログイン</NavigateLink> </div> </div> ); diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx index 3a50757..08461a5 100644 --- a/frontend/app/routes/dashboard.tsx +++ b/frontend/app/routes/dashboard.tsx @@ -1,13 +1,13 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Form, useLoaderData } from "@remix-run/react"; -import { apiGetGames } from "../.server/api/client"; import { ensureUserLoggedIn } from "../.server/auth"; +import { apiGetGames } from "../api/client"; import BorderedContainer from "../components/BorderedContainer"; import NavigateLink from "../components/NavigateLink"; import UserIcon from "../components/UserIcon"; export const meta: MetaFunction = () => [ - { title: "Dashboard | iOSDC Japan 2024 Albatross.swift" }, + { title: "Dashboard | PHPerKaigi 2025 Albatross" }, ]; export async function loader({ request }: LoaderFunctionArgs) { @@ -39,7 +39,7 @@ export default function Dashboard() { <BorderedContainer> <div className="px-4"> {games.length === 0 ? ( - <p>エントリーしている試合はありません</p> + <p>エントリーできる試合はありません</p> ) : ( <ul className="divide-y"> {games.map((game) => ( @@ -58,15 +58,12 @@ export default function Dashboard() { </span> </div> <span> - {game.state === "closed" || game.state === "finished" ? ( - <span className="text-lg text-gray-400 bg-gray-200 px-4 py-2 rounded"> - 入室 - </span> - ) : ( - <NavigateLink to={`/golf/${game.game_id}/play`}> - 入室 - </NavigateLink> - )} + <NavigateLink to={`/golf/${game.game_id}/play`}> + 対戦 + </NavigateLink> + <NavigateLink to={`/golf/${game.game_id}/watch`}> + 観戦 + </NavigateLink> </span> </li> ))} @@ -86,10 +83,10 @@ export default function Dashboard() { <a href={ process.env.NODE_ENV === "development" - ? "http://localhost:8002/iosdc-japan/2024/code-battle/admin/dashboard" - : "/iosdc-japan/2024/code-battle/admin/dashboard" + ? "http://localhost:8003/phperkaigi/2025/code-battle/admin/dashboard" + : "/phperkaigi/2025/code-battle/admin/dashboard" } - className="text-lg text-white bg-pink-600 px-4 py-2 rounded transition duration-300 hover:bg-pink-500 focus:ring focus:ring-pink-400 focus:outline-none" + className="text-lg text-white bg-sky-600 px-4 py-2 rounded transition duration-300 hover:bg-sky-500 focus:ring focus:ring-sky-400 focus:outline-none" > Admin Dashboard </a> diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx index a2860dd..e523187 100644 --- a/frontend/app/routes/golf.$gameId.play.tsx +++ b/frontend/app/routes/golf.$gameId.play.tsx @@ -1,128 +1,64 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { ClientLoaderFunctionArgs, useLoaderData } from "@remix-run/react"; +import { 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, + ApiAuthTokenContext, + apiGetGame, + apiGetGamePlayLatestState, +} from "../api/client"; +import GolfPlayApp from "../components/GolfPlayApp"; +import { setCurrentTimestampAtom, setDurationSecondsAtom, - submitResultAtom, + setGameStartedAtAtom, + setLatestGameStateAtom, } from "../states/play"; -import { PlayerState } from "../types/PlayerState"; export const meta: MetaFunction<typeof loader> = ({ data }) => [ { title: data - ? `Golf Playing ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift` - : "Golf Playing | iOSDC Japan 2024 Albatross.swift", + ? `Golf Playing ${data.game.display_name} | PHPerKaigi 2025 Albatross` + : "Golf Playing | PHPerKaigi 2025 Albatross", }, ]; export async function loader({ params, request }: LoaderFunctionArgs) { const { token, user } = await ensureUserLoggedIn(request); + const gameId = Number(params.gameId); + const fetchGame = async () => { - return (await apiGetGame(token, Number(params.gameId))).game; + return (await apiGetGame(token, gameId)).game; }; - const fetchSockToken = async () => { - return (await apiGetToken(token)).token; + const fetchGameState = async () => { + return (await apiGetGamePlayLatestState(token, gameId)).state; }; - 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: "", - })), - }, - }; + const [game, state] = await Promise.all([fetchGame(), fetchGameState()]); return { + apiAuthToken: token, game, player: user, - sockToken, - playerState, + gameState: state, }; } -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, playerState } = + const { apiAuthToken, game, player, gameState } = useLoaderData<typeof loader>(); useHydrateAtoms([ [setCurrentTimestampAtom, undefined], [setDurationSecondsAtom, game.duration_seconds], - [scoreAtom, playerState.score], - [submitResultAtom, playerState.submitResult], + [setGameStartedAtAtom, game.started_at ?? null], + [setLatestGameStateAtom, gameState], ]); return ( - <GolfPlayApp - game={game} - player={player} - initialCode={playerState.code} - sockToken={sockToken} - /> + <ApiAuthTokenContext.Provider value={apiAuthToken}> + <GolfPlayApp game={game} player={player} initialCode={gameState.code} /> + </ApiAuthTokenContext.Provider> ); } diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx index f04f6b0..fed06aa 100644 --- a/frontend/app/routes/golf.$gameId.watch.tsx +++ b/frontend/app/routes/golf.$gameId.watch.tsx @@ -1,187 +1,74 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { ClientLoaderFunctionArgs, useLoaderData } from "@remix-run/react"; +import { 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, + ApiAuthTokenContext, + apiGetGame, + apiGetGameWatchLatestStates, + apiGetGameWatchRanking, +} from "../api/client"; +import GolfWatchApp from "../components/GolfWatchApp"; +import { + rankingAtom, setCurrentTimestampAtom, setDurationSecondsAtom, - submitResultAAtom, - submitResultBAtom, + setGameStartedAtAtom, + setLatestGameStatesAtom, } from "../states/watch"; -import { PlayerState } from "../types/PlayerState"; export const meta: MetaFunction<typeof loader> = ({ data }) => [ { title: data - ? `Golf Watching ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift` - : "Golf Watching | iOSDC Japan 2024 Albatross.swift", + ? `Golf Watching ${data.game.display_name} | PHPerKaigi 2025 Albatross` + : "Golf Watching | PHPerKaigi 2025 Albatross", }, ]; export async function loader({ params, request }: LoaderFunctionArgs) { const { token } = await ensureUserLoggedIn(request); + const gameId = Number(params.gameId); + const fetchGame = async () => { - return (await apiGetGame(token, Number(params.gameId))).game; + return (await apiGetGame(token, gameId)).game; }; - const fetchSockToken = async () => { - return (await apiGetToken(token)).token; + const fetchRanking = async () => { + return (await apiGetGameWatchRanking(token, gameId)).ranking; }; - - const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]); - - if (game.game_type !== "1v1") { - 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 fetchGameStates = async () => { + return (await apiGetGameWatchLatestStates(token, gameId)).states; }; - const playerStateB = structuredClone(playerStateA); + + const [game, ranking, gameStates] = await Promise.all([ + fetchGame(), + fetchRanking(), + fetchGameStates(), + ]); return { + apiAuthToken: token, game, - sockToken, - playerStateA, - playerStateB, + ranking, + gameStates, }; } -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, playerStateA, playerStateB } = + const { apiAuthToken, game, ranking, gameStates } = useLoaderData<typeof loader>(); useHydrateAtoms([ + [rankingAtom, ranking], [setCurrentTimestampAtom, undefined], [setDurationSecondsAtom, game.duration_seconds], - [codeAAtom, playerStateA.code], - [codeBAtom, playerStateB.code], - [scoreAAtom, playerStateA.score], - [scoreBAtom, playerStateB.score], - [submitResultAAtom, playerStateA.submitResult], - [submitResultBAtom, playerStateB.submitResult], + [setGameStartedAtAtom, game.started_at ?? null], + [setLatestGameStatesAtom, gameStates], ]); - return <GolfWatchAppWithAudioPlayRequest game={game} sockToken={sockToken} />; + return ( + <ApiAuthTokenContext.Provider value={apiAuthToken}> + <GolfWatchApp game={game} /> + </ApiAuthTokenContext.Provider> + ); } diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx index b1249e0..5ca6217 100644 --- a/frontend/app/routes/login.tsx +++ b/frontend/app/routes/login.tsx @@ -3,14 +3,14 @@ import type { LoaderFunctionArgs, MetaFunction, } from "@remix-run/node"; -import { Form, json, useActionData, useLocation } from "@remix-run/react"; +import { Form, json, useActionData } from "@remix-run/react"; import { ensureUserNotLoggedIn, login } from "../.server/auth"; import BorderedContainer from "../components/BorderedContainer"; import InputText from "../components/InputText"; import SubmitButton from "../components/SubmitButton"; export const meta: MetaFunction = () => [ - { title: "Login | iOSDC Japan 2024 Albatross.swift" }, + { title: "Login | PHPerKaigi 2025 Albatross" }, ]; export async function loader({ request }: LoaderFunctionArgs) { @@ -58,10 +58,6 @@ export async function action({ request }: ActionFunctionArgs) { } export default function Login() { - const location = useLocation(); - const searchParams = new URLSearchParams(location.search); - const registrationToken = searchParams.get("registration_token"); - const loginErrors = useActionData<typeof action>(); return ( @@ -77,7 +73,7 @@ export default function Login() { のアカウントをお持ちでない場合は、イベントスタッフにお声がけください。 </p> {loginErrors?.message && ( - <p className="text-red-500 text-sm mb-4">{loginErrors.message}</p> + <p className="text-sky-500 text-sm mb-4">{loginErrors.message}</p> )} <div className="mb-4 flex flex-col gap-1"> <label @@ -113,11 +109,6 @@ export default function Login() { </p> )} </div> - <input - type="hidden" - name="registration_token" - value={registrationToken ?? ""} - /> <div className="flex justify-center"> <SubmitButton type="submit">ログイン</SubmitButton> </div> |
