SPB Git

Repository Public

FavorisClient.tsx 6.8 KB 159 lines tsx

1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * « Mes favoris » — liste des propriétés mises en ♥ (Mon univers Ka).4 * Le hub Groupe KA est le magasin central : retirer un favori = POST synchrone5 * /api/favorites/toggle (action « remove »), la rangée disparaît si le hub6 * confirme. Design éditorial Vrai-Prix (papier/encre, cartes à ombre franche).7 */8"use client";9import { useState } from "react";10import Link from "next/link";11import { useLang } from "@/components/LangContext";12import type { FavItem } from "@/lib/hubfav";1314/** Lien local vers la fiche : chemin du hub si c'est une URL Vrai-Prix,15 *  sinon retombée sur /estimation/<item_id>. */16function ficheHref(f: FavItem): string {17  if (f.url) {18    try {19      const u = new URL(f.url);20      if (u.hostname.endsWith("vrai-prix.com")) return u.pathname + u.search;21    } catch {22      /* URL invalide — retombée ci-dessous */23    }24  }25  return `/estimation/${encodeURIComponent(f.item_id)}`;26}2728export default function FavorisClient({ initial }: { initial: FavItem[] | null }) {29  const { lang } = useLang();30  const fr = lang === "fr";31  const [items, setItems] = useState<FavItem[] | null>(initial);32  const [busy, setBusy] = useState<string | null>(null);33  const [err, setErr] = useState(false);3435  const remove = async (f: FavItem) => {36    if (busy) return;37    setBusy(f.item_id);38    setErr(false);39    try {40      const r = await fetch("/api/favorites/toggle", {41        method: "POST",42        headers: { "Content-Type": "application/json" },43        body: JSON.stringify({ ...f, action: "remove" }),44      });45      if (!r.ok) throw new Error(String(r.status));46      setItems((cur) => (cur ?? []).filter((x) => x.item_id !== f.item_id));47    } catch {48      setErr(true);49    } finally {50      setBusy(null);51    }52  };5354  return (55    <div className="py-10">56      {/* en-tête */}57      <span className="kicker">{fr ? "Mon univers Ka" : "My Ka universe"}</span>58      <h1 className="vp-display mt-2 text-[clamp(28px,5vw,44px)] font-bold uppercase leading-none tracking-[-0.03em]">59        {fr ? "Mes favoris" : "My favourites"}60      </h1>61      <p className="mt-3 max-w-2xl text-[15px] leading-relaxed text-ink-2">62        {fr63          ? "Les propriétés que vous avez mises en ♥ — gardées au hub Groupe KA, les mêmes sur toutes les plateformes du groupe."64          : "The properties you hearted — stored at the Groupe KA hub, the same across every platform of the group."}65      </p>6667      {err && (68        <p className="vp-mono mt-4 rounded-[8px] border-[1.5px] border-red-600 bg-surface px-4 py-3 text-[12px] uppercase tracking-[0.04em] text-red-700">69          {fr70            ? "Hub Groupe KA injoignable — le retrait n'a pas été enregistré, réessayez."71            : "Groupe KA hub unreachable — removal was not saved, try again."}72        </p>73      )}7475      {/* hub injoignable au chargement */}76      {items === null ? (77        <div className="vp-card mt-8 p-6 sm:p-8">78          <p className="vp-display text-[18px] font-bold">79            {fr ? "Hub Groupe KA injoignable" : "Groupe KA hub unreachable"}80          </p>81          <p className="mt-2 text-[14.5px] text-ink-2">82            {fr83              ? "Vos favoris vivent sur groupe-ka.com et n'ont pas pu être lus. Rechargez la page dans un instant."84              : "Your favourites live on groupe-ka.com and could not be read. Reload the page in a moment."}85          </p>86        </div>87      ) : items.length === 0 ? (88        /* aucun favori */89        <div className="vp-card mt-8 p-6 sm:p-8">90          <p className="vp-display text-[18px] font-bold">91            {fr ? "Aucun favori pour l'instant" : "No favourites yet"}92          </p>93          <p className="mt-2 max-w-xl text-[14.5px] text-ink-2">94            {fr95              ? "Touchez le ♥ d'une fiche d'estimation pour la garder ici — elle vous suivra sur toutes les plateformes du Groupe KA."96              : "Tap the ♥ on any estimate page to keep it here — it follows you across every Groupe KA platform."}97          </p>98          <Link href="/" className="btn btn-primary vp-display mt-5 inline-flex">99            {fr ? "Estimer une propriété" : "Estimate a property"}100          </Link>101        </div>102      ) : (103        <>104          <p className="vp-mono mt-8 text-[11px] font-bold uppercase tracking-[0.08em] text-ink-3">105            {items.length}{" "}106            {fr107              ? `propriété${items.length > 1 ? "s" : ""} en favoris`108              : `favourite propert${items.length > 1 ? "ies" : "y"}`}109          </p>110          <ul className="mt-3 grid gap-4 sm:grid-cols-2">111            {items.map((f, i) => (112              <li113                key={f.item_id}114                className="vp-card vp-card-hover rise relative p-5"115                style={{ animationDelay: `${i * 60}ms` }}116              >117                <Link href={ficheHref(f)} className="block pr-10">118                  <p className="vp-mono text-[10px] font-bold text-green">119                    {String(i + 1).padStart(2, "0")}120                  </p>121                  <p className="vp-display mt-0.5 text-[16px] font-bold uppercase leading-tight">122                    {f.title}123                  </p>124                  {f.subtitle && (125                    <p className="vp-mono mt-0.5 text-[10.5px] uppercase tracking-[0.05em] text-ink-3">126                      {f.subtitle}127                    </p>128                  )}129                  <div className="mt-3 flex items-end justify-between gap-3 border-t-[1.5px] border-dashed border-[rgba(20,24,20,0.14)] pt-3">130                    <p className="vp-display text-[20px] font-bold tracking-[-0.02em]">131                      {f.price_label || "—"}132                    </p>133                    <span className="vp-mono text-[11px] font-bold uppercase tracking-[0.04em] text-green">134                      {fr ? "Voir la fiche →" : "View →"}135                    </span>136                  </div>137                </Link>138                {/* retirer (♥ plein — même geste que sur la fiche) */}139                <button140                  type="button"141                  onClick={() => remove(f)}142                  disabled={busy === f.item_id}143                  aria-label={fr ? "Retirer de mes favoris" : "Remove from my favourites"}144                  title={fr ? "Retirer de mes favoris" : "Remove from my favourites"}145                  className={`absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-full border-[1.5px] border-ink bg-ink text-[14px] leading-none text-lime transition-all hover:bg-green-deep ${146                    busy === f.item_id ? "opacity-60" : ""147                  }`}148                >149                  <span aria-hidden="true" className="translate-y-[-1px]">♥</span>150                </button>151              </li>152            ))}153          </ul>154        </>155      )}156    </div>157  );158}159
Files main
Menu

Theme