import { useEffect, useMemo, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { IconCheck, IconSearch, IconX } from '@tabler/icons-react' import StyledModal, { StyledModalProps } from './StyledModal' import LoadingSpinner from './LoadingSpinner' import api from '~/lib/api' import { formatBytes } from '~/lib/util' import classNames from '~/lib/classNames' import { EXTRACT_DEFAULT_MAX_ZOOM, EXTRACT_MAX_ZOOM, EXTRACT_MIN_ZOOM, } from '../../constants/map_regions' import type { Country, CountryCode, CountryGroup, MapExtractPreflight, } from '../../types/maps' export type CountryPickerModalProps = Omit< StyledModalProps, | 'onConfirm' | 'open' | 'confirmText' | 'cancelText' | 'confirmVariant' | 'children' | 'title' | 'large' > & { onDownloadStart?: () => void } const CountryPickerModal: React.FC = ({ onDownloadStart, ...modalProps }) => { const [selected, setSelected] = useState>(new Set()) const [search, setSearch] = useState('') const [maxzoom, setMaxzoom] = useState(EXTRACT_DEFAULT_MAX_ZOOM) const [preflight, setPreflight] = useState(null) const [loading, setLoading] = useState(false) const [downloading, setDownloading] = useState(false) const [errorMessage, setErrorMessage] = useState(null) const preflightRequestIdRef = useRef(0) const { data: countries = [], isLoading: countriesLoading } = useQuery({ queryKey: ['maps-countries'], queryFn: () => api.listCountries(), staleTime: Infinity, }) const { data: groups = [] } = useQuery({ queryKey: ['maps-country-groups'], queryFn: () => api.listCountryGroups(), staleTime: Infinity, }) const grouped = useMemo(() => { const q = search.trim().toLowerCase() const filtered = q ? countries.filter( (c) => c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q) ) : countries const buckets: Record = {} for (const country of filtered) { if (!buckets[country.continent]) buckets[country.continent] = [] buckets[country.continent].push(country) } return Object.entries(buckets).sort(([a], [b]) => a.localeCompare(b)) }, [countries, search]) const selectedCountries = useMemo( () => countries.filter((c) => selected.has(c.code)), [countries, selected] ) function toggleCountry(code: CountryCode) { setSelected((prev) => { const next = new Set(prev) if (next.has(code)) next.delete(code) else next.add(code) return next }) } function toggleGroup(group: CountryGroup) { setSelected((prev) => { const next = new Set(prev) const allIn = group.countries.every((c) => next.has(c)) if (allIn) { group.countries.forEach((c) => next.delete(c)) } else { group.countries.forEach((c) => next.add(c)) } return next }) } function clearAll() { setSelected(new Set()) } // Auto-refresh the preflight whenever selection or maxzoom changes. Debounced // so rapid multi-select clicks collapse into a single CDN round-trip, and // stale-safe via requestId so an earlier slow response can't clobber a later one. useEffect(() => { if (selected.size === 0) { setPreflight(null) setErrorMessage(null) setLoading(false) preflightRequestIdRef.current++ return } const requestId = ++preflightRequestIdRef.current setLoading(true) setErrorMessage(null) const timer = setTimeout(async () => { try { const res = await api.extractMapPreflight({ countries: [...selected], maxzoom, }) if (requestId !== preflightRequestIdRef.current) return if (!res) throw new Error('Preflight returned no data') setPreflight(res) } catch (err: any) { if (requestId !== preflightRequestIdRef.current) return console.error('Preflight failed:', err) setErrorMessage(err?.message ?? 'Estimate failed') } finally { if (requestId === preflightRequestIdRef.current) setLoading(false) } }, 400) return () => clearTimeout(timer) }, [selected, maxzoom]) async function startDownload() { if (selected.size === 0) { setErrorMessage('Pick at least one country before downloading.') return } if (loading || !preflight) { setErrorMessage('Still estimating size — hold on a moment.') return } try { setDownloading(true) setErrorMessage(null) await api.extractMapRegion({ countries: [...selected], maxzoom, estimatedBytes: preflight?.bytes, }) onDownloadStart?.() } catch (err: any) { console.error('Extract dispatch failed:', err) setErrorMessage(err?.message ?? 'Download failed') } finally { setDownloading(false) } } return (
setSearch(e.target.value)} placeholder={`Search ${countries.length} countries...`} className="w-full pl-9 pr-3 py-2 rounded-md border border-border-default bg-surface-primary text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-desert-green" />
{selected.size > 0 && ( )}
{groups.length > 0 && (

Quick picks

{groups.map((group) => { const allIn = group.countries.length > 0 && group.countries.every((c) => selected.has(c)) return ( ) })}
)}
{countriesLoading ? (
) : grouped.length === 0 ? (

No countries match "{search}".

) : ( grouped.map(([continent, list]) => (
{continent}
    {list.map((country) => { const isSelected = selected.has(country.code) return (
  • ) })}
)) )}
{selectedCountries.length > 0 && (

{selectedCountries.length} selected

{selectedCountries.map((country) => ( {country.name} ))}
)}
setMaxzoom(parseInt(e.target.value, 10))} className="w-full accent-desert-green" disabled={loading || downloading} />
z{EXTRACT_MIN_ZOOM} (world) z{EXTRACT_MAX_ZOOM} (street)

Lower zoom = smaller file, less detail. Zoom 15 shows individual streets; zoom 10 shows city-level detail.

0} />
) } type PreflightStatusProps = { errorMessage: string | null loading: boolean preflight: MapExtractPreflight | null hasSelection: boolean } function PreflightStatus({ errorMessage, loading, preflight, hasSelection }: PreflightStatusProps) { if (errorMessage) { return

{errorMessage}

} if (loading) { return

Estimating size…

} if (preflight) { return (

{preflight.tiles.toLocaleString()} tiles, ~{formatBytes(preflight.bytes, 1)}{' '} (source build {preflight.source.date})

) } if (!hasSelection) { return

Pick at least one country to estimate size.

} return

Estimating size…

} export default CountryPickerModal