Critical bug fix:
- DELETE /quizzes/{id} was returning 500 due to {\"quiz_id\": None} update using
the DB column name instead of Python attribute name (source_quiz_id).
Fixed with {QuestionModel.source_quiz_id: None, synchronize_session=False}
Extraction fix:
- Two-phase extraction was incorrectly triggering for PREP 2012/2014 format
documents that have 'Preferred Response:' in their explanation text.
Fix: check for inline 'Correct Answer:' first — if found, always use standard
extraction regardless of 'Preferred Response:' appearing elsewhere.
Quiz trash bin:
- DELETE /quizzes/{id} now soft-deletes (sets deleted_at)
- GET /quizzes/trash — list deleted quizzes (moderator)
- PATCH /quizzes/{id}/restore — restore from trash
- DELETE /quizzes/{id}/permanent — permanent delete (must be in trash first)
- TrashPage.jsx — accessible via Settings → Admin → Trash
Hide/publish quizzes:
- is_published column on quizzes (1=visible, 0=hidden)
- PATCH /quizzes/{id}/publish?published=false — hide from regular users
- Moderators see all quizzes; regular users only see published
- 👁/🙈 toggle button per quiz card (moderators only)
Quiz progress resume (cross-browser via Redis):
- POST /attempts/progress — save {answers, current_idx, mode} to Redis (7 days)
- GET /attempts/progress?quiz_id=N — retrieve saved progress
- DELETE /attempts/progress/{quiz_id} — clear on submit
- QuizPage auto-saves to Redis every 1.5s (debounced) while in progress
- ModeSelectScreen loads saved progress from server, shows Resume button
- Works across browsers, devices, and after logout
Delete attempt:
- DELETE /attempts/{id} — user can delete own attempt + clears reminders for that quiz
ConfirmButton component:
- Replaces all window.confirm() / window.prompt() across the app
- Double-click pattern: first click shows [Confirm] [Cancel] inline
- Applied to: QuizzesPage, DocumentDetailPage, QuizEditPage, QuestionBankPage
Category delete (QuestionBankPage):
- window.prompt() replaced with inline modal dialog with select dropdown
- User chooses where to move questions before deletion
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
61 lines
2.2 KiB
JavaScript
61 lines
2.2 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import api from '../api/client'
|
|
import ConfirmButton from '../components/ConfirmButton'
|
|
|
|
export default function TrashPage() {
|
|
const [trashed, setTrashed] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const navigate = useNavigate()
|
|
|
|
useEffect(() => {
|
|
api.get('/quizzes/trash')
|
|
.then(res => setTrashed(res.data))
|
|
.catch(console.error)
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
const restore = async (id) => {
|
|
await api.patch(`/quizzes/${id}/restore`)
|
|
setTrashed(prev => prev.filter(q => q.id !== id))
|
|
}
|
|
|
|
const permanentDelete = async (id) => {
|
|
await api.delete(`/quizzes/${id}/permanent`)
|
|
setTrashed(prev => prev.filter(q => q.id !== id))
|
|
}
|
|
|
|
if (loading) return <div className="loading"><div className="spinner" /></div>
|
|
|
|
return (
|
|
<div style={{ maxWidth: 700, margin: '0 auto' }}>
|
|
<div className="card" style={{ marginBottom: 16 }}>
|
|
<h2>Trash</h2>
|
|
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem', marginTop: 4 }}>
|
|
Deleted quizzes — restore or permanently delete.
|
|
</p>
|
|
</div>
|
|
|
|
{trashed.length === 0 ? (
|
|
<div className="card"><div className="empty-state">Trash is empty</div></div>
|
|
) : trashed.map(quiz => (
|
|
<div className="card" key={quiz.id} style={{ marginBottom: 10, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
|
<div>
|
|
<div style={{ fontWeight: 600 }}>{quiz.title}</div>
|
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>
|
|
{quiz.questions_count} questions · deleted {new Date(quiz.deleted_at).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
|
|
<button className="btn btn-primary btn-sm" onClick={() => restore(quiz.id)}>Restore</button>
|
|
<ConfirmButton
|
|
label="Delete forever"
|
|
confirmLabel="Yes, permanently delete"
|
|
onConfirm={() => permanentDelete(quiz.id)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|