// Auteur : Simon-Pierre Boucher — contact@spboucher.ai /** * Bouton ♥ « Mon univers Ka » — favoris unifiés du Groupe KA (KA ID). * · Connecté : bascule le favori au hub via POST /api/favorites/toggle * (synchrone — le hub groupe-ka.com est le magasin central, rien en local). * · Déconnecté : le clic envoie au SSO KA ID (retour sur la page courante). * L'état initial est lu via GET /api/favorites, partagé entre tous les boutons * de la page (une seule requête, mini-cache module 15 s, invalidé au toggle). * Utilisable dans un parent (stopPropagation + preventDefault). */ "use client"; import { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import { useLang } from "./LangContext"; export type FavPayload = { item_id: string; title: string; subtitle?: string; price_label?: string; image_url?: string; url?: string; }; type FavState = { authed: boolean; ids: string[] }; // -- mini-cache module : une requête partagée par page, TTL 15 s -------------- const TTL_MS = 15_000; let shared: { at: number; p: Promise } | null = null; function loadFavs(): Promise { const now = Date.now(); if (shared && now - shared.at < TTL_MS) return shared.p; const p: Promise = fetch("/api/favorites") .then(async (r) => { if (r.status === 401) return { authed: false, ids: [] }; if (!r.ok) throw new Error(String(r.status)); const d = (await r.json()) as { favorites?: { item_id?: string }[] }; return { authed: true, ids: (d.favorites ?? []).map((f) => String(f.item_id ?? "")), }; }) .catch(() => { shared = null; // erreur transitoire : jamais mise en cache return { authed: true, ids: [] }; }); shared = { at: now, p }; return p; } export default function FavButton({ item, size = "sm", className = "", }: { item: FavPayload; size?: "sm" | "lg"; className?: string; }) { const { lang } = useLang(); const fr = lang === "fr"; const path = usePathname(); const [authed, setAuthed] = useState(null); const [fav, setFav] = useState(false); const [busy, setBusy] = useState(false); const [flash, setFlash] = useState(false); // échec hub — bref signal visuel useEffect(() => { let alive = true; loadFavs().then((st) => { if (!alive) return; setAuthed(st.authed); setFav(st.ids.includes(item.item_id)); }); return () => { alive = false; }; }, [item.item_id]); const onClick = async (e: React.MouseEvent) => { // le bouton vit parfois dans un (carte de résultat) : ne pas naviguer e.preventDefault(); e.stopPropagation(); if (busy) return; if (authed === false) { window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`; return; } setBusy(true); try { const r = await fetch("/api/favorites/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...item, action: fav ? "remove" : "add" }), }); if (r.status === 401) { window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(path || "/")}`; return; } if (!r.ok) throw new Error(String(r.status)); const d = (await r.json()) as { favorited?: boolean }; setFav(!!d.favorited); shared = null; // les autres boutons reliront une liste fraîche } catch { setFlash(true); setTimeout(() => setFlash(false), 1200); } finally { setBusy(false); } }; const label = fav ? fr ? "Retirer de mes favoris" : "Remove from my favourites" : fr ? "Ajouter à mes favoris — Mon univers Ka" : "Add to my favourites — My Ka universe"; const dims = size === "lg" ? "h-11 w-11 text-[19px]" : "h-8 w-8 text-[14px]"; return ( ); }