// src/components/Favorites/FavoritesContext.tsx // i18n-Ready (4 Languages) • Toast Debounce Fixed • Safe Storage • Strong Types // DOM-safe timers: number (no NodeJS.Timeout) // ✅ Updated for new product schema (Money, prices{}, availability enums, i18n names) import React, { createContext, useContext, useReducer, useCallback, useEffect, useMemo, useRef, } from "react"; import { premiumToast } from "../common/PremiumUtilities"; import i18n from "../../i18n"; /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ * TYPES & INTERFACES * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ export interface FavoriteItem { id: string; name: string; description: string; price: number; image: string; category: string; // normalized category code or label calories: number; rating: number; reviewCount: number; addedAt: string; ingredients?: string[]; isNew?: boolean; isPopular?: boolean; isVegetarian?: boolean; isSpicy?: boolean; allergens?: string[]; preparationTime?: number; spiceLevel?: number; dietLabels?: string[]; isAvailable?: boolean; nutritionalInfo?: { protein: number; carbs: number; fat: number; fiber: number; }; } export interface FavoriteStats { totalFavorites: number; averagePrice: number; // raw number; UI handles currency totalCalories: number; mostFavoriteCategory: string; totalValue: number; averageRating: number; categoryCounts: Record; addedThisWeek: number; addedThisMonth: number; } interface FavoritesState { favorites: FavoriteItem[]; isLoading: boolean; error: string | null; lastUpdated: string | null; } type FavoritesAction = | { type: "SET_LOADING"; payload: boolean } | { type: "SET_ERROR"; payload: string | null } | { type: "ADD_FAVORITE"; payload: FavoriteItem } | { type: "REMOVE_FAVORITE"; payload: string } | { type: "CLEAR_FAVORITES" } | { type: "LOAD_FAVORITES"; payload: FavoriteItem[] } | { type: "UPDATE_FAVORITE"; payload: { id: string; updates: Partial }; }; /* ── New product schema compatibility (input union) ───────────────────── */ type LocaleKey = "nl" | "en" | "de" | "fr"; type CurrencyCode = "EUR" | "USD" | "GBP" | "TRY" | string; interface Money { amount: number; currency: CurrencyCode; } interface UpdatedCategory { code: string; name?: string | Record; } interface UpdatedProduct { id: string; sku?: string; slug?: string; name?: string | Record; title?: string | Record; i18nKey?: string; description?: string | Record; category?: string | UpdatedCategory; ingredients?: string[]; image?: string; images?: string[]; tags?: string[]; // pricing price?: number | string | Money; prices?: Record; // nutrition/flags calories?: number | string; dietLabels?: string[] | { code: string; label?: string }[]; rating?: number; reviewCount?: number; isNew?: boolean; isPopular?: boolean; // availability (new) or legacy availability?: "in_stock" | "out_of_stock" | "preorder" | "unavailable"; isAvailable?: boolean; // meta addedAt?: string | number | Date; } type FavoriteInput = | FavoriteItem | UpdatedProduct | (FavoriteItem & UpdatedProduct); export interface FavoritesContextType { favorites: FavoriteItem[]; totalFavorites: number; isLoading: boolean; error: string | null; addFavorite: (item: FavoriteInput) => void; removeFavorite: (id: string) => void; toggleFavorite: (item: FavoriteInput) => void; clearFavorites: () => void; isFavorite: (id: string) => boolean; getFavoritesByCategory: () => Record; getFavoritesStats: () => FavoriteStats; searchFavorites: (query: string) => FavoriteItem[]; exportFavorites: () => string; importFavorites: (data: string) => boolean; } /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ * i18n * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ type Lang = "nl" | "en" | "de" | "fr"; const L = { nl: { saveError: "Kon favorieten niet opslaan", loadError: "Kon favorieten niet laden", exportError: "Kon favorieten niet exporteren", importError: "Kon favorieten niet importeren. Controleer het bestand.", added: "{{name}} toegevoegd aan favorieten! ❤️", removed: "{{name}} verwijderd uit favorieten", cleared: "{{count}} favorieten gewist", importSuccess: "{{count}} favorieten succesvol geïmporteerd!", }, en: { saveError: "Couldn't save favorites", loadError: "Couldn't load favorites", exportError: "Couldn't export favorites", importError: "Couldn't import favorites. Please check the file.", added: "{{name}} added to favorites! ❤️", removed: "{{name}} removed from favorites", cleared: "{{count}} favorites cleared", importSuccess: "{{count}} favorites imported successfully!", }, de: { saveError: "Favoriten konnten nicht gespeichert werden", loadError: "Favoriten konnten nicht geladen werden", exportError: "Favoriten konnten nicht exportiert werden", importError: "Favoriten konnten nicht importiert werden. Bitte Datei prüfen.", added: "{{name}} zu Favoriten hinzugefügt! ❤️", removed: "{{name}} aus Favoriten entfernt", cleared: "{{count}} Favoriten gelöscht", importSuccess: "{{count}} Favoriten erfolgreich importiert!", }, fr: { saveError: "Impossible d'enregistrer les favoris", loadError: "Impossible de charger les favoris", exportError: "Impossible d'exporter les favoris", importError: "Impossible d'importer les favoris. Veuillez vérifier le fichier.", added: "{{name}} ajouté aux favoris ! ❤️", removed: "{{name}} retiré des favoris", cleared: "{{count}} favoris effacés", importSuccess: "{{count}} favoris importés avec succès !", }, } as const; type LocalKey = keyof (typeof L)["nl"]; const tpl = (str: string, params?: Record) => str.replace(/{{\s*(\w+)\s*}}/g, (_, k) => params?.[k] !== undefined ? String(params[k]) : `{{${k}}}` ); const currentLang = (): Lang => { const raw = (i18n.language || "nl").toLowerCase(); if (raw.startsWith("en")) return "en"; if (raw.startsWith("de")) return "de"; if (raw.startsWith("fr")) return "fr"; return "nl"; }; const msg = (key: LocalKey, params?: Record) => tpl(L[currentLang()][key], params); /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ * NORMALIZATION HELPERS (schema-agnostic) * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ const hasWindow = typeof window !== "undefined"; const pickLocale = ( value?: string | Record, locale: string = "en" ): string => { if (!value) return ""; if (typeof value === "string") return value; const lang = (locale.split("-")[0] as LocaleKey) || "en"; return value[lang] ?? value.en ?? value.nl ?? value.de ?? value.fr ?? ""; }; const toNumberStrict = (v: unknown): number => { if (typeof v === "number" && !Number.isNaN(v)) return v; if (typeof v === "string") { const cleaned = v.replace(/[^\d,.-]/g, "").replace(",", "."); const parsed = parseFloat(cleaned); return Number.isNaN(parsed) ? 0 : parsed; } return 0; }; const moneyToNumber = (m?: Money | number | string): number => { if (m === null || m === undefined) return 0; if (typeof m === "number" || typeof m === "string") return toNumberStrict(m); if (typeof (m as Money).amount === "number") return (m as Money).amount; return 0; }; const getBestPrice = (p: UpdatedProduct, locale: string): number => { if (p.price !== undefined) return moneyToNumber(p.price); if (p.prices && typeof p.prices === "object") { const lang = (locale.split("-")[0] as LocaleKey) || "en"; const prefByLocale: Record = { nl: "EUR", en: "EUR", de: "EUR", fr: "EUR", }; const preferred = p.prices[prefByLocale[lang]]; if (preferred) return moneyToNumber(preferred); const eur = p.prices["EUR"]; if (eur) return moneyToNumber(eur); const first = Object.values(p.prices)[0]; if (first) return moneyToNumber(first); } return 0; }; const getAvailabilityBool = (p: UpdatedProduct): boolean => { if (typeof p.isAvailable === "boolean") return p.isAvailable; switch (p.availability) { case "out_of_stock": case "unavailable": return false; case "in_stock": case "preorder": default: return true; } }; const normalizeToFavorite = (input: FavoriteInput): FavoriteItem => { const locale = i18n.language || "en"; // If it already looks like a FavoriteItem, coerce numeric fields & return if ( (input as FavoriteItem).name && (input as FavoriteItem).price !== undefined ) { const f = input as FavoriteItem; return { ...f, price: toNumberStrict(f.price), calories: toNumberStrict(f.calories), rating: toNumberStrict(f.rating) || 4.5, reviewCount: Math.max(0, Math.floor(toNumberStrict(f.reviewCount))), isAvailable: f.isAvailable !== false, addedAt: f.addedAt || new Date().toISOString(), dietLabels: f.dietLabels || [], category: f.category || "other", description: f.description || "", image: f.image || "", }; } // Otherwise, treat as UpdatedProduct const p = input as UpdatedProduct; const name = p.i18nKey || pickLocale(p.title ?? p.name, locale) || (p as any).name || ""; const description = pickLocale(p.description, locale) || ""; const cat = p.category; const category = (typeof cat === "string" ? cat : cat?.code) || (p as any).category || "other"; const price = getBestPrice(p, locale) || toNumberStrict((p as any).price); const image = p.image ?? (Array.isArray(p.images) ? p.images[0] : undefined) ?? ""; const dietLabels = Array.isArray(p.dietLabels) ? p.dietLabels .map((d: any) => (typeof d === "string" ? d : d?.code ?? "")) .filter(Boolean) : []; return { id: p.id, name, description, price, image, category, calories: toNumberStrict(p.calories), rating: toNumberStrict(p.rating) || 4.5, reviewCount: Math.max(0, Math.floor(toNumberStrict(p.reviewCount))), addedAt: p.addedAt ? new Date(p.addedAt as any).toISOString() : new Date().toISOString(), ingredients: p.ingredients || [], isNew: Boolean(p.isNew), isPopular: Boolean(p.isPopular), isVegetarian: undefined, isSpicy: undefined, allergens: undefined, preparationTime: undefined, spiceLevel: undefined, dietLabels, isAvailable: getAvailabilityBool(p), nutritionalInfo: undefined, }; }; /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ * REDUCER & STORAGE * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ const STORAGE_KEY = "sushi-breskens-favorites"; const STORAGE_VERSION = "2.0"; // bumped for new schema normalization const safeLocalStorage: Storage | null = hasWindow && typeof window.localStorage !== "undefined" ? window.localStorage : null; // in-memory fallback when localStorage is unavailable/blocked let memoryStore: string | null = null; const readFromStore = (key: string): string | null => { try { if (safeLocalStorage) return safeLocalStorage.getItem(key); return memoryStore; } catch { return null; } }; const writeToStore = (key: string, value: string) => { try { if (safeLocalStorage) safeLocalStorage.setItem(key, value); else memoryStore = value; } catch { // ignore } }; const removeFromStore = (key: string) => { try { if (safeLocalStorage) safeLocalStorage.removeItem(key); else memoryStore = null; } catch { // ignore } }; // Enhanced localStorage utilities with versioned migration & error handling const storage = { get: (): FavoriteItem[] => { try { const stored = readFromStore(STORAGE_KEY); if (!stored) return []; const parsed = JSON.parse(stored); // raw array fallback (very old) if (Array.isArray(parsed)) { const normalized = parsed.map((x: any) => normalizeToFavorite(x)); storage.set(normalized); return normalized; } if (parsed.version !== STORAGE_VERSION) { // migrate from <=1.x if (Array.isArray(parsed.favorites)) { const migrated = parsed.favorites.map((x: any) => normalizeToFavorite(x) ); storage.set(migrated); return migrated; } // unknown structure -> reset storage.clear(); return []; } if (!Array.isArray(parsed.favorites)) { throw new Error("Invalid favorites data structure"); } return parsed.favorites.map((x: any) => normalizeToFavorite(x)); } catch (error) { console.error("Error loading favorites from storage:", error); storage.clear(); return []; } }, set: (favorites: FavoriteItem[]): void => { try { const data = { version: STORAGE_VERSION, favorites, lastUpdated: new Date().toISOString(), locale: i18n.language, }; writeToStore(STORAGE_KEY, JSON.stringify(data)); } catch (error) { console.error("Error saving favorites to storage:", error); premiumToast.error(msg("saveError")); } }, clear: (): void => { try { removeFromStore(STORAGE_KEY); } catch (error) { console.error("Error clearing favorites storage:", error); } }, }; // Initial state const initialState: FavoritesState = { favorites: [], isLoading: false, error: null, lastUpdated: null, }; // Reducer function const favoritesReducer = ( state: FavoritesState, action: FavoritesAction ): FavoritesState => { switch (action.type) { case "SET_LOADING": return { ...state, isLoading: action.payload }; case "SET_ERROR": return { ...state, error: action.payload, isLoading: false }; case "LOAD_FAVORITES": return { ...state, favorites: action.payload, isLoading: false, error: null, lastUpdated: new Date().toISOString(), }; case "ADD_FAVORITE": { const exists = state.favorites.some( (fav) => fav.id === action.payload.id ); if (exists) return state; const newFavorite = normalizeToFavorite(action.payload); const newFavorites = [newFavorite, ...state.favorites]; storage.set(newFavorites); return { ...state, favorites: newFavorites, lastUpdated: new Date().toISOString(), }; } case "REMOVE_FAVORITE": { const newFavorites = state.favorites.filter( (fav) => fav.id !== action.payload ); storage.set(newFavorites); return { ...state, favorites: newFavorites, lastUpdated: new Date().toISOString(), }; } case "CLEAR_FAVORITES": storage.clear(); return { ...state, favorites: [], lastUpdated: new Date().toISOString() }; case "UPDATE_FAVORITE": { const newFavorites = state.favorites.map((fav) => fav.id === action.payload.id ? normalizeToFavorite({ ...fav, ...action.payload.updates }) : fav ); storage.set(newFavorites); return { ...state, favorites: newFavorites, lastUpdated: new Date().toISOString(), }; } default: return state; } }; /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ * UTILITY FUNCTIONS (outside component) * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const getFavoritesByCategory = ( favorites: FavoriteItem[] ): Record => favorites.reduce((acc, item) => { const key = item.category || "other"; if (!acc[key]) acc[key] = []; acc[key].push(item); return acc; }, {} as Record); const getFavoritesStats = (favorites: FavoriteItem[]): FavoriteStats => { const totalFavorites = favorites.length; if (totalFavorites === 0) { return { totalFavorites: 0, averagePrice: 0, totalCalories: 0, mostFavoriteCategory: "", totalValue: 0, averageRating: 0, categoryCounts: {}, addedThisWeek: 0, addedThisMonth: 0, }; } const totalValue = favorites.reduce( (sum, item) => sum + (toNumberStrict(item.price) || 0), 0 ); const totalCalories = favorites.reduce( (sum, item) => sum + (toNumberStrict(item.calories) || 0), 0 ); const totalRating = favorites.reduce( (sum, item) => sum + (toNumberStrict(item.rating) || 4.5), 0 ); const averagePrice = totalValue / totalFavorites; const averageRating = Number((totalRating / totalFavorites).toFixed(1)); const categoryCounts = favorites.reduce((acc, item) => { const key = item.category || "other"; acc[key] = (acc[key] || 0) + 1; return acc; }, {} as Record); const mostFavoriteCategory = Object.entries(categoryCounts).sort(([, a], [, b]) => b - a)[0]?.[0] || ""; const now = Date.now(); const addedThisWeek = favorites.filter( (item) => now - new Date(item.addedAt).getTime() <= ONE_WEEK_MS ).length; const addedThisMonth = favorites.filter( (item) => now - new Date(item.addedAt).getTime() <= THIRTY_DAYS_MS ).length; return { totalFavorites, averagePrice, totalCalories, mostFavoriteCategory, totalValue, averageRating, categoryCounts, addedThisWeek, addedThisMonth, }; }; const searchFavorites = ( favorites: FavoriteItem[], query: string ): FavoriteItem[] => { if (!query.trim()) return favorites; const q = query.toLowerCase(); return favorites.filter((item) => { const inName = item.name.toLowerCase().includes(q); const inDesc = (item.description || "").toLowerCase().includes(q); const inCat = (item.category || "").toLowerCase().includes(q); const inIngr = item.ingredients?.some((ing) => ing.toLowerCase().includes(q) ); const inLabels = item.dietLabels?.some((label) => label.toLowerCase().includes(q) ); return inName || inDesc || inCat || inIngr || inLabels; }); }; const exportFavorites = (favorites: FavoriteItem[]): string => { try { const exportData = { version: STORAGE_VERSION, exportedAt: new Date().toISOString(), locale: i18n.language, favorites, stats: getFavoritesStats(favorites), }; return JSON.stringify(exportData, null, 2); } catch (error) { console.error("Error exporting favorites:", error); premiumToast.error(msg("exportError")); return ""; } }; /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ * CONTEXT & PROVIDER * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ const FavoritesContext = createContext(null); export const FavoritesProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [state, dispatch] = useReducer(favoritesReducer, initialState); // DOM-only timer id (number). No NodeJS.Timeout anywhere. type DomTimeout = number; const toastTimeoutRef = useRef(null); const lastToastRef = useRef<{ message: string; time: number } | null>(null); const showToast = useCallback( (message: string, type: "success" | "info" | "error") => { const now = Date.now(); if ( lastToastRef.current && lastToastRef.current.message === message && now - lastToastRef.current.time < 1000 ) { return; } if (toastTimeoutRef.current !== null) { window.clearTimeout(toastTimeoutRef.current); toastTimeoutRef.current = null; } toastTimeoutRef.current = window.setTimeout(() => { premiumToast[type](message); lastToastRef.current = { message, time: Date.now() }; }, 100); }, [] ); useEffect(() => { const loadFavorites = async () => { dispatch({ type: "SET_LOADING", payload: true }); try { await new Promise((resolve) => { if (hasWindow) { window.setTimeout(resolve, 150); } else { // SSR safety: fallback setTimeout(resolve, 150); } }); const storedFavorites = storage.get(); dispatch({ type: "LOAD_FAVORITES", payload: storedFavorites }); } catch (error) { console.error("Error loading favorites:", error); dispatch({ type: "SET_ERROR", payload: msg("loadError") }); } }; loadFavorites(); }, []); const addFavorite = useCallback( (item: FavoriteInput) => { const normalized = normalizeToFavorite(item); if (!normalized.id || !normalized.name) { console.warn("Invalid favorite item:", item); return; } dispatch({ type: "ADD_FAVORITE", payload: normalized }); showToast(msg("added", { name: normalized.name }), "success"); }, [showToast] ); const removeFavorite = useCallback( (id: string) => { const favorite = state.favorites.find((fav) => fav.id === id); dispatch({ type: "REMOVE_FAVORITE", payload: id }); if (favorite) showToast(msg("removed", { name: favorite.name }), "info"); }, [state.favorites, showToast] ); const toggleFavorite = useCallback( (item: FavoriteInput) => { const id = (item as any).id; if (!id) return; const isFav = state.favorites.some((fav) => fav.id === id); if (isFav) removeFavorite(id); else addFavorite(item); }, [state.favorites, addFavorite, removeFavorite] ); const isFavorite = useCallback( (id: string): boolean => state.favorites.some((fav) => fav.id === id), [state.favorites] ); const clearFavorites = useCallback(() => { const count = state.favorites.length; if (count === 0) return; dispatch({ type: "CLEAR_FAVORITES" }); showToast(msg("cleared", { count }), "success"); }, [state.favorites.length, showToast]); const importFavorites = useCallback( (data: string): boolean => { try { const parsed = JSON.parse(data); // Accept both {favorites:[...]} and bare arrays const rawList: any[] = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.favorites) ? parsed.favorites : []; if (!rawList.length) throw new Error("Invalid import data structure"); const normalized: FavoriteItem[] = rawList .map(normalizeToFavorite) .filter((x) => x.id && x.name && typeof x.price === "number"); if (normalized.length === 0) { throw new Error("No valid favorites found in import data"); } // dedupe by id const seen = new Set(); const deduped = normalized.filter((it) => { if (seen.has(it.id)) return false; seen.add(it.id); return true; }); dispatch({ type: "CLEAR_FAVORITES" }); dispatch({ type: "LOAD_FAVORITES", payload: deduped }); showToast(msg("importSuccess", { count: deduped.length }), "success"); return true; } catch (error) { console.error("Error importing favorites:", error); showToast(msg("importError"), "error"); return false; } }, [showToast] ); const contextValue = useMemo( (): FavoritesContextType => ({ favorites: state.favorites, totalFavorites: state.favorites.length, isLoading: state.isLoading, error: state.error, addFavorite, removeFavorite, toggleFavorite, clearFavorites, isFavorite, getFavoritesByCategory: () => getFavoritesByCategory(state.favorites), getFavoritesStats: () => getFavoritesStats(state.favorites), searchFavorites: (query: string) => searchFavorites(state.favorites, query), exportFavorites: () => exportFavorites(state.favorites), importFavorites, }), [ state.favorites, state.isLoading, state.error, addFavorite, removeFavorite, toggleFavorite, clearFavorites, isFavorite, importFavorites, ] ); useEffect(() => { return () => { if (toastTimeoutRef.current !== null) { window.clearTimeout(toastTimeoutRef.current); toastTimeoutRef.current = null; } }; }, []); return ( {children} ); }; /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ * HOOKS & UTILITIES * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ export const useFavorites = (): FavoritesContextType => { const context = useContext(FavoritesContext); if (!context) { throw new Error("useFavorites must be used within a FavoritesProvider"); } return context; }; export const useFavoritesNotifications = () => { const { totalFavorites } = useFavorites(); const [previousCount, setPreviousCount] = React.useState(totalFavorites); useEffect(() => { if (totalFavorites > previousCount && previousCount > 0 && hasWindow) { const event = new CustomEvent("favorite-added", { detail: { count: totalFavorites }, }); window.dispatchEvent(event); } setPreviousCount(totalFavorites); }, [totalFavorites, previousCount]); return { totalFavorites, recentlyAdded: totalFavorites > previousCount }; }; export const useFavoritesPerformance = () => { const { favorites } = useFavorites(); const performanceInfo = useMemo( () => ({ shouldUsePagination: favorites.length > 50, shouldUseVirtualization: favorites.length > 100, memoryWarning: favorites.length > 500, totalItems: favorites.length, }), [favorites.length] ); useEffect(() => { if ( performanceInfo.shouldUsePagination && !performanceInfo.shouldUseVirtualization ) { console.info("Consider implementing pagination for better performance."); } if (performanceInfo.shouldUseVirtualization) { console.warn( "Large number of favorites detected. Consider pagination or virtualization." ); } if (performanceInfo.memoryWarning) { console.warn( "Very large favorites list. Consider implementing cleanup strategies." ); } }, [performanceInfo]); return performanceInfo; }; export const useFavoritesAnalytics = () => { const { favorites, getFavoritesStats } = useFavorites(); const analytics = useMemo(() => { const stats = getFavoritesStats(); const categories = getFavoritesByCategory(favorites); return { ...stats, uniqueCategories: Object.keys(categories).length, averageItemsPerCategory: stats.totalFavorites > 0 ? Math.round( (stats.totalFavorites / Object.keys(categories).length) * 10 ) / 10 : 0, topCategories: Object.entries(stats.categoryCounts) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([name, count]) => ({ name, count })), }; }, [favorites, getFavoritesStats]); return analytics; }; export default FavoritesProvider;