import { useEffect, useState } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } 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, EmailConfigPage, ArchivesPage, AnalyticsPage, BrandingPage, SettingsPage, BackupManagement, CMSPage, UserManagementPage, EventTypesPage, WebhookDeliveriesPage } from './pages/admin'; import { AcceptInvitePage } from './pages/public/AcceptInvitePage'; import { AdminLayout, AdminAuthWrapper } from './components/admin'; 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; } 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 */} }> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Public invitation acceptance page */} } /> {/* 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;