diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 67af7d6d..aeb5f41a 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from 'react' +import { ReactNode, useState, useEffect } from 'react' import { Link, useLocation } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { ThemeToggle } from './ThemeToggle' @@ -7,6 +7,7 @@ import { useAuth } from '../hooks/useAuth' import { checkHealth } from '../api/health' import NotificationCenter from './NotificationCenter' import SystemStatus from './SystemStatus' +import { ChevronLeft, ChevronRight } from 'lucide-react' interface LayoutProps { children: ReactNode @@ -14,9 +15,17 @@ interface LayoutProps { export default function Layout({ children }: LayoutProps) { const location = useLocation() - const [sidebarOpen, setSidebarOpen] = useState(false) + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) + const [isCollapsed, setIsCollapsed] = useState(() => { + const saved = localStorage.getItem('sidebarCollapsed') + return saved ? JSON.parse(saved) : false + }) const { logout } = useAuth() + useEffect(() => { + localStorage.setItem('sidebarCollapsed', JSON.stringify(isCollapsed)) + }, [isCollapsed]) + const { data: health } = useQuery({ queryKey: ['health'], queryFn: checkHealth, @@ -40,20 +49,21 @@ export default function Layout({ children }: LayoutProps) {
-
{/* Sidebar */} {/* Overlay for mobile */} - {sidebarOpen && ( + {/* Mobile Overlay */} + {mobileSidebarOpen && (
setSidebarOpen(false)} + className="fixed inset-0 bg-gray-900/50 z-20 lg:hidden" + onClick={() => setMobileSidebarOpen(false)} /> )} diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx index 6a85ad0d..8447ebca 100644 --- a/frontend/src/pages/Security.tsx +++ b/frontend/src/pages/Security.tsx @@ -9,6 +9,7 @@ import { getProfile, regenerateApiKey, updateProfile } from '../api/user' import { getSettings, updateSetting } from '../api/settings' import { Copy, RefreshCw, Shield, Mail, User } from 'lucide-react' import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter' +import { isValidEmail } from '../utils/validation' export default function Security() { const [oldPassword, setOldPassword] = useState('') @@ -19,9 +20,11 @@ export default function Security() { // Profile State const [name, setName] = useState('') const [email, setEmail] = useState('') + const [emailValid, setEmailValid] = useState(null) // Certificate Email State const [certEmail, setCertEmail] = useState('') + const [certEmailValid, setCertEmailValid] = useState(null) const [useUserEmail, setUseUserEmail] = useState(true) const queryClient = useQueryClient() @@ -44,6 +47,15 @@ export default function Security() { } }, [profile]) + // Validate profile email + useEffect(() => { + if (email) { + setEmailValid(isValidEmail(email)) + } else { + setEmailValid(null) + } + }, [email]) + // Initialize cert email state useEffect(() => { if (settings && profile) { @@ -58,6 +70,15 @@ export default function Security() { } }, [settings, profile]) + // Validate cert email + useEffect(() => { + if (certEmail && !useUserEmail) { + setCertEmailValid(isValidEmail(certEmail)) + } else { + setCertEmailValid(null) + } + }, [certEmail, useUserEmail]) + const updateProfileMutation = useMutation({ mutationFn: updateProfile, onSuccess: () => { @@ -144,12 +165,18 @@ export default function Security() { value={name} onChange={(e) => setName(e.target.value)} /> - setEmail(e.target.value)} - /> +
+ setEmail(e.target.value)} + className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''} + /> + {emailValid === false && ( +

Please enter a valid email address

+ )} +
+ {/* Certificate Email Configuration */} + +
+ +

Certificate Email

+
+

+ This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications. +

+ +
+
+ setUseUserEmail(e.target.checked)} + className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" + /> + +
+ + {!useUserEmail && ( +
+ setCertEmail(e.target.value)} + placeholder="certs@example.com" + className={certEmailValid === false ? 'border-red-500 focus:ring-red-500' : certEmailValid === true ? 'border-green-500 focus:ring-green-500' : ''} + /> + {certEmailValid === false && ( +

Please enter a valid email address

+ )} +
+ )} + + +
+
+ {/* Change Password */}

Change Password

@@ -199,49 +275,6 @@ export default function Security() {
- {/* Certificate Email Configuration */} - -
- -

Certificate Email

-
-

- This email address is used to register with Let's Encrypt for SSL certificate generation and expiration notifications. -

- -
-
- setUseUserEmail(e.target.checked)} - className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" - /> - -
- - {!useUserEmail && ( - setCertEmail(e.target.value)} - placeholder="certs@example.com" - /> - )} - - -
-
- {/* API Key */}

API Key

diff --git a/frontend/src/pages/SettingsLayout.tsx b/frontend/src/pages/SettingsLayout.tsx index 929b3948..5d78eb94 100644 --- a/frontend/src/pages/SettingsLayout.tsx +++ b/frontend/src/pages/SettingsLayout.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' export default function SettingsLayout() { const location = useLocation() - const [tasksOpen, setTasksOpen] = useState(true) + const [tasksOpen, setTasksOpen] = useState(false) const isActive = (path: string) => location.pathname === path diff --git a/frontend/src/pages/Setup.tsx b/frontend/src/pages/Setup.tsx index 36542a9c..f5d51cc8 100644 --- a/frontend/src/pages/Setup.tsx +++ b/frontend/src/pages/Setup.tsx @@ -7,6 +7,7 @@ import { useAuth } from '../hooks/useAuth'; import { Input } from '../components/ui/Input'; import { Button } from '../components/ui/Button'; import { PasswordStrengthMeter } from '../components/PasswordStrengthMeter'; +import { isValidEmail } from '../utils/validation'; const Setup: React.FC = () => { const navigate = useNavigate(); @@ -18,6 +19,7 @@ const Setup: React.FC = () => { password: '', }); const [error, setError] = useState(null); + const [emailValid, setEmailValid] = useState(null); const { data: status, isLoading: statusLoading } = useQuery({ queryKey: ['setupStatus'], @@ -25,6 +27,14 @@ const Setup: React.FC = () => { retry: false, }); + useEffect(() => { + if (formData.email) { + setEmailValid(isValidEmail(formData.email)); + } else { + setEmailValid(null); + } + }, [formData.email]); + useEffect(() => { if (isAuthenticated) { navigate('/'); @@ -94,18 +104,23 @@ const Setup: React.FC = () => { value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} /> - setFormData({ ...formData, email: e.target.value })} - helperText="This email will be used for Let's Encrypt certificate notifications and recovery." - /> -
+
+ setFormData({ ...formData, email: e.target.value })} + className={emailValid === false ? 'border-red-500 focus:ring-red-500' : emailValid === true ? 'border-green-500 focus:ring-green-500' : ''} + /> + {emailValid === false && ( +

Please enter a valid email address

+ )} +
+
{ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) +}