SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.0 KB · 95 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/PropertyAmenities.tsx : inclusions et commodités — grille compacte5//   d'items avec icône contextuelle (AmenityIco), confirmés (✓ données6//   structurées) puis mentionnés ; 8 visibles, « Voir les N commodités ».7// -----------------------------------------------------------------------------8import { useState } from "react";9import { Listing } from "../api";10import AmenityIco from "../components/AmenityIco";11import { IcoCheck } from "../components/Icons";12import { MoreButton, SectionCard, NBSP } from "./ui";1314/** Badges dérivés des détails structurés (inclusions confirmées ✓). */15export function badgesConfirmes(l: Listing): string[] {16  const d = l.details ?? {};17  const out: string[] = [];18  const inc = d.inclusions ?? {};19  if (inc.heating) out.push("Chauffage inclus");20  if (inc.electricity) out.push("Électricité incluse");21  if (inc.hot_water) out.push("Eau chaude incluse");22  if (inc.internet) out.push("Internet inclus");23  if (inc.cable) out.push("Câble inclus");24  const app = d.appliances ?? {};25  if (app.dishwasher) out.push("Lave-vaisselle");26  if (app.washer_dryer) out.push("Laveuse-sécheuse");27  if (app.fridge && app.stove) out.push("Électroménagers");28  if (d.ac) out.push("Climatisation");29  if (d.elevator) out.push("Ascenseur");30  if (d.balcony) out.push("Balcon");31  if (d.pool) out.push("Piscine");32  if (d.gym) out.push("Gym");33  if (d.laundry) out.push("Buanderie");34  if (d.storage) out.push("Rangement");35  if (d.parking?.available)36    out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : ""}${d.parking.included ? " inclus" : ""}`);37  if (l.furnished) out.push("Meublé");38  if (d.smoking === false) out.push("Non-fumeur");39  return out;40}4142/** Libellés anglais fréquents des sources → français ; sert aussi au dédoublonnage43 *  avec les inclusions confirmées (« Balcony » ≈ « Balcon »). */44const FR: [RegExp, string][] = [45  [/^balcony$/i, "Balcon"], [/^storage$/i, "Rangement"], [/^(in-?unit|onsite|on-site) laundry$/i, "Buanderie"],46  [/^laundry$/i, "Buanderie"], [/^dishwasher$/i, "Lave-vaisselle"], [/^hardwood floors?$/i, "Planchers de bois franc"],47  [/^high ceilings?$/i, "Plafonds hauts"], [/^parking$/i, "Stationnement"], [/^gym|fitness/i, "Gym"],48  [/^pool$/i, "Piscine"], [/^elevator$/i, "Ascenseur"], [/^furnished$/i, "Meublé"], [/^heating$/i, "Chauffage"],49  [/^air conditioning|a\/c$/i, "Climatisation"], [/^pets? (allowed|friendly)$/i, "Animaux acceptés"],50  [/^wheelchair access(ible)?$/i, "Accès fauteuil roulant"], [/^doorman$/i, "Portier"], [/^garage$/i, "Garage"],51];52const fr = (a: string) => { for (const [re, t] of FR) if (re.test(a.trim())) return t; return a; };53const norm = (s: string) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/s\b/g, "");5455export default function PropertyAmenities({ l }: { l: Listing }) {56  const [all, setAll] = useState(false);57  const confirmes = badgesConfirmes(l);58  const autres = Array.from(new Set((l.amenities ?? []).map(fr))).filter(59    (a) => !confirmes.some((b) => norm(b).includes(norm(a)) || norm(a).includes(norm(b))));60  const items = [61    ...confirmes.map((t) => ({ t, ok: true })),62    ...autres.map((t) => ({ t, ok: false })),63  ];64  const inc = l.details?.inclusions ?? {};65  const zeroFrais = inc.heating && inc.electricity && inc.hot_water;66  const LIMIT = 8;67  const shown = all ? items : items.slice(0, LIMIT);6869  return (70    <SectionCard id="inclusions" title="Inclusions et commodités"71                 sub={items.length ? `${confirmes.length} confirmée${confirmes.length > 1 ? "s" : ""} par la source${autres.length ? ` · ${autres.length} mentionnée${autres.length > 1 ? "s" : ""}` : ""}` : undefined}>72      {zeroFrais && <p className="lk-note good" style={{ marginTop: 0, marginBottom: 12 }}>Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais énergétiques cachés.</p>}73      {items.length === 0 ? (74        <p className="lk-fine">La source ne précise pas les inclusions.</p>75      ) : (76        <div className="lk-amen" role="list">77          {shown.map((it) => (78            <div className={`lk-amen-it ${it.ok ? "confirmed" : "unconfirmed"}`} role="listitem" key={it.t}79                 title={it.ok ? "Confirmé par les données structurées de la source" : "Mentionné par la source, sans confirmation structurée"}>80              <span className="lk-amen-ico" aria-hidden="true"><AmenityIco label={it.t} size={15} fallback={it.ok ? "check" : "spark"} /></span>81              <span className="lk-amen-txt">{it.t}</span>82              {it.ok && <IcoCheck size={14} className="lk-amen-conf" aria-label="confirmé" />}83            </div>84          ))}85        </div>86      )}87      {items.length > LIMIT && (88        <MoreButton onClick={() => setAll(!all)} expanded={all}>89          {all ? "Réduire" : `Voir les ${items.length} commodités`}90        </MoreButton>91      )}92    </SectionCard>93  );94}95