'use client'; import { finderQuery, type FinderFilterInput } from './finder-query'; import { ClientApiError } from './client-api'; import type { CountryQualityResponse, DistributionResponse, ExtremesResponse, FinderResponse, FramesResponse, IndicatorQualityResponse, MoversResponse, PeersResponse, PulseResponse, RaceResponse, RegionCompareResponse, RelatedResponse, ScatterResponse, StoryResponse, TrajectoryResponse, UpdatesResponse, } from './types-analytics'; /** * Browser-side fetches for the analytics API (same-origin `/api/v1/*`, rewritten to FastAPI). Every call takes an * optional AbortSignal so interactive views cancel superseded requests (slider scrubs, selector changes). */ async function get(path: string, signal?: AbortSignal): Promise { const res = await fetch(`/api/v1${path}`, { headers: { accept: 'application/json' }, signal }); if (!res.ok) throw new ClientApiError(res.status, `API ${res.status}`); return (await res.json()) as T; } function qs(query: Record): string { const p = new URLSearchParams(); for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null && v !== '') p.set(k, String(v)); const s = p.toString(); return s ? `?${s}` : ''; } export const clientAnalytics = { pulse: (signal?: AbortSignal) => get('/pulse', signal), movers: (q: { window?: number; category?: string; kind?: string; limit?: number; min_population?: number | null }, signal?: AbortSignal) => get(`/movers${qs(q)}`, signal), extremes: (q: { window?: string; topic?: string | null; min_population?: number | null }, signal?: AbortSignal) => get(`/extremes${qs(q)}`, signal), scatter: (q: { x: string; y: string; size?: string | null; year?: number | null; group?: string | null; log_x?: string | null; log_y?: string | null }, signal?: AbortSignal) => get(`/scatter${qs(q)}`, signal), trajectory: (q: { x?: string; y?: string; size?: string | null; from?: number | null; to?: number | null; group?: string | null }, signal?: AbortSignal) => get(`/trajectory${qs(q)}`, signal), finder: (filters: FinderFilterInput[], q: { mode?: 'and' | 'or'; region?: string | null; income?: string | null; sort?: string | null; limit?: number } = {}, signal?: AbortSignal) => get(`/finder${finderQuery(filters, q)}`, signal), peers: (q: { y?: string; x?: string; year?: number | null; method?: string; log_x?: string | null }, signal?: AbortSignal) => get(`/peers${qs(q)}`, signal), indicatorRelated: (slug: string, limit = 12, signal?: AbortSignal) => get(`/indicators/${encodeURIComponent(slug)}/related${qs({ limit })}`, signal), indicatorDistribution: (slug: string, q: { year?: number | null; highlight?: string | null; bins?: number }, signal?: AbortSignal) => get(`/indicators/${encodeURIComponent(slug)}/distribution${qs(q)}`, signal), indicatorFrames: (slug: string, q: { from?: number | null; to?: number | null; group?: string | null } = {}, signal?: AbortSignal) => get(`/indicators/${encodeURIComponent(slug)}/frames${qs(q)}`, signal), indicatorQuality: (slug: string, signal?: AbortSignal) => get(`/indicators/${encodeURIComponent(slug)}/quality`, signal), race: (indicator: string, q: { from?: number | null; to?: number | null; top?: number; group?: string | null } = {}, signal?: AbortSignal) => get(`/rankings/${encodeURIComponent(indicator)}/race${qs(q)}`, signal), regionsCompare: (a: string, b: string, indicators?: string[], signal?: AbortSignal) => get(`/regions/compare${qs({ a, b, indicators: indicators?.join(',') })}`, signal), countryStory: (id: string, signal?: AbortSignal) => get(`/countries/${encodeURIComponent(id)}/story`, signal), countryQuality: (id: string, signal?: AbortSignal) => get(`/countries/${encodeURIComponent(id)}/quality`, signal), updates: (signal?: AbortSignal) => get('/updates', signal), }; export type { FinderFilterInput };