Skip to content

Commit

Permalink
feat(frontend): 管理画面に提出状況詳細画面を実装 (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
Texiko-texiko authored Dec 2, 2024
1 parent 06a2898 commit dd05631
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 0 deletions.
32 changes: 32 additions & 0 deletions frontend/src/route-tree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { createFileRoute } from "@tanstack/react-router"

import { Route as rootRoute } from "./routes/__root"
import { Route as ProblemsProblemIdRouteImport } from "./routes/problems/$problemId/route"
import { Route as AdminSubmissionsSubmissionIdRouteImport } from "./routes/admin/submissions/$submissionId/route"
import { Route as AdminProblemsProblemIdIndexImport } from "./routes/admin/problems/$problemId/index"

// Create Virtual Routes
Expand Down Expand Up @@ -58,6 +59,17 @@ const AdminSubmissionsIndexLazyRoute = AdminSubmissionsIndexLazyImport.update({
import("./routes/admin/submissions/index.lazy").then((d) => d.Route),
)

const AdminSubmissionsSubmissionIdRouteRoute =
AdminSubmissionsSubmissionIdRouteImport.update({
id: "/admin/submissions/$submissionId",
path: "/admin/submissions/$submissionId",
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import("./routes/admin/submissions/$submissionId/route.lazy").then(
(d) => d.Route,
),
)

const AdminProblemsProblemIdIndexRoute =
AdminProblemsProblemIdIndexImport.update({
id: "/admin/problems/$problemId/",
Expand Down Expand Up @@ -94,6 +106,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof ProblemsIndexLazyImport
parentRoute: typeof rootRoute
}
"/admin/submissions/$submissionId": {
id: "/admin/submissions/$submissionId"
path: "/admin/submissions/$submissionId"
fullPath: "/admin/submissions/$submissionId"
preLoaderRoute: typeof AdminSubmissionsSubmissionIdRouteImport
parentRoute: typeof rootRoute
}
"/admin/submissions/": {
id: "/admin/submissions/"
path: "/admin/submissions"
Expand All @@ -117,6 +136,7 @@ export interface FileRoutesByFullPath {
"/": typeof IndexLazyRoute
"/problems/$problemId": typeof ProblemsProblemIdRouteRoute
"/problems": typeof ProblemsIndexLazyRoute
"/admin/submissions/$submissionId": typeof AdminSubmissionsSubmissionIdRouteRoute
"/admin/submissions": typeof AdminSubmissionsIndexLazyRoute
"/admin/problems/$problemId": typeof AdminProblemsProblemIdIndexRoute
}
Expand All @@ -125,6 +145,7 @@ export interface FileRoutesByTo {
"/": typeof IndexLazyRoute
"/problems/$problemId": typeof ProblemsProblemIdRouteRoute
"/problems": typeof ProblemsIndexLazyRoute
"/admin/submissions/$submissionId": typeof AdminSubmissionsSubmissionIdRouteRoute
"/admin/submissions": typeof AdminSubmissionsIndexLazyRoute
"/admin/problems/$problemId": typeof AdminProblemsProblemIdIndexRoute
}
Expand All @@ -134,6 +155,7 @@ export interface FileRoutesById {
"/": typeof IndexLazyRoute
"/problems/$problemId": typeof ProblemsProblemIdRouteRoute
"/problems/": typeof ProblemsIndexLazyRoute
"/admin/submissions/$submissionId": typeof AdminSubmissionsSubmissionIdRouteRoute
"/admin/submissions/": typeof AdminSubmissionsIndexLazyRoute
"/admin/problems/$problemId/": typeof AdminProblemsProblemIdIndexRoute
}
Expand All @@ -144,20 +166,23 @@ export interface FileRouteTypes {
| "/"
| "/problems/$problemId"
| "/problems"
| "/admin/submissions/$submissionId"
| "/admin/submissions"
| "/admin/problems/$problemId"
fileRoutesByTo: FileRoutesByTo
to:
| "/"
| "/problems/$problemId"
| "/problems"
| "/admin/submissions/$submissionId"
| "/admin/submissions"
| "/admin/problems/$problemId"
id:
| "__root__"
| "/"
| "/problems/$problemId"
| "/problems/"
| "/admin/submissions/$submissionId"
| "/admin/submissions/"
| "/admin/problems/$problemId/"
fileRoutesById: FileRoutesById
Expand All @@ -167,6 +192,7 @@ export interface RootRouteChildren {
IndexLazyRoute: typeof IndexLazyRoute
ProblemsProblemIdRouteRoute: typeof ProblemsProblemIdRouteRoute
ProblemsIndexLazyRoute: typeof ProblemsIndexLazyRoute
AdminSubmissionsSubmissionIdRouteRoute: typeof AdminSubmissionsSubmissionIdRouteRoute
AdminSubmissionsIndexLazyRoute: typeof AdminSubmissionsIndexLazyRoute
AdminProblemsProblemIdIndexRoute: typeof AdminProblemsProblemIdIndexRoute
}
Expand All @@ -175,6 +201,8 @@ const rootRouteChildren: RootRouteChildren = {
IndexLazyRoute: IndexLazyRoute,
ProblemsProblemIdRouteRoute: ProblemsProblemIdRouteRoute,
ProblemsIndexLazyRoute: ProblemsIndexLazyRoute,
AdminSubmissionsSubmissionIdRouteRoute:
AdminSubmissionsSubmissionIdRouteRoute,
AdminSubmissionsIndexLazyRoute: AdminSubmissionsIndexLazyRoute,
AdminProblemsProblemIdIndexRoute: AdminProblemsProblemIdIndexRoute,
}
Expand All @@ -192,6 +220,7 @@ export const routeTree = rootRoute
"/",
"/problems/$problemId",
"/problems/",
"/admin/submissions/$submissionId",
"/admin/submissions/",
"/admin/problems/$problemId/"
]
Expand All @@ -205,6 +234,9 @@ export const routeTree = rootRoute
"/problems/": {
"filePath": "problems/index.lazy.tsx"
},
"/admin/submissions/$submissionId": {
"filePath": "admin/submissions/$submissionId/route.tsx"
},
"/admin/submissions/": {
"filePath": "admin/submissions/index.lazy.tsx"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SubmissionInfo } from "./submission-info"
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { CheckCircle, XCircle } from "lucide-react"
import { components } from "openapi/schema"

type Submission = components["schemas"]["Submission"]

export const SubmissionInfo = ({ submission }: { submission: Submission }) => (
<Card>
<CardHeader>
<CardTitle>基本情報</CardTitle>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* 問題ID */}
<div>
<dt className="font-semibold">問題ID</dt>
<dd>{submission.problem_id}</dd>
</div>
{/* 提出ID */}
<div>
<dt className="font-semibold">提出ID</dt>
<dd>{submission.id}</dd>
</div>
{/* 提出日時 */}
<div>
<dt className="font-semibold">提出日時</dt>
<dd>{submission.submitted_at}</dd>
</div>
{/* 学生ID */}
<div>
<dt className="font-semibold">学生ID</dt>
<dd>{submission.student_id}</dd>
</div>
{/* プログラミング言語 */}
<div>
<dt className="font-semibold">プログラミング言語</dt>
<dd>{`${submission.language.name} (${submission.language.version})`}</dd>
</div>
{/* 結果 */}
<div>
<dt className="font-semibold">全体の結果</dt>
<dd className="flex items-center">
{submission.result.status === "Accepted" ? (
<CheckCircle className="mr-2 h-5 w-5 text-green-500" />
) : (
<XCircle className="mr-2 h-5 w-5 text-red-500" />
)}
{submission.result.status}
</dd>
</div>
</dl>
</CardContent>
</Card>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SubmittedCode } from "./submitted-code"
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import MonacoEditor from "@monaco-editor/react"

export const ReadOnlyCodeBlock = ({
code,
language,
}: {
code: string
language: string
}) => (
<MonacoEditor
height="100%"
language={language}
options={{
fixedOverflowWidgets: true,
lineNumbers: "on",
minimap: { enabled: false },
readOnly: true,
scrollBeyondLastLine: false,
}}
value={code}
/>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Check, Copy } from "lucide-react"
import { components } from "openapi/schema"
import { useState } from "react"

import { ReadOnlyCodeBlock } from "./read-only-code-block"

type Submission = components["schemas"]["Submission"]

export const SubmittedCode = ({ submission }: { submission: Submission }) => {
const [isCopied, setIsCopied] = useState(false)

const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(submission.code)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 2000)
} catch (error) {
console.error("Failed to copy text:", error)
}
}

return (
<Card className="flex flex-1 flex-col">
<CardHeader>
<CardTitle>提出されたコード</CardTitle>
</CardHeader>
<CardContent className="flex-1 overflow-auto">
<div className="relative h-full overflow-x-auto rounded-md bg-gray-100 p-4">
<ReadOnlyCodeBlock
code={submission.code}
language={submission.language.name.toLowerCase()}
/>
<button
aria-label="コードをコピー"
className="absolute right-5 top-5 rounded-md bg-white p-2 shadow-sm hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
onClick={copyToClipboard}
>
{isCopied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TestResults } from "./test-results"
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { ChevronDown, ChevronUp } from "lucide-react"
import { components } from "openapi/schema"
import { useState } from "react"

type Submission = components["schemas"]["Submission"]
type TestCase = components["schemas"]["TestCase"]

export const TestResults = ({
submission,
testCases,
}: {
submission: Submission
testCases: TestCase[]
}) => {
const [openIndexes, setOpenIndexes] = useState<boolean[]>(
submission.test_results.map(() => false),
)

const toggleOpen = (index: number) => {
setOpenIndexes((prev) => {
const newState = [...prev]
newState[index] = !newState[index]
return newState
})
}

return (
<Card>
<CardHeader>
<CardTitle>テスト結果</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>テストケースID</TableHead>
<TableHead>結果</TableHead>
<TableHead>メッセージ</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{submission.test_results.map((TestResults, index) => (
<>
<TableRow
className="cursor-pointer hover:bg-gray-100"
key={TestResults.test_case_id}
onClick={() => toggleOpen(index)}
>
<TableCell>{TestResults.test_case_id}</TableCell>
<TableCell
className={
TestResults.status === "Passed"
? "text-green-500"
: "text-red-500"
}
>
{TestResults.status}
</TableCell>
<TableCell>{TestResults.message || "-"}</TableCell>
<TableCell>
{openIndexes[index] ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</TableCell>
</TableRow>
{openIndexes[index] && (
<TableRow>
<TableCell colSpan={4}>
<div className="rounded-md bg-gray-50 p-4">
<h4 className="mb-2 font-semibold">入力:</h4>
<pre className="mb-4 overflow-x-auto rounded-md bg-white p-2">
<code>
{testCases[index]?.input || "データがありません"}
</code>
</pre>
<h4 className="mb-2 font-semibold">正解出力:</h4>
<pre className="overflow-x-auto rounded-md bg-white p-2">
<code>
{testCases[index]?.output || "データがありません"}
</code>
</pre>
</div>
</TableCell>
</TableRow>
)}
</>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)
}
Loading

0 comments on commit dd05631

Please sign in to comment.