Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { NavLink, useLocation, useNavigate } from 'react-router-dom'; | |
| import ShopSelector from './ShopSelector'; | |
| import { useAuth } from '../contexts/AuthContext'; | |
| interface LayoutProps { | |
| children: React.ReactNode; | |
| } | |
| const Layout: React.FC<LayoutProps> = ({ children }) => { | |
| const location = useLocation(); | |
| const navigate = useNavigate(); | |
| const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); | |
| const { user, isAdmin, logout, hasPermission } = useAuth(); | |
| const handleLogout = () => { | |
| logout(); | |
| navigate('/login'); | |
| }; | |
| const navItems = [ | |
| { name: 'Dashboard', path: '/', icon: 'dashboard', permission: 'view_dashboard' }, | |
| { name: 'Profit Analytics', path: '/profit', icon: 'bar_chart', permission: 'view_profit' }, | |
| { name: 'Orders', path: '/orders', icon: 'shopping_cart', permission: 'view_orders' }, | |
| { name: 'Data Import', path: '/import', icon: 'cloud_upload', permission: 'view_import' }, | |
| { name: 'Shops', path: '/shops', icon: 'storefront', permission: 'view_shops' }, | |
| { name: 'Settings', path: '/settings', icon: 'settings', permission: 'manage_settings' }, | |
| ]; | |
| const filteredNavItems = navItems.filter(item => hasPermission(item.permission)); | |
| if (isAdmin) { | |
| // Admin link doesn't need a permission check because isAdmin covers it, | |
| // but for type consistency we can add a dummy permission or just push to filteredNavItems | |
| // since filteredNavItems is what we iterate over. | |
| // However, navItems is defined as read-only implicitly by ts inference unless typed. | |
| // Let's just push to filteredNavItems. | |
| filteredNavItems.push({ name: 'Admin', path: '/admin', icon: 'admin_panel_settings', permission: 'admin' }); | |
| } | |
| return ( | |
| <div className="flex h-screen w-full overflow-hidden bg-background-light"> | |
| {/* Sidebar */} | |
| <aside className={`fixed inset-y-0 left-0 z-30 w-64 transform bg-white border-r border-border-light transition-transform duration-300 ease-in-out md:relative md:translate-x-0 ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}`}> | |
| <div className="flex h-full flex-col justify-between p-4"> | |
| <div className="flex flex-col gap-6"> | |
| {/* Logo */} | |
| <div className="flex items-center gap-3 px-2"> | |
| <div | |
| className="bg-gradient-to-br from-primary to-primary/70 rounded-xl size-10 shadow-lg shadow-primary/20 flex items-center justify-center" | |
| > | |
| <span className="material-symbols-outlined text-white">analytics</span> | |
| </div> | |
| <h1 className="text-text-main text-lg font-bold tracking-tight">Etsy Manager</h1> | |
| </div> | |
| {/* Shop Selector */} | |
| <div className="px-1"> | |
| <ShopSelector /> | |
| </div> | |
| {/* Navigation */} | |
| <nav className="flex flex-col gap-1"> | |
| {filteredNavItems.map((item) => { | |
| const isActive = location.pathname === item.path; | |
| return ( | |
| <NavLink | |
| key={item.path} | |
| to={item.path} | |
| onClick={() => setIsMobileMenuOpen(false)} | |
| className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors group ${isActive | |
| ? 'bg-primary/10 text-primary border border-primary/20' | |
| : 'text-text-secondary hover:text-primary hover:bg-slate-50' | |
| }`} | |
| > | |
| <span className={`material-symbols-outlined ${isActive ? 'filled' : ''}`}>{item.icon}</span> | |
| <span className={`text-sm ${isActive ? 'font-semibold' : 'font-medium'}`}>{item.name}</span> | |
| </NavLink> | |
| ); | |
| })} | |
| </nav> | |
| </div> | |
| {/* User Info */} | |
| <div className="flex items-center justify-between gap-2 px-3 py-3 mt-auto rounded-xl bg-slate-50 border border-border-light"> | |
| <div className="flex items-center gap-3 overflow-hidden"> | |
| <div className="size-8 rounded-full bg-gradient-to-br from-slate-400 to-slate-300 flex items-center justify-center flex-shrink-0"> | |
| <span className="material-symbols-outlined text-white text-sm">person</span> | |
| </div> | |
| <div className="flex flex-col overflow-hidden"> | |
| <span className="text-sm font-medium text-text-main truncate">{user?.username || 'User'}</span> | |
| <span className="text-xs text-text-secondary truncate capitalize">{user?.role || 'Guest'}</span> | |
| </div> | |
| </div> | |
| <button | |
| onClick={handleLogout} | |
| className="text-gray-400 hover:text-red-500 transition-colors p-1" | |
| title="Logout" | |
| > | |
| <span className="material-symbols-outlined text-xl">logout</span> | |
| </button> | |
| </div> | |
| </div> | |
| </aside> | |
| {/* Main Content */} | |
| <main className="flex-1 flex flex-col h-full overflow-hidden relative"> | |
| {/* Mobile Header */} | |
| <header className="md:hidden flex items-center justify-between p-4 border-b border-border-light bg-white"> | |
| <h1 className="text-text-main font-bold">Etsy Manager</h1> | |
| <button | |
| onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} | |
| className="text-text-secondary material-symbols-outlined" | |
| > | |
| menu | |
| </button> | |
| </header> | |
| {/* Content Scroll Area */} | |
| <div className="flex-1 overflow-y-auto p-4 md:p-8"> | |
| {children} | |
| </div> | |
| </main> | |
| {/* Mobile Overlay */} | |
| {isMobileMenuOpen && ( | |
| <div | |
| className="fixed inset-0 bg-black/50 z-20 md:hidden" | |
| onClick={() => setIsMobileMenuOpen(false)} | |
| ></div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default Layout; | |