spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import { finderQuery, type FinderFilterInput } from './finder-query';3import { ClientApiError } from './client-api';4import type {5 CountryQualityResponse,6 DistributionResponse,7 ExtremesResponse,8 FinderResponse,9 FramesResponse,10 IndicatorQualityResponse,11 MoversResponse,12 PeersResponse,13 PulseResponse,14 RaceResponse,15 RegionCompareResponse,16 RelatedResponse,17 ScatterResponse,18 StoryResponse,19 TrajectoryResponse,20 UpdatesResponse,21} from './types-analytics';2223/**24 * Browser-side fetches for the analytics API (same-origin `/api/v1/*`, rewritten to FastAPI). Every call takes an25 * optional AbortSignal so interactive views cancel superseded requests (slider scrubs, selector changes).26 */27async function get<T>(path: string, signal?: AbortSignal): Promise<T> {28 const res = await fetch(`/api/v1${path}`, { headers: { accept: 'application/json' }, signal });29 if (!res.ok) throw new ClientApiError(res.status, `API ${res.status}`);30 return (await res.json()) as T;31}3233function qs(query: Record<string, string | number | boolean | null | undefined>): string {34 const p = new URLSearchParams();35 for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null && v !== '') p.set(k, String(v));36 const s = p.toString();37 return s ? `?${s}` : '';38}3940export const clientAnalytics = {41 pulse: (signal?: AbortSignal) => get<PulseResponse>('/pulse', signal),42 movers: (q: { window?: number; category?: string; kind?: string; limit?: number; min_population?: number | null }, signal?: AbortSignal) => get<MoversResponse>(`/movers${qs(q)}`, signal),43 extremes: (q: { window?: string; topic?: string | null; min_population?: number | null }, signal?: AbortSignal) => get<ExtremesResponse>(`/extremes${qs(q)}`, signal),44 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<ScatterResponse>(`/scatter${qs(q)}`, signal),45 trajectory: (q: { x?: string; y?: string; size?: string | null; from?: number | null; to?: number | null; group?: string | null }, signal?: AbortSignal) => get<TrajectoryResponse>(`/trajectory${qs(q)}`, signal),46 finder: (filters: FinderFilterInput[], q: { mode?: 'and' | 'or'; region?: string | null; income?: string | null; sort?: string | null; limit?: number } = {}, signal?: AbortSignal) =>47 get<FinderResponse>(`/finder${finderQuery(filters, q)}`, signal),48 peers: (q: { y?: string; x?: string; year?: number | null; method?: string; log_x?: string | null }, signal?: AbortSignal) => get<PeersResponse>(`/peers${qs(q)}`, signal),49 indicatorRelated: (slug: string, limit = 12, signal?: AbortSignal) => get<RelatedResponse>(`/indicators/${encodeURIComponent(slug)}/related${qs({ limit })}`, signal),50 indicatorDistribution: (slug: string, q: { year?: number | null; highlight?: string | null; bins?: number }, signal?: AbortSignal) => get<DistributionResponse>(`/indicators/${encodeURIComponent(slug)}/distribution${qs(q)}`, signal),51 indicatorFrames: (slug: string, q: { from?: number | null; to?: number | null; group?: string | null } = {}, signal?: AbortSignal) => get<FramesResponse>(`/indicators/${encodeURIComponent(slug)}/frames${qs(q)}`, signal),52 indicatorQuality: (slug: string, signal?: AbortSignal) => get<IndicatorQualityResponse>(`/indicators/${encodeURIComponent(slug)}/quality`, signal),53 race: (indicator: string, q: { from?: number | null; to?: number | null; top?: number; group?: string | null } = {}, signal?: AbortSignal) => get<RaceResponse>(`/rankings/${encodeURIComponent(indicator)}/race${qs(q)}`, signal),54 regionsCompare: (a: string, b: string, indicators?: string[], signal?: AbortSignal) => get<RegionCompareResponse>(`/regions/compare${qs({ a, b, indicators: indicators?.join(',') })}`, signal),55 countryStory: (id: string, signal?: AbortSignal) => get<StoryResponse>(`/countries/${encodeURIComponent(id)}/story`, signal),56 countryQuality: (id: string, signal?: AbortSignal) => get<CountryQualityResponse>(`/countries/${encodeURIComponent(id)}/quality`, signal),57 updates: (signal?: AbortSignal) => get<UpdatesResponse>('/updates', signal),58};5960export type { FinderFilterInput };61