import { BrowserRouter, Routes, Route, Link, useLocation, useNavigate, Navigate } from 'react-router-dom' import { QueryClient, QueryClientProvider, QueryCache, MutationCache } from '@tanstack/react-query' import { ErrorBoundary } from './components/ErrorBoundary' import { ShortcutHelpBanner, SHOW_SHORTCUT_HELP_EVENT } from './components/ShortcutHelpBanner' import { OnboardingTour, useOnboarding } from './components/OnboardingTour' import { showApiError } from './lib/toast' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { requestFocusSearch } from './hooks/useFocusSearch' import Dashboard from './pages/Dashboard' import MealDetail from './pages/MealDetail' import Pantry from './pages/Pantry' import Recipes from './pages/Recipes' import Recommended from './pages/Recommended' import RecipeDetail from './pages/RecipeDetail' import ShoppingList from './pages/ShoppingList' import NotFound from './pages/NotFound' const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false, }, }, queryCache: new QueryCache({ onError: (error) => { showApiError(error, 'Failed to load data') }, }), mutationCache: new MutationCache({ onError: (error) => { showApiError(error) }, }), }) function Navigation() { const location = useLocation() const path = location.pathname const isActive = (prefix: string) => path === prefix || path.startsWith(prefix + '/') const linkClass = (prefix: string) => `inline-flex items-center px-2 sm:px-3 py-2 text-sm font-medium rounded-lg transition-colors whitespace-nowrap ${ isActive(prefix) ? 'text-primary-700 bg-primary-50' : 'text-surface-600 hover:bg-surface-100' }` return ( ) } function GlobalShortcuts() { const navigate = useNavigate() useKeyboardShortcuts({ 'g d': () => navigate('/'), 'g r': () => navigate('/recipes'), 'g p': () => navigate('/pantry'), 'g s': () => navigate('/shopping-list'), '/': () => requestFocusSearch(), '?': () => window.dispatchEvent(new CustomEvent(SHOW_SHORTCUT_HELP_EVENT)), }) return null } function App() { // Onboarding tour state lives at the App root so the localStorage // key is read once on mount. The tour itself is mounted inside the // router (it needs useLocation / useNavigate). const onboarding = useOnboarding() return (
} /> } /> } /> } /> } /> } /> } /> } /> } />
{ // Dismiss path (X / Skip / Esc / "Got it"). The tour // already wrote the localStorage key; flip the // App-level flag to true so the early-return fires and // the dialog disappears. onboarding.markComplete() }} onReset={() => { // Re-show path (?reset-tour=1). The tour already // cleared the localStorage key; flip the App-level // flag to false so the tour re-appears. onboarding.reset() }} />
) } export default App