diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-03-21 01:54:54 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-03-21 01:54:54 +0900 |
| commit | 4acf8d51b257c30b1a5dd99bd515ac22ddd5b564 (patch) | |
| tree | 495071797a64ab8364061583e7ad61c23bcbfe77 /frontend | |
| parent | 338313f3fb97aa96b7cf75596ddcbf55f6a9002d (diff) | |
| download | phperkaigi-2025-albatross-4acf8d51b257c30b1a5dd99bd515ac22ddd5b564.tar.gz phperkaigi-2025-albatross-4acf8d51b257c30b1a5dd99bd515ac22ddd5b564.tar.zst phperkaigi-2025-albatross-4acf8d51b257c30b1a5dd99bd515ac22ddd5b564.zip | |
refactor(frontend): api client
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/app/.server/auth.ts | 4 | ||||
| -rw-r--r-- | frontend/app/api/client.ts | 168 | ||||
| -rw-r--r-- | frontend/app/components/GolfPlayApp.tsx | 21 | ||||
| -rw-r--r-- | frontend/app/components/GolfWatchApp.tsx | 21 | ||||
| -rw-r--r-- | frontend/app/routes/dashboard.tsx | 6 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.play.tsx | 30 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.watch.tsx | 34 |
7 files changed, 124 insertions, 160 deletions
diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts index cbeb141..81ceadf 100644 --- a/frontend/app/.server/auth.ts +++ b/frontend/app/.server/auth.ts @@ -2,7 +2,7 @@ import { jwtDecode } from "jwt-decode"; import { redirect } from "react-router"; import { Authenticator } from "remix-auth"; import { FormStrategy } from "remix-auth-form"; -import { apiPostLogin } from "../api/client"; +import { apiLogin } from "../api/client"; import { components } from "../api/schema"; import { createUnstructuredCookie } from "./cookie"; import { cookieOptions, sessionStorage } from "./session"; @@ -13,7 +13,7 @@ authenticator.use( new FormStrategy(async ({ form }) => { const username = String(form.get("username")); const password = String(form.get("password")); - return (await apiPostLogin(username, password)).token; + return (await apiLogin(username, password)).token; }), "default", ); diff --git a/frontend/app/api/client.ts b/frontend/app/api/client.ts index 25f6c54..ee5437d 100644 --- a/frontend/app/api/client.ts +++ b/frontend/app/api/client.ts @@ -2,15 +2,15 @@ import createClient from "openapi-fetch"; import { createContext } from "react"; import type { paths } from "./schema"; -const apiClient = createClient<paths>({ +const client = createClient<paths>({ baseUrl: process.env.NODE_ENV === "development" ? "http://localhost:8003/phperkaigi/2025/code-battle/api/" : "https://t.nil.ninja/phperkaigi/2025/code-battle/api/", }); -export async function apiPostLogin(username: string, password: string) { - const { data, error } = await apiClient.POST("/login", { +export async function apiLogin(username: string, password: string) { + const { data, error } = await client.POST("/login", { body: { username, password, @@ -20,101 +20,101 @@ export async function apiPostLogin(username: string, password: string) { return data; } -export async function apiGetGames(token: string) { - const { data, error } = await apiClient.GET("/games", { - params: { - header: { Authorization: `Bearer ${token}` }, - }, - }); - if (error) throw new Error(error.message); - return data; -} +class AuthenticatedApiClient { + constructor(public readonly token: string) {} -export async function apiGetGame(token: string, gameId: number) { - const { data, error } = await apiClient.GET("/games/{game_id}", { - params: { - header: { Authorization: `Bearer ${token}` }, - path: { game_id: gameId }, - }, - }); - if (error) throw new Error(error.message); - return data; -} + async getGames() { + const { data, error } = await client.GET("/games", { + params: { + header: this._getAuthorizationHeader(), + }, + }); + if (error) throw new Error(error.message); + return data; + } -export async function apiGetGamePlayLatestState(token: string, gameId: number) { - const { data, error } = await apiClient.GET( - "/games/{game_id}/play/latest_state", - { + async getGame(gameId: number) { + const { data, error } = await client.GET("/games/{game_id}", { params: { - header: { Authorization: `Bearer ${token}` }, + header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, - }, - ); - if (error) throw new Error(error.message); - return data; -} + }); + if (error) throw new Error(error.message); + return data; + } -export async function apiPostGamePlayCode( - token: string, - gameId: number, - code: string, -) { - const { error } = await apiClient.POST("/games/{game_id}/play/code", { - params: { - header: { Authorization: `Bearer ${token}` }, - path: { game_id: gameId }, - }, - body: { code }, - }); - if (error) throw new Error(error.message); -} + async getGamePlayLatestState(gameId: number) { + const { data, error } = await client.GET( + "/games/{game_id}/play/latest_state", + { + params: { + header: this._getAuthorizationHeader(), + path: { game_id: gameId }, + }, + }, + ); + if (error) throw new Error(error.message); + return data; + } -export async function apiPostGamePlaySubmit( - token: string, - gameId: number, - code: string, -) { - const { data, error } = await apiClient.POST("/games/{game_id}/play/submit", { - params: { - header: { Authorization: `Bearer ${token}` }, - path: { game_id: gameId }, - }, - body: { code }, - }); - if (error) throw new Error(error.message); - return data; -} + async postGamePlayCode(gameId: number, code: string) { + const { error } = await client.POST("/games/{game_id}/play/code", { + params: { + header: this._getAuthorizationHeader(), + path: { game_id: gameId }, + }, + body: { code }, + }); + if (error) throw new Error(error.message); + } -export async function apiGetGameWatchRanking(token: string, gameId: number) { - const { data, error } = await apiClient.GET( - "/games/{game_id}/watch/ranking", - { + async postGamePlaySubmit(gameId: number, code: string) { + const { data, error } = await client.POST("/games/{game_id}/play/submit", { params: { - header: { Authorization: `Bearer ${token}` }, + header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, - }, - ); - if (error) throw new Error(error.message); - return data; -} + body: { code }, + }); + if (error) throw new Error(error.message); + return data; + } -export async function apiGetGameWatchLatestStates( - token: string, - gameId: number, -) { - const { data, error } = await apiClient.GET( - "/games/{game_id}/watch/latest_states", - { + async getGameWatchRanking(gameId: number) { + const { data, error } = await client.GET("/games/{game_id}/watch/ranking", { params: { - header: { Authorization: `Bearer ${token}` }, + header: this._getAuthorizationHeader(), path: { game_id: gameId }, }, - }, - ); - if (error) throw new Error(error.message); - return data; + }); + if (error) throw new Error(error.message); + return data; + } + + async getGameWatchLatestStates(gameId: number) { + const { data, error } = await client.GET( + "/games/{game_id}/watch/latest_states", + { + params: { + header: this._getAuthorizationHeader(), + path: { game_id: gameId }, + }, + }, + ); + if (error) throw new Error(error.message); + return data; + } + + _getAuthorizationHeader() { + return { Authorization: `Bearer ${this.token}` }; + } +} + +export function createApiClient(token: string) { + return new AuthenticatedApiClient(token); } -export const ApiAuthTokenContext = createContext<string>(""); +export const ApiClientContext = createContext<AuthenticatedApiClient | null>( + null, +); diff --git a/frontend/app/components/GolfPlayApp.tsx b/frontend/app/components/GolfPlayApp.tsx index 97f7cc4..71b40ce 100644 --- a/frontend/app/components/GolfPlayApp.tsx +++ b/frontend/app/components/GolfPlayApp.tsx @@ -3,13 +3,7 @@ import { useHydrateAtoms } from "jotai/utils"; import { useContext, useEffect, useState } from "react"; import { useTimer } from "react-use-precision-timer"; import { useDebouncedCallback } from "use-debounce"; -import { - ApiAuthTokenContext, - apiGetGame, - apiGetGamePlayLatestState, - apiPostGamePlayCode, - apiPostGamePlaySubmit, -} from "../api/client"; +import { ApiClientContext } from "../api/client"; import type { components } from "../api/schema"; import { gameStateKindAtom, @@ -43,7 +37,7 @@ export default function GolfPlayApp({ game, player, initialGameState }: Props) { [setLatestGameStateAtom, initialGameState], ]); - const apiAuthToken = useContext(ApiAuthTokenContext); + const apiClient = useContext(ApiClientContext)!; const gameStateKind = useAtomValue(gameStateKindAtom); const setGameStartedAt = useSetAtom(setGameStartedAtAtom); @@ -63,7 +57,7 @@ export default function GolfPlayApp({ game, player, initialGameState }: Props) { const onCodeChange = useDebouncedCallback(async (code: string) => { console.log("player:c2s:code"); if (game.game_type === "1v1") { - await apiPostGamePlayCode(apiAuthToken, game.game_id, code); + await apiClient.postGamePlayCode(game.game_id, code); } }, 1000); @@ -73,7 +67,7 @@ export default function GolfPlayApp({ game, player, initialGameState }: Props) { } console.log("player:c2s:submit"); handleSubmitCodePre(); - await apiPostGamePlaySubmit(apiAuthToken, game.game_id, code); + await apiClient.postGamePlaySubmit(game.game_id, code); handleSubmitCodePost(); }, 1000); @@ -91,13 +85,12 @@ export default function GolfPlayApp({ game, player, initialGameState }: Props) { try { if (gameStateKind === "waiting") { - const { game: g } = await apiGetGame(apiAuthToken, game.game_id); + const { game: g } = await apiClient.getGame(game.game_id); if (g.started_at != null) { setGameStartedAt(g.started_at); } } else if (gameStateKind === "gaming") { - const { state } = await apiGetGamePlayLatestState( - apiAuthToken, + const { state } = await apiClient.getGamePlayLatestState( game.game_id, ); setLatestGameState(state); @@ -114,7 +107,7 @@ export default function GolfPlayApp({ game, player, initialGameState }: Props) { }; }, [ isDataPolling, - apiAuthToken, + apiClient, game.game_id, gameStateKind, setGameStartedAt, diff --git a/frontend/app/components/GolfWatchApp.tsx b/frontend/app/components/GolfWatchApp.tsx index 185f41d..cfd5e74 100644 --- a/frontend/app/components/GolfWatchApp.tsx +++ b/frontend/app/components/GolfWatchApp.tsx @@ -2,12 +2,7 @@ import { useAtomValue, useSetAtom } from "jotai"; import { useHydrateAtoms } from "jotai/utils"; import { useContext, useEffect, useState } from "react"; import { useTimer } from "react-use-precision-timer"; -import { - ApiAuthTokenContext, - apiGetGame, - apiGetGameWatchLatestStates, - apiGetGameWatchRanking, -} from "../api/client"; +import { ApiClientContext } from "../api/client"; import type { components } from "../api/schema"; import { gameStateKindAtom, @@ -46,7 +41,7 @@ export default function GolfWatchApp({ [setLatestGameStatesAtom, initialGameStates], ]); - const apiAuthToken = useContext(ApiAuthTokenContext); + const apiClient = useContext(ApiClientContext)!; const gameStateKind = useAtomValue(gameStateKindAtom); const setGameStartedAt = useSetAtom(setGameStartedAtAtom); @@ -88,20 +83,16 @@ export default function GolfWatchApp({ try { if (gameStateKind === "waiting") { - const { game: g } = await apiGetGame(apiAuthToken, game.game_id); + const { game: g } = await apiClient.getGame(game.game_id); if (g.started_at != null) { setGameStartedAt(g.started_at); } } else if (gameStateKind === "gaming") { - const { states } = await apiGetGameWatchLatestStates( - apiAuthToken, + const { states } = await apiClient.getGameWatchLatestStates( game.game_id, ); setLatestGameStates(states); - const { ranking } = await apiGetGameWatchRanking( - apiAuthToken, - game.game_id, - ); + const { ranking } = await apiClient.getGameWatchRanking(game.game_id); setRanking(ranking); } } catch (error) { @@ -116,7 +107,7 @@ export default function GolfWatchApp({ }; }, [ isDataPolling, - apiAuthToken, + apiClient, game.game_id, gameStateKind, setGameStartedAt, diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx index ee3c62d..3f68529 100644 --- a/frontend/app/routes/dashboard.tsx +++ b/frontend/app/routes/dashboard.tsx @@ -1,7 +1,7 @@ import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { Form, useLoaderData } from "react-router"; import { ensureUserLoggedIn } from "../.server/auth"; -import { apiGetGames } from "../api/client"; +import { createApiClient } from "../api/client"; import BorderedContainerWithCaption from "../components/BorderedContainerWithCaption"; import NavigateLink from "../components/NavigateLink"; import UserIcon from "../components/UserIcon"; @@ -12,7 +12,9 @@ export const meta: MetaFunction = () => [ export async function loader({ request }: LoaderFunctionArgs) { const { user, token } = await ensureUserLoggedIn(request); - const { games } = await apiGetGames(token); + const apiClient = createApiClient(token); + + const { games } = await apiClient.getGames(); return { user, games, diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx index 4f8468d..8f41257 100644 --- a/frontend/app/routes/golf.$gameId.play.tsx +++ b/frontend/app/routes/golf.$gameId.play.tsx @@ -3,11 +3,7 @@ import { useMemo } from "react"; import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { useLoaderData } from "react-router"; import { ensureUserLoggedIn } from "../.server/auth"; -import { - ApiAuthTokenContext, - apiGetGame, - apiGetGamePlayLatestState, -} from "../api/client"; +import { ApiClientContext, createApiClient } from "../api/client"; import GolfPlayApp from "../components/GolfPlayApp"; export const meta: MetaFunction<typeof loader> = ({ data }) => [ @@ -20,29 +16,25 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => [ export async function loader({ params, request }: LoaderFunctionArgs) { const { token, user } = await ensureUserLoggedIn(request); + const apiClient = createApiClient(token); const gameId = Number(params.gameId); - const fetchGame = async () => { - return (await apiGetGame(token, gameId)).game; - }; - const fetchGameState = async () => { - return (await apiGetGamePlayLatestState(token, gameId)).state; - }; - - const [game, state] = await Promise.all([fetchGame(), fetchGameState()]); + const [{ game }, { state: gameState }] = await Promise.all([ + apiClient.getGame(gameId), + apiClient.getGamePlayLatestState(gameId), + ]); return { - apiAuthToken: token, + apiToken: token, game, player: user, - gameState: state, + gameState, }; } export default function GolfPlay() { - const { apiAuthToken, game, player, gameState } = - useLoaderData<typeof loader>(); + const { apiToken, game, player, gameState } = useLoaderData<typeof loader>(); const store = useMemo(() => { void game.game_id; @@ -52,14 +44,14 @@ export default function GolfPlay() { return ( <JotaiProvider store={store}> - <ApiAuthTokenContext.Provider value={apiAuthToken}> + <ApiClientContext.Provider value={createApiClient(apiToken)}> <GolfPlayApp key={game.game_id} game={game} player={player} initialGameState={gameState} /> - </ApiAuthTokenContext.Provider> + </ApiClientContext.Provider> </JotaiProvider> ); } diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx index cd01b17..07e8c9e 100644 --- a/frontend/app/routes/golf.$gameId.watch.tsx +++ b/frontend/app/routes/golf.$gameId.watch.tsx @@ -3,12 +3,7 @@ import { useMemo } from "react"; import type { LoaderFunctionArgs, MetaFunction } from "react-router"; import { useLoaderData } from "react-router"; import { ensureUserLoggedIn } from "../.server/auth"; -import { - ApiAuthTokenContext, - apiGetGame, - apiGetGameWatchLatestStates, - apiGetGameWatchRanking, -} from "../api/client"; +import { ApiClientContext, createApiClient } from "../api/client"; import GolfWatchApp from "../components/GolfWatchApp"; export const meta: MetaFunction<typeof loader> = ({ data }) => [ @@ -21,27 +16,18 @@ export const meta: MetaFunction<typeof loader> = ({ data }) => [ export async function loader({ params, request }: LoaderFunctionArgs) { const { token } = await ensureUserLoggedIn(request); + const apiClient = createApiClient(token); const gameId = Number(params.gameId); - const fetchGame = async () => { - return (await apiGetGame(token, gameId)).game; - }; - const fetchRanking = async () => { - return (await apiGetGameWatchRanking(token, gameId)).ranking; - }; - const fetchGameStates = async () => { - return (await apiGetGameWatchLatestStates(token, gameId)).states; - }; - - const [game, ranking, gameStates] = await Promise.all([ - fetchGame(), - fetchRanking(), - fetchGameStates(), + const [{ game }, { ranking }, { states: gameStates }] = await Promise.all([ + await apiClient.getGame(gameId), + await apiClient.getGameWatchRanking(gameId), + await apiClient.getGameWatchLatestStates(gameId), ]); return { - apiAuthToken: token, + apiToken: token, game, ranking, gameStates, @@ -49,7 +35,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } export default function GolfWatch() { - const { apiAuthToken, game, ranking, gameStates } = + const { apiToken, game, ranking, gameStates } = useLoaderData<typeof loader>(); const store = useMemo(() => { @@ -59,14 +45,14 @@ export default function GolfWatch() { return ( <JotaiProvider store={store}> - <ApiAuthTokenContext.Provider value={apiAuthToken}> + <ApiClientContext.Provider value={createApiClient(apiToken)}> <GolfWatchApp key={game.game_id} game={game} initialGameStates={gameStates} initialRanking={ranking} /> - </ApiAuthTokenContext.Provider> + </ApiClientContext.Provider> </JotaiProvider> ); } |
