import React, { createContext, useContext, useState, useEffect } from 'react'; import type { FoodItem, Category, Stall } from '../types'; interface FoodContextType { categories: Category[]; foodItems: FoodItem[]; stalls: Stall[]; isLoading: boolean; error: string | null; refreshData: (silent?: boolean) => Promise; } const FoodContext = createContext(undefined); const API_BASE_URL = 'https://cmsapi.rit-services.in/api'; const EMOJIS = ['🍦', '🍪', '🧃', '🍫', '🥨', '🥐', '🍹', '📦', '🍳', '🍱', '🍔', '🍕', '🥗', '🍲']; const COLORS = ['#FFF0F6', '#FFF7ED', '#FFFFEB', '#FDF3EF', '#F0FDF4', '#FEFCE8', '#EFF6FF']; export const FoodProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [categories, setCategories] = useState([]); const [foodItems, setFoodItems] = useState([]); const [stalls, setStalls] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const fetchData = async (silent = false) => { if (!silent) setIsLoading(true); setError(null); try { const token = localStorage.getItem('token'); const headers: Record = {}; if (token) { headers['Authorization'] = `Bearer ${token}`; } const [baseItemsRes, productsRes, stallsRes, statsRes] = await Promise.all([ fetch(`${API_BASE_URL}/base-items?size=100`, { cache: 'no-store' }), fetch(`${API_BASE_URL}/products?size=100`, { cache: 'no-store' }), fetch(`${API_BASE_URL}/stalls/active`, { cache: 'no-store' }), fetch(`${API_BASE_URL}/feedback/stats`, { cache: 'no-store', headers }).catch(() => null) ]); if (!baseItemsRes.ok || !productsRes.ok || !stallsRes.ok) { throw new Error('Failed to fetch data from server'); } const baseItemsDataRaw = await baseItemsRes.json(); const productsDataRaw = await productsRes.json(); const stallsData = await stallsRes.json(); const baseItemsData = baseItemsDataRaw.content || baseItemsDataRaw; const productsData = productsDataRaw.content || productsDataRaw; // Extract top rated items names by customer feedback count and rating maps let topRatedNames: string[] = []; const itemRatingsMap: Record = {}; if (statsRes && statsRes.ok) { try { const statsData = await statsRes.json(); const ratedItems = statsData.ratedItems || []; // Sort items by count of feedbacks descending const sorted = [...ratedItems].sort((a: any, b: any) => b.count - a.count); // Get names of top 3 items with at least 1 feedback topRatedNames = sorted .filter((x: any) => x.count > 0) .slice(0, 3) .map((x: any) => x.name.toLowerCase()); ratedItems.forEach((x: any) => { if (x.name) { itemRatingsMap[x.name.toLowerCase()] = { average: x.average, count: x.count }; } }); } catch (e) { console.error('Error parsing feedback stats:', e); } } // Map BaseItems to Categories const mappedCategories: Category[] = baseItemsData.map((item: any, index: number) => ({ id: item.id.toString(), name: item.name, description: item.description, emoji: EMOJIS[index % EMOJIS.length], image: item.imageData?.trim() ? (item.imageData.trim().startsWith('data:') ? item.imageData.trim() : `data:image/png;base64,${item.imageData.trim()}`) : '', color: COLORS[index % COLORS.length] })); // Map Stalls and build a product-to-stall lookup map const productToStallMap: Record = {}; const categoryToStallMap: Record = {}; const mappedStalls: Stall[] = stallsData.map((item: any) => { const stallInfo = { id: item.id.toString(), name: item.name }; // Map category/baseItems to this stall for indirect lookup if (item.baseItems) { item.baseItems.forEach((bi: any) => { if (!categoryToStallMap[bi.name.toLowerCase()]) { categoryToStallMap[bi.name.toLowerCase()] = stallInfo; } }); } // If this stall explicitly lists products, add them to map if (item.products) { item.products.forEach((p: any) => { if (!productToStallMap[p.id.toString()]) { productToStallMap[p.id.toString()] = stallInfo; } }); } return { id: stallInfo.id, name: stallInfo.name, description: item.description || 'Our delicious stall', imageData: item.imageData?.trim() ? (item.imageData.trim().startsWith('data:') ? item.imageData.trim() : `data:image/png;base64,${item.imageData.trim()}`) : '', active: item.active, temporarilyClosed: item.temporarilyClosed }; }); // Map Products to FoodItems using the lookup map as fallback const mappedFoodItems: FoodItem[] = productsData.map((item: any) => { const rawImg = item.imageData?.trim(); const finalImage = rawImg ? (rawImg.startsWith('data:') ? rawImg : `data:image/png;base64,${rawImg}`) : ''; const itemId = item.id.toString(); const stallFromBackend = item.stalls && item.stalls.length > 0 ? { id: item.stalls[0].id.toString(), name: item.stalls[0].name } : null; const stallFromMap = productToStallMap[itemId]; const stallFromCategory = item.category ? categoryToStallMap[item.category.toLowerCase()] : null; // Dynamic bestseller isPopular flag based on feedback stats const isBestseller = topRatedNames.includes(item.name.toLowerCase()); const ratingInfo = itemRatingsMap[item.name.toLowerCase()]; return { id: itemId, name: item.name, description: item.description || 'No description available', price: item.price || item.basePrice || 0, category: item.category, image: finalImage, isVeg: item.veg, isPopular: isBestseller, stock: item.stock, stallId: (stallFromBackend?.id || stallFromMap?.id || stallFromCategory?.id), stallName: (stallFromBackend?.name || stallFromMap?.name || stallFromCategory?.name), rating: ratingInfo ? ratingInfo.average : 5.0, ratingCount: ratingInfo ? ratingInfo.count : 0, parcellable: item.parcellable }; }); setCategories(mappedCategories); setFoodItems(mappedFoodItems); setStalls(mappedStalls); } catch (err: any) { if (!silent) setError(err.message); console.error('Error fetching food data:', err); } finally { if (!silent) setIsLoading(false); } }; useEffect(() => { fetchData(); // Silent background polling every 10 seconds to sync stock const pollInterval = setInterval(() => { fetchData(true); }, 10000); return () => clearInterval(pollInterval); }, []); return ( {children} ); }; export const useFood = () => { const context = useContext(FoodContext); if (context === undefined) { throw new Error('useFood must be used within a FoodProvider'); } return context; };