SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.9 KB · 133 lines typescript
Raw Blame History
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (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) : juste valeur détaillée, registre des7//     loyers, inondation, qualité de l'air, coût réel — nécessaires au score,8//     à « En bref » et au premier écran ;9//   · groupe DIFFÉRÉ (quand l'utilisateur approche la carte, ou après 2,5 s) :10//     commerces/transport, essence, historique, recyclées, TAL, gestionnaire,11//     Hydro-Québec (cache seulement).12//   Chaque ressource porte son statut (loading / ok / error / empty) pour des13//   squelettes et des états vides propres — jamais de undefined à l'écran.14// -----------------------------------------------------------------------------15import { useCallback, useEffect, useRef, useState } from "react";16import {17  AirNearby, CommercesNearby, CoutReel, FairValueDetail, GazNearby, Gestionnaire,18  HistoriqueLouka, HydroEstimate, Inondation, Listing, RdlNearby, Recyclees, TalHistory,19  fetchAir, fetchCommerces, fetchCoutReel, fetchFairValue, fetchGaz, fetchGestionnaire,20  fetchHistorique, fetchHydro, fetchInondation, fetchRdl, fetchRecyclees, fetchTal,21} from "../api";2223export type Res<T> =24  | { status: "idle" | "loading" }25  | { status: "ok"; data: T }26  | { status: "error"; error: string }27  | { status: "na" };   // non applicable (pas de coordonnées, pas d'adresse…)2829export interface FicheData {30  fv: Res<FairValueDetail>;31  rdl: Res<RdlNearby>;32  inondation: Res<Inondation>;33  air: Res<AirNearby>;34  cout: Res<CoutReel>;35  commerces: Res<CommercesNearby>;36  gaz: Res<GazNearby>;37  historique: Res<HistoriqueLouka>;38  recyclees: Res<Recyclees>;39  tal: Res<TalHistory>;40  gestionnaire: Res<Gestionnaire>;41  hydro: Res<HydroEstimate>;42  /** déclenche le groupe différé (appelé par le sentinelle de la carte) */43  wake: () => void;44  /** relance une ressource en erreur */45  retry: (key: keyof Omit<FicheData, "wake" | "retry">) => void;46}4748type Key = keyof Omit<FicheData, "wake" | "retry">;49const LOADING = { status: "loading" } as const;50const NA = { status: "na" } as const;51const CRITIQUE: Key[] = ["fv", "rdl", "inondation", "air", "cout"];52const DIFFERE: Key[] = ["commerces", "gaz", "historique", "recyclees", "tal", "gestionnaire", "hydro"];5354export function dataOf<T>(r: Res<T>): T | null {55  return r.status === "ok" ? r.data : null;56}5758export default function useFicheData(l: Listing | null): FicheData {59  const [state, setState] = useState<Record<Key, Res<unknown>>>({60    fv: LOADING, rdl: LOADING, inondation: LOADING, air: LOADING, cout: LOADING,61    commerces: { status: "idle" }, gaz: { status: "idle" }, historique: { status: "idle" },62    recyclees: { status: "idle" }, tal: { status: "idle" }, gestionnaire: { status: "idle" },63    hydro: { status: "idle" },64  });65  const [awake, setAwake] = useState(false);66  const uidRef = useRef<string | null>(null);67  const started = useRef<Set<Key>>(new Set());6869  const set = useCallback((k: Key, r: Res<unknown>) =>70    setState((s) => ({ ...s, [k]: r })), []);7172  const load = useCallback((k: Key, p: (() => Promise<unknown>) | null) => {73    if (!p) { set(k, NA); return; }74    const uid = uidRef.current;75    set(k, LOADING);76    p().then((d) => { if (uidRef.current === uid) set(k, { status: "ok", data: d }); })77      .catch((e: unknown) => {78        if (uidRef.current !== uid) return;79        // 404 = base absente ou hors couverture → état « non applicable » (pas une erreur)80        const msg = String(e);81        set(k, /API 404/.test(msg) ? NA : { status: "error", error: msg });82      });83  }, [set]);8485  const loaders = useCallback((k: Key): (() => Promise<unknown>) | null => {86    if (!l) return null;87    const geo = l.lat != null && l.lng != null;88    const lat = l.lat as number, lng = l.lng as number;89    switch (k) {90      case "fv": return l.price != null ? () => fetchFairValue(l.uid) : null;91      case "rdl": return geo ? () => fetchRdl(lat, lng, 600, 300) : null;92      case "inondation": return geo ? () => fetchInondation(lat, lng) : null;93      case "air": return geo ? () => fetchAir(lat, lng) : null;94      case "cout": return () => fetchCoutReel(l.uid);95      case "commerces": return geo ? () => fetchCommerces(lat, lng) : null;96      case "gaz": return geo ? () => fetchGaz(lat, lng, 60) : null;97      case "historique": return () => fetchHistorique(l.uid);98      case "recyclees": return () => fetchRecyclees(l.uid);99      case "tal": return l.address ? () => fetchTal(l.address, l.city) : null;100      case "gestionnaire": return () => fetchGestionnaire(l.source);101      case "hydro": return (l.address || l.title)102        ? () => fetchHydro(l.address || l.title, { uid: l.uid, lat: l.lat, lng: l.lng }, false) : null;103    }104  }, [l]);105106  // groupe critique : dès que l'annonce est connue107  useEffect(() => {108    if (!l) return;109    uidRef.current = l.uid;110    started.current = new Set();111    setAwake(false);112    for (const k of CRITIQUE) load(k, loaders(k));113    for (const k of DIFFERE) set(k, { status: "idle" });114    const t = setTimeout(() => setAwake(true), 2500);   // filet : réveil après 2,5 s115    return () => clearTimeout(t);116  }, [l, load, loaders, set]);117118  // groupe différé : au réveil (proximité de la carte ou délai)119  useEffect(() => {120    if (!l || !awake) return;121    for (const k of DIFFERE) {122      if (started.current.has(k)) continue;123      started.current.add(k);124      load(k, loaders(k));125    }126  }, [awake, l, load, loaders]);127128  const wake = useCallback(() => setAwake(true), []);129  const retry = useCallback((k: Key) => load(k, loaders(k)), [load, loaders]);130131  return { ...(state as unknown as Omit<FicheData, "wake" | "retry">), wake, retry };132}133