85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
|
|
import { comfortHearts, formatClock, formatCoords, formatTemp, placeLabel, precipOutlook, type Units } from "@/lib/display";
|
||
|
|
import { pixelLabel } from "@/lib/styles";
|
||
|
|
import type { Place, WeatherReport } from "@/lib/weather/types";
|
||
|
|
import { AlertsBanner } from "./AlertsBanner";
|
||
|
|
import { ConditionsGrid } from "./ConditionsGrid";
|
||
|
|
import { ForecastPanel, type ForecastView } from "./forecast/ForecastPanel";
|
||
|
|
import { HeartMeter, SkyIcon } from "./PixelArt";
|
||
|
|
import { Slot } from "./Slot";
|
||
|
|
|
||
|
|
/** Everything shown once a report has loaded: alerts, hero, conditions, forecast. */
|
||
|
|
export function WeatherView({ report, place, units, forecastView, onForecastViewChange, providerLabel, maxDaysOnPlan }: {
|
||
|
|
report: WeatherReport;
|
||
|
|
place: Place;
|
||
|
|
units: Units;
|
||
|
|
forecastView: ForecastView;
|
||
|
|
onForecastViewChange: (view: ForecastView) => void;
|
||
|
|
providerLabel: string;
|
||
|
|
maxDaysOnPlan: number;
|
||
|
|
}) {
|
||
|
|
const { current, daily } = report;
|
||
|
|
const today = daily[0];
|
||
|
|
const outlook = precipOutlook(report.hourly, report.timeZone);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col gap-6">
|
||
|
|
<AlertsBanner alerts={report.alerts} source={report.alertSource} timeZone={report.timeZone} />
|
||
|
|
|
||
|
|
{/* Hero: stacks cleanly on phones, side-by-side from sm up */}
|
||
|
|
<Slot
|
||
|
|
as="section"
|
||
|
|
className="[--cut:22px]"
|
||
|
|
innerClassName="grid items-center gap-5 p-6 sm:grid-cols-[auto_1fr] sm:gap-7 sm:px-8 sm:py-7"
|
||
|
|
>
|
||
|
|
<div className="grid aspect-square w-[132px] place-items-center justify-self-center border border-gold-dim bg-black bg-radial-[circle_at_50%_45%] from-gold-bright/12 to-transparent to-65% sm:w-[168px] sm:justify-self-start">
|
||
|
|
<SkyIcon
|
||
|
|
condition={current.condition}
|
||
|
|
isDay={current.isDay}
|
||
|
|
size={132}
|
||
|
|
className="h-auto w-[100px] animate-float motion-reduce:animate-none sm:w-[132px]"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="min-w-0">
|
||
|
|
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
||
|
|
<h2 className="font-display text-lg font-medium tracking-[0.03em] text-ink sm:text-xl">
|
||
|
|
{placeLabel(place)}
|
||
|
|
</h2>
|
||
|
|
<p className={pixelLabel}>
|
||
|
|
Local {formatClock(report.fetchedAt, report.timeZone)} · {formatCoords(place.lat, place.lon)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p className="mt-1 font-display text-[clamp(4.5rem,16vw,7rem)] leading-[0.95] font-medium tracking-[-0.02em] text-gold-bright">
|
||
|
|
{formatTemp(current.tempC, units)}
|
||
|
|
</p>
|
||
|
|
<p className="mt-1.5 text-lg first-letter:uppercase">
|
||
|
|
{current.description}
|
||
|
|
{today && (
|
||
|
|
<span className="text-stone">
|
||
|
|
{" "}· High {formatTemp(today.highC, units)}, low {formatTemp(today.lowC, units)}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
{outlook && <p className="mt-1 text-sm text-teal">{outlook}</p>}
|
||
|
|
<div className="mt-4 flex items-center gap-3 border-t border-gold-dim/40 pt-3">
|
||
|
|
<HeartMeter hearts={comfortHearts(current)} />
|
||
|
|
<span className={pixelLabel}>Comfort</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Slot>
|
||
|
|
|
||
|
|
<ConditionsGrid report={report} units={units} />
|
||
|
|
|
||
|
|
<ForecastPanel
|
||
|
|
report={report}
|
||
|
|
units={units}
|
||
|
|
view={forecastView}
|
||
|
|
onViewChange={onForecastViewChange}
|
||
|
|
providerLabel={providerLabel}
|
||
|
|
maxDaysOnPlan={maxDaysOnPlan}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|