diff --git a/src/pages/Trivia.jsx b/src/pages/Trivia.jsx index 429af08..99ea430 100644 --- a/src/pages/Trivia.jsx +++ b/src/pages/Trivia.jsx @@ -17,167 +17,509 @@ * - [ ] Multiplayer / shared score (stub for future) * - [ ] Extract quiz logic into hook (useQuiz) for reuse/testing */ -import { useEffect, useState } from 'react'; -import Loading from '../components/Loading.jsx'; -import ErrorMessage from '../components/ErrorMessage.jsx'; -import Card from '../components/Card.jsx'; -import HeroSection from '../components/HeroSection'; -import Quiz from '../Images/Quiz.jpg'; + +import { useEffect, useState, useRef } from 'react'; + +// Mock components for demonstration +const Loading = () => ( +
+
+

Loading questions...

+
+); + +const ErrorMessage = ({ error }) => { + if (!error) return null; + return ( +
+ Error: {error.message || 'Something went wrong'} +
+ ); +}; + +const Card = ({ title, children }) => ( +
+ {title &&

{title}

} + {children} +
+); + +const HeroSection = ({ image, title, subtitle }) => ( +
+

{title}

+

{subtitle}

+
+); export default function Trivia() { const [questions, setQuestions] = useState([]); - const [category, setCategory] = useState('18'); // Science: Computers - const [difficulty, setDifficulty] = useState('easy'); // Default to easy + const [category, setCategory] = useState('18'); + const [difficulty, setDifficulty] = useState('easy'); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [score, setScore] = useState(0); - const [showReview, setShowReview] = useState(false); + const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); + const [showResults, setShowResults] = useState(false); const [highScore, setHighScore] = useState(0); - const [hasSavedForSet, setHasSavedForSet] = useState(false); + const [fadeIn, setFadeIn] = useState(true); + const [initialized, setInitialized] = useState(false); + + const abortControllerRef = useRef(null); + // Load high score only once on mount useEffect(() => { - fetchQuestions(); - }, [category, difficulty]); + const stored = window.triviaHighScore || 0; + setHighScore(stored); + }, []); - // Load high score once on mount + // Initial fetch only once on mount useEffect(() => { - try { - const stored = - typeof window !== 'undefined' - ? localStorage.getItem('triviaHighScore') - : null; - if (stored !== null) setHighScore(parseInt(stored, 10) || 0); - } catch (_) { - // ignore storage errors + if (!initialized) { + setInitialized(true); + fetchQuestions(); } - }, []); + }, [initialized]); async function fetchQuestions() { + // Cancel any ongoing request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + abortControllerRef.current = new AbortController(); + try { setLoading(true); setError(null); - setScore(0); - setShowReview(false); - setHasSavedForSet(false); + setShowResults(false); + setCurrentQuestionIndex(0); const res = await fetch( - `https://opentdb.com/api.php?amount=5&category=${category}&difficulty=${difficulty}&type=multiple` + `https://opentdb.com/api.php?amount=5&category=${category}&difficulty=${difficulty}&type=multiple`, + { + signal: abortControllerRef.current.signal + } ); - if (!res.ok) throw new Error('Failed to fetch'); + + if (!res.ok) throw new Error('Failed to fetch questions'); const json = await res.json(); + if (json.response_code === 1) { + throw new Error('No questions available for this category/difficulty combination'); + } else if (json.response_code === 2) { + throw new Error('Invalid parameter'); + } else if (json.response_code === 5) { + throw new Error('Rate limit exceeded. Please wait a moment and try again.'); + } + const qs = json.results.map((q) => ({ ...q, answers: shuffle([q.correct_answer, ...q.incorrect_answers]), picked: null, })); setQuestions(qs); + setFadeIn(true); } catch (e) { - setError(e); + if (e.name === 'AbortError') { + // Request was cancelled, ignore + return; + } else if (e.name === 'TimeoutError') { + setError(new Error('Request timed out. Please try again.')); + } else { + setError(e); + } } finally { setLoading(false); } } function shuffle(arr) { - return arr.sort(() => Math.random() - 0.5); + const newArr = [...arr]; + for (let i = newArr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [newArr[i], newArr[j]] = [newArr[j], newArr[i]]; + } + return newArr; } - function pick(qIndex, answer) { + function pick(answer) { setQuestions((qs) => - qs.map((q, i) => (i === qIndex ? { ...q, picked: answer } : q)) + qs.map((q, i) => (i === currentQuestionIndex ? { ...q, picked: answer } : q)) ); - if (questions[qIndex].correct_answer === answer) { - setScore((s) => s + 1); - } } - function decodeHtml(html) { - const txt = document.createElement('textarea'); - txt.innerHTML = html; - return txt.value; + function nextQuestion() { + if (currentQuestionIndex < questions.length - 1) { + setFadeIn(false); + setTimeout(() => { + setCurrentQuestionIndex((i) => i + 1); + setFadeIn(true); + }, 300); + } else { + setShowResults(true); + updateHighScore(); + } } - const answeredCount = questions.filter((q) => q.picked !== null).length; - const totalQuestions = questions.length; - const progressPercent = - totalQuestions > 0 ? (answeredCount / totalQuestions) * 100 : 0; + function previousQuestion() { + if (currentQuestionIndex > 0) { + setFadeIn(false); + setTimeout(() => { + setCurrentQuestionIndex((i) => i - 1); + setFadeIn(true); + }, 300); + } + } - const allAnswered = answeredCount === totalQuestions && totalQuestions > 0; + function calculateScore() { + return questions.filter(q => q.picked === q.correct_answer).length; + } - // Persist high score at end of game (once per question set) - useEffect(() => { - if (!allAnswered || hasSavedForSet) return; + function updateHighScore() { + const score = calculateScore(); if (score > highScore) { - try { - if (typeof window !== 'undefined') - localStorage.setItem('triviaHighScore', String(score)); - } catch (_) { - // ignore storage errors - } + window.triviaHighScore = score; setHighScore(score); } - setHasSavedForSet(true); - }, [allAnswered, score, highScore, hasSavedForSet]); + } + + function decodeHtml(html) { + const txt = document.createElement('textarea'); + txt.innerHTML = html; + return txt.value; + } function resetHighScore() { - try { - if (typeof window !== 'undefined') - localStorage.removeItem('triviaHighScore'); - } catch (_) { - // ignore storage errors - } + window.triviaHighScore = 0; setHighScore(0); } + function handleCategoryChange(e) { + setCategory(e.target.value); + } + + function handleDifficultyChange(e) { + setDifficulty(e.target.value); + } + + function startNewQuiz() { + fetchQuestions(); + } + + const currentQuestion = questions[currentQuestionIndex]; + const progressPercent = questions.length > 0 + ? ((currentQuestionIndex + 1) / questions.length) * 100 + : 0; + + const score = calculateScore(); + const canGoNext = currentQuestion && currentQuestion.picked !== null; + return ( <> + + - Think Fast, Learn Faster + Think Fast, Learn Faster } subtitle="A trivia playground for curious minds, quick thinkers, and casual know-it-alls" /> -
-
-

Trivia Quiz

- - {difficulty.charAt(0).toUpperCase() + difficulty.slice(1)} - +
+
+
+

Trivia Quiz

+ + {difficulty.charAt(0).toUpperCase() + difficulty.slice(1)} + +
+
+
+ Current: {score}/{questions.length} +
+
+ Best: {highScore} +
+
- {/* Category + Difficulty Selector */} -
+
+ +
- {/* Loading / Error */} {loading && } - {/* Progress Bar */} - {totalQuestions > 0 && ( -
-

- Progress: {answeredCount} / {totalQuestions} answered -

-
-
0 && !showResults && ( +
+
+
-
- )} - - {/* Score + Buttons */} -
-

- Score: {score} | Best: {highScore} -

- - -
+

+ Question {currentQuestionIndex + 1} of {questions.length} +

- {/* Quiz Cards */} - {questions.map((q, idx) => ( - -
    - {q.answers.map((a) => { - const isPicked = q.picked === a; - const isCorrect = a === q.correct_answer; - let btnClass = ''; - - if (showReview) { - btnClass = isCorrect - ? 'correct' - : isPicked - ? 'wrong' - : 'neutral'; - } else if (isPicked) { - btnClass = isCorrect ? 'correct' : 'wrong'; - } + +
    + {currentQuestion.answers.map((answer) => { + const isPicked = currentQuestion.picked === answer; - return ( -
  • + return ( -
  • + ); + })} +
    + +
    + + +
    +
    +
+ )} + + {showResults && ( +
+ +

🎯 Quiz Complete!

+
+ {score} / {questions.length} +
+

+ {score === questions.length + ? '🌟 Perfect score! You\'re a trivia master!' + : score >= questions.length * 0.7 + ? '🎉 Great job! Well done!' + : score >= questions.length * 0.5 + ? '👍 Good effort! Keep practicing!' + : '💪 Keep learning and try again!'} +

+ {score > highScore && ( +

+ 🏆 New High Score! +

+ )} +

+ Best Score: {highScore} +

+ +
+ + + {questions.map((q, idx) => { + const isCorrect = q.picked === q.correct_answer; + return ( +
+

+ Question {idx + 1}: {decodeHtml(q.question)} +

+
+ {q.answers.map((answer) => { + const isPicked = q.picked === answer; + const isCorrectAnswer = answer === q.correct_answer; + let className = 'review-answer'; + + if (isCorrectAnswer) { + className += ' result-correct'; + } else if (isPicked) { + className += ' result-wrong'; + } else { + className += ' result-neutral'; + } + + return ( +
+ {decodeHtml(answer)} + {isCorrectAnswer && ' ✓ (Correct Answer)'} + {isPicked && !isCorrectAnswer && ' ✗ (Your Answer)'} +
+ ); + })} +
+

+ {isCorrect ? '✓ You got this right!' : '✗ You got this wrong'} +

+
); })} - -
- ))} - - {/* Review Section */} - {showReview && ( -
-

🎯 Quiz Complete!

-

- Final Score: {score} / {totalQuestions} -

-

Best Score: {highScore}

- +
)}
); -} +} \ No newline at end of file