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/.server/auth.ts | 96 ----- frontend/app/.server/cookie.ts | 44 --- frontend/app/.server/session.ts | 50 --- frontend/app/App.tsx | 58 +++ frontend/app/auth.ts | 45 +++ .../components/GolfPlayApps/GolfPlayAppGaming.tsx | 2 +- frontend/app/components/NavigateLink.tsx | 16 +- frontend/app/components/ProtectedRoute.tsx | 16 + frontend/app/components/PublicOnlyRoute.tsx | 16 + frontend/app/hooks/useAuth.ts | 58 +++ frontend/app/hooks/usePageTitle.ts | 7 + frontend/app/main.tsx | 20 + frontend/app/pages/DashboardPage.tsx | 108 +++++ frontend/app/pages/GolfPlayPage.tsx | 74 ++++ frontend/app/pages/GolfWatchPage.tsx | 78 ++++ frontend/app/pages/IndexPage.tsx | 39 ++ frontend/app/pages/LoginPage.tsx | 101 +++++ frontend/app/pages/TournamentPage.tsx | 429 ++++++++++++++++++++ frontend/app/root.tsx | 36 -- frontend/app/routes.ts | 4 - 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 --------------------- 27 files changed, 1062 insertions(+), 1055 deletions(-) delete mode 100644 frontend/app/.server/auth.ts delete mode 100644 frontend/app/.server/cookie.ts delete mode 100644 frontend/app/.server/session.ts create mode 100644 frontend/app/App.tsx create mode 100644 frontend/app/auth.ts create mode 100644 frontend/app/components/ProtectedRoute.tsx create mode 100644 frontend/app/components/PublicOnlyRoute.tsx create mode 100644 frontend/app/hooks/useAuth.ts create mode 100644 frontend/app/hooks/usePageTitle.ts create mode 100644 frontend/app/main.tsx create mode 100644 frontend/app/pages/DashboardPage.tsx create mode 100644 frontend/app/pages/GolfPlayPage.tsx create mode 100644 frontend/app/pages/GolfWatchPage.tsx create mode 100644 frontend/app/pages/IndexPage.tsx create mode 100644 frontend/app/pages/LoginPage.tsx create mode 100644 frontend/app/pages/TournamentPage.tsx delete mode 100644 frontend/app/root.tsx delete mode 100644 frontend/app/routes.ts 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') diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts deleted file mode 100644 index 3e24638..0000000 --- a/frontend/app/.server/auth.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { type JwtPayload, jwtDecode } from "jwt-decode"; -import { redirect } from "react-router"; -import { Authenticator } from "remix-auth"; -import { FormStrategy } from "remix-auth-form"; -import { apiLogin } from "../api/client"; -import { components } from "../api/schema"; -import { createUnstructuredCookie } from "./cookie"; -import { cookieOptions, sessionStorage } from "./session"; - -const authenticator = new Authenticator(); - -authenticator.use( - new FormStrategy(async ({ form }) => { - const username = String(form.get("username")); - const password = String(form.get("password")); - return (await apiLogin(username, password)).token; - }), - "default", -); - -export type User = components["schemas"]["User"]; - -// This cookie is used to directly store the JWT for the API server. -// Remix's createCookie() returns "structured" cookies, which cannot be reused directly by non-Remix servers. -const tokenCookie = createUnstructuredCookie("albatross_token", cookieOptions); - -/** - * @throws Error on failure - */ -export async function login(request: Request): Promise { - const jwt = await authenticator.authenticate("default", request); - - const session = await sessionStorage.getSession( - request.headers.get("cookie"), - ); - session.set("user", jwt); - - throw redirect("/dashboard", { - headers: [ - ["Set-Cookie", await sessionStorage.commitSession(session)], - ["Set-Cookie", await tokenCookie.serialize(jwt)], - ], - }); -} - -export async function logout(request: Request): Promise { - const session = await sessionStorage.getSession( - request.headers.get("cookie"), - ); - throw redirect("/", { - headers: [ - ["Set-Cookie", await sessionStorage.destroySession(session)], - [ - "Set-Cookie", - await tokenCookie.serialize("", { maxAge: 0, expires: new Date(0) }), - ], - ], - }); -} - -async function getCurrentValidSession( - request: Request, -): Promise<{ user: User; token: string } | null> { - const session = await sessionStorage.getSession( - request.headers.get("cookie"), - ); - const token = session.get("user"); - if (!token) { - return null; - } - const user = jwtDecode(token); - const exp = user.exp; - if (exp != null && new Date((exp - 3600) * 1000) < new Date()) { - // If the token will expire in less than an hour, refresh it. - return null; - } - return { user, token }; -} - -export async function ensureUserLoggedIn( - request: Request, -): Promise<{ user: User; token: string }> { - const session = await getCurrentValidSession(request); - if (!session) { - throw redirect("/login"); - } - return session; -} - -export async function ensureUserNotLoggedIn(request: Request): Promise { - const session = await getCurrentValidSession(request); - if (session) { - throw redirect("/dashboard"); - } - return null; -} diff --git a/frontend/app/.server/cookie.ts b/frontend/app/.server/cookie.ts deleted file mode 100644 index c8365eb..0000000 --- a/frontend/app/.server/cookie.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { parse as parseCookie, serialize as serializeCookie } from "cookie"; -import { Cookie, CookieOptions } from "react-router"; - -// Remix's createCookie() returns "structured" cookies, which are cookies that hold a JSON-encoded object. -// This is not suitable for interoperation with other systems that expect a simple string value. -// This function creates an "unstructured" cookie, a simple plain text. -export function createUnstructuredCookie( - name: string, - cookieOptions?: CookieOptions, -): Cookie { - const { secrets = [], ...options } = { - path: "/", - sameSite: "lax" as const, - ...cookieOptions, - }; - - return { - get name() { - return name; - }, - get isSigned() { - return secrets.length > 0; - }, - get expires() { - return typeof options.maxAge !== "undefined" - ? new Date(Date.now() + options.maxAge * 1000) - : options.expires; - }, - async parse(cookieHeader, parseOptions) { - if (!cookieHeader) return null; - const cookies = parseCookie(cookieHeader, { - ...options, - ...parseOptions, - }); - return name in cookies ? cookies[name] : null; - }, - async serialize(value, serializeOptions) { - return serializeCookie(name, value, { - ...options, - ...serializeOptions, - }); - }, - }; -} diff --git a/frontend/app/.server/session.ts b/frontend/app/.server/session.ts deleted file mode 100644 index 0edcc35..0000000 --- a/frontend/app/.server/session.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createCookieSessionStorage } from "react-router"; - -export const cookieOptions = { - sameSite: "lax" as const, - path: "/", - httpOnly: true, - secure: process.env.NODE_ENV === "production", - secrets: [process.env.ALBATROSS_COOKIE_SECRET ?? "local"], -}; - -const innerSessionStorage = createCookieSessionStorage({ - cookie: { - name: "albatross_session", - ...cookieOptions, - }, -}); -type InnerSessionStorage = typeof innerSessionStorage; - -// This class is used to recover from invalid sessions. -// It may occur if the session had been created before the authentication library was updated. -class RecoverableSessionStorage { - innerStorage: InnerSessionStorage; - - constructor(innerStorage: InnerSessionStorage) { - this.innerStorage = innerStorage; - } - - // If the session is invalid, return a new session. - // It may occur if the session had been created before the authentication library was updated. - getSession(...args: Parameters) { - try { - return this.innerStorage.getSession(...args); - } catch (e) { - void e; - return this.innerStorage.getSession(); - } - } - - commitSession(...args: Parameters) { - return this.innerStorage.commitSession(...args); - } - - destroySession(...args: Parameters) { - return this.innerStorage.destroySession(...args); - } -} - -export const sessionStorage = new RecoverableSessionStorage( - innerSessionStorage, -); diff --git a/frontend/app/App.tsx b/frontend/app/App.tsx new file mode 100644 index 0000000..fcf6977 --- /dev/null +++ b/frontend/app/App.tsx @@ -0,0 +1,58 @@ +import { Route, Router, Switch } from "wouter"; +import ProtectedRoute from "./components/ProtectedRoute"; +import PublicOnlyRoute from "./components/PublicOnlyRoute"; +import { BASE_PATH } from "./config"; +import DashboardPage from "./pages/DashboardPage"; +import GolfPlayPage from "./pages/GolfPlayPage"; +import GolfWatchPage from "./pages/GolfWatchPage"; +import IndexPage from "./pages/IndexPage"; +import LoginPage from "./pages/LoginPage"; +import TournamentPage from "./pages/TournamentPage"; + +export default function App() { + return ( + + + + + + + + + + + + + + + + + + + {(params) => ( + + + + )} + + + {(params) => ( + + + + )} + + + + + + + +
+

404 - Page not found

+
+
+
+
+ ); +} diff --git a/frontend/app/auth.ts b/frontend/app/auth.ts new file mode 100644 index 0000000..7a3d10d --- /dev/null +++ b/frontend/app/auth.ts @@ -0,0 +1,45 @@ +import { type JwtPayload, jwtDecode } from "jwt-decode"; +import type { components } from "./api/schema"; + +export type User = components["schemas"]["User"]; + +const COOKIE_NAME = "albatross_token"; + +export function getToken(): string | null { + const match = document.cookie + .split("; ") + .find((row) => row.startsWith(`${COOKIE_NAME}=`)); + if (!match) return null; + return match.split("=").slice(1).join("="); +} + +export function setToken(token: string): void { + document.cookie = `${COOKIE_NAME}=${token}; path=/; SameSite=Lax`; +} + +export function clearToken(): void { + document.cookie = `${COOKIE_NAME}=; path=/; SameSite=Lax; max-age=0`; +} + +export function getUserFromToken(): User | null { + const token = getToken(); + if (!token) return null; + try { + return jwtDecode(token); + } catch { + return null; + } +} + +export function isTokenExpired(): boolean { + const token = getToken(); + if (!token) return true; + try { + const decoded = jwtDecode(token); + if (decoded.exp == null) return false; + // If the token will expire in less than an hour, treat it as expired. + return new Date((decoded.exp - 3600) * 1000) < new Date(); + } catch { + return true; + } +} diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx index 9eab91e..025e676 100644 --- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx +++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx @@ -1,6 +1,6 @@ import { useAtomValue } from "jotai"; import React, { useRef, useState } from "react"; -import { Link } from "react-router"; +import { Link } from "wouter"; import { calcCodeSize, gamingLeftTimeSecondsAtom, diff --git a/frontend/app/components/NavigateLink.tsx b/frontend/app/components/NavigateLink.tsx index c4ee7aa..16c7858 100644 --- a/frontend/app/components/NavigateLink.tsx +++ b/frontend/app/components/NavigateLink.tsx @@ -1,10 +1,18 @@ -import { Link, LinkProps } from "react-router"; +import { Link } from "wouter"; -export default function NavigateLink(props: LinkProps) { +export default function NavigateLink({ + to, + children, +}: { + to: string; + children: React.ReactNode; +}) { return ( + > + {children} + ); } diff --git a/frontend/app/components/ProtectedRoute.tsx b/frontend/app/components/ProtectedRoute.tsx new file mode 100644 index 0000000..3aeaebc --- /dev/null +++ b/frontend/app/components/ProtectedRoute.tsx @@ -0,0 +1,16 @@ +import { Redirect } from "wouter"; +import { useAuth } from "../hooks/useAuth"; + +export default function ProtectedRoute({ + children, +}: { + children: React.ReactNode; +}) { + const { isLoggedIn } = useAuth(); + + if (!isLoggedIn) { + return ; + } + + return <>{children}; +} diff --git a/frontend/app/components/PublicOnlyRoute.tsx b/frontend/app/components/PublicOnlyRoute.tsx new file mode 100644 index 0000000..2527918 --- /dev/null +++ b/frontend/app/components/PublicOnlyRoute.tsx @@ -0,0 +1,16 @@ +import { Redirect } from "wouter"; +import { useAuth } from "../hooks/useAuth"; + +export default function PublicOnlyRoute({ + children, +}: { + children: React.ReactNode; +}) { + const { isLoggedIn } = useAuth(); + + if (isLoggedIn) { + return ; + } + + return <>{children}; +} diff --git a/frontend/app/hooks/useAuth.ts b/frontend/app/hooks/useAuth.ts new file mode 100644 index 0000000..8762734 --- /dev/null +++ b/frontend/app/hooks/useAuth.ts @@ -0,0 +1,58 @@ +import { useCallback, useSyncExternalStore } from "react"; +import { apiLogin } from "../api/client"; +import { + type User, + clearToken, + getToken, + getUserFromToken, + isTokenExpired, + setToken, +} from "../auth"; + +// Simple external store to trigger re-renders when auth state changes. +let authVersion = 0; +const listeners = new Set<() => void>(); + +function subscribe(callback: () => void) { + listeners.add(callback); + return () => listeners.delete(callback); +} + +function getSnapshot() { + return authVersion; +} + +function notifyAuthChange() { + authVersion++; + for (const listener of listeners) { + listener(); + } +} + +export function useAuth(): { + user: User | null; + token: string | null; + isLoggedIn: boolean; + login: (username: string, password: string) => Promise; + logout: () => void; +} { + useSyncExternalStore(subscribe, getSnapshot); + + const token = getToken(); + const isExpired = isTokenExpired(); + const user = isExpired ? null : getUserFromToken(); + const isLoggedIn = user !== null && !isExpired; + + const login = useCallback(async (username: string, password: string) => { + const { token } = await apiLogin(username, password); + setToken(token); + notifyAuthChange(); + }, []); + + const logout = useCallback(() => { + clearToken(); + notifyAuthChange(); + }, []); + + return { user, token: isLoggedIn ? token : null, isLoggedIn, login, logout }; +} diff --git a/frontend/app/hooks/usePageTitle.ts b/frontend/app/hooks/usePageTitle.ts new file mode 100644 index 0000000..fb8def5 --- /dev/null +++ b/frontend/app/hooks/usePageTitle.ts @@ -0,0 +1,7 @@ +import { useEffect } from "react"; + +export function usePageTitle(title: string) { + useEffect(() => { + document.title = title; + }, [title]); +} diff --git a/frontend/app/main.tsx b/frontend/app/main.tsx new file mode 100644 index 0000000..89ed944 --- /dev/null +++ b/frontend/app/main.tsx @@ -0,0 +1,20 @@ +import { config } from "@fortawesome/fontawesome-svg-core"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "@fortawesome/fontawesome-svg-core/styles.css"; +import "./tailwind.css"; +import "./shiki.css"; +import App from "./App"; + +config.autoAddCss = false; + +const root = document.getElementById("root"); +if (!root) { + throw new Error("Root element not found"); +} + +createRoot(root).render( + + + , +); 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([]); + 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 ( +
+

Loading...

+
+ ); + } + + 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/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(null); + const [gameState, setGameState] = useState(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 ( +
+

Loading...

+
+ ); + } + + const token = getToken()!; + + return ( + + + + + + ); +} 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(null); + const [ranking, setRanking] = useState([]); + 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 ( +
+

Loading...

+
+ ); + } + + const token = getToken()!; + + return ( + + + + + + ); +} 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 ( +
+ iOSDC Japan 2025 +
+
+
Swift Code Battle
+
+
+
+ +

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

+
+
+
+ ログイン +
+
+ ); +} 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(null); + const [fieldErrors, setFieldErrors] = useState<{ + username?: string; + password?: string; + }>({}); + const [submitting, setSubmitting] = useState(false); + + async function handleSubmit(e: FormEvent) { + 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 ( +
+
+ +
+

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

+ {error &&

{error}

} +
+ + + {fieldErrors.username && ( +

{fieldErrors.username}

+ )} +
+
+ + + {fieldErrors.password && ( +

{fieldErrors.password}

+ )} +
+
+ + {submitting ? "ログイン中..." : "ログイン"} + +
+
+
+
+
+ ); +} 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 ( + +
+ 予選 {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; + 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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 ( +
+

Loading...

+
+ ); + } + + if (error || !tournament) { + return ( +
+

{error || "Failed to load tournament"}

+
+ ); + } + + 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 +

+ +
+
+
+
+ +
+
+
+
+
+
+ + + + +
+
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+
+
+
+ ); +} diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx deleted file mode 100644 index 5ecab0a..0000000 --- a/frontend/app/root.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { config } from "@fortawesome/fontawesome-svg-core"; -import "@fortawesome/fontawesome-svg-core/styles.css"; -import type { LinksFunction } from "react-router"; -import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; -import "./tailwind.css"; -import "./shiki.css"; -import { BASE_PATH } from "./config"; - -config.autoAddCss = false; - -export const links: LinksFunction = () => [ - { rel: "icon", href: `${BASE_PATH}favicon.svg` }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - - ); -} - -export default function App() { - return ; -} diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts deleted file mode 100644 index 4c05936..0000000 --- a/frontend/app/routes.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { type RouteConfig } from "@react-router/dev/routes"; -import { flatRoutes } from "@react-router/fs-routes"; - -export default flatRoutes() satisfies RouteConfig; 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 From 51c2a65ad13f96389997bff8c4db937f42a3b9b3 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 13 Feb 2026 22:46:05 +0900 Subject: refactor(frontend): replace process.env with import.meta.env in API client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BFF前提のURL分岐をVite SPA向けに調整。本番URLのハードコードを VITE_API_BASE_URL環境変数に外出し。 Co-Authored-By: Claude Opus 4.6 --- frontend/app/api/client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'frontend/app') diff --git a/frontend/app/api/client.ts b/frontend/app/api/client.ts index 6b7ce80..c26f1c6 100644 --- a/frontend/app/api/client.ts +++ b/frontend/app/api/client.ts @@ -3,11 +3,11 @@ import { createContext } from "react"; import { API_BASE_PATH } from "../config"; import type { paths } from "./schema"; +const apiOrigin = import.meta.env.VITE_API_BASE_URL + ?? (import.meta.env.DEV ? "http://localhost:8004" : ""); + const client = createClient({ - baseUrl: - process.env.NODE_ENV === "development" - ? `http://localhost:8004${API_BASE_PATH}` - : `https://t.nil.ninja${API_BASE_PATH}`, + baseUrl: `${apiOrigin}${API_BASE_PATH}`, }); export async function apiLogin(username: string, password: string) { -- cgit v1.3.1 From 6c30f383a65cb000d66a85cadc96253ce7061942 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 13 Feb 2026 23:05:37 +0900 Subject: refactor(frontend): remove React Router remnants from config files Clean up old React Router references after migration to Vite + Wouter: - Replace build/ and .react-router/ with dist/ in ESLint globalIgnores - Replace ./build with ./dist in Biome ignore list - Remove formComponents and NavLink from ESLint settings Co-Authored-By: Claude Opus 4.6 --- frontend/.dockerignore | 1 - frontend/app/api/client.ts | 5 +++-- frontend/biome.json | 4 ++-- frontend/eslint.config.js | 8 ++------ 4 files changed, 7 insertions(+), 11 deletions(-) (limited to 'frontend/app') diff --git a/frontend/.dockerignore b/frontend/.dockerignore index c0b2358..de4d1f0 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,3 +1,2 @@ -build dist node_modules diff --git a/frontend/app/api/client.ts b/frontend/app/api/client.ts index c26f1c6..86f2506 100644 --- a/frontend/app/api/client.ts +++ b/frontend/app/api/client.ts @@ -3,8 +3,9 @@ import { createContext } from "react"; import { API_BASE_PATH } from "../config"; import type { paths } from "./schema"; -const apiOrigin = import.meta.env.VITE_API_BASE_URL - ?? (import.meta.env.DEV ? "http://localhost:8004" : ""); +const apiOrigin = + import.meta.env.VITE_API_BASE_URL ?? + (import.meta.env.DEV ? "http://localhost:8004" : ""); const client = createClient({ baseUrl: `${apiOrigin}${API_BASE_PATH}`, diff --git a/frontend/biome.json b/frontend/biome.json index 54c8856..6da4a7a 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -9,9 +9,9 @@ "ignoreUnknown": false, "ignore": [ "./.cache", - "./build", "./app/api/schema.d.ts", - "./app/shiki.bundle.ts" + "./app/shiki.bundle.ts", + "./dist" ] }, "formatter": { diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 538fb12..0fe10e9 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -7,7 +7,7 @@ import globals from "globals"; import ts from "typescript-eslint"; export default defineConfig( - globalIgnores(["node_modules/", ".react-router/", "build/"]), + globalIgnores(["node_modules/", "dist/"]), js.configs.recommended, ts.configs.recommended, react.configs.flat.recommended, @@ -27,11 +27,7 @@ export default defineConfig( react: { version: "detect", }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], + linkComponents: [{ name: "Link", linkAttribute: "to" }], }, }, ); -- cgit v1.3.1