Food-Ka — agrégateur de produits d'épicerie du Québec — www.food-ka.com
Python 53.9%
TypeScript 24%
CSS 14.9%
JavaScript 5.8%
HTML 1.4%
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