// ----------------------------------------------------------------------------- // Lou-Ka — Agrégateur de logements à louer (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // fiche/PropertyAmenities.tsx : inclusions et commodités — grille compacte // d'items avec icône contextuelle (AmenityIco), confirmés (✓ données // structurées) puis mentionnés ; 8 visibles, « Voir les N commodités ». // ----------------------------------------------------------------------------- import { useState } from "react"; import { Listing } from "../api"; import AmenityIco from "../components/AmenityIco"; import { IcoCheck } from "../components/Icons"; import { MoreButton, SectionCard, NBSP } from "./ui"; /** Badges dérivés des détails structurés (inclusions confirmées ✓). */ export function badgesConfirmes(l: Listing): string[] { const d = l.details ?? {}; const out: string[] = []; const inc = d.inclusions ?? {}; if (inc.heating) out.push("Chauffage inclus"); if (inc.electricity) out.push("Électricité incluse"); if (inc.hot_water) out.push("Eau chaude incluse"); if (inc.internet) out.push("Internet inclus"); if (inc.cable) out.push("Câble inclus"); const app = d.appliances ?? {}; if (app.dishwasher) out.push("Lave-vaisselle"); if (app.washer_dryer) out.push("Laveuse-sécheuse"); if (app.fridge && app.stove) out.push("Électroménagers"); if (d.ac) out.push("Climatisation"); if (d.elevator) out.push("Ascenseur"); if (d.balcony) out.push("Balcon"); if (d.pool) out.push("Piscine"); if (d.gym) out.push("Gym"); if (d.laundry) out.push("Buanderie"); if (d.storage) out.push("Rangement"); if (d.parking?.available) out.push(`Stationnement${d.parking.type ? ` ${d.parking.type}` : ""}${d.parking.included ? " inclus" : ""}`); if (l.furnished) out.push("Meublé"); if (d.smoking === false) out.push("Non-fumeur"); return out; } /** Libellés anglais fréquents des sources → français ; sert aussi au dédoublonnage * avec les inclusions confirmées (« Balcony » ≈ « Balcon »). */ const FR: [RegExp, string][] = [ [/^balcony$/i, "Balcon"], [/^storage$/i, "Rangement"], [/^(in-?unit|onsite|on-site) laundry$/i, "Buanderie"], [/^laundry$/i, "Buanderie"], [/^dishwasher$/i, "Lave-vaisselle"], [/^hardwood floors?$/i, "Planchers de bois franc"], [/^high ceilings?$/i, "Plafonds hauts"], [/^parking$/i, "Stationnement"], [/^gym|fitness/i, "Gym"], [/^pool$/i, "Piscine"], [/^elevator$/i, "Ascenseur"], [/^furnished$/i, "Meublé"], [/^heating$/i, "Chauffage"], [/^air conditioning|a\/c$/i, "Climatisation"], [/^pets? (allowed|friendly)$/i, "Animaux acceptés"], [/^wheelchair access(ible)?$/i, "Accès fauteuil roulant"], [/^doorman$/i, "Portier"], [/^garage$/i, "Garage"], ]; const fr = (a: string) => { for (const [re, t] of FR) if (re.test(a.trim())) return t; return a; }; const norm = (s: string) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/s\b/g, ""); export default function PropertyAmenities({ l }: { l: Listing }) { const [all, setAll] = useState(false); const confirmes = badgesConfirmes(l); const autres = Array.from(new Set((l.amenities ?? []).map(fr))).filter( (a) => !confirmes.some((b) => norm(b).includes(norm(a)) || norm(a).includes(norm(b)))); const items = [ ...confirmes.map((t) => ({ t, ok: true })), ...autres.map((t) => ({ t, ok: false })), ]; const inc = l.details?.inclusions ?? {}; const zeroFrais = inc.heating && inc.electricity && inc.hot_water; const LIMIT = 8; const shown = all ? items : items.slice(0, LIMIT); return ( 1 ? "s" : ""} par la source${autres.length ? ` · ${autres.length} mentionnée${autres.length > 1 ? "s" : ""}` : ""}` : undefined}> {zeroFrais &&

Chauffage, électricité et eau chaude inclus — 0{NBSP}$ de frais énergétiques cachés.

} {items.length === 0 ? (

La source ne précise pas les inclusions.

) : (
{shown.map((it) => (
{it.t} {it.ok && }
))}
)} {items.length > LIMIT && ( setAll(!all)} expanded={all}> {all ? "Réduire" : `Voir les ${items.length} commodités`} )}
); }