chore: initial setup with PostgreSQL, DragonflyDB, and Nginx routing configs
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
MessageSquare,
|
||||
Flame,
|
||||
Users,
|
||||
Network,
|
||||
Send,
|
||||
Key,
|
||||
Settings,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
Zap,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Layers,
|
||||
Plug2,
|
||||
ArrowLeftFromLine,
|
||||
BrainCircuit,
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useUIStore } from '../store/uiStore';
|
||||
|
||||
// Simple Tooltip component specifically for the collapsed sidebar
|
||||
const SidebarTooltip = ({ text, children }: { text: string, children: React.ReactNode }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
return (
|
||||
<div className="relative flex items-center" onMouseEnter={() => setIsVisible(true)} onMouseLeave={() => setIsVisible(false)}>
|
||||
{children}
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -10 }}
|
||||
className="absolute left-full ml-4 px-3 py-1.5 bg-brand-500 text-white text-xs font-semibold rounded-lg shadow-xl z-[100] whitespace-nowrap pointer-events-none"
|
||||
>
|
||||
{text}
|
||||
<div className="absolute top-1/2 -left-1 -translate-y-1/2 border-y-4 border-y-transparent border-r-4 border-r-brand-500" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Sidebar() {
|
||||
const router = useRouter();
|
||||
const { user, logout } = useAuth();
|
||||
const { isSidebarCollapsed, toggleSidebar } = useUIStore();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Wait for mount to avoid hydration mismatch
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const menuItems = [
|
||||
{ path: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
{ path: '/inbox', label: 'Inbox', icon: MessageSquare },
|
||||
{ path: '/sessions', label: 'Sessões', icon: Users },
|
||||
{ path: '/warmer', label: 'WhatsApp Warmer', icon: Flame },
|
||||
{ path: '/phonebook', label: 'Agenda', icon: Users },
|
||||
{ path: '/flow-builder', label: 'Fluxos', icon: Network },
|
||||
{ path: '/sending', label: 'Disparos', icon: Send },
|
||||
{ path: '/sectors', label: 'Setores & Equipes', icon: Layers },
|
||||
{ path: '/api-access', label: 'Acesso à API', icon: Key },
|
||||
{ path: '/secretaria', label: 'Secretária IA', icon: BrainCircuit },
|
||||
];
|
||||
|
||||
const adminItems = [
|
||||
{ path: '/admin/dashboard', label: 'Admin Dashboard', icon: LayoutDashboard },
|
||||
{ path: '/admin/plans', label: 'Plan Management', icon: Sparkles },
|
||||
{ path: '/admin/users', label: 'Users', icon: Users },
|
||||
{ path: '/admin/plugins', label: 'Plugins', icon: Plug2 },
|
||||
{ path: '/admin/settings', label: 'Global Settings', icon: Settings },
|
||||
{ path: '/baileys', label: 'Baileys', icon: Zap },
|
||||
];
|
||||
|
||||
const navItems = user?.role === 'admin' && router.pathname.startsWith('/admin')
|
||||
? adminItems
|
||||
: menuItems;
|
||||
|
||||
if (!mounted) return <aside className="w-64 bg-black" />;
|
||||
|
||||
return (
|
||||
<motion.aside
|
||||
animate={{ width: isSidebarCollapsed ? 80 : 256 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
className={`bg-black border-r border-white/5 h-screen flex-col font-sans shrink-0 z-50 overflow-y-auto relative hidden min-[401px]:flex max-[400px]:hidden transition-all`}
|
||||
>
|
||||
{/* Ambient */}
|
||||
<div className="absolute top-0 left-0 w-full h-[500px] bg-brand-500/5 blur-[100px] pointer-events-none" />
|
||||
|
||||
{/* Application Logo */}
|
||||
<div className={`h-16 flex items-center px-6 border-b border-white/5 relative z-10 shrink-0 ${isSidebarCollapsed ? 'justify-center px-0' : ''}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded bg-gradient-to-br from-brand-500 to-brand-700 flex items-center justify-center shadow-lg shadow-brand-500/20 shrink-0">
|
||||
<Sparkles className="text-white w-5 h-5 transition-transform duration-300 group-hover:rotate-12" />
|
||||
</div>
|
||||
{!isSidebarCollapsed && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="text-xl font-black text-white tracking-tight"
|
||||
>
|
||||
new<span className="text-brand-400">whats</span>
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav Menu */}
|
||||
<nav className="flex-1 px-4 py-6 space-y-2 relative z-10 overflow-y-auto custom-scrollbar">
|
||||
{navItems.map((item) => {
|
||||
const isActive = router.pathname.startsWith(item.path);
|
||||
const Icon = item.icon;
|
||||
|
||||
const content = (
|
||||
<Link key={item.path} href={item.path}>
|
||||
<div className={`relative flex items-center gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-all group overflow-hidden
|
||||
${isActive ? 'text-white font-semibold' : 'text-slate-400 hover:text-white hover:bg-white/[0.05]'}
|
||||
${isSidebarCollapsed ? 'justify-center p-2.5 mx-auto' : ''}`}
|
||||
>
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="sidebar-active"
|
||||
className="absolute inset-0 bg-brand-500/10 border border-brand-500/20 rounded-xl"
|
||||
initial={false}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-brand-500 rounded-r-full shadow-[0_0_10px_#3b82f6]" />
|
||||
)}
|
||||
|
||||
<Icon size={20} className={`relative z-10 shrink-0 ${isActive ? 'text-brand-400' : 'group-hover:scale-110 transition-transform'}`} />
|
||||
|
||||
{!isSidebarCollapsed && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="relative z-10 text-sm whitespace-nowrap"
|
||||
>
|
||||
{item.label}
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
return isSidebarCollapsed ? (
|
||||
<SidebarTooltip key={item.path} text={item.label}>
|
||||
{content}
|
||||
</SidebarTooltip>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Bottom Actions & Toggle */}
|
||||
<div className={`p-4 border-t border-white/5 relative z-10 shrink-0 ${isSidebarCollapsed ? 'items-center flex flex-col' : ''}`}>
|
||||
|
||||
{user?.role === 'admin' && router.pathname.startsWith('/admin') && (
|
||||
<Link href="/dashboard">
|
||||
<div className={`flex items-center gap-3 px-3 py-2 mb-1 text-sm rounded-xl cursor-pointer transition-all font-semibold group
|
||||
bg-white/[0.04] border border-white/8 hover:bg-brand-500/10 hover:border-brand-500/30 text-slate-400 hover:text-brand-300
|
||||
${isSidebarCollapsed ? 'justify-center w-full' : ''}`}>
|
||||
<ArrowLeftFromLine size={17} className="shrink-0 transition-transform group-hover:-translate-x-0.5" />
|
||||
{!isSidebarCollapsed && <span>Sair do Admin</span>}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{user?.role === 'admin' && !router.pathname.startsWith('/admin') && (
|
||||
<Link href="/admin/dashboard">
|
||||
<div className={`flex items-center gap-3 px-3 py-2 text-sm text-yellow-400 hover:bg-white/5 rounded-xl cursor-pointer transition-colors font-semibold ${isSidebarCollapsed ? 'justify-center w-full' : ''}`}>
|
||||
<Settings size={18} className="shrink-0" />
|
||||
{!isSidebarCollapsed && <span>Admin Panel</span>}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={logout}
|
||||
className={`flex items-center gap-3 px-3 py-2 text-sm text-slate-400 hover:text-red-400 hover:bg-white/5 rounded-xl cursor-pointer transition-colors mb-2 ${isSidebarCollapsed ? 'justify-center w-full' : ''}`}
|
||||
>
|
||||
<LogOut size={18} className="shrink-0" />
|
||||
{!isSidebarCollapsed && <span>Log Out</span>}
|
||||
</div>
|
||||
|
||||
{/* Intelligent Toggle Button */}
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className={`w-full flex items-center bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white rounded-xl py-2 transition-all group ${isSidebarCollapsed ? 'justify-center px-0' : 'px-3 justify-between'}`}
|
||||
title={isSidebarCollapsed ? 'Expandir' : 'Recolher'}
|
||||
>
|
||||
{!isSidebarCollapsed && <span className="text-xs font-bold uppercase tracking-widest opacity-40">Layout</span>}
|
||||
{isSidebarCollapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</motion.aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user