Skip to content

Commit 4c8d102

Browse files
Merge pull request #77 from Divayang-2006/main
Scope<COVID>: Add CountryTrendChart and integrate into Covid page
2 parents 4d28892 + 070fa62 commit 4c8d102

File tree

2 files changed

+273
-30
lines changed

2 files changed

+273
-30
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { useEffect, useState, useRef, useMemo, useState as useLocalState } from 'react';
2+
import Loading from './Loading.jsx';
3+
import ChartPlaceholder from './ChartPlaceholder.jsx';
4+
import ErrorMessage from './ErrorMessage.jsx';
5+
6+
function SimpleLineChart({ data, width = 600, height = 250 }) {
7+
const [hover, setHover] = useLocalState(null);
8+
if (!data || data.length === 0) return <ChartPlaceholder label="No data" />;
9+
10+
const padding = 40;
11+
const dates = data.map(d => new Date(d.date));
12+
const values = data.map(d => d.value);
13+
14+
const minY = Math.min(...values);
15+
const maxY = Math.max(...values);
16+
const yRange = maxY - minY || 1;
17+
18+
const x = i => padding + (i / (data.length - 1)) * (width - padding * 2);
19+
const y = v => height - padding - ((v - minY) / yRange) * (height - padding * 2);
20+
21+
// Smooth path using quadratic curves
22+
const path = data.reduce((acc, d, i, arr) => {
23+
const px = x(i);
24+
const py = y(d.value);
25+
if (i === 0) return `M ${px} ${py}`;
26+
const prevX = x(i - 1);
27+
const prevY = y(arr[i - 1].value);
28+
const midX = (px + prevX) / 2;
29+
const midY = (py + prevY) / 2;
30+
return acc + ` Q ${prevX} ${prevY}, ${midX} ${midY}`;
31+
}, '');
32+
33+
const yTicks = 5;
34+
const yStep = yRange / yTicks;
35+
const yLabels = Array.from({ length: yTicks + 1 }, (_, i) => minY + i * yStep);
36+
37+
return (
38+
<svg
39+
width="100%"
40+
viewBox={`0 0 ${width} ${height}`}
41+
preserveAspectRatio="none"
42+
style={{ background: 'transparent', borderRadius: 6 }}// give it trasparent background
43+
onMouseLeave={() => setHover(null)}
44+
>
45+
{/* Grid lines */}
46+
{yLabels.map((v, i) => (
47+
<line
48+
key={i}
49+
x1={padding + 10}
50+
x2={width - padding}
51+
y1={y(v)}
52+
y2={y(v)}
53+
stroke="#e5e7eb"
54+
strokeWidth="1"
55+
/>
56+
))}
57+
58+
{/* Y-axis labels */}
59+
{yLabels.map((v, i) => (
60+
<text
61+
key={i}
62+
x={padding + 8}
63+
y={y(v) + 4}
64+
textAnchor="end"
65+
fontSize="10"
66+
fill="#6b7280"
67+
>
68+
{Math.round(v).toLocaleString()}
69+
</text>
70+
))}
71+
72+
{/* Area fill */}
73+
<defs>
74+
<linearGradient id="g" x1="0" x2="0" y1="0" y2="1">
75+
<stop offset="0%" stopColor="#3b82f6" stopOpacity="0.3" />
76+
<stop offset="100%" stopColor="#3b82f6" stopOpacity="0.05" />
77+
</linearGradient>
78+
</defs>
79+
<path
80+
d={`${path} L ${x(data.length - 1)} ${height - padding} L ${x(0)} ${height - padding} Z`}
81+
fill="url(#g)"
82+
/>
83+
<path
84+
d={path}
85+
fill="none"
86+
stroke="#2563eb"
87+
strokeWidth="2.5"
88+
strokeLinejoin="round"
89+
strokeLinecap="round"
90+
/>
91+
92+
{/* Data points */}
93+
{data.map((d, i) => {
94+
const cx = x(i);
95+
const cy = y(d.value);
96+
return (
97+
<circle
98+
key={d.date}
99+
cx={cx}
100+
cy={cy}
101+
r={hover === i ? 4 : 2.5}
102+
fill={hover === i ? '#1e40af' : '#1d4ed8'}
103+
onMouseEnter={() => setHover(i)}
104+
/>
105+
);
106+
})}
107+
108+
{/* Tooltip */}
109+
{hover !== null && (
110+
<>
111+
<rect
112+
x={x(hover) - 45}
113+
y={y(data[hover].value) - 45}
114+
width="90"
115+
height="32"
116+
rx="4"
117+
fill="#111827"
118+
opacity="0.85"
119+
/>
120+
<text
121+
x={x(hover)}
122+
y={y(data[hover].value) - 28}
123+
textAnchor="middle"
124+
fill="white"
125+
fontSize="10"
126+
>
127+
{new Date(data[hover].date).toLocaleDateString()}
128+
</text>
129+
<text
130+
x={x(hover)}
131+
y={y(data[hover].value) - 15}
132+
textAnchor="middle"
133+
fill="#a5b4fc"
134+
fontSize="11"
135+
fontWeight="bold"
136+
>
137+
{data[hover].value.toLocaleString()}
138+
</text>
139+
</>
140+
)}
141+
</svg>
142+
);
143+
}
144+
145+
export default function CountryTrendChart({ slug }) {
146+
const [loading, setLoading] = useState(false);
147+
const [error, setError] = useState(null);
148+
const [series, setSeries] = useState([]);
149+
const abortRef = useRef();
150+
const lastSlugRef = useRef(slug);
151+
152+
useEffect(() => {
153+
if (!slug) {
154+
lastSlugRef.current = slug;
155+
setSeries([]);
156+
setError(null);
157+
setLoading(false);
158+
if (abortRef.current) abortRef.current.abort();
159+
return;
160+
}
161+
162+
const controller = new AbortController();
163+
abortRef.current = controller;
164+
let mounted = true;
165+
166+
async function load() {
167+
setLoading(true);
168+
setError(null);
169+
setSeries([]);
170+
try {
171+
const res = await fetch(
172+
`https://disease.sh/v3/covid-19/historical/${slug}?lastdays=all`,
173+
{ signal: controller.signal }
174+
);
175+
if (!res.ok) throw new Error('Failed to fetch country trends');
176+
const json = await res.json();
177+
178+
if (!mounted || lastSlugRef.current !== slug) return;
179+
180+
const cases = json.timeline?.cases || {};
181+
const seriesData = Object.entries(cases).map(([date, value]) => ({
182+
date,
183+
value: Number(value || 0),
184+
}));
185+
186+
seriesData.sort((a, b) => new Date(a.date) - new Date(b.date));
187+
188+
setSeries(seriesData);
189+
} catch (e) {
190+
if (e.name === 'AbortError') return;
191+
setError(e);
192+
} finally {
193+
if (mounted) setLoading(false);
194+
}
195+
}
196+
197+
lastSlugRef.current = slug;
198+
load();
199+
200+
return () => {
201+
mounted = false;
202+
controller.abort();
203+
};
204+
}, [slug]);
205+
206+
if (!slug) return <ChartPlaceholder label="Select a country to view trends" />;
207+
if (loading && series.length === 0) return <Loading />;
208+
if (error) return <ErrorMessage error={error} />;
209+
210+
return (
211+
<div className="country-trend-chart">
212+
<h3>Confirmed Cases — Cumulative</h3>
213+
<SimpleLineChart data={series} />
214+
</div>
215+
);
216+
}

