SPB Git forge

spb/valoplex

Public

ValoPlex — moteur d'évaluation spécialisé pour les plex au Québec, petit frère de Vrai-Prix.

11commits 1branches 0releases
2.4 MBsize
maindefault branch
20 days agolast push
TypeScript 91.9% Python 6% CSS 2.1%
4.6 KB · 160 lines tsx
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2/**3 * Cœur ♥ « Mon univers Ka » — favoris unifiés du Groupe KA.4 * Le hub groupe-ka.com est le magasin central : le composant lit la liste UNE5 * fois par page (magasin client partagé entre tous les cœurs) puis pousse6 * chaque toggle au serveur (/api/favorites/toggle → hub, synchrone). Sans7 * session KA ID, le clic envoie vers la connexion SSO (retour sur la page).8 */9"use client";10import { useEffect, useReducer, useState } from "react";11import { useLang } from "./LangContext";1213export type FavHeartItem = {14  item_id: string;15  title: string;16  subtitle?: string;17  price_label?: string;18  image_url?: string;19  url?: string;20};2122// -- magasin client partagé (un seul GET /api/favorites par page) -------------23// undefined = pas encore chargé · null = pas de session KA ID · Set = favoris24let favSet: Set<string> | null | undefined;25let inflight: Promise<void> | null = null;26const subs = new Set<() => void>();27const notify = () => subs.forEach((f) => f());2829async function loadFavs(): Promise<void> {30  try {31    const r = await fetch("/api/favorites", { cache: "no-store" });32    if (r.status === 401) {33      favSet = null;34      return;35    }36    if (!r.ok) {37      favSet = null;38      return;39    }40    const body = (await r.json()) as { favorites?: { item_id?: string }[] };41    favSet = new Set(42      (body.favorites ?? []).map((f) => String(f.item_id ?? "")).filter(Boolean),43    );44  } catch {45    favSet = null;46  }47}4849function ensureLoaded() {50  if (favSet === undefined && !inflight) {51    inflight = loadFavs().finally(() => {52      inflight = null;53      notify();54    });55  }56}5758/** À appeler après un retrait effectué ailleurs (ex. page /favoris). */59export function favStoreRemove(itemId: string) {60  if (favSet instanceof Set) {61    favSet.delete(itemId);62    notify();63  }64}6566export default function FavHeart({67  item,68  small = false,69}: {70  item: FavHeartItem;71  small?: boolean;72}) {73  const { lang } = useLang();74  const fr = lang === "fr";75  const [, force] = useReducer((x: number) => x + 1, 0);76  const [busy, setBusy] = useState(false);7778  useEffect(() => {79    subs.add(force);80    ensureLoaded();81    return () => {82      subs.delete(force);83    };84  }, []);8586  const fav = favSet instanceof Set && favSet.has(item.item_id);8788  const onClick = async (e: React.MouseEvent) => {89    // le cœur vit parfois DANS un <Link> (cartes) — ne pas naviguer90    e.preventDefault();91    e.stopPropagation();92    if (busy) return;93    if (!(favSet instanceof Set)) {94      // pas de session KA ID → connexion SSO, retour sur la page courante95      const next = window.location.pathname + window.location.search;96      window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(next)}`;97      return;98    }99    const action = fav ? "remove" : "add";100    setBusy(true);101    // optimiste — retour arrière si le hub refuse102    if (action === "add") favSet.add(item.item_id);103    else favSet.delete(item.item_id);104    notify();105    try {106      const r = await fetch("/api/favorites/toggle", {107        method: "POST",108        headers: { "Content-Type": "application/json" },109        body: JSON.stringify({ action, item }),110      });111      if (!r.ok) throw new Error(String(r.status));112    } catch {113      if (action === "add") favSet.delete(item.item_id);114      else favSet.add(item.item_id);115      notify();116    } finally {117      setBusy(false);118    }119  };120121  const label = fav122    ? fr123      ? "Retirer de mes favoris (Mon univers Ka)"124      : "Remove from my favourites (My Ka universe)"125    : fr126      ? "Ajouter à mes favoris (Mon univers Ka)"127      : "Add to my favourites (My Ka universe)";128  const size = small ? "h-9 w-9" : "h-11 w-11";129  const glyph = small ? 15 : 18;130131  return (132    <button133      type="button"134      onClick={onClick}135      aria-label={label}136      title={label}137      aria-pressed={fav}138      disabled={busy}139      className={`inline-flex ${size} shrink-0 cursor-pointer items-center justify-center rounded-full border-[1.5px] border-ink transition-all ${140        fav141          ? "rotate-[-3deg] bg-ink text-lime"142          : "bg-surface text-ink hover:bg-ink hover:text-lime"143      } ${busy ? "opacity-60" : ""}`}144    >145      <svg146        width={glyph}147        height={glyph}148        viewBox="0 0 24 24"149        aria-hidden="true"150        fill={fav ? "currentColor" : "none"}151        stroke="currentColor"152        strokeWidth="2.2"153        strokeLinejoin="round"154      >155        <path d="M12 20.7l-1.3-1.2C6 15.3 2.9 12.5 2.9 9 2.9 6.2 5.1 4 7.9 4c1.6 0 3.1.7 4.1 1.9C13 4.7 14.5 4 16.1 4c2.8 0 5 2.2 5 5 0 3.5-3.1 6.3-7.8 10.5L12 20.7z" />156      </svg>157    </button>158  );159}160