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%
8.0 KB · 237 lines tsx
Raw Blame History
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