import React, { useState, useCallback, useEffect } from 'react'; import { FlyerDisplay } from './components/FlyerDisplay'; import { ExtractedDataTable } from './components/ExtractedDataTable'; import { AnalysisPanel } from './components/AnalysisPanel'; import { PriceChart } from './components/PriceChart'; import { ErrorDisplay } from './components/ErrorDisplay'; import { Header } from './components/Header'; import { isImageAFlyer, extractCoreDataFromImage, extractAddressFromImage, extractLogoFromImage } from './services/geminiService'; import type { FlyerItem, Flyer, MasterGroceryItem, DealItem, ProcessingStage, StageStatus, Store, Profile, ShoppingList, ShoppingListItem } from './types'; import { BulkImporter } from './components/BulkImporter'; import { PriceHistoryChart } from './components/PriceHistoryChart'; import { supabase, uploadFlyerImage, createFlyerRecord, saveFlyerItems, getFlyers, getFlyerItems, initializeSupabase, findFlyerByChecksum, getWatchedItems, addWatchedItem, getAllMasterItems, getFlyerItemsForFlyers, countFlyerItemsForFlyers, getUserProfile, updateUserPreferences, removeWatchedItem, getShoppingLists, createShoppingList, addShoppingListItem, updateShoppingListItem, removeShoppingListItem, deleteShoppingList, uploadLogoAndUpdateStore } from './services/supabaseClient'; import { FlyerList } from './components/FlyerList'; import { recordProcessingTime, getAverageProcessingTime } from './utils/processingTimer'; import { ProcessingStatus } from './components/ProcessingStatus'; import { SupabaseConnector } from './components/SupabaseConnector'; import { generateFileChecksum } from './utils/checksum'; import { convertPdfToImageFiles } from './utils/pdfConverter'; import { BulkImportSummary } from './components/BulkImportSummary'; import { WatchedItemsList } from './components/WatchedItemsList'; import { withTimeout } from './utils/timeout'; import { Session } from '@supabase/supabase-js'; import { ProfileManager } from './components/ProfileManager'; import { ShoppingListComponent } from './components/ShoppingList'; import { SystemCheck } from './components/SystemCheck'; import { LoginPage } from './components/LoginPage'; import { VoiceAssistant } from './components/VoiceAssistant'; function App() { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isFakeAuth, setIsFakeAuth] = useState(false); const [loginError, setLoginError] = useState(null); const [flyers, setFlyers] = useState([]); const [selectedFlyer, setSelectedFlyer] = useState(null); const [flyerItems, setFlyerItems] = useState([]); const [watchedItems, setWatchedItems] = useState([]); const [masterItems, setMasterItems] = useState([]); const [activeDeals, setActiveDeals] = useState([]); const [activeDealsLoading, setActiveDealsLoading] = useState(false); const [totalActiveItems, setTotalActiveItems] = useState(0); const [isProcessing, setIsProcessing] = useState(false); const [error, setError] = useState(null); const [processingProgress, setProcessingProgress] = useState(0); const [currentFile, setCurrentFile] = useState(null); const [fileCount, setFileCount] = useState<{current: number, total: number} | null>(null); const [importSummary, setImportSummary] = useState<{ processed: string[]; skipped: string[]; errors: { fileName: string; message: string }[]; } | null>(null); const [isDbConnected, setIsDbConnected] = useState(!!supabase); const [isReady, setIsReady] = useState(false); const [isDarkMode, setIsDarkMode] = useState(false); const [unitSystem, setUnitSystem] = useState<'metric' | 'imperial'>('imperial'); const [session, setSession] = useState(null); const [profile, setProfile] = useState(null); const [isProfileManagerOpen, setIsProfileManagerOpen] = useState(false); const [isVoiceAssistantOpen, setIsVoiceAssistantOpen] = useState(false); const [processingStages, setProcessingStages] = useState([]); const [estimatedTime, setEstimatedTime] = useState(0); const [pageProgress, setPageProgress] = useState<{current: number, total: number} | null>(null); const [shoppingLists, setShoppingLists] = useState([]); const [activeListId, setActiveListId] = useState(null); // Effect to set initial theme based on user profile, local storage, or system preference useEffect(() => { if (profile && profile.preferences?.darkMode !== undefined) { // Preference from DB const dbDarkMode = profile.preferences.darkMode; setIsDarkMode(dbDarkMode); document.documentElement.classList.toggle('dark', dbDarkMode); } else { // Fallback to local storage or system preference const savedMode = localStorage.getItem('darkMode'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const initialDarkMode = savedMode !== null ? savedMode === 'true' : prefersDark; setIsDarkMode(initialDarkMode); document.documentElement.classList.toggle('dark', initialDarkMode); } }, [profile]); // Effect to set initial unit system based on user profile or local storage useEffect(() => { if (profile && profile.preferences?.unitSystem) { setUnitSystem(profile.preferences.unitSystem); } else { const savedSystem = localStorage.getItem('unitSystem') as 'metric' | 'imperial' | null; if (savedSystem) { setUnitSystem(savedSystem); } } }, [profile]); const toggleDarkMode = async () => { const newMode = !isDarkMode; setIsDarkMode(newMode); document.documentElement.classList.toggle('dark', newMode); if (session && !isFakeAuth) { const newPreferences = { ...profile?.preferences, darkMode: newMode }; setProfile(p => p ? {...p, preferences: newPreferences} : null); await updateUserPreferences(session.user.id, newPreferences); } else { localStorage.setItem('darkMode', String(newMode)); } }; const toggleUnitSystem = async () => { const newSystem = unitSystem === 'metric' ? 'imperial' : 'metric'; setUnitSystem(newSystem); if (session && !isFakeAuth) { // FIX: Explicitly type `newPreferences` to prevent TypeScript from incorrectly widening `newSystem` to a generic `string`. // This ensures compatibility with the `Profile` type definition. const newPreferences: Profile['preferences'] = { ...profile?.preferences, unitSystem: newSystem }; setProfile(p => p ? {...p, preferences: newPreferences} : null); await updateUserPreferences(session.user.id, newPreferences); } else { localStorage.setItem('unitSystem', newSystem); } }; const fetchFlyers = useCallback(async () => { if (!supabase) return; try { const allFlyers = await getFlyers(); setFlyers(allFlyers); } catch(e: any) { setError(e.message); } }, []); const fetchWatchedItems = useCallback(async (userId: string | undefined) => { if (!supabase || !userId) { setWatchedItems([]); return; } try { const items = await getWatchedItems(userId); setWatchedItems(items); } catch (e: any) { setError(`Could not fetch watched items: ${e.message}`); } }, []); const fetchShoppingLists = useCallback(async (userId: string | undefined) => { if (!supabase || !userId) { setShoppingLists([]); setActiveListId(null); return; } try { const lists = await getShoppingLists(userId); setShoppingLists(lists); if (lists.length > 0 && !activeListId) { setActiveListId(lists[0].id); } else if (lists.length === 0) { setActiveListId(null); } } catch (e: any) { setError(`Could not fetch shopping lists: ${e.message}`); } }, [activeListId]); const fetchMasterItems = useCallback(async () => { if (!supabase) return; try { const items = await getAllMasterItems(); setMasterItems(items); } catch (e: any) { setError(`Could not fetch master item list: ${e.message}`); } }, []); useEffect(() => { if (!supabase) { const storedUrl = localStorage.getItem('supabaseUrl'); const storedKey = localStorage.getItem('supabaseAnonKey'); if (storedUrl && storedKey) { initializeSupabase(storedUrl, storedKey); setIsDbConnected(true); } } else { setIsDbConnected(true); } }, []); // Effect to handle authentication state changes. useEffect(() => { if (!isDbConnected || !supabase) return; // If using fake auth, set up a mock session and state. // We don't listen to Supabase auth changes in this mode. if (isFakeAuth) { const mockSession = { user: { id: 'test-user-123', email: 'test@test.com' }, } as unknown as Session; setSession(mockSession); setProfile({ id: 'test-user-123' }); // User-specific data is empty for the fake user. setWatchedItems([]); setShoppingLists([]); return; // Early return to avoid setting up the real auth listener. } // This logic ONLY runs for real Supabase authentication. const fetchRealUserSessionData = async (session: Session | null) => { setSession(session); if (session) { const userProfile = await getUserProfile(session.user.id); setProfile(userProfile); fetchWatchedItems(session.user.id); fetchShoppingLists(session.user.id); } else { setProfile(null); setWatchedItems([]); setShoppingLists([]); } }; supabase.auth.getSession().then(({ data: { session } }) => { fetchRealUserSessionData(session); }); const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { if (_event === "SIGNED_OUT") { setIsProfileManagerOpen(false); // On sign out, always de-authenticate, regardless of fake auth status. setIsAuthenticated(false); setIsFakeAuth(false); } fetchRealUserSessionData(session); }); return () => subscription.unsubscribe(); }, [isDbConnected, fetchWatchedItems, fetchShoppingLists, isFakeAuth]); useEffect(() => { if (isDbConnected && isReady) { fetchFlyers(); fetchMasterItems(); } }, [isDbConnected, isReady, fetchFlyers, fetchMasterItems]); const resetState = useCallback(() => { setSelectedFlyer(null); setFlyerItems([]); setError(null); setProcessingProgress(0); setProcessingStages([]); setImportSummary(null); setCurrentFile(null); setPageProgress(null); setFileCount(null); }, []); const handleFlyerSelect = useCallback(async (flyer: Flyer) => { setSelectedFlyer(flyer); setError(null); setFlyerItems([]); // Clear previous items if (!supabase) return; try { const items = await getFlyerItems(flyer.id); setFlyerItems(items); } catch (e: any) { setError(e.message); } }, []); useEffect(() => { if (!isProcessing && !selectedFlyer && flyers.length > 0) { handleFlyerSelect(flyers[0]); } }, [flyers, selectedFlyer, handleFlyerSelect, isProcessing]); useEffect(() => { const findActiveDeals = async () => { if (!isDbConnected || !isReady || flyers.length === 0 || watchedItems.length === 0) { setActiveDeals([]); return; } setActiveDealsLoading(true); try { const today = new Date(); today.setHours(0, 0, 0, 0); const validFlyers = flyers.filter(flyer => { if (!flyer.valid_from || !flyer.valid_to) return false; try { const from = new Date(`${flyer.valid_from}T00:00:00`); const to = new Date(`${flyer.valid_to}T00:00:00`); return today >= from && today <= to; } catch (e) { console.error("Error parsing flyer date", e); return false; } }); if (validFlyers.length === 0) { setActiveDeals([]); return; } const validFlyerIds = validFlyers.map(f => f.id); const allItems = await getFlyerItemsForFlyers(validFlyerIds); const watchedItemIds = new Set(watchedItems.map(item => item.id)); const dealItemsRaw = allItems.filter(item => item.master_item_id && watchedItemIds.has(item.master_item_id) ); const flyerIdToStoreName = new Map(validFlyers.map(f => [f.id, f.store?.name || 'Unknown Store'])); const deals: DealItem[] = dealItemsRaw.map(item => ({ item: item.item, price_display: item.price_display, price_in_cents: item.price_in_cents, quantity: item.quantity, storeName: flyerIdToStoreName.get(item.flyer_id!) || 'Unknown Store', master_item_name: item.master_item_name, unit_price: item.unit_price, })); setActiveDeals(deals); } catch (e: any) { setError(`Could not fetch active deals: ${e.message}`); } finally { setActiveDealsLoading(false); } }; findActiveDeals(); }, [flyers, watchedItems, isDbConnected, isReady]); useEffect(() => { const calculateTotalActiveItems = async () => { if (!isDbConnected || !isReady || flyers.length === 0) { setTotalActiveItems(0); return; } try { const today = new Date(); today.setHours(0, 0, 0, 0); const validFlyers = flyers.filter(flyer => { if (!flyer.valid_from || !flyer.valid_to) return false; try { const from = new Date(`${flyer.valid_from}T00:00:00`); const to = new Date(`${flyer.valid_to}T00:00:00`); return today >= from && today <= to; } catch (e) { console.error("Error parsing flyer date", e); return false; } }); if (validFlyers.length === 0) { setTotalActiveItems(0); return; } const validFlyerIds = validFlyers.map(f => f.id); const totalCount = await countFlyerItemsForFlyers(validFlyerIds); setTotalActiveItems(totalCount); } catch (e: any) { console.error("Failed to calculate total active items:", e.message); setTotalActiveItems(0); } }; calculateTotalActiveItems(); }, [flyers, isDbConnected, isReady]); const processFiles = async (files: File[], checksum: string, originalFileName: string, updateStage?: (index: number, updates: Partial) => void) => { let stageIndex = 0; // Stage: Validating Flyer updateStage?.(stageIndex, { status: 'in-progress' }); const isFlyer = await withTimeout(isImageAFlyer(files[0]), 15000); if (!isFlyer) { throw new Error("The uploaded image does not appear to be a grocery flyer."); } updateStage?.(stageIndex++, { status: 'completed' }); // stageIndex is now 1 const pageCount = files.length; const coreDataTimeout = 60000 * pageCount; const nonCriticalTimeout = 30000; // Granular stages for core data extraction const storeInfoStageIndex = stageIndex; // Stage 1: Extracting Store Name & Sale Dates const itemExtractionStageIndex = stageIndex + 1; // Stage 2: Extracting All Items from Flyer // Mark both stages as in-progress for the single AI call updateStage?.(storeInfoStageIndex, { status: 'in-progress' }); updateStage?.(itemExtractionStageIndex, { status: 'in-progress', detail: pageCount > 1 ? `(${pageCount} pages)` : undefined }); let progressInterval: number | undefined; let extractedData; try { if (pageCount > 1) { let currentPage = 0; const intervalTime = 2500; // Attach progress bar to the item extraction stage progressInterval = window.setInterval(() => { currentPage++; if (currentPage <= pageCount) { updateStage?.(itemExtractionStageIndex, { progress: { current: currentPage, total: pageCount } }); } else { clearInterval(progressInterval); } }, intervalTime); } extractedData = await withTimeout(extractCoreDataFromImage(files, masterItems), coreDataTimeout); // Mark both stages as completed after the AI call finishes updateStage?.(storeInfoStageIndex, { status: 'completed' }); updateStage?.(itemExtractionStageIndex, { status: 'completed', progress: null }); } finally { if (progressInterval) { clearInterval(progressInterval); } } const { store_name, valid_from, valid_to, items: extractedItems } = extractedData; stageIndex += 2; // Increment by 2 for the stages we just completed. stageIndex is now 3 // Stage: Extracting Store Address let storeAddress: string | null = null; try { updateStage?.(stageIndex, { status: 'in-progress' }); storeAddress = await withTimeout(extractAddressFromImage(files[0]), nonCriticalTimeout); updateStage?.(stageIndex++, { status: 'completed' }); // stageIndex is now 4 } catch (e: any) { console.warn("Non-critical step failed: Address extraction.", e.message); updateStage?.(stageIndex++, { status: 'error', detail: '(Skipped)' }); // stageIndex is now 4 } // Stage: Extracting Store Logo let storeLogoBase64: string | null = null; try { updateStage?.(stageIndex, { status: 'in-progress' }); const logoData = await withTimeout(extractLogoFromImage(files.slice(0, 1)), nonCriticalTimeout); storeLogoBase64 = logoData.store_logo_base_64; updateStage?.(stageIndex++, { status: 'completed' }); // stageIndex is now 5 } catch (e: any) { console.warn("Non-critical step failed: Logo extraction.", e.message); updateStage?.(stageIndex++, { status: 'error', detail: '(Skipped)' }); // stageIndex is now 5 } if (!supabase) { throw new Error("Cannot process flyer: Supabase client not initialized."); } // Stage: Uploading Flyer Image updateStage?.(stageIndex, { status: 'in-progress' }); const imageUrl = await withTimeout(uploadFlyerImage(files[0]), 30000); updateStage?.(stageIndex++, { status: 'completed' }); // stageIndex is now 6 // Stage: Creating Database Record updateStage?.(stageIndex, { status: 'in-progress' }); const newFlyer = await withTimeout(createFlyerRecord(originalFileName, imageUrl, checksum, store_name, valid_from, valid_to, storeAddress), 10000); if (!newFlyer) { throw new Error("Could not create a record for the new flyer."); } // Upload logo if extracted and if the store doesn't have one already. // This is a non-critical, fire-and-forget task. if (storeLogoBase64 && newFlyer.store_id && !newFlyer.store?.logo_url) { uploadLogoAndUpdateStore(newFlyer.store_id, storeLogoBase64); } updateStage?.(stageIndex++, { status: 'completed' }); // stageIndex is now 7 // Stage: Saving Items to Database updateStage?.(stageIndex, { status: 'in-progress' }); const savedItems = await withTimeout(saveFlyerItems(extractedItems, newFlyer.id), 20000); updateStage?.(stageIndex, { status: 'completed' }); return { newFlyer, items: savedItems }; }; const setupProcessingStages = (isPdf: boolean) => { const pendingStatus: StageStatus = 'pending'; const isDbAvailable = !!supabase; const baseStages: ProcessingStage[] = [ ...(isDbAvailable ? [{ name: 'Checking for Duplicates', status: pendingStatus, critical: true }] : []), { name: 'Validating Flyer', status: pendingStatus, critical: true }, { name: 'Extracting Store Name & Sale Dates', status: pendingStatus, critical: true }, { name: 'Extracting All Items from Flyer', status: pendingStatus, critical: true }, { name: 'Extracting Store Address', status: pendingStatus, critical: false }, { name: 'Extracting Store Logo', status: pendingStatus, critical: false }, ...(isDbAvailable ? [ { name: 'Uploading Flyer Image', status: pendingStatus, critical: true }, { name: 'Creating Database Record', status: pendingStatus, critical: true }, { name: 'Saving Items to Database', status: pendingStatus, critical: true }, ] : []), ]; if (isPdf) { return [ { name: 'Analyzing PDF', status: pendingStatus, critical: true }, { name: 'Converting PDF to Images', status: pendingStatus, critical: true }, ...baseStages ]; } return baseStages; }; const handleProcessFiles = useCallback(async (files: FileList) => { if (files.length === 0) return; resetState(); setIsProcessing(true); setProcessingProgress(0); setError(null); if (!supabase) { setError("A database connection is required to process flyers."); setIsProcessing(false); return; } const summary = { processed: [] as string[], skipped: [] as string[], errors: [] as { fileName: string; message: string }[], }; const avgTime = getAverageProcessingTime(); setEstimatedTime(avgTime * files.length); for (let i = 0; i < files.length; i++) { const originalFile = files[i]; setCurrentFile(originalFile.name); setFileCount({ current: i + 1, total: files.length }); setPageProgress(null); const isPdf = originalFile.type === 'application/pdf'; setProcessingStages(setupProcessingStages(isPdf)); const updateStage = (index: number, updates: Partial) => { setProcessingStages(prev => prev.map((stage, j) => (j === index ? { ...stage, ...updates } : stage)) ); }; let currentStageIndex = 0; const startTime = Date.now(); try { let filesToProcess: File[]; let checksum = ''; if (isPdf) { updateStage(currentStageIndex, { status: 'in-progress' }); const onPdfProgress = (currentPage: number, totalPages: number) => { setPageProgress({ current: currentPage, total: totalPages }); }; const { imageFiles, pageCount } = await convertPdfToImageFiles(originalFile, onPdfProgress); filesToProcess = imageFiles; setPageProgress(null); updateStage(currentStageIndex++, { status: 'completed', detail: `(${pageCount} pages)` }); updateStage(currentStageIndex++, { status: 'completed' }); } else { filesToProcess = [originalFile]; } if (supabase) { updateStage(currentStageIndex, { status: 'in-progress' }); checksum = await generateFileChecksum(originalFile); const existing = await findFlyerByChecksum(checksum); if (existing) { console.log(`Skipping duplicate file: ${originalFile.name}`); summary.skipped.push(originalFile.name); updateStage(currentStageIndex, { status: 'completed', detail: '(Duplicate)' }); setProcessingProgress(((i + 1) / files.length) * 100); continue; } updateStage(currentStageIndex++, { status: 'completed' }); } const processFilesUpdateStage = (idx: number, updates: Partial) => updateStage(idx + currentStageIndex, updates); await processFiles(filesToProcess, checksum, originalFile.name, processFilesUpdateStage); summary.processed.push(originalFile.name); } catch (e: any) { console.error(`Failed to process ${originalFile.name}:`, e); summary.errors.push({ fileName: originalFile.name, message: e.message }); setProcessingStages(prev => prev.map(stage => { if (stage.status === 'in-progress' && (stage.critical ?? true)) { return {...stage, status: 'error'}; } return stage; })); await new Promise(resolve => setTimeout(resolve, 2000)); } finally { const duration = (Date.now() - startTime) / 1000; recordProcessingTime(duration); } setProcessingProgress(((i + 1) / files.length) * 100); } await fetchFlyers(); await fetchMasterItems(); setImportSummary(summary); setIsProcessing(false); setCurrentFile(null); setPageProgress(null); setFileCount(null); }, [resetState, fetchFlyers, masterItems, fetchMasterItems]); const handleAddWatchedItem = useCallback(async (itemName: string, category: string) => { if (!supabase || !session || isFakeAuth) return; try { const updatedOrNewItem = await addWatchedItem(session.user.id, itemName, category); setWatchedItems(prevItems => { const itemExists = prevItems.some(item => item.id === updatedOrNewItem.id); if (!itemExists) { const newItems = [...prevItems, updatedOrNewItem]; return newItems.sort((a,b) => a.name.localeCompare(b.name)); } return prevItems; // Item already existed in list }); } catch (e: any) { setError(`Could not add watched item: ${e.message}`); await fetchWatchedItems(session?.user?.id); } }, [session, fetchWatchedItems, isFakeAuth]); const handleRemoveWatchedItem = useCallback(async (masterItemId: number) => { if (!supabase || !session || isFakeAuth) return; try { await removeWatchedItem(session.user.id, masterItemId); setWatchedItems(prevItems => prevItems.filter(item => item.id !== masterItemId)); } catch (e: any) { setError(`Could not remove watched item: ${e.message}`); } }, [session, isFakeAuth]); // --- Shopping List Handlers --- const handleCreateList = useCallback(async (name: string) => { if (!session || isFakeAuth) return; try { const newList = await createShoppingList(session.user.id, name); setShoppingLists(prev => [...prev, newList]); setActiveListId(newList.id); } catch (e: any) { setError(`Could not create list: ${e.message}`); } }, [session, isFakeAuth]); const handleDeleteList = useCallback(async (listId: number) => { if (!session || isFakeAuth) return; try { await deleteShoppingList(listId); const newLists = shoppingLists.filter(l => l.id !== listId); setShoppingLists(newLists); if (activeListId === listId) { setActiveListId(newLists.length > 0 ? newLists[0].id : null); } } catch (e: any) { setError(`Could not delete list: ${e.message}`); } }, [session, shoppingLists, activeListId, isFakeAuth]); const handleAddShoppingListItem = useCallback(async (listId: number, item: { masterItemId?: number, customItemName?: string }) => { if (!session || isFakeAuth) return; try { const newItem = await addShoppingListItem(listId, item); setShoppingLists(prevLists => prevLists.map(list => { if (list.id === listId) { // Avoid adding duplicates to the state if it's already there const itemExists = list.items.some(i => i.id === newItem.id); if (itemExists) return list; return { ...list, items: [...list.items, newItem] }; } return list; })); } catch (e: any) { setError(`Could not add item to list: ${e.message}`); } }, [session, isFakeAuth]); const handleUpdateShoppingListItem = useCallback(async (itemId: number, updates: Partial) => { if (!session || !activeListId || isFakeAuth) return; try { const updatedItem = await updateShoppingListItem(itemId, updates); setShoppingLists(prevLists => prevLists.map(list => { if (list.id === activeListId) { return { ...list, items: list.items.map(i => i.id === itemId ? updatedItem : i) }; } return list; })); } catch (e: any) { setError(`Could not update list item: ${e.message}`); } }, [session, activeListId, isFakeAuth]); const handleRemoveShoppingListItem = useCallback(async (itemId: number) => { if (!session || !activeListId || isFakeAuth) return; try { await removeShoppingListItem(itemId); setShoppingLists(prevLists => prevLists.map(list => { if (list.id === activeListId) { return { ...list, items: list.items.filter(i => i.id !== itemId) }; } return list; })); } catch (e: any) { setError(`Could not remove list item: ${e.message}`); } }, [session, activeListId, isFakeAuth]); const handleFakeLogin = (email: string, pass: string) => { if (email === 'test@test.com' && pass === 'pass123') { setIsAuthenticated(true); setIsFakeAuth(true); // Mark that we are using fake auth setLoginError(null); } else { setLoginError('Invalid credentials'); } }; const handleSignOut = () => { if (supabase && !isFakeAuth) { supabase.auth.signOut(); } // For fake auth, signing out just resets local state. // The onAuthStateChange listener will handle real sign-outs. setIsAuthenticated(false); setIsFakeAuth(false); setSession(null); }; const hasData = flyerItems.length > 0; if (!isAuthenticated) { return ; } return (
setIsProfileManagerOpen(true)} onOpenVoiceAssistant={() => setIsVoiceAssistantOpen(true)} onSignOut={handleSignOut} /> {session && profile && !isFakeAuth && ( setIsProfileManagerOpen(false)} session={session} profile={profile} onProfileUpdate={(updatedProfile) => setProfile(updatedProfile)} /> )} {session && ( setIsVoiceAssistantOpen(false)} /> )}
{isDbConnected ? ( <> {isReady && ( )} setIsReady(true)} /> ) : ( setIsDbConnected(true)} /> )}
{isProcessing ? ( ) : selectedFlyer ? ( <> {hasData && ( <> handleAddShoppingListItem(activeListId!, { masterItemId })} /> )} ) : importSummary ? ( setImportSummary(null)} /> ) : (

Welcome to Flyer Crawler!

Upload a new grocery flyer to begin, or select a previously processed flyer from the list on the left.

)}
{isDbConnected && ( <> handleAddShoppingListItem(activeListId!, item)} onUpdateItem={handleUpdateShoppingListItem} onRemoveItem={handleRemoveShoppingListItem} /> handleAddShoppingListItem(activeListId!, { masterItemId })} /> )}
); } export default App;