From 8e73d12a703e90ad908962143951178c13d0d6fe Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 20 Feb 2026 23:32:22 +0900 Subject: feat: add user submission history page Allow users to view their own past submissions (code, size, status, timestamp) for each game. Adds API endpoint, backend handler, SQL query, and frontend page with expandable code display. Co-Authored-By: Claude Opus 4.6 --- frontend/app/pages/DashboardPage.tsx | 3 + frontend/app/pages/SubmissionsPage.test.tsx | 17 ++++ frontend/app/pages/SubmissionsPage.tsx | 126 ++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 frontend/app/pages/SubmissionsPage.test.tsx create mode 100644 frontend/app/pages/SubmissionsPage.tsx (limited to 'frontend/app/pages') diff --git a/frontend/app/pages/DashboardPage.tsx b/frontend/app/pages/DashboardPage.tsx index 708a867..3191f1b 100644 --- a/frontend/app/pages/DashboardPage.tsx +++ b/frontend/app/pages/DashboardPage.tsx @@ -74,6 +74,9 @@ export default function DashboardPage() { 観戦 + + 提出履歴 + ))} diff --git a/frontend/app/pages/SubmissionsPage.test.tsx b/frontend/app/pages/SubmissionsPage.test.tsx new file mode 100644 index 0000000..f01f4c9 --- /dev/null +++ b/frontend/app/pages/SubmissionsPage.test.tsx @@ -0,0 +1,17 @@ +/** + * @vitest-environment jsdom + */ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, test } from "vitest"; +import SubmissionsPage from "./SubmissionsPage"; + +afterEach(() => { + cleanup(); +}); + +describe("SubmissionsPage", () => { + test("shows loading state initially", () => { + render(); + expect(screen.getByText("Loading...")).toBeDefined(); + }); +}); diff --git a/frontend/app/pages/SubmissionsPage.tsx b/frontend/app/pages/SubmissionsPage.tsx new file mode 100644 index 0000000..2c3329d --- /dev/null +++ b/frontend/app/pages/SubmissionsPage.tsx @@ -0,0 +1,126 @@ +import { useEffect, useState } from "react"; +import { createApiClient } from "../api/client"; +import type { components } from "../api/schema"; +import BorderedContainerWithCaption from "../components/BorderedContainerWithCaption"; +import NavigateLink from "../components/NavigateLink"; +import SubmitStatusLabel from "../components/SubmitStatusLabel"; +import { APP_NAME } from "../config"; +import { usePageTitle } from "../hooks/usePageTitle"; + +type Submission = components["schemas"]["Submission"]; + +export default function SubmissionsPage({ gameId }: { gameId: string }) { + usePageTitle(`Submissions | ${APP_NAME}`); + + const [submissions, setSubmissions] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedId, setExpandedId] = useState(null); + + const numericGameId = Number(gameId); + + useEffect(() => { + const apiClient = createApiClient(); + apiClient + .getGamePlaySubmissions(numericGameId) + .then(({ submissions }) => setSubmissions(submissions)) + .catch(() => {}) + .finally(() => setLoading(false)); + }, [numericGameId]); + + if (loading) { + return ( +
+

Loading...

+
+ ); + } + + return ( +
+ +
+ {submissions.length === 0 ? ( +

提出履歴はありません

+ ) : ( +
    + {submissions.map((s) => ( +
  • +
    +
    + + + {s.code_size} + + bytes + + +
    +
    + + {formatDate(s.created_at)} + + +
    +
    + {expandedId === s.submission_id && ( +
    +											{s.code}
    +										
    + )} +
  • + ))} +
+ )} +
+
+ 対戦に戻る + ダッシュボードに戻る +
+ ); +} + +function StatusBadge({ + status, +}: { + status: components["schemas"]["ExecutionStatus"]; +}) { + const colorClass = + status === "success" + ? "bg-green-100 text-green-800" + : status === "running" + ? "bg-yellow-100 text-yellow-800" + : status === "none" + ? "bg-gray-100 text-gray-800" + : "bg-red-100 text-red-800"; + + return ( + + + + ); +} + +function formatDate(unixTimestamp: number): string { + const date = new Date(unixTimestamp * 1000); + return date.toLocaleString("ja-JP", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); +} -- cgit v1.3.1