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

+83
View File
@@ -0,0 +1,83 @@
import { useState, useEffect, useCallback } from 'react'
import { useNavigate, useParams, Navigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { apiFetch } from '../utils/api'
import MyExams from './MyExams'
import MyResults from './MyResults'
import Settings from './Settings'
type TabKey = 'exams' | 'results' | 'settings'
const TABS: { key: TabKey; label: string; icon: string }[] = [
{ key: 'exams', label: 'My Exams', icon: '◇' },
{ key: 'results', label: 'Results', icon: '✦' },
{ key: 'settings', label: 'Settings', icon: '⚙' },
]
export default function Workspace() {
const { user, loading: authLoading } = useAuth()
const navigate = useNavigate()
const { tab } = useParams<{ tab: string }>()
const activeTab = (tab as TabKey) ?? 'exams'
const [examCount, setExamCount] = useState(0)
const [resultCount, setResultCount] = useState(0)
const loadCounts = useCallback(async () => {
try {
const exams = await apiFetch<{ id: number }[]>('/api/exams')
setExamCount(exams.length)
const results = await apiFetch<{ results: unknown[]; total: number }>('/api/results')
setResultCount(results.total)
} catch {
// silently ignore
}
}, [])
useEffect(() => {
if (user) void loadCounts()
}, [user, loadCounts])
if (authLoading) return null
if (!user) return <Navigate to="/" replace />
const setTab = (key: TabKey) => navigate(`/workspace/${key}`)
return (
<div className="workspace-shell">
<aside className="workspace-sidebar">
<div className="workspace-profile">
<div className="workspace-avatar">
{user.displayName.charAt(0).toUpperCase()}
</div>
<div className="workspace-name">{user.displayName}</div>
<div className="workspace-email">{user.email}</div>
</div>
<nav className="workspace-nav">
{TABS.map((t) => (
<button
key={t.key}
className={`workspace-nav-item${activeTab === t.key ? ' active' : ''}`}
onClick={() => setTab(t.key)}
>
<span className="workspace-nav-icon">{t.icon}</span>
<span className="workspace-nav-label">{t.label}</span>
{t.key === 'exams' && examCount > 0 && (
<span className="workspace-nav-badge">{examCount}</span>
)}
{t.key === 'results' && resultCount > 0 && (
<span className="workspace-nav-badge">{resultCount}</span>
)}
</button>
))}
</nav>
</aside>
<main className="workspace-main">
{activeTab === 'exams' && <MyExams />}
{activeTab === 'results' && <MyResults />}
{activeTab === 'settings' && <Settings />}
</main>
</div>
)
}