This commit is contained in:
IGNY8 VPS (Salman)
2025-12-08 06:02:04 +00:00
parent 191287829f
commit 156742d679
23 changed files with 442 additions and 0 deletions

View 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>
</>
);
}

View 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}</>;
}

View 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}</>;
}

View 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}</>;
}

View 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;