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%
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