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:
123
src/App.tsx
123
src/App.tsx
@@ -22,11 +22,8 @@ import {
|
||||
CheckCircle2,
|
||||
Timer,
|
||||
Sparkles,
|
||||
LayoutDashboard,
|
||||
Activity,
|
||||
Target,
|
||||
Menu,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
|
||||
// Types
|
||||
@@ -422,7 +419,6 @@ function App() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
@@ -459,7 +455,7 @@ function App() {
|
||||
|
||||
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, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
@@ -481,7 +477,7 @@ function App() {
|
||||
// Error state (Vulnerabilidade 3.2)
|
||||
if (error) {
|
||||
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, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -510,93 +506,20 @@ function App() {
|
||||
if (!data) return null
|
||||
|
||||
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-[1800px] 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, rotate: 5 }}
|
||||
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">Dashboard Descomplicar</h1>
|
||||
<p className="text-xs text-zinc-500">Painel de Gestão</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-1 bg-white/5 rounded-xl p-1">
|
||||
<a href="#" className="px-4 py-2 rounded-lg bg-brand-500 text-white text-sm font-medium flex items-center gap-2">
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/monitor" className="px-4 py-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-2">
|
||||
<Activity className="w-4 h-4" />
|
||||
Monitor
|
||||
</a>
|
||||
<a href="/financial" className="px-4 py-2 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-2">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
Financeiro
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="md:hidden p-2.5 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
|
||||
>
|
||||
{mobileMenuOpen ? <X className="w-5 h-5 text-zinc-400" /> : <Menu className="w-5 h-5 text-zinc-400" />}
|
||||
</motion.button>
|
||||
<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 className="w-10 h-10 rounded-full bg-gradient-to-br from-brand-500 to-violet-600 ring-2 ring-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && (
|
||||
<motion.nav
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="md:hidden border-b border-white/5 bg-[#0a0a0f]/95 backdrop-blur-2xl overflow-hidden"
|
||||
<div className="max-w-[1800px] mx-auto px-6 lg:px-8 py-8">
|
||||
{/* Header com refresh */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div />
|
||||
<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"
|
||||
>
|
||||
<div className="px-6 py-3 flex flex-col gap-1">
|
||||
<a href="#" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 rounded-lg bg-brand-500 text-white text-sm font-medium flex items-center gap-3">
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/monitor" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-3">
|
||||
<Activity className="w-4 h-4" />
|
||||
Monitor
|
||||
</a>
|
||||
<a href="/financial" onClick={() => setMobileMenuOpen(false)} className="px-4 py-3 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10 text-sm font-medium transition-all flex items-center gap-3">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
Financeiro
|
||||
</a>
|
||||
</div>
|
||||
</motion.nav>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-[1800px] mx-auto px-6 lg:px-8 py-8">
|
||||
<RefreshCw className={`w-5 h-5 text-zinc-400 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
</motion.button>
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key="dashboard"
|
||||
@@ -880,24 +803,18 @@ function App() {
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-white/5 mt-12">
|
||||
<div className="max-w-[1800px] mx-auto px-6 lg:px-8 py-6">
|
||||
<div className="flex items-center justify-between text-sm text-zinc-500">
|
||||
{/* Footer */}
|
||||
<footer className="border-t border-white/5 mt-12">
|
||||
<div className="flex items-center justify-between text-sm text-zinc-500 py-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span>Dashboard Descomplicar v3.0</span>
|
||||
<span className="text-zinc-700">·</span>
|
||||
<span>Painel de Gestão</span>
|
||||
</div>
|
||||
<span>Actualizado: {new Date().toLocaleString('pt-PT')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
269
src/components/Layout.tsx
Normal file
269
src/components/Layout.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { NavLink, Outlet } from 'react-router-dom'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Activity,
|
||||
CreditCard,
|
||||
Zap,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Menu,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
|
||||
const SIDEBAR_KEY = 'dash-sidebar-collapsed'
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
interface NavItem {
|
||||
to: string
|
||||
label: string
|
||||
icon: React.ElementType
|
||||
}
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ to: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ to: '/monitor', label: 'Monitor', icon: Activity },
|
||||
{ to: '/financial', label: 'Financeiro', icon: CreditCard },
|
||||
]
|
||||
|
||||
function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = useState(
|
||||
typeof window !== 'undefined' ? window.innerWidth < MOBILE_BREAKPOINT : false
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
window.addEventListener('resize', onResize)
|
||||
return () => window.removeEventListener('resize', onResize)
|
||||
}, [])
|
||||
|
||||
return isMobile
|
||||
}
|
||||
|
||||
function getInitialCollapsed(isMobile: boolean): boolean {
|
||||
if (isMobile) return true
|
||||
try {
|
||||
const stored = localStorage.getItem(SIDEBAR_KEY)
|
||||
if (stored !== null) return stored === 'true'
|
||||
} catch {
|
||||
// localStorage indisponivel
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
const isMobile = useIsMobile()
|
||||
const [collapsed, setCollapsed] = useState(() => getInitialCollapsed(isMobile))
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
|
||||
// Colapsar automaticamente em mobile
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setCollapsed(true)
|
||||
setMobileOpen(false)
|
||||
}
|
||||
}, [isMobile])
|
||||
|
||||
// Persistir estado em localStorage (apenas desktop)
|
||||
useEffect(() => {
|
||||
if (!isMobile) {
|
||||
try {
|
||||
localStorage.setItem(SIDEBAR_KEY, String(collapsed))
|
||||
} catch {
|
||||
// localStorage indisponivel
|
||||
}
|
||||
}
|
||||
}, [collapsed, isMobile])
|
||||
|
||||
const toggleSidebar = () => {
|
||||
if (isMobile) {
|
||||
setMobileOpen(prev => !prev)
|
||||
} else {
|
||||
setCollapsed(prev => !prev)
|
||||
}
|
||||
}
|
||||
|
||||
const closeMobileMenu = () => setMobileOpen(false)
|
||||
|
||||
const sidebarWidth = collapsed ? 72 : 240
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-mesh">
|
||||
<div className="bg-grid min-h-screen flex">
|
||||
|
||||
{/* Overlay mobile */}
|
||||
<AnimatePresence>
|
||||
{isMobile && mobileOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
||||
onClick={closeMobileMenu}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Sidebar */}
|
||||
<AnimatePresence mode="wait">
|
||||
{(!isMobile || mobileOpen) && (
|
||||
<motion.aside
|
||||
initial={isMobile ? { x: -240 } : false}
|
||||
animate={{ x: 0 }}
|
||||
exit={isMobile ? { x: -240 } : undefined}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 35 }}
|
||||
style={!isMobile ? { width: sidebarWidth } : undefined}
|
||||
className={`
|
||||
${isMobile
|
||||
? 'fixed top-0 left-0 bottom-0 z-50 w-60'
|
||||
: 'relative flex-shrink-0 z-30'
|
||||
}
|
||||
bg-[#0a0a0f]/95 backdrop-blur-2xl border-r border-white/5
|
||||
flex flex-col transition-[width] duration-300 ease-in-out
|
||||
`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-4 py-5 border-b border-white/5 min-h-[72px]">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05, rotate: 5 }}
|
||||
className="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/30 flex-shrink-0"
|
||||
>
|
||||
<Zap className="w-5 h-5 text-white" />
|
||||
</motion.div>
|
||||
<AnimatePresence>
|
||||
{(!collapsed || isMobile) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
animate={{ opacity: 1, width: 'auto' }}
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
<h1 className="text-base font-bold text-white tracking-tight leading-tight">Descomplicar</h1>
|
||||
<p className="text-[10px] text-zinc-500">Painel de Gestao</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Fechar em mobile */}
|
||||
{isMobile && (
|
||||
<button
|
||||
onClick={closeMobileMenu}
|
||||
className="ml-auto p-1.5 rounded-lg hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-zinc-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navegacao */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-1">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.to === '/'}
|
||||
onClick={isMobile ? closeMobileMenu : undefined}
|
||||
className={({ isActive }) => `
|
||||
flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium
|
||||
transition-all duration-200 group relative
|
||||
${isActive
|
||||
? 'bg-brand-500/15 text-white border border-brand-500/30'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/5 border border-transparent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<item.icon className={`w-5 h-5 flex-shrink-0 ${isActive ? 'text-brand-400' : 'text-zinc-500 group-hover:text-zinc-300'}`} />
|
||||
<AnimatePresence>
|
||||
{(!collapsed || isMobile) && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
animate={{ opacity: 1, width: 'auto' }}
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
{item.label}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Tooltip quando colapsado (desktop) */}
|
||||
{collapsed && !isMobile && (
|
||||
<div className="absolute left-full ml-3 px-2.5 py-1.5 rounded-lg bg-zinc-800 text-white text-xs font-medium whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none shadow-xl border border-white/10 z-50">
|
||||
{item.label}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Toggle (apenas desktop) */}
|
||||
{!isMobile && (
|
||||
<div className="px-3 py-4 border-t border-white/5">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all text-zinc-400 hover:text-white"
|
||||
title={collapsed ? 'Expandir sidebar' : 'Colapsar sidebar'}
|
||||
>
|
||||
{collapsed
|
||||
? <ChevronRight className="w-4 h-4" />
|
||||
: <ChevronLeft className="w-4 h-4" />
|
||||
}
|
||||
<AnimatePresence>
|
||||
{!collapsed && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, width: 0 }}
|
||||
animate={{ opacity: 1, width: 'auto' }}
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="text-xs font-medium overflow-hidden whitespace-nowrap"
|
||||
>
|
||||
Colapsar
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Conteudo principal */}
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
{/* Header mobile com botao de menu */}
|
||||
{isMobile && (
|
||||
<header className="sticky top-0 z-30 border-b border-white/5 bg-[#0a0a0f]/90 backdrop-blur-2xl">
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="p-2 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 transition-all"
|
||||
>
|
||||
<Menu className="w-5 h-5 text-zinc-400" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-violet-600 flex items-center justify-center shadow-lg shadow-brand-500/20">
|
||||
<Zap className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="text-sm font-bold text-white">Descomplicar</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Outlet para as paginas */}
|
||||
<main className="flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
src/main.tsx
11
src/main.tsx
@@ -6,6 +6,7 @@ import './index.css'
|
||||
import App from './App.tsx'
|
||||
import Monitor from './pages/Monitor.tsx'
|
||||
import Financial from './pages/Financial.tsx'
|
||||
import Layout from './components/Layout.tsx'
|
||||
import { oidcConfig } from './auth/config.ts'
|
||||
import { AuthWrapper } from './auth/AuthWrapper.tsx'
|
||||
|
||||
@@ -15,10 +16,12 @@ createRoot(document.getElementById('root')!).render(
|
||||
<BrowserRouter>
|
||||
<AuthWrapper>
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/monitor" element={<Monitor />} />
|
||||
<Route path="/financial" element={<Financial />} />
|
||||
<Route path="/callback" element={<App />} />
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/monitor" element={<Monitor />} />
|
||||
<Route path="/financial" element={<Financial />} />
|
||||
<Route path="/callback" element={<App />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</AuthWrapper>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user