254 lines · 8.7 KB · TypeScript
RawDownload
import React from "react";import * as Sentry from "@sentry/react";import {  HeadContent,  Outlet,  Scripts,  createRootRoute,  useLocation,  useNavigate,} from "@tanstack/react-router";import { AutumnProvider } from "autumn-js/react";import posthog from "posthog-js";import { PostHogProvider } from "posthog-js/react";import type { FrontendErrorReporter } from "@executor-js/react/api/error-reporting";import { ExecutorProvider } from "@executor-js/react/api/provider";import { OrganizationProvider } from "@executor-js/react/api/organization-context";import { Skeleton } from "@executor-js/react/components/skeleton";import { Toaster } from "@executor-js/react/components/sonner";import { ExecutorPluginsProvider } from "@executor-js/sdk/client";import { plugins as clientPlugins } from "virtual:executor/plugins-client";import { AuthProvider, useAuth } from "../web/auth";import { SupportOptions } from "../web/components/support-options";import { LoginPage } from "../web/pages/login";import { Shell } from "../web/shell";import appCss from "@executor-js/react/globals.css?url"; const ONBOARDING_PATHS = new Set(["/create-org", "/setup-mcp"]); if (typeof window !== "undefined" && import.meta.env.VITE_PUBLIC_SENTRY_DSN) {  Sentry.init({    dsn: import.meta.env.VITE_PUBLIC_SENTRY_DSN,    tunnel: "/api/sentry-tunnel",    tracesSampleRate: 0,    replaysSessionSampleRate: 0.1,    replaysOnErrorSampleRate: 1.0,  });} if (typeof window !== "undefined" && import.meta.env.VITE_PUBLIC_POSTHOG_KEY) {  const analyticsPath = (import.meta.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a").replace(    /^\/+|\/+$/g,    "",  );   posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {    api_host:      import.meta.env.VITE_PUBLIC_POSTHOG_HOST ?? `${window.location.origin}/api/${analyticsPath}`,    ui_host: "https://us.posthog.com",    defaults: "2025-05-24",    person_profiles: "identified_only",    disable_session_recording: false,    session_recording: {      maskAllInputs: true,      maskTextSelector: "[data-ph-mask]",      blockSelector: "[data-ph-block]",    },  });} const captureFrontendError: FrontendErrorReporter = (error, context) => {  Sentry.captureException(error, (scope) => {    scope.setTag("executor.ui.surface", context.surface);    scope.setTag("executor.ui.action", context.action);    scope.setTag("executor.ui.severity", context.severity ?? "error");    scope.setContext("executor.ui", {      surface: context.surface,      action: context.action,      message: context.message,      metadata: context.metadata,    });    return scope;  });}; export const Route = createRootRoute({  head: () => ({    meta: [      { charSet: "utf-8" },      { name: "viewport", content: "width=device-width, initial-scale=1" },      { title: "Executor Cloud" },    ],    links: [      { rel: "icon", type: "image/x-icon", href: "/favicon.ico" },      { rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32.png" },      { rel: "icon", type: "image/png", sizes: "192x192", href: "/favicon-192.png" },      { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },      { rel: "preconnect", href: "https://fonts.googleapis.com" },      { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },      {        rel: "stylesheet",        href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Instrument+Serif&family=JetBrains+Mono:wght@400;500&display=swap",      },      { rel: "stylesheet", href: appCss },    ],  }),  component: RootComponent,  shellComponent: RootDocument,}); function RootDocument({ children }: { children: React.ReactNode }) {  return (    <html lang="en">      <head>        <HeadContent />      </head>      <body>        {children}        <Scripts />      </body>    </html>  );} function RootComponent() {  return (    <PostHogProvider client={posthog}>      <AuthProvider>        <AuthGate />      </AuthProvider>    </PostHogProvider>  );} function ShellSkeleton() {  return (    <div className="flex h-screen overflow-hidden">      {/* Desktop sidebar skeleton */}      <aside className="hidden w-52 shrink-0 border-r border-sidebar-border bg-sidebar md:flex md:flex-col lg:w-56">        <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4">          <Skeleton className="h-4 w-20" />        </div>        <nav className="flex flex-1 flex-col gap-1 overflow-y-auto p-2">          <Skeleton className="h-7 w-full rounded-md" />          <Skeleton className="h-7 w-full rounded-md" />          <Skeleton className="h-7 w-full rounded-md" />          <Skeleton className="h-7 w-full rounded-md" />          <div className="mt-5 mb-2 px-2.5">            <Skeleton className="h-3 w-14" />          </div>          <div className="flex flex-col gap-1">            <Skeleton className="h-7 w-11/12 rounded-md" />            <Skeleton className="h-7 w-10/12 rounded-md" />            <Skeleton className="h-7 w-9/12 rounded-md" />          </div>        </nav>        <div className="shrink-0 border-t border-sidebar-border px-3 py-2.5">          <div className="flex items-center gap-2.5">            <Skeleton className="size-7 rounded-full" />            <div className="flex min-w-0 flex-1 flex-col gap-1">              <Skeleton className="h-3 w-24" />              <Skeleton className="h-3 w-16" />            </div>          </div>        </div>      </aside>       {/* Main content skeleton */}      <main className="flex min-h-0 flex-1 flex-col overflow-hidden">        {/* Mobile top bar */}        <div className="flex h-12 shrink-0 items-center justify-between border-b border-border bg-background px-4 md:hidden">          <Skeleton className="size-7 rounded-md" />          <Skeleton className="h-4 w-20" />          <div className="w-7 shrink-0" />        </div>         <div className="flex min-h-0 flex-1 flex-col gap-6 px-6 py-8">          <div className="flex items-center justify-between">            <div className="flex flex-col gap-2">              <Skeleton className="h-6 w-40" />              <Skeleton className="h-4 w-64" />            </div>            <Skeleton className="h-8 w-28 rounded-md" />          </div>          <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">            {Array.from({ length: 6 }).map((_, i) => (              <Skeleton key={i} className="h-24 w-full rounded-lg" />            ))}          </div>        </div>      </main>    </div>  );} function ShellErrorFallback() {  return (    <main className="flex min-h-screen items-center justify-center bg-background px-6 py-10">      <section className="w-full max-w-md text-center">        <div className="mx-auto mb-5 flex size-11 items-center justify-center rounded-full border border-border bg-muted">          <span className="text-lg font-semibold text-muted-foreground">!</span>        </div>        <h1 className="text-xl font-semibold text-foreground">Something went wrong</h1>        <p className="mt-2 text-sm leading-6 text-muted-foreground">          We&apos;ve tracked it. Give refreshing a try, and get in touch if support is needed.        </p>        <p className="mt-6 text-xs font-medium uppercase tracking-wide text-muted-foreground">          Get support        </p>        <div className="mt-3">          <SupportOptions />        </div>      </section>    </main>  );} function AuthGate() {  const auth = useAuth();  const location = useLocation();  const navigate = useNavigate();  const isOnboardingRoute = ONBOARDING_PATHS.has(location.pathname);   const needsOrgRedirect =    auth.status === "authenticated" && auth.organization == null && !isOnboardingRoute;   React.useEffect(() => {    if (needsOrgRedirect) {      void navigate({ to: "/create-org", replace: true });    }  }, [needsOrgRedirect, navigate]);   if (auth.status === "loading") {    return <ShellSkeleton />;  }   if (auth.status === "unauthenticated") {    return <LoginPage />;  }   if (isOnboardingRoute) {    return <Outlet />;  }   if (auth.organization == null) {    return <ShellSkeleton />;  }   return (    <AutumnProvider pathPrefix="/api/billing">      <Sentry.ErrorBoundary fallback={<ShellErrorFallback />} showDialog={false}>        <ExecutorProvider onHandledError={captureFrontendError}>          <React.Suspense fallback={<ShellSkeleton />}>            <ExecutorPluginsProvider plugins={clientPlugins}>              <OrganizationProvider organizationId={auth.organization.id}>                <Shell />                <Toaster />              </OrganizationProvider>            </ExecutorPluginsProvider>          </React.Suspense>        </ExecutorProvider>      </Sentry.ErrorBoundary>    </AutumnProvider>  );}