etsy-app / frontend /components /Layout.tsx
lethientien's picture
Upload 28 files
e4dfa4d verified
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;