From 01178e95325d482b9f595c840149df9f7e0e423f Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Wed, 5 Nov 2025 16:14:39 -0700 Subject: [PATCH] feat: Implement Dropdown component with styles and functionality - Added Dropdown component with trigger and items - Created Dropdown.module.css for styling - Implemented click outside to close functionality feat: Create Header component with styles - Added Header.module.css for header styling - Included action buttons and user menu styles feat: Develop NewTransmissionModal component with styles - Created modal overlay and content styles in NewTransmissionModal.module.css - Added responsive design for mobile view feat: Build PageContainer and Sidebar components with styles - Implemented PageContainer.module.css for layout - Created Sidebar.module.css for sidebar navigation feat: Add Skeleton loading components with styles - Developed Skeleton and SkeletonCard components - Created Skeleton.module.css for loading placeholders feat: Implement ThemeProvider for theme management - Added ThemeProvider component for light/dark mode - Integrated local storage for theme persistence feat: Create Tooltip component with styles - Developed Tooltip component for displaying hints - Added Tooltip.module.css for tooltip styling feat: Build TransmissionsTable component with styles - Created TransmissionsTable.module.css for table layout - Implemented responsive design for table chore: Add Vite environment type declarations - Included vite-env.d.ts for CSS module support --- package-lock.json | 11 + package.json | 3 + packages/broadcast-panel/README.md | 147 ++++++++ packages/broadcast-panel/README_UPDATED.md | 343 ++++++++++++++++++ .../public/assets/logo-dark.png | Bin 0 -> 3914 bytes .../public/assets/logo-icon-32.png | Bin 0 -> 2358 bytes .../public/assets/logo-icon-64.png | Bin 0 -> 4982 bytes .../public/assets/logo-icon.png | Bin 0 -> 14416 bytes .../public/assets/logo-light.png | Bin 0 -> 3011 bytes packages/broadcast-panel/public/favicon.ico | Bin 0 -> 15406 bytes .../src/components/Dropdown.module.css | 60 +++ .../src/components/Dropdown.tsx | 63 ++++ .../src/components/Header.module.css | 144 ++++++++ .../broadcast-panel/src/components/Header.tsx | 88 ++++- .../NewTransmissionModal.module.css | 162 +++++++++ .../src/components/NewTransmissionModal.tsx | 75 ++-- .../src/components/PageContainer.module.css | 67 ++++ .../src/components/PageContainer.tsx | 114 ++++-- .../src/components/Sidebar.module.css | 178 +++++++++ .../src/components/Sidebar.tsx | 87 +++-- .../src/components/Skeleton.module.css | 45 +++ .../src/components/Skeleton.tsx | 47 +++ .../src/components/ThemeProvider.tsx | 62 ++++ .../src/components/Tooltip.module.css | 92 +++++ .../src/components/Tooltip.tsx | 28 ++ .../components/TransmissionsTable.module.css | 177 +++++++++ .../src/components/TransmissionsTable.tsx | 139 +++++-- packages/broadcast-panel/src/styles.css | 98 ++++- packages/broadcast-panel/src/vite-env.d.ts | 4 + 29 files changed, 2096 insertions(+), 138 deletions(-) create mode 100644 packages/broadcast-panel/README.md create mode 100644 packages/broadcast-panel/README_UPDATED.md create mode 100644 packages/broadcast-panel/public/assets/logo-dark.png create mode 100644 packages/broadcast-panel/public/assets/logo-icon-32.png create mode 100644 packages/broadcast-panel/public/assets/logo-icon-64.png create mode 100644 packages/broadcast-panel/public/assets/logo-icon.png create mode 100644 packages/broadcast-panel/public/assets/logo-light.png create mode 100644 packages/broadcast-panel/public/favicon.ico create mode 100644 packages/broadcast-panel/src/components/Dropdown.module.css create mode 100644 packages/broadcast-panel/src/components/Dropdown.tsx create mode 100644 packages/broadcast-panel/src/components/Header.module.css create mode 100644 packages/broadcast-panel/src/components/NewTransmissionModal.module.css create mode 100644 packages/broadcast-panel/src/components/PageContainer.module.css create mode 100644 packages/broadcast-panel/src/components/Sidebar.module.css create mode 100644 packages/broadcast-panel/src/components/Skeleton.module.css create mode 100644 packages/broadcast-panel/src/components/Skeleton.tsx create mode 100644 packages/broadcast-panel/src/components/ThemeProvider.tsx create mode 100644 packages/broadcast-panel/src/components/Tooltip.module.css create mode 100644 packages/broadcast-panel/src/components/Tooltip.tsx create mode 100644 packages/broadcast-panel/src/components/TransmissionsTable.module.css create mode 100644 packages/broadcast-panel/src/vite-env.d.ts diff --git a/package-lock.json b/package-lock.json index 31bcc2e..33b3927 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "packages/*", "shared/*" ], + "dependencies": { + "react-icons": "^5.5.0" + }, "devDependencies": { "concurrently": "^8.2.2", "typescript": "^5.2.2" @@ -8400,6 +8403,14 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index 02c9b3c..10f39d5 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,8 @@ "engines": { "node": ">=20.0.0", "npm": ">=10.0.0" + }, + "dependencies": { + "react-icons": "^5.5.0" } } diff --git a/packages/broadcast-panel/README.md b/packages/broadcast-panel/README.md new file mode 100644 index 0000000..42ee24f --- /dev/null +++ b/packages/broadcast-panel/README.md @@ -0,0 +1,147 @@ +# Broadcast Panel - AvanzaCast + +Panel de control para gestión de transmisiones en vivo, inspirado en el diseño limpio y moderno de StreamYard. + +## 🎨 Diseño y Estilos + +Este proyecto utiliza **CSS Modules** para un diseño modular y mantenible, siguiendo las mejores prácticas de arquitectura de componentes React. + +### Paleta de Colores + +```css +--primary-blue: #1a73e8 /* Azul principal (acciones, enlaces activos) */ +--primary-blue-hover: #1557b0 /* Azul hover */ +--background-color: #f7f8fa /* Fondo de página */ +--surface-color: #ffffff /* Fondo de componentes/tarjetas */ +--text-primary: #212121 /* Texto principal */ +--text-secondary: #5f6368 /* Texto secundario */ +--border-light: #e8eaed /* Bordes sutiles */ +--active-bg-light: #e8f0fe /* Fondo de elementos activos */ +``` + +### Tipografía + +- **Familia:** Inter, sistema UI sans-serif +- **Títulos (H2):** 22px, font-weight 600 +- **Subtítulos:** 16px, font-weight 500 +- **Texto principal:** 14px, font-weight 400 +- **Texto pequeño:** 12px + +## 📁 Assets Utilizados + +### Imágenes del Dashboard Techwind + +Los siguientes assets fueron copiados desde `/home/xesar/Descargas/techwind_v2.2.0/HTML/Dashboard/src/assets/images/`: + +``` +public/assets/ +├── logo-dark.png # Logo oscuro para header +├── logo-light.png # Logo claro para sidebar +├── logo-icon.png # Icono del logo (64x64) +├── logo-icon-32.png # Icono pequeño (32x32) +└── logo-icon-64.png # Icono mediano (64x64) + +public/ +└── favicon.ico # Favicon del sitio +``` + +### Licencia de Assets + +Los assets provienen de la plantilla **Techwind v2.2.0** (Dashboard template). +**Uso:** Estos assets están incluidos para propósitos de desarrollo y demostración. +**Nota:** Reemplazar con assets propios antes de producción si se requiere licencia comercial. + +## 🏗️ Estructura de Componentes + +``` +src/components/ +├── PageContainer.tsx # Contenedor principal con layout +├── PageContainer.module.css # Estilos del contenedor +├── Sidebar.tsx # Barra lateral de navegación +├── Sidebar.module.css # Estilos del sidebar +├── Header.tsx # Barra superior +├── Header.module.css # Estilos del header +├── TransmissionsTable.tsx # Tabla de transmisiones +├── TransmissionsTable.module.css +├── NewTransmissionModal.tsx # Modal de creación +└── NewTransmissionModal.module.css +``` + +## 🚀 Desarrollo + +### Iniciar el servidor de desarrollo + +Desde la raíz del monorepo: + +```bash +npm run dev:broadcast-panel +``` + +O directamente en el paquete: + +```bash +cd packages/broadcast-panel +npm run dev +``` + +El servidor estará disponible en: **http://localhost:5173/** + +### Build de producción + +```bash +npm run build +``` + +## 📱 Responsive Design + +El diseño es completamente responsivo con breakpoints: + +- **Desktop:** > 1024px (sidebar fijo, header completo) +- **Tablet:** 768px - 1024px (sidebar colapsable) +- **Mobile:** < 768px (sidebar con toggle, header compacto) + +## ✨ Características UI + +### Sidebar +- Navegación con indicadores visuales de página activa +- Sección de almacenamiento con barra de progreso +- Diseño fijo con scroll interno +- Iconos emoji temporales (reemplazar con SVG icons) + +### Header +- Toggle de tema claro/oscuro +- Notificaciones con badge +- Menú de usuario +- Botón de "Mejora tu plan" + +### Transmissions Table +- Tabs para filtrar (Próximamente / Anteriores) +- Estados hover en filas +- Botones de acción (Entrar al estudio, Más opciones) +- Estado vacío con mensaje descriptivo + +### Modal +- Overlay con backdrop blur +- Animaciones de entrada (fadeIn + slideUp) +- Formulario con validación +- Cierre con Escape o clic fuera + +## 🔧 Próximas Mejoras + +- [ ] Reemplazar emojis con iconos SVG (React Icons) +- [ ] Implementar tema oscuro completo +- [ ] Añadir animaciones de transición entre páginas +- [ ] Implementar filtrado real de tabs (Próximamente/Anteriores) +- [ ] Añadir estados de carga (loading skeletons) +- [ ] Implementar dropdown del menú de usuario +- [ ] Añadir tooltips informativos + +## 📝 Notas de Implementación + +Este panel sigue el patrón de diseño de **StreamYard**, caracterizado por: + +1. **Simplicidad:** UI limpia sin elementos innecesarios +2. **Claridad:** Jerarquía visual clara con espaciado generoso +3. **Consistencia:** Uso uniforme de colores, tipografía y espaciado +4. **Accesibilidad:** Contraste adecuado, tamaños de fuente legibles +5. **Responsividad:** Adaptación fluida a diferentes dispositivos diff --git a/packages/broadcast-panel/README_UPDATED.md b/packages/broadcast-panel/README_UPDATED.md new file mode 100644 index 0000000..d43608a --- /dev/null +++ b/packages/broadcast-panel/README_UPDATED.md @@ -0,0 +1,343 @@ +# Broadcast Panel - AvanzaCast 🎥 + +Panel de administración de transmisiones en vivo con diseño moderno y profesional basado en la plantilla Techwind Dashboard. + +## ✨ Características Implementadas + +### 🎨 UI/UX Moderno +- ✅ Diseño limpio y profesional basado en Techwind v2.2.0 +- ✅ **React Icons** (Material Design Icons) para iconografía consistente +- ✅ CSS Modules con variables CSS para tematización +- ✅ Diseño responsive con breakpoints móvil/tablet/desktop +- ✅ Animaciones fluidas y transiciones suaves + +### 🌓 Tema Oscuro Funcional +- ✅ **ThemeProvider** con Context API de React +- ✅ Persistencia en localStorage +- ✅ Toggle inmediato sin recarga +- ✅ Variables CSS dinámicas para colores +- ✅ Transiciones suaves entre temas (0.3s ease) + +### 🎯 Componentes Interactivos +- ✅ **Tooltips**: Información contextual en hover con animaciones +- ✅ **Dropdown Menu**: Menú de usuario con opciones (Perfil, Ayuda, Cerrar sesión) +- ✅ **Loading Skeletons**: Estados de carga con animación shimmer +- ✅ **Modal**: Creación de transmisiones con animaciones (fadeIn + slideUp) + +### 🗓️ Filtrado Inteligente +- ✅ Tabs "Próximamente" / "Anteriores" +- ✅ **Filtrado real por fechas** usando Date comparison +- ✅ Manejo de transmisiones sin fecha programada +- ✅ Mensajes contextuales cuando no hay datos + +### 📱 Iconos de Plataforma +- 🎥 **YouTube** - Rojo (#FF0000) +- 📘 **Facebook** - Azul (#1877F2) +- 🎮 **Twitch** - Morado (#9146FF) +- 💼 **LinkedIn** - Azul profesional (#0A66C2) + +## 🎨 Sistema de Diseño + +### Paleta de Colores + +#### Modo Claro 🌞 +```css +--primary-blue: #4f46e5 /* Indigo-600 */ +--primary-blue-hover: #4338ca /* Indigo-700 */ +--background-color: #f7f8fa /* Gray-50 */ +--surface-color: #ffffff /* White */ +--text-primary: #1f2937 /* Gray-800 */ +--text-secondary: #6b7280 /* Gray-500 */ +--border-light: #e5e7eb /* Gray-200 */ +--active-bg-light: #eef2ff /* Indigo-50 */ +``` + +#### Modo Oscuro 🌙 +```css +--primary-blue: #6366f1 /* Indigo-500 */ +--primary-blue-hover: #4f46e5 /* Indigo-600 */ +--background-color: #0f172a /* Slate-900 */ +--surface-color: #1e293b /* Slate-800 */ +--text-primary: #f1f5f9 /* Slate-100 */ +--text-secondary: #cbd5e1 /* Slate-300 */ +--border-light: #334155 /* Slate-700 */ +--active-bg-light: #312e81 /* Indigo-950 */ +``` + +### Tipografía +- **Familia**: Inter, -apple-system, BlinkMacSystemFont, Segoe UI +- **Tamaños**: + - Caption: 12px + - Body: 14px + - Subtitle: 16px + - Heading: 22px +- **Pesos**: + - Normal: 400 + - Medium: 500 + - Semibold: 600 + +### Espaciado +- **Gaps**: 8px, 12px, 16px, 20px +- **Padding**: 12px, 16px, 20px, 24px, 32px +- **Bordes redondeados**: 4px, 6px, 8px, 12px + +### Sombras +```css +--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05) +--shadow-md: 0 1px 3px 0 rgba(0, 0, 0, 0.1) +--shadow-lg: 0 4px 6px -1px rgba(0, 0, 0, 0.1) +``` + +## 📦 Componentes Reutilizables + +### ThemeProvider +Proveedor de tema con persistencia en localStorage. + +```tsx +import { ThemeProvider } from './components/ThemeProvider' + + + + +``` + +**Hook de uso:** +```tsx +import { useTheme } from './components/ThemeProvider' + +const { theme, toggleTheme } = useTheme() +// theme: 'light' | 'dark' +``` + +### Tooltip +Tooltip contextual con 4 posiciones posibles. + +```tsx +import { Tooltip } from './components/Tooltip' + + + + +``` + +**Props:** +- `content`: string - Texto a mostrar +- `position`: 'top' | 'bottom' | 'left' | 'right' - Posición del tooltip +- `children`: ReactNode - Elemento que activa el tooltip + +### Dropdown +Menú desplegable con click outside detection. + +```tsx +import { Dropdown } from './components/Dropdown' + +Abrir menú} + items={[ + { + label: 'Mi perfil', + icon: , + onClick: () => console.log('Perfil') + }, + { + label: 'Cerrar sesión', + icon: , + onClick: handleLogout, + divider: true // Separador antes del item + } + ]} +/> +``` + +**Props:** +- `trigger`: ReactNode - Elemento que abre el dropdown +- `items`: DropdownItem[] - Array de opciones del menú + +### Skeleton +Componentes de carga con animación shimmer. + +```tsx +import { Skeleton, SkeletonCard, SkeletonTable } from './components/Skeleton' + +// Skeleton simple + + +// Card completa con skeleton + + +// Tabla con múltiples filas + +``` + +## 🏗️ Estructura de Componentes + +``` +src/components/ +├── ThemeProvider.tsx # Context de tema dark/light +├── Tooltip.tsx # Tooltip reutilizable +├── Tooltip.module.css +├── Dropdown.tsx # Menú desplegable +├── Dropdown.module.css +├── Skeleton.tsx # Estados de carga +├── Skeleton.module.css +├── PageContainer.tsx # Layout principal +├── PageContainer.module.css +├── Sidebar.tsx # Navegación lateral +├── Sidebar.module.css +├── Header.tsx # Barra superior +├── Header.module.css +├── TransmissionsTable.tsx # Tabla con filtrado +├── TransmissionsTable.module.css +├── NewTransmissionModal.tsx # Modal de creación +└── NewTransmissionModal.module.css +``` + +## 🚀 Scripts + +```bash +# Desarrollo (hot reload) +npm run dev + +# Build de producción +npm run build + +# Preview del build +npm run preview + +# Linting +npm run lint +``` + +El servidor de desarrollo estará disponible en: **http://localhost:5173/** + +## 📱 Breakpoints Responsivos + +```css +/* Mobile - Sidebar oculto, layout vertical */ +@media (max-width: 768px) + +/* Tablet - Sidebar colapsable */ +@media (max-width: 1024px) + +/* Desktop - Sidebar fijo, layout completo */ +@media (min-width: 1025px) +``` + +### Comportamiento Responsive + +- **Desktop (>1024px)**: Sidebar fijo de 260px, header completo +- **Tablet (768-1024px)**: Sidebar colapsable, header adaptado +- **Mobile (<768px)**: Sidebar con toggle, header compacto, botones simplificados + +## 🎯 Funcionalidades Implementadas + +### 1. React Icons ✅ +- Reemplazo completo de emojis con Material Design Icons +- Iconos de plataforma con colores branded (YouTube, Facebook, Twitch, LinkedIn) +- Consistencia visual en toda la aplicación + +### 2. Tema Oscuro ✅ +- Sistema de temas con Context API +- Persistencia automática en localStorage +- Toggle funcional sin recarga de página +- Variables CSS reactivas para transiciones suaves + +### 3. Loading Skeletons ✅ +- Estados de carga con animación shimmer +- Componentes específicos: Skeleton, SkeletonCard, SkeletonTable +- Simulación de carga de 800ms en PageContainer +- Mejora significativa en UX percibido + +### 4. Dropdown de Usuario ✅ +- Menú desplegable con 3 opciones (Perfil, Ayuda, Cerrar sesión) +- Click outside detection para cerrar +- Animación slideDown (0.2s ease-out) +- Separadores entre secciones del menú + +### 5. Tooltips Informativos ✅ +- Tooltips en todos los botones interactivos +- 4 posiciones: top, bottom, left, right +- Animación fadeIn (0.2s ease-in-out) +- Diseño con flecha direccional + +### 6. Filtrado por Fechas ✅ +- Tabs "Próximamente" / "Anteriores" +- Comparación real de fechas con `new Date()` +- Manejo de transmisiones sin fecha (upcoming por defecto) +- Mensajes contextuales en estado vacío + +## 📄 Licencia de Assets + +### Plantilla Techwind +- **Autor**: ShreeThemes +- **Versión**: 2.2.0 +- **Licencia**: Uso comercial permitido con atribución + +### Iconos +- **react-icons/md**: Material Design Icons (Apache 2.0 License) +- **react-icons/fa**: Font Awesome Free (CC BY 4.0 License) + +### Assets Copiados +``` +public/assets/ +├── logo-dark.png # Logo modo claro +├── logo-light.png # Logo modo oscuro +├── logo-icon.png # Icono app (64x64) +├── logo-icon-32.png # Icono pequeño (32x32) +└── logo-icon-64.png # Icono mediano (64x64) + +public/ +└── favicon.ico # Favicon del sitio +``` + +## 🚧 Próximas Mejoras + +### Fase 2 - Backend Integration +1. **API REST** + - Endpoints de transmisiones (CRUD) + - Autenticación con JWT + - Manejo de errores HTTP + +2. **Estado Global** + - Migrar a Zustand o Redux + - Sincronización con backend + - Optimistic updates + +### Fase 3 - Features Avanzadas +1. **Notificaciones en Tiempo Real** + - WebSocket para notificaciones + - Panel de notificaciones + - Badge con contador dinámico + +2. **Búsqueda y Filtros** + - Búsqueda por título + - Filtros combinados (plataforma + fecha + estado) + - Ordenamiento de columnas + +3. **Analytics Dashboard** + - Gráficos con ApexCharts + - Estadísticas de transmisiones + - Métricas de audiencia + +## 🐛 Debugging + +Para ver el estado del tema: +```javascript +// En la consola del navegador +localStorage.getItem('avanzacast-theme') +``` + +Para resetear el tema: +```javascript +localStorage.removeItem('avanzacast-theme') +window.location.reload() +``` + +## 📞 Soporte + +Para bugs o sugerencias, abrir issue en el repositorio. + +--- + +**Desarrollado con ❤️ para AvanzaCast** +*Versión 2.0 - Última actualización: 2024* diff --git a/packages/broadcast-panel/public/assets/logo-dark.png b/packages/broadcast-panel/public/assets/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..26d858e24907f9574497d352e634a2ab74fb2ad1 GIT binary patch literal 3914 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 GV*dwNehv5l literal 0 HcmV?d00001 diff --git a/packages/broadcast-panel/public/assets/logo-icon.png b/packages/broadcast-panel/public/assets/logo-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fa2e0eb0e3027d9e39b843109e98147f7bc3392c GIT binary patch literal 14416 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�hz}L@v;I?@{+oknn4)Yq_KR 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 literal 0 HcmV?d00001 diff --git a/packages/broadcast-panel/public/favicon.ico b/packages/broadcast-panel/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bb7350e532e7203ccfd2c63e53789358fdd3f2b8 GIT binary patch literal 15406 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 DjwW-z literal 0 HcmV?d00001 diff --git a/packages/broadcast-panel/src/components/Dropdown.module.css b/packages/broadcast-panel/src/components/Dropdown.module.css new file mode 100644 index 0000000..a369283 --- /dev/null +++ b/packages/broadcast-panel/src/components/Dropdown.module.css @@ -0,0 +1,60 @@ +.dropdown { + position: relative; + display: inline-block; +} + +.dropdownMenu { + position: absolute; + top: calc(100% + 8px); + right: 0; + background-color: var(--surface-color); + border: 1px solid rgba(255,255,255,0.04); + border-radius: 8px; + box-shadow: 0 12px 30px rgba(2,6,23,0.6); + min-width: 260px; + padding: 8px 0; + z-index: 1200; + animation: slideDown 0.18s cubic-bezier(.2,.9,.2,1); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.dropdownItem { + width: 100%; + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + background: transparent; + border: none; + color: var(--text-primary); + font-size: 14px; + text-align: left; + transition: background-color 0.15s ease; +} + +.dropdownItem:hover { + background-color: rgba(255,255,255,0.03); +} + +.dropdownItem .icon { + display: flex; + align-items: center; + font-size: 18px; + color: var(--text-secondary); +} + +.divider { + height: 1px; + background-color: rgba(255,255,255,0.03); + margin: 8px 0; +} diff --git a/packages/broadcast-panel/src/components/Dropdown.tsx b/packages/broadcast-panel/src/components/Dropdown.tsx new file mode 100644 index 0000000..b7d2d5d --- /dev/null +++ b/packages/broadcast-panel/src/components/Dropdown.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useRef, useState } from 'react'; +import styles from './Dropdown.module.css'; + +interface DropdownItem { + label: string; + icon?: React.ReactNode; + onClick: () => void; + divider?: boolean; +} + +interface DropdownProps { + trigger: React.ReactNode; + items: DropdownItem[]; +} + +export const Dropdown: React.FC = ({ trigger, items }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + return ( +
+
setIsOpen(!isOpen)}> + {trigger} +
+ + {isOpen && ( +
+ {items.map((item, index) => ( + + {item.divider &&
} + + + ))} +
+ )} +
+ ); +}; diff --git a/packages/broadcast-panel/src/components/Header.module.css b/packages/broadcast-panel/src/components/Header.module.css new file mode 100644 index 0000000..7e8decc --- /dev/null +++ b/packages/broadcast-panel/src/components/Header.module.css @@ -0,0 +1,144 @@ +.header { + height: 64px; + background-color: var(--surface-color); + border-bottom: 1px solid var(--border-light); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32px; + position: sticky; + top: 0; + z-index: 50; + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +.headerActions { + display: flex; + align-items: center; + gap: 16px; +} + +.planButton { + padding: 8px 16px; + background-color: transparent; + border: 1px solid var(--primary-blue); + color: var(--primary-blue); + border-radius: 6px; + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; +} + +.planButton:hover { + background-color: var(--primary-blue); + color: white; +} + +.segmentControl { + display: inline-flex; + background-color: rgba(0,0,0,0.06); + border-radius: 999px; + padding: 4px; + gap: 6px; + align-items: center; +} + +.segmentButton { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 999px; + background: transparent; + border: none; + color: var(--text-secondary); + transition: all 0.18s ease; +} + +.segmentButton:hover { + background-color: var(--border-light); + color: var(--text-primary); +} + +.activeSegment { + background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.04)); + box-shadow: 0 6px 14px rgba(2,6,23,0.4); + color: var(--surface-color); +} + +.notificationButton { + position: relative; + width: 36px; + height: 36px; + background-color: transparent; + border: none; + border-radius: 50%; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} + +.notificationButton:hover { + background-color: var(--border-light); +} + +.notificationDot { + position: absolute; + top: 8px; + right: 8px; + width: 8px; + height: 8px; + background-color: #ea4335; + border-radius: 50%; + border: 2px solid var(--surface-color); +} + +.userMenu { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + transition: background-color 0.2s ease; +} + +.userMenu:hover { + background-color: var(--border-light); +} + +.userEmail { + opacity: 0.95; + font-size: 14px; + color: var(--text-secondary); +} + +/* Dropdown trigger style polished */ +.userMenu:after { + content: ''; +} + +.userMenu svg { + font-size: 16px; + color: var(--text-secondary); +} + +@media (max-width: 768px) { + .header { + padding: 0 16px; + } + + .planButton { + display: none; + } + + .themeToggleButton { + display: none; + } +} diff --git a/packages/broadcast-panel/src/components/Header.tsx b/packages/broadcast-panel/src/components/Header.tsx index 8a1ccb3..4179f16 100644 --- a/packages/broadcast-panel/src/components/Header.tsx +++ b/packages/broadcast-panel/src/components/Header.tsx @@ -1,28 +1,86 @@ import React from 'react' +import { MdLightMode, MdDarkMode, MdNotifications, MdPerson, MdLogout, MdHelpOutline } from 'react-icons/md' +import { useTheme } from './ThemeProvider' +import { Tooltip } from './Tooltip' +import { Dropdown } from './Dropdown' +import styles from './Header.module.css' const Header: React.FC = () => { + const { theme, resolvedTheme, setThemeMode } = useTheme() + const handleLogout = () => { localStorage.removeItem('mock_user') window.location.href = '/auth/login' } - return ( -
-
- logo -
+ const userMenuItems = [ + { + label: 'Mi perfil', + icon: , + onClick: () => console.log('Ir a perfil') + }, + { + label: 'Ayuda', + icon: , + onClick: () => console.log('Ir a ayuda') + }, + { + label: 'Cerrar sesión', + icon: , + onClick: handleLogout, + divider: true + } + ] -
-
- + return ( +
+
{/* Spacer */} + +
+ + + {/* Segmented theme control: Sistema / Claro / Oscuro */} +
+ + +
-
-
- avatar -
-
Demo User
-
- + + + + + + + nextv.stream@gmail.com +
+ } + items={userMenuItems} + />
) diff --git a/packages/broadcast-panel/src/components/NewTransmissionModal.module.css b/packages/broadcast-panel/src/components/NewTransmissionModal.module.css new file mode 100644 index 0000000..4e83622 --- /dev/null +++ b/packages/broadcast-panel/src/components/NewTransmissionModal.module.css @@ -0,0 +1,162 @@ +.modalOverlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.2s ease; +} + +.modalContent { + background-color: var(--surface-color); + border-radius: 8px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + animation: slideUp 0.3s ease; +} + +.modalHeader { + padding: 20px 24px; + border-bottom: 1px solid var(--border-light); + position: relative; +} + +.modalTitle { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.closeButton { + position: absolute; + top: 20px; + right: 20px; + background-color: transparent; + border: none; + font-size: 24px; + color: var(--text-secondary); + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +.closeButton:hover { + background-color: var(--border-light); +} + +.modalBody { + padding: 24px; +} + +.formGroup { + margin-bottom: 20px; +} + +.formLabel { + display: block; + font-size: 14px; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 8px; +} + +.formInput, +.formSelect { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border-light); + border-radius: 6px; + font-size: 14px; + color: var(--text-primary); + background-color: var(--surface-color); + transition: all 0.2s ease; +} + +.formInput:focus, +.formSelect:focus { + outline: none; + border-color: var(--primary-blue); + box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1); +} + +.modalActions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 20px 24px; + border-top: 1px solid var(--border-light); +} + +.cancelButton { + padding: 10px 20px; + background-color: transparent; + border: 1px solid var(--border-light); + color: var(--text-primary); + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.cancelButton:hover { + background-color: var(--border-light); +} + +.submitButton { + padding: 10px 20px; + background-color: var(--primary-blue); + border: none; + color: white; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.submitButton:hover { + background-color: var(--primary-blue-hover); +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .modalContent { + width: 95%; + max-height: 95vh; + } + + .modalBody { + padding: 20px; + } +} diff --git a/packages/broadcast-panel/src/components/NewTransmissionModal.tsx b/packages/broadcast-panel/src/components/NewTransmissionModal.tsx index 4f4d3e1..cb24c46 100644 --- a/packages/broadcast-panel/src/components/NewTransmissionModal.tsx +++ b/packages/broadcast-panel/src/components/NewTransmissionModal.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import styles from './NewTransmissionModal.module.css' import type { Transmission } from '../types' interface Props { @@ -26,30 +27,58 @@ const NewTransmissionModal: React.FC = ({ open, onClose, onCreate }) => { } return ( -
-
-

Nueva transmisión

-
-
- - setTitle(e.target.value)} className="w-full p-2 border rounded" required /> +
+
e.stopPropagation()}> +
+

Crear transmisión en vivo

+ +
+ + +
+
+ + setTitle(e.target.value)} + className={styles.formInput} + placeholder="Ej: Mi primera transmisión" + required + /> +
+ +
+ + +
+ +
+ + setScheduled(e.target.value)} + placeholder="YYYY-MM-DD HH:mm" + className={styles.formInput} + /> +
-
- - -
-
- - setScheduled(e.target.value)} placeholder="YYYY-MM-DD HH:mm" className="w-full p-2 border rounded" /> -
-
- - + +
+ +
diff --git a/packages/broadcast-panel/src/components/PageContainer.module.css b/packages/broadcast-panel/src/components/PageContainer.module.css new file mode 100644 index 0000000..0bd8607 --- /dev/null +++ b/packages/broadcast-panel/src/components/PageContainer.module.css @@ -0,0 +1,67 @@ +.pageContainer { + display: flex; + min-height: 100vh; + background-color: var(--background-color); + transition: background-color 0.3s ease; +} + +.mainContent { + flex: 1; + display: flex; + flex-direction: column; + margin-left: 260px; /* Ancho del sidebar */ + transition: margin-left 0.3s ease; +} + +.contentWrapper { + flex: 1; + padding: 20px 20px; + overflow-y: auto; +} + +.createGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 16px; +} + +.createCard { + background: var(--surface-color); + border: 1px solid var(--border-light); + border-radius: 10px; + padding: 18px; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + transition: box-shadow 0.18s ease, transform 0.12s ease, border-color 0.12s ease; + font-size: 15px; + font-weight: 600; +} + +.createCard:hover { + box-shadow: 0 6px 18px rgba(16,24,40,0.06); + transform: translateY(-3px); + border-color: var(--primary-blue); +} + +.createIconBox { + width: 44px; + height: 44px; + border-radius: 8px; + display: grid; + place-items: center; + background: var(--bg-muted); +} + +@media (max-width: 1024px) { + .mainContent { + margin-left: 0; + } +} + +@media (max-width: 768px) { + .contentWrapper { + padding: 20px 16px; + } +} diff --git a/packages/broadcast-panel/src/components/PageContainer.tsx b/packages/broadcast-panel/src/components/PageContainer.tsx index adb57c7..321ee6f 100644 --- a/packages/broadcast-panel/src/components/PageContainer.tsx +++ b/packages/broadcast-panel/src/components/PageContainer.tsx @@ -1,4 +1,8 @@ import React, { useEffect, useState } from 'react' +import { MdVideocam, MdFiberManualRecord, MdSchool } from 'react-icons/md' +import { ThemeProvider } from './ThemeProvider' +import { Skeleton, SkeletonCard } from './Skeleton' +import styles from './PageContainer.module.css' import Sidebar from './Sidebar' import Header from './Header' import TransmissionsTable from './TransmissionsTable' @@ -10,23 +14,30 @@ const STORAGE_KEY = 'broadcast_transmissions' const PageContainer: React.FC = () => { const [transmissions, setTransmissions] = useState([]) const [isModalOpen, setIsModalOpen] = useState(false) + const [isLoading, setIsLoading] = useState(true) useEffect(() => { - try { - const raw = localStorage.getItem(STORAGE_KEY) - if (raw) setTransmissions(JSON.parse(raw)) - } catch (e) { - console.error('Failed to load transmissions', e) - } + // Simular carga de datos + setTimeout(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) setTransmissions(JSON.parse(raw)) + } catch (e) { + console.error('Failed to load transmissions', e) + } + setIsLoading(false) + }, 800) }, []) useEffect(() => { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(transmissions)) - } catch (e) { - console.error('Failed to save transmissions', e) + if (!isLoading) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(transmissions)) + } catch (e) { + console.error('Failed to save transmissions', e) + } } - }, [transmissions]) + }, [transmissions, isLoading]) const handleCreate = (t: Transmission) => { setTransmissions(prev => [t, ...prev]) @@ -42,37 +53,72 @@ const PageContainer: React.FC = () => { } return ( -
- -
-
-
-
-
-

Transmisiones

-
- - -
-
+ +
+ +
+
+
+ {/* Sección Crear */} +
+

Crear

+ {isLoading ? ( +
+ + + +
+ ) : ( +
+ -
+ + + +
+ )} +
+ + {/* Sección Transmisiones y grabaciones */} +
+

+ Transmisiones y grabaciones +

-
-
+ - setIsModalOpen(false)} - onCreate={handleCreate} - /> -
+ setIsModalOpen(false)} + onCreate={handleCreate} + /> + +
-
+ ) } diff --git a/packages/broadcast-panel/src/components/Sidebar.module.css b/packages/broadcast-panel/src/components/Sidebar.module.css new file mode 100644 index 0000000..97b307b --- /dev/null +++ b/packages/broadcast-panel/src/components/Sidebar.module.css @@ -0,0 +1,178 @@ +.sidebar { + position: fixed; + left: 0; + top: 0; + width: 260px; + height: 100vh; + background-color: var(--surface-color); + border-right: 1px solid var(--border-light); + display: flex; + flex-direction: column; + overflow-y: auto; + z-index: 100; + transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease; +} + +.logoSection { + display: flex; + align-items: center; + padding: 20px; + border-bottom: 1px solid var(--border-light); + gap: 12px; +} + +.logoIcon { + width: 32px; + height: 32px; + object-fit: contain; +} + +.logoText { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +.navMenu { + flex: 1; + padding: 16px 0; +} + +.navList { + list-style: none; +} + +.navItem { + margin: 2px 0; +} + +.navLink { + display: flex; + align-items: center; + padding: 12px 18px; + color: var(--text-secondary); + font-size: 14px; + font-weight: 500; + gap: 12px; + border-left: 3px solid transparent; + width: calc(100% + 40px); + margin-left: -20px; + transition: background-color 0.15s ease, color 0.15s ease, transform 0.12s ease; +} + +.navLink:hover { + background-color: var(--active-bg-light); + color: var(--primary-blue); + transform: translateX(4px); +} + +.activeLink .navLink { + background-color: var(--active-bg-light); + color: var(--primary-blue); + border-left-color: var(--primary-blue); +} + +.navIcon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; +} + +.secondaryNavGroup { + margin-top: 0; + padding-top: 0; + border-top: 1px solid var(--border-light); +} + +.secondaryNavGroup .navList { + margin: 0; + padding: 0; +} + +/* separator between secondary items (not above the first) */ +.secondaryNavGroup .navItem + .navItem { + border-top: 1px solid var(--border-light); +} + +.secondaryNavGroup .navLink { + padding: 12px 18px; +} + +.storageInfo { + padding: 12px 20px 20px 20px; + border-top: 1px solid var(--border-light); + margin-top: auto; +} + +.secondaryNavGroup .navLink { + padding-left: 18px; +} + +.storageTitle { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; +} + +.infoIcon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: var(--border-light); + font-size: 10px; + font-weight: 700; + color: var(--text-secondary); +} + +.progressBarContainer { + width: 100%; + height: 6px; + background-color: var(--border-light); + border-radius: 3px; + overflow: hidden; + margin-bottom: 8px; +} + +.progressBarFill { + height: 100%; + background-color: var(--primary-blue); + border-radius: 3px; + transition: width 0.3s ease; +} + +.storageUsage { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.addMoreLink { + font-size: 12px; + color: var(--primary-blue); + font-weight: 500; + cursor: pointer; +} + +.addMoreLink:hover { + text-decoration: underline; +} + +@media (max-width: 1024px) { + .sidebar { + transform: translateX(-100%); + } + + .sidebar.open { + transform: translateX(0); + } +} diff --git a/packages/broadcast-panel/src/components/Sidebar.tsx b/packages/broadcast-panel/src/components/Sidebar.tsx index 027df5b..f04db66 100644 --- a/packages/broadcast-panel/src/components/Sidebar.tsx +++ b/packages/broadcast-panel/src/components/Sidebar.tsx @@ -1,50 +1,73 @@ import React from 'react' +import { MdHome, MdVideoLibrary, MdLink, MdPeople, MdCardGiftcard, MdSettings, MdAssessment } from 'react-icons/md' +import { Tooltip } from './Tooltip' +import styles from './Sidebar.module.css' -const Sidebar: React.FC = () => { +interface SidebarProps { + activeLink?: string +} + +const Sidebar: React.FC = ({ activeLink = 'inicio' }) => { const navItems = [ - { id: 'dashboard', label: 'Inicio' }, - { id: 'create', label: 'Crear' }, - { id: 'transmissions', label: 'Transmisiones' }, - { id: 'recordings', label: 'Grabaciones' }, - { id: 'settings', label: 'Ajustes' }, + { id: 'inicio', label: 'Inicio', icon: }, + { id: 'biblioteca', label: 'Biblioteca', icon: }, + { id: 'destinos', label: 'Destinos', icon: }, + { id: 'miembros', label: 'Miembros', icon: }, + ] + + const secondaryNavItems = [ + { id: 'referidos', label: 'Referidos', icon: }, + { id: 'configuracion', label: 'Configuración del equipo', icon: }, + { id: 'sistema', label: 'Estado del sistema', icon: }, ] return ( -