aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-07-31 21:02:40 +0900
committernsfisis <nsfisis@gmail.com>2024-07-31 22:20:11 +0900
commit3840c6d8e4261f182657b11ba55f61da04d70b28 (patch)
tree253a3094f3606682982b91b9be7a03f1e4b3f723 /frontend
parentd9971c0443fae16af1e003c920f1347c9233b5ac (diff)
downloadiosdc-japan-2024-albatross-3840c6d8e4261f182657b11ba55f61da04d70b28.tar.gz
iosdc-japan-2024-albatross-3840c6d8e4261f182657b11ba55f61da04d70b28.tar.zst
iosdc-japan-2024-albatross-3840c6d8e4261f182657b11ba55f61da04d70b28.zip
feat: implement /admin/games and /admin/games/{gameId}
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/.server/api/schema.d.ts198
-rw-r--r--frontend/app/routes/admin.dashboard.tsx3
-rw-r--r--frontend/app/routes/admin.games.tsx49
-rw-r--r--frontend/app/routes/admin.games_.$gameId.tsx127
-rw-r--r--frontend/app/routes/admin.users.tsx2
-rw-r--r--frontend/app/routes/golf.$gameId.play.tsx2
-rw-r--r--frontend/app/routes/golf.$gameId.watch.tsx2
7 files changed, 379 insertions, 4 deletions
diff --git a/frontend/app/.server/api/schema.d.ts b/frontend/app/.server/api/schema.d.ts
index d12600c..000c876 100644
--- a/frontend/app/.server/api/schema.d.ts
+++ b/frontend/app/.server/api/schema.d.ts
@@ -198,7 +198,9 @@ export interface paths {
[name: string]: unknown;
};
content: {
- "application/json": components["schemas"]["Game"];
+ "application/json": {
+ game: components["schemas"]["Game"];
+ };
};
};
/** @description Forbidden */
@@ -287,6 +289,200 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/admin/games": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List games */
+ get: {
+ parameters: {
+ query?: never;
+ header: {
+ Authorization: string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description List of games */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ games: components["schemas"]["Game"][];
+ };
+ };
+ };
+ /** @description Forbidden */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Forbidden operation */
+ message: string;
+ };
+ };
+ };
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ [path: `/admin/games/${integer}`]: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get a game */
+ get: {
+ parameters: {
+ query?: never;
+ header: {
+ Authorization: string;
+ };
+ path: {
+ game_id: number;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description A game */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ game: components["schemas"]["Game"];
+ };
+ };
+ };
+ /** @description Forbidden */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Forbidden operation */
+ message: string;
+ };
+ };
+ };
+ /** @description Not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Not found */
+ message: string;
+ };
+ };
+ };
+ };
+ };
+ /** Update a game */
+ put: {
+ parameters: {
+ query?: never;
+ header: {
+ Authorization: string;
+ };
+ path: {
+ game_id: number;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ /**
+ * @example closed
+ * @enum {string}
+ */
+ state?: "closed" | "waiting_entries" | "waiting_start" | "prepare" | "starting" | "gaming" | "finished";
+ /** @example Game 1 */
+ display_name?: string;
+ /** @example 360 */
+ duration_seconds?: number;
+ /** @example 946684800 */
+ started_at?: number | null;
+ /** @example 1 */
+ problem_id?: number | null;
+ };
+ };
+ };
+ responses: {
+ /** @description Successfully updated */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Invalid request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Invalid request */
+ message: string;
+ };
+ };
+ };
+ /** @description Forbidden */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Forbidden operation */
+ message: string;
+ };
+ };
+ };
+ /** @description Not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @example Not found */
+ message: string;
+ };
+ };
+ };
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record<string, never>;
export interface components {
diff --git a/frontend/app/routes/admin.dashboard.tsx b/frontend/app/routes/admin.dashboard.tsx
index d5f3809..af82731 100644
--- a/frontend/app/routes/admin.dashboard.tsx
+++ b/frontend/app/routes/admin.dashboard.tsx
@@ -23,6 +23,9 @@ export default function AdminDashboard() {
<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
new file mode 100644
index 0000000..8362c6c
--- /dev/null
+++ b/frontend/app/routes/admin.games.tsx
@@ -0,0 +1,49 @@
+import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
+import { useLoaderData, Link } from "@remix-run/react";
+import { isAuthenticated } from "../.server/auth";
+import { apiClient } from "../.server/api/client";
+
+export const meta: MetaFunction = () => {
+ return [{ title: "[Admin] Games | iOSDC 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 { data, error } = await apiClient.GET("/admin/games", {
+ params: {
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ });
+ if (error) {
+ throw new Error(error.message);
+ }
+ return { games: data.games };
+}
+
+export default function AdminGames() {
+ 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>
+ );
+}
diff --git a/frontend/app/routes/admin.games_.$gameId.tsx b/frontend/app/routes/admin.games_.$gameId.tsx
new file mode 100644
index 0000000..5d83fb4
--- /dev/null
+++ b/frontend/app/routes/admin.games_.$gameId.tsx
@@ -0,0 +1,127 @@
+import type {
+ LoaderFunctionArgs,
+ MetaFunction,
+ ActionFunctionArgs,
+} from "@remix-run/node";
+import { useLoaderData, Form } from "@remix-run/react";
+import { isAuthenticated } from "../.server/auth";
+import { apiClient } from "../.server/api/client";
+
+export const meta: MetaFunction<typeof loader> = ({ data }) => {
+ return [
+ {
+ title: data
+ ? `[Admin] Game Edit ${data.game.display_name} | iOSDC 2024 Albatross.swift`
+ : "[Admin] Game Edit | iOSDC 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 { data, error } = await apiClient.GET("/admin/games/{game_id}", {
+ params: {
+ path: {
+ game_id: Number(gameId),
+ },
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ });
+ if (error) {
+ throw new Error(error.message);
+ }
+ return { game: data.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 formData = await request.formData();
+ const action = formData.get("action");
+
+ const nextState =
+ action === "open"
+ ? "waiting_entries"
+ : action === "start"
+ ? "waiting_start"
+ : null;
+ if (!nextState) {
+ throw new Error("Invalid action");
+ }
+
+ const { error } = await apiClient.PUT("/admin/games/{game_id}", {
+ params: {
+ path: {
+ game_id: Number(gameId),
+ },
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ body: {
+ state: nextState,
+ },
+ });
+ if (error) {
+ throw new Error(error.message);
+ }
+}
+
+export default function AdminGameEdit() {
+ 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">
+ <button
+ type="submit"
+ name="action"
+ value="open"
+ disabled={game.state !== "closed"}
+ >
+ Open
+ </button>
+ <button
+ type="submit"
+ name="action"
+ value="start"
+ disabled={game.state !== "waiting_start"}
+ >
+ Start
+ </button>
+ </Form>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/frontend/app/routes/admin.users.tsx b/frontend/app/routes/admin.users.tsx
index d9901a2..61a25bf 100644
--- a/frontend/app/routes/admin.users.tsx
+++ b/frontend/app/routes/admin.users.tsx
@@ -37,7 +37,7 @@ export default function AdminUsers() {
<ul>
{users.map((user) => (
<li key={user.user_id}>
- {user.display_name} (uid={user.user_id} username={user.username})
+ {user.display_name} (id={user.user_id} username={user.username})
{user.is_admin && <span> admin</span>}
</li>
))}
diff --git a/frontend/app/routes/golf.$gameId.play.tsx b/frontend/app/routes/golf.$gameId.play.tsx
index 3932b4b..2a3a68b 100644
--- a/frontend/app/routes/golf.$gameId.play.tsx
+++ b/frontend/app/routes/golf.$gameId.play.tsx
@@ -35,7 +35,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
if (error) {
throw new Error(error.message);
}
- return data;
+ return data.game;
};
const fetchSockToken = async () => {
diff --git a/frontend/app/routes/golf.$gameId.watch.tsx b/frontend/app/routes/golf.$gameId.watch.tsx
index eeb6fb9..da7baf1 100644
--- a/frontend/app/routes/golf.$gameId.watch.tsx
+++ b/frontend/app/routes/golf.$gameId.watch.tsx
@@ -35,7 +35,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
if (error) {
throw new Error(error.message);
}
- return data;
+ return data.game;
};
const fetchSockToken = async () => {