SPB Git forge

spb/vrai-prix

Public

Vrai-Prix — l'évaluation du vrai prix des propriétés résidentielles au Québec.

60commits 1branches 0releases
12.3 MBsize
maindefault branch
17 days agolast push
TypeScript 90.2% JavaScript 3.5% Python 3.4% CSS 1.9% HTML 0.6%
4.7 KB · 149 lines tsx
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Bouton ♥ « Mon univers Ka » — favoris unifiés du Groupe KA (KA ID).4 *   · Connecté   : bascule le favori au hub via POST /api/favorites/toggle5 *     (synchrone — le hub groupe-ka.com est le magasin central, rien en local).6 *   · Déconnecté : le clic envoie au SSO KA ID (retour sur la page courante).7 * L'état initial est lu via GET /api/favorites, partagé entre tous les boutons8 * de la page (une seule requête, mini-cache module 15 s, invalidé au toggle).9 * Utilisable dans un <Link> parent (stopPropagation + preventDefault).10 */11"use client";12import { useEffect, useState } from "react";13import { usePathname } from "next/navigation";14import { useLang } from "./LangContext";1516export type FavPayload = {17  item_id: string;18  title: string;19  subtitle?: string;20  price_label?: string;21  image_url?: string;22  url?: string;23};2425type FavState = { authed: boolean; ids: string[] };2627// -- mini-cache module : une requête partagée par page, TTL 15 s --------------28const TTL_MS = 15_000;29let shared: { at: number; p: Promise<FavState> } | null = null;3031function loadFavs(): Promise<FavState> {32  const now = Date.now();33  if (shared && now - shared.at < TTL_MS) return shared.p;34  const p: Promise<FavState> = fetch("/api/favorites")35    .then(async (r) => {36      if (r.status === 401) return { authed: false, ids: [] };37      if (!r.ok) throw new Error(String(r.status));38      const d = (await r.json()) as { favorites?: { item_id?: string }[] };39      return {40        authed: true,41        ids: (d.favorites ?? []).map((f) => String(f.item_id ?? "")),42      };43    })44    .catch(() => {45      shared = null; // erreur transitoire : jamais mise en cache46      return { authed: true, ids: [] };47    });48  shared = { at: now, p };49  return p;50}5152export default function FavButton({53  item,54  size = "sm",55  className = "",56}: {57  item: FavPayload;58  size?: "sm" | "lg";59  className?: string;60}) {61  const { lang } = useLang();62  const fr = lang === "fr";63  const path = usePathname();64  const [authed, setAuthed] = useState<boolean | null>(null);65  const [fav, setFav] = useState(false);66  const [busy, setBusy] = useState(false);67  const [flash, setFlash] = useState(false); // échec hub — bref signal visuel6869  useEffect(() => {70    let alive = true;71    loadFavs().then((st) => {72      if (!alive) return;73      setAuthed(st.authed);74      setFav(st.ids.includes(item.item_id));75    });76    return () => {77      alive = false;78    };79  }, [item.item_id]);8081  const onClick = async (e: React.MouseEvent) => {82    // le bouton vit parfois dans un <Link> (carte de résultat) : ne pas naviguer83    e.preventDefault();84    e.stopPropagation();85    if (busy) return;86    if (authed === false) {87      window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`;88      return;89    }90    setBusy(true);91    try {92      const r = await fetch("/api/favorites/toggle", {93        method: "POST",94        headers: { "Content-Type": "application/json" },95        body: JSON.stringify({ ...item, action: fav ? "remove" : "add" }),96      });97      if (r.status === 401) {98        window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`;99        return;100      }101      if (!r.ok) throw new Error(String(r.status));102      const d = (await r.json()) as { favorited?: boolean };103      setFav(!!d.favorited);104      shared = null; // les autres boutons reliront une liste fraîche105    } catch {106      setFlash(true);107      setTimeout(() => setFlash(false), 1200);108    } finally {109      setBusy(false);110    }111  };112113  const label = fav114    ? fr115      ? "Retirer de mes favoris"116      : "Remove from my favourites"117    : fr118      ? "Ajouter à mes favoris — Mon univers Ka"119      : "Add to my favourites — My Ka universe";120  const dims =121    size === "lg" ? "h-11 w-11 text-[19px]" : "h-8 w-8 text-[14px]";122123  return (124    <button125      type="button"126      onClick={onClick}127      disabled={busy}128      aria-pressed={fav}129      aria-label={label}130      title={131        flash132          ? fr133            ? "Hub Groupe KA injoignable — réessayez"134            : "Groupe KA hub unreachable — try again"135          : label136      }137      className={`flex shrink-0 items-center justify-center rounded-full border-[1.5px] border-ink leading-none transition-all ${dims} ${138        fav139          ? "bg-ink text-lime hover:bg-green-deep"140          : "bg-surface text-ink-3 hover:bg-lime-soft hover:text-ink"141      } ${busy ? "opacity-60" : ""} ${flash ? "!border-red-600 !text-red-600" : ""} ${className}`}142    >143      <span aria-hidden="true" className="translate-y-[-1px]">144        {fav ? "♥" : "♡"}145      </span>146    </button>147  );148}149