Se mejoro estetica y estandarizaron estilos
This commit is contained in:
34
src/components/ResponsiveSidebar.jsx
Normal file
34
src/components/ResponsiveSidebar.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react';
|
||||
import Sidebar, { MobileMenuButton } from './Sidebar';
|
||||
|
||||
export default function ResponsiveSidebar({ children }) {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const toggleMobileMenu = () => setIsMobileMenuOpen(prev => !prev);
|
||||
const closeMobileMenu = () => setIsMobileMenuOpen(false);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
isMobileOpen={isMobileMenuOpen}
|
||||
onMobileClose={closeMobileMenu}
|
||||
/>
|
||||
|
||||
{/* Contenido principal */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Header móvil - siempre visible en móviles */}
|
||||
<header className="lg:hidden bg-white shadow-sm border-b border-gray-200 px-4 py-3 flex items-center justify-between sticky top-0 z-20">
|
||||
<MobileMenuButton onClick={toggleMobileMenu} />
|
||||
<h1 className="text-lg font-semibold text-gray-900">EFC Dashboard</h1>
|
||||
<div className="w-8"></div> {/* Spacer para centrar el título */}
|
||||
</header>
|
||||
|
||||
{/* Contenido */}
|
||||
<main className="flex-1 overflow-y-auto p-4 lg:p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
src/components/SIDEBAR_README.md
Normal file
115
src/components/SIDEBAR_README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Sidebar Responsivo - Instrucciones de Uso
|
||||
|
||||
El sidebar ha sido actualizado para ser completamente responsivo. Aquí tienes las opciones de implementación:
|
||||
|
||||
## Opción 1: Usar ResponsiveSidebar (Recomendado para nuevos proyectos)
|
||||
|
||||
```jsx
|
||||
import ResponsiveSidebar from './components/ResponsiveSidebar';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ResponsiveSidebar>
|
||||
<h1>Contenido de tu aplicación</h1>
|
||||
<p>Todo tu contenido va aquí...</p>
|
||||
</ResponsiveSidebar>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Header fijo en móviles con botón de menú
|
||||
- Control completo del estado del sidebar
|
||||
- Diseño consistente en todas las pantallas
|
||||
|
||||
## Opción 2: Sidebar Standalone (Para proyectos existentes)
|
||||
|
||||
```jsx
|
||||
import Sidebar from './components/Sidebar';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<Sidebar /> {/* ¡Ahora funciona automáticamente en móviles! */}
|
||||
<main className="flex-1 p-4">
|
||||
Tu contenido aquí...
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Características:**
|
||||
- Botón flotante automático en móviles (solo cuando es necesario)
|
||||
- Funciona sin configuración adicional
|
||||
- Mantiene compatibilidad con código existente
|
||||
|
||||
## Funcionalidades Responsivas
|
||||
|
||||
### Desktop (≥1024px)
|
||||
- Sidebar fijo en el lado izquierdo
|
||||
- Botón de colapsar/expandir
|
||||
- Ancho: 256px (expandido) / 64px (colapsado)
|
||||
|
||||
### Móvil (<1024px)
|
||||
|
||||
#### Con ResponsiveSidebar:
|
||||
- Header fijo con botón de menú siempre visible
|
||||
- Sidebar se desliza desde la izquierda
|
||||
- Overlay oscuro de fondo
|
||||
|
||||
#### Con Sidebar standalone:
|
||||
- Botón flotante elegante en esquina superior izquierda (solo cuando está cerrado)
|
||||
- Sidebar se desliza desde la izquierda al hacer clic
|
||||
- Se oculta automáticamente al navegar
|
||||
|
||||
### Auto-cierre en móviles:
|
||||
- Al hacer clic en el overlay
|
||||
- Al navegar a otra página
|
||||
- Al redimensionar la ventana a desktop
|
||||
- Al hacer clic en el botón X
|
||||
|
||||
## Componentes Exportados
|
||||
|
||||
- `Sidebar`: Componente principal del sidebar
|
||||
- `MobileMenuButton`: Botón para abrir el menú móvil
|
||||
- `ResponsiveSidebar`: Wrapper completo con header móvil
|
||||
|
||||
## Props del Sidebar
|
||||
|
||||
```typescript
|
||||
interface SidebarProps {
|
||||
isMobileOpen?: boolean; // Estado del menú móvil (opcional)
|
||||
onMobileClose?: () => void; // Función para cerrar el menú móvil (opcional)
|
||||
}
|
||||
```
|
||||
|
||||
**Nota:** Si no pasas estas props, el Sidebar manejará su propio estado automáticamente.
|
||||
|
||||
## Migración de Código Existente
|
||||
|
||||
### Si ya usas `<Sidebar />`:
|
||||
✅ **No necesitas cambiar nada!** El sidebar ahora funciona automáticamente en móviles.
|
||||
|
||||
### Si quieres el header móvil:
|
||||
```jsx
|
||||
// Cambia esto:
|
||||
<div className="flex">
|
||||
<Sidebar />
|
||||
<main>Contenido</main>
|
||||
</div>
|
||||
|
||||
// Por esto:
|
||||
<ResponsiveSidebar>
|
||||
Contenido
|
||||
</ResponsiveSidebar>
|
||||
```
|
||||
|
||||
## Estilos y Diseño
|
||||
|
||||
- **Botón flotante**: Diseño sutil que coincide con el tema del sidebar
|
||||
- **Backdrop blur**: Efecto de cristal esmerilado en el botón
|
||||
- **Transiciones suaves**: Animaciones consistentes
|
||||
- **Z-index apropiado**: Sin conflictos con otros elementos
|
||||
|
||||
¡El sidebar es ahora completamente responsivo y funciona perfectamente en cualquier dispositivo! 📱💻
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useUser } from '../context/UserContext';
|
||||
|
||||
export default function Sidebar() {
|
||||
export default function Sidebar({ isMobileOpen, onMobileClose }) {
|
||||
// Leer si el usuario es importador desde localStorage
|
||||
const isImportador = typeof window !== 'undefined' && localStorage.getItem('user_is_importador') === 'true';
|
||||
// Leer grupos del usuario desde localStorage
|
||||
@@ -18,7 +18,15 @@ export default function Sidebar() {
|
||||
const isGroup35 = Array.isArray(userGroups) && userGroups.length === 2 && userGroups.includes(3) && userGroups.includes(5);
|
||||
// Leer DEBUG_MODE desde variables de entorno
|
||||
const isDebugMode = import.meta.env.VITE_DEBUG_MODE === 'true';
|
||||
|
||||
// Estados para responsividad
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [internalMobileOpen, setInternalMobileOpen] = useState(false);
|
||||
|
||||
// Usar estado interno si no se pasan props
|
||||
const mobileOpen = isMobileOpen !== undefined ? isMobileOpen : internalMobileOpen;
|
||||
const handleMobileClose = onMobileClose || (() => setInternalMobileOpen(false));
|
||||
const handleMobileOpen = () => setInternalMobileOpen(true);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { user: currentUser, loading } = useUser();
|
||||
@@ -30,6 +38,23 @@ export default function Sidebar() {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
// Cerrar menú móvil cuando se navega o cuando la pantalla es grande
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1024) { // lg breakpoint
|
||||
handleMobileClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
// Cerrar menú móvil cuando cambia la ubicación
|
||||
useEffect(() => {
|
||||
handleMobileClose();
|
||||
}, [location.pathname]);
|
||||
|
||||
// El usuario y loading ahora vienen del contexto global
|
||||
|
||||
// Definir todas las secciones
|
||||
@@ -196,35 +221,79 @@ export default function Sidebar() {
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<div className={`bg-slate-900 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} h-screen flex flex-col shadow-xl`}>
|
||||
{/* Header - Logo y colapsar */}
|
||||
<div className="p-4 border-b border-slate-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center">
|
||||
{/* Logo de la organización */}
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3 shadow-lg">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
<>
|
||||
{/* Botón flotante para abrir menú en móvil - solo cuando se usa standalone */}
|
||||
{!mobileOpen && isMobileOpen === undefined && (
|
||||
<button
|
||||
onClick={handleMobileOpen}
|
||||
className="lg:hidden fixed top-4 left-4 z-30 p-2.5 bg-slate-900/95 backdrop-blur-sm text-white rounded-xl shadow-lg hover:bg-slate-800/95 transition-all duration-200 border border-slate-700/50"
|
||||
aria-label="Abrir menú"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Overlay para móviles */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
||||
onClick={handleMobileClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className={`
|
||||
bg-slate-900 text-white transition-all duration-300 flex flex-col shadow-xl
|
||||
${isCollapsed ? 'w-16' : 'w-64'}
|
||||
fixed lg:relative inset-y-0 left-0 z-50
|
||||
${mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||
h-screen
|
||||
`}>
|
||||
{/* Header - Logo y colapsar */}
|
||||
<div className="p-4 border-b border-slate-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<div className="flex items-center">
|
||||
{/* Logo de la organización */}
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg flex items-center justify-center mr-3 shadow-lg">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-lg font-bold text-white">EFC Dashboard</h1>
|
||||
</div>
|
||||
<h1 className="text-lg font-bold text-white">EFC Dashboard</h1>
|
||||
)}
|
||||
|
||||
{/* Botones de control */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Botón cerrar en móvil */}
|
||||
<button
|
||||
onClick={handleMobileClose}
|
||||
className="lg:hidden p-1.5 rounded-lg hover:bg-slate-700 transition-all duration-200"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Botón colapsar en desktop */}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="hidden lg:block p-1.5 rounded-lg hover:bg-slate-700 transition-all duration-200 hover:shadow-md"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isCollapsed ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-1.5 rounded-lg hover:bg-slate-700 transition-all duration-200 hover:shadow-md"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isCollapsed ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-3 space-y-4 overflow-y-auto overflow-x-hidden">
|
||||
@@ -404,6 +473,32 @@ export default function Sidebar() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Hook personalizado para manejar el menú móvil desde otros componentes
|
||||
export function useMobileSidebar() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggle = () => setIsOpen(prev => !prev);
|
||||
const close = () => setIsOpen(false);
|
||||
|
||||
return { isOpen, toggle, close };
|
||||
}
|
||||
|
||||
// Componente botón para abrir menú móvil
|
||||
export function MobileMenuButton({ onClick }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="lg:hidden p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors duration-200"
|
||||
aria-label="Abrir menú"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user