Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// fiche/PropertyHero.tsx : héro de la fiche — prix + capsule « vs marché »,5// adresse (H1) + ville, ligne résumé, puis galerie, puis actions6// (favoris · partager · Voir l'annonce). Ordre DOM = ordre visuel.7// -----------------------------------------------------------------------------8import { Listing, fmtAvailability, fmtPrice, sourceName } from "../api";9import { IcoDoc, IcoExternal, IcoHeart, IcoShare } from "../components/Icons";10import PropertyGallery from "./PropertyGallery";11import { ComparaisonPrix, ligneResume } from "./synthese";12import { NBSP } from "./ui";1314export function PriceCapsule({ cmp, short = false }: { cmp: ComparaisonPrix | null; short?: boolean }) {15 if (!cmp) return null;16 return (17 <span className={`lk-capsule ${cmp.tone}`}>18 {short ? <b>{cmp.court}</b> : <><b>{cmp.label}</b><span aria-hidden="true">·</span>{cmp.court}</>}19 </span>20 );21}2223export default function PropertyHero({ l, cmp, fav, onFav, onShare, actionsRef }: {24 l: Listing; cmp: ComparaisonPrix | null; fav: boolean;25 onFav: () => void; onShare: () => void;26 actionsRef: React.RefObject<HTMLDivElement>;27}) {28 const dispo = fmtAvailability(l.availability_date);29 const resume = ligneResume(l, dispo);30 const titre = l.title || l.address;31 const adresse = l.address && l.address !== l.title ? l.address : l.title;32 const where = [l.sector, l.city].filter(Boolean).join(", ");33 const hist = (l.price_history ?? []).filter((h) => h.price != null);34 const baisse = hist.length >= 2 && hist[0].price! < hist[1].price! ? hist[1].price! : null;3536 return (37 <header className="lk-hero" aria-label="Résumé du logement">38 <div className="lk-hero-top">39 <div>40 <div className="lk-price">41 {fmtPrice(l.price, l.price_label)}42 {l.price != null && <small>/{NBSP}mois</small>}43 {baisse != null && <small style={{ textDecoration: "line-through", opacity: 0.7 }}>{fmtPrice(baisse)}</small>}44 </div>45 <div className="lk-price-row" style={{ marginTop: 8 }}>46 <PriceCapsule cmp={cmp} />47 {l.details?.price_from && <span className="lk-capsule neutral">à partir de</span>}48 {l.ka_reco && <span className="lk-capsule brand">Recommandé pour vous</span>}49 </div>50 </div>51 </div>52 <h1 className="lk-h1">53 {adresse || titre}54 {where && <span className="lk-city">{where}</span>}55 </h1>56 {resume.length > 0 && (57 <p className="lk-summary" aria-label="Résumé">58 {resume.map((r, i) => (59 <span key={r}>{i > 0 && <span className="sep" aria-hidden="true">· </span>}{r}</span>60 ))}61 </p>62 )}63 <PropertyGallery images={l.images ?? []} titre={titre} unitType={l.unit_type || undefined} />64 <div className="lk-actions" ref={actionsRef}>65 <button type="button" className={`lk-btn lk-btn-ghost lk-btn-icon ${fav ? "on" : ""}`}66 aria-pressed={fav} aria-label={fav ? "Retirer des favoris" : "Ajouter aux favoris"} onClick={onFav}>67 <IcoHeart size={19} filled={fav} />68 </button>69 <button type="button" className="lk-btn lk-btn-ghost lk-btn-icon" aria-label="Partager" onClick={onShare}>70 <IcoShare size={18} />71 </button>72 <a className="lk-btn lk-btn-ghost lk-btn-icon" aria-label="Télécharger la fiche PDF" title="Fiche PDF"73 href={`/api/listings/${encodeURIComponent(l.uid)}/pdf`} download>74 <IcoDoc size={18} />75 </a>76 <a className="lk-btn lk-btn-primary" href={`/passerelle/${encodeURIComponent(l.uid)}`}77 target="_blank" rel="noopener noreferrer">78 Voir l'annonce <IcoExternal size={16} />79 <span className="visually-hidden"> chez {sourceName(l.source)}</span>80 </a>81 </div>82 </header>83 );84}85