aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/api/client.ts17
-rw-r--r--frontend/app/api/schema.d.ts65
-rw-r--r--frontend/app/components/Gaming/Score.tsx4
-rw-r--r--frontend/app/routes/tournament.tsx441
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>
+ );
+}