feat: implementar sidebar colapsavel profissional

Substitui navegacao por header/menu mobile por sidebar lateral colapsavel
com toggle, persistencia localStorage e responsividade automatica.

- Novo componente Layout.tsx com sidebar, tooltips e overlay mobile
- Estado colapsado persistido em localStorage (desktop)
- Colapsada por defeito em mobile com drawer animado
- Animacoes suaves via framer-motion (spring)
- Removida navegacao duplicada de App.tsx, Monitor.tsx e Financial.tsx
- Rotas envolvidas pelo Layout via React Router Outlet

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 14:58:06 +00:00
parent 8148eb47fe
commit a4271fd06a
5 changed files with 340 additions and 226 deletions
+22 -60
View File
@@ -1,20 +1,15 @@
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import { Link } from 'react-router-dom'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
} from 'recharts'
import {
Zap,
RefreshCw,
ArrowLeft,
TrendingUp,
TrendingDown,
DollarSign,
Receipt,
PiggyBank,
Activity,
LayoutDashboard,
} from 'lucide-react'
interface FinancialData {
@@ -105,7 +100,7 @@ export default function Financial() {
if (loading) {
return (
<div className="min-h-screen bg-mesh flex items-center justify-center">
<div className="flex items-center justify-center py-20">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<DollarSign className="w-12 h-12 text-brand-400 mx-auto mb-4 animate-pulse" />
<p className="text-zinc-400">A carregar dados financeiros...</p>
@@ -127,59 +122,28 @@ export default function Financial() {
}))
return (
<div className="min-h-screen bg-mesh">
<div className="bg-grid min-h-screen">
{/* Header */}
<header className="sticky top-0 z-50 border-b border-white/5 bg-[#0a0a0f]/90 backdrop-blur-2xl">
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<motion.div
whileHover={{ scale: 1.05 }}
className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/30"
>
<Zap className="w-6 h-6 text-white" />
</motion.div>
<div>
<h1 className="text-xl font-bold text-white tracking-tight">Financeiro</h1>
<p className="text-xs text-zinc-500">Vendas e Despesas {new Date().getFullYear()}</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${lucroColor}`}>
{lucroLabel}: {formatEUR(Math.abs(data.lucro_ano))}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
<Link
to="/monitor"
className="hidden md:flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<Activity className="w-4 h-4" />
Monitor
</Link>
<Link
to="/"
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<ArrowLeft className="w-4 h-4" />
<span className="hidden md:inline">Dashboard</span>
</Link>
</div>
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
{/* Header com resumo e refresh */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-white tracking-tight">Financeiro</h2>
<p className="text-xs text-zinc-500">Vendas e Despesas {new Date().getFullYear()}</p>
</div>
<div className="flex items-center gap-3">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${lucroColor}`}>
{lucroLabel}: {formatEUR(Math.abs(data.lucro_ano))}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
<motion.div variants={containerVariants} initial="hidden" animate="show">
{/* Summary Cards */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5 mb-8">
@@ -280,8 +244,6 @@ export default function Financial() {
</motion.div>
</div>
</motion.div>
</main>
</div>
</div>
</div>
)
}
+22 -59
View File
@@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { motion } from 'framer-motion'
import { Link } from 'react-router-dom'
import {
Server,
Wifi,
@@ -11,11 +10,8 @@ import {
RefreshCw,
CheckCircle2,
AlertTriangle,
Zap,
ArrowLeft,
Activity,
Clock,
TrendingUp,
Wrench,
Code,
Network,
@@ -363,7 +359,7 @@ export default function Monitor() {
if (loading) {
return (
<div className="min-h-screen bg-mesh flex items-center justify-center">
<div className="flex items-center justify-center py-20">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-center">
<Activity className="w-12 h-12 text-brand-400 mx-auto mb-4 animate-pulse" />
<p className="text-zinc-400">A carregar monitorizacao...</p>
@@ -392,59 +388,28 @@ export default function Monitor() {
}[data.overall]
return (
<div className="min-h-screen bg-mesh">
<div className="bg-grid min-h-screen">
{/* Header */}
<header className="sticky top-0 z-50 border-b border-white/5 bg-[#0a0a0f]/90 backdrop-blur-2xl">
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<motion.div
whileHover={{ scale: 1.05 }}
className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/30"
>
<Zap className="w-6 h-6 text-white" />
</motion.div>
<div>
<h1 className="text-xl font-bold text-white tracking-tight">Monitorizacao</h1>
<p className="text-xs text-zinc-500">Cluster Proxmox Descomplicar</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${overallColor}`}>
{overallLabel}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
<Link
to="/financial"
className="hidden md:flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<TrendingUp className="w-4 h-4" />
Financeiro
</Link>
<Link
to="/"
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-400 hover:text-white transition-all text-sm"
>
<ArrowLeft className="w-4 h-4" />
Dashboard
</Link>
</div>
<div className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
{/* Header com status e refresh */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-white tracking-tight">Monitorizacao</h2>
<p className="text-xs text-zinc-500">Cluster Proxmox Descomplicar</p>
</div>
<div className="flex items-center gap-3">
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${overallColor}`}>
{overallLabel}
</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={fetchData}
disabled={refreshing}
className="p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
>
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
</motion.button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-[1600px] mx-auto px-6 lg:px-8 py-8">
<motion.div variants={containerVariants} initial="hidden" animate="show">
{/* Section 1: Cluster Overview */}
@@ -661,9 +626,7 @@ export default function Monitor() {
<span>Auto-refresh: 60s</span>
</div>
</motion.div>
</main>
</div>
</div>
</div>
)
}