Skip to content

Commit ef02b81

Browse files
Merge branch 'main' into debouncing
2 parents f85b32a + 1ab00cd commit ef02b81

File tree

14 files changed

+1487
-266
lines changed

14 files changed

+1487
-266
lines changed

.github/workflows/require-assigned-issue.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# Here if the issue is not referenced in PR an alert pops up
12
name: Require referenced assigned issue
23

34
on:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist
33
.DS_Store
44
*.log
55
package-lock.json
6+
.env

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ src/
2424

2525
If you’re unsure where to start, open a discussion or comment on an issue to be assigned.
2626

27+
> Pro Tip : If you raise a valid issue - you WILL BE assigned that
28+
2729
## Contribution Ideas
2830
General:
2931
- Add accessibility (aria labels, focus states, skip links)

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12+
"leaflet": "^1.9.4",
13+
"lucide-react": "^0.546.0",
1214
"react": "^18.3.1",
1315
"react-dom": "^18.3.1",
14-
"react-router-dom": "^6.27.0",
15-
"leaflet": "^1.9.4",
16-
"react-leaflet": "^4.2.1"
16+
"react-leaflet": "^4.2.1",
17+
"react-router-dom": "^6.27.0"
1718
},
1819
"devDependencies": {
1920
"@vitejs/plugin-react": "^4.3.1",

src/App.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default function App() {
5656
<ThemeSwitcher theme={theme} toggleTheme={toggleTheme} />
5757
<main className="container">
5858
<Routes>
59+
{/* Different Routes */}
5960
<Route path="/" element={<Home />} />
6061
<Route path="/weather" element={<Weather />} />
6162
<Route path="/crypto" element={<Crypto />} />

src/components/Card.jsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
export default function Card({ title, children, footer }) {
1+
export default function Card({ title,japaneseTitle,image, children, footer }) {
22
return (
33
<div className="card">
4+
{image && (
5+
<img
6+
src={image}
7+
alt={title}
8+
className="w-full h-64 object-cover"
9+
/>
10+
)}
411
{title && <h3>{title}</h3>}
12+
{japaneseTitle && <p className="text-sm italic text-gray-500 dark:text-gray-300">{japaneseTitle}</p>}
513
<div className="card-body">{children}</div>
614
{footer && <div className="card-footer">{footer}</div>}
715
</div>
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+
}

0 commit comments

Comments
 (0)