init
This commit is contained in:
commit
6250d23297
53 changed files with 6381 additions and 0 deletions
177
components/HomeLanding.tsx
Normal file
177
components/HomeLanding.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue