import { APP_VERSION, TOOLS, en, shouldShowConsent } from "@snapotter/shared"; import { Component, type ErrorInfo, lazy, type ReactNode, Suspense, useEffect } from "react"; import { BrowserRouter, Navigate, Route, Routes, useLocation, useParams } from "react-router-dom"; import { Toaster, toast } from "sonner"; import { ConnectionMonitor } from "./components/common/connection-monitor"; import { KeyboardShortcutProvider } from "./components/common/keyboard-shortcut-provider"; import { RouteAnnouncer } from "./components/common/route-announcer"; import { I18nProvider } from "./contexts/i18n-context"; import { useAuth } from "./hooks/use-auth"; import { useMobile } from "./hooks/use-mobile"; import { identify, initAnalytics, setAnalyticsConsent } from "./lib/analytics"; import { useAnalyticsStore } from "./stores/analytics-store"; // Lazy-load all pages so each page's JS (and its icons/deps) is only // downloaded when the user navigates there, shrinking the main bundle. const AutomatePage = lazy(() => import("./pages/automate-page").then((m) => ({ default: m.AutomatePage })), ); const ChangePasswordPage = lazy(() => import("./pages/change-password-page").then((m) => ({ default: m.ChangePasswordPage })), ); const FilesPage = lazy(() => import("./pages/files-page").then((m) => ({ default: m.FilesPage }))); const HomePage = lazy(() => import("./pages/home-page").then((m) => ({ default: m.HomePage }))); const LoginPage = lazy(() => import("./pages/login-page").then((m) => ({ default: m.LoginPage }))); const PrivacyPolicyPage = lazy(() => import("./pages/privacy-policy-page").then((m) => ({ default: m.PrivacyPolicyPage })), ); const AnalyticsConsentPage = lazy(() => import("./pages/analytics-consent-page").then((m) => ({ default: m.AnalyticsConsentPage })), ); const EditorPage = lazy(() => import("./pages/editor-page").then((m) => ({ default: m.EditorPage })), ); const ToolPage = lazy(() => import("./pages/tool-page").then((m) => ({ default: m.ToolPage }))); class ErrorBoundary extends Component< { children: ReactNode }, { hasError: boolean; error: Error | null } > { constructor(props: { children: ReactNode }) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, info: ErrorInfo) { console.error("Uncaught render error:", error, info.componentStack); } render() { if (this.state.hasError) { return (

{en.common.somethingWentWrong}

{this.state.error?.message || en.common.unexpectedError}

); } return this.props.children; } } function LegacyToolRedirect() { const { toolId } = useParams<{ toolId: string }>(); const tool = TOOLS.find((t) => t.id === toolId); if (tool) return ; return ; } function AuthGuard({ children }: { children: React.ReactNode }) { const { loading, authEnabled, isAuthenticated, mustChangePassword, analyticsEnabled, analyticsConsentShownAt, analyticsConsentRemindAt, } = useAuth(); const storeConsent = useAnalyticsStore((s) => s.consent); const setStoreConsent = useAnalyticsStore((s) => s.setConsent); const analyticsConfig = useAnalyticsStore((s) => s.config); const location = useLocation(); // biome-ignore lint/correctness/useExhaustiveDependencies: only hydrate on session load, not on store changes useEffect(() => { if ( !loading && analyticsEnabled !== undefined && storeConsent.analyticsConsentShownAt === null && storeConsent.analyticsEnabled === null ) { setStoreConsent({ analyticsEnabled: analyticsEnabled ?? null, analyticsConsentShownAt: analyticsConsentShownAt ?? null, analyticsConsentRemindAt: analyticsConsentRemindAt ?? null, }); } }, [loading, analyticsEnabled, analyticsConsentShownAt, setStoreConsent]); // When auth is disabled, redirect away from login/change-password to prevent escalation if ( !loading && !authEnabled && (location.pathname === "/login" || location.pathname === "/change-password") ) { return ; } // Don't guard the login or change-password pages if ( location.pathname === "/login" || location.pathname === "/change-password" || location.pathname === "/privacy" || location.pathname === "/analytics-consent" ) { return <>{children}; } if (loading) { return (

{en.common.loading}

); } if (authEnabled && !isAuthenticated) { return ; } // Force password change before allowing access to the app if (authEnabled && mustChangePassword) { return ; } const effectiveConsent = { analyticsEnabled: storeConsent.analyticsEnabled ?? analyticsEnabled ?? null, analyticsConsentShownAt: storeConsent.analyticsConsentShownAt ?? analyticsConsentShownAt ?? null, analyticsConsentRemindAt: storeConsent.analyticsConsentRemindAt ?? analyticsConsentRemindAt ?? null, }; const serverEnabled = analyticsConfig?.enabled ?? false; if ( authEnabled && analyticsConfig !== null && shouldShowConsent(effectiveConsent, serverEnabled) ) { return ; } return <>{children}; } // Single page-level loading fallback — shown while JS for a route downloads. function PageLoader() { return (
); } export function App() { const isMobile = useMobile(); const analyticsConfig = useAnalyticsStore((s) => s.config); const analyticsConfigLoaded = useAnalyticsStore((s) => s.configLoaded); const fetchAnalyticsConfig = useAnalyticsStore((s) => s.fetchConfig); const analyticsConsent = useAnalyticsStore((s) => s.consent); useEffect(() => { fetchAnalyticsConfig(); }, [fetchAnalyticsConfig]); useEffect(() => { if (localStorage.getItem("snapotter-welcome") === "1") { localStorage.removeItem("snapotter-welcome"); toast("Hello from the otter side! 🦦", { duration: 5000, }); } }, []); useEffect(() => { if ( !analyticsConfigLoaded || !analyticsConfig?.enabled || analyticsConsent.analyticsEnabled !== true ) return; void (async () => { setAnalyticsConsent(true); await initAnalytics(analyticsConfig); identify( analyticsConfig.instanceId, { version: APP_VERSION }, { instance_id: analyticsConfig.instanceId }, ); })(); }, [analyticsConfigLoaded, analyticsConfig, analyticsConsent.analyticsEnabled]); return ( {en.a11y.skipToContent} }> } /> } /> } /> } /> } /> } /> {/* Redirects: old color tools consolidated into adjust-colors */} } /> } /> } /> } /> } /> } /> {/* Modality-prefixed tool routes */} } /> {/* Legacy: redirect old /:toolId URLs to /:modality/:toolId */} } /> } /> ); }