diff --git a/backend/app/routers/attempts.py b/backend/app/routers/attempts.py index 7f09e18..14c7c6a 100644 --- a/backend/app/routers/attempts.py +++ b/backend/app/routers/attempts.py @@ -28,6 +28,7 @@ router = APIRouter() @router.post("/start", response_model=AttemptResponse) def start_attempt( quiz_id: int, + fresh: bool = False, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): @@ -35,6 +36,24 @@ def start_attempt( if not quiz: raise HTTPException(status_code=404, detail="Quiz not found") + # Reuse the most recent incomplete attempt unless fresh=true + if not fresh: + existing = db.query(QuizAttempt).filter( + QuizAttempt.quiz_id == quiz_id, + QuizAttempt.user_id == current_user.id, + QuizAttempt.completed_at.is_(None), + ).order_by(QuizAttempt.started_at.desc()).first() + if existing: + return AttemptResponse( + id=existing.id, + quiz_id=existing.quiz_id, + score=0, + total_questions=existing.total_questions, + percentage=0.0, + started_at=existing.started_at, + completed_at=None, + ) + attempt = QuizAttempt( quiz_id=quiz_id, user_id=current_user.id, @@ -271,6 +290,30 @@ def get_in_progress_attempt( ) +@router.get("/in-progress") +def get_in_progress_attempts( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """Return all incomplete (not yet submitted) attempts for this user.""" + incomplete = db.query(QuizAttempt).filter( + QuizAttempt.user_id == current_user.id, + QuizAttempt.completed_at.is_(None), + ).order_by(QuizAttempt.started_at.desc()).all() + + result = [] + for a in incomplete: + quiz = db.query(Quiz).filter(Quiz.id == a.quiz_id).first() + result.append({ + "attempt_id": a.id, + "quiz_id": a.quiz_id, + "quiz_title": quiz.title if quiz else f"Quiz {a.quiz_id}", + "total_questions": a.total_questions, + "started_at": a.started_at.isoformat() if a.started_at else None, + }) + return result + + @router.get("/history") def get_quiz_history( db: Session = Depends(get_db), diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 154a939..df7bac0 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -79,6 +79,7 @@ export default function Navbar() { // Close menu on route change useEffect(() => { setMenuOpen(false) }, [location.pathname]) + const navLinks = user ? [ { to: '/', label: 'Dashboard' }, { to: '/quizzes', label: 'Quizzes' }, diff --git a/frontend/src/pages/QuizPage.jsx b/frontend/src/pages/QuizPage.jsx index 3298c02..aa1c654 100644 --- a/frontend/src/pages/QuizPage.jsx +++ b/frontend/src/pages/QuizPage.jsx @@ -164,6 +164,16 @@ export default function QuizPage() { toastRef.current = setTimeout(() => setToast(''), 3000) } + const [leaveTarget, setLeaveTarget] = useState(null) + + // Warn before tab/window close when mid-quiz + useEffect(() => { + if (!attemptId) return + const handler = (e) => { e.preventDefault(); e.returnValue = 'You have an in-progress quiz. Your progress is saved.' } + window.addEventListener('beforeunload', handler) + return () => window.removeEventListener('beforeunload', handler) + }, [attemptId]) + useEffect(() => { const load = async () => { try { @@ -229,13 +239,7 @@ useEffect(() => { const setAnswer = (questionId, value) => setAnswers(prev => ({ ...prev, [questionId]: value })) - const safeNavigate = (targetIdx) => { - setCurrentIdx(targetIdx) - setTimeout(() => { - // Scroll to top of page so user sees the full question from the start - window.scrollTo({ top: 0, behavior: 'smooth' }) - }, 50) - } + const safeNavigate = (targetIdx) => setCurrentIdx(targetIdx) const handleSubmit = useCallback(async (autoSubmit = false) => { if (!attemptId || submitting) return @@ -307,6 +311,22 @@ useEffect(() => { return (
+ Your progress is saved — resume from where you left off next time. +
+