Merge pull request #38 from operacle/develop

Refactor: Split Sidebar.tsx into smaller components
This commit is contained in:
Tola Leng
2025-06-08 21:46:27 +07:00
committed by GitHub
19 changed files with 844 additions and 357 deletions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

+107 -129
View File
@@ -1,137 +1,115 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { authService } from "@/services/authService";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { LanguageProvider } from "@/contexts/LanguageContext";
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from './contexts/ThemeContext';
import { LanguageProvider } from './contexts/LanguageContext';
import { SidebarProvider } from './contexts/SidebarContext';
import { ErrorBoundary } from './components/ErrorBoundary';
import { Toaster } from './components/ui/sonner';
import { authService } from './services/authService';
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import ServiceDetail from "./pages/ServiceDetail";
import Settings from "./pages/Settings";
import Profile from "./pages/Profile";
import NotFound from "./pages/NotFound";
import SslDomain from "./pages/SslDomain";
import ScheduleIncident from "./pages/ScheduleIncident";
import OperationalPage from "./pages/OperationalPage";
import PublicStatusPage from "./pages/PublicStatusPage";
// Create a Protected route component
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const isAuthenticated = authService.isAuthenticated();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
// Pages
import Index from './pages/Index';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import ServiceDetail from './pages/ServiceDetail';
import Settings from './pages/Settings';
import Profile from './pages/Profile';
import SslDomain from './pages/SslDomain';
import ScheduleIncident from './pages/ScheduleIncident';
import OperationalPage from './pages/OperationalPage';
import PublicStatusPage from './pages/PublicStatusPage';
import NotFound from './pages/NotFound';
const queryClient = new QueryClient();
const App = () => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Check authentication status when the app loads
const checkAuth = async () => {
try {
// Just check the auth state
authService.isAuthenticated();
} finally {
setIsLoading(false);
}
};
checkAuth();
}, []);
if (isLoading) {
return <div className="flex items-center justify-center h-screen bg-background text-foreground">Loading...</div>;
}
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<LanguageProvider>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/service/:id"
element={
<ProtectedRoute>
<ServiceDetail />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route
path="/ssl-domain"
element={
<ProtectedRoute>
<SslDomain />
</ProtectedRoute>
}
/>
<Route
path="/schedule-incident"
element={
<ProtectedRoute>
<ScheduleIncident />
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
{/* Public status page route */}
<Route path="/status/:slug" element={<PublicStatusPage />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</LanguageProvider>
</ThemeProvider>
</QueryClientProvider>
);
// Protected Route Component
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const isAuthenticated = authService.isAuthenticated();
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
};
function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<LanguageProvider>
<SidebarProvider>
<Router>
<div className="min-h-screen bg-background text-foreground">
<Routes>
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/service/:id"
element={
<ProtectedRoute>
<ServiceDetail />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<Settings />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route
path="/ssl-domain"
element={
<ProtectedRoute>
<SslDomain />
</ProtectedRoute>
}
/>
<Route
path="/schedule-incident"
element={
<ProtectedRoute>
<ScheduleIncident />
</ProtectedRoute>
}
/>
<Route
path="/operational-page"
element={
<ProtectedRoute>
<OperationalPage />
</ProtectedRoute>
}
/>
<Route path="/status/:pageSlug" element={<PublicStatusPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Toaster />
</div>
</Router>
</SidebarProvider>
</LanguageProvider>
</ThemeProvider>
</QueryClientProvider>
</ErrorBoundary>
);
}
export default App;
@@ -0,0 +1,49 @@
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
interface ErrorBoundaryProps {
children: React.ReactNode;
}
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center p-6">
<h2 className="text-2xl font-bold text-destructive mb-4">Something went wrong</h2>
<p className="text-muted-foreground mb-4">
{this.state.error?.message || 'An unexpected error occurred'}
</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Reload Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}
+14 -132
View File
@@ -1,140 +1,22 @@
import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, FileText, Settings, User, UserCog, Bell, FileClock, Database, RefreshCw, Info, ChevronDown, BookOpen } from "lucide-react";
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
import { Link, useLocation } from "react-router-dom";
import { useState, useEffect } from "react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useLanguage } from "@/contexts/LanguageContext";
import { SidebarHeader } from "./sidebar/SidebarHeader";
import { MainNavigation } from "./sidebar/MainNavigation";
import { SettingsPanel } from "./sidebar/SettingsPanel";
interface SidebarProps {
collapsed: boolean;
}
export const Sidebar = ({
collapsed
}: SidebarProps) => {
const {
theme
} = useTheme();
const {
t
} = useLanguage();
const location = useLocation();
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
// Update active settings item based on URL
useEffect(() => {
if (location.pathname === '/settings') {
const params = new URLSearchParams(location.search);
const panel = params.get('panel');
if (panel) {
setActiveSettingsItem(panel);
}
}
}, [location]);
export const Sidebar = ({ collapsed }: SidebarProps) => {
const { theme } = useTheme();
const handleSettingsItemClick = (item: string) => {
setActiveSettingsItem(item);
};
const getMenuItemClasses = (isActive: boolean) => {
return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`;
};
// New larger icon size for the main menu
const mainIconSize = "h-6 w-6";
return <div className={`${collapsed ? 'w-16' : 'w-64'} ${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'} border-r flex flex-col transition-all duration-300 h-full`}>
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
<div className="h-8 w-8 bg-green-500 rounded flex items-center justify-center mr-2">
<span className="text-white font-bold">C</span>
</div>
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
</div>
<nav className="my-2 mx-1 py-1 px-1">
<Link to="/dashboard" className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${location.pathname === '/dashboard' ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<Globe className={`${mainIconSize} text-purple-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("uptimeMonitoring")}</span>}
</Link>
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<Boxes className={`${mainIconSize} text-blue-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("instanceMonitoring")}</span>}
</div>
<Link to="/ssl-domain" className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${location.pathname === '/ssl-domain' ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<Radar className={`${mainIconSize} text-cyan-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("sslDomain")}</span>}
</Link>
<Link to="/schedule-incident" className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${location.pathname === '/schedule-incident' ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<Calendar className={`${mainIconSize} text-emerald-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("scheduleIncident")}</span>}
</Link>
<Link to="/operational-page" className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${location.pathname === '/operational-page' ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<BarChart2 className={`${mainIconSize} text-amber-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("operationalPage")}</span>}
</Link>
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<LineChart className={`${mainIconSize} text-rose-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("reports")}</span>}
</div>
<div className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200`}>
<FileText className={`${mainIconSize} text-indigo-400`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t("apiDocumentation")}</span>}
</div>
</nav>
{!collapsed && <div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
<CollapsibleTrigger className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${theme === 'dark' ? 'hover:bg-[#1a1a1a]' : 'hover:bg-sidebar-accent'}`}>
<div className="flex items-center">
<span className="font-medium tracking-wide">{t("settingPanel")}</span>
</div>
<div className="flex items-center">
<Settings className="h-4 w-4 mr-1" />
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className={`${theme === 'dark' ? 'bg-[#121212]' : 'bg-sidebar'} flex-1 flex flex-col`}>
<div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1">
<ScrollArea className="h-full">
<div className="space-y-2 pr-4">
<Link to={`/settings?panel=general`} className={getMenuItemClasses(activeSettingsItem === 'general')} onClick={() => handleSettingsItemClick('general')}>
<Settings className="h-4 w-4 mr-2" />
<span className="text-sm">{t("generalSettings")}</span>
</Link>
<Link to={`/settings?panel=users`} className={getMenuItemClasses(activeSettingsItem === 'users')} onClick={() => handleSettingsItemClick('users')}>
<User className="h-4 w-4 mr-2" />
<span className="text-sm">{t("userManagement")}</span>
</Link>
<Link to={`/settings?panel=notifications`} className={getMenuItemClasses(activeSettingsItem === 'notifications')} onClick={() => handleSettingsItemClick('notifications')}>
<Bell className="h-4 w-4 mr-2" />
<span className="text-sm">{t("notificationSettings")}</span>
</Link>
<Link to={`/settings?panel=templates`} className={getMenuItemClasses(activeSettingsItem === 'templates')} onClick={() => handleSettingsItemClick('templates')}>
<BookOpen className="h-4 w-4 mr-2" />
<span className="text-sm">{t("alertsTemplates")}</span>
</Link>
<Link to={`/settings?panel=data-retention`} className={getMenuItemClasses(activeSettingsItem === 'data-retention')} onClick={() => handleSettingsItemClick('data-retention')}>
<Database className="h-4 w-4 mr-2" />
<span className="text-sm">{t("dataRetention")}</span>
</Link>
<Link to={`/settings?panel=about`} className={getMenuItemClasses(activeSettingsItem === 'about')} onClick={() => handleSettingsItemClick('about')}>
<Info className="h-4 w-4 mr-2" />
<span className="text-sm">{t("aboutSystem")}</span>
</Link>
</div>
</ScrollArea>
</div>
</CollapsibleContent>
</Collapsible>
</div>}
{collapsed && <div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
<Link to="/settings">
<Settings className={`${mainIconSize} text-purple-400`} />
</Link>
</div>}
</div>;
return (
<div className={`${collapsed ? 'w-16' : 'w-64'} ${theme === 'dark' ? 'bg-[#121212] border-[#1e1e1e]' : 'bg-sidebar border-sidebar-border'} border-r flex flex-col transition-all duration-300 h-full`}>
<SidebarHeader collapsed={collapsed} />
<MainNavigation collapsed={collapsed} />
<SettingsPanel collapsed={collapsed} />
</div>
);
};
@@ -0,0 +1,27 @@
import React from "react";
import { MenuItem } from "./MenuItem";
import { mainMenuItems } from "./navigationData";
interface MainNavigationProps {
collapsed: boolean;
}
export const MainNavigation: React.FC<MainNavigationProps> = ({ collapsed }) => {
return (
<nav className="my-2 mx-1 py-1 px-1">
{mainMenuItems.map((item) => (
<MenuItem
key={item.id}
id={item.id}
path={item.path}
icon={item.icon}
translationKey={item.translationKey}
color={item.color}
hasNavigation={item.hasNavigation}
collapsed={collapsed}
/>
))}
</nav>
);
};
@@ -0,0 +1,54 @@
import React from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
import { LucideIcon } from "lucide-react";
interface MenuItemProps {
id: string;
path: string | null;
icon: LucideIcon;
translationKey: string;
color: string;
hasNavigation: boolean;
collapsed: boolean;
}
export const MenuItem: React.FC<MenuItemProps> = ({
id,
path,
icon: Icon,
translationKey,
color,
hasNavigation,
collapsed
}) => {
const { theme } = useTheme();
const { t } = useLanguage();
const location = useLocation();
const navigate = useNavigate();
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (hasNavigation && path) {
// Use navigate instead of window.location to prevent full page reload
navigate(path, { replace: false });
}
};
const isActive = path && location.pathname === path;
const mainIconSize = "h-6 w-6";
return (
<div
className={`${collapsed ? 'p-3' : 'p-2 pl-3'} mb-1 rounded-lg ${isActive ? theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-gray-800' : 'bg-sidebar-accent'}`} flex items-center ${collapsed ? 'justify-center' : ''} transition-colors duration-200 cursor-pointer`}
onClick={handleClick}
>
<Icon className={`${mainIconSize} ${color}`} />
{!collapsed && <span className="ml-2.5 font-medium text-foreground tracking-wide text-[15px]">{t(translationKey)}</span>}
</div>
);
};
@@ -0,0 +1,100 @@
import React, { useState, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useTheme } from "@/contexts/ThemeContext";
import { useLanguage } from "@/contexts/LanguageContext";
import { Settings, ChevronDown } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { ScrollArea } from "@/components/ui/scroll-area";
import { settingsMenuItems } from "./navigationData";
interface SettingsPanelProps {
collapsed: boolean;
}
export const SettingsPanel: React.FC<SettingsPanelProps> = ({ collapsed }) => {
const { theme } = useTheme();
const { t } = useLanguage();
const location = useLocation();
const navigate = useNavigate();
const [activeSettingsItem, setActiveSettingsItem] = useState<string | null>("general");
const [settingsPanelOpen, setSettingsPanelOpen] = useState(false);
// Update active settings item based on URL
useEffect(() => {
if (location.pathname === '/settings') {
const params = new URLSearchParams(location.search);
const panel = params.get('panel');
if (panel) {
setActiveSettingsItem(panel);
}
}
}, [location]);
const handleSettingsItemClick = (item: string) => {
setActiveSettingsItem(item);
};
const handleMenuItemClick = (path: string, event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
// Use navigate instead of window.location to prevent full page reload
navigate(path, { replace: false });
};
const getMenuItemClasses = (isActive: boolean) => {
return `p-2 ${isActive ? theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent' : `hover:${theme === 'dark' ? 'bg-[#1a1a1a]' : 'bg-sidebar-accent'}`} rounded-lg flex items-center`;
};
if (collapsed) {
const mainIconSize = "h-6 w-6";
return (
<div className={`border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4 flex justify-center`}>
<div
onClick={(e) => handleMenuItemClick('/settings', e)}
className="cursor-pointer"
>
<Settings className={`${mainIconSize} text-purple-400`} />
</div>
</div>
);
}
return (
<div className={`flex-1 flex flex-col border-t ${theme === 'dark' ? 'border-[#1e1e1e] bg-[#121212]' : 'border-sidebar-border bg-sidebar'} p-4`}>
<Collapsible open={settingsPanelOpen} onOpenChange={setSettingsPanelOpen} className="w-full flex flex-col flex-1">
<CollapsibleTrigger className={`flex items-center justify-between w-full mb-4 px-2 py-2 rounded-lg ${theme === 'dark' ? 'hover:bg-[#1a1a1a]' : 'hover:bg-sidebar-accent'}`}>
<div className="flex items-center">
<span className="font-medium tracking-wide">{t("settingPanel")}</span>
</div>
<div className="flex items-center">
<Settings className="h-4 w-4 mr-1" />
<ChevronDown className={`h-4 w-4 transition-transform duration-200 ${settingsPanelOpen ? 'rotate-180' : ''}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className={`${theme === 'dark' ? 'bg-[#121212]' : 'bg-sidebar'} flex-1 flex flex-col`}>
<div className="max-h-[300px] overflow-y-auto custom-scrollbar relative pr-1">
<ScrollArea className="h-full">
<div className="space-y-2 pr-4">
{settingsMenuItems.map((item) => (
<div
key={item.id}
className={getMenuItemClasses(activeSettingsItem === item.id)}
onClick={(e) => {
handleMenuItemClick(`/settings?panel=${item.id}`, e);
handleSettingsItemClick(item.id);
}}
>
<item.icon className="h-4 w-4 mr-2" />
<span className="text-sm">{t(item.translationKey)}</span>
</div>
))}
</div>
</ScrollArea>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
};
@@ -0,0 +1,20 @@
import React from "react";
import { useTheme } from "@/contexts/ThemeContext";
interface SidebarHeaderProps {
collapsed: boolean;
}
export const SidebarHeader: React.FC<SidebarHeaderProps> = ({ collapsed }) => {
const { theme } = useTheme();
return (
<div className={`p-4 ${theme === 'dark' ? 'border-[#1e1e1e]' : 'border-sidebar-border'} border-b flex items-center ${collapsed ? 'justify-center' : ''}`}>
<div className="h-8 w-8 bg-green-500 rounded flex items-center justify-center mr-2">
<span className="text-white font-bold">C</span>
</div>
{!collapsed && <h1 className="text-xl font-semibold">CheckCle App</h1>}
</div>
);
};
@@ -0,0 +1,6 @@
export { SidebarHeader } from './SidebarHeader';
export { MainNavigation } from './MainNavigation';
export { MenuItem } from './MenuItem';
export { SettingsPanel } from './SettingsPanel';
export { mainMenuItems, settingsMenuItems } from './navigationData';
@@ -0,0 +1,94 @@
import { Globe, Boxes, Radar, Calendar, BarChart2, LineChart, FileText, Settings, User, Bell, Database, Info, BookOpen } from "lucide-react";
export const mainMenuItems = [
{
id: 'uptime-monitoring',
path: '/dashboard',
icon: Globe,
translationKey: 'uptimeMonitoring',
color: 'text-purple-400',
hasNavigation: true
},
{
id: 'instance-monitoring',
path: null,
icon: Boxes,
translationKey: 'instanceMonitoring',
color: 'text-blue-400',
hasNavigation: false
},
{
id: 'ssl-domain',
path: '/ssl-domain',
icon: Radar,
translationKey: 'sslDomain',
color: 'text-cyan-400',
hasNavigation: true
},
{
id: 'schedule-incident',
path: '/schedule-incident',
icon: Calendar,
translationKey: 'scheduleIncident',
color: 'text-emerald-400',
hasNavigation: true
},
{
id: 'operational-page',
path: '/operational-page',
icon: BarChart2,
translationKey: 'operationalPage',
color: 'text-amber-400',
hasNavigation: true
},
{
id: 'reports',
path: null,
icon: LineChart,
translationKey: 'reports',
color: 'text-rose-400',
hasNavigation: false
},
{
id: 'api-documentation',
path: null,
icon: FileText,
translationKey: 'apiDocumentation',
color: 'text-indigo-400',
hasNavigation: false
}
];
export const settingsMenuItems = [
{
id: 'general',
icon: Settings,
translationKey: 'generalSettings'
},
{
id: 'users',
icon: User,
translationKey: 'userManagement'
},
{
id: 'notifications',
icon: Bell,
translationKey: 'notificationSettings'
},
{
id: 'templates',
icon: BookOpen,
translationKey: 'alertsTemplates'
},
{
id: 'data-retention',
icon: Database,
translationKey: 'dataRetention'
},
{
id: 'about',
icon: Info,
translationKey: 'aboutSystem'
}
];
@@ -0,0 +1,38 @@
import React, { createContext, useContext, useState } from 'react';
interface SidebarContextType {
sidebarCollapsed: boolean;
setSidebarCollapsed: (collapsed: boolean) => void;
toggleSidebar: () => void;
}
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const toggleSidebar = () => {
setSidebarCollapsed(prev => !prev);
};
const value = {
sidebarCollapsed,
setSidebarCollapsed,
toggleSidebar
};
return (
<SidebarContext.Provider value={value}>
{children}
</SidebarContext.Provider>
);
};
export const useSidebar = () => {
const context = useContext(SidebarContext);
if (context === undefined) {
throw new Error('useSidebar must be used within a SidebarProvider');
}
return context;
};
+5 -5
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar";
@@ -8,11 +8,11 @@ import { serviceService } from "@/services/serviceService";
import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom";
import { LoadingState } from "@/components/services/LoadingState";
import { useSidebar } from "@/contexts/SidebarContext";
const Dashboard = () => {
// State for sidebar collapse functionality
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
@@ -71,4 +71,4 @@ const Dashboard = () => {
);
};
export default Dashboard;
export default Dashboard;
+20 -5
View File
@@ -1,14 +1,29 @@
// Update this page (the content is just a fallback if you fail to update the page)
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { authService } from '@/services/authService';
const Index = () => {
const navigate = useNavigate();
useEffect(() => {
// Check if user is authenticated and redirect accordingly
if (authService.isAuthenticated()) {
navigate('/dashboard', { replace: true });
} else {
navigate('/login', { replace: true });
}
}, [navigate]);
// Show a loading state while redirecting
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Welcome to Your Blank App</h1>
<p className="text-xl text-gray-600">Start building your amazing project here!</p>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
);
};
export default Index;
export default Index;
+291 -67
View File
@@ -1,11 +1,10 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { authService } from '@/services/authService';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui/use-toast';
import { Eye, EyeOff, Mail, LogIn, Settings } from "lucide-react";
import { toast } from '@/hooks/use-toast';
import { Eye, EyeOff, Mail, LogIn, Settings, AlertCircle } from "lucide-react";
import { useLanguage } from '@/contexts/LanguageContext';
import { useTheme } from '@/contexts/ThemeContext';
import { API_ENDPOINTS, getCurrentEndpoint, setApiEndpoint } from '@/lib/pocketbase';
@@ -13,51 +12,183 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
const MAX_LOGIN_ATTEMPTS = 5;
const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes
interface LoginAttempts {
count: number;
lastAttempt: number;
lockedUntil?: number;
}
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [currentEndpoint, setCurrentEndpoint] = useState(getCurrentEndpoint());
const [errors, setErrors] = useState<{ email?: string; password?: string; general?: string }>({});
const [loginAttempts, setLoginAttempts] = useState<LoginAttempts>({ count: 0, lastAttempt: 0 });
const [isLocked, setIsLocked] = useState(false);
const [lockoutTimeRemaining, setLockoutTimeRemaining] = useState(0);
const navigate = useNavigate();
const { t } = useLanguage();
const { theme } = useTheme();
// Add responsiveness check
const [isMobile, setIsMobile] = useState(false);
// Check if we're in development mode
const isDevelopment = import.meta.env.DEV;
useEffect(() => {
const checkScreenSize = () => {
setIsMobile(window.innerWidth < 640);
};
checkScreenSize();
window.addEventListener('resize', checkScreenSize);
return () => {
window.removeEventListener('resize', checkScreenSize);
};
}, []);
// Load login attempts from localStorage
useEffect(() => {
const stored = localStorage.getItem('login_attempts');
if (stored) {
try {
const attempts: LoginAttempts = JSON.parse(stored);
setLoginAttempts(attempts);
if (attempts.lockedUntil && Date.now() < attempts.lockedUntil) {
setIsLocked(true);
setLockoutTimeRemaining(Math.ceil((attempts.lockedUntil - Date.now()) / 1000));
}
} catch {
localStorage.removeItem('login_attempts');
}
}
}, []);
// Update lockout timer
useEffect(() => {
if (isLocked && lockoutTimeRemaining > 0) {
const timer = setInterval(() => {
setLockoutTimeRemaining(prev => {
if (prev <= 1) {
setIsLocked(false);
setLoginAttempts({ count: 0, lastAttempt: 0 });
localStorage.removeItem('login_attempts');
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}
}, [isLocked, lockoutTimeRemaining]);
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const validatePassword = (password: string): boolean => {
return password.length >= 6; // Minimum 6 characters
};
const validateForm = (): boolean => {
const newErrors: { email?: string; password?: string } = {};
if (!email.trim()) {
newErrors.email = t("emailRequired");
} else if (!validateEmail(email)) {
newErrors.email = t("emailInvalid");
}
if (!password) {
newErrors.password = t("passwordRequired");
} else if (!validatePassword(password)) {
newErrors.password = t("passwordTooShort");
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const updateLoginAttempts = useCallback((failed: boolean) => {
const now = Date.now();
if (failed) {
const newCount = loginAttempts.count + 1;
let newAttempts: LoginAttempts = {
count: newCount,
lastAttempt: now
};
if (newCount >= MAX_LOGIN_ATTEMPTS) {
newAttempts.lockedUntil = now + LOCKOUT_DURATION;
setIsLocked(true);
setLockoutTimeRemaining(LOCKOUT_DURATION / 1000);
}
setLoginAttempts(newAttempts);
localStorage.setItem('login_attempts', JSON.stringify(newAttempts));
} else {
// Reset on successful login
setLoginAttempts({ count: 0, lastAttempt: 0 });
localStorage.removeItem('login_attempts');
}
}, [loginAttempts.count]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isLocked) {
toast({
variant: "destructive",
title: t("accountLocked"),
description: t("tooManyAttempts"),
});
return;
}
if (!validateForm()) {
return;
}
setLoading(true);
setErrors({});
try {
await authService.login({ email, password });
await authService.login({ email: email.toLowerCase().trim(), password });
updateLoginAttempts(false);
toast({
title: t("loginSuccessful"),
description: t("loginSuccessMessage"),
});
navigate('/dashboard');
} catch (error) {
console.error("Login error details:", error);
updateLoginAttempts(true);
// Generic error message for security
const remainingAttempts = MAX_LOGIN_ATTEMPTS - (loginAttempts.count + 1);
let errorMessage = t("invalidCredentials");
if (remainingAttempts > 0 && remainingAttempts <= 2) {
errorMessage += ` ${t("attemptsRemaining")}: ${remainingAttempts}`;
}
setErrors({ general: errorMessage });
toast({
variant: "destructive",
title: t("loginFailed"),
description: error instanceof Error
? error.message
: `${t("authenticationFailed")}. Server: ${currentEndpoint}`,
description: errorMessage,
});
} finally {
setLoading(false);
@@ -73,96 +204,175 @@ const Login = () => {
setShowPassword(!showPassword);
};
const formatLockoutTime = (seconds: number): string => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
return (
<div className="flex items-center justify-center min-h-screen bg-background p-4">
<div className="w-full max-w-md p-4 sm:p-8 space-y-6 rounded-xl bg-card shadow-xl border border-border/20">
<div className="text-center relative">
<div className="absolute right-0 top-0">
<Popover>
{/* <PopoverContent className="w-[260px] sm:w-80"> //this allow test api connection in login page
<div className="space-y-4">
<h4 className="font-medium">API Endpoint Settings</h4>
<RadioGroup
value={currentEndpoint}
onValueChange={handleEndpointChange}
className="gap-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.LOCAL} id="local" />
<Label htmlFor="local" className="truncate text-sm">Local: {API_ENDPOINTS.LOCAL}</Label>
{/* API Endpoint Settings - Only show in development */}
{/*isDevelopment && (
<div className="absolute right-0 top-0">
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Settings className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] sm:w-80">
<div className="space-y-4">
<h4 className="font-medium">API Endpoint Settings</h4>
<RadioGroup
value={currentEndpoint}
onValueChange={handleEndpointChange}
className="gap-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.LOCAL} id="local" />
<Label htmlFor="local" className="truncate text-sm">
Local: {API_ENDPOINTS.LOCAL}
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.REMOTE} id="remote" />
<Label htmlFor="remote" className="truncate text-sm">
Remote: {API_ENDPOINTS.REMOTE}
</Label>
</div>
</RadioGroup>
<div className="text-xs text-muted-foreground truncate">
Current: {currentEndpoint}
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value={API_ENDPOINTS.REMOTE} id="remote" />
<Label htmlFor="remote" className="truncate text-sm">Remote: {API_ENDPOINTS.REMOTE}</Label>
</div>
</RadioGroup>
<div className="text-xs text-muted-foreground truncate">
Current endpoint: {currentEndpoint}
</div>
</div>
</PopoverContent> */}
</Popover>
</div>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">{t("signInToYourAccount")}</h1>
</div>
<div>
<div className="mt-4 sm:mt-6 relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border"></div>
</PopoverContent>
</Popover>
</div>
<div className="relative flex justify-center text-xs sm:text-sm">
<span className="px-4 bg-card text-muted-foreground">{t("orContinueWith")}</span>
)*/}
{/* Logo */}
<img
src="/checkcle_logo.svg"
alt="Checkcle Logo"
className="h-16 sm:h-20 mx-auto mb-4"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight text-foreground">
{t("signInToYourAccount")}
</h1>
{isLocked && (
<div className="mt-4 p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<div className="flex items-center gap-2 text-destructive text-sm">
<AlertCircle className="h-4 w-4" />
<span>
{t("accountLocked")} - {formatLockoutTime(lockoutTimeRemaining)}
</span>
</div>
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="mt-4 sm:mt-6 space-y-4 sm:space-y-6">
{/* General Error */}
{errors.general && (
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<div className="flex items-center gap-2 text-destructive text-sm">
<AlertCircle className="h-4 w-4" />
<span>{errors.general}</span>
</div>
</div>
)}
<div className="space-y-1 sm:space-y-2">
<label className="text-xs sm:text-sm font-medium text-foreground" htmlFor="email">{t("email")}</label>
<label htmlFor="email" className="text-xs sm:text-sm font-medium text-foreground">
{t("email")}
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Mail className="h-4 w-4 text-muted-foreground" />
</div>
<Input
id="email"
name="email"
placeholder="your.email@provider.com"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => {
setEmail(e.target.value);
if (errors.email) {
setErrors(prev => ({ ...prev, email: undefined }));
}
}}
required
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
autoComplete="email"
disabled={loading || isLocked}
className={`pl-10 text-sm sm:text-base h-9 sm:h-10 ${
errors.email ? 'border-destructive focus:border-destructive' : ''
}`}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
</div>
{errors.email && (
<p id="email-error" className="text-xs text-destructive">
{errors.email}
</p>
)}
</div>
<div className="space-y-1 sm:space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs sm:text-sm font-medium text-foreground" htmlFor="password">{t("password")}</label>
<a href="#" className="text-xs sm:text-sm text-emerald-500 hover:text-emerald-400">{t("forgot")}</a>
<label htmlFor="password" className="text-xs sm:text-sm font-medium text-foreground">
{t("password")}
</label>
<a
href="#"
className="text-xs sm:text-sm text-primary hover:text-primary/80 transition-colors"
onClick={(e) => e.preventDefault()}
>
{t("forgot")}
</a>
</div>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
strokeLinejoin="round" className="text-muted-foreground">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<Input
id="password"
name="password"
placeholder="••••••••••••"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => {
setPassword(e.target.value);
if (errors.password) {
setErrors(prev => ({ ...prev, password: undefined }));
}
}}
required
className="pl-10 text-sm sm:text-base h-9 sm:h-10"
autoComplete="current-password"
disabled={loading || isLocked}
className={`pl-10 pr-10 text-sm sm:text-base h-9 sm:h-10 ${
errors.password ? 'border-destructive focus:border-destructive' : ''
}`}
aria-describedby={errors.password ? 'password-error' : undefined}
/>
<button
type="button"
className="absolute inset-y-0 right-0 flex items-center pr-3"
onClick={togglePasswordVisibility}
disabled={loading || isLocked}
aria-label={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? (
@@ -172,19 +382,33 @@ const Login = () => {
)}
</button>
</div>
{errors.password && (
<p id="password-error" className="text-xs text-destructive">
{errors.password}
</p>
)}
</div>
<Button
type="submit"
className="w-full py-2 sm:py-6 bg-emerald-500 hover:bg-emerald-600 text-white font-medium text-sm sm:text-base"
disabled={loading}
<Button
type="submit"
className="w-full py-2 sm:py-6 font-medium text-sm sm:text-base"
disabled={loading || isLocked}
>
{loading ? t("signingIn") : t("signIn")}
{!loading && <LogIn className="ml-2 h-4 w-4" />}
{loading ? (
<>
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent mr-2" />
{t("signingIn")}
</>
) : (
<>
{t("signIn")}
<LogIn className="ml-2 h-4 w-4" />
</>
)}
</Button>
<p className="text-xxs sm:text-xs text-center text-muted-foreground">
{t("bySigningIn")} <a href="#" className="text-emerald-500">{t("termsAndConditions")}</a> {t("and")} <a href="#" className="text-emerald-500">{t("privacyPolicy")}</a>.
<p className="text-xs sm:text-xs text-center text-muted-foreground">
{t("bySigningIn")} <a href="#" className="text-primary hover:text-primary/80">{t("termsAndConditions")}</a> {t("and")} <a href="#" className="text-primary hover:text-primary/80">{t("privacyPolicy")}</a>.
</p>
</form>
</div>
+4 -4
View File
@@ -1,15 +1,15 @@
import React, { useState } from "react";
import React from "react";
import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { OperationalPageContent } from '@/components/operational-page/OperationalPageContent';
import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom";
import { useSidebar } from "@/contexts/SidebarContext";
const OperationalPage = () => {
// State for sidebar collapse functionality
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
+3 -3
View File
@@ -9,11 +9,11 @@ import { ProfileContent } from "@/components/profile/ProfileContent";
import { User } from "@/services/userService";
import { useToast } from "@/hooks/use-toast";
import { Loader2 } from "lucide-react";
import { useSidebar } from "@/contexts/SidebarContext";
const Profile = () => {
// State for sidebar collapse functionality
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
+4 -6
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect } from 'react';
import { Header } from "@/components/dashboard/Header";
import { Sidebar } from "@/components/dashboard/Sidebar";
import { useTheme } from "@/contexts/ThemeContext";
@@ -8,11 +8,11 @@ import { ScheduleIncidentContent } from "@/components/schedule-incident/Schedule
import { authService } from "@/services/authService";
import { useNavigate } from "react-router-dom";
import { initMaintenanceNotifications, stopMaintenanceNotifications } from "@/services/maintenance/maintenanceNotificationService";
import { useSidebar } from "@/contexts/SidebarContext";
const ScheduleIncident = () => {
// State for sidebar collapse functionality
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current theme and language
const { theme } = useTheme();
@@ -24,12 +24,10 @@ const ScheduleIncident = () => {
// Initialize maintenance notifications
useEffect(() => {
console.log("Initializing maintenance notifications");
initMaintenanceNotifications();
// Clean up on unmount
return () => {
console.log("Stopping maintenance notifications");
stopMaintenanceNotifications();
};
}, []);
+3 -3
View File
@@ -10,11 +10,11 @@ import { NotificationSettings } from "@/components/settings/notification-setting
import { AlertsTemplates } from "@/components/settings/alerts-templates";
import { AboutSystem } from "@/components/settings/about-system";
import DataRetentionSettings from "@/components/settings/data-retention/DataRetentionSettings";
import { useSidebar } from "@/contexts/SidebarContext";
const Settings = () => {
// State for sidebar collapse functionality
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();
+4 -3
View File
@@ -1,3 +1,4 @@
import React, { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { authService } from "@/services/authService";
@@ -8,14 +9,14 @@ import { SSLDomainContent } from "@/components/ssl-domain/SSLDomainContent";
import { LoadingState } from "@/components/services/LoadingState";
import { fetchSSLCertificates, shouldRunDailyCheck, checkAllCertificatesAndNotify } from "@/services/sslCertificateService";
import { useLanguage } from "@/contexts/LanguageContext";
import { useSidebar } from "@/contexts/SidebarContext";
const SslDomain = () => {
// Get language context for translations
const { t } = useLanguage();
// State for sidebar collapse functionality
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
// Use shared sidebar state
const { sidebarCollapsed, toggleSidebar } = useSidebar();
// Get current user
const currentUser = authService.getCurrentUser();