Skip to content

Commit 7b927be

Browse files
Improved UI of Covid Page
1 parent 58acabb commit 7b927be

1 file changed

Lines changed: 295 additions & 60 deletions

File tree

src/pages/Covid.jsx

Lines changed: 295 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,310 @@
1-
/**
2-
* COVID-19 DASHBOARD TODOs
3-
* ------------------------
4-
* Easy:
5-
* - [ ] Show date of summary data (API provides Date field)
6-
* - [ ] Format numbers with abbreviation utility
7-
* - [ ] Add refresh button
8-
* - [ ] Add note/link about data source & potential delays
9-
* Medium:
10-
* - [ ] Country search input (filter dropdown)
11-
* - [ ] Sort countries (Total Confirmed, New Confirmed, Deaths)
12-
* - [ ] Persist last selected country
13-
* - [ ] Basic trend chart (cases over time) for selected country
14-
* Advanced:
15-
* - [ ] Multi-country comparison chart
16-
* - [ ] Data normalization per million population (needs population API)
17-
* - [ ] Offline cache last fetch
18-
* - [ ] Extract service + hook (useCovidSummary, useCountryTrends)
19-
*/
20-
import { useEffect, useState } from 'react';
21-
import Loading from '../components/Loading.jsx';
22-
import ErrorMessage from '../components/ErrorMessage.jsx';
23-
import Card from '../components/Card.jsx';
1+
import { useEffect, useState, useCallback, useRef } from 'react';
2+
import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
3+
import { Activity, Users, AlertCircle, TrendingUp, Globe, RefreshCw, Calendar } from 'lucide-react';
244

255
export default function Covid() {
266
const [summary, setSummary] = useState(null);
7+
const [historical, setHistorical] = useState(null);
278
const [loading, setLoading] = useState(false);
289
const [error, setError] = useState(null);
2910
const [country, setCountry] = useState('');
11+
const [searchTerm, setSearchTerm] = useState('');
12+
const isFetchingRef = useRef(false);
3013

31-
useEffect(() => { fetchSummary(); }, []);
32-
33-
async function fetchSummary() {
14+
const fetchSummary = useCallback(async () => {
15+
if (isFetchingRef.current) return;
16+
isFetchingRef.current = true;
3417
try {
35-
setLoading(true); setError(null);
36-
const res = await fetch('https://api.covid19api.com/summary');
37-
if (!res.ok) throw new Error('Failed to fetch');
18+
setLoading(true);
19+
setError(null);
20+
const res = await fetch('https://disease.sh/v3/covid-19/countries');
21+
if (!res.ok) throw new Error('Failed to fetch country data');
3822
const json = await res.json();
3923
setSummary(json);
40-
} catch (e) { setError(e); } finally { setLoading(false); }
41-
}
24+
} catch (e) {
25+
setError(e.message);
26+
} finally {
27+
setLoading(false);
28+
isFetchingRef.current = false;
29+
}
30+
}, []);
31+
32+
const fetchHistorical = useCallback(async (countryName) => {
33+
if (!countryName) return;
34+
try {
35+
const res = await fetch(`https://disease.sh/v3/covid-19/historical/${countryName}?lastdays=30`);
36+
if (!res.ok) throw new Error('Failed to fetch historical data');
37+
const json = await res.json();
38+
setHistorical(json);
39+
} catch (e) {
40+
console.error('Historical data error:', e);
41+
}
42+
}, []);
43+
44+
useEffect(() => {
45+
fetchSummary();
46+
}, [fetchSummary]);
47+
48+
useEffect(() => {
49+
if (country) {
50+
fetchHistorical(country);
51+
}
52+
}, [country, fetchHistorical]);
53+
54+
const countries = summary || [];
55+
const filteredCountries = countries.filter(c =>
56+
c.country.toLowerCase().includes(searchTerm.toLowerCase())
57+
);
58+
const selected = countries.find(c => c.country === country);
59+
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+
acc.recovered += c.recovered;
67+
acc.active += c.active;
68+
return acc;
69+
},
70+
{ cases: 0, todayCases: 0, deaths: 0, todayDeaths: 0, recovered: 0, active: 0 }
71+
);
72+
73+
const topCountries = [...countries]
74+
.sort((a, b) => b.cases - a.cases)
75+
.slice(0, 10)
76+
.map(c => ({
77+
name: c.country,
78+
cases: c.cases,
79+
deaths: c.deaths,
80+
recovered: c.recovered
81+
}));
82+
83+
const globalPieData = [
84+
{ name: 'Active', value: global.active, color: '#f59e0b' },
85+
{ name: 'Recovered', value: global.recovered, color: '#10b981' },
86+
{ name: 'Deaths', value: global.deaths, color: '#ef4444' }
87+
];
88+
89+
const historicalChartData = historical?.timeline ?
90+
Object.keys(historical.timeline.cases).map(date => ({
91+
date: new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
92+
cases: historical.timeline.cases[date],
93+
deaths: historical.timeline.deaths[date],
94+
recovered: historical.timeline.recovered?.[date] || 0
95+
})) : [];
4296

