spb/toit-ka Public
Toit-Ka — louer ou acheter un toit au Québec, un seul endroit (fusion Lou-Ka × Immo-Ka) — www.toit-ka.com
Python 40.2%
TypeScript 39%
CSS 20.2%
HTML 0.7%
1// -----------------------------------------------------------------------------2// Author: Simon-Pierre Boucher3// Contact: contact@spboucher.ai4// Project: Toit-Ka5// components/ListingCard.tsx : carte d'annonce (grille) — bi-univers.6// Le badge d'univers (Louer lime / Acheter rouge) garde la grille lisible7// quand les deux univers se croisent (recherche unifiée, favoris).8// -----------------------------------------------------------------------------9import { Link } from "react-router-dom";10import { Listing, fichePath, fmtArea, fmtPrice, sourceName } from "../api";11import { useAccount } from "../account";12import { Ico, IcoHeart } from "./Icons";1314export default function ListingCard({ l, showTx = false }:15 { l: Listing; showTx?: boolean }) {16 const img = l.images && l.images.length > 0 ? l.images[0] : null;17 const { favs, toggleFav } = useAccount();18 const fav = favs.has(l.uid);19 const louer = l.transaction_type === "louer";2021 const meta: { ico: string; txt: string }[] = [];22 if (l.bedrooms != null) meta.push({ ico: "bed", txt: `${l.bedrooms} ch.` });23 if (l.bathrooms != null) meta.push({ ico: "bath", txt: `${l.bathrooms} sdb` });24 const area = fmtArea(l.area_sqft);25 if (area) meta.push({ ico: "area", txt: area });26 if (louer && l.pets === "oui") meta.push({ ico: "paw", txt: "animaux ok" });27 if (louer && l.furnished === 1) meta.push({ ico: "sofa", txt: "meublé" });2829 const dispo = louer && l.availability_date30 ? (l.availability_date === "now" ? "Libre maintenant" : `Libre ${l.availability_date}`)31 : l.mls ? `MLS ${l.mls}` : "";3233 const price = l.price != null34 ? <>{l.price.toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $35 {louer && <em> /mois</em>}</>36 : <>{l.price_label || "Prix sur demande"}</>;3738 return (39 <Link to={fichePath(l)} className="card">40 <div className="card-img">41 {img ? (42 <img src={img} alt={l.title || l.address} loading="lazy" />43 ) : (44 <div className="noimg"><Ico name="home" size={34} /></div>45 )}46 {showTx47 ? <span className={`badge tx-${l.transaction_type}`}>{louer ? "À louer" : "À vendre"}</span>48 : l.type && <span className="badge type">{l.type}</span>}49 {l.images.length > 1 && (50 <span className="badge right"><Ico name="camera" size={12} /> {l.images.length}</span>51 )}52 <button53 className={`fav-btn ${fav ? "on" : ""}`}54 aria-label={fav ? "Retirer des favoris" : "Ajouter aux favoris"}55 aria-pressed={fav}56 onClick={(e) => { e.preventDefault(); e.stopPropagation(); toggleFav(l); }}57 >58 <IcoHeart size={16} filled={fav} />59 </button>60 </div>61 <div className="card-body">62 <div className="card-price">{price}</div>63 <div className="card-title">{l.address || l.title}</div>64 <div className="card-meta">65 {showTx && l.type && <span>{l.type}</span>}66 {showTx && l.type && (l.sector || l.city) && <span className="sep" />}67 {l.sector && <span>{l.sector}</span>}68 {l.sector && l.city && <span className="sep" />}69 {l.city && <span>{l.city}</span>}70 </div>71 {meta.length > 0 && (72 <div className="card-specs">73 {meta.map((m) => (74 <span key={m.ico}><Ico name={m.ico} size={13} /> {m.txt}</span>75 ))}76 </div>77 )}78 <div className="card-foot">79 <span className="source-tag">{sourceName(l.source)}</span>80 {dispo && <span className="avail">{dispo}</span>}81 </div>82 </div>83 </Link>84 );85}86