diff --git a/src/components/CountryTrendChart.jsx b/src/components/CountryTrendChart.jsx new file mode 100644 index 0000000..d0cdd0a --- /dev/null +++ b/src/components/CountryTrendChart.jsx @@ -0,0 +1,216 @@ +import { useEffect, useState, useRef, useMemo, useState as useLocalState } 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()} + + + )} + + ); +} + +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); + } + } + + 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/pages/Covid.jsx b/src/pages/Covid.jsx index 6155022..f4959d4 100644 --- a/src/pages/Covid.jsx +++ b/src/pages/Covid.jsx @@ -17,72 +17,99 @@ * - [ ] Offline cache last fetch * - [ ] Extract service + hook (useCovidSummary, useCountryTrends) */ -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useRef } from 'react'; import Loading from '../components/Loading.jsx'; import ErrorMessage from '../components/ErrorMessage.jsx'; import Card from '../components/Card.jsx'; +import CountryTrendChart from '../components/CountryTrendChart.jsx'; export default function Covid() { const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [country, setCountry] = useState(''); + const isFetchingRef = useRef(false); const fetchSummary = useCallback(async () => { - if (loading) return; + if (isFetchingRef.current) return; + isFetchingRef.current = true; try { - setLoading(true); + setLoading(true); setError(null); - const res = await fetch('https://api.covid19api.com/summary'); - if (!res.ok) throw new Error('Failed to fetch'); + const res = await fetch('https://disease.sh/v3/covid-19/countries'); + if (!res.ok) throw new Error('Failed to fetch country data'); + const json = await res.json(); setSummary(json); - } catch (e) { - setError(e); - } finally { - setLoading(false); + } catch (e) { + setError(e); + } finally { + setLoading(false); + isFetchingRef.current = false; } - }, [loading]); + }, []); useEffect(() => { fetchSummary(); - }, []); + }, [fetchSummary]); - const global = summary?.Global; - const countries = summary?.Countries || []; - const selected = countries.find(c => c.Slug === country); + const countries = summary || []; + const selected = countries.find(c => c.countryInfo.iso3 === country || c.country === country); + + // Compute simple global summary (sum across all countries) + const global = countries.reduce( + (acc, c) => { + acc.cases += c.cases; + acc.todayCases += c.todayCases; + acc.deaths += c.deaths; + acc.todayDeaths += c.todayDeaths; + return acc; + }, + { cases: 0, todayCases: 0, deaths: 0, todayDeaths: 0 } + ); return (
-

COVID-19 Tracker

- -
+

COVID-19 Tracker

+ +
+ {loading && !summary && } - {global && ( + + {countries.length > 0 && ( -

New Confirmed: {global.NewConfirmed.toLocaleString()}

-

Total Confirmed: {global.TotalConfirmed.toLocaleString()}

-

Total Deaths: {global.TotalDeaths.toLocaleString()}

+

New Confirmed: {global.todayCases.toLocaleString()}

+

Total Confirmed: {global.cases.toLocaleString()}

+

Total Deaths: {global.deaths.toLocaleString()}

)} -