SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%
4.8 KB · 115 lines typescript
Raw Blame History
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