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
255export 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