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;