// Which services actually have a driver near the rider. // // The map only ever shows the selected service, so an empty map means both // "nobody is driving tonight" and "nobody is on a moto, though three cars are // a street away" — and the rider has no way to tell which. That ambiguity is // what leaves someone staring at a blank map instead of switching service and // getting a ride. // // This answers it once for every service, so the picker can show availability // and the request screen can point at a service that would actually work. import { useEffect, useRef, useState } from "react"; import { SERVICES, type ServiceId } from "@/constants/services"; import { fetchAPI } from "@/lib/fetch"; import { SEARCH_MAX_RADIUS_M } from "@/lib/use-nearby-drivers"; // Slower than the map's own poll: this drives a hint, not the pins, and it // scans every service rather than one. const POLL_MS = 15000; // ~110 m, so GPS jitter doesn't restart the request loop on every fix. const QUANTIZE = 1e3; const quantize = (v: number): number => Math.round(v * QUANTIZE) / QUANTIZE; export type ServiceAvailability = { /** Driver count per service, every service present (zeros included). */ counts: Record; /** Services with at least one driver in range. */ available: ServiceId[]; /** Radius the counts were measured over, in metres. */ radius: number; loading: boolean; }; const emptyCounts = (): Record => { const counts = {} as Record; for (const service of SERVICES) counts[service.id] = 0; return counts; }; export const useServiceAvailability = ( latitude: number | null, longitude: number | null, ): ServiceAvailability => { const [counts, setCounts] = useState>(emptyCounts); const [radius, setRadius] = useState(SEARCH_MAX_RADIUS_M); const [loading, setLoading] = useState(true); const latKey = latitude === null ? null : quantize(latitude); const lngKey = longitude === null ? null : quantize(longitude); const coords = useRef({ latitude, longitude }); coords.current = { latitude, longitude }; useEffect(() => { if (latKey === null || lngKey === null) return; let cancelled = false; let timer: ReturnType; const run = async () => { const { latitude: lat, longitude: lng } = coords.current; if (lat === null || lng === null) return; try { const res = await fetchAPI( `/(api)/driver/availability?lat=${lat}&lng=${lng}&radius=${SEARCH_MAX_RADIUS_M}`, ); if (cancelled) return; const data = res.data as { radius: number; counts: Record; }; // Merge onto a full set of zeros so a service the server didn't // mention still renders as "none nearby" rather than blank. const next = emptyCounts(); for (const service of SERVICES) { next[service.id] = data.counts?.[service.id] ?? 0; } setCounts(next); setRadius(data.radius ?? SEARCH_MAX_RADIUS_M); } catch { // Leave the last known counts in place — a dropped request shouldn't // flash "no drivers anywhere" at the rider. } finally { if (!cancelled) { setLoading(false); timer = setTimeout(run, POLL_MS); } } }; void run(); return () => { cancelled = true; clearTimeout(timer); }; }, [latKey, lngKey]); const available = SERVICES.map((s) => s.id).filter((id) => counts[id] > 0); return { counts, available, radius, loading }; };