Initial commit: Dockerized, Postgres, CI/CD pipeline
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 3m36s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 3m36s
This commit is contained in:
129
src/components/Layout.tsx
Normal file
129
src/components/Layout.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
||||
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, Loader2 } from 'lucide-react';
|
||||
import type { DateRange, OrderData } from '../types';
|
||||
import { fetchData } from '../dataService';
|
||||
|
||||
const Layout = () => {
|
||||
const location = useLocation();
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
||||
return localStorage.getItem('graph_sidebar_collapsed') === 'true';
|
||||
});
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() => {
|
||||
const saved = localStorage.getItem('nexstar_date_range');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
return { start: new Date(parsed.start), end: new Date(parsed.end) };
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
const end = new Date();
|
||||
const start = new Date();
|
||||
start.setMonth(start.getMonth() - 1);
|
||||
return { start, end };
|
||||
});
|
||||
|
||||
const [ordersData, setOrdersData] = useState<OrderData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadInitial = async () => {
|
||||
setIsLoading(true);
|
||||
const data = await fetchData();
|
||||
setOrdersData(data);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loadInitial();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('nexstar_date_range', JSON.stringify({
|
||||
start: dateRange.start.toISOString(),
|
||||
end: dateRange.end.toISOString()
|
||||
}));
|
||||
}, [dateRange]);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
const newState = !isSidebarCollapsed;
|
||||
setIsSidebarCollapsed(newState);
|
||||
localStorage.setItem('graph_sidebar_collapsed', String(newState));
|
||||
};
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/graph', icon: LayoutDashboard },
|
||||
{ name: 'Produtos', href: '/products', icon: Package },
|
||||
{ name: 'Clientes', href: '/clients', icon: Users },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-dark-bg text-dark-text overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<aside className={`bg-dark-sidebar border-r border-dark-border flex flex-col transition-all duration-300 ${isSidebarCollapsed ? 'w-20' : 'w-64'}`}>
|
||||
<div className={`h-20 px-6 border-b border-dark-border flex items-center ${isSidebarCollapsed ? 'justify-center' : 'justify-between'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`p-1.5 bg-brand-primary/20 rounded-lg text-brand-primary`}>
|
||||
<BarChart3 className="w-6 h-6" />
|
||||
</div>
|
||||
{!isSidebarCollapsed && <span className="text-xl font-bold text-dark-text">Nexstar</span>}
|
||||
</div>
|
||||
{!isSidebarCollapsed && (
|
||||
<button onClick={toggleSidebar} className="text-dark-muted hover:text-dark-text transition-colors cursor-pointer">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isSidebarCollapsed && (
|
||||
<div className="p-4 border-b border-dark-border flex justify-center">
|
||||
<button onClick={toggleSidebar} className="text-dark-muted hover:text-dark-text transition-colors cursor-pointer">
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2 overflow-y-auto">
|
||||
{navigation.map((item) => {
|
||||
const isActive = location.pathname === item.href || (item.href !== '/graph' && location.pathname.startsWith(item.href));
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.href}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-xl transition-all ${
|
||||
isActive
|
||||
? 'bg-brand-primary/10 text-brand-primary font-semibold shadow-md shadow-brand-primary/5'
|
||||
: 'text-dark-muted hover:bg-dark-card hover:text-dark-text'
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-5 h-5 shrink-0" />
|
||||
{!isSidebarCollapsed && <span className="font-medium">{item.name}</span>}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col h-screen overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="h-20 bg-dark-header border-b border-dark-border flex items-center px-8 shrink-0">
|
||||
<h2 className="text-xl font-bold text-dark-text">Painel de Análise</h2>
|
||||
</header>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-1 overflow-y-auto p-8 relative">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader2 className="w-8 h-8 text-brand-primary animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<Outlet context={{ dateRange, setDateRange, ordersData }} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
Reference in New Issue
Block a user