asdasd
This commit is contained in:
843
tenant-temp/frontend/src/App.tsx
Normal file
843
tenant-temp/frontend/src/App.tsx
Normal file
@@ -0,0 +1,843 @@
|
||||
import { Suspense, lazy, useEffect } from "react";
|
||||
import { Routes, Route, Navigate } from "react-router-dom";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import AppLayout from "./layout/AppLayout";
|
||||
import { ScrollToTop } from "./components/common/ScrollToTop";
|
||||
import ProtectedRoute from "./components/auth/ProtectedRoute";
|
||||
import ModuleGuard from "./components/common/ModuleGuard";
|
||||
import AdminGuard from "./components/auth/AdminGuard";
|
||||
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
|
||||
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
|
||||
import { useAuthStore } from "./store/authStore";
|
||||
|
||||
// Auth pages - loaded immediately (needed for login)
|
||||
import SignIn from "./pages/AuthPages/SignIn";
|
||||
import SignUp from "./pages/AuthPages/SignUp";
|
||||
import NotFound from "./pages/OtherPage/NotFound";
|
||||
|
||||
// Lazy load all other pages - only loads when navigated to
|
||||
const Home = lazy(() => import("./pages/Dashboard/Home"));
|
||||
|
||||
// Planner Module - Lazy loaded
|
||||
const PlannerDashboard = lazy(() => import("./pages/Planner/Dashboard"));
|
||||
const Keywords = lazy(() => import("./pages/Planner/Keywords"));
|
||||
const Clusters = lazy(() => import("./pages/Planner/Clusters"));
|
||||
const ClusterDetail = lazy(() => import("./pages/Planner/ClusterDetail"));
|
||||
const Ideas = lazy(() => import("./pages/Planner/Ideas"));
|
||||
const KeywordOpportunities = lazy(() => import("./pages/Planner/KeywordOpportunities"));
|
||||
|
||||
// Writer Module - Lazy loaded
|
||||
const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard"));
|
||||
const Tasks = lazy(() => import("./pages/Writer/Tasks"));
|
||||
const Content = lazy(() => import("./pages/Writer/Content"));
|
||||
const ContentView = lazy(() => import("./pages/Writer/ContentView"));
|
||||
const Drafts = lazy(() => import("./pages/Writer/Drafts"));
|
||||
const Images = lazy(() => import("./pages/Writer/Images"));
|
||||
const Review = lazy(() => import("./pages/Writer/Review"));
|
||||
const Published = lazy(() => import("./pages/Writer/Published"));
|
||||
|
||||
// Automation Module - Lazy loaded
|
||||
const AutomationPage = lazy(() => import("./pages/Automation/AutomationPage"));
|
||||
|
||||
// Linker Module - Lazy loaded
|
||||
const LinkerDashboard = lazy(() => import("./pages/Linker/Dashboard"));
|
||||
const LinkerContentList = lazy(() => import("./pages/Linker/ContentList"));
|
||||
|
||||
// Optimizer Module - Lazy loaded
|
||||
const OptimizerDashboard = lazy(() => import("./pages/Optimizer/Dashboard"));
|
||||
const OptimizerContentSelector = lazy(() => import("./pages/Optimizer/ContentSelector"));
|
||||
const AnalysisPreview = lazy(() => import("./pages/Optimizer/AnalysisPreview"));
|
||||
|
||||
// Thinker Module - Lazy loaded
|
||||
const ThinkerDashboard = lazy(() => import("./pages/Thinker/Dashboard"));
|
||||
const Prompts = lazy(() => import("./pages/Thinker/Prompts"));
|
||||
const AuthorProfiles = lazy(() => import("./pages/Thinker/AuthorProfiles"));
|
||||
const ThinkerProfile = lazy(() => import("./pages/Thinker/Profile"));
|
||||
const Strategies = lazy(() => import("./pages/Thinker/Strategies"));
|
||||
const ImageTesting = lazy(() => import("./pages/Thinker/ImageTesting"));
|
||||
|
||||
// Billing Module - Lazy loaded
|
||||
const Credits = lazy(() => import("./pages/Billing/Credits"));
|
||||
const Transactions = lazy(() => import("./pages/Billing/Transactions"));
|
||||
const Usage = lazy(() => import("./pages/Billing/Usage"));
|
||||
const CreditsAndBilling = lazy(() => import("./pages/Settings/CreditsAndBilling"));
|
||||
const PurchaseCreditsPage = lazy(() => import("./pages/account/PurchaseCreditsPage"));
|
||||
const AccountBillingPage = lazy(() => import("./pages/account/AccountBillingPage"));
|
||||
const PlansAndBillingPage = lazy(() => import("./pages/account/PlansAndBillingPage"));
|
||||
const AccountSettingsPage = lazy(() => import("./pages/account/AccountSettingsPage"));
|
||||
const TeamManagementPage = lazy(() => import("./pages/account/TeamManagementPage"));
|
||||
const UsageAnalyticsPage = lazy(() => import("./pages/account/UsageAnalyticsPage"));
|
||||
|
||||
// Admin Module - Lazy loaded (mixed folder casing in repo, match actual file paths)
|
||||
const AdminBilling = lazy(() => import("./pages/Admin/AdminBilling"));
|
||||
const PaymentApprovalPage = lazy(() => import("./pages/admin/PaymentApprovalPage"));
|
||||
const AdminSystemDashboard = lazy(() => import("./pages/admin/AdminSystemDashboard"));
|
||||
const AdminAllAccountsPage = lazy(() => import("./pages/admin/AdminAllAccountsPage"));
|
||||
const AdminSubscriptionsPage = lazy(() => import("./pages/admin/AdminSubscriptionsPage"));
|
||||
const AdminAccountLimitsPage = lazy(() => import("./pages/admin/AdminAccountLimitsPage"));
|
||||
const AdminAllInvoicesPage = lazy(() => import("./pages/admin/AdminAllInvoicesPage"));
|
||||
const AdminAllPaymentsPage = lazy(() => import("./pages/admin/AdminAllPaymentsPage"));
|
||||
const AdminCreditPackagesPage = lazy(() => import("./pages/admin/AdminCreditPackagesPage"));
|
||||
const AdminCreditCostsPage = lazy(() => import("./pages/Admin/AdminCreditCostsPage"));
|
||||
const AdminAllUsersPage = lazy(() => import("./pages/admin/AdminAllUsersPage"));
|
||||
const AdminRolesPermissionsPage = lazy(() => import("./pages/admin/AdminRolesPermissionsPage"));
|
||||
const AdminActivityLogsPage = lazy(() => import("./pages/admin/AdminActivityLogsPage"));
|
||||
const AdminSystemSettingsPage = lazy(() => import("./pages/admin/AdminSystemSettingsPage"));
|
||||
const AdminSystemHealthPage = lazy(() => import("./pages/admin/AdminSystemHealthPage"));
|
||||
const AdminAPIMonitorPage = lazy(() => import("./pages/admin/AdminAPIMonitorPage"));
|
||||
|
||||
// Reference Data - Lazy loaded
|
||||
const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
|
||||
const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
|
||||
|
||||
// Setup Pages - Lazy loaded
|
||||
const IndustriesSectorsKeywords = lazy(() => import("./pages/Setup/IndustriesSectorsKeywords"));
|
||||
|
||||
// Settings - Lazy loaded
|
||||
const GeneralSettings = lazy(() => import("./pages/Settings/General"));
|
||||
const ProfileSettingsPage = lazy(() => import("./pages/settings/ProfileSettingsPage"));
|
||||
const Users = lazy(() => import("./pages/Settings/Users"));
|
||||
const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions"));
|
||||
const SystemSettings = lazy(() => import("./pages/Settings/System"));
|
||||
const AccountSettings = lazy(() => import("./pages/Settings/Account"));
|
||||
const ModuleSettings = lazy(() => import("./pages/Settings/Modules"));
|
||||
const AISettings = lazy(() => import("./pages/Settings/AI"));
|
||||
const Plans = lazy(() => import("./pages/Settings/Plans"));
|
||||
const Industries = lazy(() => import("./pages/Settings/Industries"));
|
||||
const MasterStatus = lazy(() => import("./pages/Settings/MasterStatus"));
|
||||
const ApiMonitor = lazy(() => import("./pages/Settings/ApiMonitor"));
|
||||
const DebugStatus = lazy(() => import("./pages/Settings/DebugStatus"));
|
||||
const Integration = lazy(() => import("./pages/Settings/Integration"));
|
||||
const Publishing = lazy(() => import("./pages/Settings/Publishing"));
|
||||
const Sites = lazy(() => import("./pages/Settings/Sites"));
|
||||
const ImportExport = lazy(() => import("./pages/Settings/ImportExport"));
|
||||
|
||||
// Sites - Lazy loaded
|
||||
const SiteList = lazy(() => import("./pages/Sites/List"));
|
||||
const SiteManage = lazy(() => import("./pages/Sites/Manage"));
|
||||
const SiteDashboard = lazy(() => import("./pages/Sites/Dashboard"));
|
||||
const SiteContent = lazy(() => import("./pages/Sites/Content"));
|
||||
const PageManager = lazy(() => import("./pages/Sites/PageManager"));
|
||||
const PostEditor = lazy(() => import("./pages/Sites/PostEditor"));
|
||||
const SiteSettings = lazy(() => import("./pages/Sites/Settings"));
|
||||
const SyncDashboard = lazy(() => import("./pages/Sites/SyncDashboard"));
|
||||
const DeploymentPanel = lazy(() => import("./pages/Sites/DeploymentPanel"));
|
||||
|
||||
// Help - Lazy loaded
|
||||
const Help = lazy(() => import("./pages/Help/Help"));
|
||||
const Docs = lazy(() => import("./pages/Help/Docs"));
|
||||
const SystemTesting = lazy(() => import("./pages/Help/SystemTesting"));
|
||||
const FunctionTesting = lazy(() => import("./pages/Help/FunctionTesting"));
|
||||
|
||||
// Components - Lazy loaded
|
||||
const Components = lazy(() => import("./pages/Components"));
|
||||
|
||||
// UI Elements - Lazy loaded (rarely used)
|
||||
const Alerts = lazy(() => import("./pages/Settings/UiElements/Alerts"));
|
||||
const Avatars = lazy(() => import("./pages/Settings/UiElements/Avatars"));
|
||||
const Badges = lazy(() => import("./pages/Settings/UiElements/Badges"));
|
||||
const Breadcrumb = lazy(() => import("./pages/Settings/UiElements/Breadcrumb"));
|
||||
const Buttons = lazy(() => import("./pages/Settings/UiElements/Buttons"));
|
||||
const ButtonsGroup = lazy(() => import("./pages/Settings/UiElements/ButtonsGroup"));
|
||||
const Cards = lazy(() => import("./pages/Settings/UiElements/Cards"));
|
||||
const Carousel = lazy(() => import("./pages/Settings/UiElements/Carousel"));
|
||||
const Dropdowns = lazy(() => import("./pages/Settings/UiElements/Dropdowns"));
|
||||
const ImagesUI = lazy(() => import("./pages/Settings/UiElements/Images"));
|
||||
const Links = lazy(() => import("./pages/Settings/UiElements/Links"));
|
||||
const List = lazy(() => import("./pages/Settings/UiElements/List"));
|
||||
const Modals = lazy(() => import("./pages/Settings/UiElements/Modals"));
|
||||
const Notifications = lazy(() => import("./pages/Settings/UiElements/Notifications"));
|
||||
const Pagination = lazy(() => import("./pages/Settings/UiElements/Pagination"));
|
||||
const Popovers = lazy(() => import("./pages/Settings/UiElements/Popovers"));
|
||||
const PricingTable = lazy(() => import("./pages/Settings/UiElements/PricingTable"));
|
||||
const Progressbar = lazy(() => import("./pages/Settings/UiElements/Progressbar"));
|
||||
const Ribbons = lazy(() => import("./pages/Settings/UiElements/Ribbons"));
|
||||
const Spinners = lazy(() => import("./pages/Settings/UiElements/Spinners"));
|
||||
const Tabs = lazy(() => import("./pages/Settings/UiElements/Tabs"));
|
||||
const Tooltips = lazy(() => import("./pages/Settings/UiElements/Tooltips"));
|
||||
const Videos = lazy(() => import("./pages/Settings/UiElements/Videos"));
|
||||
|
||||
export default function App() {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const refreshUser = useAuthStore((state) => state.refreshUser);
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
|
||||
useEffect(() => {
|
||||
const { token } = useAuthStore.getState();
|
||||
if (!isAuthenticated || !token) return;
|
||||
|
||||
refreshUser().catch((error) => {
|
||||
// Avoid log spam on auth pages when token is missing/expired
|
||||
if (error?.message?.includes('Authentication credentials were not provided')) {
|
||||
return;
|
||||
}
|
||||
console.warn('Session validation failed:', error);
|
||||
logout();
|
||||
});
|
||||
}, [isAuthenticated, refreshUser, logout]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalErrorDisplay />
|
||||
<LoadingStateMonitor />
|
||||
<HelmetProvider>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
{/* Auth Routes - Public */}
|
||||
<Route path="/signin" element={<SignIn />} />
|
||||
<Route path="/signup" element={<SignUp />} />
|
||||
|
||||
{/* Protected Routes - Require Authentication */}
|
||||
<Route
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppLayout />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
>
|
||||
{/* Dashboard */}
|
||||
<Route index path="/" element={
|
||||
<Suspense fallback={null}>
|
||||
<Home />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Planner Module - Redirect dashboard to keywords */}
|
||||
<Route path="/planner" element={<Navigate to="/planner/keywords" replace />} />
|
||||
<Route path="/planner/keywords" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="planner">
|
||||
<Keywords />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/planner/clusters" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="planner">
|
||||
<Clusters />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/planner/clusters/:id" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="planner">
|
||||
<ClusterDetail />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/planner/ideas" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="planner">
|
||||
<Ideas />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Writer Module - Redirect dashboard to tasks */}
|
||||
<Route path="/writer" element={<Navigate to="/writer/tasks" replace />} />
|
||||
<Route path="/writer/tasks" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="writer">
|
||||
<Tasks />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
{/* Writer Content Routes - Order matters: list route must come before detail route */}
|
||||
<Route path="/writer/content" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="writer">
|
||||
<Content />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
{/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */}
|
||||
<Route path="/writer/content/:id" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="writer">
|
||||
<ContentView />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/writer/drafts" element={<Navigate to="/writer/content" replace />} />
|
||||
<Route path="/writer/images" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="writer">
|
||||
<Images />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/writer/review" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="writer">
|
||||
<Review />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/writer/published" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="writer">
|
||||
<Published />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Automation Module */}
|
||||
<Route path="/automation" element={
|
||||
<Suspense fallback={null}>
|
||||
<AutomationPage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Linker Module - Redirect dashboard to content */}
|
||||
<Route path="/linker" element={<Navigate to="/linker/content" replace />} />
|
||||
<Route path="/linker/content" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="linker">
|
||||
<LinkerContentList />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Optimizer Module - Redirect dashboard to content */}
|
||||
<Route path="/optimizer" element={<Navigate to="/optimizer/content" replace />} />
|
||||
<Route path="/optimizer/content" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="optimizer">
|
||||
<OptimizerContentSelector />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/optimizer/analyze/:id" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="optimizer">
|
||||
<AnalysisPreview />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Thinker Module */}
|
||||
{/* Thinker Module - Redirect dashboard to prompts */}
|
||||
<Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} />
|
||||
<Route path="/thinker/prompts" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="thinker">
|
||||
<Prompts />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/thinker/author-profiles" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="thinker">
|
||||
<AuthorProfiles />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/thinker/profile" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="thinker">
|
||||
<ThinkerProfile />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/thinker/strategies" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="thinker">
|
||||
<Strategies />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/thinker/image-testing" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleGuard module="thinker">
|
||||
<ImageTesting />
|
||||
</ModuleGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Billing Module */}
|
||||
<Route path="/billing" element={<Navigate to="/billing/overview" replace />} />
|
||||
<Route path="/billing/overview" element={
|
||||
<Suspense fallback={null}>
|
||||
<CreditsAndBilling />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/billing/credits" element={
|
||||
<Suspense fallback={null}>
|
||||
<Credits />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/billing/transactions" element={
|
||||
<Suspense fallback={null}>
|
||||
<Transactions />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/billing/usage" element={
|
||||
<Suspense fallback={null}>
|
||||
<Usage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Account Section - Billing & Management Pages */}
|
||||
<Route path="/account/plans" element={
|
||||
<Suspense fallback={null}>
|
||||
<PlansAndBillingPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/account/billing" element={
|
||||
<Suspense fallback={null}>
|
||||
<AccountBillingPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/account/purchase-credits" element={
|
||||
<Suspense fallback={null}>
|
||||
<PurchaseCreditsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/account/settings" element={
|
||||
<Suspense fallback={null}>
|
||||
<AccountSettingsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/account/team" element={
|
||||
<Suspense fallback={null}>
|
||||
<TeamManagementPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/account/usage" element={
|
||||
<Suspense fallback={null}>
|
||||
<UsageAnalyticsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Admin Routes */}
|
||||
{/* Admin Dashboard */}
|
||||
<Route path="/admin/dashboard" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminSystemDashboard />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Admin Account Management */}
|
||||
<Route path="/admin/accounts" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminAllAccountsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/subscriptions" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminSubscriptionsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/account-limits" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminAccountLimitsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Admin Billing Administration */}
|
||||
<Route path="/admin/billing" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminBilling />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/invoices" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminAllInvoicesPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/payments" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminAllPaymentsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/payments/approvals" element={
|
||||
<Suspense fallback={null}>
|
||||
<PaymentApprovalPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/credit-packages" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminCreditPackagesPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/credit-costs" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminCreditCostsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Admin User Administration */}
|
||||
<Route path="/admin/users" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminAllUsersPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/roles" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminRolesPermissionsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/activity-logs" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminActivityLogsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Admin System Configuration */}
|
||||
<Route path="/admin/settings/system" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminSystemSettingsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Admin Monitoring */}
|
||||
<Route path="/admin/monitoring/health" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminSystemHealthPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/admin/monitoring/api" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminAPIMonitorPage />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Reference Data */}
|
||||
<Route path="/reference/seed-keywords" element={
|
||||
<Suspense fallback={null}>
|
||||
<SeedKeywords />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/planner/keyword-opportunities" element={
|
||||
<Suspense fallback={null}>
|
||||
<KeywordOpportunities />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/reference/industries" element={
|
||||
<Suspense fallback={null}>
|
||||
<ReferenceIndustries />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Setup Pages */}
|
||||
<Route path="/setup/add-keywords" element={
|
||||
<Suspense fallback={null}>
|
||||
<IndustriesSectorsKeywords />
|
||||
</Suspense>
|
||||
} />
|
||||
{/* Legacy redirect */}
|
||||
<Route path="/setup/industries-sectors-keywords" element={<Navigate to="/setup/add-keywords" replace />} />
|
||||
|
||||
{/* Settings */}
|
||||
<Route path="/settings/profile" element={
|
||||
<Suspense fallback={null}>
|
||||
<ProfileSettingsPage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings" element={
|
||||
<Suspense fallback={null}>
|
||||
<GeneralSettings />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/users" element={
|
||||
<Suspense fallback={null}>
|
||||
<Users />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/subscriptions" element={
|
||||
<Suspense fallback={null}>
|
||||
<Subscriptions />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/system" element={
|
||||
<Suspense fallback={null}>
|
||||
<SystemSettings />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/account" element={
|
||||
<Suspense fallback={null}>
|
||||
<AccountSettings />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/modules" element={
|
||||
<Suspense fallback={null}>
|
||||
<ModuleSettings />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/ai" element={
|
||||
<Suspense fallback={null}>
|
||||
<AISettings />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/plans" element={
|
||||
<Suspense fallback={null}>
|
||||
<Plans />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/industries" element={
|
||||
<Suspense fallback={null}>
|
||||
<Industries />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/status" element={
|
||||
<Suspense fallback={null}>
|
||||
<MasterStatus />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/api-monitor" element={
|
||||
<Suspense fallback={null}>
|
||||
<ApiMonitor />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/debug-status" element={
|
||||
<Suspense fallback={null}>
|
||||
<DebugStatus />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/integration" element={
|
||||
<Suspense fallback={null}>
|
||||
<AdminGuard>
|
||||
<Integration />
|
||||
</AdminGuard>
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/publishing" element={
|
||||
<Suspense fallback={null}>
|
||||
<Publishing />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/sites" element={
|
||||
<Suspense fallback={null}>
|
||||
<Sites />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/settings/import-export" element={
|
||||
<Suspense fallback={null}>
|
||||
<ImportExport />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Sites Management */}
|
||||
<Route path="/sites" element={
|
||||
<Suspense fallback={null}>
|
||||
<SiteList />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/manage" element={
|
||||
<Suspense fallback={null}>
|
||||
<SiteManage />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id" element={
|
||||
<Suspense fallback={null}>
|
||||
<SiteDashboard />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/pages" element={
|
||||
<Suspense fallback={null}>
|
||||
<PageManager />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/pages/new" element={
|
||||
<Suspense fallback={null}>
|
||||
<PageManager />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/pages/:pageId/edit" element={
|
||||
<Suspense fallback={null}>
|
||||
<PageManager />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/content" element={
|
||||
<Suspense fallback={null}>
|
||||
<SiteContent />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/settings" element={
|
||||
<Suspense fallback={null}>
|
||||
<SiteSettings />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/sync" element={
|
||||
<Suspense fallback={null}>
|
||||
<SyncDashboard />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/deploy" element={
|
||||
<Suspense fallback={null}>
|
||||
<DeploymentPanel />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/posts/:postId" element={
|
||||
<Suspense fallback={null}>
|
||||
<PostEditor />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/sites/:id/posts/:postId/edit" element={
|
||||
<Suspense fallback={null}>
|
||||
<PostEditor />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
|
||||
{/* Help */}
|
||||
<Route path="/help" element={
|
||||
<Suspense fallback={null}>
|
||||
<Help />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/help/docs" element={
|
||||
<Suspense fallback={null}>
|
||||
<Docs />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/help/system-testing" element={
|
||||
<Suspense fallback={null}>
|
||||
<SystemTesting />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/help/function-testing" element={
|
||||
<Suspense fallback={null}>
|
||||
<FunctionTesting />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* UI Elements */}
|
||||
<Route path="/ui-elements/alerts" element={
|
||||
<Suspense fallback={null}>
|
||||
<Alerts />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/avatars" element={
|
||||
<Suspense fallback={null}>
|
||||
<Avatars />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/badges" element={
|
||||
<Suspense fallback={null}>
|
||||
<Badges />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/breadcrumb" element={
|
||||
<Suspense fallback={null}>
|
||||
<Breadcrumb />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/buttons" element={
|
||||
<Suspense fallback={null}>
|
||||
<Buttons />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/buttons-group" element={
|
||||
<Suspense fallback={null}>
|
||||
<ButtonsGroup />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/cards" element={
|
||||
<Suspense fallback={null}>
|
||||
<Cards />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/carousel" element={
|
||||
<Suspense fallback={null}>
|
||||
<Carousel />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/dropdowns" element={
|
||||
<Suspense fallback={null}>
|
||||
<Dropdowns />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/images" element={
|
||||
<Suspense fallback={null}>
|
||||
<ImagesUI />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/links" element={
|
||||
<Suspense fallback={null}>
|
||||
<Links />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/list" element={
|
||||
<Suspense fallback={null}>
|
||||
<List />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/modals" element={
|
||||
<Suspense fallback={null}>
|
||||
<Modals />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/notifications" element={
|
||||
<Suspense fallback={null}>
|
||||
<Notifications />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/pagination" element={
|
||||
<Suspense fallback={null}>
|
||||
<Pagination />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/popovers" element={
|
||||
<Suspense fallback={null}>
|
||||
<Popovers />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/pricing-table" element={
|
||||
<Suspense fallback={null}>
|
||||
<PricingTable />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/progressbar" element={
|
||||
<Suspense fallback={null}>
|
||||
<Progressbar />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/ribbons" element={
|
||||
<Suspense fallback={null}>
|
||||
<Ribbons />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/spinners" element={
|
||||
<Suspense fallback={null}>
|
||||
<Spinners />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/tabs" element={
|
||||
<Suspense fallback={null}>
|
||||
<Tabs />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/tooltips" element={
|
||||
<Suspense fallback={null}>
|
||||
<Tooltips />
|
||||
</Suspense>
|
||||
} />
|
||||
<Route path="/ui-elements/videos" element={
|
||||
<Suspense fallback={null}>
|
||||
<Videos />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Components (Showcase Page) */}
|
||||
<Route path="/components" element={
|
||||
<Suspense fallback={null}>
|
||||
<Components />
|
||||
</Suspense>
|
||||
} />
|
||||
|
||||
{/* Redirect old notification route */}
|
||||
<Route path="/notifications" element={
|
||||
<Suspense fallback={null}>
|
||||
<Notifications />
|
||||
</Suspense>
|
||||
} />
|
||||
</Route>
|
||||
|
||||
{/* Fallback Route */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</HelmetProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
tenant-temp/frontend/src/components/auth/AdminGuard.tsx
Normal file
25
tenant-temp/frontend/src/components/auth/AdminGuard.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
|
||||
interface AdminGuardProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AdminGuard - restricts access to system account (aws-admin/default) or developer
|
||||
*/
|
||||
export default function AdminGuard({ children }: AdminGuardProps) {
|
||||
const { user } = useAuthStore();
|
||||
const role = user?.role;
|
||||
const accountSlug = user?.account?.slug;
|
||||
const isSystemAccount = accountSlug === 'aws-admin' || accountSlug === 'default-account' || accountSlug === 'default';
|
||||
const allowed = role === 'developer' || isSystemAccount;
|
||||
|
||||
if (!allowed) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
183
tenant-temp/frontend/src/components/auth/ProtectedRoute.tsx
Normal file
183
tenant-temp/frontend/src/components/auth/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { useEffect, ReactNode, useState } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useAuthStore } from "../../store/authStore";
|
||||
import { useErrorHandler } from "../../hooks/useErrorHandler";
|
||||
import { trackLoading } from "../common/LoadingStateMonitor";
|
||||
import { fetchAPI } from "../../services/api";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProtectedRoute component - guards routes requiring authentication
|
||||
* Redirects to /signin if user is not authenticated
|
||||
*/
|
||||
export default function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { isAuthenticated, loading, user, logout } = useAuthStore();
|
||||
const location = useLocation();
|
||||
const { addError } = useErrorHandler('ProtectedRoute');
|
||||
const [showError, setShowError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
const [paymentCheck, setPaymentCheck] = useState<{
|
||||
loading: boolean;
|
||||
hasDefault: boolean;
|
||||
hasAny: boolean;
|
||||
}>({ loading: true, hasDefault: false, hasAny: false });
|
||||
|
||||
const PLAN_ALLOWED_PATHS = [
|
||||
'/account/plans',
|
||||
'/account/billing',
|
||||
'/account/purchase-credits',
|
||||
'/account/settings',
|
||||
'/account/team',
|
||||
'/account/usage',
|
||||
'/billing',
|
||||
];
|
||||
|
||||
const isPlanAllowedPath = PLAN_ALLOWED_PATHS.some((prefix) =>
|
||||
location.pathname.startsWith(prefix)
|
||||
);
|
||||
|
||||
// Track loading state
|
||||
useEffect(() => {
|
||||
trackLoading('auth-loading', loading);
|
||||
}, [loading]);
|
||||
|
||||
// Fetch payment methods to confirm default method availability
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
setPaymentCheck({ loading: false, hasDefault: false, hasAny: false });
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const loadPaymentMethods = async () => {
|
||||
setPaymentCheck((prev) => ({ ...prev, loading: true }));
|
||||
try {
|
||||
const data = await fetchAPI('/v1/billing/payment-methods/');
|
||||
const methods = data?.results || [];
|
||||
const hasAny = methods.length > 0;
|
||||
// Treat id 14 as the intended default, or any method marked default
|
||||
const hasDefault = methods.some((m: any) => m.is_default) || methods.some((m: any) => String(m.id) === '14');
|
||||
if (!cancelled) {
|
||||
setPaymentCheck({ loading: false, hasDefault, hasAny });
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setPaymentCheck({ loading: false, hasDefault: false, hasAny: false });
|
||||
console.warn('ProtectedRoute: failed to fetch payment methods', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadPaymentMethods();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Validate account + plan whenever auth/user changes
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.account) {
|
||||
setErrorMessage('This user is not linked to an account. Please contact support.');
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
}, [isAuthenticated, user, logout]);
|
||||
|
||||
// Immediate check on mount: if loading is true, reset it immediately
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
console.warn('ProtectedRoute: Loading state is true on mount, resetting immediately');
|
||||
useAuthStore.setState({ loading: false });
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Safety timeout: if loading becomes true and stays stuck, show error
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
const timeout1 = setTimeout(() => {
|
||||
setErrorMessage('Authentication check is taking longer than expected. This may indicate a network or server issue.');
|
||||
setShowError(true);
|
||||
addError(new Error('Auth loading stuck for 3 seconds'), 'ProtectedRoute');
|
||||
}, 3000);
|
||||
|
||||
const timeout2 = setTimeout(() => {
|
||||
console.error('ProtectedRoute: Loading state stuck for 5 seconds, forcing reset');
|
||||
useAuthStore.setState({ loading: false });
|
||||
setShowError(false);
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout1);
|
||||
clearTimeout(timeout2);
|
||||
};
|
||||
} else {
|
||||
setShowError(false);
|
||||
}
|
||||
}, [loading, addError]);
|
||||
|
||||
// Show loading state while checking authentication
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<div className="text-center max-w-md px-4">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500 mb-4"></div>
|
||||
<p className="text-lg font-medium text-gray-800 dark:text-white mb-2">Loading...</p>
|
||||
|
||||
{showError && (
|
||||
<div className="mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200 mb-3">
|
||||
{errorMessage}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
useAuthStore.setState({ loading: false });
|
||||
setShowError(false);
|
||||
window.location.reload();
|
||||
}}
|
||||
className="px-4 py-2 text-sm bg-yellow-600 text-white rounded hover:bg-yellow-700"
|
||||
>
|
||||
Retry or Reload Page
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to signin if not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/signin" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
// If authenticated but missing an active plan, keep user inside billing/onboarding
|
||||
const accountStatus = user?.account?.status;
|
||||
const accountInactive = accountStatus && ['suspended', 'cancelled'].includes(accountStatus);
|
||||
const missingPlan = user?.account && !user.account.plan;
|
||||
const missingPayment = !paymentCheck.loading && (!paymentCheck.hasDefault || !paymentCheck.hasAny);
|
||||
|
||||
if ((missingPlan || accountInactive || missingPayment) && !isPlanAllowedPath) {
|
||||
if (paymentCheck.loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<div className="text-center max-w-md px-4">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500 mb-4"></div>
|
||||
<p className="text-lg font-medium text-gray-800 dark:text-white mb-2">Checking billing status...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Navigate to="/account/plans" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
41
tenant-temp/frontend/src/components/common/ModuleGuard.tsx
Normal file
41
tenant-temp/frontend/src/components/common/ModuleGuard.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import { isModuleEnabled } from '../../config/modules.config';
|
||||
import { isUpgradeError } from '../../utils/upgrade';
|
||||
|
||||
interface ModuleGuardProps {
|
||||
module: string;
|
||||
children: ReactNode;
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleGuard - Protects routes based on module enable status
|
||||
* Redirects to settings page if module is disabled
|
||||
*/
|
||||
export default function ModuleGuard({ module, children, redirectTo = '/settings/modules' }: ModuleGuardProps) {
|
||||
const { moduleEnableSettings, loadModuleEnableSettings, loading } = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Load module enable settings if not already loaded
|
||||
if (!moduleEnableSettings && !loading) {
|
||||
loadModuleEnableSettings();
|
||||
}
|
||||
}, [moduleEnableSettings, loading, loadModuleEnableSettings]);
|
||||
|
||||
// While loading, show children (optimistic rendering)
|
||||
if (loading || !moduleEnableSettings) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Check if module is enabled
|
||||
const enabled = isModuleEnabled(module, moduleEnableSettings as any);
|
||||
|
||||
if (!enabled) {
|
||||
return <Navigate to={redirectTo} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
636
tenant-temp/frontend/src/layout/AppSidebar.tsx
Normal file
636
tenant-temp/frontend/src/layout/AppSidebar.tsx
Normal file
@@ -0,0 +1,636 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
// Assume these icons are imported from an icon library
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
GridIcon,
|
||||
HorizontaLDots,
|
||||
ListIcon,
|
||||
PieChartIcon,
|
||||
PlugInIcon,
|
||||
TaskIcon,
|
||||
BoltIcon,
|
||||
DocsIcon,
|
||||
PageIcon,
|
||||
DollarLineIcon,
|
||||
FileIcon,
|
||||
UserIcon,
|
||||
UserCircleIcon,
|
||||
} from "../icons";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import SidebarWidget from "./SidebarWidget";
|
||||
import { APP_VERSION } from "../config/version";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useSettingsStore } from "../store/settingsStore";
|
||||
import ApiStatusIndicator from "../components/sidebar/ApiStatusIndicator";
|
||||
|
||||
type NavItem = {
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
path?: string;
|
||||
subItems?: { name: string; path: string; pro?: boolean; new?: boolean }[];
|
||||
};
|
||||
|
||||
type MenuSection = {
|
||||
label: string;
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
const AppSidebar: React.FC = () => {
|
||||
const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar();
|
||||
const location = useLocation();
|
||||
const { user, isAuthenticated } = useAuthStore();
|
||||
const { moduleEnableSettings, isModuleEnabled: checkModuleEnabled, loadModuleEnableSettings, loading: settingsLoading } = useSettingsStore();
|
||||
|
||||
// Show admin menu only for system account (aws-admin/default) or developer
|
||||
const isAwsAdminAccount = Boolean(
|
||||
user?.account?.slug === 'aws-admin' ||
|
||||
user?.account?.slug === 'default-account' ||
|
||||
user?.account?.slug === 'default' ||
|
||||
user?.role === 'developer'
|
||||
);
|
||||
|
||||
// Helper to check if module is enabled - memoized to prevent infinite loops
|
||||
const moduleEnabled = useCallback((moduleName: string): boolean => {
|
||||
if (!moduleEnableSettings) return true; // Default to enabled if not loaded
|
||||
return checkModuleEnabled(moduleName);
|
||||
}, [moduleEnableSettings, checkModuleEnabled]);
|
||||
|
||||
const [openSubmenu, setOpenSubmenu] = useState<{
|
||||
sectionIndex: number;
|
||||
itemIndex: number;
|
||||
} | null>(null);
|
||||
const [subMenuHeight, setSubMenuHeight] = useState<Record<string, number>>(
|
||||
{}
|
||||
);
|
||||
const subMenuRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
const isActive = useCallback(
|
||||
(path: string) => location.pathname === path,
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
// Load module enable settings on mount (only once) - but only if user is authenticated
|
||||
useEffect(() => {
|
||||
// Only load if user is authenticated and settings aren't already loaded
|
||||
// Skip for non-module pages to reduce unnecessary calls (e.g., account/billing/signup)
|
||||
const path = location.pathname || '';
|
||||
const isModulePage = [
|
||||
'/planner',
|
||||
'/writer',
|
||||
'/automation',
|
||||
'/thinker',
|
||||
'/linker',
|
||||
'/optimizer',
|
||||
'/publisher',
|
||||
'/dashboard',
|
||||
'/home',
|
||||
].some((p) => path.startsWith(p));
|
||||
|
||||
if (user && isAuthenticated && isModulePage && !moduleEnableSettings && !settingsLoading) {
|
||||
loadModuleEnableSettings().catch((error) => {
|
||||
console.warn('Failed to load module enable settings:', error);
|
||||
});
|
||||
}
|
||||
}, [user, isAuthenticated, location.pathname]); // Only run when user/auth or route changes
|
||||
|
||||
// Define menu sections with useMemo to prevent recreation on every render
|
||||
// Filter out disabled modules based on module enable settings
|
||||
// New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS
|
||||
const menuSections: MenuSection[] = useMemo(() => {
|
||||
// SETUP section items (single items, no dropdowns - submenus shown as in-page navigation)
|
||||
const setupItems: NavItem[] = [
|
||||
{
|
||||
icon: <DocsIcon />,
|
||||
name: "Add Keywords",
|
||||
path: "/setup/add-keywords",
|
||||
},
|
||||
{
|
||||
icon: <GridIcon />,
|
||||
name: "Sites",
|
||||
path: "/sites", // Submenus shown as in-page navigation
|
||||
},
|
||||
];
|
||||
|
||||
// Add Thinker if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('thinker')) {
|
||||
setupItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Thinker",
|
||||
path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation
|
||||
});
|
||||
}
|
||||
|
||||
// WORKFLOW section items (single items, no dropdowns - submenus shown as in-page navigation)
|
||||
const workflowItems: NavItem[] = [];
|
||||
|
||||
// Add Planner if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('planner')) {
|
||||
workflowItems.push({
|
||||
icon: <ListIcon />,
|
||||
name: "Planner",
|
||||
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
|
||||
});
|
||||
}
|
||||
|
||||
// Add Writer if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('writer')) {
|
||||
workflowItems.push({
|
||||
icon: <TaskIcon />,
|
||||
name: "Writer",
|
||||
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
|
||||
});
|
||||
}
|
||||
|
||||
// Add Automation (always available if Writer is enabled)
|
||||
if (moduleEnabled('writer')) {
|
||||
workflowItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Automation",
|
||||
path: "/automation",
|
||||
});
|
||||
}
|
||||
|
||||
// Add Linker if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('linker')) {
|
||||
workflowItems.push({
|
||||
icon: <PlugInIcon />,
|
||||
name: "Linker",
|
||||
path: "/linker/content",
|
||||
});
|
||||
}
|
||||
|
||||
// Add Optimizer if enabled (single item, no dropdown)
|
||||
if (moduleEnabled('optimizer')) {
|
||||
workflowItems.push({
|
||||
icon: <BoltIcon />,
|
||||
name: "Optimizer",
|
||||
path: "/optimizer/content",
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
// Dashboard is standalone (no section header)
|
||||
{
|
||||
label: "", // Empty label for standalone Dashboard
|
||||
items: [
|
||||
{
|
||||
icon: <GridIcon />,
|
||||
name: "Dashboard",
|
||||
path: "/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "SETUP",
|
||||
items: setupItems,
|
||||
},
|
||||
{
|
||||
label: "WORKFLOW",
|
||||
items: workflowItems,
|
||||
},
|
||||
{
|
||||
label: "ACCOUNT",
|
||||
items: [
|
||||
{
|
||||
icon: <UserCircleIcon />,
|
||||
name: "Account Settings",
|
||||
path: "/account/settings",
|
||||
},
|
||||
{
|
||||
icon: <DollarLineIcon />,
|
||||
name: "Plans & Billing",
|
||||
path: "/account/billing",
|
||||
},
|
||||
{
|
||||
icon: <DollarLineIcon />,
|
||||
name: "Plans",
|
||||
path: "/account/plans",
|
||||
},
|
||||
{
|
||||
icon: <UserIcon />,
|
||||
name: "Team Management",
|
||||
path: "/account/team",
|
||||
},
|
||||
{
|
||||
icon: <PieChartIcon />,
|
||||
name: "Usage & Analytics",
|
||||
path: "/account/usage",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "SETTINGS",
|
||||
items: [
|
||||
{
|
||||
icon: <UserCircleIcon />,
|
||||
name: "Profile Settings",
|
||||
path: "/settings/profile",
|
||||
},
|
||||
// Integration is admin-only; hide for non-privileged users (handled in render)
|
||||
{
|
||||
icon: <PlugInIcon />,
|
||||
name: "Integration",
|
||||
path: "/settings/integration",
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
icon: <PageIcon />,
|
||||
name: "Publishing",
|
||||
path: "/settings/publishing",
|
||||
},
|
||||
{
|
||||
icon: <FileIcon />,
|
||||
name: "Import / Export",
|
||||
path: "/settings/import-export",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "HELP & DOCS",
|
||||
items: [
|
||||
{
|
||||
icon: <DocsIcon />,
|
||||
name: "Help & Documentation",
|
||||
path: "/help",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}, [moduleEnabled]);
|
||||
|
||||
// Admin section - only shown for users in aws-admin account
|
||||
const adminSection: MenuSection = useMemo(() => ({
|
||||
label: "ADMIN",
|
||||
items: [
|
||||
{
|
||||
icon: <GridIcon />,
|
||||
name: "System Dashboard",
|
||||
path: "/admin/dashboard",
|
||||
},
|
||||
{
|
||||
icon: <UserIcon />,
|
||||
name: "Account Management",
|
||||
subItems: [
|
||||
{ name: "All Accounts", path: "/admin/accounts" },
|
||||
{ name: "Subscriptions", path: "/admin/subscriptions" },
|
||||
{ name: "Account Limits", path: "/admin/account-limits" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <DollarLineIcon />,
|
||||
name: "Billing Administration",
|
||||
subItems: [
|
||||
{ name: "Billing Overview", path: "/admin/billing" },
|
||||
{ name: "Invoices", path: "/admin/invoices" },
|
||||
{ name: "Payments", path: "/admin/payments" },
|
||||
{ name: "Credit Costs Config", path: "/admin/credit-costs" },
|
||||
{ name: "Credit Packages", path: "/admin/credit-packages" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <UserCircleIcon />,
|
||||
name: "User Administration",
|
||||
subItems: [
|
||||
{ name: "All Users", path: "/admin/users" },
|
||||
{ name: "Roles & Permissions", path: "/admin/roles" },
|
||||
{ name: "Activity Logs", path: "/admin/activity-logs" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <PlugInIcon />,
|
||||
name: "System Configuration",
|
||||
subItems: [
|
||||
{ name: "System Settings", path: "/admin/system-settings" },
|
||||
{ name: "AI Settings", path: "/admin/ai-settings" },
|
||||
{ name: "Module Settings", path: "/admin/module-settings" },
|
||||
{ name: "Integration Settings", path: "/admin/integration-settings" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <PieChartIcon />,
|
||||
name: "Monitoring",
|
||||
subItems: [
|
||||
{ name: "System Health", path: "/settings/status" },
|
||||
{ name: "API Monitor", path: "/settings/api-monitor" },
|
||||
{ name: "Debug Status", path: "/settings/debug-status" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <BoltIcon />,
|
||||
name: "Developer Tools",
|
||||
subItems: [
|
||||
{ name: "Function Testing", path: "/admin/function-testing" },
|
||||
{ name: "System Testing", path: "/admin/system-testing" },
|
||||
{ name: "UI Elements", path: "/admin/ui-elements" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}), []);
|
||||
|
||||
// Combine all sections, including admin if user is in aws-admin account
|
||||
const allSections = useMemo(() => {
|
||||
const baseSections = menuSections.map(section => {
|
||||
// Filter adminOnly items for non-system users
|
||||
const filteredItems = section.items.filter((item: any) => {
|
||||
if ((item as any).adminOnly && !isAwsAdminAccount) return false;
|
||||
return true;
|
||||
});
|
||||
return { ...section, items: filteredItems };
|
||||
});
|
||||
return isAwsAdminAccount
|
||||
? [...baseSections, adminSection]
|
||||
: baseSections;
|
||||
}, [isAwsAdminAccount, menuSections, adminSection]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
let foundMatch = false;
|
||||
|
||||
// Find the matching submenu for the current path
|
||||
allSections.forEach((section, sectionIndex) => {
|
||||
section.items.forEach((nav, itemIndex) => {
|
||||
if (nav.subItems && !foundMatch) {
|
||||
const shouldOpen = nav.subItems.some((subItem) => {
|
||||
if (currentPath === subItem.path) return true;
|
||||
if (subItem.path !== '/' && currentPath.startsWith(subItem.path + '/')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (shouldOpen) {
|
||||
setOpenSubmenu((prev) => {
|
||||
// Only update if different to prevent infinite loops
|
||||
if (prev?.sectionIndex === sectionIndex && prev?.itemIndex === itemIndex) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
sectionIndex,
|
||||
itemIndex,
|
||||
};
|
||||
});
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// If no match found and we're not on a submenu path, don't change the state
|
||||
// This allows manual toggles to persist
|
||||
}, [location.pathname, allSections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openSubmenu !== null) {
|
||||
const key = `${openSubmenu.sectionIndex}-${openSubmenu.itemIndex}`;
|
||||
// Use requestAnimationFrame and setTimeout to ensure DOM is ready
|
||||
const frameId = requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
const element = subMenuRefs.current[key];
|
||||
if (element) {
|
||||
// scrollHeight should work even when height is 0px due to overflow-hidden
|
||||
const scrollHeight = element.scrollHeight;
|
||||
if (scrollHeight > 0) {
|
||||
setSubMenuHeight((prevHeights) => {
|
||||
// Only update if height changed to prevent infinite loops
|
||||
if (prevHeights[key] === scrollHeight) {
|
||||
return prevHeights;
|
||||
}
|
||||
return {
|
||||
...prevHeights,
|
||||
[key]: scrollHeight,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
return () => cancelAnimationFrame(frameId);
|
||||
}
|
||||
}, [openSubmenu]);
|
||||
|
||||
const handleSubmenuToggle = (sectionIndex: number, itemIndex: number) => {
|
||||
setOpenSubmenu((prevOpenSubmenu) => {
|
||||
if (
|
||||
prevOpenSubmenu &&
|
||||
prevOpenSubmenu.sectionIndex === sectionIndex &&
|
||||
prevOpenSubmenu.itemIndex === itemIndex
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { sectionIndex, itemIndex };
|
||||
});
|
||||
};
|
||||
|
||||
const renderMenuItems = (items: NavItem[], sectionIndex: number) => (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{items.map((nav, itemIndex) => (
|
||||
<li key={nav.name}>
|
||||
{nav.subItems ? (
|
||||
<button
|
||||
onClick={() => handleSubmenuToggle(sectionIndex, itemIndex)}
|
||||
className={`menu-item group ${
|
||||
openSubmenu?.sectionIndex === sectionIndex && openSubmenu?.itemIndex === itemIndex ||
|
||||
(nav.subItems && nav.subItems.some(subItem => isActive(subItem.path)))
|
||||
? "menu-item-active"
|
||||
: "menu-item-inactive"
|
||||
} cursor-pointer ${
|
||||
!isExpanded && !isHovered
|
||||
? "lg:justify-center"
|
||||
: "lg:justify-start"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`menu-item-icon-size ${
|
||||
(openSubmenu?.sectionIndex === sectionIndex && openSubmenu?.itemIndex === itemIndex) ||
|
||||
(nav.subItems && nav.subItems.some(subItem => isActive(subItem.path)))
|
||||
? "menu-item-icon-active"
|
||||
: "menu-item-icon-inactive"
|
||||
}`}
|
||||
>
|
||||
{nav.icon}
|
||||
</span>
|
||||
{(isExpanded || isHovered || isMobileOpen) && (
|
||||
<span className="menu-item-text">{nav.name}</span>
|
||||
)}
|
||||
{(isExpanded || isHovered || isMobileOpen) && (
|
||||
<ChevronDownIcon
|
||||
className={`ml-auto w-5 h-5 transition-transform duration-200 ${
|
||||
openSubmenu?.sectionIndex === sectionIndex &&
|
||||
openSubmenu?.itemIndex === itemIndex
|
||||
? "rotate-180 text-brand-500"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
nav.path && (
|
||||
<Link
|
||||
to={nav.path}
|
||||
className={`menu-item group ${
|
||||
isActive(nav.path) ? "menu-item-active" : "menu-item-inactive"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`menu-item-icon-size ${
|
||||
isActive(nav.path)
|
||||
? "menu-item-icon-active"
|
||||
: "menu-item-icon-inactive"
|
||||
}`}
|
||||
>
|
||||
{nav.icon}
|
||||
</span>
|
||||
{(isExpanded || isHovered || isMobileOpen) && (
|
||||
<span className="menu-item-text">{nav.name}</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
{nav.subItems && (isExpanded || isHovered || isMobileOpen) && (
|
||||
<div
|
||||
ref={(el) => {
|
||||
subMenuRefs.current[`${sectionIndex}-${itemIndex}`] = el;
|
||||
}}
|
||||
className="overflow-hidden transition-all duration-300"
|
||||
style={{
|
||||
height:
|
||||
openSubmenu?.sectionIndex === sectionIndex && openSubmenu?.itemIndex === itemIndex
|
||||
? `${subMenuHeight[`${sectionIndex}-${itemIndex}`]}px`
|
||||
: "0px",
|
||||
}}
|
||||
>
|
||||
<ul className="mt-2 flex flex-col gap-1 ml-9">
|
||||
{nav.subItems.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
<Link
|
||||
to={subItem.path}
|
||||
className={`menu-dropdown-item ${
|
||||
isActive(subItem.path)
|
||||
? "menu-dropdown-item-active"
|
||||
: "menu-dropdown-item-inactive"
|
||||
}`}
|
||||
>
|
||||
{subItem.name}
|
||||
<span className="flex items-center gap-1 ml-auto">
|
||||
{subItem.new && (
|
||||
<span
|
||||
className={`ml-auto ${
|
||||
isActive(subItem.path)
|
||||
? "menu-dropdown-badge-active"
|
||||
: "menu-dropdown-badge-inactive"
|
||||
} menu-dropdown-badge`}
|
||||
>
|
||||
new
|
||||
</span>
|
||||
)}
|
||||
{subItem.pro && (
|
||||
<span
|
||||
className={`ml-auto ${
|
||||
isActive(subItem.path)
|
||||
? "menu-dropdown-badge-active"
|
||||
: "menu-dropdown-badge-inactive"
|
||||
} menu-dropdown-badge`}
|
||||
>
|
||||
pro
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`fixed mt-16 flex flex-col lg:mt-0 top-0 px-5 left-0 bg-white dark:bg-gray-900 dark:border-gray-800 text-gray-900 h-screen transition-all duration-300 ease-in-out z-50 border-r border-gray-200
|
||||
${
|
||||
isExpanded || isMobileOpen
|
||||
? "w-[290px]"
|
||||
: isHovered
|
||||
? "w-[290px]"
|
||||
: "w-[90px]"
|
||||
}
|
||||
${isMobileOpen ? "translate-x-0" : "-translate-x-full"}
|
||||
lg:translate-x-0`}
|
||||
onMouseEnter={() => !isExpanded && setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
className={`py-8 flex flex-col justify-center items-center gap-3`}
|
||||
>
|
||||
<Link to="/" className="flex justify-center items-center">
|
||||
{isExpanded || isHovered || isMobileOpen ? (
|
||||
<>
|
||||
<img
|
||||
className="dark:hidden"
|
||||
src="/images/logo/logo.svg"
|
||||
alt="Logo"
|
||||
width={113}
|
||||
height={30}
|
||||
/>
|
||||
<img
|
||||
className="hidden dark:block"
|
||||
src="/images/logo/logo-dark.svg"
|
||||
alt="Logo"
|
||||
width={113}
|
||||
height={30}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<img
|
||||
src="/images/logo/logo-icon.svg"
|
||||
alt="Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
{/* Version Badge - Only show when sidebar is expanded */}
|
||||
{(isExpanded || isHovered || isMobileOpen) && (
|
||||
<div className="flex justify-center items-center">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-900 dark:bg-gray-700 text-gray-100 dark:text-gray-300">
|
||||
v{APP_VERSION}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar">
|
||||
{/* API Status Indicator - above OVERVIEW section */}
|
||||
<ApiStatusIndicator />
|
||||
<nav className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
{allSections.map((section, sectionIndex) => (
|
||||
<div key={section.label || `section-${sectionIndex}`}>
|
||||
{section.label && (
|
||||
<h2
|
||||
className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${
|
||||
!isExpanded && !isHovered
|
||||
? "lg:justify-center"
|
||||
: "justify-start"
|
||||
}`}
|
||||
>
|
||||
{isExpanded || isHovered || isMobileOpen ? (
|
||||
section.label
|
||||
) : (
|
||||
<HorizontaLDots className="size-6" />
|
||||
)}
|
||||
</h2>
|
||||
)}
|
||||
{renderMenuItems(section.items, sectionIndex)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
{isExpanded || isHovered || isMobileOpen ? <SidebarWidget /> : null}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppSidebar;
|
||||
Reference in New Issue
Block a user