130 lines
5.1 KiB
JavaScript
130 lines
5.1 KiB
JavaScript
import { useState } from 'react';
|
|
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|
import { BusinessProvider, useBusiness } from './context/BusinessContext';
|
|
import apiClient from './api/client';
|
|
import BusinessReviewModal from './components/BusinessReviewModal';
|
|
import Sidebar from './components/Sidebar';
|
|
import Businesses from './pages/Businesses';
|
|
import Providers from './pages/Providers';
|
|
import GlobalSms from './pages/GlobalSms';
|
|
import Analytics from './pages/Analytics';
|
|
import Events from './pages/Events';
|
|
import Templates from './pages/Templates';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
function SubLayout({ children }) {
|
|
const { activeBusiness, activeBusinessId, hasGlobalSms } = useBusiness();
|
|
const [reviewBusiness, setReviewBusiness] = useState(null);
|
|
const [reviewLoading, setReviewLoading] = useState(false);
|
|
const [reviewError, setReviewError] = useState('');
|
|
|
|
async function handleOpenReview() {
|
|
if (!activeBusinessId || reviewLoading) return;
|
|
|
|
setReviewError('');
|
|
|
|
if (activeBusiness?.scrapeArtifacts?.json) {
|
|
setReviewBusiness(activeBusiness);
|
|
return;
|
|
}
|
|
|
|
setReviewLoading(true);
|
|
try {
|
|
const response = await apiClient.get(`/api/businesses/${activeBusinessId}`);
|
|
setReviewBusiness(response.data);
|
|
} catch (error) {
|
|
setReviewError(error.response?.data?.error || 'Failed to load brand review.');
|
|
} finally {
|
|
setReviewLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex min-h-screen bg-page-bg">
|
|
<Sidebar
|
|
onOpenReview={handleOpenReview}
|
|
reviewLoading={reviewLoading}
|
|
reviewError={reviewError}
|
|
/>
|
|
<main className="flex-1 ml-60 flex flex-col">
|
|
<header className="h-16 border-b border-border-main bg-white flex items-center justify-end px-8 z-10 shrink-0">
|
|
{hasGlobalSms && (
|
|
<Link
|
|
to={`/${activeBusinessId}/settings`}
|
|
className="flex h-10 w-10 items-center justify-center rounded-full border border-border-soft bg-gray-50 text-gray-500 transition-colors hover:border-gray-300 hover:bg-white hover:text-primary-blue"
|
|
title="Settings"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
</Link>
|
|
)}
|
|
</header>
|
|
<div className="flex-1 p-5 overflow-auto">
|
|
{children}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
{reviewBusiness && (
|
|
<BusinessReviewModal
|
|
business={reviewBusiness}
|
|
onClose={() => setReviewBusiness(null)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Guard: redirect to / if no active business in session.
|
|
// Also enforce cURL-first: only the cURL profile route is available until an active profile exists.
|
|
function BusinessGuard({ children, isGlobalSmsRoute }) {
|
|
const { activeBusinessId, loading, isSetupComplete } = useBusiness();
|
|
const location = useLocation();
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-page-bg">
|
|
<div className="w-8 h-8 border-2 border-gray-200 border-t-primary-blue rounded-full animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!activeBusinessId) {
|
|
return <Navigate to="/" state={{ from: location }} replace />;
|
|
}
|
|
|
|
if (!isSetupComplete && !isGlobalSmsRoute) {
|
|
return <Navigate to={`/${activeBusinessId}/global-sms`} replace />;
|
|
}
|
|
|
|
return children;
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<BusinessProvider>
|
|
<BrowserRouter>
|
|
<Routes>
|
|
<Route path="/" element={<Businesses />} />
|
|
<Route path="/:businessId/settings" element={
|
|
<BusinessGuard><SubLayout><Providers /></SubLayout></BusinessGuard>
|
|
} />
|
|
<Route path="/:businessId/global-sms" element={
|
|
<BusinessGuard isGlobalSmsRoute><SubLayout><GlobalSms /></SubLayout></BusinessGuard>
|
|
} />
|
|
<Route path="/:businessId/analytics" element={
|
|
<BusinessGuard><SubLayout><Analytics /></SubLayout></BusinessGuard>
|
|
} />
|
|
<Route path="/:businessId/events" element={
|
|
<BusinessGuard><SubLayout><Events /></SubLayout></BusinessGuard>
|
|
} />
|
|
<Route path="/:businessId/templates" element={
|
|
<BusinessGuard><SubLayout><Templates /></SubLayout></BusinessGuard>
|
|
} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</BrowserRouter>
|
|
</BusinessProvider>
|
|
);
|
|
}
|