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 (
{/* Back button */}

{state === 'DC' ? "District of Columbia" : `Find a district in ${getStateName(state)}`}

Search by district name, or try one of the suggested districts with the highest arrest counts.

{/* "Interesting" suggestions */} {!query && (
Suggested districts — most arrests in 2021–22 {loadingSugg ? (

Loading suggestions…

) : suggestions.length > 0 ? (
{suggestions.map(renderDistrictCard)}
) : (

No districts with reported arrests found in this state. Try searching by name below.

)}
)} {/* Live search */}
setQuery(e.target.value)} style={{ width: '100%', padding: 'var(--space-2)', fontSize: '1rem', border: '1px solid var(--cv-rule)', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)' }} /> {query && (
{loadingSearch ? (

Searching…

) : searchResults.length > 0 ? (
{searchResults.slice(0, 20).map(renderDistrictCard)}
) : (

No districts found matching "{query}".

)} {/* Back to suggestions */} {searchResults.length > 0 && ( )}
)}
) } function getStateName(code) { const names = { AL:'Alabama',AK:'Alaska',AZ:'Arizona',AR:'Arkansas',CA:'California',CO:'Colorado',CT:'Connecticut',DE:'Delaware',DC:'District of Columbia',FL:'Florida',GA:'Georgia',HI:'Hawaii',ID:'Idaho',IL:'Illinois',IN:'Indiana',IA:'Iowa',KS:'Kansas',KY:'Kentucky',LA:'Louisiana',ME:'Maine',MD:'Maryland',MA:'Massachusetts',MI:'Michigan',MN:'Minnesota',MS:'Mississippi',MO:'Missouri',MT:'Montana',NE:'Nebraska',NV:'Nevada',NH:'New Hampshire',NJ:'New Jersey',NM:'New Mexico',NY:'New York',NC:'North Carolina',ND:'North Dakota',OH:'Ohio',OK:'Oklahoma',OR:'Oregon',PA:'Pennsylvania',RI:'Rhode Island',SC:'South Carolina',SD:'South Dakota',TN:'Tennessee',TX:'Texas',UT:'Utah',VT:'Vermont',VA:'Virginia',WA:'Washington',WV:'West Virginia',WI:'Wisconsin',WY:'Wyoming' } return names[code] || code }