// ============================================================ // Bubbzii V2 — Main App with Routing // ============================================================ import { useEffect, lazy, Suspense, useRef } from 'react'; import { BrowserRouter, Routes, Route, useLocation, Navigate, Link } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { api, ApiError } from './api/client'; import { normalizeUser } from './api/auth'; import { useAuthStore } from './stores/authStore'; import { Navbar } from './components/layout/Navbar'; import { Footer } from './components/layout/Footer'; import { ProtectedRoute } from './components/layout/ProtectedRoute'; import { GuestOnlyRoute } from './components/auth/GuestOnlyRoute'; import { ToastProvider } from './components/ui/Toast'; import { MaintenanceBanner } from './components/ui/MaintenanceBanner'; import { SeoRouteSync } from './components/seo/SeoRouteSync'; import './pages/v4.css'; import './pages/manager-inner-theme.css'; import './pages/host-dashboard-theme.css'; import './pages/dashboard-content-theme.css'; import { loadManagerPreferences, type ManagerChromeTheme } from './lib/managerPreferences'; import { getAuthScope, type AuthScope } from './lib/authStorage'; import { loadAdminPreferences } from './lib/adminPreferences'; import { getSettingsRoute } from './lib/rolePermissions'; // Pages import { AccountPage } from './pages/AccountPage'; import { OnboardingPage } from './pages/OnboardingPage'; import { ProfilePage } from './pages/ProfilePage'; import { SubscriptionPage } from './pages/SubscriptionPage'; import { BubbziipediaPage } from './pages/BubbziipediaPage'; import { GoogleAuthCallbackPage } from './pages/GoogleAuthCallbackPage'; import { InviteAcceptPage } from './pages/InviteAcceptPage'; import { WaitlistPage } from './pages/WaitlistPage'; import { ShowcaseDemo } from './pages/ShowcaseDemo'; import { V4AboutPage, V4ContactPage, V4FaqPage, V4FaviconLabPage, V4GetStartedPage, V4HomePage, V4IntegrationsPage, V4LoginPage, V4PricingPage, V4PrivacyPage, V4Shell, V4TermsPage, } from './pages/V4Pages'; // Dashboards — lazy loaded for code-splitting (ensures each dashboard is in its own chunk) const CleanerDashboard = lazy(() => import('./dashboard/CleanerDashboard').then(m => ({ default: m.CleanerDashboard }))); const HostDashboard = lazy(() => import('./dashboard/HostDashboard').then(m => ({ default: m.HostDashboard }))); const ManagerDashboard = lazy(() => import('./dashboard/ManagerDashboard').then(m => ({ default: m.ManagerDashboard }))); const JobDetailPage = lazy(() => import('./dashboard/JobDetailPage').then(m => ({ default: m.JobDetailPage }))); const PropertyDetailPage = lazy(() => import('./pages/PropertyDetailPage').then(m => ({ default: m.PropertyDetailPage }))); // Admin const AdminLogin = lazy(() => import('./admin/AdminLogin').then(m => ({ default: m.AdminLogin }))); const AdminDashboard = lazy(() => import('./admin/AdminDashboard').then(m => ({ default: m.AdminDashboard }))); // Portals import { HostPortalPage } from './pages/HostPortalPage'; import { PhotoPortalPage } from './pages/PhotoPortalPage'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutes retry: 1, refetchOnWindowFocus: false, }, }, }); type AuthBootstrapProps = { children: React.ReactNode; }; const AUTH_BOOTSTRAP_RETRY_DELAYS_MS = [0, 250, 1000] as const; function isUnauthorizedError(error: unknown): boolean { return error instanceof ApiError && error.status === 401; } function waitForAuthBootstrapRetry(delayMs: number): Promise { return new Promise(resolve => window.setTimeout(resolve, delayMs)); } function AuthBootstrap({ children }: AuthBootstrapProps) { const { initializeAuth, restoreAuth, logout, authChecked, user, setAuthChecked } = useAuthStore(); const location = useLocation(); const scope = getAuthScope(location.pathname); const lastScopeRef = useRef(null); const isAdminPath = scope === 'admin'; useEffect(() => { let cancelled = false; const scopeChanged = lastScopeRef.current !== scope; lastScopeRef.current = scope; const bootstrap = async () => { const isAdminPath = scope === 'admin'; // Reuse the last verified local profile while the canonical profile // request is retried. The server-side HttpOnly cookie remains the // authentication source of truth. restoreAuth(scope); const profileEndpoints = isAdminPath ? ['/api/v1/admin/me'] : ['/api/v1/profiles/me']; for (const endpoint of profileEndpoints) { for (const delayMs of AUTH_BOOTSTRAP_RETRY_DELAYS_MS) { if (delayMs > 0) { await waitForAuthBootstrapRetry(delayMs); } if (cancelled) return; try { const response = await api.get<{ ok?: boolean; success?: boolean; user?: unknown; profile?: unknown; data?: unknown; message?: string }>(endpoint); const rawUser = response.profile || response.user || response.data; if (!cancelled && rawUser) { const normalized = normalizeUser(rawUser as never); const user = endpoint === '/api/v1/admin/me' ? normalizeUser({ ...(normalized as object), role: 'admin' } as never) : normalized; initializeAuth(user, scope); return; } } catch (err) { // ApiClient clears the scoped session only for an actual HTTP 401. // Network/server failures are transient and must not destroy a valid token. if (isUnauthorizedError(err)) { return; } // Retry the profile request before completing bootstrap softly. } } } if (!cancelled) { // Keep the token/session intact when the profile request is unavailable. // A later navigation or reload can retry without forcing a login. setAuthChecked(true); } }; if (scopeChanged && authChecked) { setAuthChecked(false); } if (!authChecked || scopeChanged) { void bootstrap(); } return () => { cancelled = true; }; }, [authChecked, initializeAuth, location.pathname, logout, restoreAuth, scope, setAuthChecked]); // A previously verified local profile can render immediately while the // HttpOnly-cookie profile check revalidates the session in the background. const showChildren = authChecked || Boolean(user); if (!showChildren) { return (

