Agrégateur de produits québécois — www.fabri-ka.com
Python 38.4%
HTML 30.3%
TypeScript 17.2%
CSS 11%
JavaScript 3.2%
1// ---------------------------------------------------------------------------2// Fabri-Ka — favoris « Mon univers Ka » (magasin central du hub Groupe KA)3// Aucun stockage local : l'état vient du hub (GET /api/favorites) et chaque ♥4// y est poussé (POST /api/favorites/toggle), avec mise à jour optimiste et5// retour arrière si le hub refuse. Non connecté -> le clic ♥ envoie vers la6// connexion KA ID puis ramène sur la page en cours.7// ---------------------------------------------------------------------------8import {9 createContext,10 useCallback,11 useContext,12 useEffect,13 useState,14 type MouseEvent,15 type ReactNode,16} from 'react'17import { useLocation } from 'react-router-dom'18import {19 fetchFavorites,20 productFavItem,21 toggleFavorite,22 type FavoriteItem,23 type Product,24} from './api'25import { IconHeart } from './components/Icons'2627interface FavoritesState {28 /** premier chargement effectué (connecté ou non) */29 ready: boolean30 /** session KA ID active (sinon, toggle => redirection connexion) */31 loggedIn: boolean32 /** item_id (uid produit) des favoris, tels que lus au hub */33 ids: ReadonlySet<string>34 toggle: (item: FavoriteItem) => void35}3637const FavoritesContext = createContext<FavoritesState>({38 ready: false,39 loggedIn: false,40 ids: new Set(),41 toggle: () => {},42})4344export function useFavorites(): FavoritesState {45 return useContext(FavoritesContext)46}4748export function FavoritesProvider({ children }: { children: ReactNode }) {49 const location = useLocation()50 const [ready, setReady] = useState(false)51 const [loggedIn, setLoggedIn] = useState(false)52 const [ids, setIds] = useState<Set<string>>(new Set())5354 // Un seul chargement au montage : après la connexion SSO, la page se55 // recharge entièrement, donc l'état revient toujours frais du hub.56 useEffect(() => {57 const controller = new AbortController()58 fetchFavorites(controller.signal)59 .then((r) => {60 if (r === null) {61 setLoggedIn(false)62 setIds(new Set())63 } else {64 setLoggedIn(true)65 setIds(new Set(r.ids))66 }67 setReady(true)68 })69 .catch(() => {})70 return () => controller.abort()71 }, [])7273 const toggle = useCallback(74 (item: FavoriteItem) => {75 if (!loggedIn) {76 const next = location.pathname + location.search77 window.location.href = `/api/auth/ka/login?next=${encodeURIComponent(next)}`78 return79 }80 const on = !ids.has(item.item_id)81 const optimistic = new Set(ids)82 if (on) optimistic.add(item.item_id)83 else optimistic.delete(item.item_id)84 setIds(optimistic)85 toggleFavorite(on, item)86 .then((ok) => {87 if (ok) return88 // le hub n'a pas pris le ♥ : on rétablit l'état précédent89 setIds((cur) => {90 const revert = new Set(cur)91 if (on) revert.delete(item.item_id)92 else revert.add(item.item_id)93 return revert94 })95 })96 .catch(() => {})97 },98 [ids, loggedIn, location.pathname, location.search]99 )100101 return (102 <FavoritesContext.Provider value={{ ready, loggedIn, ids, toggle }}>103 {children}104 </FavoritesContext.Provider>105 )106}107108/** Bouton ♥ d'un produit (cartes et fiche) — état lu/poussé au hub KA. */109export function FavButton({110 product,111 size = 17,112 className,113}: {114 product: Product115 size?: number116 className?: string117}) {118 const { ids, toggle } = useFavorites()119 const on = ids.has(product.uid)120 const label = on ? 'Retirer des favoris' : 'Ajouter aux favoris'121122 function onClick(e: MouseEvent<HTMLButtonElement>) {123 // les cartes produit sont des <Link> : le ♥ ne doit pas naviguer124 e.preventDefault()125 e.stopPropagation()126 toggle(productFavItem(product))127 }128129 return (130 <button131 type="button"132 className={`fav-btn${on ? ' fav-btn-on' : ''}${className ? ` ${className}` : ''}`}133 onClick={onClick}134 aria-pressed={on}135 aria-label={label}136 title={label}137 >138 <IconHeart size={size} filled={on} />139 </button>140 )141}142