43-
const global = summary?.Global;
44-
const countries = summary?.Countries || [];
45-
const selected = countries.find(c => c.Slug === country);
97+
const formatNumber = (num) => {
98+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
99+
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
100+
return num.toLocaleString();
101+
};
46102

47103
return (
48-
<div>
49-
<h2>COVID-19 Tracker</h2>
50-
{loading && <Loading />}
51-
<ErrorMessage error={error} />
52-
{global && (
53-
<Card title="Global Stats">
54-
<p>New Confirmed: {global.NewConfirmed.toLocaleString()}</p>
55-
<p>Total Confirmed: {global.TotalConfirmed.toLocaleString()}</p>
56-
<p>Total Deaths: {global.TotalDeaths.toLocaleString()}</p>
57-
</Card>
58-
)}
59-
<label>Select Country:
60-
<select value={country} onChange={e => setCountry(e.target.value)}>
61-
<option value="">--</option>
62-
{countries.map(c => <option key={c.Slug} value={c.Slug}>{c.Country}</option>)}
63-
</select>
64-
</label>
65-
{selected && (
66-
<Card title={selected.Country}>
67-
<p>New Confirmed: {selected.NewConfirmed.toLocaleString()}</p>
68-
<p>Total Confirmed: {selected.TotalConfirmed.toLocaleString()}</p>
69-
<p>Total Deaths: {selected.TotalDeaths.toLocaleString()}</p>
70-
</Card>
71-
)}
72-
{/* TODO: Add daily trends chart using ChartPlaceholder */}
104+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-4 md:p-8">
105+
<div className="max-w-7xl mx-auto">
106+
{/* Header */}
107+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-8">
108+
<div>
109+
<h1 className="text-4xl font-bold text-gray-800 flex items-center gap-3">
110+
<Activity className="text-red-500" size={40} />
111+
COVID-19 Dashboard
112+
</h1>
113+
<p className="text-gray-600 mt-2">Real-time global pandemic statistics</p>
114+
</div>
115+
<button
116+
onClick={fetchSummary}
117+
disabled={loading}
118+
className="flex items-center gap-2 bg-indigo-600 text-white px-6 py-3 rounded-lg hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg hover:shadow-xl"
119+
>
120+
<RefreshCw size={18} className={loading ? 'animate-spin' : ''} />
121+
{loading ? 'Refreshing...' : 'Refresh Data'}
122+
</button>
123+
</div>
124+
125+
{loading && !summary && (
126+
<div className="flex justify-center items-center py-20">
127+
<div className="animate-spin rounded-full h-16 w-16 border-b-4 border-indigo-600"></div>
128+
</div>
129+
)}
130+
131+
{error && (
132+
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg mb-6 flex items-center gap-3">
133+
<AlertCircle size={24} />
134+
<p>{error}</p>
135+
</div>
136+
)}
137+
138+
{countries.length > 0 && (
139+
<>
140+
{/* Global Statistics Cards */}
141+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
142+
<div className="bg-white rounded-xl shadow-lg p-6 border-l-4 border-blue-500 hover:shadow-xl transition-shadow">
143+
<div className="flex items-center justify-between mb-2">
144+
<h3 className="text-gray-600 text-sm font-semibold uppercase">Total Cases</h3>
145+
<Globe className="text-blue-500" size={24} />
146+
</div>
147+
<p className="text-3xl font-bold text-gray-800">{formatNumber(global.cases)}</p>
148+
<p className="text-sm text-blue-600 mt-1">+{formatNumber(global.todayCases)} today</p>
149+
</div>
150+
151+
<div className="bg-white rounded-xl shadow-lg p-6 border-l-4 border-yellow-500 hover:shadow-xl transition-shadow">
152+
<div className="flex items-center justify-between mb-2">
153+
<h3 className="text-gray-600 text-sm font-semibold uppercase">Active Cases</h3>
154+
<TrendingUp className="text-yellow-500" size={24} />
155+
</div>
156+
<p className="text-3xl font-bold text-gray-800">{formatNumber(global.active)}</p>
157+
<p className="text-sm text-gray-500 mt-1">Currently infected</p>
158+
</div>
159+
160+
<div className="bg-white rounded-xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-xl transition-shadow">
161+
<div className="flex items-center justify-between mb-2">
162+
<h3 className="text-gray-600 text-sm font-semibold uppercase">Recovered</h3>
163+
<Users className="text-green-500" size={24} />
164+
</div>
165+
<p className="text-3xl font-bold text-gray-800">{formatNumber(global.recovered)}</p>
166+
<p className="text-sm text-green-600 mt-1">{((global.recovered/global.cases)*100).toFixed(1)}% recovery rate</p>
167+
</div>
168+
169+
<div className="bg-white rounded-xl shadow-lg p-6 border-l-4 border-red-500 hover:shadow-xl transition-shadow">
170+
<div className="flex items-center justify-between mb-2">
171+
<h3 className="text-gray-600 text-sm font-semibold uppercase">Deaths</h3>
172+
<AlertCircle className="text-red-500" size={24} />
173+
</div>
174+
<p className="text-3xl font-bold text-gray-800">{formatNumber(global.deaths)}</p>
175+
<p className="text-sm text-red-600 mt-1">+{formatNumber(global.todayDeaths)} today</p>
176+
</div>
177+
</div>
178+
179+
{/* Charts Row */}
180+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
181+
{/* Top 10 Countries Chart */}
182+
<div className="bg-white rounded-xl shadow-lg p-6">
183+
<h3 className="text-xl font-bold text-gray-800 mb-4">Top 10 Countries by Cases</h3>
184+
<ResponsiveContainer width="100%" height={300}>
185+
<BarChart data={topCountries}>
186+
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
187+
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} tick={{ fontSize: 12 }} />
188+
<YAxis tick={{ fontSize: 12 }} />
189+
<Tooltip formatter={(value) => formatNumber(value)} />
190+
<Bar dataKey="cases" fill="#3b82f6" radius={[8, 8, 0, 0]} />
191+
</BarChart>
192+
</ResponsiveContainer>
193+
</div>
194+
195+
{/* Global Distribution Pie Chart */}
196+
<div className="bg-white rounded-xl shadow-lg p-6">
197+
<h3 className="text-xl font-bold text-gray-800 mb-4">Global Case Distribution</h3>
198+
<ResponsiveContainer width="100%" height={300}>
199+
<PieChart>
200+
<Pie
201+
data={globalPieData}
202+
cx="50%"
203+
cy="50%"
204+
labelLine={false}
205+
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(1)}%`}
206+
outerRadius={100}
207+
fill="#8884d8"
208+
dataKey="value"
209+
>
210+
{globalPieData.map((entry, index) => (
211+
<Cell key={`cell-${index}`} fill={entry.color} />
212+
))}
213+
</Pie>
214+
<Tooltip formatter={(value) => formatNumber(value)} />
215+
</PieChart>
216+
</ResponsiveContainer>
217+
</div>
218+
</div>
219+
220+
{/* Country Selection */}
221+
<div className="bg-white rounded-xl shadow-lg p-6 mb-8">
222+
<h3 className="text-xl font-bold text-gray-800 mb-4">Country Analysis</h3>
223+
<div className="flex flex-col md:flex-row gap-4 mb-6">
224+
<input
225+
type="text"
226+
placeholder="Search countries..."
227+
value={searchTerm}
228+
onChange={(e) => setSearchTerm(e.target.value)}
229+
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
230+
/>
231+
<select
232+
value={country}
233+
onChange={(e) => setCountry(e.target.value)}
234+
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent min-w-[200px]"
235+
>
236+
<option value="">Select a country</option>
237+
{filteredCountries.map((c) => (
238+
<option key={c.countryInfo._id || c.country} value={c.country}>
239+
{c.country}
240+
</option>
241+
))}
242+
</select>
243+
</div>
244+
245+
{selected && (
246+
<div>
247+
<div className="flex items-center gap-3 mb-4">
248+
<img src={selected.countryInfo.flag} alt={selected.country} className="w-12 h-8 object-cover rounded shadow" />
249+
<h4 className="text-2xl font-bold text-gray-800">{selected.country}</h4>
250+
</div>
251+
252+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
253+
<div className="bg-blue-50 p-4 rounded-lg">
254+
<p className="text-sm text-gray-600 mb-1">Total Cases</p>
255+
<p className="text-2xl font-bold text-blue-600">{formatNumber(selected.cases)}</p>
256+
<p className="text-xs text-blue-500 mt-1">+{formatNumber(selected.todayCases)} today</p>
257+
</div>
258+
<div className="bg-yellow-50 p-4 rounded-lg">
259+
<p className="text-sm text-gray-600 mb-1">Active</p>
260+
<p className="text-2xl font-bold text-yellow-600">{formatNumber(selected.active)}</p>
261+
</div>
262+
<div className="bg-green-50 p-4 rounded-lg">
263+
<p className="text-sm text-gray-600 mb-1">Recovered</p>
264+
<p className="text-2xl font-bold text-green-600">{formatNumber(selected.recovered)}</p>
265+
</div>
266+
<div className="bg-red-50 p-4 rounded-lg">
267+
<p className="text-sm text-gray-600 mb-1">Deaths</p>
268+
<p className="text-2xl font-bold text-red-600">{formatNumber(selected.deaths)}</p>
269+
<p className="text-xs text-red-500 mt-1">+{formatNumber(selected.todayDeaths)} today</p>
270+
</div>
271+
</div>
272+
273+
<div className="flex items-center gap-2 text-sm text-gray-500">
274+
<Calendar size={16} />
275+
<span>Last updated: {new Date(selected.updated).toLocaleString()}</span>
276+
</div>
277+
</div>
278+
)}
279+
</div>
280+
281+
{/* Historical Trend Chart */}
282+
{historicalChartData.length > 0 && (
283+
<div className="bg-white rounded-xl shadow-lg p-6">
284+
<h3 className="text-xl font-bold text-gray-800 mb-4">30-Day Trend for {selected?.country}</h3>
285+
<ResponsiveContainer width="100%" height={350}>
286+
<LineChart data={historicalChartData}>
287+
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
288+
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
289+
<YAxis tick={{ fontSize: 12 }} />
290+
<Tooltip formatter={(value) => formatNumber(value)} />
291+
<Legend />
292+
<Line type="monotone" dataKey="cases" stroke="#3b82f6" strokeWidth={2} dot={false} name="Total Cases" />
293+
<Line type="monotone" dataKey="deaths" stroke="#ef4444" strokeWidth={2} dot={false} name="Deaths" />
294+
<Line type="monotone" dataKey="recovered" stroke="#10b981" strokeWidth={2} dot={false} name="Recovered" />
295+
</LineChart>
296+
</ResponsiveContainer>
297+
</div>
298+
)}
299+
300+
{/* Data Source */}
301+
<div className="mt-8 text-center text-sm text-gray-500">
302+
<p>Data source: <a href="https://disease.sh" target="_blank" rel="noopener noreferrer" className="text-indigo-600 hover:underline">disease.sh API</a></p>
303+
<p className="mt-1">Data may have delays. For official information, consult WHO and local health authorities.</p>
304+
</div>
305+
</>
306+
)}
307+
</div>
73308
</div>
74309
);
75-
}
310+
}

0 commit comments

Comments
 (0)