aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/.eslintrc.cjs142
-rw-r--r--frontend/app/.server/api/client.ts130
-rw-r--r--frontend/app/.server/auth.ts166
-rw-r--r--frontend/app/.server/session.ts18
-rw-r--r--frontend/app/components/GolfPlayApp.client.tsx260
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppConnecting.tsx18
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppFinished.tsx14
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppGaming.tsx68
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppStarting.tsx20
-rw-r--r--frontend/app/components/GolfPlayApps/GolfPlayAppWaiting.tsx1150
-rw-r--r--frontend/app/components/GolfWatchApp.client.tsx218
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppConnecting.tsx18
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppFinished.tsx14
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppGaming.tsx70
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppStarting.tsx20
-rw-r--r--frontend/app/components/GolfWatchApps/GolfWatchAppWaiting.tsx2
-rw-r--r--frontend/app/entry.client.tsx12
-rw-r--r--frontend/app/entry.server.tsx190
-rw-r--r--frontend/app/root.tsx44
-rw-r--r--frontend/app/routes/_index.tsx36
-rw-r--r--frontend/app/routes/admin.dashboard.tsx38
-rw-r--r--frontend/app/routes/admin.games.tsx52
-rw-r--r--frontend/app/routes/admin.games_.$gameId.tsx172
-rw-r--r--frontend/app/routes/admin.users.tsx50
-rw-r--r--frontend/app/routes/dashboard.tsx122
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx54
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx54
-rw-r--r--frontend/app/routes/login.tsx116
-rw-r--r--frontend/app/routes/logout.tsx4
-rw-r--r--frontend/biome.json31
-rw-r--r--frontend/package.json105
-rw-r--r--frontend/postcss.config.js8
-rw-r--r--frontend/tailwind.config.ts10
-rw-r--r--frontend/tsconfig.json58
-rw-r--r--frontend/vite.config.ts20
35 files changed, 1752 insertions, 1752 deletions
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(),
+ ],
});