diff options
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/app/api/client.ts | 17 | ||||
| -rw-r--r-- | frontend/app/api/schema.d.ts | 65 | ||||
| -rw-r--r-- | frontend/app/components/Gaming/Score.tsx | 4 | ||||
| -rw-r--r-- | frontend/app/routes/tournament.tsx | 441 |
4 files changed, 525 insertions, 2 deletions
diff --git a/frontend/app/api/client.ts b/frontend/app/api/client.ts index 10dc7ef..6b7ce80 100644 --- a/frontend/app/api/client.ts +++ b/frontend/app/api/client.ts @@ -107,6 +107,23 @@ class AuthenticatedApiClient { return data; } + async getTournament( + game1: number, + game2: number, + game3: number, + game4: number, + game5: number, + ) { + const { data, error } = await client.GET("/tournament", { + params: { + header: this._getAuthorizationHeader(), + query: { game1, game2, game3, game4, game5 }, + }, + }); + if (error) throw new Error(error.message); + return data; + } + _getAuthorizationHeader() { return { Authorization: `Bearer ${this.token}` }; } diff --git a/frontend/app/api/schema.d.ts b/frontend/app/api/schema.d.ts index b58b27f..04bfc10 100644 --- a/frontend/app/api/schema.d.ts +++ b/frontend/app/api/schema.d.ts @@ -140,6 +140,23 @@ export interface paths { patch?: never; trace?: never; }; + "/tournament": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get tournament bracket data */ + get: operations["getTournament"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record<string, never>; export interface components { @@ -219,6 +236,21 @@ export interface components { /** @example echo 'hello world'; */ code: string | null; }; + Tournament: { + matches: components["schemas"]["TournamentMatch"][]; + }; + TournamentMatch: { + /** @example 1 */ + game_id: number; + player1?: components["schemas"]["User"]; + player2?: components["schemas"]["User"]; + /** @example 1 */ + player1_score?: number; + /** @example 1 */ + player2_score?: number; + /** @example 1 */ + winner?: number; + }; }; responses: { /** @description Bad request */ @@ -509,4 +541,37 @@ export interface operations { 404: components["responses"]["NotFound"]; }; }; + getTournament: { + parameters: { + query: { + game1: number; + game2: number; + game3: number; + game4: number; + game5: number; + }; + header: { + Authorization: components["parameters"]["header_authorization"]; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Tournament data */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + tournament: components["schemas"]["Tournament"]; + }; + }; + }; + 401: components["responses"]["Unauthorized"]; + 403: components["responses"]["Forbidden"]; + 404: components["responses"]["NotFound"]; + }; + }; } diff --git a/frontend/app/components/Gaming/Score.tsx b/frontend/app/components/Gaming/Score.tsx index 9b6283f..b4a415c 100644 --- a/frontend/app/components/Gaming/Score.tsx +++ b/frontend/app/components/Gaming/Score.tsx @@ -13,8 +13,8 @@ export default function Score({ status, score }: Props) { if (status === "running") { intervalId = setInterval(() => { - const maxValue = Math.pow(10, String(score).length) - 1; - const minValue = Math.pow(10, String(score).length - 1); + const maxValue = Math.pow(10, String(score ?? 100).length) - 1; + const minValue = Math.pow(10, String(score ?? 100).length - 1); const randomValue = Math.floor(Math.random() * (maxValue - minValue + 1)) + minValue; setDisplayScore(randomValue); diff --git a/frontend/app/routes/tournament.tsx b/frontend/app/routes/tournament.tsx new file mode 100644 index 0000000..1e1aa08 --- /dev/null +++ b/frontend/app/routes/tournament.tsx @@ -0,0 +1,441 @@ +import type { LoaderFunctionArgs, MetaFunction } from "react-router"; +import { useLoaderData } from "react-router"; +import { ensureUserLoggedIn } from "../.server/auth"; +import { createApiClient } from "../api/client"; +import type { components } from "../api/schema"; +import BorderedContainer from "../components/BorderedContainer"; +import UserIcon from "../components/UserIcon"; + +export const meta: MetaFunction = () => [ + { title: "Tournament | iOSDC Japan 2025 Albatross" }, +]; + +export async function loader({ request }: LoaderFunctionArgs) { + const { token } = await ensureUserLoggedIn(request); + const apiClient = createApiClient(token); + + const url = new URL(request.url); + const game1Param = url.searchParams.get("game1"); + const game2Param = url.searchParams.get("game2"); + const game3Param = url.searchParams.get("game3"); + const game4Param = url.searchParams.get("game4"); + const game5Param = url.searchParams.get("game5"); + const player1Param = url.searchParams.get("player1"); + const player2Param = url.searchParams.get("player2"); + const player3Param = url.searchParams.get("player3"); + const player4Param = url.searchParams.get("player4"); + const player5Param = url.searchParams.get("player5"); + const player6Param = url.searchParams.get("player6"); + + if (!game1Param || !game2Param || !game3Param || !game4Param || !game5Param) { + throw new Response( + "Missing required query parameters: game1, game2, game3, game4, game5", + { + status: 400, + }, + ); + } + if ( + !player1Param || + !player2Param || + !player3Param || + !player4Param || + !player5Param || + !player6Param + ) { + throw new Response( + "Missing required query parameters: player1, player2, player3, player4, player5, player6", + { + status: 400, + }, + ); + } + + const game1 = Number(game1Param); + const game2 = Number(game2Param); + const game3 = Number(game3Param); + const game4 = Number(game4Param); + const game5 = Number(game5Param); + + if (!game1 || !game2 || !game3 || !game4 || !game5) { + throw new Response("Invalid game IDs: must be positive integers", { + status: 400, + }); + } + + const { tournament } = await apiClient.getTournament( + game1, + game2, + game3, + game4, + game5, + ); + return { + tournament, + playerIDs: [ + Number(player1Param), + Number(player2Param), + Number(player3Param), + Number(player4Param), + Number(player5Param), + Number(player6Param), + ], + }; +} + +type TournamentMatch = components["schemas"]["TournamentMatch"]; +type User = components["schemas"]["User"]; + +function Player({ player, rank }: { player: User | null; rank: number }) { + return ( + <BorderedContainer> + <div className="flex flex-col items-center gap-2"> + <span className="text-gray-800 text-md">予選 {rank} 位</span> + <span className="font-medium text-lg">{player?.display_name}</span> + {player?.icon_path && ( + <UserIcon + iconPath={player.icon_path} + displayName={player.display_name} + className="w-16 h-16 my-auto" + /> + )} + </div> + </BorderedContainer> + ); +} + +function BranchVL({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div></div> + <div className={`border-l-4 ${className}`}></div> + </div> + ); +} + +function BranchVR({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div className={`border-r-4 ${className}`}></div> + <div></div> + </div> + ); +} + +function BranchVL2({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-3"> + <div className={`border-r-4 ${className}`}></div> + <div className={`border-t-4 p-2 font-bold text-xl ${className}`}> + {score} + </div> + <div className={`border-t-4 ${className}`}></div> + </div> + ); +} + +function BranchVR2({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-3"> + <div className={`border-t-4 ${className}`}></div> + <div className={`border-t-4 p-2 font-bold text-xl ${className}`}> + {score} + </div> + <div className={`border-l-4 ${className}`}></div> + </div> + ); +} + +function BranchV3({ className = "" }: { className?: string }) { + return <div className={`border-r-4 ${className}`}></div>; +} + +function BranchH({ + score, + className1, + className2, + className3, +}: { + score?: number | null; + className1: string; + className2: string; + className3: string; +}) { + return ( + <div className="grid grid-cols-3"> + <div className={`border-t-4 ${className1}`}></div> + <div className={`border-t-4 ${className2}`}></div> + <div className={`border-t-4 p-2 font-bold text-xl ${className3}`}> + {score} + </div> + </div> + ); +} + +function BranchH2({ + score, + className1, + className2, + className3, +}: { + score?: number | null; + className1: string; + className2: string; + className3: string; +}) { + return ( + <div className="grid grid-cols-3"> + <div + className={`border-t-4 p-2 font-bold text-xl text-right ${className1}`} + > + {score} + </div> + <div className={`border-t-4 ${className2}`}></div> + <div className={`border-t-4 ${className3}`}></div> + </div> + ); +} + +function BranchL({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div></div> + <div + className={`border-l-4 border-t-4 p-2 font-bold text-xl ${className}`} + > + {score} + </div> + </div> + ); +} + +function BranchR({ + score, + className = "", +}: { score: number | null; className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div + className={`border-r-4 border-t-4 p-2 font-bold text-xl text-right ${className}`} + > + {score} + </div> + <div></div> + </div> + ); +} + +function BranchL2({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div className={`border-l-4 ${className}`}></div> + <div></div> + </div> + ); +} + +function BranchR2({ className = "" }: { className?: string }) { + return ( + <div className="grid grid-cols-2"> + <div></div> + <div className={`border-r-4 ${className}`}></div> + </div> + ); +} + +function getPlayer(match: TournamentMatch, playerID: number): User | null { + if (match.player1?.user_id === playerID) return match.player1; + else if (match.player2?.user_id === playerID) return match.player2; + else return null; +} + +function getScore(match: TournamentMatch, playerIDs: number[]): number | null { + if (match.player1 && playerIDs.includes(match.player1.user_id)) + return match.player1_score ?? null; + if (match.player2 && playerIDs.includes(match.player2.user_id)) + return match.player2_score ?? null; + else return null; +} + +function getBorderColor(match: TournamentMatch, playerIDs: number[]): string { + if (!match.winner) { + return "border-black"; + } else if (playerIDs.includes(match.winner)) { + return "border-pink-700"; + } else { + return "border-gray-400"; + } +} + +export default function Tournament() { + const { tournament, playerIDs } = useLoaderData<typeof loader>(); + + const match1 = tournament.matches[0]!; + const match2 = tournament.matches[1]!; + const match3 = tournament.matches[2]!; + const match4 = tournament.matches[3]!; + const match5 = tournament.matches[4]!; + + const playerID1 = playerIDs[0]!; + const playerID2 = playerIDs[1]!; + const playerID3 = playerIDs[2]!; + const playerID4 = playerIDs[3]!; + const playerID5 = playerIDs[4]!; + const playerID6 = playerIDs[5]!; + + const player5 = getPlayer(match1, playerID5); + const player4 = getPlayer(match1, playerID4); + const player3 = getPlayer(match2, playerID3); + const player6 = getPlayer(match2, playerID6); + const player1 = getPlayer(match3, playerID1); + const player2 = getPlayer(match4, playerID2); + + return ( + <div className="p-6 bg-gray-100 min-h-screen"> + <div className="max-w-4xl mx-auto"> + <h1 className="text-3xl font-bold text-transparent bg-clip-text bg-iosdc-japan text-center mb-8"> + iOSDC Japan 2025 Swift Code Battle + </h1> + + <div className="grid grid-rows-5"> + <div className="grid grid-cols-6"> + <div></div> + <div></div> + <BranchV3 + className={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + playerID3, + playerID6, + playerID2, + ])} + /> + <div></div> + <div></div> + <div></div> + </div> + <div className="grid grid-cols-6"> + <div></div> + <BranchVL2 + score={getScore(match5, [playerID1, playerID5, playerID4])} + className={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + /> + <BranchH + className1={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + className2={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + className3={getBorderColor(match5, [ + playerID1, + playerID5, + playerID4, + ])} + /> + <BranchH + className1={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + className2={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + className3={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + /> + <BranchVR2 + score={getScore(match5, [playerID3, playerID6, playerID2])} + className={getBorderColor(match5, [ + playerID3, + playerID6, + playerID2, + ])} + /> + <div></div> + </div> + <div className="grid grid-cols-6"> + <BranchL + score={getScore(match3, [playerID1])} + className={getBorderColor(match3, [playerID1])} + /> + <BranchH + score={getScore(match3, [playerID5, playerID4])} + className1={getBorderColor(match3, [playerID1])} + className2={getBorderColor(match3, [playerID5, playerID4])} + className3={getBorderColor(match3, [playerID5, playerID4])} + /> + <BranchL2 + className={getBorderColor(match3, [playerID5, playerID4])} + /> + <BranchR2 + className={getBorderColor(match4, [playerID3, playerID6])} + /> + <BranchH2 + score={getScore(match4, [playerID3, playerID6])} + className1={getBorderColor(match4, [playerID3, playerID6])} + className2={getBorderColor(match4, [playerID3, playerID6])} + className3={getBorderColor(match4, [playerID2])} + /> + <BranchR + score={getScore(match4, [playerID2])} + className={getBorderColor(match4, [playerID2])} + /> + </div> + <div className="grid grid-cols-6"> + <BranchVL className={getBorderColor(match3, [playerID1])} /> + <BranchL + score={getScore(match1, [playerID5])} + className={getBorderColor(match1, [playerID5])} + /> + <BranchR + score={getScore(match1, [playerID4])} + className={getBorderColor(match1, [playerID4])} + /> + <BranchL + score={getScore(match2, [playerID3])} + className={getBorderColor(match2, [playerID3])} + /> + <BranchR + score={getScore(match2, [playerID6])} + className={getBorderColor(match2, [playerID6])} + /> + <BranchVR className={getBorderColor(match4, [playerID2])} /> + </div> + <div className="grid grid-cols-6 gap-6"> + <Player player={player1} rank={1} /> + <Player player={player5} rank={5} /> + <Player player={player4} rank={4} /> + <Player player={player3} rank={3} /> + <Player player={player6} rank={6} /> + <Player player={player2} rank={2} /> + </div> + </div> + </div> + </div> + ); +} |
