@@ -296,6 +296,34 @@ export default function Weather() {
296296 // Helper to convert °C to °F
297297 const displayTemp = ( c ) => ( unit === "C" ? c : Math . round ( ( c * 9 ) / 5 + 32 ) ) ;
298298
299+ // Normalize icon URL returned by wttr.in (they sometimes return // links)
300+ const getIconUrl = ( iconArrOrStr ) => {
301+ if ( ! iconArrOrStr ) return null ;
302+
303+ // If API returned an array like [{ value: "//..." }]
304+ if ( Array . isArray ( iconArrOrStr ) ) {
305+ const url = iconArrOrStr ?. [ 0 ] ?. value ;
306+ if ( ! url ) return null ;
307+ return url . startsWith ( "//" ) ? `https:${ url } ` : url ;
308+ }
309+
310+ // If API returned a plain string (rare) or already a URL
311+ if ( typeof iconArrOrStr === "string" ) {
312+ return iconArrOrStr . startsWith ( "//" )
313+ ? `https:${ iconArrOrStr } `
314+ : iconArrOrStr ;
315+ }
316+
317+ return null ;
318+ } ;
319+
320+ // Format wttr.in hourly time values like "0", "300", "1500" -> "00:00", "03:00", "15:00"
321+ const formatWttTime = ( t ) => {
322+ if ( t == null ) return "" ;
323+ const s = String ( t ) . padStart ( 4 , "0" ) ;
324+ return `${ s . slice ( 0 , 2 ) } :${ s . slice ( 2 ) } ` ;
325+ } ;
326+
299327 const getBadgeStyle = ( condition ) => {
300328 if ( ! condition ) return { color : "#E0E0E0" , label : "Clear 🌤️" } ;
301329
@@ -383,6 +411,164 @@ export default function Weather() {
383411 Switch to °{ unit === "C" ? "F" : "C" }
384412 </ button >
385413 </ div >
414+ { loading && < Loading /> }
415+ { error && (
416+ < ErrorMessage
417+ message = { error . message }
418+ onRetry = { ( ) => fetchWeather ( city ) }
419+ />
420+ ) }
421+
422+ { data && ! loading && (
423+ < div className = "dashboard-grid" >
424+ { /* Current Weather */ }
425+ < Card title = "Current Weather" size = "large" >
426+ < h2 > { data . nearest_area ?. [ 0 ] ?. areaName ?. [ 0 ] ?. value || city } </ h2 >
427+ < p style = { { display : "flex" , alignItems : "center" , gap : "12px" } } >
428+ { current && getIconUrl ( current . weatherIconUrl ) && (
429+ < img
430+ src = { getIconUrl ( current . weatherIconUrl ) }
431+ alt = { current . weatherDesc ?. [ 0 ] ?. value || "weather icon" }
432+ style = { { width : 48 , height : 48 , objectFit : "contain" } }
433+ />
434+ ) }
435+ < span >
436+ < strong > Temperature:</ strong > { " " }
437+ { displayTemp ( Number ( current . temp_C ) ) } °{ unit }
438+ </ span >
439+ </ p >
440+ < p >
441+ < strong > Humidity:</ strong > { current . humidity } %
442+ </ p >
443+ < p >
444+ < strong > Desc:</ strong > { current . weatherDesc ?. [ 0 ] ?. value }
445+ </ p >
446+ </ Card >
447+
448+ { /* 3-Day Forecast */ }
449+ { forecast . map ( ( day , i ) => {
450+ const condition =
451+ day . hourly ?. [ 0 ] ?. weatherDesc ?. [ 0 ] ?. value || "Clear" ;
452+ const badge = getBadgeStyle ( condition ) ;
453+
454+ return (
455+ < Card key = { i } title = { i === 0 ? "Today" : `Day ${ i + 1 } ` } >
456+ { /* Badge Section */ }
457+ { /* Forecast icon (use first hourly entry icon) */ }
458+ { day . hourly ?. [ 0 ] &&
459+ getIconUrl ( day . hourly ?. [ 0 ] ?. weatherIconUrl ) && (
460+ < div style = { { marginTop : 8 } } >
461+ < img
462+ src = { getIconUrl ( day . hourly ?. [ 0 ] ?. weatherIconUrl ) }
463+ alt = {
464+ day . hourly ?. [ 0 ] ?. weatherDesc ?. [ 0 ] ?. value ||
465+ "forecast icon"
466+ }
467+ style = { { width : 40 , height : 40 , objectFit : "contain" } }
468+ onError = { ( e ) =>
469+ ( e . currentTarget . style . display = "none" )
470+ }
471+ />
472+ </ div >
473+ ) }
474+
475+ { /* Full-day hourly timeline (0:00 - 23:00) */ }
476+ { day . hourly && (
477+ < div
478+ style = { {
479+ display : "block" ,
480+ marginTop : 8 ,
481+ } }
482+ >
483+ < div
484+ style = { {
485+ display : "flex" ,
486+ gap : 12 ,
487+ overflowX : "auto" ,
488+ padding : "8px 4px" ,
489+ WebkitOverflowScrolling : "touch" ,
490+ } }
491+ >
492+ { day . hourly . map ( ( h , idx ) => {
493+ const icon = getIconUrl ( h . weatherIconUrl ) ;
494+ const t = h . time ?? h . Time ?? "" ;
495+ const temp =
496+ h . tempC ?? h . temp_C ?? h . tempC ?? h . tempF ?? "" ;
497+
498+ return (
499+ < div
500+ key = { idx }
501+ style = { {
502+ minWidth : 72 ,
503+ padding : 6 ,
504+ borderRadius : 6 ,
505+ background : "rgba(0,0,0,0.03)" ,
506+ display : "flex" ,
507+ flexDirection : "column" ,
508+ alignItems : "center" ,
509+ justifyContent : "center" ,
510+ fontSize : 12 ,
511+ } }
512+ >
513+ { icon ? (
514+ < img
515+ src = { icon }
516+ alt = { h . weatherDesc ?. [ 0 ] ?. value || "hour icon" }
517+ style = { {
518+ width : 36 ,
519+ height : 36 ,
520+ objectFit : "contain" ,
521+ } }
522+ loading = "lazy"
523+ onError = { ( e ) =>
524+ ( e . currentTarget . style . display = "none" )
525+ }
526+ />
527+ ) : (
528+ < div style = { { fontSize : 18 } } > 🌤️</ div >
529+ ) }
530+ < div style = { { marginTop : 6 } } >
531+ { formatWttTime ( t ) }
532+ </ div >
533+ < div style = { { fontWeight : 700 } } >
534+ { displayTemp ( Number ( temp ) ) } °{ unit }
535+ </ div >
536+ </ div >
537+ ) ;
538+ } ) }
539+ </ div >
540+ </ div >
541+ ) }
542+
543+ < div style = {
544+ { display :"flex" , gap :"8px" , marginTop :"17px" }
545+ } >
546+ < strong > Avg Temp:</ strong > { displayTemp ( Number ( day . avgtempC ) ) }
547+ °{ unit }
548+ < div
549+ style = { {
550+ backgroundColor : badge . color ,
551+ borderRadius : "8px" ,
552+ padding : "4px 8px" ,
553+ display : "inline-block" ,
554+ fontSize : "12px" ,
555+ fontWeight : "bold" ,
556+ marginBottom : "8px" ,
557+ color : "#333" ,
558+ } }
559+ >
560+ { badge . label }
561+ </ div >
562+ </ div >
563+ < p >
564+ < strong > Sunrise:</ strong > { day . astronomy ?. [ 0 ] ?. sunrise }
565+ </ p >
566+ < p >
567+ < strong > Sunset:</ strong > { day . astronomy ?. [ 0 ] ?. sunset }
568+ </ p >
569+ </ Card >
570+ ) ;
571+ } ) }
386572 </ div >
387573
388574 { loading && < Loading /> }
0 commit comments