src/pages/Covid.jsx

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,72 +17,99 @@
1717
* - [ ] Offline cache last fetch
1818
* - [ ] Extract service + hook (useCovidSummary, useCountryTrends)
1919
*/
20-
import { useEffect, useState, useCallback } from 'react';
20+
import { useEffect, useState, useCallback, useRef } 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 CountryTrendChart from '../components/CountryTrendChart.jsx';
2425

2526
export default function Covid() {
2627
const [summary, setSummary] = useState(null);
2728
const [loading, setLoading] = useState(false);
2829
const [error, setError] = useState(null);
2930
const [country, setCountry] = useState('');
31+
const isFetchingRef = useRef(false);
3032

3133
const fetchSummary = useCallback(async () => {
32-
if (loading) return;
34+
if (isFetchingRef.current) return;
35+
isFetchingRef.current = true;
3336
try {
34-
setLoading(true);
37+
setLoading(true);
3538
setError(null);
36-
const res = await fetch('https://api.covid19api.com/summary');
37-
if (!res.ok) throw new Error('Failed to fetch');
39+
const res = await fetch('https://disease.sh/v3/covid-19/countries');
40+
if (!res.ok) throw new Error('Failed to fetch country data');
41+
3842
const json = await res.json();
3943
setSummary(json);
40-
} catch (e) {
41-
setError(e);
42-
} finally {
43-
setLoading(false);
44+
} catch (e) {
45+
setError(e);
46+
} finally {
47+
setLoading(false);
48+
isFetchingRef.current = false;
4449
}
45-
}, [loading]);
50+
}, []);
4651

