Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 47.5%
HTML 27.9%
TypeScript 15.5%
CSS 7.2%
JavaScript 2%
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/useFicheData.ts : orchestration des données secondaires de la fiche.5// Un seul endroit charge les API annexes (au lieu d'un fetch par bloc) :6// · groupe CRITIQUE (au montage) : inondation, qualité de l'air — nécessaires7// à « En bref » et au premier écran ;8// · groupe DIFFÉRÉ (quand l'utilisateur approche la carte, ou après 2,5 s) :9// commerces/transport, essence, Hydro-Québec (cache seulement).10// Le prix (Vrai-Prix), le quartier, les POI et l'historique de prix arrivent11// déjà avec l'annonce (/api/listings/{uid}). Le financement a son propre12// cycle (calculateur interactif). Chaque ressource porte son statut13// (loading / ok / error / na) : squelettes et états vides propres.14// -----------------------------------------------------------------------------15import { useCallback, useEffect, useRef, useState } from "react";16import {17 AirNearby, CommercesNearby, GazNearby, HydroEstimate, Inondation, Listing,18 fetchAir, fetchCommerces, fetchGaz, fetchHydro, fetchInondation,19} from "../api";2021export type Res<T> =22 | { status: "idle" | "loading" }23 | { status: "ok"; data: T }24 | { status: "error"; error: string }25 | { status: "na" }; // non applicable (pas de coordonnées, pas d'adresse…)2627export interface FicheData {28 inondation: Res<Inondation>;29 air: Res<AirNearby>;30 commerces: Res<CommercesNearby>;31 gaz: Res<GazNearby>;32 hydro: Res<HydroEstimate>;33 /** déclenche le groupe différé (appelé par la sentinelle de la carte) */34 wake: () => void;35 /** relance une ressource en erreur */36 retry: (key: keyof Omit<FicheData, "wake" | "retry">) => void;37}3839type Key = keyof Omit<FicheData, "wake" | "retry">;40const LOADING = { status: "loading" } as const;41const NA = { status: "na" } as const;42const CRITIQUE: Key[] = ["inondation", "air"];43const DIFFERE: Key[] = ["commerces", "gaz", "hydro"];4445export function dataOf<T>(r: Res<T>): T | null {46 return r.status === "ok" ? r.data : null;47}4849export default function useFicheData(l: Listing | null): FicheData {50 const [state, setState] = useState<Record<Key, Res<unknown>>>({51 inondation: LOADING, air: LOADING,52 commerces: { status: "idle" }, gaz: { status: "idle" }, hydro: { status: "idle" },53 });54 const [awake, setAwake] = useState(false);55 const uidRef = useRef<string | null>(null);56 const started = useRef<Set<Key>>(new Set());5758 const set = useCallback((k: Key, r: Res<unknown>) =>59 setState((s) => ({ ...s, [k]: r })), []);6061 const load = useCallback((k: Key, p: (() => Promise<unknown>) | null) => {62 if (!p) { set(k, NA); return; }63 const uid = uidRef.current;64 set(k, LOADING);65 p().then((d) => { if (uidRef.current === uid) set(k, { status: "ok", data: d }); })66 .catch((e: unknown) => {67 if (uidRef.current !== uid) return;68 // 404 = base absente ou hors couverture → état « non applicable » (pas une erreur)69 const msg = String(e);70 set(k, /API 404/.test(msg) ? NA : { status: "error", error: msg });71 });72 }, [set]);7374 const loaders = useCallback((k: Key): (() => Promise<unknown>) | null => {75 if (!l) return null;76 const geo = l.lat != null && l.lng != null;77 const lat = l.lat as number, lng = l.lng as number;78 switch (k) {79 case "inondation": return geo ? () => fetchInondation(lat, lng) : null;80 case "air": return geo ? () => fetchAir(lat, lng) : null;81 case "commerces": return geo ? () => fetchCommerces(lat, lng) : null;82 case "gaz": return geo ? () => fetchGaz(lat, lng, 60) : null;83 case "hydro": return (l.address || l.title)84 ? () => fetchHydro(l.address || l.title, { uid: l.uid, lat: l.lat, lng: l.lng }, false) : null;85 }86 }, [l]);8788 // groupe critique : dès que l'annonce est connue89 useEffect(() => {90 if (!l) return;91 uidRef.current = l.uid;92 started.current = new Set();93 setAwake(false);94 for (const k of CRITIQUE) load(k, loaders(k));95 for (const k of DIFFERE) set(k, { status: "idle" });96 const t = setTimeout(() => setAwake(true), 2500); // filet : réveil après 2,5 s97 return () => clearTimeout(t);98 }, [l, load, loaders, set]);99100 // groupe différé : au réveil (proximité de la carte ou délai)101 useEffect(() => {102 if (!l || !awake) return;103 for (const k of DIFFERE) {104 if (started.current.has(k)) continue;105 started.current.add(k);106 load(k, loaders(k));107 }108 }, [awake, l, load, loaders]);109110 const wake = useCallback(() => setAwake(true), []);111 const retry = useCallback((k: Key) => load(k, loaders(k)), [load, loaders]);112113 return { ...(state as unknown as Omit<FicheData, "wake" | "retry">), wake, retry };114}115