Skip to content

Commit f874fa6

Browse files
committed
integrate Recharts and add hourly forecast chart
1 parent 914fc89 commit f874fa6

File tree

2 files changed

+88
-37
lines changed

2 files changed

+88
-37
lines changed

package.json

Lines changed: 3 additions & 2 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",
1213
"react": "^18.3.1",
1314
"react-dom": "^18.3.1",
15+
"react-leaflet": "^4.2.1",
1416
"react-router-dom": "^6.27.0",
15-
"leaflet": "^1.9.4",
16-
"react-leaflet": "^4.2.1"
17+
"recharts": "^3.2.1"
1718
},
1819
"devDependencies": {
1920
"@vitejs/plugin-react": "^4.3.1",

src/pages/Weather.jsx

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* - [x] Add error retry button component
1616
* - [ ] Add favorites list (pin cities)
1717
* Advanced:
18-
* - [ ] Hourly forecast visualization (line / area chart)
18+
* - [x] Hourly forecast visualization (line / area chart)
1919
* - [x] Animate background transitions
2020
* - [ ] Add geolocation: auto-detect user city (with permission)
2121
*/
@@ -30,6 +30,17 @@ import {
3030
clearWeatherCache,
3131
getCacheStats,
3232
} from "../services/weather.js";
33+
import {
34+
LineChart,
35+
Line,
36+
XAxis,
37+
YAxis,
38+
Tooltip,
39+
ResponsiveContainer,
40+
CartesianGrid,
41+
AreaChart,
42+
Area,
43+
} from "recharts";
3344

3445
// Helper to determine weather background class
3546
const weatherToClass = (desc = "") => {
@@ -65,7 +76,10 @@ function renderWeatherAnimation(variant) {
6576
<>
6677
<svg className="cloud-svg cloud--left" viewBox="0 0 220 80" aria-hidden>
6778
<g filter="url(#cloudBlur)">
68-
<path className="cloud-shape" d="M20 50 C20 34 42 22 62 26 C70 16 92 12 110 22 C130 8 160 12 170 28 C196 30 206 44 190 54 L30 60 C22 60 20 54 20 50 Z" />
79+
<path
80+
className="cloud-shape"
81+
d="M20 50 C20 34 42 22 62 26 C70 16 92 12 110 22 C130 8 160 12 170 28 C196 30 206 44 190 54 L30 60 C22 60 20 54 20 50 Z"
82+
/>
6983
</g>
7084
<defs>
7185
<filter id="cloudBlur" x="-20%" y="-20%" width="140%" height="140%">
@@ -105,7 +119,7 @@ function renderWeatherAnimation(variant) {
105119
left: `${(i / 12) * 100}%`,
106120
animationDelay: `${(i % 6) * 0.4}s`,
107121
"--dur": `${10 + (i % 6)}s`,
108-
"--drift": `${(i % 2 === 0 ? -40 : 40)}px`,
122+
"--drift": `${i % 2 === 0 ? -40 : 40}px`,
109123
width: `${8 + (i % 3) * 4}px`,
110124
height: `${8 + (i % 3) * 4}px`,
111125
}}
@@ -213,6 +227,15 @@ export default function Weather() {
213227
return { color: "#E0E0E0", label: "Clear 🌤️" };
214228
};
215229

230+
// Hourly forecast chart data
231+
const hourlyData =
232+
data?.weather?.[0]?.hourly?.map((h) => ({
233+
time: h.time.length === 1 ? "00:00" : `${h.time.padStart(4, "0").slice(0, 2)}:00`,
234+
temp: displayTemp(Number(h.tempC)),
235+
feelsLike: displayTemp(Number(h.FeelsLikeC)),
236+
humidity: Number(h.humidity),
237+
})) || [];
238+
216239
return (
217240
<div
218241
className="weather-page"
@@ -256,10 +279,7 @@ export default function Weather() {
256279

257280
{loading && <Loading />}
258281
{error && (
259-
<ErrorMessage
260-
message={error.message}
261-
onRetry={() => fetchWeather(city)}
262-
/>
282+
<ErrorMessage message={error.message} onRetry={() => fetchWeather(city)} />
263283
)}
264284

265285
{data && !loading && (
@@ -276,8 +296,8 @@ export default function Weather() {
276296
/>
277297
)}
278298
<span>
279-
<strong>Temperature:</strong>{" "}
280-
{displayTemp(Number(current.temp_C))}°{unit}
299+
<strong>Temperature:</strong> {displayTemp(Number(current.temp_C))}°
300+
{unit}
281301
</span>
282302
</p>
283303
<p>
@@ -290,36 +310,25 @@ export default function Weather() {
290310

291311
{/* 3-Day Forecast */}
292312
{forecast.map((day, i) => {
293-
const condition =
294-
day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear";
313+
const condition = day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear";
295314
const badge = getBadgeStyle(condition);
296315

297316
return (
298317
<Card key={i} title={i === 0 ? "Today" : `Day ${i + 1}`}>
299-
{day.hourly?.[0] &&
300-
getIconUrl(day.hourly?.[0]?.weatherIconUrl) && (
301-
<div style={{ marginTop: 8 }}>
302-
<img
303-
src={getIconUrl(day.hourly?.[0]?.weatherIconUrl)}
304-
alt={
305-
day.hourly?.[0]?.weatherDesc?.[0]?.value ||
306-
"forecast icon"
307-
}
308-
style={{ width: 40, height: 40, objectFit: "contain" }}
309-
onError={(e) =>
310-
(e.currentTarget.style.display = "none")
311-
}
312-
/>
313-
</div>
314-
)}
315-
316-
<div
317-
style={{
318-
display: "flex",
319-
gap: "8px",
320-
marginTop: "17px",
321-
}}
322-
>
318+
{day.hourly?.[0] && getIconUrl(day.hourly?.[0]?.weatherIconUrl) && (
319+
<div style={{ marginTop: 8 }}>
320+
<img
321+
src={getIconUrl(day.hourly?.[0]?.weatherIconUrl)}
322+
alt={
323+
day.hourly?.[0]?.weatherDesc?.[0]?.value || "forecast icon"
324+
}
325+
style={{ width: 40, height: 40, objectFit: "contain" }}
326+
onError={(e) => (e.currentTarget.style.display = "none")}
327+
/>
328+
</div>
329+
)}
330+
331+
<div style={{ display: "flex", gap: "8px", marginTop: "17px" }}>
323332
<strong>Avg Temp:</strong>{" "}
324333
{displayTemp(Number(day.avgtempC))}°{unit}
325334
<div
@@ -346,6 +355,47 @@ export default function Weather() {
346355
</Card>
347356
);
348357
})}
358+
359+
{/* Hourly Forecast Visualization */}
360+
{hourlyData.length > 0 && (
361+
<Card title="Hourly Forecast (Next 24h)">
362+
<div style={{ width: "100%", height: 300 }}>
363+
<ResponsiveContainer>
364+
<AreaChart data={hourlyData}>
365+
<defs>
366+
<linearGradient id="tempGradient" x1="0" y1="0" x2="0" y2="1">
367+
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
368+
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
369+
</linearGradient>
370+
</defs>
371+
<XAxis dataKey="time" />
372+
<YAxis
373+
label={{
374+
value: ${unit}`,
375+
angle: -90,
376+
position: "insideLeft",
377+
}}
378+
/>
379+
<Tooltip
380+
contentStyle={{
381+
backgroundColor: "rgba(255,255,255,0.9)",
382+
borderRadius: "8px",
383+
}}
384+
/>
385+
<CartesianGrid strokeDasharray="3 3" />
386+
<Area
387+
type="monotone"
388+
dataKey="temp"
389+
stroke="#8884d8"
390+
fillOpacity={1}
391+
fill="url(#tempGradient)"
392+
/>
393+
<Line type="monotone" dataKey="feelsLike" stroke="#82ca9d" />
394+
</AreaChart>
395+
</ResponsiveContainer>
396+
</div>
397+
</Card>
398+
)}
349399
</div>
350400
)}
351401

0 commit comments

Comments
 (0)