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// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/PropertyAmenities.tsx : inclusions et caractéristiques — grille5// compacte d'items avec icône contextuelle (AmenityIco), dédoublonnés ;6// 8 visibles, « Voir les N ». Les « Inclusions » / « Exclusions » textuelles7// de Centris (details) sont ajoutées en note quand elles existent.8// -----------------------------------------------------------------------------9import { useState } from "react";10import { Listing } from "../api";11import AmenityIco from "../components/AmenityIco";12import { Accordion, MoreButton, SectionCard } from "./ui";1314const norm = (s: string) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/\s+/g, " ").trim();1516export default function PropertyAmenities({ l }: { l: Listing }) {17 const [all, setAll] = useState(false);18 const seen = new Set<string>();19 const items = (l.features ?? []).map((f) => f.trim()).filter((f) => {20 const k = norm(f);21 if (!k || seen.has(k)) return false;22 seen.add(k); return true;23 });24 const d = l.details ?? {};25 const inclusions = typeof d["Inclusions"] === "string" ? String(d["Inclusions"]).trim() : "";26 const exclusions = typeof d["Exclusions"] === "string" ? String(d["Exclusions"]).trim() : "";27 const LIMIT = 8;28 const shown = all ? items : items.slice(0, LIMIT);2930 if (items.length === 0 && !inclusions && !exclusions) return null;3132 return (33 <SectionCard id="inclusions" title="Caractéristiques et inclusions"34 sub={items.length ? `${items.length} élément${items.length > 1 ? "s" : ""} publié${items.length > 1 ? "s" : ""} par la source` : undefined}>35 {items.length > 0 && (36 <div className="ik-amen" role="list">37 {shown.map((t) => (38 <div className="ik-amen-it" role="listitem" key={t}>39 <span className="ik-amen-ico" aria-hidden="true"><AmenityIco label={t} size={15} fallback="check" /></span>40 <span className="ik-amen-txt">{t}</span>41 </div>42 ))}43 </div>44 )}45 {items.length > LIMIT && (46 <MoreButton onClick={() => setAll(!all)} expanded={all}>47 {all ? "Réduire" : `Voir les ${items.length} caractéristiques`}48 </MoreButton>49 )}50 {(inclusions || exclusions) && (51 <Accordion title="Inclusions et exclusions de la vente" small defaultOpen={items.length === 0}>52 {inclusions && <p><b>Inclus :</b> {inclusions}</p>}53 {exclusions && <p><b>Exclus :</b> {exclusions}</p>}54 </Accordion>55 )}56 </SectionCard>57 );58}59