feat: add MyExams, MyResults, Settings, and Workspace components
Release Pipeline / build-and-release (push) Failing after 41s

- Implemented MyExams component for managing user exams, including loading, deleting, and creating new exams.
- Created MyResults component to display user results with options to review and delete results.
- Developed Settings component for updating user display name and changing password.
- Introduced Workspace component to serve as a container for MyExams, MyResults, and Settings, with navigation between tabs.
This commit is contained in:
LaurensBartelds committed 2026-04-25 20:20:23 +02:00
1 parent ad777ef1c3
commit 62af83ce53
16 files changed
+2192 -47

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
import { useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import { apiFetch } from '../utils/api'
import type { Exam } from '../types'
interface SavedExam {
id: number
title: string
subtitle: string | null
question_count: number
created_at: number
updated_at: number
}
export default function MyExams() {
const navigate = useNavigate()
const [exams, setExams] = useState<SavedExam[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null)
const loadExams = useCallback(async () => {
setLoading(true)
setError(null)
try {
const data = await apiFetch<SavedExam[]>('/api/exams')
setExams(data)
} catch (e) {
setError((e as Error).message)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
void loadExams()
}, [loadExams])
const deleteExam = async (id: number) => {
try {
await apiFetch(`/api/exams/${id}`, { method: 'DELETE' })
setDeleteConfirm(null)
void loadExams()
} catch (e) {
setError((e as Error).message)
}
}
const createNewExam = () => {
navigate('/exam-editor/new')
}
const formatDate = (ts: number) => {
const d = new Date(ts * 1000)
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
return (
<div className="workspace-section fade-in">
<div className="workspace-section-header">
<div>
<div className="workspace-section-eyebrow">My Workspace</div>
<h2 className="workspace-section-title">My Exams</h2>
</div>
<button className="btn btn-primary btn-sm" onClick={createNewExam}>
+ New Exam
</button>
</div>
{error && (
<div className="workspace-alert workspace-alert-error">
<span>{error}</span>
<button onClick={() => setError(null)} className="workspace-alert-close">×</button>
</div>
)}
{loading ? (
<div className="workspace-empty">Loading…</div>
) : exams.length === 0 ? (
<div className="workspace-empty">
<div className="workspace-empty-icon">◇</div>
<div className="workspace-empty-title">No saved exams yet</div>
<div className="workspace-empty-desc">
Upload an exam from the home page, or create a new one to get started.
</div>
<button className="btn btn-primary" onClick={createNewExam} style={{ marginTop: 16 }}>
Create Your First Exam
</button>
</div>
) : (
<div className="workspace-list">
{exams.map((exam) => (
<div key={exam.id} className="workspace-card">
<div className="workspace-card-body">
<div className="workspace-card-title">{exam.title}</div>
{exam.subtitle && (
<div className="workspace-card-sub">{exam.subtitle}</div>
)}
<div className="workspace-card-meta">
<span>{exam.question_count} question{exam.question_count !== 1 ? 's' : ''}</span>
<span>·</span>
<span>Updated {formatDate(exam.updated_at)}</span>
</div>
</div>
<div className="workspace-card-actions">
<button
className="btn btn-sm"
onClick={() => navigate(`/exam-editor/${exam.id}`)}
>
Edit
</button>
{deleteConfirm === exam.id ? (
<div className="workspace-delete-confirm">
<button className="btn btn-sm admin-btn-danger" onClick={() => void deleteExam(exam.id)}>
Confirm
</button>
<button className="btn btn-ghost btn-sm" onClick={() => setDeleteConfirm(null)}>
Cancel
</button>
</div>
) : (
<button
className="btn btn-ghost btn-sm admin-btn-danger-ghost"
onClick={() => setDeleteConfirm(exam.id)}
title="Delete exam"
>
✕
</button>
)}
</div>
</div>
))}
</div>
)}
</div>
)
}