2026-10-31 21:20:16 -05:00
"use client" ;
import { useState } from "react" ;
import {
compass , dayName , formatClock , formatDuration , formatPrecip , formatShortDate , formatTemp , formatWind ,
tempValue , uvLabel , type Units ,
} from "@/lib/display" ;
import { pixelLabel } from "@/lib/styles" ;
import type { DailyForecast } from "@/lib/weather/types" ;
import { SkyIcon } from "../PixelArt" ;
/ * *
* The daily forecast ( up to 14 days ) as rows :
*
* Today Oct 21 [ icon ] 60 % 39 ° ━ ━ ━ ━ ━ █ █ █ █ █ ━ ━ ━ ━ 52 °
*
* The bar shows each day 's low→high against the whole period' s range ,
* so warm and cold days stand out at a glance . Tap a row for its details .
* /
export function DailyView ( { daily , units , timeZone , providerLabel , maxDaysOnPlan } : {
daily : DailyForecast [ ] ;
units : Units ;
timeZone : string | null ;
providerLabel : string ;
maxDaysOnPlan : number ;
} ) {
const [ openDay , setOpenDay ] = useState < string | null > ( null ) ;
if ( daily . length === 0 ) {
return < p className = { pixelLabel } > Daily forecast unavailable for this location . < / p > ;
}
const periodLow = Math . min ( . . . daily . map ( ( d ) = > tempValue ( d . lowC , units ) ) ) ;
const periodHigh = Math . max ( . . . daily . map ( ( d ) = > tempValue ( d . highC , units ) ) ) ;
const span = Math . max ( 1 , periodHigh - periodLow ) ;
const percentAlong = ( celsius : number ) = > ( ( tempValue ( celsius , units ) - periodLow ) / span ) * 100 ;
return (
< div className = "flex flex-col gap-3" >
< ol className = "border border-gold-dim" >
{ daily . map ( ( day ) = > {
const isOpen = openDay === day . time ;
return (
< li key = { day . time } className = "border-t border-gold-dim/50 first:border-t-0" >
< button
type = "button"
aria - expanded = { isOpen }
onClick = { ( ) = > setOpenDay ( isOpen ? null : day . time ) }
className = "grid w-full cursor-pointer grid-cols-[4.5rem_28px_2.5rem_2.25rem_1fr_2.25rem_14px] items-center gap-2 px-3 py-2.5 text-left transition-colors hover:bg-gold/6 sm:grid-cols-[7rem_32px_3rem_2.5rem_1fr_2.5rem_16px] sm:gap-3 sm:px-4"
>
< span className = "leading-tight" >
< span className = "block font-medium" > { dayName ( day . time , timeZone ) } < / span >
< span className = "block text-xs text-stone" > { formatShortDate ( day . time , timeZone ) } < / span >
< / span >
< SkyIcon condition = { day . condition } size = { 28 } / >
< span className = "font-pixel text-xs text-teal" >
{ day . precipChance !== null && day . precipChance >= 10 ? ` ${ day . precipChance } % ` : "" }
< / span >
< span className = "text-right font-display text-stone tabular-nums" > { formatTemp ( day . lowC , units ) } < / span >
{ /* Range bar: the gold segment spans this day's low → high */ }
< span className = "relative h-1.5 bg-gold-dim/40" aria-hidden = "true" >
< span
className = "absolute inset-y-0 bg-linear-to-r from-gold to-gold-bright"
style = { { left : ` ${ percentAlong ( day . lowC ) } % ` , right : ` ${ 100 - percentAlong ( day . highC ) } % ` } }
/ >
< / span >
< span className = "font-display text-gold-bright tabular-nums" > { formatTemp ( day . highC , units ) } < / span >
< span
aria - hidden = "true"
className = { ` justify-self-end font-pixel text-xs text-stone transition-transform duration-150 ${ isOpen ? "rotate-90 text-gold-bright" : "" } ` }
>
›
< / span >
< / button >
{ isOpen && < DayDetails day = { day } units = { units } timeZone = { timeZone } / > }
< / li >
) ;
} ) }
< / ol >
{ daily . length < 14 && (
< p className = { pixelLabel } >
2026-10-31 21:50:28 -05:00
{ providerLabel } provides a { maxDaysOnPlan } - day outlook . Switch to Open - Meteo above for 14 days .
2026-10-31 21:20:16 -05:00
< / p >
) }
< / div >
) ;
}
function DayDetails ( { day , units , timeZone } : { day : DailyForecast ; units : Units ; timeZone : string | null } ) {
const wind =
day . windKph === null
? null
: ` ${ formatWind ( day . windKph , units ) } ${ day . windDeg !== null ? ` from ${ compass ( day . windDeg ) } ` : "" } ${ day . gustKph ? ` , gusts ${ formatWind ( day . gustKph , units ) } ` : "" } ` ;
const facts : [ string , string | null ] [ ] = [
[ "Precipitation" , day . precipMm !== null ? ` ${ formatPrecip ( day . precipMm , units ) } ${ day . precipChance !== null ? ` ( ${ day . precipChance } % chance) ` : "" } ` : null ] ,
[ "Wind" , wind ] ,
[ "UV index" , day . uvIndexMax !== null ? ` ${ Math . round ( day . uvIndexMax ) } ${ uvLabel ( day . uvIndexMax ) } ` : null ] ,
[ "Humidity" , day . humidity !== null ? ` ${ Math . round ( day . humidity ) } % ` : null ] ,
[ "Sunrise" , day . sunrise ? formatClock ( day . sunrise , timeZone ) : null ] ,
[ "Sunset" , day . sunset ? formatClock ( day . sunset , timeZone ) : null ] ,
[ "Daylight" , day . sunrise && day . sunset ? formatDuration ( day . sunrise , day . sunset ) : null ] ,
] ;
return (
< div className = "border-t border-gold-dim/30 bg-black px-4 pt-3 pb-4" >
{ day . summary && < p className = "mb-2 first-letter:uppercase" > { day . summary } < / p > }
< dl className = "grid grid-cols-2 gap-x-6 gap-y-2 sm:grid-cols-4" >
{ facts
. filter ( ( [ , value ] ) = > value !== null )
. map ( ( [ label , value ] ) = > (
< div key = { label } >
< dt className = { pixelLabel } > { label } < / dt >
< dd className = "text-sm" > { value } < / dd >
< / div >
) ) }
< / dl >
< / div >
) ;
}