4752
useEffect(() => {
4853
fetchSummary();
49-
}, []);
54+
}, [fetchSummary]);
5055

51-
const global = summary?.Global;
52-
const countries = summary?.Countries || [];
53-
const selected = countries.find(c => c.Slug === country);
56+
const countries = summary || [];
57+
const selected = countries.find(c => c.countryInfo.iso3 === country || c.country === country);
58+
59+
// Compute simple global summary (sum across all countries)
60+
const global = countries.reduce(
61+
(acc, c) => {
62+
acc.cases += c.cases;
63+
acc.todayCases += c.todayCases;
64+
acc.deaths += c.deaths;
65+
acc.todayDeaths += c.todayDeaths;
66+
return acc;
67+
},
68+
{ cases: 0, todayCases: 0, deaths: 0, todayDeaths: 0 }
69+
);
5470

5571
return (
5672
<div>
5773
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
58-
<h2>COVID-19 Tracker</h2>
59-
<button onClick={fetchSummary} disabled={loading}>
60-
{loading ? 'Refreshing...' : 'Refresh'}
61-
</button>
62-
</div>
74+
<h2>COVID-19 Tracker</h2>
75+
<button onClick={fetchSummary} disabled={loading}>
76+
{loading ? 'Refreshing...' : 'Refresh'}
77+
</button>
78+
</div>
79+
6380
{loading && !summary && <Loading />}
6481
<ErrorMessage error={error} />
65-
{global && (
82+
83+
{countries.length > 0 && (
6684
<Card title="Global Stats">
67-
<p>New Confirmed: {global.NewConfirmed.toLocaleString()}</p>
68-
<p>Total Confirmed: {global.TotalConfirmed.toLocaleString()}</p>
69-
<p>Total Deaths: {global.TotalDeaths.toLocaleString()}</p>
85+
<p>New Confirmed: {global.todayCases.toLocaleString()}</p>
86+
<p>Total Confirmed: {global.cases.toLocaleString()}</p>
87+
<p>Total Deaths: {global.deaths.toLocaleString()}</p>
7088
</Card>
7189
)}
72-
<label>Select Country:
90+
91+
<label>
92+
Select Country:{' '}
7393
<select value={country} onChange={e => setCountry(e.target.value)}>
7494
<option value="">--</option>
75-
{countries.map(c => <option key={c.Slug} value={c.Slug}>{c.Country}</option>)}
95+
{countries.map(c => (
96+
<option key={c.countryInfo._id || c.country} value={c.country}>
97+
{c.country}
98+
</option>
99+
))}
76100
</select>
77101
</label>
102+
78103
{selected && (
79-
<Card title={selected.Country}>
80-
<p>New Confirmed: {selected.NewConfirmed.toLocaleString()}</p>
81-
<p>Total Confirmed: {selected.TotalConfirmed.toLocaleString()}</p>
82-
<p>Total Deaths: {selected.TotalDeaths.toLocaleString()}</p>
104+
<Card title={selected.country}>
105+
<p>New Confirmed: {selected.todayCases.toLocaleString()}</p>
106+
<p>Total Confirmed: {selected.cases.toLocaleString()}</p>
107+
<p>Total Deaths: {selected.deaths.toLocaleString()}</p>
108+
<p>Updated: {new Date(selected.updated).toLocaleString()}</p>
83109
</Card>
84110
)}
85-
{/* TODO: Add daily trends chart using ChartPlaceholder */}
111+
112+
<CountryTrendChart slug={country} />
86113
</div>
87114
);
88115
}

0 commit comments

Comments
 (0)