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// pages/Inspecteurs.tsx : annuaire complet des inspecteurs en bâtiment de la4// province (17 régions), filtrable par région et par texte — /api/inspecteurs.5// -----------------------------------------------------------------------------6import { useEffect, useMemo, useState } from "react";78type Inspecteur = {9 id: string;10 name: string;11 city: string;12 region: string;13 address: string;14 phone: string;15 website: string;16 rating: number | null;17 reviews: number | null;18 category: string;19};2021type Doc = {22 generated: string | null;23 total: number;24 count: number;25 regions: { region: string; n: number }[];26 movers: Inspecteur[];27};2829const telHref = (p: string) => "tel:+1" + p.replace(/\D/g, "").slice(-10);3031const host = (u: string) => {32 try {33 return new URL(u).hostname.replace(/^www\./, "");34 } catch {35 return u;36 }37};3839export default function InspecteursPage() {40 const [doc, setDoc] = useState<Doc | null>(null);41 const [error, setError] = useState<string | null>(null);42 const [q, setQ] = useState("");43 const [region, setRegion] = useState("");4445 useEffect(() => {46 fetch("/api/inspecteurs")47 .then((r) => {48 if (!r.ok) throw new Error("HTTP " + r.status);49 return r.json();50 })51 .then(setDoc)52 .catch((e) => setError(String(e)));53 }, []);5455 const filtered = useMemo(() => {56 if (!doc) return [];57 const needle = q.trim().toLowerCase();58 return doc.movers.filter(59 (m) =>60 (!region || m.region === region) &&61 (!needle || (m.name + " " + m.city).toLowerCase().includes(needle)),62 );63 }, [doc, q, region]);6465 const groups = useMemo(() => {66 const order = (doc?.regions ?? []).map((r) => r.region);67 const by: Record<string, Inspecteur[]> = {};68 for (const m of filtered) (by[m.region] ??= []).push(m);69 return order70 .filter((r) => by[r]?.length)71 .map((r) => ({ region: r, movers: by[r] }));72 }, [doc, filtered]);7374 const filtering = q.trim() !== "" || region !== "";7576 return (77 <div className="container sources">78 <span className="kicker">Annuaire — inspection de bâtiment</span>79 <h1>Inspecteurs en bâtiment du Québec</h1>80 <p className="sub">81 L'annuaire le plus complet des inspecteurs en bâtiment de la province :{" "}82 <b>{doc ? doc.total : "…"}</b> entreprises d'inspection (préachat,83 prévente, préréception) répertoriées dans les 17 régions84 administratives du Québec, avec téléphone, site web et note Google.85 Recensement continu par le Groupe KA86 {doc?.generated ? ` — mis à jour le ${doc.generated}` : ""}.87 </p>8889 {error && <div className="notice">⚠️ {error}</div>}90 {!doc && !error && <div className="notice">Chargement…</div>}9192 {doc && (93 <>94 <div95 style={{96 display: "flex",97 flexWrap: "wrap",98 gap: 10,99 alignItems: "center",100 margin: "6px 0 10px",101 }}102 >103 <input104 type="search"105 value={q}106 onChange={(e) => setQ(e.target.value)}107 placeholder="Rechercher un inspecteur ou une ville…"108 aria-label="Rechercher un inspecteur ou une ville"109 style={{110 flex: "1 1 240px",111 maxWidth: 380,112 padding: "10px 14px",113 border: "1.5px solid var(--line)",114 borderRadius: 10,115 font: "inherit",116 background: "var(--surface)",117 color: "inherit",118 }}119 />120 <select121 value={region}122 onChange={(e) => setRegion(e.target.value)}123 aria-label="Filtrer par région administrative"124 style={{125 padding: "10px 12px",126 border: "1.5px solid var(--line)",127 borderRadius: 10,128 font: "inherit",129 background: "var(--surface)",130 color: "inherit",131 maxWidth: "100%",132 }}133 >134 <option value="">Toutes les régions ({doc.total})</option>135 {doc.regions.map((r) => (136 <option key={r.region} value={r.region}>137 {r.region} ({r.n})138 </option>139 ))}140 </select>141 {filtering && (142 <span style={{ color: "var(--ink-2)", fontSize: 14 }}>143 {filtered.length} résultat{filtered.length > 1 ? "s" : ""}144 </span>145 )}146 </div>147148 {filtered.length === 0 && (149 <div className="notice">150 Aucun inspecteur ne correspond à cette recherche.151 </div>152 )}153154 {groups.map((g) => (155 <section key={g.region}>156 <h2 style={{ marginTop: 34 }}>157 {g.region}{" "}158 <span style={{ color: "var(--ink-2)", fontWeight: 600 }}>159 ({g.movers.length})160 </span>161 </h2>162 <div className="src-wrap" style={{ marginTop: 12 }}>163 <table className="src-table">164 <thead>165 <tr>166 <th>Entreprise</th>167 <th>Ville</th>168 <th>Téléphone</th>169 <th>Site web</th>170 <th>Note Google</th>171 </tr>172 </thead>173 <tbody>174 {g.movers.map((m) => (175 <tr key={m.id}>176 <td>177 <b>{m.name}</b>178 {m.category && (179 <div180 style={{ fontSize: 12, color: "var(--ink-2)" }}181 >182 {m.category}183 </div>184 )}185 </td>186 <td>{m.city}</td>187 <td style={{ whiteSpace: "nowrap" }}>188 {m.phone ? (189 <a href={telHref(m.phone)}>{m.phone}</a>190 ) : (191 "—"192 )}193 </td>194 <td>195 {m.website ? (196 <a197 href={m.website}198 target="_blank"199 rel="noopener noreferrer nofollow"200 >201 {host(m.website)}202 </a>203 ) : (204 "—"205 )}206 </td>207 <td style={{ whiteSpace: "nowrap" }}>208 {m.rating209 ? `★ ${m.rating.toFixed(1)} (${m.reviews ?? 0})`210 : "—"}211 </td>212 </tr>213 ))}214 </tbody>215 </table>216 </div>217 </section>218 ))}219220 <p221 className="sub"222 style={{ marginTop: 34, fontSize: 13, maxWidth: 720 }}223 >224 Annuaire informatif compilé à partir des fiches publiques Google.225 Avant de mandater un inspecteur en bâtiment, vérifiez son226 adhésion à une association reconnue (AIBQ, InterNACHI Québec…),227 sa formation, son assurance responsabilité professionnelle228 (erreurs et omissions) et exigez une convention de service ainsi229 qu'un rapport écrit conforme à une norme de pratique. Le Groupe KA230 n'a aucun lien d'affaires avec les entreprises listées.231 </p>232 </>233 )}234 </div>235 );236}237