Checking session...

); } return <>{children}; } function ScrollToHash() { const { hash, pathname } = useLocation(); useEffect(() => { if (hash) { const id = hash.replace('#', ''); const el = document.getElementById(id); if (el) { const raf = window.requestAnimationFrame(() => { const top = el.getBoundingClientRect().top + window.scrollY - 72; window.scrollTo({ top, behavior: 'auto' }); }); return () => window.cancelAnimationFrame(raf); } return; } // No hash — scroll to top on route change window.scrollTo(0, 0); }, [hash, pathname]); return null; } function AppChrome() { const { pathname } = useLocation(); const { user } = useAuthStore(); const v4RootPaths = new Set([ '/', '/login', '/signup', '/get-started', '/about', '/contact', '/faq', '/pricing', '/integrations', '/privacy', '/terms', '/features', '/waitlist', '/demo', '/host-portal', '/photo-portal', ]); const isInvitationFlow = pathname.startsWith('/invite/'); const isAppManagedPath = pathname.startsWith('/dashboard') || pathname.startsWith('/admin') || pathname.startsWith('/onboarding') || pathname.startsWith('/settings') || pathname.startsWith('/v4') || pathname.startsWith('/photo-portal') || pathname.startsWith('/host-portal') || pathname.startsWith('/invite/') || pathname.startsWith('/account') || pathname.startsWith('/profile') || pathname.startsWith('/subscription') || pathname === '/bubbziipedia' || pathname === '/favicon-lab' || pathname === '/auth/google/callback'; const isPublicNotFound = !isAppManagedPath && !v4RootPaths.has(pathname); const isV4PublicPath = pathname.startsWith('/v4') || v4RootPaths.has(pathname) || isInvitationFlow || isPublicNotFound; const managerChromeTheme: ManagerChromeTheme = pathname.startsWith('/dashboard/manager') ? loadManagerPreferences(user?.id).managerChromeTheme : 'green'; const adminChromeTheme: ManagerChromeTheme = pathname.startsWith('/admin') ? loadAdminPreferences(user?.id).chromeTheme : 'green'; const adminShellClass = adminChromeTheme === 'pink' ? 'flex flex-col min-h-screen bg-[#f6eeea] text-[#3a312c] font-sans' : 'flex flex-col min-h-screen bg-[#eff4ef] text-[#27352f] font-sans'; const appShellClass = pathname.startsWith('/dashboard/manager') ? managerChromeTheme === 'pink' ? 'flex flex-col min-h-screen bg-[#f6eeea] text-[#3a312c] font-sans' : 'flex flex-col min-h-screen bg-[#eff4ef] text-[#27352f] font-sans' : pathname.startsWith('/dashboard/cleaner') ? 'flex flex-col min-h-screen bg-[#f6f1ea] text-[#2f2724] font-sans' : pathname.startsWith('/dashboard/host') ? 'flex flex-col min-h-screen bg-[#f6f1ea] text-[#2f2724] font-sans' : pathname.startsWith('/admin') ? adminShellClass : isV4PublicPath ? 'flex flex-col min-h-screen bg-[#deebe2] text-[#38302a] font-sans' : 'flex flex-col min-h-screen bg-bubbzii-navy text-white font-sans'; const hideChrome = pathname.startsWith('/dashboard') || pathname.startsWith('/admin') || pathname.startsWith('/onboarding') || pathname.startsWith('/settings') || pathname.startsWith('/v4') || pathname.startsWith('/photo-portal') || pathname.startsWith('/host-portal') || pathname === '/demo' || isPublicNotFound || v4RootPaths.has(pathname) || isInvitationFlow; return (
{!hideChrome && }
}> {/* Public Routes */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Protected Routes — Authenticated Users */} } /> {/* Cleaner Dashboard — section routes */} } /> } /> } /> } /> } /> } /> {/* Manager Dashboard — tab routes */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Property Detail — All Roles */} } /> } /> } /> } /> } /> {/* Host Dashboard — view routes */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Admin */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Portals */} } /> } /> {/* 404 */} } />
{!hideChrome &&
}
); } function SettingsRouteRedirect() { const { user } = useAuthStore(); return ; } export default function App() { return ( ); } function NotFound() { return (
Well, that took a wrong turn.

Page not found

This route is off the map. Let's get you back to the clean side.

Go Home
); }