SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
3.6 KB · 48 lines tsx
Raw Blame History
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