Public Access
- Updated database schema to include `storage_quota_bytes` and `plan` for users. - Implemented storage usage calculations in `storageRepo` and added tests for storage functionality. - Created `StorageManager` component to display and manage user storage. - Enhanced user settings to show storage details and warnings for high usage. - Added translations for new storage-related strings in English and Dutch. - Updated styles for storage overview and details in CSS.
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
|
import { useNavigate, useParams, Navigate } from 'react-router-dom'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useAuth } from '../contexts/AuthContext'
|
|
import * as examApi from '../api/examApi'
|
|
import * as resultApi from '../api/resultApi'
|
|
import type { Exam } from '../types'
|
|
import MyExams from './MyExams'
|
|
import MyResults from './MyResults'
|
|
import Settings from './Settings'
|
|
import StorageManager from './StorageManager'
|
|
|
|
type TabKey = 'exams' | 'results' | 'storage' | 'settings'
|
|
|
|
const TAB_KEYS: { key: TabKey; labelKey: string; icon: string }[] = [
|
|
{ key: 'exams', labelKey: 'workspace.myExams', icon: '◇' },
|
|
{ key: 'results', labelKey: 'workspace.results', icon: '✦' },
|
|
{ key: 'storage', labelKey: 'workspace.storage', icon: '◉' },
|
|
{ key: 'settings', labelKey: 'workspace.settings', icon: '⚙' },
|
|
]
|
|
|
|
interface Props {
|
|
onLoadExam: (exam: Exam, saveToWorkspace?: boolean, examId?: number | null) => void
|
|
}
|
|
|
|
export default function Workspace({ onLoadExam }: Props) {
|
|
const { t } = useTranslation()
|
|
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 examApi.listExams()
|
|
setExamCount(exams.length)
|
|
const results = await resultApi.listResults()
|
|
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">
|
|
{TAB_KEYS.map((tabItem) => (
|
|
<button
|
|
key={tabItem.key}
|
|
className={`workspace-nav-item${activeTab === tabItem.key ? ' active' : ''}`}
|
|
onClick={() => setTab(tabItem.key)}
|
|
>
|
|
<span className="workspace-nav-icon">{tabItem.icon}</span>
|
|
<span className="workspace-nav-label">{t(tabItem.labelKey)}</span>
|
|
{tabItem.key === 'exams' && examCount > 0 && (
|
|
<span className="workspace-nav-badge">{examCount}</span>
|
|
)}
|
|
{tabItem.key === 'results' && resultCount > 0 && (
|
|
<span className="workspace-nav-badge">{resultCount}</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</aside>
|
|
|
|
<main className="workspace-main">
|
|
{activeTab === 'exams' && <MyExams onLoadExam={onLoadExam} />}
|
|
{activeTab === 'results' && <MyResults onLoadExam={onLoadExam} />}
|
|
{activeTab === 'storage' && <StorageManager />}
|
|
{activeTab === 'settings' && <Settings />}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|