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%
4.0 KB · 85 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/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