diff --git a/src/components/CountryTrendChart.jsx b/src/components/CountryTrendChart.jsx index d0cdd0a..73f359d 100644 --- a/src/components/CountryTrendChart.jsx +++ b/src/components/CountryTrendChart.jsx @@ -1,216 +1,216 @@ -import { useEffect, useState, useRef, useMemo, useState as useLocalState } from 'react'; +import { useEffect, useState, useRef, useMemo } from 'react'; import Loading from './Loading.jsx'; import ChartPlaceholder from './ChartPlaceholder.jsx'; import ErrorMessage from './ErrorMessage.jsx'; function SimpleLineChart({ data, width = 600, height = 250 }) { - const [hover, setHover] = useLocalState(null); - if (!data || data.length === 0) return ; - - const padding = 40; - const dates = data.map(d => new Date(d.date)); - const values = data.map(d => d.value); - - const minY = Math.min(...values); - const maxY = Math.max(...values); - const yRange = maxY - minY || 1; - - const x = i => padding + (i / (data.length - 1)) * (width - padding * 2); - const y = v => height - padding - ((v - minY) / yRange) * (height - padding * 2); - - // Smooth path using quadratic curves - const path = data.reduce((acc, d, i, arr) => { - const px = x(i); - const py = y(d.value); - if (i === 0) return `M ${px} ${py}`; - const prevX = x(i - 1); - const prevY = y(arr[i - 1].value); - const midX = (px + prevX) / 2; - const midY = (py + prevY) / 2; - return acc + ` Q ${prevX} ${prevY}, ${midX} ${midY}`; - }, ''); - - const yTicks = 5; - const yStep = yRange / yTicks; - const yLabels = Array.from({ length: yTicks + 1 }, (_, i) => minY + i * yStep); - - return ( - setHover(null)} - > - {/* Grid lines */} - {yLabels.map((v, i) => ( - - ))} - - {/* Y-axis labels */} - {yLabels.map((v, i) => ( - - {Math.round(v).toLocaleString()} - - ))} - - {/* Area fill */} - - - - - - - - - - {/* Data points */} - {data.map((d, i) => { - const cx = x(i); - const cy = y(d.value); - return ( - setHover(i)} - /> - ); - })} - - {/* Tooltip */} - {hover !== null && ( - <> - - - {new Date(data[hover].date).toLocaleDateString()} - - - {data[hover].value.toLocaleString()} - - - )} - - ); + const [hover, setHover] = useState(null); + if (!data || data.length === 0) return ; + + const padding = 40; + const dates = data.map(d => new Date(d.date)); + const values = data.map(d => d.value); + + const minY = Math.min(...values); + const maxY = Math.max(...values); + const yRange = maxY - minY || 1; + + const x = i => padding + (i / (data.length - 1)) * (width - padding * 2); + const y = v => height - padding - ((v - minY) / yRange) * (height - padding * 2); + + // Smooth path using quadratic curves + const path = data.reduce((acc, d, i, arr) => { + const px = x(i); + const py = y(d.value); + if (i === 0) return `M ${px} ${py}`; + const prevX = x(i - 1); + const prevY = y(arr[i - 1].value); + const midX = (px + prevX) / 2; + const midY = (py + prevY) / 2; + return acc + ` Q ${prevX} ${prevY}, ${midX} ${midY}`; + }, ''); + + const yTicks = 5; + const yStep = yRange / yTicks; + const yLabels = Array.from({ length: yTicks + 1 }, (_, i) => minY + i * yStep); + + return ( + setHover(null)} + > + {/* Grid lines */} + {yLabels.map((v, i) => ( + + ))} + + {/* Y-axis labels */} + {yLabels.map((v, i) => ( + + {Math.round(v).toLocaleString()} + + ))} + + {/* Area fill */} + + + + + + + + + + {/* Data points */} + {data.map((d, i) => { + const cx = x(i); + const cy = y(d.value); + return ( + setHover(i)} + /> + ); + })} + + {/* Tooltip */} + {hover !== null && ( + <> + + + {new Date(data[hover].date).toLocaleDateString()} + + + {data[hover].value.toLocaleString()} + + + )} + + ); } export default function CountryTrendChart({ slug }) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [series, setSeries] = useState([]); - const abortRef = useRef(); - const lastSlugRef = useRef(slug); - - useEffect(() => { - if (!slug) { - lastSlugRef.current = slug; - setSeries([]); - setError(null); - setLoading(false); - if (abortRef.current) abortRef.current.abort(); - return; - } - - const controller = new AbortController(); - abortRef.current = controller; - let mounted = true; - - async function load() { - setLoading(true); - setError(null); - setSeries([]); - try { - const res = await fetch( - `https://disease.sh/v3/covid-19/historical/${slug}?lastdays=all`, - { signal: controller.signal } - ); - if (!res.ok) throw new Error('Failed to fetch country trends'); - const json = await res.json(); - - if (!mounted || lastSlugRef.current !== slug) return; - - const cases = json.timeline?.cases || {}; - const seriesData = Object.entries(cases).map(([date, value]) => ({ - date, - value: Number(value || 0), - })); - - seriesData.sort((a, b) => new Date(a.date) - new Date(b.date)); - - setSeries(seriesData); - } catch (e) { - if (e.name === 'AbortError') return; - setError(e); - } finally { - if (mounted) setLoading(false); - } - } - + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [series, setSeries] = useState([]); + const abortRef = useRef(); + const lastSlugRef = useRef(slug); + + useEffect(() => { + if (!slug) { lastSlugRef.current = slug; - load(); - - return () => { - mounted = false; - controller.abort(); - }; - }, [slug]); - - if (!slug) return ; - if (loading && series.length === 0) return ; - if (error) return ; - - return ( -
-

Confirmed Cases — Cumulative

- -
- ); + setSeries([]); + setError(null); + setLoading(false); + if (abortRef.current) abortRef.current.abort(); + return; + } + + const controller = new AbortController(); + abortRef.current = controller; + let mounted = true; + + async function load() { + setLoading(true); + setError(null); + setSeries([]); + try { + const res = await fetch( + `https://disease.sh/v3/covid-19/historical/${slug}?lastdays=all`, + { signal: controller.signal } + ); + if (!res.ok) throw new Error('Failed to fetch country trends'); + const json = await res.json(); + + if (!mounted || lastSlugRef.current !== slug) return; + + const cases = json.timeline?.cases || {}; + const seriesData = Object.entries(cases).map(([date, value]) => ({ + date, + value: Number(value || 0), + })); + + seriesData.sort((a, b) => new Date(a.date) - new Date(b.date)); + + setSeries(seriesData); + } catch (e) { + if (e.name === 'AbortError') return; + setError(e); + } finally { + if (mounted) setLoading(false); + } + } + + lastSlugRef.current = slug; + load(); + + return () => { + mounted = false; + controller.abort(); + }; + }, [slug]); + + if (!slug) return ; + if (loading && series.length === 0) return ; + if (error) return ; + + return ( +
+

Confirmed Cases — Cumulative

+ +
+ ); } diff --git a/src/components/HeroSection.jsx b/src/components/HeroSection.jsx index f1be622..52b95f3 100644 --- a/src/components/HeroSection.jsx +++ b/src/components/HeroSection.jsx @@ -1,21 +1,31 @@ - import React from 'react'; - -const HeroSection = ({ image, title, subtitle }) => { +export default function HeroSection({ image, title, subtitle }) { return (
-
-
-

{title}

- {subtitle &&

{subtitle}

} +
+
+ {image && ( + + )} +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )}
); -}; - -export default HeroSection; +} diff --git a/src/pages/Trivia.jsx b/src/pages/Trivia.jsx index 084a78e..429af08 100644 --- a/src/pages/Trivia.jsx +++ b/src/pages/Trivia.jsx @@ -9,7 +9,7 @@ * Medium: * - [ ] Timer per question + bonus points for speed * - [ ] Show progress bar (questions answered / total) - * - [ ] Local high score persistence + * - [x] Local high score persistence * - [ ] Review mode (see all answers after completion) * Advanced: * - [ ] Question set builder (choose amount, difficulty, category) @@ -32,17 +32,33 @@ export default function Trivia() { const [error, setError] = useState(null); const [score, setScore] = useState(0); const [showReview, setShowReview] = useState(false); + const [highScore, setHighScore] = useState(0); + const [hasSavedForSet, setHasSavedForSet] = useState(false); useEffect(() => { fetchQuestions(); }, [category, difficulty]); + // Load high score 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 + } + }, []); + async function fetchQuestions() { try { setLoading(true); setError(null); setScore(0); setShowReview(false); + setHasSavedForSet(false); const res = await fetch( `https://opentdb.com/api.php?amount=5&category=${category}&difficulty=${difficulty}&type=multiple` @@ -89,195 +105,249 @@ export default function Trivia() { const allAnswered = answeredCount === totalQuestions && totalQuestions > 0; - return ( - <> - - 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)} - -
+ // Persist high score at end of game (once per question set) + useEffect(() => { + if (!allAnswered || hasSavedForSet) return; + if (score > highScore) { + try { + if (typeof window !== 'undefined') + localStorage.setItem('triviaHighScore', String(score)); + } catch (_) { + // ignore storage errors + } + setHighScore(score); + } + setHasSavedForSet(true); + }, [allAnswered, score, highScore, hasSavedForSet]); - {/* Category Selector */} -
- - {/* Difficulty Selector */} - -
+ function resetHighScore() { + try { + if (typeof window !== 'undefined') + localStorage.removeItem('triviaHighScore'); + } catch (_) { + // ignore storage errors + } + setHighScore(0); + } - {/* Loading / Error */} - {loading && } - + return ( + <> + + Think Fast, Learn Faster + + } + subtitle="A trivia playground for curious minds, quick thinkers, and casual know-it-alls" + /> - {/* Progress Bar */} - {totalQuestions > 0 && ( -
-

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

-
+
+

Trivia Quiz

+ + {difficulty.charAt(0).toUpperCase() + difficulty.slice(1)} + +
+ + {/* Category + Difficulty Selector */} +
+ + +
+ + {/* Loading / Error */} + {loading && } + + + {/* Progress Bar */} + {totalQuestions > 0 && ( +
+

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

+ > +
+
-
- )} + )} - {/* Score + Review Button */} -
-

Score: {score}

- -
- - {/* 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'; - } - - return ( -
  • - -
  • - ); - })} -
-
- ))} - - {/* Review Section */} - {showReview && ( -
-

🎯 Quiz Complete!

-

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

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

+
- )} -
+ + {/* 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'; + } + + return ( +
  • + +
  • + ); + })} +
+
+ ))} + + {/* Review Section */} + {showReview && ( +
+

🎯 Quiz Complete!

+

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

+

Best Score: {highScore}

+ +
+ )} + ); } - diff --git a/vite.config.js.timestamp-1761116311330-9bd7f4dd0b574.mjs b/vite.config.js.timestamp-1761116311330-9bd7f4dd0b574.mjs new file mode 100644 index 0000000..a9084ad --- /dev/null +++ b/vite.config.js.timestamp-1761116311330-9bd7f4dd0b574.mjs @@ -0,0 +1,10 @@ +// vite.config.js +import { defineConfig } from "file:///Users/venishakalola/Desktop/hacktoberfest/react-verse/node_modules/vite/dist/node/index.js"; +import react from "file:///Users/venishakalola/Desktop/hacktoberfest/react-verse/node_modules/@vitejs/plugin-react/dist/index.js"; +var vite_config_default = defineConfig({ + plugins: [react()] +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvdmVuaXNoYWthbG9sYS9EZXNrdG9wL2hhY2t0b2JlcmZlc3QvcmVhY3QtdmVyc2VcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9Vc2Vycy92ZW5pc2hha2Fsb2xhL0Rlc2t0b3AvaGFja3RvYmVyZmVzdC9yZWFjdC12ZXJzZS92aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vVXNlcnMvdmVuaXNoYWthbG9sYS9EZXNrdG9wL2hhY2t0b2JlcmZlc3QvcmVhY3QtdmVyc2Uvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJztcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtyZWFjdCgpXSxcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFvVixTQUFTLG9CQUFvQjtBQUNqWCxPQUFPLFdBQVc7QUFFbEIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sQ0FBQztBQUNuQixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=