mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
432 lines
17 KiB
TypeScript
432 lines
17 KiB
TypeScript
import { lazy, Suspense, useEffect, useRef } from "react";
|
|
import { Toaster } from "@/components/ui/toaster";
|
|
import { Toaster as Sonner } from "@/components/ui/sonner";
|
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
|
|
import { AuthProvider } from "@/hooks/useAuth";
|
|
import { AuthModalProvider } from "@/contexts/AuthModalContext";
|
|
import { MFAStepUpProvider } from "@/contexts/MFAStepUpContext";
|
|
import { APIConnectivityProvider, useAPIConnectivity } from "@/contexts/APIConnectivityContext";
|
|
import { LocationAutoDetectProvider } from "@/components/providers/LocationAutoDetectProvider";
|
|
import { AnalyticsWrapper } from "@/components/analytics/AnalyticsWrapper";
|
|
import { Footer } from "@/components/layout/Footer";
|
|
import { PageLoader } from "@/components/loading/PageSkeletons";
|
|
import { RouteErrorBoundary } from "@/components/error/RouteErrorBoundary";
|
|
import { AdminErrorBoundary } from "@/components/error/AdminErrorBoundary";
|
|
import { EntityErrorBoundary } from "@/components/error/EntityErrorBoundary";
|
|
import { breadcrumb } from "@/lib/errorBreadcrumbs";
|
|
import { handleError } from "@/lib/errorHandler";
|
|
import { RetryStatusIndicator } from "@/components/ui/retry-status-indicator";
|
|
import { APIStatusBanner } from "@/components/ui/api-status-banner";
|
|
import { ResilienceProvider } from "@/components/layout/ResilienceProvider";
|
|
import { useAdminRoutePreload } from "@/hooks/useAdminRoutePreload";
|
|
import { useVersionCheck } from "@/hooks/useVersionCheck";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
// Core routes (eager-loaded for best UX)
|
|
import Index from "./pages/Index";
|
|
import Parks from "./pages/Parks";
|
|
import Rides from "./pages/Rides";
|
|
import Search from "./pages/Search";
|
|
import Auth from "./pages/Auth";
|
|
|
|
// Temporary test component for error logging verification
|
|
import { TestErrorLogging } from "./test-error-logging";
|
|
|
|
// Detail routes (lazy-loaded)
|
|
const ParkDetail = lazy(() => import("./pages/ParkDetail"));
|
|
const RideDetail = lazy(() => import("./pages/RideDetail"));
|
|
const ParkRides = lazy(() => import("./pages/ParkRides"));
|
|
const Manufacturers = lazy(() => import("./pages/Manufacturers"));
|
|
const ManufacturerDetail = lazy(() => import("./pages/ManufacturerDetail"));
|
|
const ManufacturerRides = lazy(() => import("./pages/ManufacturerRides"));
|
|
const ManufacturerModels = lazy(() => import("./pages/ManufacturerModels"));
|
|
const RideModelDetail = lazy(() => import("./pages/RideModelDetail"));
|
|
const RideModelRides = lazy(() => import("./pages/RideModelRides"));
|
|
const Designers = lazy(() => import("./pages/Designers"));
|
|
const DesignerDetail = lazy(() => import("./pages/DesignerDetail"));
|
|
const DesignerRides = lazy(() => import("./pages/DesignerRides"));
|
|
const ParkOwners = lazy(() => import("./pages/ParkOwners"));
|
|
const PropertyOwnerDetail = lazy(() => import("./pages/PropertyOwnerDetail"));
|
|
const OwnerParks = lazy(() => import("./pages/OwnerParks"));
|
|
const Operators = lazy(() => import("./pages/Operators"));
|
|
const OperatorDetail = lazy(() => import("./pages/OperatorDetail"));
|
|
const OperatorParks = lazy(() => import("./pages/OperatorParks"));
|
|
const BlogIndex = lazy(() => import("./pages/BlogIndex"));
|
|
const BlogPost = lazy(() => import("./pages/BlogPost"));
|
|
const Terms = lazy(() => import("./pages/Terms"));
|
|
const Privacy = lazy(() => import("./pages/Privacy"));
|
|
const SubmissionGuidelines = lazy(() => import("./pages/SubmissionGuidelines"));
|
|
const Contact = lazy(() => import("./pages/Contact"));
|
|
|
|
// Admin routes (lazy-loaded - heavy bundle)
|
|
const AdminDashboard = lazy(() => import("./pages/AdminDashboard"));
|
|
const AdminModeration = lazy(() => import("./pages/AdminModeration"));
|
|
const AdminReports = lazy(() => import("./pages/AdminReports"));
|
|
const AdminSystemLog = lazy(() => import("./pages/AdminSystemLog"));
|
|
const AdminUsers = lazy(() => import("./pages/AdminUsers"));
|
|
const AdminBlog = lazy(() => import("./pages/AdminBlog"));
|
|
const AdminSettings = lazy(() => import("./pages/AdminSettings"));
|
|
const AdminContact = lazy(() => import("./pages/admin/AdminContact"));
|
|
const AdminEmailSettings = lazy(() => import("./pages/admin/AdminEmailSettings"));
|
|
const ErrorMonitoring = lazy(() => import("./pages/admin/ErrorMonitoring"));
|
|
const ErrorLookup = lazy(() => import("./pages/admin/ErrorLookup"));
|
|
|
|
// User routes (lazy-loaded)
|
|
const Profile = lazy(() => import("./pages/Profile"));
|
|
const UserSettings = lazy(() => import("./pages/UserSettings"));
|
|
const AuthCallback = lazy(() => import("./pages/AuthCallback"));
|
|
|
|
// Utility routes (lazy-loaded)
|
|
const NotFound = lazy(() => import("./pages/NotFound"));
|
|
const ForceLogout = lazy(() => import("./pages/ForceLogout"));
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnWindowFocus: false, // Disable automatic refetch on tab focus
|
|
refetchOnMount: true, // Keep refetch on component mount
|
|
refetchOnReconnect: true, // Keep refetch on network reconnect
|
|
retry: 1, // Keep retry attempts
|
|
staleTime: 30000, // 30 seconds - queries stay fresh for 30s
|
|
gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache for 5 mins
|
|
},
|
|
mutations: {
|
|
onError: (error: unknown, variables: unknown, context: unknown) => {
|
|
// Track mutation errors with breadcrumbs
|
|
const contextObj = context as { endpoint?: string } | undefined;
|
|
const errorObj = error as { status?: number } | undefined;
|
|
|
|
breadcrumb.apiCall(
|
|
contextObj?.endpoint || 'mutation',
|
|
'MUTATION',
|
|
errorObj?.status || 500
|
|
);
|
|
|
|
// Handle error with tracking
|
|
handleError(error, {
|
|
action: 'Mutation failed',
|
|
metadata: {
|
|
variables,
|
|
context,
|
|
},
|
|
});
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Navigation tracking component - must be inside Router context
|
|
function NavigationTracker() {
|
|
const location = useLocation();
|
|
const prevLocation = useRef<string>('');
|
|
|
|
useEffect(() => {
|
|
const from = prevLocation.current || undefined;
|
|
breadcrumb.navigation(location.pathname, from);
|
|
prevLocation.current = location.pathname;
|
|
|
|
// Clear chunk load reload flag on successful navigation
|
|
sessionStorage.removeItem('chunk-load-reload');
|
|
}, [location.pathname]);
|
|
|
|
return null;
|
|
}
|
|
|
|
function AppContent(): React.JSX.Element {
|
|
// Check if API status banner is visible to add padding
|
|
const { isAPIReachable, isBannerDismissed } = useAPIConnectivity();
|
|
const showBanner = !isAPIReachable && !isBannerDismissed;
|
|
|
|
// Preload admin routes for moderators/admins
|
|
useAdminRoutePreload();
|
|
|
|
// Monitor for new deployments
|
|
useVersionCheck();
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<ResilienceProvider>
|
|
<APIStatusBanner />
|
|
<div className={cn(showBanner && "pt-20")}>
|
|
<NavigationTracker />
|
|
<LocationAutoDetectProvider />
|
|
<RetryStatusIndicator />
|
|
<Toaster />
|
|
<Sonner />
|
|
<div className="min-h-screen flex flex-col">
|
|
<div className="flex-1">
|
|
<Suspense fallback={<PageLoader />}>
|
|
<RouteErrorBoundary>
|
|
<Routes>
|
|
{/* Core routes - eager loaded */}
|
|
<Route path="/" element={<Index />} />
|
|
<Route path="/parks" element={<Parks />} />
|
|
<Route path="/rides" element={<Rides />} />
|
|
<Route path="/search" element={<Search />} />
|
|
<Route path="/auth" element={<Auth />} />
|
|
|
|
{/* Detail routes with entity error boundaries */}
|
|
<Route
|
|
path="/parks/:slug"
|
|
element={
|
|
<EntityErrorBoundary entityType="park">
|
|
<ParkDetail />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/parks/:parkSlug/rides"
|
|
element={
|
|
<EntityErrorBoundary entityType="park">
|
|
<ParkRides />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/parks/:parkSlug/rides/:rideSlug"
|
|
element={
|
|
<EntityErrorBoundary entityType="ride">
|
|
<RideDetail />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route path="/manufacturers" element={<Manufacturers />} />
|
|
<Route
|
|
path="/manufacturers/:slug"
|
|
element={
|
|
<EntityErrorBoundary entityType="manufacturer">
|
|
<ManufacturerDetail />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/manufacturers/:manufacturerSlug/rides"
|
|
element={
|
|
<EntityErrorBoundary entityType="manufacturer">
|
|
<ManufacturerRides />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/manufacturers/:manufacturerSlug/models"
|
|
element={
|
|
<EntityErrorBoundary entityType="manufacturer">
|
|
<ManufacturerModels />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/manufacturers/:manufacturerSlug/models/:modelSlug"
|
|
element={
|
|
<EntityErrorBoundary entityType="manufacturer">
|
|
<RideModelDetail />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/manufacturers/:manufacturerSlug/models/:modelSlug/rides"
|
|
element={
|
|
<EntityErrorBoundary entityType="manufacturer">
|
|
<RideModelRides />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route path="/designers" element={<Designers />} />
|
|
<Route
|
|
path="/designers/:slug"
|
|
element={
|
|
<EntityErrorBoundary entityType="designer">
|
|
<DesignerDetail />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/designers/:designerSlug/rides"
|
|
element={
|
|
<EntityErrorBoundary entityType="designer">
|
|
<DesignerRides />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route path="/owners" element={<ParkOwners />} />
|
|
<Route
|
|
path="/owners/:slug"
|
|
element={
|
|
<EntityErrorBoundary entityType="owner">
|
|
<PropertyOwnerDetail />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/owners/:ownerSlug/parks"
|
|
element={
|
|
<EntityErrorBoundary entityType="owner">
|
|
<OwnerParks />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route path="/operators" element={<Operators />} />
|
|
<Route
|
|
path="/operators/:slug"
|
|
element={
|
|
<EntityErrorBoundary entityType="operator">
|
|
<OperatorDetail />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/operators/:operatorSlug/parks"
|
|
element={
|
|
<EntityErrorBoundary entityType="operator">
|
|
<OperatorParks />
|
|
</EntityErrorBoundary>
|
|
}
|
|
/>
|
|
<Route path="/blog" element={<BlogIndex />} />
|
|
<Route path="/blog/:slug" element={<BlogPost />} />
|
|
<Route path="/terms" element={<Terms />} />
|
|
<Route path="/privacy" element={<Privacy />} />
|
|
<Route path="/submission-guidelines" element={<SubmissionGuidelines />} />
|
|
<Route path="/contact" element={<Contact />} />
|
|
|
|
{/* User routes - lazy loaded */}
|
|
<Route path="/auth/callback" element={<AuthCallback />} />
|
|
<Route path="/profile" element={<Profile />} />
|
|
<Route path="/profile/:username" element={<Profile />} />
|
|
<Route path="/settings" element={<UserSettings />} />
|
|
|
|
{/* Admin routes with admin error boundaries */}
|
|
<Route
|
|
path="/admin"
|
|
element={
|
|
<AdminErrorBoundary section="Dashboard">
|
|
<AdminDashboard />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/moderation"
|
|
element={
|
|
<AdminErrorBoundary section="Moderation Queue">
|
|
<AdminModeration />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/reports"
|
|
element={
|
|
<AdminErrorBoundary section="Reports">
|
|
<AdminReports />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/system-log"
|
|
element={
|
|
<AdminErrorBoundary section="System Log">
|
|
<AdminSystemLog />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/users"
|
|
element={
|
|
<AdminErrorBoundary section="User Management">
|
|
<AdminUsers />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/blog"
|
|
element={
|
|
<AdminErrorBoundary section="Blog Management">
|
|
<AdminBlog />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/settings"
|
|
element={
|
|
<AdminErrorBoundary section="Settings">
|
|
<AdminSettings />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/contact"
|
|
element={
|
|
<AdminErrorBoundary section="Contact Management">
|
|
<AdminContact />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/email-settings"
|
|
element={
|
|
<AdminErrorBoundary section="Email Settings">
|
|
<AdminEmailSettings />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/error-monitoring"
|
|
element={
|
|
<AdminErrorBoundary section="Error Monitoring">
|
|
<ErrorMonitoring />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin/error-lookup"
|
|
element={
|
|
<AdminErrorBoundary section="Error Lookup">
|
|
<ErrorLookup />
|
|
</AdminErrorBoundary>
|
|
}
|
|
/>
|
|
|
|
{/* Utility routes - lazy loaded */}
|
|
<Route path="/force-logout" element={<ForceLogout />} />
|
|
|
|
{/* Temporary test route - DELETE AFTER TESTING */}
|
|
<Route path="/test-error-logging" element={<TestErrorLogging />} />
|
|
|
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
|
<Route path="*" element={<NotFound />} />
|
|
</Routes>
|
|
</RouteErrorBoundary>
|
|
</Suspense>
|
|
</div>
|
|
<Footer />
|
|
</div>
|
|
</div>
|
|
</ResilienceProvider>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
const App = (): React.JSX.Element => {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<AuthProvider>
|
|
<AuthModalProvider>
|
|
<MFAStepUpProvider>
|
|
<APIConnectivityProvider>
|
|
<BrowserRouter>
|
|
<AppContent />
|
|
</BrowserRouter>
|
|
</APIConnectivityProvider>
|
|
</MFAStepUpProvider>
|
|
</AuthModalProvider>
|
|
</AuthProvider>
|
|
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} position="bottom" />}
|
|
<AnalyticsWrapper />
|
|
</QueryClientProvider>
|
|
);
|
|
};
|
|
|
|
export default App;
|