/** * Main App Component - Integrated with Real API */ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useCurrentUser, useMasquerade, useLogout } from './hooks/useAuth'; import { useCurrentBusiness } from './hooks/useBusiness'; import { useUpdateBusiness } from './hooks/useBusiness'; import { setCookie } from './utils/cookies'; // Import Login Page import LoginPage from './pages/LoginPage'; import MFAVerifyPage from './pages/MFAVerifyPage'; import OAuthCallback from './pages/OAuthCallback'; // Import layouts import BusinessLayout from './layouts/BusinessLayout'; import PlatformLayout from './layouts/PlatformLayout'; import CustomerLayout from './layouts/CustomerLayout'; import MarketingLayout from './layouts/MarketingLayout'; // Import marketing pages import HomePage from './pages/marketing/HomePage'; import FeaturesPage from './pages/marketing/FeaturesPage'; import PricingPage from './pages/marketing/PricingPage'; import AboutPage from './pages/marketing/AboutPage'; import ContactPage from './pages/marketing/ContactPage'; import SignupPage from './pages/marketing/SignupPage'; // Import pages import Dashboard from './pages/Dashboard'; import Scheduler from './pages/Scheduler'; import Customers from './pages/Customers'; import Settings from './pages/Settings'; import Payments from './pages/Payments'; import Resources from './pages/Resources'; import Services from './pages/Services'; import Staff from './pages/Staff'; import CustomerDashboard from './pages/customer/CustomerDashboard'; import CustomerSupport from './pages/customer/CustomerSupport'; import ResourceDashboard from './pages/resource/ResourceDashboard'; import BookingPage from './pages/customer/BookingPage'; import TrialExpired from './pages/TrialExpired'; import Upgrade from './pages/Upgrade'; // Import platform pages import PlatformDashboard from './pages/platform/PlatformDashboard'; import PlatformBusinesses from './pages/platform/PlatformBusinesses'; import PlatformSupportPage from './pages/platform/PlatformSupport'; import PlatformUsers from './pages/platform/PlatformUsers'; import PlatformStaff from './pages/platform/PlatformStaff'; import PlatformSettings from './pages/platform/PlatformSettings'; import ProfileSettings from './pages/ProfileSettings'; import VerifyEmail from './pages/VerifyEmail'; import EmailVerificationRequired from './pages/EmailVerificationRequired'; import AcceptInvitePage from './pages/AcceptInvitePage'; import TenantOnboardPage from './pages/TenantOnboardPage'; import Tickets from './pages/Tickets'; // Import Tickets page import HelpGuide from './pages/HelpGuide'; // Import Platform Guide page import HelpTicketing from './pages/HelpTicketing'; // Import Help page for ticketing import HelpApiDocs from './pages/HelpApiDocs'; // Import API documentation page import HelpPluginDocs from './pages/HelpPluginDocs'; // Import Plugin documentation page import PlatformSupport from './pages/PlatformSupport'; // Import Platform Support page (for businesses to contact SmoothSchedule) import PluginMarketplace from './pages/PluginMarketplace'; // Import Plugin Marketplace page import MyPlugins from './pages/MyPlugins'; // Import My Plugins page import Tasks from './pages/Tasks'; // Import Tasks page for scheduled plugin executions import EmailTemplates from './pages/EmailTemplates'; // Import Email Templates page import { Toaster } from 'react-hot-toast'; // Import Toaster for notifications const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1, staleTime: 30000, // 30 seconds }, }, }); /** * Loading Component */ const LoadingScreen: React.FC = () => { const { t } = useTranslation(); return (

