wayfarer-weather/components/HomeLanding.tsx

178 lines
6.8 KiB
TypeScript
Raw Normal View History

2026-10-31 21:20:16 -05:00
"use client";
import { useEffect, useState } from "react";
import { formatCoords } from "@/lib/display";
import { outlineButton, pixelLabel, sectionHeading } from "@/lib/styles";
import type { Place } from "@/lib/weather/types";
import { HeartMeter, SkyIcon } from "./PixelArt";
import { Slot } from "./Slot";
const WAYPOINTS: { place: Place; terrain: string }[] = [
{
place: { name: "New York", region: "New York", country: "US", lat: 40.7128, lon: -74.006 },
terrain: "Atlantic harbor",
},
{
place: { name: "Seattle", region: "Washington", country: "US", lat: 47.6062, lon: -122.3321 },
terrain: "Puget Sound",
},
{
place: { name: "Denver", region: "Colorado", country: "US", lat: 39.7392, lon: -104.9903 },
terrain: "Front Range",
},
{
place: { name: "Reykjavik", country: "IS", lat: 64.1466, lon: -21.9426 },
terrain: "North Atlantic",
},
{
place: { name: "London", country: "GB", lat: 51.5072, lon: -0.1276 },
terrain: "Thames valley",
},
{
place: { name: "Tokyo", country: "JP", lat: 35.6762, lon: 139.6503 },
terrain: "Tokyo Bay",
},
];
const HIGHLIGHTS = [
{
label: "Horizons",
title: "Hourly & 14-day",
detail: "Crosshair temperature & precip chart, exact hourly table, and daily range bars.",
},
{
label: "Live sky map",
title: "Animated radar",
detail: "High-detail US NEXRAD & global RainViewer loops, plus 5 OpenWeather tile layers.",
},
{
label: "Field readings",
title: "Comfort & AQI",
detail: "Five-heart comfort gauge, US AQI & PM2.5, UV stamina bars, NWS alerts, and moon phase.",
},
];
/**
* Homepage landing view shown before a destination is chosen.
* Introduces the station aesthetic and lets visitors jump straight into a waypoint.
*/
export function HomeLanding({
onSelectPlace,
onFocusSearch,
activeProviderLabel,
}: {
onSelectPlace: (place: Place) => void;
onFocusSearch: () => void;
activeProviderLabel?: string;
}) {
const [isDaytime, setIsDaytime] = useState(true);
useEffect(() => {
const hour = new Date().getHours();
setIsDaytime(hour >= 6 && hour < 19);
}, []);
return (
<div className="flex flex-col gap-6">
{/* Hero welcome frame */}
<Slot
as="section"
className="[--cut:22px]"
innerClassName="grid items-center gap-6 p-6 sm:grid-cols-[auto_1fr] sm:gap-8 sm:px-8 sm:py-8"
>
<div className="flex flex-col items-center gap-3 justify-self-center sm:justify-self-start">
<div className="relative grid aspect-square w-[148px] place-items-center border border-gold-dim bg-black bg-radial-[circle_at_50%_45%] from-gold-bright/14 to-transparent to-70% sm:w-[168px]">
<SkyIcon
condition="clear"
isDay={isDaytime}
size={112}
className="h-auto w-[96px] animate-float motion-reduce:animate-none sm:w-[112px]"
/>
<div className="absolute right-2 bottom-2 flex gap-1.5 border border-gold-dim/70 bg-panel/90 px-1.5 py-1">
<SkyIcon condition="clouds" size={16} />
<SkyIcon condition="rain" size={16} />
<SkyIcon condition="storm" size={16} />
</div>
</div>
<div className="flex items-center gap-2">
<HeartMeter hearts={4.5} />
</div>
</div>
<div className="flex flex-col gap-3">
<p className="font-pixel text-[0.7rem] tracking-[0.06em] text-teal">
{activeProviderLabel ? `${activeProviderLabel} active · Ready` : "Weather station ready"}
</p>
<h2 className="font-display text-2xl leading-tight font-medium tracking-[0.02em] text-gold-bright sm:text-3xl">
Read the skies before you set out.
</h2>
<p className="max-w-[54ch] text-sm leading-relaxed text-stone sm:text-base">
Search any town or city above, share your current coordinates, or choose a waypoint below to inspect live conditions, hourly horizons, and animated radar.
</p>
<div className="mt-1 flex flex-wrap items-center gap-3">
<button type="button" onClick={onFocusSearch} className={outlineButton}>
Search a destination
</button>
<span className={pixelLabel}>Or pick a waypoint ↓</span>
</div>
</div>
</Slot>
{/* Quick-start waypoints */}
<section aria-labelledby="waypoints-heading" className="flex flex-col gap-3">
<div className="flex items-center gap-3">
<h2 id="waypoints-heading" className={sectionHeading}>
Suggested waypoints
</h2>
<span className="h-px flex-1 bg-linear-to-r from-gold-dim/70 to-transparent" aria-hidden="true" />
</div>
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-3">
{WAYPOINTS.map(({ place, terrain }) => (
<Slot key={place.name} className="[--cut:10px] transition-transform duration-150 hover:-translate-y-0.5" innerClassName="p-0">
<button
type="button"
onClick={() => onSelectPlace(place)}
className="group flex h-full w-full cursor-pointer flex-col justify-between gap-2 p-3.5 text-left transition-colors hover:bg-gold/8 focus-visible:outline-offset-[-2px]"
>
<div className="flex items-start justify-between gap-2">
<div>
<p className="font-display text-base font-medium text-ink transition-colors group-hover:text-gold-bright">
{place.name}
</p>
<p className="text-xs text-stone">
{place.region ? `${place.region}, ${place.country}` : place.country} · {terrain}
</p>
</div>
<span
aria-hidden="true"
className="font-pixel text-xs text-gold-dim transition-transform group-hover:translate-x-0.5 group-hover:text-gold-bright"
>
›
</span>
</div>
<p className="font-pixel text-[0.65rem] text-teal/85 tabular-nums">
{formatCoords(place.lat, place.lon, 1)}
</p>
</button>
</Slot>
))}
</div>
</section>
{/* Station overview */}
<section aria-label="What Wayfarer includes" className="grid grid-cols-1 gap-2.5 sm:grid-cols-3">
{HIGHLIGHTS.map((item) => (
<Slot key={item.title} className="[--cut:10px]" innerClassName="flex flex-col gap-1 p-3.5">
<p className={pixelLabel}>{item.label}</p>
<p className="font-display text-base font-medium text-gold-bright">{item.title}</p>
<p className="text-xs leading-relaxed text-stone">{item.detail}</p>
</Slot>
))}
</section>
</div>
);
}