import { useState, useEffect } from 'react' import { searchDistricts } from '../hooks/useApi.js' const TOP_DISTRICTS_URL = `${import.meta.env.BASE_URL}data/top_districts.json` const SUGGESTION_COUNT = 8 // Module-level cache: the fixture covers every state, so it is fetched at most // once per page load no matter how many states the visitor browses through. let topDistrictsPromise = null function loadTopDistricts() { if (!topDistrictsPromise) { topDistrictsPromise = fetch(TOP_DISTRICTS_URL) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status} loading ${TOP_DISTRICTS_URL}`) return res.json() }) .catch((err) => { // Don't poison the cache with a rejected promise — let a later visit retry. topDistrictsPromise = null throw err }) } return topDistrictsPromise } /** * District search screen with: * 1. "Interesting" suggestions — the districts with the most arrests in the * selected state, read from the committed `public/data/top_districts.json` * fixture (built by `scripts/build-top-districts.mjs`). * 2. Live-search as you type → /api/v1/districts?q=...&state=XX * * The suggestions used to be computed at runtime from * `/estimates?state=XX&year=21-22&limit=500`. That endpoint returns rows * `ORDER BY LEAID, RACE, SEX` at eight rows per district, so the cap selected * the ~62 lowest-LEAID districts in the state rather than the busiest ones — * California has 11,488 rows. The fixture is ranked over the complete result * set, and it also removes a multi-second fetch from this screen. */ export default function DistrictSearch({ state, onSelect, onBack }) { const [query, setQuery] = useState('') const [searchResults, setSearchResults] = useState([]) const [suggestions, setSuggestions] = useState([]) // "interesting" districts sorted by arrests desc const [loadingSugg, setLoadingSugg] = useState(true) const [loadingSearch, setLoadingSearch] = useState(false) // ——— Read the suggestion fixture for this state ——— useEffect(() => { let cancelled = false setLoadingSugg(true) loadTopDistricts() .then((fixture) => { if (cancelled) return const forState = fixture?.states?.[state] || [] setSuggestions( forState.slice(0, SUGGESTION_COUNT).map((d) => ({ leaid: d.leaid, lea_name: d.name, state, observed_arrests: d.arrests, })), ) }) .catch((err) => { console.error('Failed to load suggested districts:', err) if (!cancelled) setSuggestions([]) // degrade gracefully — just show search box }) .finally(() => { if (!cancelled) setLoadingSugg(false) }) return () => { cancelled = true } }, [state]) // ——— Live search as user types (debounced by React's natural event batching) ——— useEffect(() => { if (!query.trim()) { setSearchResults([]); return } const timer = setTimeout(async () => { setLoadingSearch(true) try { const data = await searchDistricts(query, state) setSearchResults(data || []) } catch (err) { console.error('District search failed:', err) setSearchResults([]) } finally { setLoadingSearch(false) } }, 300) return () => clearTimeout(timer) }, [query, state]) // ——— Render a district card (used for both suggestions and search results) ——— const renderDistrictCard = (dist) => ( ) return (
Search by district name, or try one of the suggested districts with the highest arrest counts.
{/* "Interesting" suggestions */} {!query && (Loading suggestions…
) : suggestions.length > 0 ? (No districts with reported arrests found in this state. Try searching by name below.
)}Searching…
) : searchResults.length > 0 ? (No districts found matching "{query}".
)} {/* Back to suggestions */} {searchResults.length > 0 && ( )}