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()
}}
/>