Add dashboard in-progress quizzes
This commit is contained in:
parent
d4ef94a117
commit
dd959371d1
4 changed files with 64 additions and 50 deletions
50
frontend/src/components/InProgressQuizzes.jsx
Normal file
50
frontend/src/components/InProgressQuizzes.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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()}>
|
||||
|
|
|
|||
|
|
@ -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 />}
|
||||
|
|
|
|||
Loading…
Reference in a new issue