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/DesktopAside.tsx : colonne droite sticky (≥ 1024 px) — prix, capsule5// marché, adresse, CTA, PDF, Immo-Ka Score en miniature, constats clés,6// favoris / partage, courtier. Masquée sur mobile (display:none, pas de7// réordonnancement).8// -----------------------------------------------------------------------------9import { Listing, fmtPrice, sourceName } from "../api";10import { Ico, IcoHeart } from "../components/Icons";11import { ScoreRing } from "./ImmoKaScore";12import { PriceCapsule } from "./PropertyHero";13import { BriefList } from "./PropertySummary";14import { ComparaisonPrix, Constat, ImmoKaScore, estLocation } from "./synthese";15import { NBSP, relTime } from "./ui";1617export default function DesktopAside({ l, cmp, score, brief, fav, onFav, onShare }: {18 l: Listing; cmp: ComparaisonPrix | null; score: ImmoKaScore; brief: Constat[];19 fav: boolean; onFav: () => void; onShare: () => void;20}) {21 const maj = relTime(l.updated_at);22 return (23 <aside className="ik-aside" aria-label="Résumé et actions">24 <div className="ik-card ik-aside-card">25 <div>26 <div className="ik-price">{fmtPrice(l.price, l.price_label)}{estLocation(l) && l.price != null && <small>/{NBSP}mois</small>}</div>27 <div className="ik-price-row" style={{ marginTop: 8 }}><PriceCapsule cmp={cmp} /></div>28 </div>29 <div className="ik-aside-addr">{l.address || l.title}{l.city ? <><br />{[l.sector, l.city].filter(Boolean).join(", ")}</> : null}</div>30 <a className="ik-btn ik-btn-primary" href={l.url} target="_blank" rel="noopener noreferrer">31 Voir l'annonce chez {sourceName(l.source)} <Ico name="external" size={16} />32 </a>33 <div className="ik-actions">34 <button type="button" className={`ik-btn ik-btn-ghost ${fav ? "on" : ""}`} style={{ flex: 1 }} aria-pressed={fav} onClick={onFav}>35 <IcoHeart size={17} filled={fav} /> {fav ? "Favori" : "Favoris"}36 </button>37 <button type="button" className="ik-btn ik-btn-ghost" style={{ flex: 1 }} onClick={onShare}><Ico name="share" size={16} /> Partager</button>38 <a className="ik-btn ik-btn-ghost ik-btn-icon" aria-label="Fiche PDF" title="Fiche PDF" href={`/api/listings/${encodeURIComponent(l.uid)}/pdf`} download><Ico name="doc" size={17} /></a>39 </div>40 <button type="button" className="ik-btn ik-btn-ghost ik-ka-inline" onClick={() => window.dispatchEvent(new Event("ik:askka"))}>41 <Ico name="sparkles" size={16} /> Demander à Ka42 </button>43 {(score.value != null || brief.length > 0) && <hr className="ik-aside-sep" />}44 {score.value != null && (45 <div className="ik-aside-score">46 <ScoreRing value={score.value} partial={score.partial} size={56} />47 <div className="ik-aside-score-txt">48 <b>Immo-Ka Score · {score.label}</b>49 {score.partial ? "Score partiel" : `Emplacement ${score.emplacement} · Prix ${score.prix}`}50 </div>51 </div>52 )}53 {brief.length > 0 && <BriefList items={brief.slice(0, 4)} compact />}54 {(l.broker_name || l.broker_phone) && (55 <>56 <hr className="ik-aside-sep" />57 <div className="ik-aside-broker">58 <span className="ik-aside-broker-k">Courtier</span>59 {l.broker_name && <b>{l.broker_name}</b>}60 {l.broker_phone && <a href={`tel:${l.broker_phone.replace(/\s/g, "")}`}><Ico name="phone" size={14} /> {l.broker_phone}</a>}61 </div>62 </>63 )}64 {maj && <div className="ik-aside-meta">Synchronisé {maj} · {sourceName(l.source)}</div>}65 </div>66 </aside>67 );68}69