Spaces:
Runtime error
Runtime error
| import React, { useState, useEffect } from 'react'; | |
| import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'; | |
| import { useAuth } from '../context/AuthContext'; | |
| import { useWebsites } from '../context/WebsiteContext'; | |
| import toast from 'react-hot-toast'; | |
| import { | |
| LayoutDashboard, | |
| MessageSquare, | |
| FileText, | |
| HelpCircle, | |
| Settings as SettingsIcon, | |
| LogOut, | |
| Menu, | |
| X, | |
| User, | |
| Globe, | |
| Users, | |
| Bell | |
| } from 'lucide-react'; | |
| const Layout = ({ children }) => { | |
| const { user, logout } = useAuth(); | |
| const { websites } = useWebsites(); | |
| const location = useLocation(); | |
| const navigate = useNavigate(); | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(false); | |
| const [unreadNotifications, setUnreadNotifications] = useState(0); | |
| const hasWebsites = websites.length > 0; | |
| useEffect(() => { | |
| const fetchUnreadCount = async () => { | |
| try { | |
| const response = await api.get('/notifications/unread-count'); | |
| setUnreadNotifications(response.data.count); | |
| } catch (error) { | |
| console.error('Failed to fetch unread count:', error); | |
| } | |
| }; | |
| if (user) { | |
| fetchUnreadCount(); | |
| const interval = setInterval(fetchUnreadCount, 60000); // Poll every minute | |
| return () => clearInterval(interval); | |
| } | |
| }, [user]); | |
| const handleLogout = () => { | |
| logout(); | |
| navigate('/login'); | |
| }; | |
| const navItems = [ | |
| { icon: LayoutDashboard, label: 'Dashboard', path: '/dashboard' }, | |
| { icon: Globe, label: 'Websites', path: '/websites' }, | |
| { icon: MessageSquare, label: 'Chats', path: '/chats' }, | |
| { icon: Users, label: 'Users', path: '/users' }, | |
| { icon: FileText, label: 'Content', path: '/content' }, | |
| { icon: HelpCircle, label: 'Unanswered', path: '/unanswered-questions' }, | |
| { icon: MessageSquare, label: 'FAQs', path: '/faqs' }, | |
| { icon: Bell, label: 'Notifications', path: '/notifications' }, | |
| { icon: SettingsIcon, label: 'Settings', path: '/settings' }, | |
| ]; | |
| return ( | |
| <div className="min-h-screen bg-secondary-50 flex"> | |
| {/* Sidebar */} | |
| <aside | |
| className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-secondary-200 transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0 ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full' | |
| }`} | |
| > | |
| <div className="h-full flex flex-col"> | |
| {/* Logo */} | |
| <div className="h-16 flex items-center px-6 border-b border-secondary-100"> | |
| <span className="text-xl font-bold bg-gradient-to-r from-primary-600 to-primary-400 bg-clip-text text-transparent"> | |
| CustomerAgent | |
| </span> | |
| </div> | |
| {/* Navigation */} | |
| <nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto"> | |
| {navItems.map((item) => { | |
| const Icon = item.icon; | |
| const isActive = location.pathname === item.path; | |
| const isAlwaysEnabled = ['Websites', 'Settings', 'Notifications'].includes(item.label); | |
| const isDisabled = !hasWebsites && !isAlwaysEnabled; | |
| return ( | |
| <Link | |
| key={item.path} | |
| to={isDisabled ? '#' : item.path} | |
| onClick={(e) => { | |
| if (isDisabled) { | |
| e.preventDefault(); | |
| toast.error('Please add a website first to access this section', { | |
| id: 'sidebar-disabled-toast' | |
| }); | |
| } | |
| }} | |
| className={`flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 ${isActive | |
| ? 'bg-primary-50 text-primary-700 shadow-sm' | |
| : isDisabled | |
| ? 'text-secondary-300 cursor-not-allowed' | |
| : 'text-secondary-600 hover:bg-secondary-50 hover:text-secondary-900' | |
| }`} | |
| > | |
| <Icon className={`w-5 h-5 mr-3 ${isActive ? 'text-primary-600' : isDisabled ? 'text-secondary-200' : 'text-secondary-400'}`} /> | |
| {item.label} | |
| </Link> | |
| ); | |
| })} | |
| </nav> | |
| {/* User Profile */} | |
| <div className="p-4 border-t border-secondary-100"> | |
| <div className="flex items-center p-3 rounded-xl bg-secondary-50 mb-3"> | |
| <div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center text-primary-600 font-semibold"> | |
| {user?.email?.[0].toUpperCase() || <User className="w-5 h-5" />} | |
| </div> | |
| <div className="ml-3 overflow-hidden"> | |
| <p className="text-sm font-medium text-secondary-900 truncate">{user?.email}</p> | |
| <p className="text-xs text-secondary-500 truncate">Admin</p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={handleLogout} | |
| className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-red-600 bg-red-50 rounded-lg hover:bg-red-100 transition-colors" | |
| > | |
| <LogOut className="w-4 h-4 mr-2" /> | |
| Sign Out | |
| </button> | |
| </div> | |
| </div> | |
| </aside> | |
| {/* Mobile Overlay */} | |
| {isSidebarOpen && ( | |
| <div | |
| className="fixed inset-0 z-40 bg-secondary-900/50 backdrop-blur-sm lg:hidden" | |
| onClick={() => setIsSidebarOpen(false)} | |
| /> | |
| )} | |
| {/* Main Content */} | |
| <div className="flex-1 flex flex-col min-w-0 overflow-hidden"> | |
| <header className="h-16 bg-white border-b border-secondary-200 flex items-center justify-between px-4 lg:px-8 shrink-0"> | |
| <div className="flex items-center gap-4"> | |
| <button | |
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | |
| className="p-2 rounded-lg text-secondary-600 hover:bg-secondary-100 lg:hidden" | |
| > | |
| {isSidebarOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />} | |
| </button> | |
| <span className="text-lg font-bold text-secondary-900 lg:hidden">CustomerAgent</span> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <Link | |
| to="/notifications" | |
| className="p-2 bg-secondary-50 text-secondary-600 hover:bg-secondary-100 rounded-xl transition-all relative group" | |
| title="Notifications" | |
| > | |
| <Bell className="w-5 h-5 group-hover:rotate-12 transition-transform" /> | |
| {unreadNotifications > 0 && ( | |
| <span className="absolute top-1.5 right-1.5 w-2.5 h-2.5 bg-red-500 border-2 border-white rounded-full animate-pulse" /> | |
| )} | |
| </Link> | |
| </div> | |
| </header> | |
| {/* Page Content */} | |
| <main className="flex-1 overflow-y-auto p-4 lg:p-8"> | |
| <div className="max-w-7xl mx-auto"> | |
| <Outlet /> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Layout; | |