aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/routes
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/routes')
-rw-r--r--frontend/app/routes/_index.tsx38
-rw-r--r--frontend/app/routes/dashboard.tsx27
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx114
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx189
-rw-r--r--frontend/app/routes/login.tsx15
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>