"use client"; import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; import { formatClock } from "@/lib/display"; import { loadFrames, RADAR_LAYERS, type RadarLayerId } from "@/lib/radar/layers"; import { outlineButton, pixelLabel, sectionHeading } from "@/lib/styles"; import type { Place, RadarFrame } from "@/lib/weather/types"; import { Slot } from "../Slot"; import { ChoiceChips } from "../ui/ChoiceChips"; // Leaflet needs `window`, so the map only ever renders in the browser. const RadarMap = dynamic(() => import("./RadarMap"), { ssr: false, loading: () =>
Loading map…
, }); const FRAME_MS = 700; // animation speed const REFRESH_MS = 5 * 60_000; // fetch fresh frames every 5 minutes /** The radar section: layer picker, animated map, and playback controls. */ export function RadarPanel({ place, timeZone, hasOpenWeather }: { place: Place; timeZone: string | null; hasOpenWeather: boolean }) { // US places start on the sharper NEXRAD radar; everywhere else on RainViewer. const [layerId, setLayerId] = useState(place.country === "US" ? "nexrad" : "rainviewer"); const [frames, setFrames] = useState([]); const [frameIndex, setFrameIndex] = useState(0); const [playing, setPlaying] = useState(false); const [error, setError] = useState(null); // Switch between US-only NEXRAD and global RainViewer when crossing borders. useEffect(() => { setLayerId((current) => { if (place.country !== "US" && current === "nexrad") return "rainviewer"; if (place.country === "US" && current === "rainviewer") return "nexrad"; if (!hasOpenWeather && current.startsWith("owm-")) return place.country === "US" ? "nexrad" : "rainviewer"; return current; }); }, [place.country, hasOpenWeather]); const layer = RADAR_LAYERS.find((l) => l.id === layerId)!; const isAnimated = frames.length > 1; // Load frames for the chosen layer, then keep them fresh. useEffect(() => { let cancelled = false; async function refresh() { try { const loaded = await loadFrames(layerId); if (cancelled) return; setFrames(loaded); setFrameIndex(loaded.length - 1); // start on the latest frame setError(null); } catch (err) { if (!cancelled) setError((err as Error).message); } } refresh(); const timer = setInterval(refresh, REFRESH_MS); return () => { cancelled = true; clearInterval(timer); }; }, [layerId]); // Autoplay unless the person prefers reduced motion. useEffect(() => { setPlaying(!window.matchMedia("(prefers-reduced-motion: reduce)").matches); }, []); // Step through frames while playing. useEffect(() => { if (!playing || !isAnimated) return; const timer = setInterval(() => setFrameIndex((i) => (i + 1) % frames.length), FRAME_MS); return () => clearInterval(timer); }, [playing, isAnimated, frames.length]); const frame = frames[frameIndex]; const isLatest = frameIndex === frames.length - 1; return (

Radar and maps

hasOpenWeather || !l.needsOpenWeather).map((l) => ({ value: l.id, label: l.label }))} /> {/* Height goes on the outer frame: the inner one is already h-full, and two height utilities on one element would fight over which wins. */} {isAnimated && frame && (
{ setPlaying(false); setFrameIndex(Number(e.target.value)); }} className="min-w-0 flex-1 accent-gold" /> {isLatest ? "Latest" : formatClock(frame.time, timeZone)}
)}

{error ?? layer.hint}

); }