SPB Git

spb/ora-ka Public

Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)

Python 80% TypeScript 12.9% CSS 6.8%
4.8 KB · 180 lines typescript
Raw Blame History
1// -----------------------------------------------------------------------------2// Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// api.ts : client typé de l'API FastAPI5// -----------------------------------------------------------------------------67export interface Vehicle {8  uid: string;9  kind: string;10  source: string;11  external_id: string;12  url: string;13  title: string;14  make: string;15  model: string;16  trim: string;17  year: number | null;18  price: number | null;19  price_label: string;20  mileage_km: number | null;21  mileage_label: string;22  transmission: string;23  fuel: string;24  drivetrain: string;25  body_type: string;26  exterior_color: string;27  interior_color: string;28  engine: string;29  doors: number | null;30  seats: number | null;31  vin: string;32  stock_number: string;33  dealer_name: string;34  city: string;35  region: string;36  description: string;37  features: string[];38  details: Record<string, unknown>;39  images: string[];40  carfax_url: string;41  first_seen: number;42  updated_at: number;43  active: number;44}4546export interface VehicleDetail extends Vehicle {47  price_history: { ts: number; price: number | null }[];48  similar: Vehicle[];49}5051export interface Facets {52  makes: { make: string; n: number }[];53  models: { model: string; n: number }[];54  body_types: string[];55  fuels: string[];56  regions: { region: string; n: number }[];57  sources: { source: string; dealer_name: string; n: number }[];58  years: { y_min: number | null; y_max: number | null }[];59}6061export interface Source {62  id: string;63  name: string;64  url: string;65  listing_url?: string;66  city?: string;67  region?: string;68  platform?: string;69  status?: string;70  active_listings: number;71  last_sync: number | null;72}7374export interface Stats {75  total: number;76  sources: number;77  regions: number;78  avg_price: number | null;79  avg_km: number | null;80  avg_year: number | null;81  by_region: { region: string; n: number; avg_price: number | null }[];82  by_make: { make: string; n: number; avg_price: number | null }[];83  by_body: { body_type: string; n: number }[];84  price_drops: {85    uid: string; title: string; year: number | null; price: number;86    prev_price: number; images: string[]; dealer_name: string; city: string;87  }[];88  recent_syncs: {89    source: string; ts: number; found: number; added: number;90    updated: number; removed: number; ok: number; message: string;91  }[];92}9394export interface VehicleQuery {95  kind?: string;96  make?: string;97  model?: string;98  body_type?: string;99  fuel?: string;100  transmission?: string;101  drivetrain?: string;102  region?: string;103  source?: string;104  year_min?: number;105  year_max?: number;106  price_min?: number;107  price_max?: number;108  km_max?: number;109  q?: string;110  sort?: string;111  limit?: number;112  offset?: number;113}114115// Préfixe de base (ex. "/auto") pour tous les appels API relatifs.116export const API_BASE = import.meta.env.BASE_URL.replace(/\/$/, "");117118async function get<T>(path: string): Promise<T> {119  const res = await fetch(API_BASE + path);120  if (!res.ok) throw new Error(`API ${res.status}`);121  return res.json();122}123124export function fetchVehicles(query: VehicleQuery) {125  const params = new URLSearchParams();126  for (const [k, v] of Object.entries(query)) {127    if (v !== undefined && v !== null && v !== "") params.set(k, String(v));128  }129  return get<{ total: number; count: number; vehicles: Vehicle[] }>(130    `/api/vehicles?${params.toString()}`131  );132}133134export function fetchVehicle(uid: string) {135  return get<VehicleDetail>(`/api/vehicles/${encodeURIComponent(uid)}`);136}137138export function fetchFacets(make?: string, kind?: string) {139  const params = new URLSearchParams();140  if (make) params.set("make", make);141  if (kind) params.set("kind", kind);142  const qs = params.toString();143  return get<Facets>(`/api/facets${qs ? `?${qs}` : ""}`);144}145146export function fetchSources() {147  return get<{ sources: Source[] }>("/api/sources");148}149150export function fetchStats() {151  return get<Stats>("/api/stats");152}153154// -- noms d'affichage des sources (peuplés depuis /api/sources) ---------------155const names: Record<string, string> = {};156157export function registerSourceNames(sources: Source[]) {158  for (const s of sources) names[s.id] = s.name;159}160161export function sourceName(id: string): string {162  return names[id] ?? id;163}164165// -- formatteurs ---------------------------------------------------------------166export const fmtPrice = (p: number | null | undefined) =>167  p == null ? "Prix sur demande" : `${Math.round(p).toLocaleString("fr-CA")} $`;168169export const fmtKm = (km: number | null | undefined) =>170  km == null ? "— km" : `${Math.round(km).toLocaleString("fr-CA")} km`;171172export const fmtDate = (ts: number | null | undefined) =>173  ts == null174    ? "—"175    : new Date(ts * 1000).toLocaleDateString("fr-CA", {176        day: "numeric",177        month: "short",178        year: "numeric",179      });180