From 00e50b2dcfed209669c46da54dc07905d65887b8 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Thu, 1 Aug 2024 22:07:29 +0900
Subject: chore(frontend): [biome] organize imports

---
 frontend/app/entry.client.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'frontend/app/entry.client.tsx')

diff --git a/frontend/app/entry.client.tsx b/frontend/app/entry.client.tsx
index 442da4f..7860ee3 100644
--- a/frontend/app/entry.client.tsx
+++ b/frontend/app/entry.client.tsx
@@ -1,5 +1,5 @@
 import { RemixBrowser } from "@remix-run/react";
-import { startTransition, StrictMode } from "react";
+import { StrictMode, startTransition } from "react";
 import { hydrateRoot } from "react-dom/client";
 
 startTransition(() => {
-- 
cgit v1.2.3-70-g09d2


From 8f2cceacc8fde328033de7f05bb12e7b1246dd86 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Thu, 1 Aug 2024 22:22:12 +0900
Subject: chore(frontend): [biome] format

---
 frontend/.eslintrc.cjs                             |  142 +--
 frontend/app/.server/api/client.ts                 |  130 +--
 frontend/app/.server/auth.ts                       |  166 +--
 frontend/app/.server/session.ts                    |   18 +-
 frontend/app/components/GolfPlayApp.client.tsx     |  260 ++---
 .../GolfPlayApps/GolfPlayAppConnecting.tsx         |   18 +-
 .../GolfPlayApps/GolfPlayAppFinished.tsx           |   14 +-
 .../components/GolfPlayApps/GolfPlayAppGaming.tsx  |   68 +-
 .../GolfPlayApps/GolfPlayAppStarting.tsx           |   20 +-
 .../components/GolfPlayApps/GolfPlayAppWaiting.tsx | 1150 ++++++++++----------
 frontend/app/components/GolfWatchApp.client.tsx    |  218 ++--
 .../GolfWatchApps/GolfWatchAppConnecting.tsx       |   18 +-
 .../GolfWatchApps/GolfWatchAppFinished.tsx         |   14 +-
 .../GolfWatchApps/GolfWatchAppGaming.tsx           |   70 +-
 .../GolfWatchApps/GolfWatchAppStarting.tsx         |   20 +-
 .../GolfWatchApps/GolfWatchAppWaiting.tsx          |    2 +-
 frontend/app/entry.client.tsx                      |   12 +-
 frontend/app/entry.server.tsx                      |  190 ++--
 frontend/app/root.tsx                              |   44 +-
 frontend/app/routes/_index.tsx                     |   36 +-
 frontend/app/routes/admin.dashboard.tsx            |   38 +-
 frontend/app/routes/admin.games.tsx                |   52 +-
 frontend/app/routes/admin.games_.$gameId.tsx       |  172 +--
 frontend/app/routes/admin.users.tsx                |   50 +-
 frontend/app/routes/dashboard.tsx                  |  122 +--
 frontend/app/routes/golf.$gameId.play.tsx          |   54 +-
 frontend/app/routes/golf.$gameId.watch.tsx         |   54 +-
 frontend/app/routes/login.tsx                      |  116 +-
 frontend/app/routes/logout.tsx                     |    4 +-
 frontend/biome.json                                |   31 +-
 frontend/package.json                              |  105 +-
 frontend/postcss.config.js                         |    8 +-
 frontend/tailwind.config.ts                        |   10 +-
 frontend/tsconfig.json                             |   58 +-
 frontend/vite.config.ts                            |   20 +-
 35 files changed, 1752 insertions(+), 1752 deletions(-)

(limited to 'frontend/app/entry.client.tsx')

diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index b87e395..aa810b3 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -1,78 +1,78 @@
 /** @type {import('eslint').Linter.Config} */
 module.exports = {
-  root: true,
-  parserOptions: {
-    ecmaVersion: "latest",
-    sourceType: "module",
-    ecmaFeatures: {
-      jsx: true,
-    },
-  },
-  env: {
-    browser: true,
-    commonjs: true,
-    es6: true,
-  },
-  ignorePatterns: ["!**/.server", "!**/.client"],
+	root: true,
+	parserOptions: {
+		ecmaVersion: "latest",
+		sourceType: "module",
+		ecmaFeatures: {
+			jsx: true,
+		},
+	},
+	env: {
+		browser: true,
+		commonjs: true,
+		es6: true,
+	},
+	ignorePatterns: ["!**/.server", "!**/.client"],
 
-  // Base config
-  extends: ["eslint:recommended"],
+	// Base config
+	extends: ["eslint:recommended"],
 
-  overrides: [
-    // React
-    {
-      files: ["**/*.{js,jsx,ts,tsx}"],
-      plugins: ["react", "jsx-a11y"],
-      extends: [
-        "plugin:react/recommended",
-        "plugin:react/jsx-runtime",
-        "plugin:react-hooks/recommended",
-        "plugin:jsx-a11y/recommended",
-      ],
-      settings: {
-        react: {
-          version: "detect",
-        },
-        formComponents: ["Form"],
-        linkComponents: [
-          { name: "Link", linkAttribute: "to" },
-          { name: "NavLink", linkAttribute: "to" },
-        ],
-        "import/resolver": {
-          typescript: {},
-        },
-      },
-    },
+	overrides: [
+		// React
+		{
+			files: ["**/*.{js,jsx,ts,tsx}"],
+			plugins: ["react", "jsx-a11y"],
+			extends: [
+				"plugin:react/recommended",
+				"plugin:react/jsx-runtime",
+				"plugin:react-hooks/recommended",
+				"plugin:jsx-a11y/recommended",
+			],
+			settings: {
+				react: {
+					version: "detect",
+				},
+				formComponents: ["Form"],
+				linkComponents: [
+					{ name: "Link", linkAttribute: "to" },
+					{ name: "NavLink", linkAttribute: "to" },
+				],
+				"import/resolver": {
+					typescript: {},
+				},
+			},
+		},
 
-    // Typescript
-    {
-      files: ["**/*.{ts,tsx}"],
-      plugins: ["@typescript-eslint", "import"],
-      parser: "@typescript-eslint/parser",
-      settings: {
-        "import/internal-regex": "^~/",
-        "import/resolver": {
-          node: {
-            extensions: [".ts", ".tsx"],
-          },
-          typescript: {
-            alwaysTryTypes: true,
-          },
-        },
-      },
-      extends: [
-        "plugin:@typescript-eslint/recommended",
-        "plugin:import/recommended",
-        "plugin:import/typescript",
-      ],
-    },
+		// Typescript
+		{
+			files: ["**/*.{ts,tsx}"],
+			plugins: ["@typescript-eslint", "import"],
+			parser: "@typescript-eslint/parser",
+			settings: {
+				"import/internal-regex": "^~/",
+				"import/resolver": {
+					node: {
+						extensions: [".ts", ".tsx"],
+					},
+					typescript: {
+						alwaysTryTypes: true,
+					},
+				},
+			},
+			extends: [
+				"plugin:@typescript-eslint/recommended",
+				"plugin:import/recommended",
+				"plugin:import/typescript",
+			],
+		},
 
-    // Node
-    {
-      files: [".eslintrc.cjs"],
-      env: {
-        node: true,
-      },
-    },
-  ],
+		// Node
+		{
+			files: [".eslintrc.cjs"],
+			env: {
+				node: true,
+			},
+		},
+	],
 };
diff --git a/frontend/app/.server/api/client.ts b/frontend/app/.server/api/client.ts
index b3c7e9c..a78180b 100644
--- a/frontend/app/.server/api/client.ts
+++ b/frontend/app/.server/api/client.ts
@@ -2,94 +2,94 @@ import createClient from "openapi-fetch";
 import type { operations, paths } from "./schema";
 
 const apiClient = createClient<paths>({
-  baseUrl:
-    process.env.NODE_ENV === "development"
-      ? "http://localhost:8002/api/"
-      : "http://api-server/api/",
+	baseUrl:
+		process.env.NODE_ENV === "development"
+			? "http://localhost:8002/api/"
+			: "http://api-server/api/",
 });
 
 export async function apiPostLogin(username: string, password: string) {
-  const { data, error } = await apiClient.POST("/login", {
-    body: { username, password },
-  });
-  if (error) throw new Error(error.message);
-  return data;
+	const { data, error } = await apiClient.POST("/login", {
+		body: { username, password },
+	});
+	if (error) throw new Error(error.message);
+	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;
+	const { data, error } = await apiClient.GET("/games", {
+		params: {
+			header: { Authorization: `Bearer ${token}` },
+		},
+	});
+	if (error) throw new Error(error.message);
+	return data;
 }
 
 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;
+	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;
 }
 
 export async function apiGetToken(token: string) {
-  const { data, error } = await apiClient.GET("/token", {
-    params: {
-      header: { Authorization: `Bearer ${token}` },
-    },
-  });
-  if (error) throw new Error(error.message);
-  return data;
+	const { data, error } = await apiClient.GET("/token", {
+		params: {
+			header: { Authorization: `Bearer ${token}` },
+		},
+	});
+	if (error) throw new Error(error.message);
+	return data;
 }
 
 export async function adminApiGetUsers(token: string) {
-  const { data, error } = await apiClient.GET("/admin/users", {
-    params: {
-      header: { Authorization: `Bearer ${token}` },
-    },
-  });
-  if (error) throw new Error(error.message);
-  return data;
+	const { data, error } = await apiClient.GET("/admin/users", {
+		params: {
+			header: { Authorization: `Bearer ${token}` },
+		},
+	});
+	if (error) throw new Error(error.message);
+	return data;
 }
 
 export async function adminApiGetGames(token: string) {
-  const { data, error } = await apiClient.GET("/admin/games", {
-    params: {
-      header: { Authorization: `Bearer ${token}` },
-    },
-  });
-  if (error) throw new Error(error.message);
-  return data;
+	const { data, error } = await apiClient.GET("/admin/games", {
+		params: {
+			header: { Authorization: `Bearer ${token}` },
+		},
+	});
+	if (error) throw new Error(error.message);
+	return data;
 }
 
 export async function adminApiGetGame(token: string, gameId: number) {
-  const { data, error } = await apiClient.GET("/admin/games/{game_id}", {
-    params: {
-      header: { Authorization: `Bearer ${token}` },
-      path: { game_id: gameId },
-    },
-  });
-  if (error) throw new Error(error.message);
-  return data;
+	const { data, error } = await apiClient.GET("/admin/games/{game_id}", {
+		params: {
+			header: { Authorization: `Bearer ${token}` },
+			path: { game_id: gameId },
+		},
+	});
+	if (error) throw new Error(error.message);
+	return data;
 }
 
 export async function adminApiPutGame(
-  token: string,
-  gameId: number,
-  body: operations["adminPutGame"]["requestBody"]["content"]["application/json"],
+	token: string,
+	gameId: number,
+	body: operations["adminPutGame"]["requestBody"]["content"]["application/json"],
 ) {
-  const { data, error } = await apiClient.PUT("/admin/games/{game_id}", {
-    params: {
-      header: { Authorization: `Bearer ${token}` },
-      path: { game_id: gameId },
-    },
-    body,
-  });
-  if (error) throw new Error(error.message);
-  return data;
+	const { data, error } = await apiClient.PUT("/admin/games/{game_id}", {
+		params: {
+			header: { Authorization: `Bearer ${token}` },
+			path: { game_id: gameId },
+		},
+		body,
+	});
+	if (error) throw new Error(error.message);
+	return data;
 }
diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts
index 3d9a492..0c7742a 100644
--- a/frontend/app/.server/auth.ts
+++ b/frontend/app/.server/auth.ts
@@ -9,105 +9,105 @@ import { sessionStorage } from "./session";
 export const authenticator = new Authenticator<string>(sessionStorage);
 
 async function login(username: string, password: string): Promise<string> {
-  return (await apiPostLogin(username, password)).token;
+	return (await apiPostLogin(username, password)).token;
 }
 
 authenticator.use(
-  new FormStrategy(async ({ form }) => {
-    const username = String(form.get("username"));
-    const password = String(form.get("password"));
-    return await login(username, password);
-  }),
-  "default",
+	new FormStrategy(async ({ form }) => {
+		const username = String(form.get("username"));
+		const password = String(form.get("password"));
+		return await login(username, password);
+	}),
+	"default",
 );
 
 export type User = components["schemas"]["User"];
 
 export async function isAuthenticated(
-  request: Request | Session,
-  options?: {
-    successRedirect?: never;
-    failureRedirect?: never;
-    headers?: never;
-  },
+	request: Request | Session,
+	options?: {
+		successRedirect?: never;
+		failureRedirect?: never;
+		headers?: never;
+	},
 ): Promise<{ user: User; token: string } | null>;
 export async function isAuthenticated(
-  request: Request | Session,
-  options: {
-    successRedirect: string;
-    failureRedirect?: never;
-    headers?: HeadersInit;
-  },
+	request: Request | Session,
+	options: {
+		successRedirect: string;
+		failureRedirect?: never;
+		headers?: HeadersInit;
+	},
 ): Promise<null>;
 export async function isAuthenticated(
-  request: Request | Session,
-  options: {
-    successRedirect?: never;
-    failureRedirect: string;
-    headers?: HeadersInit;
-  },
+	request: Request | Session,
+	options: {
+		successRedirect?: never;
+		failureRedirect: string;
+		headers?: HeadersInit;
+	},
 ): Promise<{ user: User; token: string }>;
 export async function isAuthenticated(
-  request: Request | Session,
-  options: {
-    successRedirect: string;
-    failureRedirect: string;
-    headers?: HeadersInit;
-  },
+	request: Request | Session,
+	options: {
+		successRedirect: string;
+		failureRedirect: string;
+		headers?: HeadersInit;
+	},
 ): Promise<null>;
 export async function isAuthenticated(
-  request: Request | Session,
-  options:
-    | {
-        successRedirect?: never;
-        failureRedirect?: never;
-        headers?: never;
-      }
-    | {
-        successRedirect: string;
-        failureRedirect?: never;
-        headers?: HeadersInit;
-      }
-    | {
-        successRedirect?: never;
-        failureRedirect: string;
-        headers?: HeadersInit;
-      }
-    | {
-        successRedirect: string;
-        failureRedirect: string;
-        headers?: HeadersInit;
-      } = {},
+	request: Request | Session,
+	options:
+		| {
+				successRedirect?: never;
+				failureRedirect?: never;
+				headers?: never;
+		  }
+		| {
+				successRedirect: string;
+				failureRedirect?: never;
+				headers?: HeadersInit;
+		  }
+		| {
+				successRedirect?: never;
+				failureRedirect: string;
+				headers?: HeadersInit;
+		  }
+		| {
+				successRedirect: string;
+				failureRedirect: string;
+				headers?: HeadersInit;
+		  } = {},
 ): Promise<{ user: User; token: string } | null> {
-  // This function's signature should be compatible with `authenticator.isAuthenticated` but TypeScript does not infer it correctly.
-  let jwt;
-  const { successRedirect, failureRedirect, headers } = options;
-  if (successRedirect && failureRedirect) {
-    jwt = await authenticator.isAuthenticated(request, {
-      successRedirect,
-      failureRedirect,
-      headers,
-    });
-  } else if (!successRedirect && failureRedirect) {
-    jwt = await authenticator.isAuthenticated(request, {
-      failureRedirect,
-      headers,
-    });
-  } else if (successRedirect && !failureRedirect) {
-    jwt = await authenticator.isAuthenticated(request, {
-      successRedirect,
-      headers,
-    });
-  } else {
-    jwt = await authenticator.isAuthenticated(request);
-  }
+	// This function's signature should be compatible with `authenticator.isAuthenticated` but TypeScript does not infer it correctly.
+	let jwt;
+	const { successRedirect, failureRedirect, headers } = options;
+	if (successRedirect && failureRedirect) {
+		jwt = await authenticator.isAuthenticated(request, {
+			successRedirect,
+			failureRedirect,
+			headers,
+		});
+	} else if (!successRedirect && failureRedirect) {
+		jwt = await authenticator.isAuthenticated(request, {
+			failureRedirect,
+			headers,
+		});
+	} else if (successRedirect && !failureRedirect) {
+		jwt = await authenticator.isAuthenticated(request, {
+			successRedirect,
+			headers,
+		});
+	} else {
+		jwt = await authenticator.isAuthenticated(request);
+	}
 
-  if (!jwt) {
-    return null;
-  }
-  const user = jwtDecode<User>(jwt);
-  return {
-    user,
-    token: jwt,
-  };
+	if (!jwt) {
+		return null;
+	}
+	const user = jwtDecode<User>(jwt);
+	return {
+		user,
+		token: jwt,
+	};
 }
diff --git a/frontend/app/.server/session.ts b/frontend/app/.server/session.ts
index 2000853..79810f4 100644
--- a/frontend/app/.server/session.ts
+++ b/frontend/app/.server/session.ts
@@ -1,13 +1,13 @@
 import { createCookieSessionStorage } from "@remix-run/node";
 
 export const sessionStorage = createCookieSessionStorage({
-  cookie: {
-    name: "albatross_session",
-    sameSite: "lax",
-    path: "/",
-    httpOnly: true,
-    secrets: ["TODO"],
-    // secure: process.env.NODE_ENV === "production",
-    secure: false, // TODO
-  },
+	cookie: {
+		name: "albatross_session",
+		sameSite: "lax",
+		path: "/",
+		httpOnly: true,
+		secrets: ["TODO"],
+		// secure: process.env.NODE_ENV === "production",
+		secure: false, // TODO
+	},
 });
diff --git a/frontend/app/components/GolfPlayApp.client.tsx b/frontend/app/components/GolfPlayApp.client.tsx
index 0ab8fff..ace0710 100644
--- a/frontend/app/components/GolfPlayApp.client.tsx
+++ b/frontend/app/components/GolfPlayApp.client.tsx
@@ -16,136 +16,136 @@ type Problem = components["schemas"]["Problem"];
 type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished";
 
 export default function GolfPlayApp({
-  game,
-  sockToken,
+	game,
+	sockToken,
 }: {
-  game: Game;
-  sockToken: string;
+	game: Game;
+	sockToken: string;
 }) {
-  // const socketUrl = `wss://t.nil.ninja/iosdc-japan/2024/sock/golf/${game.game_id}/play?token=${sockToken}`;
-  const socketUrl =
-    process.env.NODE_ENV === "development"
-      ? `ws://localhost:8002/sock/golf/${game.game_id}/play?token=${sockToken}`
-      : `ws://api-server/sock/golf/${game.game_id}/play?token=${sockToken}`;
-
-  const { sendJsonMessage, lastJsonMessage, readyState } =
-    useWebSocket<WebSocketMessage>(socketUrl, {});
-
-  const [gameState, setGameState] = useState<GameState>("connecting");
-
-  const [problem, setProblem] = useState<Problem | null>(null);
-
-  const [startedAt, setStartedAt] = useState<number | null>(null);
-
-  const [timeLeftSeconds, setTimeLeftSeconds] = useState<number | null>(null);
-
-  useEffect(() => {
-    if (gameState === "starting" && startedAt !== null) {
-      const timer1 = setInterval(() => {
-        setTimeLeftSeconds((prev) => {
-          if (prev === null) {
-            return null;
-          }
-          if (prev <= 1) {
-            clearInterval(timer1);
-            setGameState("gaming");
-            return 0;
-          }
-          return prev - 1;
-        });
-      }, 1000);
-
-      const timer2 = setInterval(() => {
-        const nowSec = Math.floor(Date.now() / 1000);
-        const finishedAt = startedAt + game.duration_seconds;
-        if (nowSec >= finishedAt) {
-          clearInterval(timer2);
-          setGameState("finished");
-        }
-      }, 1000);
-
-      return () => {
-        clearInterval(timer1);
-        clearInterval(timer2);
-      };
-    }
-  }, [gameState, startedAt, game.duration_seconds]);
-
-  const [currentScore, setCurrentScore] = useState<number | null>(null);
-
-  const onCodeChange = useDebouncedCallback((code: string) => {
-    console.log("player:c2s:code");
-    sendJsonMessage({
-      type: "player:c2s:code",
-      data: { code },
-    });
-  }, 1000);
-
-  if (readyState === ReadyState.UNINSTANTIATED) {
-    throw new Error("WebSocket is not connected");
-  }
-
-  useEffect(() => {
-    if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) {
-      if (gameState !== "finished") {
-        setGameState("connecting");
-      }
-    } else if (readyState === ReadyState.CONNECTING) {
-      setGameState("connecting");
-    } else if (readyState === ReadyState.OPEN) {
-      if (lastJsonMessage !== null) {
-        console.log(lastJsonMessage.type);
-        if (lastJsonMessage.type === "player:s2c:prepare") {
-          const { problem } = lastJsonMessage.data;
-          setProblem(problem);
-          console.log("player:c2s:ready");
-          sendJsonMessage({ type: "player:c2s:ready" });
-        } else if (lastJsonMessage.type === "player:s2c:start") {
-          if (
-            gameState !== "starting" &&
-            gameState !== "gaming" &&
-            gameState !== "finished"
-          ) {
-            const { start_at } = lastJsonMessage.data;
-            setStartedAt(start_at);
-            const nowSec = Math.floor(Date.now() / 1000);
-            setTimeLeftSeconds(start_at - nowSec);
-            setGameState("starting");
-          }
-        } else if (lastJsonMessage.type === "player:s2c:execresult") {
-          const { score } = lastJsonMessage.data;
-          if (
-            score !== null &&
-            (currentScore === null || score < currentScore)
-          ) {
-            setCurrentScore(score);
-          }
-        }
-      } else {
-        setGameState("waiting");
-        console.log("player:c2s:entry");
-        sendJsonMessage({ type: "player:c2s:entry" });
-      }
-    }
-  }, [sendJsonMessage, lastJsonMessage, readyState, gameState, currentScore]);
-
-  if (gameState === "connecting") {
-    return <GolfPlayAppConnecting />;
-  } else if (gameState === "waiting") {
-    return <GolfPlayAppWaiting />;
-  } else if (gameState === "starting") {
-    return <GolfPlayAppStarting timeLeft={timeLeftSeconds!} />;
-  } else if (gameState === "gaming") {
-    return (
-      <GolfPlayAppGaming
-        problem={problem!.description}
-        onCodeChange={onCodeChange}
-        currentScore={currentScore}
-      />
-    );
-  } else if (gameState === "finished") {
-    return <GolfPlayAppFinished />;
-  } else {
-    return null;
-  }
+	// const socketUrl = `wss://t.nil.ninja/iosdc-japan/2024/sock/golf/${game.game_id}/play?token=${sockToken}`;
+	const socketUrl =
+		process.env.NODE_ENV === "development"
+			? `ws://localhost:8002/sock/golf/${game.game_id}/play?token=${sockToken}`
+			: `ws://api-server/sock/golf/${game.game_id}/play?token=${sockToken}`;
+
+	const { sendJsonMessage, lastJsonMessage, readyState } =
+		useWebSocket<WebSocketMessage>(socketUrl, {});
+
+	const [gameState, setGameState] = useState<GameState>("connecting");
+
+	const [problem, setProblem] = useState<Problem | null>(null);
+
+	const [startedAt, setStartedAt] = useState<number | null>(null);
+
+	const [timeLeftSeconds, setTimeLeftSeconds] = useState<number | null>(null);
+
+	useEffect(() => {
+		if (gameState === "starting" && startedAt !== null) {
+			const timer1 = setInterval(() => {
+				setTimeLeftSeconds((prev) => {
+					if (prev === null) {
+						return null;
+					}
+					if (prev <= 1) {
+						clearInterval(timer1);
+						setGameState("gaming");
+						return 0;
+					}
+					return prev - 1;
+				});
+			}, 1000);
+
+			const timer2 = setInterval(() => {
+				const nowSec = Math.floor(Date.now() / 1000);
+				const finishedAt = startedAt + game.duration_seconds;
+				if (nowSec >= finishedAt) {
+					clearInterval(timer2);
+					setGameState("finished");
+				}
+			}, 1000);
+
+			return () => {
+				clearInterval(timer1);
+				clearInterval(timer2);
+			};
+		}
+	}, [gameState, startedAt, game.duration_seconds]);
+
+	const [currentScore, setCurrentScore] = useState<number | null>(null);
+
+	const onCodeChange = useDebouncedCallback((code: string) => {
+		console.log("player:c2s:code");
+		sendJsonMessage({
+			type: "player:c2s:code",
+			data: { code },
+		});
+	}, 1000);
+
+	if (readyState === ReadyState.UNINSTANTIATED) {
+		throw new Error("WebSocket is not connected");
+	}
+
+	useEffect(() => {
+		if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) {
+			if (gameState !== "finished") {
+				setGameState("connecting");
+			}
+		} else if (readyState === ReadyState.CONNECTING) {
+			setGameState("connecting");
+		} else if (readyState === ReadyState.OPEN) {
+			if (lastJsonMessage !== null) {
+				console.log(lastJsonMessage.type);
+				if (lastJsonMessage.type === "player:s2c:prepare") {
+					const { problem } = lastJsonMessage.data;
+					setProblem(problem);
+					console.log("player:c2s:ready");
+					sendJsonMessage({ type: "player:c2s:ready" });
+				} else if (lastJsonMessage.type === "player:s2c:start") {
+					if (
+						gameState !== "starting" &&
+						gameState !== "gaming" &&
+						gameState !== "finished"
+					) {
+						const { start_at } = lastJsonMessage.data;
+						setStartedAt(start_at);
+						const nowSec = Math.floor(Date.now() / 1000);
+						setTimeLeftSeconds(start_at - nowSec);
+						setGameState("starting");
+					}
+				} else if (lastJsonMessage.type === "player:s2c:execresult") {
+					const { score } = lastJsonMessage.data;
+					if (
+						score !== null &&
+						(currentScore === null || score < currentScore)
+					) {
+						setCurrentScore(score);
+					}
+				}
+			} else {
+				setGameState("waiting");
+				console.log("player:c2s:entry");
+				sendJsonMessage({ type: "player:c2s:entry" });
+			}
+		}
+	}, [sendJsonMessage, lastJsonMessage, readyState, gameState, currentScore]);
+
+	if (gameState === "connecting") {
+		return <GolfPlayAppConnecting />;
+	} else if (gameState === "waiting") {
+		return <GolfPlayAppWaiting />;
+	} else if (gameState === "starting") {
+		return <GolfPlayAppStarting timeLeft={timeLeftSeconds!} />;
+	} else if (gameState === "gaming") {
+		return (
+			<GolfPlayAppGaming
+				problem={problem!.description}
+				onCodeChange={onCodeChange}
+				currentScore={currentScore}
+			/>
+		);
+	} else if (gameState === "finished") {
+		return <GolfPlayAppFinished />;
+	} else {
+		return null;
+	}
 }
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx
index 6a954c1..d97131b 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx
@@ -1,11 +1,11 @@
 export default function GolfPlayAppConnecting() {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-black-600 mb-4">
-          Connecting...
-        </h1>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<div className="text-center">
+				<h1 className="text-4xl font-bold text-black-600 mb-4">
+					Connecting...
+				</h1>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx
index db06890..c3ef2d4 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx
@@ -1,9 +1,9 @@
 export default function GolfPlayAppFinished() {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-black-600 mb-4">Finished</h1>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<div className="text-center">
+				<h1 className="text-4xl font-bold text-black-600 mb-4">Finished</h1>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
index fc672f5..75ab18e 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx
@@ -1,41 +1,41 @@
 type Props = {
-  problem: string;
-  onCodeChange: (code: string) => void;
-  currentScore: number | null;
+	problem: string;
+	onCodeChange: (code: string) => void;
+	currentScore: number | null;
 };
 
 export default function GolfPlayAppGaming({
-  problem,
-  onCodeChange,
-  currentScore,
+	problem,
+	onCodeChange,
+	currentScore,
 }: Props) {
-  const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-    onCodeChange(e.target.value);
-  };
+	const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+		onCodeChange(e.target.value);
+	};
 
-  return (
-    <div className="min-h-screen flex">
-      <div className="mx-auto flex min-h-full flex-grow">
-        <div className="flex w-1/2 flex-col justify-between p-4">
-          <div>
-            <div className="mb-2 text-xl font-bold">TODO</div>
-            <div className="text-gray-700">{problem}</div>
-          </div>
-          <div className="mb-4 mt-auto">
-            <div className="font-semibold text-green-500">
-              Score: {currentScore == null ? "-" : `${currentScore}`}
-            </div>
-          </div>
-        </div>
-        <div className="w-1/2 p-4 flex">
-          <div className="flex-grow">
-            <textarea
-              className="h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
-              onChange={handleTextChange}
-            ></textarea>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen flex">
+			<div className="mx-auto flex min-h-full flex-grow">
+				<div className="flex w-1/2 flex-col justify-between p-4">
+					<div>
+						<div className="mb-2 text-xl font-bold">TODO</div>
+						<div className="text-gray-700">{problem}</div>
+					</div>
+					<div className="mb-4 mt-auto">
+						<div className="font-semibold text-green-500">
+							Score: {currentScore == null ? "-" : `${currentScore}`}
+						</div>
+					</div>
+				</div>
+				<div className="w-1/2 p-4 flex">
+					<div className="flex-grow">
+						<textarea
+							className="h-full w-full rounded-lg border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
+							onChange={handleTextChange}
+						></textarea>
+					</div>
+				</div>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
index df179f1..0d60fc9 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx
@@ -1,15 +1,15 @@
 type Props = {
-  timeLeft: number;
+	timeLeft: number;
 };
 
 export default function GolfPlayAppStarting({ timeLeft }: Props) {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-black-600 mb-4">
-          Starting... ({timeLeft} s)
-        </h1>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<div className="text-center">
+				<h1 className="text-4xl font-bold text-black-600 mb-4">
+					Starting... ({timeLeft} s)
+				</h1>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
index f4fc6c5..a1da33e 100644
--- a/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
+++ b/frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx
@@ -1,584 +1,584 @@
 export default function GolfPlayAppWaiting() {
-  return (
-    <>
-      <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-        <div className="text-center">
-          <h1 className="text-4xl font-bold text-black-600 mb-4">Waiting...</h1>
-        </div>
-      </div>
-      <style>
-        {`
+	return (
+		<>
+			<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+				<div className="text-center">
+					<h1 className="text-4xl font-bold text-black-600 mb-4">Waiting...</h1>
+				</div>
+			</div>
+			<style>
+				{`
         @keyframes changeHeight {
           0% { height: 20%; }
           50% { height: 100%; }
           100% { height: 20%; }
         }
       `}
-      </style>
-      <div
-        style={{
-          position: "fixed",
-          bottom: 0,
-          width: "100%",
-          display: "flex",
-          justifyContent: "center",
-          alignItems: "flex-end",
-          height: "100px",
-          margin: "0 2px",
-        }}
-      >
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "2.0s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.9s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.8s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.7s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.6s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.5s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.4s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.3s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.2s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.1s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.0s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.9s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.8s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.7s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.6s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.5s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.4s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.3s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.2s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.1s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.5s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.4s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.3s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.2s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.1s",
-          }}
-        ></div>
+			</style>
+			<div
+				style={{
+					position: "fixed",
+					bottom: 0,
+					width: "100%",
+					display: "flex",
+					justifyContent: "center",
+					alignItems: "flex-end",
+					height: "100px",
+					margin: "0 2px",
+				}}
+			>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "2.0s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.9s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.8s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.7s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.6s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.5s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.4s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.3s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.2s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.1s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.0s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.9s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.8s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.7s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.6s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.5s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.4s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.3s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.2s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.1s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.5s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.4s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.3s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.2s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.1s",
+					}}
+				></div>
 
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.1s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.2s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.3s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.4s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.5s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.1s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.2s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.3s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.4s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.5s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.6s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.7s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.8s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "0.9s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.0s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.1s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.2s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.3s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.4s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.5s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.6s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.7s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.8s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "1.9s",
-          }}
-        ></div>
-        <div
-          style={{
-            width: "2%",
-            margin: "0 2px",
-            background:
-              "linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
-            display: "inline-block",
-            animation: "changeHeight 1s infinite ease-in-out",
-            animationDelay: "2.0s",
-          }}
-        ></div>
-      </div>
-    </>
-  );
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.1s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.2s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.3s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.4s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.5s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.1s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.2s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.3s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.4s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.5s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.6s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.7s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.8s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "0.9s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.0s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.1s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.2s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.3s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.4s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.5s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.6s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.7s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.8s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "1.9s",
+					}}
+				></div>
+				<div
+					style={{
+						width: "2%",
+						margin: "0 2px",
+						background:
+							"linear-gradient(345deg, rgb(230, 36, 136) 0%, rgb(240, 184, 106) 100%)",
+						display: "inline-block",
+						animation: "changeHeight 1s infinite ease-in-out",
+						animationDelay: "2.0s",
+					}}
+				></div>
+			</div>
+		</>
+	);
 }
diff --git a/frontend/app/components/GolfWatchApp.client.tsx b/frontend/app/components/GolfWatchApp.client.tsx
index 0373944..aefe945 100644
--- a/frontend/app/components/GolfWatchApp.client.tsx
+++ b/frontend/app/components/GolfWatchApp.client.tsx
@@ -14,126 +14,126 @@ type Game = components["schemas"]["Game"];
 type GameState = "connecting" | "waiting" | "starting" | "gaming" | "finished";
 
 export default function GolfWatchApp({
-  game,
-  sockToken,
+	game,
+	sockToken,
 }: {
-  game: Game;
-  sockToken: string;
+	game: Game;
+	sockToken: string;
 }) {
-  // const socketUrl = `wss://t.nil.ninja/iosdc-japan/2024/sock/golf/${game.game_id}/watch?token=${sockToken}`;
-  const socketUrl =
-    process.env.NODE_ENV === "development"
-      ? `ws://localhost:8002/sock/golf/${game.game_id}/watch?token=${sockToken}`
-      : `ws://api-server/sock/golf/${game.game_id}/watch?token=${sockToken}`;
+	// const socketUrl = `wss://t.nil.ninja/iosdc-japan/2024/sock/golf/${game.game_id}/watch?token=${sockToken}`;
+	const socketUrl =
+		process.env.NODE_ENV === "development"
+			? `ws://localhost:8002/sock/golf/${game.game_id}/watch?token=${sockToken}`
+			: `ws://api-server/sock/golf/${game.game_id}/watch?token=${sockToken}`;
 
-  const { lastJsonMessage, readyState } = useWebSocket<WebSocketMessage>(
-    socketUrl,
-    {},
-  );
+	const { lastJsonMessage, readyState } = useWebSocket<WebSocketMessage>(
+		socketUrl,
+		{},
+	);
 
-  const [gameState, setGameState] = useState<GameState>("connecting");
+	const [gameState, setGameState] = useState<GameState>("connecting");
 
-  const [startedAt, setStartedAt] = useState<number | null>(null);
+	const [startedAt, setStartedAt] = useState<number | null>(null);
 
-  const [timeLeftSeconds, setTimeLeftSeconds] = useState<number | null>(null);
+	const [timeLeftSeconds, setTimeLeftSeconds] = useState<number | null>(null);
 
-  useEffect(() => {
-    if (gameState === "starting" && startedAt !== null) {
-      const timer1 = setInterval(() => {
-        setTimeLeftSeconds((prev) => {
-          if (prev === null) {
-            return null;
-          }
-          if (prev <= 1) {
-            clearInterval(timer1);
-            setGameState("gaming");
-            return 0;
-          }
-          return prev - 1;
-        });
-      }, 1000);
+	useEffect(() => {
+		if (gameState === "starting" && startedAt !== null) {
+			const timer1 = setInterval(() => {
+				setTimeLeftSeconds((prev) => {
+					if (prev === null) {
+						return null;
+					}
+					if (prev <= 1) {
+						clearInterval(timer1);
+						setGameState("gaming");
+						return 0;
+					}
+					return prev - 1;
+				});
+			}, 1000);
 
-      const timer2 = setInterval(() => {
-        const nowSec = Math.floor(Date.now() / 1000);
-        const finishedAt = startedAt + game.duration_seconds;
-        if (nowSec >= finishedAt) {
-          clearInterval(timer2);
-          setGameState("finished");
-        }
-      }, 1000);
+			const timer2 = setInterval(() => {
+				const nowSec = Math.floor(Date.now() / 1000);
+				const finishedAt = startedAt + game.duration_seconds;
+				if (nowSec >= finishedAt) {
+					clearInterval(timer2);
+					setGameState("finished");
+				}
+			}, 1000);
 
-      return () => {
-        clearInterval(timer1);
-        clearInterval(timer2);
-      };
-    }
-  }, [gameState, startedAt, game.duration_seconds]);
+			return () => {
+				clearInterval(timer1);
+				clearInterval(timer2);
+			};
+		}
+	}, [gameState, startedAt, game.duration_seconds]);
 
-  const [scoreA, setScoreA] = useState<number | null>(null);
-  const [scoreB, setScoreB] = useState<number | null>(null);
-  const [codeA, setCodeA] = useState<string>("");
-  const [codeB, setCodeB] = useState<string>("");
+	const [scoreA, setScoreA] = useState<number | null>(null);
+	const [scoreB, setScoreB] = useState<number | null>(null);
+	const [codeA, setCodeA] = useState<string>("");
+	const [codeB, setCodeB] = useState<string>("");
 
-  if (readyState === ReadyState.UNINSTANTIATED) {
-    throw new Error("WebSocket is not connected");
-  }
+	if (readyState === ReadyState.UNINSTANTIATED) {
+		throw new Error("WebSocket is not connected");
+	}
 
-  useEffect(() => {
-    if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) {
-      if (gameState !== "finished") {
-        setGameState("connecting");
-      }
-    } else if (readyState === ReadyState.CONNECTING) {
-      setGameState("connecting");
-    } else if (readyState === ReadyState.OPEN) {
-      if (lastJsonMessage !== null) {
-        console.log(lastJsonMessage.type);
-        if (lastJsonMessage.type === "watcher:s2c:start") {
-          if (
-            gameState !== "starting" &&
-            gameState !== "gaming" &&
-            gameState !== "finished"
-          ) {
-            const { start_at } = lastJsonMessage.data;
-            setStartedAt(start_at);
-            const nowSec = Math.floor(Date.now() / 1000);
-            setTimeLeftSeconds(start_at - nowSec);
-            setGameState("starting");
-          }
-        } else if (lastJsonMessage.type === "watcher:s2c:code") {
-          const { player_id, code } = lastJsonMessage.data;
-          setCodeA(code);
-        } else if (lastJsonMessage.type === "watcher:s2c:execresult") {
-          const { score } = lastJsonMessage.data;
-          if (score !== null && (scoreA === null || score < scoreA)) {
-            setScoreA(score);
-          }
-        }
-      } else {
-        setGameState("waiting");
-      }
-    }
-  }, [lastJsonMessage, readyState, gameState, scoreA]);
+	useEffect(() => {
+		if (readyState === ReadyState.CLOSING || readyState === ReadyState.CLOSED) {
+			if (gameState !== "finished") {
+				setGameState("connecting");
+			}
+		} else if (readyState === ReadyState.CONNECTING) {
+			setGameState("connecting");
+		} else if (readyState === ReadyState.OPEN) {
+			if (lastJsonMessage !== null) {
+				console.log(lastJsonMessage.type);
+				if (lastJsonMessage.type === "watcher:s2c:start") {
+					if (
+						gameState !== "starting" &&
+						gameState !== "gaming" &&
+						gameState !== "finished"
+					) {
+						const { start_at } = lastJsonMessage.data;
+						setStartedAt(start_at);
+						const nowSec = Math.floor(Date.now() / 1000);
+						setTimeLeftSeconds(start_at - nowSec);
+						setGameState("starting");
+					}
+				} else if (lastJsonMessage.type === "watcher:s2c:code") {
+					const { player_id, code } = lastJsonMessage.data;
+					setCodeA(code);
+				} else if (lastJsonMessage.type === "watcher:s2c:execresult") {
+					const { score } = lastJsonMessage.data;
+					if (score !== null && (scoreA === null || score < scoreA)) {
+						setScoreA(score);
+					}
+				}
+			} else {
+				setGameState("waiting");
+			}
+		}
+	}, [lastJsonMessage, readyState, gameState, scoreA]);
 
-  if (gameState === "connecting") {
-    return <GolfWatchAppConnecting />;
-  } else if (gameState === "waiting") {
-    return <GolfWatchAppWaiting />;
-  } else if (gameState === "starting") {
-    return <GolfWatchAppStarting timeLeft={timeLeftSeconds!} />;
-  } else if (gameState === "gaming") {
-    return (
-      <GolfWatchAppGaming
-        problem={game.problem!.description}
-        codeA={codeA}
-        scoreA={scoreA}
-        codeB={codeB}
-        scoreB={scoreB}
-      />
-    );
-  } else if (gameState === "finished") {
-    return <GolfWatchAppFinished />;
-  } else {
-    return null;
-  }
+	if (gameState === "connecting") {
+		return <GolfWatchAppConnecting />;
+	} else if (gameState === "waiting") {
+		return <GolfWatchAppWaiting />;
+	} else if (gameState === "starting") {
+		return <GolfWatchAppStarting timeLeft={timeLeftSeconds!} />;
+	} else if (gameState === "gaming") {
+		return (
+			<GolfWatchAppGaming
+				problem={game.problem!.description}
+				codeA={codeA}
+				scoreA={scoreA}
+				codeB={codeB}
+				scoreB={scoreB}
+			/>
+		);
+	} else if (gameState === "finished") {
+		return <GolfWatchAppFinished />;
+	} else {
+		return null;
+	}
 }
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx
index b91339a..3704656 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx
@@ -1,11 +1,11 @@
 export default function GolfWatchAppConnecting() {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-black-600 mb-4">
-          Connecting...
-        </h1>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<div className="text-center">
+				<h1 className="text-4xl font-bold text-black-600 mb-4">
+					Connecting...
+				</h1>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx
index 866e18a..58cb2df 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx
@@ -1,9 +1,9 @@
 export default function GolfWatchAppFinished() {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-black-600 mb-4">Finished</h1>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<div className="text-center">
+				<h1 className="text-4xl font-bold text-black-600 mb-4">Finished</h1>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
index 10d68fe..22277f8 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx
@@ -1,41 +1,41 @@
 type Props = {
-  problem: string;
-  codeA: string;
-  scoreA: number | null;
-  codeB: string;
-  scoreB: number | null;
+	problem: string;
+	codeA: string;
+	scoreA: number | null;
+	codeB: string;
+	scoreB: number | null;
 };
 
 export default function GolfWatchAppGaming({
-  problem,
-  codeA,
-  scoreA,
-  codeB,
-  scoreB,
+	problem,
+	codeA,
+	scoreA,
+	codeB,
+	scoreB,
 }: Props) {
-  return (
-    <div style={{ display: "flex", flexDirection: "column" }}>
-      <div style={{ display: "flex", flex: 1, justifyContent: "center" }}>
-        <div>{problem}</div>
-      </div>
-      <div style={{ display: "flex", flex: 3 }}>
-        <div style={{ display: "flex", flex: 3, flexDirection: "column" }}>
-          <div style={{ flex: 1, justifyContent: "center" }}>{scoreA}</div>
-          <div style={{ flex: 3 }}>
-            <pre>
-              <code>{codeA}</code>
-            </pre>
-          </div>
-        </div>
-        <div style={{ display: "flex", flex: 3, flexDirection: "column" }}>
-          <div style={{ flex: 1, justifyContent: "center" }}>{scoreB}</div>
-          <div style={{ flex: 3 }}>
-            <pre>
-              <code>{codeB}</code>
-            </pre>
-          </div>
-        </div>
-      </div>
-    </div>
-  );
+	return (
+		<div style={{ display: "flex", flexDirection: "column" }}>
+			<div style={{ display: "flex", flex: 1, justifyContent: "center" }}>
+				<div>{problem}</div>
+			</div>
+			<div style={{ display: "flex", flex: 3 }}>
+				<div style={{ display: "flex", flex: 3, flexDirection: "column" }}>
+					<div style={{ flex: 1, justifyContent: "center" }}>{scoreA}</div>
+					<div style={{ flex: 3 }}>
+						<pre>
+							<code>{codeA}</code>
+						</pre>
+					</div>
+				</div>
+				<div style={{ display: "flex", flex: 3, flexDirection: "column" }}>
+					<div style={{ flex: 1, justifyContent: "center" }}>{scoreB}</div>
+					<div style={{ flex: 3 }}>
+						<pre>
+							<code>{codeB}</code>
+						</pre>
+					</div>
+				</div>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
index 3c9919c..ef72cec 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx
@@ -1,15 +1,15 @@
 type Props = {
-  timeLeft: number;
+	timeLeft: number;
 };
 
 export default function GolfWatchAppStarting({ timeLeft }: Props) {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-black-600 mb-4">
-          Starting... ({timeLeft} s)
-        </h1>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<div className="text-center">
+				<h1 className="text-4xl font-bold text-black-600 mb-4">
+					Starting... ({timeLeft} s)
+				</h1>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx
index 6733b3b..d58ec19 100644
--- a/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx
+++ b/frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx
@@ -1,3 +1,3 @@
 export default function GolfWatchAppWaiting() {
-  return <div>Waiting...</div>;
+	return <div>Waiting...</div>;
 }
diff --git a/frontend/app/entry.client.tsx b/frontend/app/entry.client.tsx
index 7860ee3..92d1585 100644
--- a/frontend/app/entry.client.tsx
+++ b/frontend/app/entry.client.tsx
@@ -3,10 +3,10 @@ import { StrictMode, startTransition } from "react";
 import { hydrateRoot } from "react-dom/client";
 
 startTransition(() => {
-  hydrateRoot(
-    document,
-    <StrictMode>
-      <RemixBrowser />
-    </StrictMode>,
-  );
+	hydrateRoot(
+		document,
+		<StrictMode>
+			<RemixBrowser />
+		</StrictMode>,
+	);
 });
diff --git a/frontend/app/entry.server.tsx b/frontend/app/entry.server.tsx
index 77b6f15..a1ffa99 100644
--- a/frontend/app/entry.server.tsx
+++ b/frontend/app/entry.server.tsx
@@ -9,116 +9,116 @@ import { renderToPipeableStream } from "react-dom/server";
 const ABORT_DELAY = 5_000;
 
 export default function handleRequest(
-  request: Request,
-  responseStatusCode: number,
-  responseHeaders: Headers,
-  remixContext: EntryContext,
+	request: Request,
+	responseStatusCode: number,
+	responseHeaders: Headers,
+	remixContext: EntryContext,
 ) {
-  return isbot(request.headers.get("user-agent") || "")
-    ? handleBotRequest(
-        request,
-        responseStatusCode,
-        responseHeaders,
-        remixContext,
-      )
-    : handleBrowserRequest(
-        request,
-        responseStatusCode,
-        responseHeaders,
-        remixContext,
-      );
+	return isbot(request.headers.get("user-agent") || "")
+		? handleBotRequest(
+				request,
+				responseStatusCode,
+				responseHeaders,
+				remixContext,
+			)
+		: handleBrowserRequest(
+				request,
+				responseStatusCode,
+				responseHeaders,
+				remixContext,
+			);
 }
 
 function handleBotRequest(
-  request: Request,
-  responseStatusCode: number,
-  responseHeaders: Headers,
-  remixContext: EntryContext,
+	request: Request,
+	responseStatusCode: number,
+	responseHeaders: Headers,
+	remixContext: EntryContext,
 ) {
-  return new Promise((resolve, reject) => {
-    let shellRendered = false;
-    const { pipe, abort } = renderToPipeableStream(
-      <RemixServer
-        context={remixContext}
-        url={request.url}
-        abortDelay={ABORT_DELAY}
-      />,
-      {
-        onAllReady() {
-          shellRendered = true;
-          const body = new PassThrough();
-          const stream = createReadableStreamFromReadable(body);
+	return new Promise((resolve, reject) => {
+		let shellRendered = false;
+		const { pipe, abort } = renderToPipeableStream(
+			<RemixServer
+				context={remixContext}
+				url={request.url}
+				abortDelay={ABORT_DELAY}
+			/>,
+			{
+				onAllReady() {
+					shellRendered = true;
+					const body = new PassThrough();
+					const stream = createReadableStreamFromReadable(body);
 
-          responseHeaders.set("Content-Type", "text/html");
+					responseHeaders.set("Content-Type", "text/html");
 
-          resolve(
-            new Response(stream, {
-              headers: responseHeaders,
-              status: responseStatusCode,
-            }),
-          );
+					resolve(
+						new Response(stream, {
+							headers: responseHeaders,
+							status: responseStatusCode,
+						}),
+					);
 
-          pipe(body);
-        },
-        onShellError(error: unknown) {
-          reject(error);
-        },
-        onError(error: unknown) {
-          responseStatusCode = 500;
-          if (shellRendered) {
-            console.error(error);
-          }
-        },
-      },
-    );
+					pipe(body);
+				},
+				onShellError(error: unknown) {
+					reject(error);
+				},
+				onError(error: unknown) {
+					responseStatusCode = 500;
+					if (shellRendered) {
+						console.error(error);
+					}
+				},
+			},
+		);
 
-    setTimeout(abort, ABORT_DELAY);
-  });
+		setTimeout(abort, ABORT_DELAY);
+	});
 }
 
 function handleBrowserRequest(
-  request: Request,
-  responseStatusCode: number,
-  responseHeaders: Headers,
-  remixContext: EntryContext,
+	request: Request,
+	responseStatusCode: number,
+	responseHeaders: Headers,
+	remixContext: EntryContext,
 ) {
-  return new Promise((resolve, reject) => {
-    let shellRendered = false;
-    const { pipe, abort } = renderToPipeableStream(
-      <RemixServer
-        context={remixContext}
-        url={request.url}
-        abortDelay={ABORT_DELAY}
-      />,
-      {
-        onShellReady() {
-          shellRendered = true;
-          const body = new PassThrough();
-          const stream = createReadableStreamFromReadable(body);
+	return new Promise((resolve, reject) => {
+		let shellRendered = false;
+		const { pipe, abort } = renderToPipeableStream(
+			<RemixServer
+				context={remixContext}
+				url={request.url}
+				abortDelay={ABORT_DELAY}
+			/>,
+			{
+				onShellReady() {
+					shellRendered = true;
+					const body = new PassThrough();
+					const stream = createReadableStreamFromReadable(body);
 
-          responseHeaders.set("Content-Type", "text/html");
+					responseHeaders.set("Content-Type", "text/html");
 
-          resolve(
-            new Response(stream, {
-              headers: responseHeaders,
-              status: responseStatusCode,
-            }),
-          );
+					resolve(
+						new Response(stream, {
+							headers: responseHeaders,
+							status: responseStatusCode,
+						}),
+					);
 
-          pipe(body);
-        },
-        onShellError(error: unknown) {
-          reject(error);
-        },
-        onError(error: unknown) {
-          responseStatusCode = 500;
-          if (shellRendered) {
-            console.error(error);
-          }
-        },
-      },
-    );
+					pipe(body);
+				},
+				onShellError(error: unknown) {
+					reject(error);
+				},
+				onError(error: unknown) {
+					responseStatusCode = 500;
+					if (shellRendered) {
+						console.error(error);
+					}
+				},
+			},
+		);
 
-    setTimeout(abort, ABORT_DELAY);
-  });
+		setTimeout(abort, ABORT_DELAY);
+	});
 }
diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx
index 9768f45..58a06dd 100644
--- a/frontend/app/root.tsx
+++ b/frontend/app/root.tsx
@@ -1,35 +1,35 @@
 import type { LinksFunction } from "@remix-run/node";
 import {
-  Links,
-  Meta,
-  Outlet,
-  Scripts,
-  ScrollRestoration,
+	Links,
+	Meta,
+	Outlet,
+	Scripts,
+	ScrollRestoration,
 } from "@remix-run/react";
 import "./tailwind.css";
 
 export const links: LinksFunction = () => {
-  return [{ rel: "icon", href: "/favicon.svg" }];
+	return [{ rel: "icon", href: "/favicon.svg" }];
 };
 
 export function Layout({ children }: { children: React.ReactNode }) {
-  return (
-    <html lang="en">
-      <head>
-        <meta charSet="utf-8" />
-        <meta name="viewport" content="width=device-width, initial-scale=1" />
-        <Meta />
-        <Links />
-      </head>
-      <body>
-        {children}
-        <ScrollRestoration />
-        <Scripts />
-      </body>
-    </html>
-  );
+	return (
+		<html lang="en">
+			<head>
+				<meta charSet="utf-8" />
+				<meta name="viewport" content="width=device-width, initial-scale=1" />
+				<Meta />
+				<Links />
+			</head>
+			<body>
+				{children}
+				<ScrollRestoration />
+				<Scripts />
+			</body>
+		</html>
+	);
 }
 
 export default function App() {
-  return <Outlet />;
+	return <Outlet />;
 }
diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx
index b5951ab..c56d4b2 100644
--- a/frontend/app/routes/_index.tsx
+++ b/frontend/app/routes/_index.tsx
@@ -2,25 +2,25 @@ import type { MetaFunction } from "@remix-run/node";
 import { Link } from "@remix-run/react";
 
 export const meta: MetaFunction = () => {
-  return [{ title: "iOSDC Japan 2024 Albatross.swift" }];
+	return [{ title: "iOSDC Japan 2024 Albatross.swift" }];
 };
 
 export default function Index() {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <div className="text-center">
-        <h1 className="text-4xl font-bold text-blue-600 mb-4">
-          iOSDC Japan 2024 Albatross.swift
-        </h1>
-        <p>
-          <Link
-            to="/login"
-            className="text-lg text-white bg-blue-500 px-4 py-2 rounded hover:bg-blue-600 transition duration-300"
-          >
-            Login
-          </Link>
-        </p>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<div className="text-center">
+				<h1 className="text-4xl font-bold text-blue-600 mb-4">
+					iOSDC Japan 2024 Albatross.swift
+				</h1>
+				<p>
+					<Link
+						to="/login"
+						className="text-lg text-white bg-blue-500 px-4 py-2 rounded hover:bg-blue-600 transition duration-300"
+					>
+						Login
+					</Link>
+				</p>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/routes/admin.dashboard.tsx b/frontend/app/routes/admin.dashboard.tsx
index 1d172af..ce3e910 100644
--- a/frontend/app/routes/admin.dashboard.tsx
+++ b/frontend/app/routes/admin.dashboard.tsx
@@ -3,29 +3,29 @@ import { Link } from "@remix-run/react";
 import { isAuthenticated } from "../.server/auth";
 
 export const meta: MetaFunction = () => {
-  return [{ title: "[Admin] Dashboard | iOSDC Japan 2024 Albatross.swift" }];
+	return [{ title: "[Admin] Dashboard | iOSDC Japan 2024 Albatross.swift" }];
 };
 
 export async function loader({ request }: LoaderFunctionArgs) {
-  const { user } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
-  if (!user.is_admin) {
-    throw new Error("Unauthorized");
-  }
-  return null;
+	const { user } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
+	if (!user.is_admin) {
+		throw new Error("Unauthorized");
+	}
+	return null;
 }
 
 export default function AdminDashboard() {
-  return (
-    <div>
-      <h1>[Admin] Dashboard</h1>
-      <p>
-        <Link to="/admin/users">Users</Link>
-      </p>
-      <p>
-        <Link to="/admin/games">Games</Link>
-      </p>
-    </div>
-  );
+	return (
+		<div>
+			<h1>[Admin] Dashboard</h1>
+			<p>
+				<Link to="/admin/users">Users</Link>
+			</p>
+			<p>
+				<Link to="/admin/games">Games</Link>
+			</p>
+		</div>
+	);
 }
diff --git a/frontend/app/routes/admin.games.tsx b/frontend/app/routes/admin.games.tsx
index adba7f5..af3554e 100644
--- a/frontend/app/routes/admin.games.tsx
+++ b/frontend/app/routes/admin.games.tsx
@@ -4,37 +4,37 @@ import { adminApiGetGames } from "../.server/api/client";
 import { isAuthenticated } from "../.server/auth";
 
 export const meta: MetaFunction = () => {
-  return [{ title: "[Admin] Games | iOSDC Japan 2024 Albatross.swift" }];
+	return [{ title: "[Admin] Games | iOSDC Japan 2024 Albatross.swift" }];
 };
 
 export async function loader({ request }: LoaderFunctionArgs) {
-  const { user, token } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
-  if (!user.is_admin) {
-    throw new Error("Unauthorized");
-  }
-  const { games } = await adminApiGetGames(token);
-  return { games };
+	const { user, token } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
+	if (!user.is_admin) {
+		throw new Error("Unauthorized");
+	}
+	const { games } = await adminApiGetGames(token);
+	return { games };
 }
 
 export default function AdminGames() {
-  const { games } = useLoaderData<typeof loader>()!;
+	const { games } = useLoaderData<typeof loader>()!;
 
-  return (
-    <div>
-      <div>
-        <h1>[Admin] Games</h1>
-        <ul>
-          {games.map((game) => (
-            <li key={game.game_id}>
-              <Link to={`/admin/games/${game.game_id}`}>
-                {game.display_name} (id={game.game_id})
-              </Link>
-            </li>
-          ))}
-        </ul>
-      </div>
-    </div>
-  );
+	return (
+		<div>
+			<div>
+				<h1>[Admin] Games</h1>
+				<ul>
+					{games.map((game) => (
+						<li key={game.game_id}>
+							<Link to={`/admin/games/${game.game_id}`}>
+								{game.display_name} (id={game.game_id})
+							</Link>
+						</li>
+					))}
+				</ul>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/routes/admin.games_.$gameId.tsx b/frontend/app/routes/admin.games_.$gameId.tsx
index a7bd651..34860ab 100644
--- a/frontend/app/routes/admin.games_.$gameId.tsx
+++ b/frontend/app/routes/admin.games_.$gameId.tsx
@@ -1,107 +1,107 @@
 import type {
-  ActionFunctionArgs,
-  LoaderFunctionArgs,
-  MetaFunction,
+	ActionFunctionArgs,
+	LoaderFunctionArgs,
+	MetaFunction,
 } from "@remix-run/node";
 import { Form, useLoaderData } from "@remix-run/react";
 import { adminApiGetGame, adminApiPutGame } from "../.server/api/client";
 import { isAuthenticated } from "../.server/auth";
 
 export const meta: MetaFunction<typeof loader> = ({ data }) => {
-  return [
-    {
-      title: data
-        ? `[Admin] Game Edit ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift`
-        : "[Admin] Game Edit | iOSDC Japan 2024 Albatross.swift",
-    },
-  ];
+	return [
+		{
+			title: data
+				? `[Admin] Game Edit ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift`
+				: "[Admin] Game Edit | iOSDC Japan 2024 Albatross.swift",
+		},
+	];
 };
 
 export async function loader({ request, params }: LoaderFunctionArgs) {
-  const { user, token } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
-  if (!user.is_admin) {
-    throw new Error("Unauthorized");
-  }
-  const { gameId } = params;
-  const { game } = await adminApiGetGame(token, Number(gameId));
-  return { game };
+	const { user, token } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
+	if (!user.is_admin) {
+		throw new Error("Unauthorized");
+	}
+	const { gameId } = params;
+	const { game } = await adminApiGetGame(token, Number(gameId));
+	return { game };
 }
 
 export async function action({ request, params }: ActionFunctionArgs) {
-  const { user, token } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
-  if (!user.is_admin) {
-    throw new Error("Unauthorized");
-  }
-  const { gameId } = params;
+	const { user, token } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
+	if (!user.is_admin) {
+		throw new Error("Unauthorized");
+	}
+	const { gameId } = params;
 
-  const formData = await request.formData();
-  const action = formData.get("action");
+	const formData = await request.formData();
+	const action = formData.get("action");
 
-  const nextState =
-    action === "open"
-      ? "waiting_entries"
-      : action === "start"
-        ? "prepare"
-        : null;
-  if (!nextState) {
-    throw new Error("Invalid action");
-  }
+	const nextState =
+		action === "open"
+			? "waiting_entries"
+			: action === "start"
+				? "prepare"
+				: null;
+	if (!nextState) {
+		throw new Error("Invalid action");
+	}
 
-  await adminApiPutGame(token, Number(gameId), {
-    state: nextState,
-  });
-  return null;
+	await adminApiPutGame(token, Number(gameId), {
+		state: nextState,
+	});
+	return null;
 }
 
 export default function AdminGameEdit() {
-  const { game } = useLoaderData<typeof loader>()!;
+	const { game } = useLoaderData<typeof loader>()!;
 
-  return (
-    <div>
-      <div>
-        <h1>[Admin] Game Edit {game.display_name}</h1>
-        <ul>
-          <li>ID: {game.game_id}</li>
-          <li>State: {game.state}</li>
-          <li>Display Name: {game.display_name}</li>
-          <li>Duration Seconds: {game.duration_seconds}</li>
-          <li>
-            Started At:{" "}
-            {game.started_at
-              ? new Date(game.started_at * 1000).toString()
-              : "-"}
-          </li>
-          <li>Problem ID: {game.problem ? game.problem.problem_id : "-"}</li>
-        </ul>
-        <div>
-          <Form method="post">
-            <div>
-              <button
-                type="submit"
-                name="action"
-                value="open"
-                disabled={game.state !== "closed"}
-              >
-                Open
-              </button>
-            </div>
-            <div>
-              <button
-                type="submit"
-                name="action"
-                value="start"
-                disabled={game.state !== "waiting_start"}
-              >
-                Start
-              </button>
-            </div>
-          </Form>
-        </div>
-      </div>
-    </div>
-  );
+	return (
+		<div>
+			<div>
+				<h1>[Admin] Game Edit {game.display_name}</h1>
+				<ul>
+					<li>ID: {game.game_id}</li>
+					<li>State: {game.state}</li>
+					<li>Display Name: {game.display_name}</li>
+					<li>Duration Seconds: {game.duration_seconds}</li>
+					<li>
+						Started At:{" "}
+						{game.started_at
+							? new Date(game.started_at * 1000).toString()
+							: "-"}
+					</li>
+					<li>Problem ID: {game.problem ? game.problem.problem_id : "-"}</li>
+				</ul>
+				<div>
+					<Form method="post">
+						<div>
+							<button
+								type="submit"
+								name="action"
+								value="open"
+								disabled={game.state !== "closed"}
+							>
+								Open
+							</button>
+						</div>
+						<div>
+							<button
+								type="submit"
+								name="action"
+								value="start"
+								disabled={game.state !== "waiting_start"}
+							>
+								Start
+							</button>
+						</div>
+					</Form>
+				</div>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/routes/admin.users.tsx b/frontend/app/routes/admin.users.tsx
index 8d9a8f2..9eed263 100644
--- a/frontend/app/routes/admin.users.tsx
+++ b/frontend/app/routes/admin.users.tsx
@@ -4,36 +4,36 @@ import { adminApiGetUsers } from "../.server/api/client";
 import { isAuthenticated } from "../.server/auth";
 
 export const meta: MetaFunction = () => {
-  return [{ title: "[Admin] Users | iOSDC Japan 2024 Albatross.swift" }];
+	return [{ title: "[Admin] Users | iOSDC Japan 2024 Albatross.swift" }];
 };
 
 export async function loader({ request }: LoaderFunctionArgs) {
-  const { user, token } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
-  if (!user.is_admin) {
-    throw new Error("Unauthorized");
-  }
-  const { users } = await adminApiGetUsers(token);
-  return { users };
+	const { user, token } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
+	if (!user.is_admin) {
+		throw new Error("Unauthorized");
+	}
+	const { users } = await adminApiGetUsers(token);
+	return { users };
 }
 
 export default function AdminUsers() {
-  const { users } = useLoaderData<typeof loader>()!;
+	const { users } = useLoaderData<typeof loader>()!;
 
-  return (
-    <div>
-      <div>
-        <h1>[Admin] Users</h1>
-        <ul>
-          {users.map((user) => (
-            <li key={user.user_id}>
-              {user.display_name} (id={user.user_id} username={user.username})
-              {user.is_admin && <span> admin</span>}
-            </li>
-          ))}
-        </ul>
-      </div>
-    </div>
-  );
+	return (
+		<div>
+			<div>
+				<h1>[Admin] Users</h1>
+				<ul>
+					{users.map((user) => (
+						<li key={user.user_id}>
+							{user.display_name} (id={user.user_id} username={user.username})
+							{user.is_admin && <span> admin</span>}
+						</li>
+					))}
+				</ul>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/routes/dashboard.tsx b/frontend/app/routes/dashboard.tsx
index 1f80634..1c2137d 100644
--- a/frontend/app/routes/dashboard.tsx
+++ b/frontend/app/routes/dashboard.tsx
@@ -5,72 +5,72 @@ import { apiGetGames } from "../.server/api/client";
 import { isAuthenticated } from "../.server/auth";
 
 export const meta: MetaFunction = () => {
-  return [{ title: "Dashboard | iOSDC Japan 2024 Albatross.swift" }];
+	return [{ title: "Dashboard | iOSDC Japan 2024 Albatross.swift" }];
 };
 
 export async function loader({ request }: LoaderFunctionArgs) {
-  const { user, token } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
-  if (user.is_admin) {
-    return redirect("/admin/dashboard");
-  }
-  const { games } = await apiGetGames(token);
-  return {
-    user,
-    games,
-  };
+	const { user, token } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
+	if (user.is_admin) {
+		return redirect("/admin/dashboard");
+	}
+	const { games } = await apiGetGames(token);
+	return {
+		user,
+		games,
+	};
 }
 
 export default function Dashboard() {
-  const { user, games } = useLoaderData<typeof loader>()!;
+	const { user, games } = useLoaderData<typeof loader>()!;
 
-  return (
-    <div className="min-h-screen p-8">
-      <div className="p-6 rounded shadow-md max-w-4xl mx-auto">
-        <h1 className="text-3xl font-bold mb-4">
-          {user.username}{" "}
-          {user.is_admin && <span className="text-red-500 text-lg">admin</span>}
-        </h1>
-        <h2 className="text-2xl font-semibold mb-2">User</h2>
-        <div className="mb-6">
-          <ul className="list-disc list-inside">
-            <li>Name: {user.display_name}</li>
-          </ul>
-        </div>
-        <h2 className="text-2xl font-semibold mb-2">Games</h2>
-        <div>
-          <ul className="list-disc list-inside">
-            {games.map((game) => (
-              <li key={game.game_id}>
-                {game.display_name}{" "}
-                {game.state === "closed" || game.state === "finished" ? (
-                  <span className="inline-block px-6 py-2 text-gray-400 bg-gray-200 cursor-not-allowed rounded">
-                    Entry
-                  </span>
-                ) : (
-                  <Link
-                    to={`/golf/${game.game_id}/play`}
-                    className="inline-block px-6 py-2 text-white bg-blue-500 hover:bg-blue-700 rounded"
-                  >
-                    Entry
-                  </Link>
-                )}
-              </li>
-            ))}
-          </ul>
-        </div>
-        <div>
-          <Form method="post" action="/logout">
-            <button
-              className="mt-6 px-6 py-2 text-white bg-red-500 hover:bg-red-700 rounded"
-              type="submit"
-            >
-              Logout
-            </button>
-          </Form>
-        </div>
-      </div>
-    </div>
-  );
+	return (
+		<div className="min-h-screen p-8">
+			<div className="p-6 rounded shadow-md max-w-4xl mx-auto">
+				<h1 className="text-3xl font-bold mb-4">
+					{user.username}{" "}
+					{user.is_admin && <span className="text-red-500 text-lg">admin</span>}
+				</h1>
+				<h2 className="text-2xl font-semibold mb-2">User</h2>
+				<div className="mb-6">
+					<ul className="list-disc list-inside">
+						<li>Name: {user.display_name}</li>
+					</ul>
+				</div>
+				<h2 className="text-2xl font-semibold mb-2">Games</h2>
+				<div>
+					<ul className="list-disc list-inside">
+						{games.map((game) => (
+							<li key={game.game_id}>
+								{game.display_name}{" "}
+								{game.state === "closed" || game.state === "finished" ? (
+									<span className="inline-block px-6 py-2 text-gray-400 bg-gray-200 cursor-not-allowed rounded">
+										Entry
+									</span>
+								) : (
+									<Link
+										to={`/golf/${game.game_id}/play`}
+										className="inline-block px-6 py-2 text-white bg-blue-500 hover:bg-blue-700 rounded"
+									>
+										Entry
+									</Link>
+								)}
+							</li>
+						))}
+					</ul>
+				</div>
+				<div>
+					<Form method="post" action="/logout">
+						<button
+							className="mt-6 px-6 py-2 text-white bg-red-500 hover:bg-red-700 rounded"
+							type="submit"
+						>
+							Logout
+						</button>
+					</Form>
+				</div>
+			</div>
+		</div>
+	);
 }
diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx
index 0ce3353..d498200 100644
--- a/frontend/app/routes/golf.$gameId.play.tsx
+++ b/frontend/app/routes/golf.$gameId.play.tsx
@@ -7,40 +7,40 @@ import GolfPlayApp from "../components/GolfPlayApp.client";
 import GolfPlayAppConnecting from "../components/GolfPlayApps/GolfPlayAppConnecting";
 
 export const meta: MetaFunction<typeof loader> = ({ data }) => {
-  return [
-    {
-      title: data
-        ? `Golf Playing ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift`
-        : "Golf Playing | iOSDC Japan 2024 Albatross.swift",
-    },
-  ];
+	return [
+		{
+			title: data
+				? `Golf Playing ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift`
+				: "Golf Playing | iOSDC Japan 2024 Albatross.swift",
+		},
+	];
 };
 
 export async function loader({ params, request }: LoaderFunctionArgs) {
-  const { token } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
+	const { token } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
 
-  const fetchGame = async () => {
-    return (await apiGetGame(token, Number(params.gameId))).game;
-  };
-  const fetchSockToken = async () => {
-    return (await apiGetToken(token)).token;
-  };
+	const fetchGame = async () => {
+		return (await apiGetGame(token, Number(params.gameId))).game;
+	};
+	const fetchSockToken = async () => {
+		return (await apiGetToken(token)).token;
+	};
 
-  const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]);
-  return {
-    game,
-    sockToken,
-  };
+	const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]);
+	return {
+		game,
+		sockToken,
+	};
 }
 
 export default function GolfPlay() {
-  const { game, sockToken } = useLoaderData<typeof loader>();
+	const { game, sockToken } = useLoaderData<typeof loader>();
 
-  return (
-    <ClientOnly fallback={<GolfPlayAppConnecting />}>
-      {() => <GolfPlayApp game={game} sockToken={sockToken} />}
-    </ClientOnly>
-  );
+	return (
+		<ClientOnly fallback={<GolfPlayAppConnecting />}>
+			{() => <GolfPlayApp game={game} sockToken={sockToken} />}
+		</ClientOnly>
+	);
 }
diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx
index 0f0e085..0203e27 100644
--- a/frontend/app/routes/golf.$gameId.watch.tsx
+++ b/frontend/app/routes/golf.$gameId.watch.tsx
@@ -7,40 +7,40 @@ import GolfWatchApp from "../components/GolfWatchApp.client";
 import GolfWatchAppConnecting from "../components/GolfWatchApps/GolfWatchAppConnecting";
 
 export const meta: MetaFunction<typeof loader> = ({ data }) => {
-  return [
-    {
-      title: data
-        ? `Golf Watching ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift`
-        : "Golf Watching | iOSDC Japan 2024 Albatross.swift",
-    },
-  ];
+	return [
+		{
+			title: data
+				? `Golf Watching ${data.game.display_name} | iOSDC Japan 2024 Albatross.swift`
+				: "Golf Watching | iOSDC Japan 2024 Albatross.swift",
+		},
+	];
 };
 
 export async function loader({ params, request }: LoaderFunctionArgs) {
-  const { token } = await isAuthenticated(request, {
-    failureRedirect: "/login",
-  });
+	const { token } = await isAuthenticated(request, {
+		failureRedirect: "/login",
+	});
 
-  const fetchGame = async () => {
-    return (await apiGetGame(token, Number(params.gameId))).game;
-  };
-  const fetchSockToken = async () => {
-    return (await apiGetToken(token)).token;
-  };
+	const fetchGame = async () => {
+		return (await apiGetGame(token, Number(params.gameId))).game;
+	};
+	const fetchSockToken = async () => {
+		return (await apiGetToken(token)).token;
+	};
 
-  const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]);
-  return {
-    game,
-    sockToken,
-  };
+	const [game, sockToken] = await Promise.all([fetchGame(), fetchSockToken()]);
+	return {
+		game,
+		sockToken,
+	};
 }
 
 export default function GolfWatch() {
-  const { game, sockToken } = useLoaderData<typeof loader>();
+	const { game, sockToken } = useLoaderData<typeof loader>();
 
-  return (
-    <ClientOnly fallback={<GolfWatchAppConnecting />}>
-      {() => <GolfWatchApp game={game} sockToken={sockToken} />}
-    </ClientOnly>
-  );
+	return (
+		<ClientOnly fallback={<GolfWatchAppConnecting />}>
+			{() => <GolfWatchApp game={game} sockToken={sockToken} />}
+		</ClientOnly>
+	);
 }
diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx
index f63df08..95effaa 100644
--- a/frontend/app/routes/login.tsx
+++ b/frontend/app/routes/login.tsx
@@ -1,74 +1,74 @@
 import type {
-  ActionFunctionArgs,
-  LoaderFunctionArgs,
-  MetaFunction,
+	ActionFunctionArgs,
+	LoaderFunctionArgs,
+	MetaFunction,
 } from "@remix-run/node";
 import { Form } from "@remix-run/react";
 import { authenticator } from "../.server/auth";
 
 export const meta: MetaFunction = () => {
-  return [{ title: "Login | iOSDC Japan 2024 Albatross.swift" }];
+	return [{ title: "Login | iOSDC Japan 2024 Albatross.swift" }];
 };
 
 export async function loader({ request }: LoaderFunctionArgs) {
-  return await authenticator.isAuthenticated(request, {
-    successRedirect: "/dashboard",
-  });
+	return await authenticator.isAuthenticated(request, {
+		successRedirect: "/dashboard",
+	});
 }
 
 export async function action({ request }: ActionFunctionArgs) {
-  return await authenticator.authenticate("default", request, {
-    successRedirect: "/dashboard",
-    failureRedirect: "/login",
-  });
+	return await authenticator.authenticate("default", request, {
+		successRedirect: "/dashboard",
+		failureRedirect: "/login",
+	});
 }
 
 export default function Login() {
-  return (
-    <div className="min-h-screen bg-gray-100 flex items-center justify-center">
-      <Form
-        method="post"
-        className="bg-white p-8 rounded shadow-md w-full max-w-sm"
-      >
-        <h2 className="text-2xl font-bold mb-6 text-center">Login</h2>
-        <div className="mb-4">
-          <label
-            htmlFor="username"
-            className="block text-sm font-medium text-gray-700"
-          >
-            Username
-          </label>
-          <input
-            type="text"
-            name="username"
-            id="username"
-            required
-            className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
-          />
-        </div>
-        <div className="mb-6">
-          <label
-            htmlFor="password"
-            className="block text-sm font-medium text-gray-700"
-          >
-            Password
-          </label>
-          <input
-            type="password"
-            name="password"
-            id="password"
-            autoComplete="current-password"
-            required
-            className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
-          />
-        </div>
-        <button
-          type="submit"
-          className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition duration-300"
-        >
-          Log In
-        </button>
-      </Form>
-    </div>
-  );
+	return (
+		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
+			<Form
+				method="post"
+				className="bg-white p-8 rounded shadow-md w-full max-w-sm"
+			>
+				<h2 className="text-2xl font-bold mb-6 text-center">Login</h2>
+				<div className="mb-4">
+					<label
+						htmlFor="username"
+						className="block text-sm font-medium text-gray-700"
+					>
+						Username
+					</label>
+					<input
+						type="text"
+						name="username"
+						id="username"
+						required
+						className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
+					/>
+				</div>
+				<div className="mb-6">
+					<label
+						htmlFor="password"
+						className="block text-sm font-medium text-gray-700"
+					>
+						Password
+					</label>
+					<input
+						type="password"
+						name="password"
+						id="password"
+						autoComplete="current-password"
+						required
+						className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
+					/>
+				</div>
+				<button
+					type="submit"
+					className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition duration-300"
+				>
+					Log In
+				</button>
+			</Form>
+		</div>
+	);
 }
diff --git a/frontend/app/routes/logout.tsx b/frontend/app/routes/logout.tsx
index 030bde0..012d9e9 100644
--- a/frontend/app/routes/logout.tsx
+++ b/frontend/app/routes/logout.tsx
@@ -2,6 +2,6 @@ import type { ActionFunctionArgs } from "@remix-run/node";
 import { authenticator } from "../.server/auth";
 
 export async function action({ request }: ActionFunctionArgs) {
-  await authenticator.logout(request, { redirectTo: "/" });
-  return null;
+	await authenticator.logout(request, { redirectTo: "/" });
+	return null;
 }
diff --git a/frontend/biome.json b/frontend/biome.json
index c13b764..2bafdc7 100644
--- a/frontend/biome.json
+++ b/frontend/biome.json
@@ -1,18 +1,17 @@
 {
-  "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
-  "files": {
-    "ignore": ["./.cache", "./build", "./app/.server/api/schema.d.ts"]
-  },
-  "vcs": {
-    "enabled": true,
-    "clientKind": "git",
-    "useIgnoreFile": true
-  },
-  "linter": {
-    "enabled": false
-  },
-  "formatter": {
-    "indentStyle": "space",
-    "indentWidth": 2
-  }
+	"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
+	"files": {
+		"ignore": ["./.cache", "./build", "./app/.server/api/schema.d.ts"]
+	},
+	"vcs": {
+		"enabled": true,
+		"clientKind": "git",
+		"useIgnoreFile": true
+	},
+	"linter": {
+		"enabled": false
+	},
+	"formatter": {
+		"enabled": true
+	}
 }
diff --git a/frontend/package.json b/frontend/package.json
index e7fdf45..d54b3e0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,54 +1,55 @@
 {
-  "name": "iosdc-japan-2024-albatross-frontend",
-  "private": true,
-  "sideEffects": false,
-  "type": "module",
-  "scripts": {
-    "build": "remix vite:build",
-    "dev": "remix vite:dev",
-    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
-    "openapi-typescript": "npx --no-install openapi-typescript --output ./app/.server/api/schema.d.ts ../openapi.yaml",
-    "start": "remix-serve ./build/server/index.js",
-    "typecheck": "tsc"
-  },
-  "dependencies": {
-    "@remix-run/node": "^2.10.3",
-    "@remix-run/react": "^2.10.3",
-    "@remix-run/serve": "^2.10.3",
-    "isbot": "^5.1.13",
-    "jwt-decode": "^4.0.0",
-    "openapi-fetch": "^0.10.2",
-    "react": "^18.3.1",
-    "react-dom": "^18.3.1",
-    "react-use-websocket": "^4.8.1",
-    "remix-auth": "^3.7.0",
-    "remix-auth-form": "^1.5.0",
-    "remix-utils": "^7.6.0",
-    "sakura.css": "^1.5.0",
-    "use-debounce": "^10.0.1"
-  },
-  "devDependencies": {
-    "@biomejs/biome": "^1.8.3",
-    "@remix-run/dev": "^2.10.3",
-    "@types/react": "^18.3.3",
-    "@types/react-dom": "^18.3.0",
-    "@typescript-eslint/eslint-plugin": "^6.7.4",
-    "@typescript-eslint/parser": "^6.7.4",
-    "autoprefixer": "^10.4.19",
-    "eslint": "^8.56.0",
-    "eslint-import-resolver-typescript": "^3.6.1",
-    "eslint-plugin-import": "^2.29.1",
-    "eslint-plugin-jsx-a11y": "^6.9.0",
-    "eslint-plugin-react": "^7.35.0",
-    "eslint-plugin-react-hooks": "^4.6.2",
-    "openapi-typescript": "^7.1.0",
-    "postcss": "^8.4.40",
-    "tailwindcss": "^3.4.7",
-    "typescript": "^5.5.4",
-    "vite": "^5.3.5",
-    "vite-tsconfig-paths": "^4.3.2"
-  },
-  "engines": {
-    "node": ">=20.0.0"
-  }
+	"name": "iosdc-japan-2024-albatross-frontend",
+	"private": true,
+	"sideEffects": false,
+	"type": "module",
+	"scripts": {
+		"build": "remix vite:build",
+		"check": "biome check --write",
+		"dev": "remix vite:dev",
+		"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
+		"openapi-typescript": "openapi-typescript --output ./app/.server/api/schema.d.ts ../openapi.yaml",
+		"start": "remix-serve ./build/server/index.js",
+		"typecheck": "tsc"
+	},
+	"dependencies": {
+		"@remix-run/node": "^2.10.3",
+		"@remix-run/react": "^2.10.3",
+		"@remix-run/serve": "^2.10.3",
+		"isbot": "^5.1.13",
+		"jwt-decode": "^4.0.0",
+		"openapi-fetch": "^0.10.2",
+		"react": "^18.3.1",
+		"react-dom": "^18.3.1",
+		"react-use-websocket": "^4.8.1",
+		"remix-auth": "^3.7.0",
+		"remix-auth-form": "^1.5.0",
+		"remix-utils": "^7.6.0",
+		"sakura.css": "^1.5.0",
+		"use-debounce": "^10.0.1"
+	},
+	"devDependencies": {
+		"@biomejs/biome": "^1.8.3",
+		"@remix-run/dev": "^2.10.3",
+		"@types/react": "^18.3.3",
+		"@types/react-dom": "^18.3.0",
+		"@typescript-eslint/eslint-plugin": "^6.7.4",
+		"@typescript-eslint/parser": "^6.7.4",
+		"autoprefixer": "^10.4.19",
+		"eslint": "^8.56.0",
+		"eslint-import-resolver-typescript": "^3.6.1",
+		"eslint-plugin-import": "^2.29.1",
+		"eslint-plugin-jsx-a11y": "^6.9.0",
+		"eslint-plugin-react": "^7.35.0",
+		"eslint-plugin-react-hooks": "^4.6.2",
+		"openapi-typescript": "^7.1.0",
+		"postcss": "^8.4.40",
+		"tailwindcss": "^3.4.7",
+		"typescript": "^5.5.4",
+		"vite": "^5.3.5",
+		"vite-tsconfig-paths": "^4.3.2"
+	},
+	"engines": {
+		"node": ">=20.0.0"
+	}
 }
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js
index 2aa7205..7b75c83 100644
--- a/frontend/postcss.config.js
+++ b/frontend/postcss.config.js
@@ -1,6 +1,6 @@
 export default {
-  plugins: {
-    tailwindcss: {},
-    autoprefixer: {},
-  },
+	plugins: {
+		tailwindcss: {},
+		autoprefixer: {},
+	},
 };
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
index 34d03da..acee505 100644
--- a/frontend/tailwind.config.ts
+++ b/frontend/tailwind.config.ts
@@ -1,9 +1,9 @@
 import type { Config } from "tailwindcss";
 
 export default {
-  content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
-  theme: {
-    extend: {},
-  },
-  plugins: [],
+	content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
+	theme: {
+		extend: {},
+	},
+	plugins: [],
 } satisfies Config;
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index d52b6b9..33e429f 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -1,31 +1,31 @@
 {
-  "include": [
-    "**/*.ts",
-    "**/*.tsx",
-    "**/.server/**/*.ts",
-    "**/.server/**/*.tsx",
-    "**/.client/**/*.ts",
-    "**/.client/**/*.tsx"
-  ],
-  "compilerOptions": {
-    "lib": ["DOM", "DOM.Iterable", "ES2022"],
-    "types": ["@remix-run/node", "vite/client"],
-    "isolatedModules": true,
-    "esModuleInterop": true,
-    "jsx": "react-jsx",
-    "module": "ESNext",
-    "moduleResolution": "Bundler",
-    "resolveJsonModule": true,
-    "target": "ES2022",
-    "strict": true,
-    "noUncheckedIndexedAccess": true,
-    "allowJs": true,
-    "skipLibCheck": true,
-    "forceConsistentCasingInFileNames": true,
-    "baseUrl": ".",
-    "paths": {
-      "~/*": ["./app/*"]
-    },
-    "noEmit": true
-  }
+	"include": [
+		"**/*.ts",
+		"**/*.tsx",
+		"**/.server/**/*.ts",
+		"**/.server/**/*.tsx",
+		"**/.client/**/*.ts",
+		"**/.client/**/*.tsx"
+	],
+	"compilerOptions": {
+		"lib": ["DOM", "DOM.Iterable", "ES2022"],
+		"types": ["@remix-run/node", "vite/client"],
+		"isolatedModules": true,
+		"esModuleInterop": true,
+		"jsx": "react-jsx",
+		"module": "ESNext",
+		"moduleResolution": "Bundler",
+		"resolveJsonModule": true,
+		"target": "ES2022",
+		"strict": true,
+		"noUncheckedIndexedAccess": true,
+		"allowJs": true,
+		"skipLibCheck": true,
+		"forceConsistentCasingInFileNames": true,
+		"baseUrl": ".",
+		"paths": {
+			"~/*": ["./app/*"]
+		},
+		"noEmit": true
+	}
 }
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 54066fb..e07fb91 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -3,14 +3,14 @@ import { defineConfig } from "vite";
 import tsconfigPaths from "vite-tsconfig-paths";
 
 export default defineConfig({
-  plugins: [
-    remix({
-      future: {
-        v3_fetcherPersist: true,
-        v3_relativeSplatPath: true,
-        v3_throwAbortReason: true,
-      },
-    }),
-    tsconfigPaths(),
-  ],
+	plugins: [
+		remix({
+			future: {
+				v3_fetcherPersist: true,
+				v3_relativeSplatPath: true,
+				v3_throwAbortReason: true,
+			},
+		}),
+		tsconfigPaths(),
+	],
 });
-- 
cgit v1.2.3-70-g09d2

