diff options
| author | nsfisis <nsfisis@gmail.com> | 2024-07-31 21:02:40 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2024-07-31 22:20:11 +0900 |
| commit | 3840c6d8e4261f182657b11ba55f61da04d70b28 (patch) | |
| tree | 253a3094f3606682982b91b9be7a03f1e4b3f723 /frontend | |
| parent | d9971c0443fae16af1e003c920f1347c9233b5ac (diff) | |
| download | iosdc-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.ts | 198 | ||||
| -rw-r--r-- | frontend/app/routes/admin.dashboard.tsx | 3 | ||||
| -rw-r--r-- | frontend/app/routes/admin.games.tsx | 49 | ||||
| -rw-r--r-- | frontend/app/routes/admin.games_.$gameId.tsx | 127 | ||||
| -rw-r--r-- | frontend/app/routes/admin.users.tsx | 2 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.play.tsx | 2 | ||||
| -rw-r--r-- | frontend/app/routes/golf.$gameId.watch.tsx | 2 |
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 () => { |
