"use client"; import { type RefObject, useEffect, useId, useRef, useState } from "react"; import { namePlace, searchPlaces } from "@/lib/client-api"; import { formatCoords } from "@/lib/display"; import { outlineButton } from "@/lib/styles"; import type { Place } from "@/lib/weather/types"; /** City search with debounced suggestions, arrow-key navigation, and "use my location". */ export function PlaceSearch({ onSelect, inputRef, geocoderUsesOpenWeather = false, }: { onSelect: (place: Place) => void; inputRef?: RefObject; geocoderUsesOpenWeather?: boolean; }) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [activeIndex, setActiveIndex] = useState(-1); const [searching, setSearching] = useState(false); const [open, setOpen] = useState(false); const [message, setMessage] = useState(null); const [locating, setLocating] = useState(false); const listId = useId(); const containerRef = useRef(null); // Close the dropdown when clicking outside the search box. useEffect(() => { function handleOutsidePointer(event: PointerEvent) { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setOpen(false); } } document.addEventListener("pointerdown", handleOutsidePointer); return () => document.removeEventListener("pointerdown", handleOutsidePointer); }, []); // Wait until typing pauses for 350 ms, then search. Cancel stale requests. useEffect(() => { const trimmed = query.trim(); if (trimmed.length < 2) { setResults([]); setSearching(false); setActiveIndex(-1); return; } setSearching(true); const controller = new AbortController(); const timer = setTimeout(async () => { try { const places = await searchPlaces(trimmed, controller.signal); setResults(places); setActiveIndex(places.length > 0 ? 0 : -1); setOpen(true); setMessage(places.length ? null : `No places match “${trimmed}”. Try adding a state or country.`); } catch (error) { if (!controller.signal.aborted) setMessage((error as Error).message); } finally { if (!controller.signal.aborted) setSearching(false); } }, 350); return () => { clearTimeout(timer); controller.abort(); }; }, [query]); function choose(place: Place) { onSelect(place); setQuery(""); setResults([]); setActiveIndex(-1); setOpen(false); setMessage(null); } function useMyLocation() { if (!navigator.geolocation) { setMessage("This browser can't share its location. Search for a city instead."); return; } setLocating(true); setMessage(null); navigator.geolocation.getCurrentPosition( async ({ coords }) => { try { choose(await namePlace(coords.latitude, coords.longitude)); } catch (error) { setMessage((error as Error).message); } finally { setLocating(false); } }, () => { setLocating(false); setMessage("Location access was blocked. Allow it in your browser, or search for a city."); }, { timeout: 10_000, maximumAge: 300_000 }, ); } const showDropdown = open && results.length > 0; return (
{/* Magnifying glass glyph */} = 0 ? `${listId}-opt-${activeIndex}` : undefined} placeholder="Search a town, city, or region…" autoComplete="off" // text-base (16px) stops iOS from zooming in on focus className="min-h-[46px] w-full min-w-0 rounded-none border border-gold-dim bg-panel/80 pr-20 pl-10 text-base text-ink transition-colors placeholder:text-stone hover:border-gold/70 focus-visible:border-teal focus-visible:outline-offset-0" value={query} onFocus={() => { if (results.length > 0) setOpen(true); }} onChange={(e) => { setQuery(e.target.value); if (!e.target.value.trim()) setMessage(null); }} onKeyDown={(e) => { if (e.key === "Escape") { setOpen(false); return; } if (e.key === "ArrowDown" && results.length > 0) { e.preventDefault(); setOpen(true); setActiveIndex((prev) => (prev + 1) % results.length); } else if (e.key === "ArrowUp" && results.length > 0) { e.preventDefault(); setOpen(true); setActiveIndex((prev) => (prev <= 0 ? results.length - 1 : prev - 1)); } else if (e.key === "Enter" && results.length > 0) { e.preventDefault(); choose(results[activeIndex >= 0 ? activeIndex : 0]); } }} />
{searching && …} {query.length > 0 && ( )}
{showDropdown && (
    {results.map((place, index) => { const isActive = index === activeIndex; return (
  • ); })}

{geocoderUsesOpenWeather ? ( <> Location search by{" "} OpenWeather ) : ( <> Location data by{" "} Open-Meteo.com {" "} &{" "} GeoNames {" "} ( CC BY 4.0 ) )}

)} {message &&

{message}

}
); }