Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)
Python 69.3%
TypeScript 16.7%
CSS 7.9%
JavaScript 4.7%
HTML 1.4%
1// ==============================================================================2// Author: Simon-Pierre Boucher <contact@spboucher.ai>3// File: favorites.tsx4// Desc: Favoris ♥ « Mon univers Ka » — le hub Groupe KA (groupe-ka.com) est5// le magasin central des favoris du groupe (aucun stockage local ni6// navigateur) : lecture/écriture via l'API locale /api/favorites qui7// relaie au hub. FavProvider charge la liste une fois ; FavButton =8// cœur sur les cartes de restos et la fiche détail. Non connecté →9// clic = « Se connecter avec KA ID ». Calqué sur foodka/favorites.tsx.10// ==============================================================================11import {12 ReactNode, createContext, useCallback, useContext,13 useEffect, useMemo, useState,14} from "react";15import { FavItem, Restaurant, fmtPrice, toggleFavorite } from "./api";1617interface FavState {18 ready: boolean; // premier chargement terminé19 connected: boolean; // session KA ID active20 ids: Set<string>; // uid des restos en favoris21 items: FavItem[];22 toggle: (item: FavItem) => void;23}2425const FavContext = createContext<FavState>({26 ready: false, connected: false, ids: new Set(), items: [], toggle: () => {},27});2829/** Restaurant -> item de favori du hub (nom, ville, prix médian, image, url). */30export function restoFavItem(r: Restaurant): FavItem {31 const ms = r.menu_summary;32 return {33 item_id: r.uid,34 title: r.name || "Restaurant",35 subtitle: [r.city, r.region].filter(Boolean).join(" · ")36 || r.address || "",37 price_label: ms?.price_median != null38 ? `${fmtPrice(ms.price_median)} · plat médian`39 : (r.price_range || ""),40 image_url: ms?.image || (r.images ?? [])[1] || (r.images ?? [])[0] || "",41 url: `https://www.resto-ka.com/resto/${encodeURIComponent(r.uid)}`,42 };43}4445export function FavProvider({ children }: { children: ReactNode }) {46 const [ready, setReady] = useState(false);47 const [connected, setConnected] = useState(false);48 const [items, setItems] = useState<FavItem[]>([]);4950 useEffect(() => {51 fetch("/api/favorites", { credentials: "same-origin" })52 .then(async (res) => {53 if (res.status === 401) return; // pas connecté : cœurs vides54 if (!res.ok) throw new Error(`API ${res.status}`);55 const data = (await res.json()) as { items?: FavItem[] };56 setConnected(true);57 setItems(data.items ?? []);58 })59 .catch(() => {}) // hub muet : cœurs vides60 .finally(() => setReady(true));61 }, []);6263 const toggle = useCallback((item: FavItem) => {64 if (!connected) {65 // non connecté : passage par la connexion KA ID (hub Groupe KA)66 window.location.assign("/api/auth/ka/login");67 return;68 }69 const prev = items;70 const on = !prev.some((i) => i.item_id === item.item_id);71 // optimiste : le hub confirme (sinon on rétablit)72 setItems(on ? [...prev, item]73 : prev.filter((i) => i.item_id !== item.item_id));74 toggleFavorite(on, item).catch(() => setItems(prev));75 }, [connected, items]);7677 const ids = useMemo(78 () => new Set(items.map((i) => i.item_id)), [items]);7980 return (81 <FavContext.Provider value={{ ready, connected, ids, items, toggle }}>82 {children}83 </FavContext.Provider>84 );85}8687export const useFav = () => useContext(FavContext);8889/** Cœur ♥ — cartes de resto (par défaut) et fiche détail (`big`). */90export function FavButton({ r, big = false }: { r: Restaurant; big?: boolean }) {91 const { connected, ids, toggle } = useFav();92 const on = ids.has(r.uid);93 const label = !connected94 ? "Se connecter avec KA ID pour ajouter aux favoris"95 : on96 ? "Retirer de mes favoris (Mon univers Ka)"97 : "Ajouter à mes favoris (Mon univers Ka)";98 return (99 <button100 type="button"101 className={`fav-btn${on ? " on" : ""}${big ? " big" : ""}`}102 aria-label={label}103 aria-pressed={on}104 title={label}105 onClick={(e) => {106 e.preventDefault(); // la carte entière est un lien107 e.stopPropagation();108 toggle(restoFavItem(r));109 }}110 >111 {on ? "♥" : "♡"}112 </button>113 );114}115