/** * React Hook for fetching country boundaries with institution counts from the Geo API * * This hook fetches country polygons for low-zoom map views where individual * institution points would be too dense to display meaningfully. * * API Endpoint: * - GET /api/geo/countries - Returns country polygons with institution counts */ import { useState, useEffect, useCallback } from 'react'; // API base URL const GEO_API_BASE = '/api/geo'; /** * Country data from the Geo API */ export interface CountryData { iso_a2: string; iso_a3: string | null; name: string; institution_count: number; centroid: [number | null, number | null]; // [lon, lat] area_km2: number | null; } /** * GeoJSON Feature for a country */ export interface CountryFeature { type: 'Feature'; id: string; geometry: GeoJSON.Geometry; properties: CountryData; } export interface CountriesGeoJSON { type: 'FeatureCollection'; features: CountryFeature[]; metadata: { count: number; total_institutions: number; countries_with_data: number; type_filter: string | null; simplified: boolean; }; } export interface UseGeoApiCountriesReturn { geojson: CountriesGeoJSON | null; isLoading: boolean; error: Error | null; refresh: (typeFilter?: string | string[]) => Promise; countriesWithData: CountryData[]; totalInstitutions: number; } /** * React hook to fetch country boundaries with institution counts * * @param typeFilter - Optional institution type filter (single type or array of types) * @returns Country GeoJSON with institution counts */ export function useGeoApiCountries(typeFilter?: string | string[]): UseGeoApiCountriesReturn { const [geojson, setGeojson] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const refresh = useCallback(async (filter?: string | string[]) => { const activeFilter = filter !== undefined ? filter : typeFilter; setIsLoading(true); setError(null); try { const url = new URL(`${GEO_API_BASE}/countries`, window.location.origin); url.searchParams.set('simplified', 'true'); url.searchParams.set('with_counts', 'true'); // Support single type or array of types if (activeFilter) { if (Array.isArray(activeFilter)) { // Pass multiple types as comma-separated url.searchParams.set('type', activeFilter.join(',')); } else { url.searchParams.set('type', activeFilter); } } console.log(`[GeoAPI Countries] Fetching from ${url.toString()}`); const response = await fetch(url.toString(), { headers: { 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`Geo API request failed: ${response.status} ${response.statusText}`); } const data: CountriesGeoJSON = await response.json(); console.log(`[GeoAPI Countries] Loaded ${data.features.length} countries, ${data.metadata.total_institutions} institutions`); setGeojson(data); } catch (err) { console.error('[GeoAPI Countries] Failed to load countries:', err); setError(err instanceof Error ? err : new Error('Failed to load country boundaries')); } finally { setIsLoading(false); } }, [typeFilter]); // Initial load useEffect(() => { refresh(); }, [refresh]); // Computed values const countriesWithData = geojson?.features .filter(f => f.properties.institution_count > 0) .map(f => f.properties) .sort((a, b) => b.institution_count - a.institution_count) || []; const totalInstitutions = geojson?.metadata.total_institutions || 0; return { geojson, isLoading, error, refresh, countriesWithData, totalInstitutions, }; } /** * Hook to get country detail with institution breakdown by type */ export function useGeoApiCountryDetail(isoA2: string | null) { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!isoA2) { setData(null); return; } async function fetchDetail() { setIsLoading(true); setError(null); try { const url = `${GEO_API_BASE}/countries/${isoA2}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to load country detail: ${response.status}`); } const result = await response.json(); setData(result); } catch (err) { setError(err instanceof Error ? err : new Error('Failed to load country detail')); } finally { setIsLoading(false); } } fetchDetail(); }, [isoA2]); return { data, isLoading, error }; }