import { useEffect, useState } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate, useParams } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { analyticsService } from './services/analytics.service'; import { GalleryAuthProvider, MaintenanceProvider } from './contexts'; import { ThemeProvider } from './contexts/ThemeContext'; import { GalleryPage } from './pages/GalleryPage'; import { ClientAccessPage } from './pages/ClientAccessPage'; import { PreviewPage } from './pages/gallery/PreviewPage'; import { LegalPage } from './pages/public/LegalPage'; import { AdminLoginPage, AdminDashboard, EventsListPage, CreateEventPage, EventDetailsPage, EventFeedbackPage, ArchivesPage, AnalyticsPage, SettingsPage, UserManagementPage, CustomerManagementPage, CustomerDetailPage, WebhookDeliveriesPage } from './pages/admin'; import { AcceptInvitePage } from './pages/public/AcceptInvitePage'; import { CustomerLoginPage, CustomerDashboardPage, CustomerAcceptInvitePage, CustomerLayout, CustomerProfilePage, CustomerCalendarPage, CustomerQuotesPage, CustomerBillsPage, CustomerResetPasswordPage, } from './pages/customer'; import { CustomerAuthProvider } from './contexts/CustomerAuthContext'; import { AdminLayout, AdminAuthWrapper } from './components/admin'; import { ClientsLayout } from './components/admin/ClientsLayout'; import { RequireFeature } from './components/admin/RequireFeature'; import { PageErrorBoundary, OfflineIndicator, SkipLink, DynamicFavicon, RobotsMetaTags, CMSContentBlock } from './components/common'; import { MaintenanceWrapper } from './components/MaintenanceWrapper'; import { GlobalThemeProvider } from './components/GlobalThemeProvider'; import { usePublicSettings } from './hooks/usePublicSettings'; // Create a client const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1, }, }, }); // Bootstraps Umami analytics from /public/settings. Lives inside QueryClientProvider // so it shares the public-settings cache with every other consumer of usePublicSettings. function AnalyticsBootstrap() { const { data: settings, isError } = usePublicSettings(); useEffect(() => { if (!settings && !isError) return; const envUmamiUrl = import.meta.env.VITE_UMAMI_URL; const envUmamiWebsiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID; if (settings?.umami_enabled && settings.umami_url && settings.umami_website_id) { analyticsService.initialize({ websiteId: settings.umami_website_id, hostUrl: settings.umami_url, autoTrack: true, doNotTrack: true, }); return; } if (envUmamiUrl && envUmamiWebsiteId && (isError || settings?.enable_analytics !== false)) { analyticsService.initialize({ websiteId: envUmamiWebsiteId, hostUrl: envUmamiUrl, autoTrack: true, doNotTrack: true, }); } }, [settings, isError]); return null; } /** * Backward-compat redirect for /admin/customers/:id → /admin/clients/accounts/:id. * Needed because can't interpolate route params and we * want stale bookmarks / email links to keep working after the Clients * section reorg. */ function RedirectCustomerDetail() { const { id } = useParams(); return ; } function App() { // Track dark mode for toast theming const [toastTheme, setToastTheme] = useState<'light' | 'dark'>('light'); useEffect(() => { const observer = new MutationObserver(() => { setToastTheme(document.documentElement.classList.contains('dark') ? 'dark' : 'light'); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); return () => observer.disconnect(); }, []); return ( {/* Public gallery routes */} } /> } /> } /> {/* Admin routes - wrap with AdminAuthProvider */} }> } /> }> } /> } /> } /> } /> } /> } /> {/* Feature-gated surfaces — redirect to /admin/dashboard when flag is off. */} }> } /> }> } /> {/* Clients section (#354 follow-up). Parent route gated by the top-level `clients` flag — when off the sidebar entry is hidden and every /admin/clients/* URL redirects to /admin/dashboard. Inside, the ClientsLayout renders a Settings-style sub-nav and the active sub-feature's page through an Outlet. Each sub-route is feature-flagged independently. */} }> }> }> } /> } /> {/* Default: send /admin/clients (no sub-path) to the first available sub-feature. Today that's always accounts; when calendar/quotes ship they get their own routes here and the empty-state in ClientsLayout handles the rare "parent on, all children off" case. */} } /> {/* Old /admin/customers paths now live under /admin/clients/accounts. Kept indefinitely as redirects so existing bookmarks and email links don't 404. */} } /> } /> } /> } /> {/* Old top-level routes — these surfaces now live as Settings tabs (#feature-flags-settings-reorg). Kept indefinitely as redirects so existing bookmarks and external links don't 404. */} } /> } /> } /> } /> } /> } /> {/* Public invitation acceptance page */} } /> {/* Customer surface (#354). Strictly separate provider / cookie / API surface from /admin/*. The customerPortal feature flag hides the *admin-side* surfaces (sidebar entry, /admin/customers routes, CustomerAccountPicker) via RequireFeature. The customer-side /customer/* tree stays publicly reachable so existing customers can still log in even if the admin temporarily flips the flag off — and because RequireFeature reads from FeatureFlagsProvider (admin-only context), gating these routes here would crash unauthenticated visitors with an unmounted-provider error. */} {/* Public surfaces: login, accept-invite, reset — no CustomerLayout (their own branded shells). */} } /> } /> } /> {/* Authenticated surfaces share the sidebar layout (Outlet pattern, mirrors AdminLayout). The CustomerLayout itself enforces auth — bouncing unauthenticated visitors to /customer/login. */} }> } /> } /> } /> } /> } /> } /> } /> {/* Public legal pages */} } /> } /> } /> {/* Default redirect */} } /> {/* Customisable 404 (#324) — caught here for any path that didn't match. Top-level `/:slug` is consumed above by LegalPage; this picks up deeper unknown paths. */} } /> {/* Offline indicator */} {/* Toast notifications */} ); } export default App;