aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/.server/auth.ts4
-rw-r--r--frontend/app/api/client.ts168
-rw-r--r--frontend/app/components/GolfPlayApp.tsx21
-rw-r--r--frontend/app/components/GolfWatchApp.tsx21
-rw-r--r--frontend/app/routes/dashboard.tsx6
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx30
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx34
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>
);
}