99 * Medium:
1010 * - [ ] Timer per question + bonus points for speed
1111 * - [ ] Show progress bar (questions answered / total)
12- * - [ ] Local high score persistence
12+ * - [x ] Local high score persistence
1313 * - [ ] Review mode (see all answers after completion)
1414 * Advanced:
1515 * - [ ] Question set builder (choose amount, difficulty, category)
@@ -21,69 +21,312 @@ import { useEffect, useState } from 'react';
2121import Loading from '../components/Loading.jsx' ;
2222import ErrorMessage from '../components/ErrorMessage.jsx' ;
2323import Card from '../components/Card.jsx' ;
24+ import HeroSection from '../components/HeroSection' ;
25+ import Quiz from '../Images/Quiz.jpg' ;
2426
2527export default function Trivia ( ) {
2628 const [ questions , setQuestions ] = useState ( [ ] ) ;
2729 const [ category , setCategory ] = useState ( '18' ) ; // Science: Computers
30+ const [ difficulty , setDifficulty ] = useState ( 'easy' ) ; // Default to easy
2831 const [ loading , setLoading ] = useState ( false ) ;
2932 const [ error , setError ] = useState ( null ) ;
3033 const [ score , setScore ] = useState ( 0 ) ;
34+ const [ showReview , setShowReview ] = useState ( false ) ;
35+ const [ highScore , setHighScore ] = useState ( 0 ) ;
36+ const [ hasSavedForSet , setHasSavedForSet ] = useState ( false ) ;
3137
32- useEffect ( ( ) => { fetchQuestions ( ) ; } , [ category ] ) ;
38+ useEffect ( ( ) => {
39+ fetchQuestions ( ) ;
40+ } , [ category , difficulty ] ) ;
41+
42+ // Load high score once on mount
43+ useEffect ( ( ) => {
44+ try {
45+ const stored = typeof window !== 'undefined' ? localStorage . getItem ( 'triviaHighScore' ) : null ;
46+ if ( stored !== null ) setHighScore ( parseInt ( stored , 10 ) || 0 ) ;
47+ } catch ( _ ) {
48+ // ignore storage errors
49+ }
50+ } , [ ] ) ;
3351
3452 async function fetchQuestions ( ) {
3553 try {
36- setLoading ( true ) ; setError ( null ) ; setScore ( 0 ) ;
37- const res = await fetch ( `https://opentdb.com/api.php?amount=5&category=${ category } &type=multiple` ) ;
54+ setLoading ( true ) ;
55+ setError ( null ) ;
56+ setScore ( 0 ) ;
57+ setShowReview ( false ) ;
58+ setHasSavedForSet ( false ) ;
59+
60+ const res = await fetch (
61+ `https://opentdb.com/api.php?amount=5&category=${ category } &difficulty=${ difficulty } &type=multiple`
62+ ) ;
3863 if ( ! res . ok ) throw new Error ( 'Failed to fetch' ) ;
3964 const json = await res . json ( ) ;
40- const qs = json . results . map ( q => ( {
65+
66+ const qs = json . results . map ( ( q ) => ( {
4167 ...q ,
4268 answers : shuffle ( [ q . correct_answer , ...q . incorrect_answers ] ) ,
43- picked : null
69+ picked : null ,
4470 } ) ) ;
4571 setQuestions ( qs ) ;
46- } catch ( e ) { setError ( e ) ; } finally { setLoading ( false ) ; }
72+ } catch ( e ) {
73+ setError ( e ) ;
74+ } finally {
75+ setLoading ( false ) ;
76+ }
4777 }
4878
49- function shuffle ( arr ) { return arr . sort ( ( ) => Math . random ( ) - 0.5 ) ; }
79+ function shuffle ( arr ) {
80+ return arr . sort ( ( ) => Math . random ( ) - 0.5 ) ;
81+ }
5082
5183 function pick ( qIndex , answer ) {
52- setQuestions ( qs => qs . map ( ( q , i ) => i === qIndex ? { ...q , picked : answer } : q ) ) ;
53- if ( questions [ qIndex ] . correct_answer === answer ) setScore ( s => s + 1 ) ;
84+ setQuestions ( ( qs ) =>
85+ qs . map ( ( q , i ) => ( i === qIndex ? { ...q , picked : answer } : q ) )
86+ ) ;
87+ if ( questions [ qIndex ] . correct_answer === answer ) {
88+ setScore ( ( s ) => s + 1 ) ;
89+ }
90+ }
91+
92+ function decodeHtml ( html ) {
93+ const txt = document . createElement ( 'textarea' ) ;
94+ txt . innerHTML = html ;
95+ return txt . value ;
96+ }
97+
98+ const answeredCount = questions . filter ( ( q ) => q . picked !== null ) . length ;
99+ const totalQuestions = questions . length ;
100+ const progressPercent =
101+ totalQuestions > 0 ? ( answeredCount / totalQuestions ) * 100 : 0 ;
102+
103+ const allAnswered = answeredCount === totalQuestions && totalQuestions > 0 ;
104+
105+ // Persist high score at end of game (once per question set)
106+ useEffect ( ( ) => {
107+ if ( ! allAnswered || hasSavedForSet ) return ;
108+ if ( score > highScore ) {
109+ try {
110+ if ( typeof window !== 'undefined' ) localStorage . setItem ( 'triviaHighScore' , String ( score ) ) ;
111+ } catch ( _ ) {
112+ // ignore storage errors
113+ }
114+ setHighScore ( score ) ;
115+ }
116+ setHasSavedForSet ( true ) ;
117+ } , [ allAnswered , score , highScore , hasSavedForSet ] ) ;
118+
119+ function resetHighScore ( ) {
120+ try {
121+ if ( typeof window !== 'undefined' ) localStorage . removeItem ( 'triviaHighScore' ) ;
122+ } catch ( _ ) {
123+ // ignore storage errors
124+ }
125+ setHighScore ( 0 ) ;
54126 }
55127
56128 return (
57- < div >
58- < h2 > Trivia Quiz</ h2 >
59- < label > Category:
60- < select value = { category } onChange = { e => setCategory ( e . target . value ) } >
61- < option value = "18" > Science: Computers</ option >
62- < option value = "21" > Sports</ option >
63- < option value = "23" > History</ option >
64- </ select >
65- </ label >
129+ < >
130+ < HeroSection
131+ image = { Quiz }
132+ title = {
133+ < >
134+ Think Fast, < span style = { { color : 'grey' } } > Learn Faster</ span >
135+ </ >
136+ }
137+ subtitle = "A trivia playground for curious minds, quick thinkers, and casual know-it-alls"
138+ />
139+ < div style = { { padding : '20px' } } >
140+ < div style = { { display : 'flex' , alignItems : 'center' , gap : '10px' , marginBottom : '20px' } } >
141+ < h2 style = { { margin : 0 } } > Trivia Quiz</ h2 >
142+ < span style = { {
143+ backgroundColor : difficulty === 'easy' ? '#4caf50' : difficulty === 'medium' ? '#ff9800' : '#f44336' ,
144+ color : 'white' ,
145+ padding : '4px 8px' ,
146+ borderRadius : '4px' ,
147+ fontSize : '0.9em'
148+ } } >
149+ { difficulty . charAt ( 0 ) . toUpperCase ( ) + difficulty . slice ( 1 ) }
150+ </ span >
151+ </ div >
152+
153+ { /* Category Selector */ }
154+ < div style = { { display : 'flex' , gap : '20px' , marginBottom : '20px' } } >
155+ < label >
156+ Category:{ ' ' }
157+ < select
158+ value = { category }
159+ onChange = { ( e ) => setCategory ( e . target . value ) }
160+ disabled = { showReview }
161+ >
162+ < option value = "18" > Science: Computers</ option >
163+ < option value = "21" > Sports</ option >
164+ < option value = "23" > History</ option >
165+ </ select >
166+ </ label >
167+ { /* Difficulty Selector */ }
168+ < label >
169+ Difficulty:{ ' ' }
170+ < select
171+ value = { difficulty }
172+ onChange = { ( e ) => setDifficulty ( e . target . value ) }
173+ disabled = { showReview }
174+ >
175+ < option value = "easy" > Easy</ option >
176+ < option value = "medium" > Medium</ option >
177+ < option value = "hard" > Hard</ option >
178+ </ select >
179+ </ label >
180+ </ div >
181+
182+ { /* Loading / Error */ }
66183 { loading && < Loading /> }
67184 < ErrorMessage error = { error } />
68- < p > Score: { score } </ p >
185+
186+ { /* Progress Bar */ }
187+ { totalQuestions > 0 && (
188+ < div style = { { margin : '15px 0' } } >
189+ < p >
190+ Progress: { answeredCount } / { totalQuestions } answered
191+ </ p >
192+ < div
193+ style = { {
194+ height : '10px' ,
195+ width : '100%' ,
196+ background : '#ddd' ,
197+ borderRadius : '8px' ,
198+ overflow : 'hidden' ,
199+ } }
200+ >
201+ < div
202+ style = { {
203+ width : `${ progressPercent } %` ,
204+ height : '100%' ,
205+ background : '#4caf50' ,
206+ transition : 'width 0.3s ease' ,
207+ } }
208+ > </ div >
209+ </ div >
210+ </ div >
211+ ) }
212+
213+ { /* Score + Review Button */ }
214+ < div
215+ style = { {
216+ display : 'flex' ,
217+ alignItems : 'center' ,
218+ gap : '10px' ,
219+ marginBottom : '15px' ,
220+ } }
221+ >
222+ < p style = { { margin : 0 , fontWeight : 'bold' } } > Score: { score } | Best: { highScore } </ p >
223+ < button
224+ onClick = { resetHighScore }
225+ style = { {
226+ background : '#6c757d' ,
227+ color : '#fff' ,
228+ padding : '6px 10px' ,
229+ border : 'none' ,
230+ borderRadius : '6px' ,
231+ cursor : 'pointer' ,
232+ } }
233+ >
234+ Reset High Score
235+ </ button >
236+ < button
237+ onClick = { ( ) => setShowReview ( true ) }
238+ disabled = { ! allAnswered || showReview }
239+ style = { {
240+ background : allAnswered ? '#007bff' : '#ccc' ,
241+ color : '#fff' ,
242+ padding : '6px 12px' ,
243+ border : 'none' ,
244+ borderRadius : '6px' ,
245+ cursor : allAnswered && ! showReview ? 'pointer' : 'not-allowed' ,
246+ } }
247+ >
248+ Review Answers
249+ </ button >
250+ </ div >
251+
252+ { /* Quiz Cards */ }
69253 { questions . map ( ( q , idx ) => (
70- < Card key = { idx } title = { `Q${ idx + 1 } : ${ decodeHtml ( q . question ) } ` } >
254+ < Card key = { idx } title = { `Q${ idx + 1 } : ${ decodeHtml ( q . question ) } ` } >
71255 < ul >
72- { q . answers . map ( a => (
73- < li key = { a } >
74- < button disabled = { q . picked } className = { a === q . correct_answer ? ( q . picked && a === q . picked ? 'correct' : '' ) : ( q . picked === a ? 'wrong' :'' ) } onClick = { ( ) => pick ( idx , a ) } > { decodeHtml ( a ) } </ button >
75- </ li >
76- ) ) }
256+ { q . answers . map ( ( a ) => {
257+ const isPicked = q . picked === a ;
258+ const isCorrect = a === q . correct_answer ;
259+ let btnClass = '' ;
260+
261+ if ( showReview ) {
262+ btnClass = isCorrect
263+ ? 'correct'
264+ : isPicked
265+ ? 'wrong'
266+ : 'neutral' ;
267+ } else if ( isPicked ) {
268+ btnClass = isCorrect ? 'correct' : 'wrong' ;
269+ }
270+
271+ return (
272+ < li key = { a } >
273+ < button
274+ disabled = { ! ! q . picked || showReview }
275+ onClick = { ( ) => pick ( idx , a ) }
276+ style = { {
277+ margin : '5px' ,
278+ padding : '8px 12px' ,
279+ borderRadius : '6px' ,
280+ cursor : q . picked || showReview ? 'default' : 'pointer' ,
281+ border :
282+ btnClass === 'correct'
283+ ? '2px solid green'
284+ : btnClass === 'wrong'
285+ ? '2px solid red'
286+ : '1px solid #ccc' ,
287+ background :
288+ btnClass === 'correct'
289+ ? '#c8e6c9'
290+ : btnClass === 'wrong'
291+ ? '#ffcdd2'
292+ : '#fff' ,
293+ color : 'black' , // Always black text
294+ fontWeight : '500' ,
295+ } }
296+ >
297+ { decodeHtml ( a ) }
298+ </ button >
299+ </ li >
300+ ) ;
301+ } ) }
77302 </ ul >
78303 </ Card >
79304 ) ) }
80- { /* TODO: Add scoreboard persistence */ }
305+
306+ { /* Review Section */ }
307+ { showReview && (
308+ < div style = { { marginTop : '20px' , textAlign : 'center' } } >
309+ < h3 > 🎯 Quiz Complete!</ h3 >
310+ < p >
311+ Final Score: { score } / { totalQuestions }
312+ </ p >
313+ < p > Best Score: { highScore } </ p >
314+ < button
315+ onClick = { fetchQuestions }
316+ style = { {
317+ background : '#007bff' ,
318+ color : '#fff' ,
319+ padding : '10px 16px' ,
320+ border : 'none' ,
321+ borderRadius : '8px' ,
322+ cursor : 'pointer' ,
323+ } }
324+ >
325+ Play Again
326+ </ button >
327+ </ div >
328+ ) }
81329 </ div >
330+ </ >
82331 ) ;
83- }
84-
85- function decodeHtml ( html ) {
86- const txt = document . createElement ( 'textarea' ) ;
87- txt . innerHTML = html ;
88- return txt . value ;
89- }
332+ }
0 commit comments