From e239fe743fc66a8712cf9886d3dfed3cc41fce36 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 13 Feb 2026 22:40:45 +0900 Subject: 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 --- frontend/app/routes/_index.tsx | 45 --- frontend/app/routes/dashboard.tsx | 88 ------ frontend/app/routes/golf.$gameId.play.tsx | 62 ---- frontend/app/routes/golf.$gameId.watch.tsx | 63 ----- frontend/app/routes/login.tsx | 115 -------- frontend/app/routes/logout.tsx | 7 - frontend/app/routes/tournament.tsx | 440 ----------------------------- 7 files changed, 820 deletions(-) delete mode 100644 frontend/app/routes/_index.tsx delete mode 100644 frontend/app/routes/dashboard.tsx delete mode 100644 frontend/app/routes/golf.$gameId.play.tsx delete mode 100644 frontend/app/routes/golf.$gameId.watch.tsx delete mode 100644 frontend/app/routes/login.tsx delete mode 100644 frontend/app/routes/logout.tsx delete mode 100644 frontend/app/routes/tournament.tsx (limited to 'frontend/app/routes') 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 ( -
- iOSDC Japan 2025 -
-
-
Swift Code Battle
-
-
-
- -

- Swift コードバトルは指示された動作をする Swift - コードをより短く書けた方が勝ち、という 1 対 1 - の対戦コンテンツです。9/6 - に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での - コードバトルを 9/19 (金) day0 - に実施します。ここでは短いコードが正義です! - 可読性も保守性も放り投げた、イベントならではのコードをお楽しみください! -

-
-
-
- ログイン -
-
- ); -} 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()!; - - return ( -
- {user.icon_path && ( - - )} -

{user.display_name}

- -
- {games.length === 0 ? ( -

エントリーできる試合はありません

- ) : ( -
    - {games.map((game) => ( -
  • -
    - - {game.display_name} - -
    -
    - - 対戦 - - - 観戦 - -
    -
  • - ))} -
- )} -
-
-
- -
- {user.is_admin && ( - - Admin Dashboard - - )} -
- ); -} 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 = ({ 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(); - - const store = useMemo(() => { - void game.game_id; - void player.user_id; - return createStore(); - }, [game.game_id, player.user_id]); - - return ( - - - - - - ); -} 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 = ({ 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(); - - const store = useMemo(() => { - void game.game_id; - return createStore(); - }, [game.game_id]); - - return ( - - - - - - ); -} 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(); - - return ( -
-
- -
-

- fortee アカウントでログイン -

- {loginErrors?.message && ( -

{loginErrors.message}

- )} -
- - - {loginErrors?.errors?.username && ( -

- {loginErrors.errors.username} -

- )} -
-
- - - {loginErrors?.errors?.password && ( -

- {loginErrors.errors.password} -

- )} -
-
- ログイン -
-
-
-
-
- ); -} 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 ( - -
- 予選 {rank} 位 - {player?.display_name} - {player?.icon_path && ( - - )} -
-
- ); -} - -function BranchVL({ className = "" }: { className?: string }) { - return ( -
-
-
-
- ); -} - -function BranchVR({ className = "" }: { className?: string }) { - return ( -
-
-
-
- ); -} - -function BranchVL2({ - score, - className = "", -}: { score: number | null; className?: string }) { - return ( -
-
-
- {score} -
-
-
- ); -} - -function BranchVR2({ - score, - className = "", -}: { score: number | null; className?: string }) { - return ( -
-
-
- {score} -
-
-
- ); -} - -function BranchV3({ className = "" }: { className?: string }) { - return
; -} - -function BranchH({ - score, - className1, - className2, - className3, -}: { - score?: number | null; - className1: string; - className2: string; - className3: string; -}) { - return ( -
-
-
-
- {score} -
-
- ); -} - -function BranchH2({ - score, - className1, - className2, - className3, -}: { - score?: number | null; - className1: string; - className2: string; - className3: string; -}) { - return ( -
-
- {score} -
-
-
-
- ); -} - -function BranchL({ - score, - className = "", -}: { score: number | null; className?: string }) { - return ( -
-
-
- {score} -
-
- ); -} - -function BranchR({ - score, - className = "", -}: { score: number | null; className?: string }) { - return ( -
-
- {score} -
-
-
- ); -} - -function BranchL2({ className = "" }: { className?: string }) { - return ( -
-
-
-
- ); -} - -function BranchR2({ className = "" }: { className?: string }) { - return ( -
-
-
-
- ); -} - -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(); - - 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 ( -
-
-

- iOSDC Japan 2025 Swift Code Battle -

- -
-
-
-
- -
-
-
-
-
-
- - - - -
-
-
- - - - - - -
-
- - - - - - -
-
- - - - - - -
-
-
-
- ); -} -- cgit v1.3.1