Public Access
feat: add MyExams, MyResults, Settings, and Workspace components
Release Pipeline / build-and-release (push) Failing after 41s
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:
1 parent
ad777ef1c3
commit
62af83ce53
16 files changed
+2192
-47
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user