feat: enhance exam loading functionality with examId support and update UI components
Test / backend (push) Successful in 14s
Test / frontend (push) Successful in 24s

This commit is contained in:
LaurensBartelds committed 2026-04-26 23:25:24 +02:00
1 parent 50efba6f34
commit 2b7939d478
5 files changed
+41 -5

No files matched your search

+5 -2
View File
@@ -129,7 +129,7 @@ function AppInner() {
setExamState(withNotes(state)); setExamState(withNotes(state));
}; };
const loadExam = (data: Exam, saveToWorkspace = false) => { const loadExam = (data: Exam, saveToWorkspace = false, examId?: number | null) => {
if (saveToWorkspace && user && isAuthenticated()) { if (saveToWorkspace && user && isAuthenticated()) {
examApi.createExam(data) examApi.createExam(data)
.then((res) => { .then((res) => {
@@ -139,6 +139,9 @@ function AppInner() {
// silent fail — exam not saved but user can still take it // silent fail — exam not saved but user can still take it
}); });
} }
if (examId != null) {
savedExamIdRef.current = examId;
}
const fm = const fm =
tweaks.feedbackMode === "auto" tweaks.feedbackMode === "auto"
? (data.feedbackMode ?? "end") ? (data.feedbackMode ?? "end")
@@ -494,7 +497,7 @@ function AppInner() {
} }
/> />
<Route path="/workspace" element={<Navigate to="/workspace/exams" replace />} /> <Route path="/workspace" element={<Navigate to="/workspace/exams" replace />} />
<Route path="/workspace/:tab" element={<Workspace />} /> <Route path="/workspace/:tab" element={<Workspace onLoadExam={loadExam} />} />
<Route path="/exam-editor/:id" element={<ExamEditor />} /> <Route path="/exam-editor/:id" element={<ExamEditor />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
+27 -1
View File
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import * as examApi from '../api/examApi' import * as examApi from '../api/examApi'
import type { Exam } from '../types'
interface SavedExam { interface SavedExam {
id: number id: number
@@ -12,13 +13,18 @@ interface SavedExam {
updated_at: number updated_at: number
} }
export default function MyExams() { interface Props {
onLoadExam: (exam: Exam, saveToWorkspace?: boolean, examId?: number | null) => void
}
export default function MyExams({ onLoadExam }: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const [exams, setExams] = useState<SavedExam[]>([]) const [exams, setExams] = useState<SavedExam[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null) const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null)
const [startingId, setStartingId] = useState<number | null>(null)
const loadExams = useCallback(async () => { const loadExams = useCallback(async () => {
setLoading(true) setLoading(true)
@@ -51,6 +57,17 @@ export default function MyExams() {
navigate('/exam-editor/new') navigate('/exam-editor/new')
} }
const startExam = async (id: number) => {
setStartingId(id)
try {
const exam = await examApi.getExam(id)
onLoadExam(exam, false, id)
} catch (e) {
setError((e as Error).message)
setStartingId(null)
}
}
const formatDate = (ts: number) => { const formatDate = (ts: number) => {
const d = new Date(ts * 1000) const d = new Date(ts * 1000)
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
@@ -104,9 +121,17 @@ export default function MyExams() {
</div> </div>
</div> </div>
<div className="workspace-card-actions"> <div className="workspace-card-actions">
<button
className="btn btn-primary btn-sm"
onClick={() => void startExam(exam.id)}
disabled={startingId === exam.id}
>
{startingId === exam.id ? t('common.loading') : t('workspace.takeExam')}
</button>
<button <button
className="btn btn-sm" className="btn btn-sm"
onClick={() => navigate(`/exam-editor/${exam.id}`)} onClick={() => navigate(`/exam-editor/${exam.id}`)}
disabled={startingId === exam.id}
> >
{t('common.edit')} {t('common.edit')}
</button> </button>
@@ -124,6 +149,7 @@ export default function MyExams() {
className="btn btn-ghost btn-sm admin-btn-danger-ghost" className="btn btn-ghost btn-sm admin-btn-danger-ghost"
onClick={() => setDeleteConfirm(exam.id)} onClick={() => setDeleteConfirm(exam.id)}
title={t('common.delete')} title={t('common.delete')}
disabled={startingId === exam.id}
> >
✕ ✕
</button> </button>
+7 -2
View File
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import * as examApi from '../api/examApi' import * as examApi from '../api/examApi'
import * as resultApi from '../api/resultApi' import * as resultApi from '../api/resultApi'
import type { Exam } from '../types'
import MyExams from './MyExams' import MyExams from './MyExams'
import MyResults from './MyResults' import MyResults from './MyResults'
import Settings from './Settings' import Settings from './Settings'
@@ -16,7 +17,11 @@ const TAB_KEYS: { key: TabKey; labelKey: string; icon: string }[] = [
{ key: 'settings', labelKey: 'workspace.settings', icon: '⚙' }, { key: 'settings', labelKey: 'workspace.settings', icon: '⚙' },
] ]
export default function Workspace() { interface Props {
onLoadExam: (exam: Exam, saveToWorkspace?: boolean, examId?: number | null) => void
}
export default function Workspace({ onLoadExam }: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const { user, loading: authLoading } = useAuth() const { user, loading: authLoading } = useAuth()
const navigate = useNavigate() const navigate = useNavigate()
@@ -77,7 +82,7 @@ export default function Workspace() {
</aside> </aside>
<main className="workspace-main"> <main className="workspace-main">
{activeTab === 'exams' && <MyExams />} {activeTab === 'exams' && <MyExams onLoadExam={onLoadExam} />}
{activeTab === 'results' && <MyResults />} {activeTab === 'results' && <MyResults />}
{activeTab === 'settings' && <Settings />} {activeTab === 'settings' && <Settings />}
</main> </main>
+1
View File
@@ -149,6 +149,7 @@
"noSavedExams": "No saved exams yet", "noSavedExams": "No saved exams yet",
"noSavedExamsDesc": "Upload an exam from the home page, or create a new one to get started.", "noSavedExamsDesc": "Upload an exam from the home page, or create a new one to get started.",
"createFirstExam": "Create Your First Exam", "createFirstExam": "Create Your First Exam",
"takeExam": "Take exam",
"noAttempts": "No attempts yet", "noAttempts": "No attempts yet",
"noAttemptsDesc": "Take an exam to see your results here.", "noAttemptsDesc": "Take an exam to see your results here.",
"attempts": "attempt", "attempts": "attempt",
+1
View File
@@ -149,6 +149,7 @@
"noSavedExams": "Nog geen opgeslagen examens", "noSavedExams": "Nog geen opgeslagen examens",
"noSavedExamsDesc": "Upload een examen vanaf de homepagina, of maak een nieuw examen aan om te beginnen.", "noSavedExamsDesc": "Upload een examen vanaf de homepagina, of maak een nieuw examen aan om te beginnen.",
"createFirstExam": "Maak je eerste examen", "createFirstExam": "Maak je eerste examen",
"takeExam": "Examen maken",
"noAttempts": "Nog geen pogingen", "noAttempts": "Nog geen pogingen",
"noAttemptsDesc": "Maak een examen om hier je resultaten te zien.", "noAttemptsDesc": "Maak een examen om hier je resultaten te zien.",
"attempts": "poging", "attempts": "poging",