SPB Git forge

spb/food-ka

Public

Food-Ka — agrégateur de produits d'épicerie du Québec — www.food-ka.com

55commits 1branches 0releases
10.2 MBsize
maindefault branch
9 days agolast push
Python 53.9% TypeScript 24% CSS 14.9% JavaScript 5.8% HTML 1.4%
4.8 KB · 134 lines tsx
Raw Blame History
1// -----------------------------------------------------------------------------2// Food-Ka — Agrégateur de produits d'épicerie (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// favorites.tsx : favoris ♥ « Mon univers Ka » — le hub Groupe KA est le5//   magasin central des favoris du groupe (aucun stockage navigateur) :6//   lecture/écriture via l'API locale /api/favorites qui relaie au hub.7//   FavProvider charge la liste une fois ; FavButton = cœur sur les cartes8//   produit et la fiche. Non connecté → redirection vers la connexion KA ID9//   avec retour à la page courante.10// -----------------------------------------------------------------------------11import {12  ReactNode, createContext, useCallback, useContext,13  useEffect, useMemo, useState,14} from "react";15import { Product, fmtPrice, sourceName } from "./api";1617/** Item de favori tel que poussé au hub Groupe KA. */18export interface FavItem {19  item_id: string;20  title: string;21  subtitle?: string;22  price_label?: string;23  image_url?: string;24  url?: string;25}2627interface FavState {28  ready: boolean;              // premier chargement terminé29  connected: boolean;          // session KA ID active30  ids: Set<string>;            // uid des produits en favoris31  items: FavItem[];32  toggle: (item: FavItem) => void;33}3435const FavContext = createContext<FavState>({36  ready: false, connected: false, ids: new Set(), items: [], toggle: () => {},37});3839/** Produit -> item de favori du hub (titre, bannière · format, prix, image). */40export function productFavItem(p: Product): FavItem {41  let price = fmtPrice(p.price, p.price_label);42  if (p.on_sale && p.regular_price != null)43    price += ` (rég. ${fmtPrice(p.regular_price)})`;44  return {45    item_id: p.uid,46    title: p.name || "Produit",47    subtitle: [sourceName(p.source), p.size_label || p.category]48      .filter(Boolean).join(" · "),49    price_label: price,50    image_url: p.images && p.images.length > 0 ? p.images[0] : "",51    url: `https://www.food-ka.com/produit/${encodeURIComponent(p.uid)}`,52  };53}5455export function FavProvider({ children }: { children: ReactNode }) {56  const [ready, setReady] = useState(false);57  const [connected, setConnected] = useState(false);58  const [items, setItems] = useState<FavItem[]>([]);5960  useEffect(() => {61    fetch("/api/favorites", { credentials: "same-origin" })62      .then(async (res) => {63        if (res.status === 401) return;          // pas connecté : cœurs vides64        if (!res.ok) throw new Error(`API ${res.status}`);65        const data = (await res.json()) as { items?: FavItem[] };66        setConnected(true);67        setItems(data.items ?? []);68      })69      .catch(() => {})                            // hub muet : cœurs vides70      .finally(() => setReady(true));71  }, []);7273  const toggle = useCallback((item: FavItem) => {74    if (!connected) {75      // non connecté : passage par la connexion KA ID, retour à la page76      const next = encodeURIComponent(77        window.location.pathname + window.location.search);78      window.location.assign(`/api/auth/ka/login?next=${next}`);79      return;80    }81    const prev = items;82    const on = !prev.some((i) => i.item_id === item.item_id);83    // optimiste : le hub confirme (sinon on rétablit)84    setItems(on ? [...prev, item]85                : prev.filter((i) => i.item_id !== item.item_id));86    fetch("/api/favorites/toggle", {87      method: "POST",88      credentials: "same-origin",89      headers: { "Content-Type": "application/json" },90      body: JSON.stringify({ on, item }),91    })92      .then((res) => { if (!res.ok) throw new Error(`API ${res.status}`); })93      .catch(() => setItems(prev));94  }, [connected, items]);9596  const ids = useMemo(97    () => new Set(items.map((i) => i.item_id)), [items]);9899  return (100    <FavContext.Provider value={{ ready, connected, ids, items, toggle }}>101      {children}102    </FavContext.Provider>103  );104}105106export const useFav = () => useContext(FavContext);107108/** Cœur ♥ — cartes produit (par défaut) et fiche produit (`big`). */109export function FavButton({ p, big = false }: { p: Product; big?: boolean }) {110  const { ids, toggle } = useFav();111  const on = ids.has(p.uid);112  const label = on113    ? "Retirer de mes favoris (Mon univers Ka)"114    : "Ajouter à mes favoris (Mon univers Ka)";115  return (116    <button117      type="button"118      className={`fav-btn${on ? " on" : ""}${big ? " big" : ""}`}119      aria-label={label}120      aria-pressed={on}121      title={label}122      onClick={(e) => {123        e.preventDefault();       // la carte entière est un lien124        e.stopPropagation();125        toggle(productFavItem(p));126      }}127    >128      <svg viewBox="0 0 24 24" aria-hidden="true">129        <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />130      </svg>131    </button>132  );133}134