// Auteur : Simon-Pierre Boucher — contact@spboucher.ai /** * Cœur ♥ « Mon univers Ka » — favoris unifiés du Groupe KA. * Le hub groupe-ka.com est le magasin central : le composant lit la liste UNE * fois par page (magasin client partagé entre tous les cœurs) puis pousse * chaque toggle au serveur (/api/favorites/toggle → hub, synchrone). Sans * session KA ID, le clic envoie vers la connexion SSO (retour sur la page). */ "use client"; import { useEffect, useReducer, useState } from "react"; import { useLang } from "./LangContext"; export type FavHeartItem = { item_id: string; title: string; subtitle?: string; price_label?: string; image_url?: string; url?: string; }; // -- magasin client partagé (un seul GET /api/favorites par page) ------------- // undefined = pas encore chargé · null = pas de session KA ID · Set = favoris let favSet: Set | null | undefined; let inflight: Promise | null = null; const subs = new Set<() => void>(); const notify = () => subs.forEach((f) => f()); async function loadFavs(): Promise { try { const r = await fetch("/api/favorites", { cache: "no-store" }); if (r.status === 401) { favSet = null; return; } if (!r.ok) { favSet = null; return; } const body = (await r.json()) as { favorites?: { item_id?: string }[] }; favSet = new Set( (body.favorites ?? []).map((f) => String(f.item_id ?? "")).filter(Boolean), ); } catch { favSet = null; } } function ensureLoaded() { if (favSet === undefined && !inflight) { inflight = loadFavs().finally(() => { inflight = null; notify(); }); } } /** À appeler après un retrait effectué ailleurs (ex. page /favoris). */ export function favStoreRemove(itemId: string) { if (favSet instanceof Set) { favSet.delete(itemId); notify(); } } export default function FavHeart({ item, small = false, }: { item: FavHeartItem; small?: boolean; }) { const { lang } = useLang(); const fr = lang === "fr"; const [, force] = useReducer((x: number) => x + 1, 0); const [busy, setBusy] = useState(false); useEffect(() => { subs.add(force); ensureLoaded(); return () => { subs.delete(force); }; }, []); const fav = favSet instanceof Set && favSet.has(item.item_id); const onClick = async (e: React.MouseEvent) => { // le cœur vit parfois DANS un (cartes) — ne pas naviguer e.preventDefault(); e.stopPropagation(); if (busy) return; if (!(favSet instanceof Set)) { // pas de session KA ID → connexion SSO, retour sur la page courante const next = window.location.pathname + window.location.search; window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(next)}`; return; } const action = fav ? "remove" : "add"; setBusy(true); // optimiste — retour arrière si le hub refuse if (action === "add") favSet.add(item.item_id); else favSet.delete(item.item_id); notify(); try { const r = await fetch("/api/favorites/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, item }), }); if (!r.ok) throw new Error(String(r.status)); } catch { if (action === "add") favSet.delete(item.item_id); else favSet.add(item.item_id); notify(); } finally { setBusy(false); } }; const label = fav ? fr ? "Retirer de mes favoris (Mon univers Ka)" : "Remove from my favourites (My Ka universe)" : fr ? "Ajouter à mes favoris (Mon univers Ka)" : "Add to my favourites (My Ka universe)"; const size = small ? "h-9 w-9" : "h-11 w-11"; const glyph = small ? 15 : 18; return ( ); }