feat: Implement Phase 1 & 2 - Full monorepo architecture
## Backend API (apps/api) - Express.js server with TypeScript - JWT authentication with access/refresh tokens - Multi-tenant middleware (schema per tenant) - Complete CRUD routes: auth, cfdis, transactions, contacts, categories, metrics, alerts - SAT integration: CFDI 4.0 XML parser, FIEL authentication - Metrics engine: 50+ financial metrics (Core, Startup, Enterprise) - Rate limiting, CORS, Helmet security ## Frontend Web (apps/web) - Next.js 14 with App Router - Authentication pages: login, register, forgot-password - Dashboard layout with Sidebar and Header - Dashboard pages: overview, cash-flow, revenue, expenses, metrics - Zustand stores for auth and UI state - Theme support with flash prevention ## Database Package (packages/database) - PostgreSQL migrations with multi-tenant architecture - Public schema: plans, tenants, users, sessions, subscriptions - Tenant schema: sat_credentials, cfdis, transactions, contacts, accounts, alerts - Tenant management functions - Seed data for plans and super admin ## Shared Package (packages/shared) - TypeScript types: auth, tenant, financial, metrics, reports - Zod validation schemas for all entities - Utility functions for formatting ## UI Package (packages/ui) - Chart components: LineChart, BarChart, AreaChart, PieChart - Data components: DataTable, MetricCard, KPICard, AlertBadge - PeriodSelector and Skeleton components ## Infrastructure - Docker Compose: PostgreSQL 15, Redis 7, MinIO, Mailhog - Makefile with 25+ development commands - Development scripts: dev-setup.sh, dev-down.sh - Complete .env.example template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
306
apps/web/src/components/layout/Sidebar.tsx
Normal file
306
apps/web/src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
LineChart,
|
||||
Wallet,
|
||||
History,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
TrendingUp,
|
||||
Bot,
|
||||
Shield,
|
||||
Bell,
|
||||
} from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Item de navegación
|
||||
*/
|
||||
interface NavItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
badge?: string | number;
|
||||
children?: NavItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Grupos de navegación
|
||||
*/
|
||||
interface NavGroup {
|
||||
label?: string;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Navegación principal
|
||||
*/
|
||||
const navigation: NavGroup[] = [
|
||||
{
|
||||
items: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/dashboard',
|
||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Trading',
|
||||
items: [
|
||||
{
|
||||
label: 'Estrategias',
|
||||
href: '/strategies',
|
||||
icon: <Bot className="h-5 w-5" />,
|
||||
badge: 3,
|
||||
},
|
||||
{
|
||||
label: 'Portfolio',
|
||||
href: '/portfolio',
|
||||
icon: <Wallet className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
label: 'Mercados',
|
||||
href: '/markets',
|
||||
icon: <TrendingUp className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
label: 'Historial',
|
||||
href: '/history',
|
||||
icon: <History className="h-5 w-5" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Analisis',
|
||||
items: [
|
||||
{
|
||||
label: 'Performance',
|
||||
href: '/analytics',
|
||||
icon: <LineChart className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
label: 'Riesgo',
|
||||
href: '/risk',
|
||||
icon: <Shield className="h-5 w-5" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Sistema',
|
||||
items: [
|
||||
{
|
||||
label: 'Notificaciones',
|
||||
href: '/notifications',
|
||||
icon: <Bell className="h-5 w-5" />,
|
||||
badge: 5,
|
||||
},
|
||||
{
|
||||
label: 'Configuracion',
|
||||
href: '/settings',
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
label: 'Ayuda',
|
||||
href: '/help',
|
||||
icon: <HelpCircle className="h-5 w-5" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Componente NavLink
|
||||
*/
|
||||
interface NavLinkProps {
|
||||
item: NavItem;
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
const NavLink: React.FC<NavLinkProps> = ({ item, collapsed }) => {
|
||||
const pathname = usePathname();
|
||||
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 py-2.5 rounded-lg',
|
||||
'text-sm font-medium transition-all duration-200',
|
||||
'group relative',
|
||||
isActive
|
||||
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-700/50 dark:hover:text-white',
|
||||
collapsed && 'justify-center px-2'
|
||||
)}
|
||||
>
|
||||
{/* Active Indicator */}
|
||||
{isActive && (
|
||||
<span className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary-600 dark:bg-primary-400 rounded-r-full" />
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<span
|
||||
className={cn(
|
||||
'flex-shrink-0 transition-colors',
|
||||
isActive
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-slate-400 group-hover:text-slate-600 dark:text-slate-500 dark:group-hover:text-slate-300'
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
|
||||
{/* Label */}
|
||||
{!collapsed && (
|
||||
<span className="flex-1 truncate">{item.label}</span>
|
||||
)}
|
||||
|
||||
{/* Badge */}
|
||||
{item.badge && !collapsed && (
|
||||
<span className="flex-shrink-0 px-2 py-0.5 text-xs font-semibold rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/50 dark:text-primary-300">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Tooltip when collapsed */}
|
||||
{collapsed && (
|
||||
<span className="absolute left-full ml-2 px-2 py-1 text-sm font-medium text-white bg-slate-900 dark:bg-slate-700 rounded opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50">
|
||||
{item.label}
|
||||
{item.badge && (
|
||||
<span className="ml-2 px-1.5 py-0.5 text-xs rounded-full bg-primary-500 text-white">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Componente Sidebar
|
||||
*
|
||||
* Barra lateral de navegación con soporte para colapsado y grupos de navegación.
|
||||
*/
|
||||
export const Sidebar: React.FC = () => {
|
||||
const { sidebarOpen, sidebarCollapsed, toggleSidebarCollapsed, isMobile, setSidebarOpen } = useUIStore();
|
||||
|
||||
// En mobile, cerrar al hacer click en overlay
|
||||
const handleOverlayClick = () => {
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay mobile */}
|
||||
{isMobile && sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||
onClick={handleOverlayClick}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed top-0 left-0 z-50 h-full',
|
||||
'bg-white dark:bg-slate-900',
|
||||
'border-r border-slate-200 dark:border-slate-700',
|
||||
'flex flex-col',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
// Width
|
||||
sidebarCollapsed ? 'w-20' : 'w-64',
|
||||
// Mobile
|
||||
isMobile && !sidebarOpen && '-translate-x-full',
|
||||
isMobile && sidebarOpen && 'translate-x-0',
|
||||
// Desktop
|
||||
!isMobile && 'translate-x-0'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={cn(
|
||||
'flex items-center h-16 px-4',
|
||||
'border-b border-slate-200 dark:border-slate-700',
|
||||
sidebarCollapsed && 'justify-center'
|
||||
)}>
|
||||
{/* Logo */}
|
||||
<Link href="/dashboard" className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-horux-gradient flex items-center justify-center">
|
||||
<span className="text-white font-bold text-xl">H</span>
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<span className="text-xl font-bold text-slate-900 dark:text-white">
|
||||
Horux
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-y-auto py-4 px-3 space-y-6">
|
||||
{navigation.map((group, idx) => (
|
||||
<div key={idx}>
|
||||
{/* Group Label */}
|
||||
{group.label && !sidebarCollapsed && (
|
||||
<h3 className="px-3 mb-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
|
||||
{group.label}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
{/* Separator when collapsed */}
|
||||
{group.label && sidebarCollapsed && (
|
||||
<div className="my-2 border-t border-slate-200 dark:border-slate-700" />
|
||||
)}
|
||||
|
||||
{/* Items */}
|
||||
<div className="space-y-1">
|
||||
{group.items.map((item) => (
|
||||
<NavLink key={item.href} item={item} collapsed={sidebarCollapsed} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Footer - Collapse Button */}
|
||||
{!isMobile && (
|
||||
<div className="p-3 border-t border-slate-200 dark:border-slate-700">
|
||||
<button
|
||||
onClick={toggleSidebarCollapsed}
|
||||
className={cn(
|
||||
'flex items-center gap-3 w-full px-3 py-2.5 rounded-lg',
|
||||
'text-sm font-medium text-slate-600 dark:text-slate-400',
|
||||
'hover:bg-slate-100 dark:hover:bg-slate-700/50',
|
||||
'transition-all duration-200',
|
||||
sidebarCollapsed && 'justify-center'
|
||||
)}
|
||||
aria-label={sidebarCollapsed ? 'Expandir sidebar' : 'Colapsar sidebar'}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
) : (
|
||||
<>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
<span>Colapsar</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
Reference in New Issue
Block a user