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
3546const 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