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%
6.4 KB · 112 lines tsx
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/PropertyQuickFacts.tsx : « La propriété » — grille compacte icône +5//   valeur + libellé (type, chambres, salles de bain, salles d'eau, superficie,6//   terrain, année, stationnement, MLS), rangées pratiques (agence, courtier,7//   en ligne depuis, synchronisé), puis TOUTES les caractéristiques publiées8//   par la source et le tableau des pièces en accordéons (aucune donnée9//   supprimée par rapport à la fiche v2).10// -----------------------------------------------------------------------------11import { ReactNode } from "react";12import { Listing, Room, fmtArea, fmtPrice, sourceName } from "../api";13import { Ico } from "../components/Icons";14import { Accordion, SectionCard, NBSP, relTime } from "./ui";1516// clés techniques de `details` jamais montrées dans « Caractéristiques »17const DETAIL_HIDDEN = new Set([18  "pieces", "price_from", "cover_thumb", "photo_captions", "img_audited",19  "needs_image_review", "postal_code", "region", "transaction",20  "prix_pi2", "prix_m2", "listing_origin_url", "listed_at",21]);22// clés déjà représentées dans la grille (évite le doublon dans le tableau)23const IN_GRID = new Set(["Type de propriété", "Année de construction", "Superficie habitable", "Superficie du terrain", "Stationnement (total)"]);2425export default function PropertyQuickFacts({ l }: { l: Listing }) {26  const d = l.details ?? {};27  const facts: { ico: ReactNode; v: string; l: string }[] = [];28  if (l.property_type) facts.push({ ico: <Ico name="home" size={17} />, v: l.property_type, l: "Type" });29  if (l.bedrooms != null) facts.push({ ico: <Ico name="bed" size={17} />, v: `${Math.round(l.bedrooms)}`, l: `Chambre${l.bedrooms > 1 ? "s" : ""}` });30  if (l.bathrooms != null) facts.push({ ico: <Ico name="bath" size={17} />, v: `${l.bathrooms}`, l: `Salle${l.bathrooms > 1 ? "s" : ""} de bain` });31  if (l.powder_rooms != null && l.powder_rooms > 0) facts.push({ ico: <Ico name="drop" size={17} />, v: `${l.powder_rooms}`, l: `Salle${l.powder_rooms > 1 ? "s" : ""} d'eau` });32  if (l.area_sqft != null) facts.push({ ico: <Ico name="area" size={17} />, v: fmtArea(l.area_sqft)!, l: "Superficie habitable" });33  if (l.lot_sqft != null) facts.push({ ico: <Ico name="land" size={17} />, v: fmtArea(l.lot_sqft)!, l: "Terrain" });34  if (l.year_built != null) facts.push({ ico: <Ico name="calendar" size={17} />, v: String(l.year_built), l: "Année de construction" });35  const park = d["Stationnement (total)"] ?? d["Stationnement"];36  if (park != null && String(park).trim()) facts.push({ ico: <Ico name="car" size={17} />, v: String(park), l: "Stationnement" });37  const pieces = d["Nb de pièces"];38  if (pieces != null && String(pieces).trim()) facts.push({ ico: <Ico name="layers" size={17} />, v: String(pieces), l: "Pièces" });39  if (l.mls) facts.push({ ico: <Ico name="tag" size={17} />, v: l.mls, l: "Nº Centris / MLS" });4041  const enLigne = l.days_on_market ?? (l.first_seen ? Math.max(0, Math.round((Date.now() / 1000 - l.first_seen) / 86400)) : null);42  const rows: [string, ReactNode][] = [["Agence / source", sourceName(l.source)]];43  if (l.agency && l.agency !== sourceName(l.source)) rows.push(["Bannière", l.agency]);44  if (l.broker_name) rows.push(["Courtier inscripteur", l.broker_name]);45  if (l.broker_phone) rows.push(["Téléphone", <a href={`tel:${l.broker_phone.replace(/\s/g, "")}`}>{l.broker_phone}</a>]);46  if (l.price_label && l.price != null && l.price_label.replace(/\s/g, "") !== fmtPrice(l.price).replace(/\s/g, ""))47    rows.push(["Prix affiché par la source", l.price_label]);48  if (enLigne != null) rows.push(["En ligne sur Immo-Ka", enLigne === 0 ? "depuis aujourd'hui" : `depuis ${enLigne}${NBSP}jour${enLigne > 1 ? "s" : ""}`]);49  const maj = relTime(l.updated_at);50  if (maj) rows.push(["Synchronisé", maj]);5152  const detEntries = Object.entries(d)53    .filter(([k, v]) => !DETAIL_HIDDEN.has(k) && !IN_GRID.has(k) && (typeof v === "string" || typeof v === "number") && String(v).trim());54  const rooms: Room[] = Array.isArray(d.pieces) ? (d.pieces as Room[]) : [];5556  if (facts.length === 0 && rows.length <= 1 && detEntries.length === 0 && rooms.length === 0) return null;5758  return (59    <SectionCard id="propriete" title="La propriété">60      {facts.length > 0 && (61        <div className="ik-facts" role="list">62          {facts.map((x) => (63            <div className="ik-fact" role="listitem" key={x.l + x.v}>64              <span className="ik-fact-ico" aria-hidden="true">{x.ico}</span>65              <span className="ik-fact-txt">66                <div className="ik-fact-v" title={x.v}>{x.v}</div>67                <div className="ik-fact-l">{x.l}</div>68              </span>69            </div>70          ))}71        </div>72      )}73      <div className="ik-facts-rows">74        {rows.map(([k, v]) => (75          <div className="ik-kv" key={k}><span className="k">{k}</span><span className="v">{v}</span></div>76        ))}77      </div>78      {detEntries.length > 0 && (79        <Accordion title="Toutes les caractéristiques publiées" meta={`${detEntries.length}`}>80          <div className="ik-facts-rows" style={{ marginTop: 0 }}>81            {detEntries.map(([k, v]) => (82              <div className="ik-kv" key={k}>83                <span className="k">{k}</span>84                {/^https?:\/\//.test(String(v))85                  ? <span className="v"><a href={String(v)} target="_blank" rel="noopener noreferrer">Ouvrir ↗</a></span>86                  : <span className="v">{String(v)}</span>}87              </div>88            ))}89          </div>90        </Accordion>91      )}92      {rooms.length > 0 && (93        <Accordion title="Pièces et dimensions" meta={`${rooms.length} pièce${rooms.length > 1 ? "s" : ""}`}>94          <div className="ik-table-wrap">95            <table className="ik-table">96              <thead><tr><th>Pièce</th><th>Niveau</th><th>Dimensions</th><th>Revêtement</th></tr></thead>97              <tbody>98                {rooms.map((r, i) => (99                  <tr key={i}>100                    <td>{r.nom || "—"}</td><td>{r.niveau || "—"}</td>101                    <td>{r.dimensions || "—"}</td><td>{r.revetement || "—"}</td>102                  </tr>103                ))}104              </tbody>105            </table>106          </div>107        </Accordion>108      )}109    </SectionCard>110  );111}112