{t('common.loading')}

); }; /** * Error Component */ const ErrorScreen: React.FC<{ error: Error }> = ({ error }) => { const { t } = useTranslation(); return (

{t('common.error')}

{error.message}

); }; /** * App Content - Handles routing based on auth state */ const AppContent: React.FC = () => { // Check for tokens in URL FIRST - before any queries execute // This handles login/masquerade redirects that pass tokens in the URL const [processingUrlTokens] = useState(() => { const params = new URLSearchParams(window.location.search); return !!(params.get('access_token') && params.get('refresh_token')); }); const { data: user, isLoading: userLoading, error: userError } = useCurrentUser(); const { data: business, isLoading: businessLoading, error: businessError } = useCurrentBusiness(); const [darkMode, setDarkMode] = useState(() => { // Check localStorage first, then system preference const saved = localStorage.getItem('darkMode'); if (saved !== null) { return JSON.parse(saved); } return window.matchMedia('(prefers-color-scheme: dark)').matches; }); const updateBusinessMutation = useUpdateBusiness(); const masqueradeMutation = useMasquerade(); const logoutMutation = useLogout(); // Apply dark mode class and persist to localStorage React.useEffect(() => { document.documentElement.classList.toggle('dark', darkMode); localStorage.setItem('darkMode', JSON.stringify(darkMode)); }, [darkMode]); // Handle tokens in URL (from login or masquerade redirect) React.useEffect(() => { const params = new URLSearchParams(window.location.search); const accessToken = params.get('access_token'); const refreshToken = params.get('refresh_token'); if (accessToken && refreshToken) { // Extract masquerade stack if present (for masquerade banner) const masqueradeStackParam = params.get('masquerade_stack'); if (masqueradeStackParam) { try { const masqueradeStack = JSON.parse(decodeURIComponent(masqueradeStackParam)); localStorage.setItem('masquerade_stack', JSON.stringify(masqueradeStack)); } catch (e) { console.error('Failed to parse masquerade stack', e); } } // For backward compatibility, also check for original_user parameter const originalUserParam = params.get('original_user'); if (originalUserParam && !masqueradeStackParam) { try { const originalUser = JSON.parse(decodeURIComponent(originalUserParam)); // Convert old format to new stack format (single entry) const stack = [{ user_id: originalUser.id, username: originalUser.username, role: originalUser.role, business_id: originalUser.business, business_subdomain: originalUser.business_subdomain, }]; localStorage.setItem('masquerade_stack', JSON.stringify(stack)); } catch (e) { console.error('Failed to parse original user', e); } } // Set cookies using helper (handles domain correctly) setCookie('access_token', accessToken, 7); setCookie('refresh_token', refreshToken, 7); // Clear session cookie to prevent interference with JWT // (Django session cookie might take precedence over JWT) document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.lvh.me'; document.cookie = 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; // Clean URL const newUrl = window.location.pathname + window.location.hash; window.history.replaceState({}, '', newUrl); // Force reload to ensure auth state is picked up window.location.reload(); } }, []); // Show loading while processing URL tokens (before reload happens) if (processingUrlTokens) { return ; } // Loading state if (userLoading) { return ; } // Helper to detect root domain (for marketing site) const isRootDomain = (): boolean => { const hostname = window.location.hostname; return hostname === 'lvh.me' || hostname === 'localhost' || hostname === '127.0.0.1'; }; // On root domain, ALWAYS show marketing site (even if logged in) // Logged-in users will see a "Go to Dashboard" link in the navbar if (isRootDomain()) { return ( }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } // Not authenticated - show marketing pages if (!user) { return ( }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } // Error state if (userError) { return ; } // Subdomain validation for logged-in users const currentHostname = window.location.hostname; const isPlatformDomain = currentHostname === 'platform.lvh.me'; const currentSubdomain = currentHostname.split('.')[0]; const isBusinessSubdomain = !isRootDomain() && !isPlatformDomain && currentSubdomain !== 'api'; const isPlatformUser = ['superuser', 'platform_manager', 'platform_support'].includes(user.role); const isBusinessUser = ['owner', 'manager', 'staff', 'resource'].includes(user.role); const isCustomer = user.role === 'customer'; // RULE: Platform users must be on platform subdomain (not business subdomains) if (isPlatformUser && isBusinessSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `http://platform.lvh.me${port}/`; return ; } // RULE: Business users must be on their own business subdomain if (isBusinessUser && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; return ; } // RULE: Customers must be on their business subdomain if (isCustomer && isPlatformDomain && user.business_subdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; return ; } if (isCustomer && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; return ; } // Handlers const toggleTheme = () => setDarkMode((prev) => !prev); const handleSignOut = () => { logoutMutation.mutate(); }; const handleUpdateBusiness = (updates: Partial) => { updateBusinessMutation.mutate(updates); }; const handleMasquerade = (targetUser: any) => { // Call the masquerade API with the target user's id const userId = targetUser.id; if (!userId) { console.error('Cannot masquerade: no user id available', targetUser); return; } // Ensure userId is a number const userPk = typeof userId === 'string' ? parseInt(userId, 10) : userId; masqueradeMutation.mutate(userPk); }; // Helper to check access based on roles const hasAccess = (allowedRoles: string[]) => allowedRoles.includes(user.role); if (isPlatformUser) { return ( } > {(user.role === 'superuser' || user.role === 'platform_manager') && ( <> } /> } /> } /> } /> )} } /> } /> } /> } /> } /> {user.role === 'superuser' && ( } /> )} } /> } /> } /> ); } // Customer users if (user.role === 'customer') { // Wait for business data to load if (businessLoading) { return ; } // Handle business not found for customers if (!business) { return (

Business Not Found

Unable to load business data. Please try again.

); } return ( } > } /> } /> } /> } /> } /> } /> } /> ); } // Business loading - show loading with user info if (businessLoading) { return ; } // Business error or no business found if (businessError || !business) { // If user has a business subdomain, redirect them there if (user.business_subdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `http://${user.business_subdomain}.lvh.me${port}/`; return ; } // No business subdomain - show error return (

Business Not Found

{businessError instanceof Error ? businessError.message : 'Unable to load business data. Please check your subdomain or try again.'}

); } // Business users (owner, manager, staff, resource) if (['owner', 'manager', 'staff', 'resource'].includes(user.role)) { // Check if email verification is required if (!user.email_verified) { return ( } /> } /> } /> ); } // Check if trial has expired const isTrialExpired = business.isTrialExpired || (business.status === 'Trial' && business.trialEnd && new Date(business.trialEnd) < new Date()); // Allowed routes when trial is expired const allowedWhenExpired = ['/trial-expired', '/upgrade', '/settings', '/profile']; const currentPath = window.location.pathname; const isOnAllowedRoute = allowedWhenExpired.some(route => currentPath.startsWith(route)); // If trial expired and not on allowed route, redirect to trial-expired if (isTrialExpired && !isOnAllowedRoute) { return ( } /> } /> } /> : } /> } /> ); } return ( } > {/* Trial and Upgrade Routes */} } /> } /> {/* Regular Routes */} : } /> } /> } /> } /> } /> } /> } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> : } />

Messages

Messages feature coming soon...

) : ( ) } /> : } /> } /> } /> } />
); } // Fallback return ; }; /** * Main App Component */ const App: React.FC = () => { return ( {/* Add Toaster component for notifications */} ); }; export default App;