Merge pull request #38 from operacle/develop
Refactor: Split Sidebar.tsx into smaller components
This commit is contained in:
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
+107
-129
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user