Skip to content

Commit 0dfe644

Browse files
local high score persistence in Trivia
1 parent 58acabb commit 0dfe644

File tree

4 files changed

+311
-34
lines changed

4 files changed

+311
-34
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import ChartPlaceholder from './ChartPlaceholder.jsx';
2+
3+
export default function CountryTrendChart({ slug }) {
4+
return <ChartPlaceholder label={`Country Trend for ${slug || 'N/A'} (legacy)`} />;
5+
}

src/components/HeroSection.jsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export default function HeroSection({ image, title, subtitle }) {
2+
return (
3+
<div className="relative overflow-hidden rounded-xl mb-6 bg-gradient-to-r from-indigo-50 to-blue-50">
4+
<div className="max-w-7xl mx-auto px-4 py-10 flex items-center gap-6">
5+
{image ? (
6+
<img
7+
src={image}
8+
alt=""
9+
className="w-24 h-24 object-cover rounded-lg shadow-md hidden sm:block"
10+
/>
11+
) : null}
12+
<div>
13+
<h1 className="text-3xl md:text-4xl font-extrabold text-gray-800">{title}</h1>
14+
{subtitle && <p className="text-gray-600 mt-2">{subtitle}</p>}
15+
</div>
16+
</div>
17+
</div>
18+
);
19+
}

src/pages/Trivia.jsx

Lines changed: 277 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
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';
2121
import Loading from '../components/Loading.jsx';
2222
import ErrorMessage from '../components/ErrorMessage.jsx';
2323
import Card from '../components/Card.jsx';
24+
import HeroSection from '../components/HeroSection';
25+
import Quiz from '../Images/Quiz.jpg';
2426

2527
export 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+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// vite.config.js
2+
import { defineConfig } from "file:///Users/venishakalola/Desktop/hacktoberfest/react-verse/node_modules/vite/dist/node/index.js";
3+
import react from "file:///Users/venishakalola/Desktop/hacktoberfest/react-verse/node_modules/@vitejs/plugin-react/dist/index.js";
4+
var vite_config_default = defineConfig({
5+
plugins: [react()]
6+
});
7+
export {
8+
vite_config_default as default
9+
};
10+
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvdmVuaXNoYWthbG9sYS9EZXNrdG9wL2hhY2t0b2JlcmZlc3QvcmVhY3QtdmVyc2VcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy92ZW5pc2hha2Fsb2xhL0Rlc2t0b3AvaGFja3RvYmVyZmVzdC9yZWFjdC12ZXJzZS92aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvdmVuaXNoYWthbG9sYS9EZXNrdG9wL2hhY2t0b2JlcmZlc3QvcmVhY3QtdmVyc2Uvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJztcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtyZWFjdCgpXSxcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFvVixTQUFTLG9CQUFvQjtBQUNqWCxPQUFPLFdBQVc7QUFFbEIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sQ0FBQztBQUNuQixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=

0 commit comments

Comments
 (0)