"use client"; import { useState } from "react"; import { compass, dayName, formatHour, formatPrecip, formatTemp, formatWind, uvLabel, type Units } from "@/lib/display"; import { pixelLabel } from "@/lib/styles"; import type { HourlyForecast } from "@/lib/weather/types"; import { SkyIcon } from "../PixelArt"; import { ChoiceChips } from "../ui/ChoiceChips"; import { HourlyChart } from "./HourlyChart"; type Range = "24" | "48" | "168"; type Mode = "chart" | "table"; /** Chart sizing per range, in pixels per hour, so longer ranges stay scrollable but compact. */ const PX_PER_HOUR: Record = { "24": 34, "48": 22, "168": 10 }; const LABEL_EVERY_HOURS: Record = { "24": 3, "48": 6, "168": 12 }; /** Hourly forecast: range picker, chart or table, and a readout of the selected hour. */ export function HourlyView({ hourly, stepHours, units, timeZone }: { hourly: HourlyForecast[]; stepHours: 1 | 3; units: Units; timeZone: string | null; }) { const [range, setRange] = useState("48"); const [mode, setMode] = useState("chart"); const [selected, setSelected] = useState(0); if (hourly.length === 0) { return

Hourly forecast unavailable for this location.

; } const availableHours = hourly.length * stepHours; const ranges: { value: Range; label: string }[] = [ { value: "24", label: "24 hours" }, { value: "48", label: "48 hours" }, { value: "168", label: "7 days" }, ]; // Offer a range only if the provider delivers at least most of it. const offered = ranges.filter((r) => r.value === "24" || availableHours >= Number(r.value) * 0.75); const activeRange = offered.some((r) => r.value === range) ? range : offered[offered.length - 1].value; const hours = hourly.slice(0, Math.ceil(Number(activeRange) / stepHours)); const current = hours[Math.min(selected, hours.length - 1)]; return (
{ setRange(r); setSelected(0); }} />
{stepHours === 3 &&

This provider forecasts in 3-hour steps.

} {mode === "chart" ? ( <> {/* Legend: two series share the temperature band */}
) : ( )}
); } /** The details of one hour, shown above the chart and driven by the crosshair. */ function HourReadout({ hour, units, timeZone }: { hour: HourlyForecast; units: Units; timeZone: string | null }) { const facts = [ hour.feelsLikeC !== null && ["Feels like", formatTemp(hour.feelsLikeC, units)], hour.precipChance !== null && ["Precip", `${hour.precipChance}%${hour.precipMm ? ` · ${formatPrecip(hour.precipMm, units)}` : ""}`], ["Wind", `${formatWind(hour.windKph, units)}${hour.windDeg !== null ? ` ${compass(hour.windDeg)}` : ""}${hour.gustKph ? `, gusts ${formatWind(hour.gustKph, units)}` : ""}`], hour.humidity !== null && ["Humidity", `${hour.humidity}%`], hour.uvIndex !== null && ["UV", `${Math.round(hour.uvIndex)} ${uvLabel(hour.uvIndex)}`], ].filter(Boolean) as [string, string][]; return (

{dayName(hour.time, timeZone)} {formatHour(hour.time, timeZone)} · {formatTemp(hour.tempC, units)}

{facts.map(([label, value]) => (
{label}
{value}
))}
); } /** Every hour as a table row — the accessible, exact-numbers view of the chart. */ function HourlyTable({ hours, units, timeZone }: { hours: HourlyForecast[]; units: Units; timeZone: string | null }) { const cell = "px-3 py-2 text-right tabular-nums whitespace-nowrap"; return (
{hours.map((h) => ( ))}
Time Temp Feels Precip Wind Humidity UV
{dayName(h.time, timeZone)} {formatHour(h.time, timeZone)} {formatTemp(h.tempC, units)} {h.feelsLikeC !== null ? formatTemp(h.feelsLikeC, units) : "–"} {h.precipChance !== null ? `${h.precipChance}%` : "–"} {formatWind(h.windKph, units)}{h.windDeg !== null ? ` ${compass(h.windDeg)}` : ""} {h.humidity !== null ? `${h.humidity}%` : "–"} {h.uvIndex !== null ? Math.round(h.uvIndex) : "–"}
); }