Add import/export functionality
This commit is contained in:
+77
-10
@@ -7,6 +7,7 @@ import { javascript } from "@codemirror/lang-javascript";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
CheckCircle2,
|
||||
Download,
|
||||
Eye,
|
||||
FileText,
|
||||
History,
|
||||
@@ -698,6 +699,8 @@ function AdminApp({ refreshAuth }: { refreshAuth: () => void }) {
|
||||
if (!selectedTask.groupId && groupData[0]) {
|
||||
setSelectedTask((task) => ({ ...task, groupId: groupData[0].id }));
|
||||
}
|
||||
|
||||
return { groupData, submissionData };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -802,6 +805,53 @@ function AdminApp({ refreshAuth }: { refreshAuth: () => void }) {
|
||||
});
|
||||
}
|
||||
|
||||
async function exportTasks() {
|
||||
setError("");
|
||||
setMessage("");
|
||||
const response = await fetch("/api/admin/export");
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => ({ error: response.statusText }));
|
||||
throw new ApiError(typeof payload.error === "string" ? payload.error : "Could not export tasks.", collectErrorDetails(payload));
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const disposition = response.headers.get("content-disposition") ?? "";
|
||||
const fileNameMatch = disposition.match(/filename="([^"]+)"/);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileNameMatch?.[1] ?? `frontend-judge-tasks-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
setMessage("Task export downloaded.");
|
||||
}
|
||||
|
||||
async function importTasks(file: File) {
|
||||
setError("");
|
||||
setMessage("");
|
||||
const shouldImport = window.confirm("Importing will replace all current groups, tasks, and submissions. Continue?");
|
||||
if (!shouldImport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(await file.text()) as unknown;
|
||||
const result = await api<{ groups: number; tasks: number; warnings: string[] }>("/api/admin/import", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(parsed)
|
||||
});
|
||||
const { groupData } = await loadAdminData();
|
||||
setSelectedTask({ ...emptyTask, groupId: groupData[0]?.id ?? "" });
|
||||
setSelectedSubmission(null);
|
||||
setMessage(
|
||||
[
|
||||
`Imported ${result.groups} group(s) and ${result.tasks} task(s).`,
|
||||
...(result.warnings ?? [])
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<header className="topbar">
|
||||
@@ -809,16 +859,33 @@ function AdminApp({ refreshAuth }: { refreshAuth: () => void }) {
|
||||
<h1>Admin dashboard</h1>
|
||||
<p>Create day groups, tasks, checks, and reference screenshots.</p>
|
||||
</div>
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={async () => {
|
||||
await api("/api/auth/logout", { method: "POST" });
|
||||
refreshAuth();
|
||||
}}
|
||||
title="Log out"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
</button>
|
||||
<div className="topbar-actions">
|
||||
<button className="secondary" onClick={() => exportTasks().catch((err) => setError(errorMessage(err, "Could not export tasks.")))}>
|
||||
<Download size={17} /> Export
|
||||
</button>
|
||||
<label className="upload-button">
|
||||
<Upload size={17} /> Import
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (file) importTasks(file).catch((err) => setError(errorMessage(err, "Could not import tasks.")));
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="ghost"
|
||||
onClick={async () => {
|
||||
await api("/api/auth/logout", { method: "POST" });
|
||||
refreshAuth();
|
||||
}}
|
||||
title="Log out"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="admin-layout">
|
||||
|
||||
@@ -102,6 +102,14 @@ p {
|
||||
color: #5b6b79;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.student-layout,
|
||||
.admin-layout {
|
||||
display: grid;
|
||||
@@ -645,6 +653,7 @@ p {
|
||||
.success {
|
||||
color: #167142;
|
||||
font-weight: 750;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
|
||||
Reference in New Issue
Block a user