Add dashboard in-progress quizzes

This commit is contained in:
Daniel 2026-05-12 16:41:44 +02:00
parent d4ef94a117
commit dd959371d1
4 changed files with 64 additions and 50 deletions

View file

@ -0,0 +1,50 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '../api/client'
import ConfirmButton from './ConfirmButton'
export default function InProgressQuizzes() {
const [inProgress, setInProgress] = useState([])
const navigate = useNavigate()
useEffect(() => {
api.get('/attempts/in-progress').then(res => setInProgress(res.data)).catch(() => {})
}, [])
const deleteAttempt = async (attemptId) => {
await api.delete(`/attempts/${attemptId}`)
setInProgress(prev => prev.filter(a => a.attempt_id !== attemptId))
}
if (inProgress.length === 0) return null
return (
<div className="card" style={{ marginBottom: 16, borderLeft: '4px solid #f59e0b' }}>
<h2 style={{ marginBottom: 12, fontSize: '1rem', color: '#92400e' }}>
In Progress ({inProgress.length})
</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{inProgress.map(a => (
<div key={a.attempt_id} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, gap: 12,
}}>
<div>
<div style={{ fontWeight: 600, fontSize: '0.9rem' }}>{a.quiz_title}</div>
<div style={{ fontSize: '0.78rem', color: 'var(--text-muted)' }}>
Started {new Date(a.started_at).toLocaleDateString()} · {a.total_questions} questions
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<button className="btn btn-primary btn-sm" onClick={() => navigate(`/quizzes/${a.quiz_id}`)}>Resume</button>
<ConfirmButton
label="Delete" confirmLabel="Yes, delete"
onConfirm={() => deleteAttempt(a.attempt_id)}
/>
</div>
</div>
))}
</div>
</div>
)
}

View file

@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import api from '../api/client'
import LineChart from '../components/LineChart'
import InProgressQuizzes from '../components/InProgressQuizzes'
import { useAuth } from '../context/AuthContext'
function greeting(name) {
@ -74,6 +75,8 @@ export default function DashboardPage() {
</div>
)}
<InProgressQuizzes />
{/* Performance graph with dropdown */}
{history.length > 0 && (
<div className="card">

View file

@ -417,6 +417,10 @@ export default function QuizPage() {
toastRef.current = setTimeout(() => setToast(''), 3000)
}
const adjustImageZoom = (delta) => {
setImageZoom(z => Math.max(1, Math.min(4, z + delta)))
}
useEffect(() => {
try {
const saved = localStorage.getItem(`quiz-highlights:${id}`)
@ -1061,10 +1065,12 @@ const timerStarted = timeLeft !== null
{expandedImagePath && (
<div className="image-lightbox" role="dialog" aria-modal="true" aria-label="Expanded question image" onClick={() => setExpandedImagePath('')}>
<div className="image-lightbox-controls" onClick={e => e.stopPropagation()}>
<button type="button" onClick={() => setImageZoom(z => Math.max(1, z - 0.25))} disabled={imageZoom <= 1}>-</button>
<button type="button" onClick={() => adjustImageZoom(-0.5)} disabled={imageZoom <= 1}>-</button>
<span>{Math.round(imageZoom * 100)}%</span>
<button type="button" onClick={() => setImageZoom(z => Math.min(4, z + 0.25))} disabled={imageZoom >= 4}>+</button>
<button type="button" onClick={() => setImageZoom(1)} disabled={imageZoom === 1}>Reset</button>
<button type="button" onClick={() => adjustImageZoom(0.5)} disabled={imageZoom >= 4}>+</button>
{[1, 2, 3, 4].map(zoom => (
<button key={zoom} type="button" onClick={() => setImageZoom(zoom)} disabled={imageZoom === zoom}>{zoom}x</button>
))}
</div>
<button className="image-lightbox-close" onClick={() => setExpandedImagePath('')} type="button" aria-label="Close expanded image">×</button>
<div className="image-lightbox-viewport" onClick={e => e.stopPropagation()}>

View file

@ -4,56 +4,11 @@ import { useAuth } from '../context/AuthContext'
import api from '../api/client'
import ConfirmButton from '../components/ConfirmButton'
import Dialog from '../components/Dialog'
import InProgressQuizzes from '../components/InProgressQuizzes'
import { useDialog } from '../hooks/useDialog'
const TeachChat = lazy(() => import('../components/TeachChat'))
function InProgressSection() {
const [inProgress, setInProgress] = useState([])
const navigate = useNavigate()
useEffect(() => {
api.get('/attempts/in-progress').then(res => setInProgress(res.data)).catch(() => {})
}, [])
const deleteAttempt = async (attemptId) => {
await api.delete(`/attempts/${attemptId}`)
setInProgress(prev => prev.filter(a => a.attempt_id !== attemptId))
}
if (inProgress.length === 0) return null
return (
<div className="card" style={{ marginBottom: 16, borderLeft: '4px solid #f59e0b' }}>
<h2 style={{ marginBottom: 12, fontSize: '1rem', color: '#92400e' }}>
In Progress ({inProgress.length})
</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{inProgress.map(a => (
<div key={a.attempt_id} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, gap: 12,
}}>
<div>
<div style={{ fontWeight: 600, fontSize: '0.9rem' }}>{a.quiz_title}</div>
<div style={{ fontSize: '0.78rem', color: 'var(--text-muted)' }}>
Started {new Date(a.started_at).toLocaleDateString()} · {a.total_questions} questions
</div>
</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
<button className="btn btn-primary btn-sm" onClick={() => navigate(`/quizzes/${a.quiz_id}`)}>Resume</button>
<ConfirmButton
label="Delete" confirmLabel="Yes, delete"
onConfirm={() => deleteAttempt(a.attempt_id)}
/>
</div>
</div>
))}
</div>
</div>
)
}
function PastAttemptsSection() {
const [open, setOpen] = useState(false)
const [history, setHistory] = useState(null)
@ -506,7 +461,7 @@ export default function QuizzesPage() {
)}
{/* In-progress quizzes */}
{!isSearching && <InProgressSection />}
{!isSearching && <InProgressQuizzes />}
{/* Past attempts (loads on demand) */}
{!isSearching && <PastAttemptsSection />}