Ctrl K
ComunidadGuías…

frontend/src/App.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 = () => (
  <div className="min-h-[40vh] animate-pulse px-4 py-10 sm:px-6">
    <div className="mx-auto max-w-7xl space-y-4">
      <div className="h-6 w-52 rounded-md bg-secondary/70" />
      <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
        {Array.from({ length: 6 }).map((_, i) => (
          <div key={i} className="h-28 rounded-xl border border-border bg-card/70" />
        ))}
      </div>
    </div>
  </div>
);

const withRouteFallback = (element: JSX.Element) => (
  <Suspense fallback={<RouteLoader />}>{element}</Suspense>
);

/**
 * 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 = () => (
  <ErrorBoundary scope="app">
    <Sonner />
    {/* 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. */}
    <AppProviders>
    <BrowserRouter>
      <GlobalErrorWire />
      {/* Navigation can request these as soon as the first screen is usable. */}
      <ShortcutsHelp />
      <OnboardingTour />
      <AfterInitialPaint>
        <Suspense fallback={null}>
          <FirstSuccessListener />
        </Suspense>
      </AfterInitialPaint>
      <RoutePrefetcher />
      <AppShell>
        <Routes>
          <Route path="/" element={withRouteFallback(<Index />)} />
          <Route path="/about" element={withRouteFallback(<AboutPage />)} />
          <Route path="/compare" element={withRouteFallback(<ComparePage />)} />
          <Route path="/compare/:competitor" element={withRouteFallback(<ComparePage />)} />
          <Route path="/tool/:slug" element={withRouteFallback(<ToolPage />)} />
          <Route path="/tools" element={withRouteFallback(<AllToolsPage />)} />
          <Route path="/tools/:slug" element={withRouteFallback(<NonPdfToolPage />)} />
          <Route path="/batch" element={withRouteFallback(<BatchPage />)} />
          <Route path="/pipeline" element={withRouteFallback(<PipelinePage />)} />
          <Route path="/blog" element={withRouteFallback(<BlogPage />)} />
          <Route path="/blog/:slug" element={withRouteFallback(<BlogPostPage />)} />
          <Route path="/privacy" element={withRouteFallback(<PrivacyPage />)} />
          <Route path="/security" element={withRouteFallback(<SecurityPage />)} />
          <Route path="/terms" element={withRouteFallback(<TermsPage />)} />
          <Route path="/my-stuff" element={withRouteFallback(<MyStuffPage />)} />
          <Route path="/my-stuff/vault" element={withRouteFallback(<VaultPage />)} />
          <Route path="/account" element={withRouteFallback(<AccountPage />)} />
          <Route path="/account/keys" element={withRouteFallback(<AccountPage />)} />
          {/* 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. */}
          <Route path="/account/sign-in" element={null} />
          <Route path="/account/sign-up" element={null} />
          <Route path="/account/settings" element={null} />
          <Route path="/settings" element={null} />
          <Route path="/ai" element={null} />
          <Route path="/api" element={null} />
          <Route path="/trust" element={null} />
          <Route path="/status" element={withRouteFallback(<StatusPage />)} />
          <Route path="/support" element={withRouteFallback(<SupportPage />)} />
          <Route path="*" element={withRouteFallback(<NotFound />)} />
        </Routes>
      </AppShell>
    </BrowserRouter>
    </AppProviders>
  </ErrorBoundary>
);

export default App;