Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
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