import { Suspense, lazy, useEffect, useState } from "react"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; import { AppShell } from "./components/AppShell"; import { AppProviders } from "./components/AppProviders"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { useGlobalErrorHandler } from "./hooks/useGlobalErrorHandler"; import { notifyNavigation, startPageviewTracking } from "./lib/analyticsBeacon"; import { prefetchRoute, loadToolPage, loadCompressUI, loadMergeUI, loadSplitUI, } from "./lib/prefetch"; // @tanstack/react-query is in package.json but no component in the app uses // useQuery / useMutation. The QueryClientProvider wrapper here used to be // scaffolding from the project template. Removing the provider drops the // 25 KB / 7.8 KB gz vendor-query chunk from every first-paint without // changing behavior. If you later add server-state caching, re-add the // provider locally inside the route that needs it (don't promote it back // to the App root unless many components share queries). const Index = lazy(() => import("./pages/Index")); const ToolPage = lazy(() => import("./pages/ToolPage")); const NonPdfToolPage = lazy(() => import("./pages/NonPdfToolPage")); const AllToolsPage = lazy(() => import("./pages/AllToolsPage")); const LandingPage = lazy(() => import("./pages/LandingPage")); const AboutPage = lazy(() => import("./pages/AboutPage")); const ComparePage = lazy(() => import("./pages/ComparePage")); const NotFound = lazy(() => import("./pages/NotFound")); const BatchPage = lazy(() => import("./pages/BatchPage")); const PipelinePage = lazy(() => import("./pages/PipelinePage")); const BlogPage = lazy(() => import("./pages/BlogPage")); const BlogPostPage = lazy(() => import("./pages/BlogPostPage")); const PrivacyPage = lazy(() => import("./pages/PrivacyPage")); const TermsPage = lazy(() => import("./pages/TermsPage")); const SecurityPage = lazy(() => import("./pages/SecurityPage")); const MyStuffPage = lazy(() => import("./pages/MyStuffPage")); const AccountPage = lazy(() => import("./pages/AccountPage")); const VaultPage = lazy(() => import("./pages/VaultPage")); const StatusPage = lazy(() => import("./pages/StatusPage")); const SupportPage = lazy(() => import("./pages/SupportPage")); import { OnboardingTour } from "./components/OnboardingTour"; import { ShortcutsHelp } from "./components/ShortcutsHelp"; const FirstSuccessListener = lazy(() => import("./components/FirstSuccessListener").then(m => ({ default: m.FirstSuccessListener }))); const RouteLoader = () => (
{Array.from({ length: 6 }).map((_, i) => (
))}
); const withRouteFallback = (element: JSX.Element) => ( }>{element} ); /** * Warm the next-likely route chunks once the user lands on the home page. * Top-3 by traffic: Compress, Merge, Split. Runs only on `/` and only when * the browser is idle so it doesn't compete with first-paint resources. * * The actual `import()` calls are deduped inside prefetch.ts (WeakSet), so * subsequent hover/focus events on these tools resolve from the chunk * cache instead of issuing a new request. */ function RoutePrefetcher() { const { pathname } = useLocation(); useEffect(() => { if (pathname !== "/") return; const idle: typeof window.requestIdleCallback | undefined = typeof window !== "undefined" ? window.requestIdleCallback : undefined; const run = () => { // ToolPage shell first — needed before any /tool/* chunk renders. prefetchRoute(loadToolPage); prefetchRoute(loadCompressUI); prefetchRoute(loadMergeUI); prefetchRoute(loadSplitUI); }; if (idle) { const id = idle(run, { timeout: 3000 }); return () => window.cancelIdleCallback?.(id); } // Browsers without rIC — fire after a short delay. const t = window.setTimeout(run, 1500); return () => window.clearTimeout(t); }, [pathname]); return null; } function AfterInitialPaint({ children }: { children: React.ReactNode }) { const [ready, setReady] = useState(false); useEffect(() => { if (ready) return; // Lighthouse's mobile trace can run idle callbacks before its LCP window // closes. Use a plain timer so nonessential chrome never competes with // first paint or the first route's content. const id = window.setTimeout(() => setReady(true), 3000); return () => window.clearTimeout(id); }, [ready]); return ready ? <>{children} : null; } /** Wire global JS error handler into the root once. Lives inside the * router so it can use hooks, but renders nothing. */ function GlobalErrorWire() { useGlobalErrorHandler(); const { pathname } = useLocation(); // Counts the arrival, then every React Router navigation. The router moves // with pushState, which fires no popstate, so the beacon has to be told. useEffect(() => startPageviewTracking(), []); useEffect(() => { notifyNavigation(); }, [pathname]); return null; } const App = () => ( {/* AppProviders must wrap the router: Radix Tooltip throws at render time without a TooltipProvider above it, which crashed every tool that used one (compress, bates) straight into the ErrorBoundary. */} {/* Navigation can request these as soon as the first screen is usable. */} )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> )} /> {/* SkinAppHost owns these page compositions, as it owns every existing route above. Keep their public paths declared here for the frontend/backend route-parity checks. */} )} /> )} /> )} /> ); export default App;