import React, { Suspense, lazy, useCallback, useEffect, useRef, useState, } from 'react'; import { HashRouter as Router, Routes, Route, Navigate, useLocation, useNavigate, useSearchParams, } from 'react-router-dom'; import { Loader2Icon, LoaderCircleIcon } from 'lucide-react'; import { recomaze_ai_personalization_env } from './env'; import { canSeeSeoAnalytics } from './lib/seo/analytics-access'; import { Sidebar, SidebarShowButton } from './components/layout/sidebar'; import { seoOnlyHome, seoPageAllowed } from './components/layout/nav-config'; import { SeoBadgesProvider } from './context/seo-badges.context'; import { Topbar } from './components/layout/topbar'; import { MobileNav } from './components/layout/mobile-nav'; import { ConnectionNotice } from './components/layout/connection-notice'; import { navShownFromCookie, writeNavCookie } from './lib/navigation/nav-model'; import { TooltipProvider } from './components/ui/tooltip'; import { Toaster } from './components/ui/sonner'; import { getCreditBalance } from './service/credits/credits.service'; import { useAppStateContext } from './context/user.data.context'; import { isCopilotEnabled } from './service/copilot/access'; import { useAccessControl } from './hooks/useAccessControl'; import { signupMirrorRedirect } from './lib/signup-mirror/access'; import PreviewBanner from './components/PreviewBanner'; import { updatePreviewMode } from './service/user/user.service'; import VerificationModal from './components/VerificationModal'; import ErrorBoundary from './components/ErrorBoundary'; import { CopilotPanel } from './components/copilot/CopilotPanel'; import { useCopilot } from './context/copilot.context'; const Support = lazy(() => import('./pages/Support')); const Settings = lazy(() => import('./pages/Settings')); const SeoDashboard = lazy(() => import('./pages/seo/SeoDashboard')); const Customization = lazy(() => import('./pages/Customization')); const ImportProducts = lazy(() => import('./pages/ImportProducts')); const PriceImport = lazy(() => import('./pages/PriceImport')); const Pricing = lazy(() => import('./pages/Pricing')); const AiVisibility = lazy(() => import('./pages/AiVisibility')); const AiVisibilityContents = lazy(() => import('./pages/AiVisibilityContents')); const AiVisibilityPageTemplates = lazy( () => import('./pages/AiVisibilityPageTemplates') ); const ProductImprovements = lazy(() => import('./pages/ProductImprovements')); const AgentAnalytics = lazy(() => import('./pages/AgentAnalytics')); const KnowledgeBase = lazy(() => import('./pages/KnowledgeBase')); const ContentGenerator = lazy(() => import('./pages/ContentGenerator')); const ContentChecker = lazy(() => import('./pages/ContentChecker')); const OptimizedTextPages = lazy(() => import('./pages/OptimizedTextPages')); const PromptTemplatesPage = lazy(() => import('./pages/PromptTemplatesPage')); const Campaigns = lazy(() => import('./pages/Campaigns')); const CampaignCreate = lazy(() => import('./pages/CampaignCreate')); const CampaignDetail = lazy(() => import('./pages/CampaignDetail')); const CatalogQuality = lazy(() => import('./pages/CatalogQuality')); const Connectors = lazy(() => import('./pages/Connectors')); const NotFound = lazy(() => import('./pages/NotFound')); const Hq = lazy(() => import('./pages/Hq')); const CompanyId = lazy(() => import('./pages/CompanyId')); const SignupMirror = lazy(() => import('./pages/SignupMirror')); const OnboardingChain = lazy(() => import('./pages/OnboardingChain')); const UiKitchenSink = lazy(() => import('./pages/dev/UiKitchenSink')); const SeoGeneralSettings = lazy(() => import('./pages/seo/SeoGeneralSettings')); const SeoTitlesSettings = lazy(() => import('./pages/seo/SeoTitlesSettings')); const SeoSitemapSettings = lazy(() => import('./pages/seo/SeoSitemapSettings')); const SeoRedirects = lazy(() => import('./pages/seo/SeoRedirects')); const SeoNotFound = lazy(() => import('./pages/seo/SeoNotFound')); const SeoLinks = lazy(() => import('./pages/seo/SeoLinks')); const SeoCheckup = lazy(() => import('./pages/seo/SeoCheckup')); const SeoBulkEditor = lazy(() => import('./pages/seo/SeoBulkEditor')); const SeoPerformance = lazy(() => import('./pages/seo/SeoPerformance')); const SeoSetup = lazy(() => import('./pages/seo/SeoSetup')); const SeoAnalytics = lazy(() => import('./pages/seo/SeoAnalytics')); const SeoLocalSettings = lazy(() => import('./pages/seo/SeoLocalSettings')); const SeoRoles = lazy(() => import('./pages/seo/SeoRoles')); const LlmTxt = lazy(() => import('./pages/LlmTxt')); const PageLoader = (): JSX.Element => (
); const BARE_ROUTES = ['/signup-mirror', '/onboarding-chain']; /** * The pages a role given SEO pages alone can open: they work from the site, * without the Recomaze account (no HQ, Copilot, Analytics or billing). */ const SEO_ONLY_PAGES: [string, React.ComponentType][] = [ ['/seo', SeoDashboard], ['/seo/general', SeoGeneralSettings], ['/seo/titles', SeoTitlesSettings], ['/seo/sitemap', SeoSitemapSettings], ['/seo/redirects', SeoRedirects], ['/seo/404', SeoNotFound], ['/seo/links', SeoLinks], ['/seo/checkup', SeoCheckup], ['/seo/performance', SeoPerformance], ['/seo/bulk-editor', SeoBulkEditor], ['/seo/setup', SeoSetup], ['/seo/local', SeoLocalSettings], ]; // Everything that needs router hooks (useLocation / useNavigate) lives here, // inside . This is also where the Signup Mirror resume redirect fires // and where the full-screen onboarding routes bypass the app chrome. const AppShell = (): JSX.Element => { const { token, clientId, user, userFailed, setUser } = useAppStateContext(); const { canAccessAllRoutes, canSeeBulkUpload } = useAccessControl(user); // Recomaze opens the SEO module account by account (Recomaze_Seo_Access). const seoEnabled: boolean = recomaze_ai_personalization_env?.seo_enabled ?? true; const location = useLocation(); const navigate = useNavigate(); const resumedRef = useRef(false); // A link from outside the app (the editor's "Fix in Copilot") opens the // Copilot with ?copilot= in its composer. It is not sent from here: a link // anyone can hand over must not spend credits or run tools on its own. // The query is dropped so a reload does not put it back. const [params, setParams] = useSearchParams(); const { draftCopilotTurn } = useCopilot(); const copilotPrompt = params.get('copilot'); useEffect(() => { if (!copilotPrompt) return; draftCopilotTurn(copilotPrompt); const rest = new URLSearchParams(params); rest.delete('copilot'); setParams(rest, { replace: true }); }, [copilotPrompt, draftCopilotTurn, params, setParams]); const [navShown, setNavShown] = useState(() => navShownFromCookie(document.cookie) ); const toggleNav = useCallback(() => { setNavShown(shown => { writeNavCookie(!shown); return !shown; }); }, []); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [credits, setCredits] = useState<{ used: number; granted: number; } | null>(null); const email = user?.email ?? recomaze_ai_personalization_env?.current_user_email; useEffect(() => { if (!token) return; getCreditBalance(token, clientId).then(balance => { setCredits( typeof balance.granted === 'number' && balance.granted > 0 ? { used: balance.used ?? 0, granted: balance.granted } : null ); }); }, [token, clientId]); useEffect(() => { if (resumedRef.current || !user) return; if (location.pathname !== '/' && location.pathname !== '/hq') return; const target = signupMirrorRedirect(user); if (target) { resumedRef.current = true; navigate(target, { replace: true }); } }, [user, location.pathname, navigate]); const handleActivatePreview = async () => { if (!token) return; try { await updatePreviewMode({ token, preview_mode: true }); if (user) setUser({ ...user, preview_mode: true }); } catch (error) { console.error('Error enabling preview mode:', error); } }; const handleDeactivatePreview = async () => { if (!token) return; try { await updatePreviewMode({ token, preview_mode: false }); if (user) setUser({ ...user, preview_mode: false }); } catch (error) { console.error('Error disabling preview mode:', error); } }; // A fresh install lands on the app root while /me is still in flight, so // rendering straight away painted the dashboard and only then jumped to the // Mirror. Hold the shell until the account is known and the merchant sees // one screen: the one they belong on. // A /me that failed (the API down) holds nothing: the shell renders, and // the connection notice says why the pages are empty. const awaitingResumeDecision: boolean = !!token && !user && !userFailed && (location.pathname === '/' || location.pathname === '/hq'); if (awaitingResumeDecision) { return (
); } // Full-screen onboarding routes: no sidebar, no banners, no copilot. They // still take the pinned shell, so they fill the viewport instead of leaving // dead space under a short screen - and, since the admin stylesheet stops // the document itself from scrolling, they carry their own scroll or a tall // screen would be unreachable. if (BARE_ROUTES.includes(location.pathname)) { return (
}> } /> } />
); } return ( ); }; const App = (): JSX.Element => { const { token, clientId, user, plans, setToken, setClientId, fetchUser, fetchPlans, setCanUseWpApi, } = useAppStateContext(); useEffect(() => { if (!token && recomaze_ai_personalization_env?.auth?.token) setToken(recomaze_ai_personalization_env?.auth?.token); if (!clientId && recomaze_ai_personalization_env?.auth?.client_id) setClientId(recomaze_ai_personalization_env?.auth?.client_id); if (!user && token) fetchUser(); if (!plans && token) fetchPlans(); if (!recomaze_ai_personalization_env?.can_use_wp_api) setCanUseWpApi(false); }, [token, clientId]); return ( ); }; export default App;