wayfarer-weather/components/HomeLanding.tsx
Cayden Schwechten 6250d23297 init
2026-10-31 21:20:16 -05:00

177 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}