aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/pages
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/pages')
-rw-r--r--frontend/app/pages/DashboardPage.tsx108
-rw-r--r--frontend/app/pages/GolfPlayPage.tsx74
-rw-r--r--frontend/app/pages/GolfWatchPage.tsx78
-rw-r--r--frontend/app/pages/IndexPage.tsx39
-rw-r--r--frontend/app/pages/LoginPage.tsx101
-rw-r--r--frontend/app/pages/TournamentPage.tsx429
6 files changed, 829 insertions, 0 deletions
diff --git a/frontend/app/pages/DashboardPage.tsx b/frontend/app/pages/DashboardPage.tsx
new file mode 100644
index 0000000..c81014d
--- /dev/null
+++ b/frontend/app/pages/DashboardPage.tsx
@@ -0,0 +1,108 @@
+import { useEffect, useState } from "react";
+import { useLocation } from "wouter";
+import { createApiClient } from "../api/client";
+import type { components } from "../api/schema";
+import { getToken } from "../auth";
+import BorderedContainerWithCaption from "../components/BorderedContainerWithCaption";
+import NavigateLink from "../components/NavigateLink";
+import UserIcon from "../components/UserIcon";
+import { APP_NAME, BASE_PATH } from "../config";
+import { useAuth } from "../hooks/useAuth";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+type Game = components["schemas"]["Game"];
+
+export default function DashboardPage() {
+ usePageTitle(`Dashboard | ${APP_NAME}`);
+
+ const { user, logout } = useAuth();
+ const [, navigate] = useLocation();
+
+ const [games, setGames] = useState<Game[]>([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const token = getToken();
+ if (!token) return;
+ const apiClient = createApiClient(token);
+ apiClient
+ .getGames()
+ .then(({ games }) => setGames(games))
+ .finally(() => setLoading(false));
+ }, []);
+
+ function handleLogout() {
+ logout();
+ navigate("/");
+ }
+
+ if (loading) {
+ return (
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center">
+ <p className="text-gray-500">Loading...</p>
+ </div>
+ );
+ }
+
+ return (
+ <div className="p-6 bg-gray-100 min-h-screen flex flex-col items-center gap-4">
+ {user?.icon_path && (
+ <UserIcon
+ iconPath={user.icon_path}
+ displayName={user.display_name}
+ className="w-24 h-24"
+ />
+ )}
+ <h1 className="text-3xl font-bold text-gray-800">{user?.display_name}</h1>
+ <BorderedContainerWithCaption caption="試合一覧">
+ <div className="px-4">
+ {games.length === 0 ? (
+ <p>エントリーできる試合はありません</p>
+ ) : (
+ <ul className="divide-y divide-gray-300">
+ {games.map((game) => (
+ <li
+ key={game.game_id}
+ className="flex justify-between items-center py-2 gap-4"
+ >
+ <div>
+ <span className="font-medium text-gray-800">
+ {game.display_name}
+ </span>
+ </div>
+ <div className="flex gap-2">
+ <NavigateLink to={`/golf/${game.game_id}/play`}>
+ 対戦
+ </NavigateLink>
+ <NavigateLink to={`/golf/${game.game_id}/watch`}>
+ 観戦
+ </NavigateLink>
+ </div>
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ </BorderedContainerWithCaption>
+ <button
+ type="button"
+ onClick={handleLogout}
+ className="px-4 py-2 bg-red-500 text-white rounded-sm transition duration-300 hover:bg-red-700 focus:ring-3 focus:ring-red-400 focus:outline-hidden"
+ >
+ ログアウト
+ </button>
+ {user?.is_admin && (
+ <a
+ href={
+ import.meta.env.DEV
+ ? `http://localhost:8004${BASE_PATH}admin/dashboard`
+ : `${BASE_PATH}admin/dashboard`
+ }
+ className="text-lg text-white bg-sky-600 px-4 py-2 rounded-sm transition duration-300 hover:bg-sky-500 focus:ring-3 focus:ring-sky-400 focus:outline-hidden"
+ >
+ Admin Dashboard
+ </a>
+ )}
+ </div>
+ );
+}
diff --git a/frontend/app/pages/GolfPlayPage.tsx b/frontend/app/pages/GolfPlayPage.tsx
new file mode 100644
index 0000000..3fddbf8
--- /dev/null
+++ b/frontend/app/pages/GolfPlayPage.tsx
@@ -0,0 +1,74 @@
+import { Provider as JotaiProvider, createStore } from "jotai";
+import { useEffect, useMemo, useState } from "react";
+import { useLocation } from "wouter";
+import { ApiClientContext, createApiClient } from "../api/client";
+import type { components } from "../api/schema";
+import { getToken } from "../auth";
+import GolfPlayApp from "../components/GolfPlayApp";
+import { APP_NAME } from "../config";
+import { useAuth } from "../hooks/useAuth";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+type Game = components["schemas"]["Game"];
+type LatestGameState = components["schemas"]["LatestGameState"];
+
+export default function GolfPlayPage({ gameId }: { gameId: string }) {
+ const { user } = useAuth();
+ const [, navigate] = useLocation();
+
+ const [game, setGame] = useState<Game | null>(null);
+ const [gameState, setGameState] = useState<LatestGameState | null>(null);
+ const [loading, setLoading] = useState(true);
+
+ const gameIdNum = Number(gameId);
+
+ usePageTitle(
+ game
+ ? `Golf Playing ${game.display_name} | ${APP_NAME}`
+ : `Golf Playing | ${APP_NAME}`,
+ );
+
+ useEffect(() => {
+ const token = getToken();
+ if (!token) return;
+ const apiClient = createApiClient(token);
+ Promise.all([
+ apiClient.getGame(gameIdNum),
+ apiClient.getGamePlayLatestState(gameIdNum),
+ ])
+ .then(([{ game }, { state }]) => {
+ setGame(game);
+ setGameState(state);
+ })
+ .catch(() => navigate("/dashboard"))
+ .finally(() => setLoading(false));
+ }, [gameIdNum, navigate]);
+
+ const store = useMemo(() => {
+ if (!game || !user) return null;
+ return createStore();
+ }, [game, user]);
+
+ if (loading || !game || !gameState || !user || !store) {
+ return (
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center">
+ <p className="text-gray-500">Loading...</p>
+ </div>
+ );
+ }
+
+ const token = getToken()!;
+
+ return (
+ <JotaiProvider store={store}>
+ <ApiClientContext.Provider value={createApiClient(token)}>
+ <GolfPlayApp
+ key={game.game_id}
+ game={game}
+ player={user}
+ initialGameState={gameState}
+ />
+ </ApiClientContext.Provider>
+ </JotaiProvider>
+ );
+}
diff --git a/frontend/app/pages/GolfWatchPage.tsx b/frontend/app/pages/GolfWatchPage.tsx
new file mode 100644
index 0000000..317f860
--- /dev/null
+++ b/frontend/app/pages/GolfWatchPage.tsx
@@ -0,0 +1,78 @@
+import { Provider as JotaiProvider, createStore } from "jotai";
+import { useEffect, useMemo, useState } from "react";
+import { useLocation } from "wouter";
+import { ApiClientContext, createApiClient } from "../api/client";
+import type { components } from "../api/schema";
+import { getToken } from "../auth";
+import GolfWatchApp from "../components/GolfWatchApp";
+import { APP_NAME } from "../config";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+type Game = components["schemas"]["Game"];
+type LatestGameState = components["schemas"]["LatestGameState"];
+type RankingEntry = components["schemas"]["RankingEntry"];
+
+export default function GolfWatchPage({ gameId }: { gameId: string }) {
+ const [, navigate] = useLocation();
+
+ const [game, setGame] = useState<Game | null>(null);
+ const [ranking, setRanking] = useState<RankingEntry[]>([]);
+ const [gameStates, setGameStates] = useState<{
+ [key: string]: LatestGameState;
+ }>({});
+ const [loading, setLoading] = useState(true);
+
+ const gameIdNum = Number(gameId);
+
+ usePageTitle(
+ game
+ ? `Golf Watching ${game.display_name} | ${APP_NAME}`
+ : `Golf Watching | ${APP_NAME}`,
+ );
+
+ useEffect(() => {
+ const token = getToken();
+ if (!token) return;
+ const apiClient = createApiClient(token);
+ Promise.all([
+ apiClient.getGame(gameIdNum),
+ apiClient.getGameWatchRanking(gameIdNum),
+ apiClient.getGameWatchLatestStates(gameIdNum),
+ ])
+ .then(([{ game }, { ranking }, { states }]) => {
+ setGame(game);
+ setRanking(ranking);
+ setGameStates(states);
+ })
+ .catch(() => navigate("/dashboard"))
+ .finally(() => setLoading(false));
+ }, [gameIdNum, navigate]);
+
+ const store = useMemo(() => {
+ if (!game) return null;
+ return createStore();
+ }, [game]);
+
+ if (loading || !game || !store) {
+ return (
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center">
+ <p className="text-gray-500">Loading...</p>
+ </div>
+ );
+ }
+
+ const token = getToken()!;
+
+ return (
+ <JotaiProvider store={store}>
+ <ApiClientContext.Provider value={createApiClient(token)}>
+ <GolfWatchApp
+ key={game.game_id}
+ game={game}
+ initialGameStates={gameStates}
+ initialRanking={ranking}
+ />
+ </ApiClientContext.Provider>
+ </JotaiProvider>
+ );
+}
diff --git a/frontend/app/pages/IndexPage.tsx b/frontend/app/pages/IndexPage.tsx
new file mode 100644
index 0000000..088cdc5
--- /dev/null
+++ b/frontend/app/pages/IndexPage.tsx
@@ -0,0 +1,39 @@
+import BorderedContainer from "../components/BorderedContainer";
+import NavigateLink from "../components/NavigateLink";
+import { APP_NAME, BASE_PATH } from "../config";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+export default function IndexPage() {
+ usePageTitle(APP_NAME);
+
+ return (
+ <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center gap-y-6">
+ <img
+ src={`${BASE_PATH}logo.svg`}
+ alt="iOSDC Japan 2025"
+ className="w-96 h-auto"
+ />
+ <div className="text-center">
+ <div className="font-bold text-transparent bg-clip-text bg-iosdc-japan">
+ <div className="text-6xl">Swift Code Battle</div>
+ </div>
+ </div>
+ <div className="mx-2">
+ <BorderedContainer>
+ <p className="text-gray-900 max-w-prose">
+ Swift コードバトルは指示された動作をする Swift
+ コードをより短く書けた方が勝ち、という 1 対 1
+ の対戦コンテンツです。9/6
+ に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での
+ コードバトルを 9/19 (金) day0
+ に実施します。ここでは短いコードが正義です!
+ 可読性も保守性も放り投げた、イベントならではのコードをお楽しみください!
+ </p>
+ </BorderedContainer>
+ </div>
+ <div>
+ <NavigateLink to="/login">ログイン</NavigateLink>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/pages/LoginPage.tsx b/frontend/app/pages/LoginPage.tsx
new file mode 100644
index 0000000..c10b819
--- /dev/null
+++ b/frontend/app/pages/LoginPage.tsx
@@ -0,0 +1,101 @@
+import { type FormEvent, useState } from "react";
+import { useLocation } from "wouter";
+import BorderedContainer from "../components/BorderedContainer";
+import InputText from "../components/InputText";
+import SubmitButton from "../components/SubmitButton";
+import { APP_NAME } from "../config";
+import { useAuth } from "../hooks/useAuth";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+export default function LoginPage() {
+ usePageTitle(`Login | ${APP_NAME}`);
+
+ const { login } = useAuth();
+ const [, navigate] = useLocation();
+
+ const [error, setError] = useState<string | null>(null);
+ const [fieldErrors, setFieldErrors] = useState<{
+ username?: string;
+ password?: string;
+ }>({});
+ const [submitting, setSubmitting] = useState(false);
+
+ async function handleSubmit(e: FormEvent<HTMLFormElement>) {
+ e.preventDefault();
+ const formData = new FormData(e.currentTarget);
+ const username = String(formData.get("username"));
+ const password = String(formData.get("password"));
+
+ const errors: { username?: string; password?: string } = {};
+ if (username === "") errors.username = "ユーザー名を入力してください";
+ if (password === "") errors.password = "パスワードを入力してください";
+ if (Object.keys(errors).length > 0) {
+ setFieldErrors(errors);
+ setError("ユーザー名またはパスワードが誤っています");
+ return;
+ }
+
+ setSubmitting(true);
+ setError(null);
+ setFieldErrors({});
+
+ try {
+ await login(username, password);
+ navigate("/dashboard");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "ログインに失敗しました");
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center">
+ <div className="mx-2">
+ <BorderedContainer>
+ <form onSubmit={handleSubmit} className="w-full max-w-sm p-2">
+ <h2 className="text-2xl mb-6 text-center">
+ fortee アカウントでログイン
+ </h2>
+ {error && <p className="text-sky-500 text-sm mb-4">{error}</p>}
+ <div className="mb-4 flex flex-col gap-1">
+ <label
+ htmlFor="username"
+ className="block text-sm font-medium text-gray-700"
+ >
+ ユーザー名
+ </label>
+ <InputText type="text" name="username" id="username" required />
+ {fieldErrors.username && (
+ <p className="text-red-500 text-sm">{fieldErrors.username}</p>
+ )}
+ </div>
+ <div className="mb-6 flex flex-col gap-1">
+ <label
+ htmlFor="password"
+ className="block text-sm font-medium text-gray-700"
+ >
+ パスワード
+ </label>
+ <InputText
+ type="password"
+ name="password"
+ id="password"
+ autoComplete="current-password"
+ required
+ />
+ {fieldErrors.password && (
+ <p className="text-red-500 text-sm">{fieldErrors.password}</p>
+ )}
+ </div>
+ <div className="flex justify-center">
+ <SubmitButton type="submit" disabled={submitting}>
+ {submitting ? "ログイン中..." : "ログイン"}
+ </SubmitButton>
+ </div>
+ </form>
+ </BorderedContainer>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/pages/TournamentPage.tsx b/frontend/app/pages/TournamentPage.tsx
new file mode 100644
index 0000000..43ea790
--- /dev/null
+++ b/frontend/app/pages/TournamentPage.tsx
@@ -0,0 +1,429 @@
+import { useEffect, useState } from "react";
+import { createApiClient } from "../api/client";
+import type { components } from "../api/schema";
+import { getToken } from "../auth";
+import BorderedContainer from "../components/BorderedContainer";
+import UserIcon from "../components/UserIcon";
+import { APP_NAME } from "../config";
+import { usePageTitle } from "../hooks/usePageTitle";
+
+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;
+ if (match.player2?.user_id === playerID) return match.player2;
+ 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;
+ return null;
+}
+
+function getBorderColor(match: TournamentMatch, playerIDs: number[]): string {
+ if (!match.winner) {
+ return "border-black";
+ }
+ if (playerIDs.includes(match.winner)) {
+ return "border-pink-700";
+ }
+ return "border-gray-400";
+}
+
+export default function TournamentPage() {
+ usePageTitle(`Tournament | ${APP_NAME}`);
+
+ const [tournament, setTournament] = useState<{
+ matches: TournamentMatch[];
+ } | null>(null);
+ const [playerIDs, setPlayerIDs] = useState<number[]>([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const game1 = Number(params.get("game1"));
+ const game2 = Number(params.get("game2"));
+ const game3 = Number(params.get("game3"));
+ const game4 = Number(params.get("game4"));
+ const game5 = Number(params.get("game5"));
+
+ if (!game1 || !game2 || !game3 || !game4 || !game5) {
+ setError("Missing or invalid game parameters");
+ setLoading(false);
+ return;
+ }
+
+ const pIDs = [
+ Number(params.get("player1")),
+ Number(params.get("player2")),
+ Number(params.get("player3")),
+ Number(params.get("player4")),
+ Number(params.get("player5")),
+ Number(params.get("player6")),
+ ];
+
+ if (pIDs.some((id) => !id)) {
+ setError("Missing or invalid player parameters");
+ setLoading(false);
+ return;
+ }
+
+ setPlayerIDs(pIDs);
+
+ const token = getToken();
+ if (!token) return;
+ const apiClient = createApiClient(token);
+ apiClient
+ .getTournament(game1, game2, game3, game4, game5)
+ .then(({ tournament }) => setTournament(tournament))
+ .catch(() => setError("Failed to load tournament"))
+ .finally(() => setLoading(false));
+ }, []);
+
+ if (loading) {
+ return (
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center">
+ <p className="text-gray-500">Loading...</p>
+ </div>
+ );
+ }
+
+ if (error || !tournament) {
+ return (
+ <div className="min-h-screen bg-gray-100 flex items-center justify-center">
+ <p className="text-red-500">{error || "Failed to load tournament"}</p>
+ </div>
+ );
+ }
+
+ 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-5xl 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>
+ );
+}