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 0000000..26d858e Binary files /dev/null and b/packages/broadcast-panel/public/assets/logo-dark.png differ diff --git a/packages/broadcast-panel/public/assets/logo-icon-32.png b/packages/broadcast-panel/public/assets/logo-icon-32.png new file mode 100644 index 0000000..c759ea3 Binary files /dev/null and b/packages/broadcast-panel/public/assets/logo-icon-32.png differ diff --git a/packages/broadcast-panel/public/assets/logo-icon-64.png b/packages/broadcast-panel/public/assets/logo-icon-64.png new file mode 100644 index 0000000..36e1453 Binary files /dev/null and b/packages/broadcast-panel/public/assets/logo-icon-64.png differ 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 0000000..fa2e0eb Binary files /dev/null and b/packages/broadcast-panel/public/assets/logo-icon.png differ diff --git a/packages/broadcast-panel/public/assets/logo-light.png b/packages/broadcast-panel/public/assets/logo-light.png new file mode 100644 index 0000000..37625bd Binary files /dev/null and b/packages/broadcast-panel/public/assets/logo-light.png differ diff --git a/packages/broadcast-panel/public/favicon.ico b/packages/broadcast-panel/public/favicon.ico new file mode 100644 index 0000000..bb7350e Binary files /dev/null and b/packages/broadcast-panel/public/favicon.ico differ 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 ( -