diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-13 22:40:45 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-13 23:07:26 +0900 |
| commit | e239fe743fc66a8712cf9886d3dfed3cc41fce36 (patch) | |
| tree | e3452fb13dce114cea0e8371dbb049118aa1229e /frontend/app/routes | |
| parent | 482c3a52a0fcc5870a7db4a190475caf61b211a3 (diff) | |
| download | phperkaigi-2026-albatross-e239fe743fc66a8712cf9886d3dfed3cc41fce36.tar.gz phperkaigi-2026-albatross-e239fe743fc66a8712cf9886d3dfed3cc41fce36.tar.zst phperkaigi-2026-albatross-e239fe743fc66a8712cf9886d3dfed3cc41fce36.zip | |
refactor(frontend): replace React Router BFF with Wouter SPA
Remove React Router 7 SSR/BFF architecture (server-side loaders,
actions, sessions, remix-auth) and replace with a client-side SPA
using Wouter for routing and cookie-based JWT auth.
- Replace reactRouter() Vite plugin with @vitejs/plugin-react
- Add index.html + app/main.tsx as SPA entry points
- Add Wouter routing with auth guards (ProtectedRoute/PublicOnlyRoute)
- Add client-side auth (app/auth.ts) and useAuth hook
- Migrate all route files to app/pages/ with client-side data fetching
- Update NavigateLink and GolfPlayAppGaming to use Wouter Link
- Remove .server/, routes/, root.tsx, react-router.config.ts
- Clean up tsconfig.json (remove .react-router references)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'frontend/app/routes')
| -rw-r--r-- | frontend/app/routes/_index.tsx | 45 | ||||
| -rw-r--r-- | frontend/app/routes/dashboard.tsx | 88 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.play.tsx | 62 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.watch.tsx | 63 | ||||
| -rw-r--r-- | frontend/app/routes/login.tsx | 115 | ||||
| -rw-r--r-- | frontend/app/routes/logout.tsx | 7 | ||||
| -rw-r--r-- | frontend/app/routes/tournament.tsx | 440 |
7 files changed, 0 insertions, 820 deletions
diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx deleted file mode 100644 index 207b175..0000000 --- a/frontend/app/routes/_index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { LoaderFunctionArgs, MetaFunction } from "react-router"; -import { ensureUserNotLoggedIn } from "../.server/auth"; -import BorderedContainer from "../components/BorderedContainer"; -import NavigateLink from "../components/NavigateLink"; -import { APP_NAME, BASE_PATH } from "../config"; - -export const meta: MetaFunction = () => [{ title: APP_NAME }]; - -export async function loader({ request }: LoaderFunctionArgs) { - await ensureUserNotLoggedIn(request); - return null; -} - -export default function Index() { - 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/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx deleted file mode 100644 index f44fe79..0000000 --- a/frontend/app/routes/dashboard.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type { LoaderFunctionArgs, MetaFunction } from "react-router"; -import { Form, useLoaderData } from "react-router"; -import { ensureUserLoggedIn } from "../.server/auth"; -import { createApiClient } from "../api/client"; -import BorderedContainerWithCaption from "../components/BorderedContainerWithCaption"; -import NavigateLink from "../components/NavigateLink"; -import UserIcon from "../components/UserIcon"; -import { APP_NAME, BASE_PATH } from "../config"; - -export const meta: MetaFunction = () => [{ title: `Dashboard | ${APP_NAME}` }]; - -export async function loader({ request }: LoaderFunctionArgs) { - const { user, token } = await ensureUserLoggedIn(request); - const apiClient = createApiClient(token); - - const { games } = await apiClient.getGames(); - return { - user, - games, - }; -} - -export default function Dashboard() { - const { user, games } = useLoaderData<typeof loader>()!; - - 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> - <Form method="post" action="/logout"> - <button - type="submit" - 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> - </Form> - {user.is_admin && ( - <a - href={ - process.env.NODE_ENV === "development" - ? `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/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx deleted file mode 100644 index c063d05..0000000 --- a/frontend/app/routes/golf.$gameId.play.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Provider as JotaiProvider, createStore } from "jotai"; -import { useMemo } from "react"; -import type { LoaderFunctionArgs, MetaFunction } from "react-router"; -import { redirect, useLoaderData } from "react-router"; -import { ensureUserLoggedIn } from "../.server/auth"; -import { ApiClientContext, createApiClient } from "../api/client"; -import GolfPlayApp from "../components/GolfPlayApp"; -import { APP_NAME } from "../config"; - -export const meta: MetaFunction<typeof loader> = ({ data }) => [ - { - title: data - ? `Golf Playing ${data.game.display_name} | ${APP_NAME}` - : `Golf Playing | ${APP_NAME}`, - }, -]; - -export async function loader({ params, request }: LoaderFunctionArgs) { - const { token, user } = await ensureUserLoggedIn(request); - const apiClient = createApiClient(token); - - const gameId = Number(params.gameId); - - try { - const [{ game }, { state: gameState }] = await Promise.all([ - apiClient.getGame(gameId), - apiClient.getGamePlayLatestState(gameId), - ]); - - return { - apiToken: token, - game, - player: user, - gameState, - }; - } catch { - throw redirect("/dashboard"); - } -} - -export default function GolfPlay() { - const { apiToken, game, player, gameState } = useLoaderData<typeof loader>(); - - const store = useMemo(() => { - void game.game_id; - void player.user_id; - return createStore(); - }, [game.game_id, player.user_id]); - - return ( - <JotaiProvider store={store}> - <ApiClientContext.Provider value={createApiClient(apiToken)}> - <GolfPlayApp - key={game.game_id} - game={game} - player={player} - initialGameState={gameState} - /> - </ApiClientContext.Provider> - </JotaiProvider> - ); -} diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx deleted file mode 100644 index 7468be5..0000000 --- a/frontend/app/routes/golf.$gameId.watch.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Provider as JotaiProvider, createStore } from "jotai"; -import { useMemo } from "react"; -import type { LoaderFunctionArgs, MetaFunction } from "react-router"; -import { redirect, useLoaderData } from "react-router"; -import { ensureUserLoggedIn } from "../.server/auth"; -import { ApiClientContext, createApiClient } from "../api/client"; -import GolfWatchApp from "../components/GolfWatchApp"; -import { APP_NAME } from "../config"; - -export const meta: MetaFunction<typeof loader> = ({ data }) => [ - { - title: data - ? `Golf Watching ${data.game.display_name} | ${APP_NAME}` - : `Golf Watching | ${APP_NAME}`, - }, -]; - -export async function loader({ params, request }: LoaderFunctionArgs) { - const { token } = await ensureUserLoggedIn(request); - const apiClient = createApiClient(token); - - const gameId = Number(params.gameId); - - try { - const [{ game }, { ranking }, { states: gameStates }] = await Promise.all([ - await apiClient.getGame(gameId), - await apiClient.getGameWatchRanking(gameId), - await apiClient.getGameWatchLatestStates(gameId), - ]); - - return { - apiToken: token, - game, - ranking, - gameStates, - }; - } catch { - throw redirect("/dashboard"); - } -} - -export default function GolfWatch() { - const { apiToken, game, ranking, gameStates } = - useLoaderData<typeof loader>(); - - const store = useMemo(() => { - void game.game_id; - return createStore(); - }, [game.game_id]); - - return ( - <JotaiProvider store={store}> - <ApiClientContext.Provider value={createApiClient(apiToken)}> - <GolfWatchApp - key={game.game_id} - game={game} - initialGameStates={gameStates} - initialRanking={ranking} - /> - </ApiClientContext.Provider> - </JotaiProvider> - ); -} diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx deleted file mode 100644 index e394a6c..0000000 --- a/frontend/app/routes/login.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import type { - ActionFunctionArgs, - LoaderFunctionArgs, - MetaFunction, -} from "react-router"; -import { Form, data, useActionData } from "react-router"; -import { ensureUserNotLoggedIn, login } from "../.server/auth"; -import BorderedContainer from "../components/BorderedContainer"; -import InputText from "../components/InputText"; -import SubmitButton from "../components/SubmitButton"; -import { APP_NAME } from "../config"; - -export const meta: MetaFunction = () => [{ title: `Login | ${APP_NAME}` }]; - -export async function loader({ request }: LoaderFunctionArgs) { - return await ensureUserNotLoggedIn(request); -} - -export async function action({ request }: ActionFunctionArgs) { - const formData = await request.clone().formData(); - const username = String(formData.get("username")); - const password = String(formData.get("password")); - if (username === "" || password === "") { - return data( - { - message: "ユーザー名またはパスワードが誤っています", - errors: { - username: - username === "" ? "ユーザー名を入力してください" : undefined, - password: - password === "" ? "パスワードを入力してください" : undefined, - }, - }, - { status: 400 }, - ); - } - - try { - await login(request); - } catch (error) { - if (error instanceof Error) { - return data( - { - message: error.message, - errors: { - username: undefined, - password: undefined, - }, - }, - { status: 400 }, - ); - } else { - throw error; - } - } - return null; -} - -export default function Login() { - const loginErrors = useActionData<typeof action>(); - - return ( - <div className="min-h-screen bg-gray-100 flex items-center justify-center"> - <div className="mx-2"> - <BorderedContainer> - <Form method="post" className="w-full max-w-sm p-2"> - <h2 className="text-2xl mb-6 text-center"> - fortee アカウントでログイン - </h2> - {loginErrors?.message && ( - <p className="text-sky-500 text-sm mb-4">{loginErrors.message}</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 /> - {loginErrors?.errors?.username && ( - <p className="text-red-500 text-sm"> - {loginErrors.errors.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 - /> - {loginErrors?.errors?.password && ( - <p className="text-red-500 text-sm"> - {loginErrors.errors.password} - </p> - )} - </div> - <div className="flex justify-center"> - <SubmitButton type="submit">ログイン</SubmitButton> - </div> - </Form> - </BorderedContainer> - </div> - </div> - ); -} diff --git a/frontend/app/routes/logout.tsx b/frontend/app/routes/logout.tsx deleted file mode 100644 index 9616b4d..0000000 --- a/frontend/app/routes/logout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import type { ActionFunctionArgs } from "react-router"; -import { logout } from "../.server/auth"; - -export async function action({ request }: ActionFunctionArgs) { - await logout(request); - return null; -} diff --git a/frontend/app/routes/tournament.tsx b/frontend/app/routes/tournament.tsx deleted file mode 100644 index 162bd1a..0000000 --- a/frontend/app/routes/tournament.tsx +++ /dev/null @@ -1,440 +0,0 @@ -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"; -import { APP_NAME } from "../config"; - -export const meta: MetaFunction = () => [{ title: `Tournament | ${APP_NAME}` }]; - -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-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> - ); -} |
