TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1"use client";2import Link from "next/link";3import { useState } from "react";4import type { ListingCard as Card } from "@/lib/types";5import { fmtPrice, fmtArea, fmtInt } from "@/lib/format";6import { CATEGORY_LABEL } from "@/lib/normalize";7import { useCompare, useFavorites } from "@/lib/favorites";8import { IconHeart, IconCompare } from "./icons";9import { VerdictPill } from "./verdict-pill";1011export function listingHref(c: { uid: string }) { return `/annonce/${encodeURIComponent(c.uid)}`; }1213export function ListingCardView({ c, compact = false, priority = false }: { c: Card; compact?: boolean; priority?: boolean }) {14 const fav = useFavorites(); const cmp = useCompare();15 const [imgOk, setImgOk] = useState(true);16 const rent = c.market === "rent";17 const sub = [c.typeLabel && !rent ? CATEGORY_LABEL[c.category] ?? c.typeLabel : c.typeLabel ?? CATEGORY_LABEL[c.category], c.bedrooms != null ? `${fmtInt(c.bedrooms)} ch.` : null, c.bathrooms != null ? `${fmtInt(c.bathrooms)} sdb` : null, c.areaSqft ? fmtArea(c.areaSqft) : null].filter(Boolean).join(" · ");18 return (19 <article className={`tv-card overflow-hidden group relative ${compact ? "flex gap-3" : ""}`}>20 <Link href={listingHref(c)} className={`block relative bg-surface-2 ${compact ? "w-28 h-24 shrink-0" : "aspect-[4/3]"}`} aria-label={`${c.address ?? "Propriété"}, ${c.city ?? ""}`}>21 {c.image && imgOk ? (22 // eslint-disable-next-line @next/next/no-img-element23 <img src={c.image} alt="" className="absolute inset-0 w-full h-full object-cover transition group-hover:scale-[1.02]" loading={priority ? "eager" : "lazy"} referrerPolicy="no-referrer" onError={() => setImgOk(false)} />24 ) : (25 <div className="absolute inset-0 grid place-items-center text-ink-3 text-xs">Photo non disponible</div>26 )}27 {!compact && c.imageCount > 1 && <span className="absolute bottom-2 right-2 tv-badge bg-black/55 text-white backdrop-blur">{c.imageCount} photos</span>}28 {!compact && <span className="absolute top-2 left-2 tv-badge bg-white/90 text-ink-2">{rent ? "À louer" : "À vendre"}</span>}29 </Link>30 {!compact && (31 <div className="absolute top-2 right-2 flex gap-1">32 <button type="button" onClick={() => cmp.toggle(c.uid)} className={`p-1.5 rounded-full bg-white/90 hover:bg-white shadow ${cmp.has(c.uid) ? "text-brand" : "text-ink-2"}`} aria-pressed={cmp.has(c.uid)} aria-label="Comparer" title="Ajouter au comparateur"><IconCompare width={16} height={16} /></button>33 <button type="button" onClick={() => fav.toggle(c.uid)} className={`p-1.5 rounded-full bg-white/90 hover:bg-white shadow ${fav.has(c.uid) ? "text-brand" : "text-ink-2"}`} aria-pressed={fav.has(c.uid)} aria-label={fav.has(c.uid) ? "Retirer des favoris" : "Ajouter aux favoris"}><IconHeart width={16} height={16} filled={fav.has(c.uid)} /></button>34 </div>35 )}36 <div className={`p-3 ${compact ? "min-w-0 flex-1" : ""}`}>37 <div className="flex flex-wrap items-center justify-between gap-x-2 gap-y-1">38 <p className={`tv-price text-ink whitespace-nowrap ${compact ? "text-base" : "text-[17px] sm:text-lg"}`}>{fmtPrice(c.price, { rent })}</p>39 {!compact && <VerdictPill verdict={c.verdict} ratio={c.ratio} confidence={c.confidence} small />}40 </div>41 <Link href={listingHref(c)} className="block mt-0.5 text-sm font-medium text-ink truncate hover:underline">{c.address ?? "Adresse non précisée"}</Link>42 <p className="text-xs text-ink-2 truncate">{c.city}{c.region && c.region !== c.city ? `, ${c.region}` : ""}</p>43 <p className="mt-1 text-xs text-ink-3 truncate">{sub}</p>44 </div>45 </article>46 );47}48