import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { useToast } from "../components/Toast"; import { useI18n } from "../i18n"; import { useWallet } from "../wallet/WalletProvider"; import { addFavorite, getFavoriteIds, removeFavorite } from "./api"; type FavoriteStatus = "unknown" | "favorited" | "notFavorited"; type FavoritesContextValue = { favoriteIds: Set; pendingIds: Set; statusFor: (resourceId: string) => FavoriteStatus; ensureFavoriteIds: (resourceIds: string[]) => Promise; toggleFavorite: (resourceId: string) => Promise; markFavorite: (resourceId: string, favorited: boolean) => void; }; const FavoritesContext = createContext(null); export function FavoritesProvider({ children }: { children: ReactNode }) { const { t } = useI18n(); const { showToast } = useToast(); const wallet = useWallet(); const [favoriteIds, setFavoriteIds] = useState>(() => new Set()); const [knownIds, setKnownIds] = useState>(() => new Set()); const [pendingIds, setPendingIds] = useState>(() => new Set()); const pendingAfterLoginRef = useRef(null); useEffect(() => { if (wallet.status === "loggedOut") { setFavoriteIds(new Set()); setKnownIds(new Set()); setPendingIds(new Set()); pendingAfterLoginRef.current = null; } }, [wallet.status]); const markFavorite = useCallback((resourceId: string, favorited: boolean) => { setKnownIds((prev) => new Set(prev).add(resourceId)); setFavoriteIds((prev) => { const next = new Set(prev); if (favorited) next.add(resourceId); else next.delete(resourceId); return next; }); }, []); const ensureFavoriteIds = useCallback( async (resourceIds: string[]) => { if (!wallet.token || wallet.status !== "loggedIn") return; const missing = [...new Set(resourceIds)].filter( (id) => !knownIds.has(id), ); if (missing.length === 0) return; const ids = await getFavoriteIds(wallet.token, missing); setKnownIds((prev) => { const next = new Set(prev); missing.forEach((id) => next.add(id)); return next; }); setFavoriteIds((prev) => { const next = new Set(prev); ids.forEach((id) => next.add(id)); return next; }); }, [knownIds, wallet.status, wallet.token], ); const runFavoriteMutation = useCallback( async (resourceId: string) => { if (!wallet.token) return; const currentlyFavorite = favoriteIds.has(resourceId); setPendingIds((prev) => new Set(prev).add(resourceId)); markFavorite(resourceId, !currentlyFavorite); try { if (currentlyFavorite) await removeFavorite(wallet.token, resourceId); else await addFavorite(wallet.token, resourceId); showToast( currentlyFavorite ? t("favoriteRemoved") : t("favoriteAdded"), ); } catch (error) { markFavorite(resourceId, currentlyFavorite); showToast(t("favoriteFailed"), "error"); throw error; } finally { setPendingIds((prev) => { const next = new Set(prev); next.delete(resourceId); return next; }); } }, [favoriteIds, markFavorite, showToast, t, wallet.token], ); const toggleFavorite = useCallback( async (resourceId: string) => { if (!wallet.token || wallet.status !== "loggedIn") { pendingAfterLoginRef.current = resourceId; wallet.openLoginModal(); showToast(t("favoriteLoginRequired")); return; } await runFavoriteMutation(resourceId); }, [runFavoriteMutation, showToast, t, wallet], ); useEffect(() => { if (wallet.status !== "loggedIn" || !wallet.token) return; const pending = pendingAfterLoginRef.current; if (!pending) return; pendingAfterLoginRef.current = null; void runFavoriteMutation(pending).catch(() => undefined); }, [runFavoriteMutation, wallet.status, wallet.token]); const statusFor = useCallback( (resourceId: string): FavoriteStatus => { if (favoriteIds.has(resourceId)) return "favorited"; if (knownIds.has(resourceId)) return "notFavorited"; return "unknown"; }, [favoriteIds, knownIds], ); const value = useMemo( () => ({ favoriteIds, pendingIds, statusFor, ensureFavoriteIds, toggleFavorite, markFavorite, }), [ ensureFavoriteIds, favoriteIds, markFavorite, pendingIds, statusFor, toggleFavorite, ], ); return ( {children} ); } export function useFavorites() { const ctx = useContext(FavoritesContext); if (!ctx) throw new Error("useFavorites must be used within FavoritesProvider"); return ctx; }