Public Access
feat: enhance exam loading functionality with examId support and update UI components
This commit is contained in:
1 parent
50efba6f34
commit
2b7939d478
5 files changed
+41
-5
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user