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
This commit is contained in:
parent
e036be8671
commit
01178e9532
11
package-lock.json
generated
11
package-lock.json
generated
@ -11,6 +11,9 @@
|
|||||||
"packages/*",
|
"packages/*",
|
||||||
"shared/*"
|
"shared/*"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"react-icons": "^5.5.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
@ -8400,6 +8403,14 @@
|
|||||||
"react": "^18.2.0"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
|||||||
@ -34,5 +34,8 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0",
|
"node": ">=20.0.0",
|
||||||
"npm": ">=10.0.0"
|
"npm": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-icons": "^5.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
packages/broadcast-panel/README.md
Normal file
147
packages/broadcast-panel/README.md
Normal file
@ -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
|
||||||
343
packages/broadcast-panel/README_UPDATED.md
Normal file
343
packages/broadcast-panel/README_UPDATED.md
Normal file
@ -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'
|
||||||
|
|
||||||
|
<ThemeProvider>
|
||||||
|
<App />
|
||||||
|
</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'
|
||||||
|
|
||||||
|
<Tooltip content="Texto del tooltip" position="bottom">
|
||||||
|
<button>Hover aquí</button>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
trigger={<button>Abrir menú</button>}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
label: 'Mi perfil',
|
||||||
|
icon: <MdPerson />,
|
||||||
|
onClick: () => console.log('Perfil')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cerrar sesión',
|
||||||
|
icon: <MdLogout />,
|
||||||
|
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
|
||||||
|
<Skeleton width="200px" height="20px" borderRadius="6px" />
|
||||||
|
|
||||||
|
// Card completa con skeleton
|
||||||
|
<SkeletonCard />
|
||||||
|
|
||||||
|
// Tabla con múltiples filas
|
||||||
|
<SkeletonTable rows={5} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ 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*
|
||||||
BIN
packages/broadcast-panel/public/assets/logo-dark.png
Normal file
BIN
packages/broadcast-panel/public/assets/logo-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
packages/broadcast-panel/public/assets/logo-icon-32.png
Normal file
BIN
packages/broadcast-panel/public/assets/logo-icon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/broadcast-panel/public/assets/logo-icon-64.png
Normal file
BIN
packages/broadcast-panel/public/assets/logo-icon-64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
packages/broadcast-panel/public/assets/logo-icon.png
Normal file
BIN
packages/broadcast-panel/public/assets/logo-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
packages/broadcast-panel/public/assets/logo-light.png
Normal file
BIN
packages/broadcast-panel/public/assets/logo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
packages/broadcast-panel/public/favicon.ico
Normal file
BIN
packages/broadcast-panel/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
60
packages/broadcast-panel/src/components/Dropdown.module.css
Normal file
60
packages/broadcast-panel/src/components/Dropdown.module.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
63
packages/broadcast-panel/src/components/Dropdown.tsx
Normal file
63
packages/broadcast-panel/src/components/Dropdown.tsx
Normal file
@ -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<DropdownProps> = ({ trigger, items }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<div className={styles.dropdown} ref={dropdownRef}>
|
||||||
|
<div onClick={() => setIsOpen(!isOpen)}>
|
||||||
|
{trigger}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className={styles.dropdownMenu}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
{item.divider && <div className={styles.divider} />}
|
||||||
|
<button
|
||||||
|
className={styles.dropdownItem}
|
||||||
|
onClick={() => {
|
||||||
|
item.onClick();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.icon && <span className={styles.icon}>{item.icon}</span>}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</button>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
144
packages/broadcast-panel/src/components/Header.module.css
Normal file
144
packages/broadcast-panel/src/components/Header.module.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +1,86 @@
|
|||||||
import React from 'react'
|
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 Header: React.FC = () => {
|
||||||
|
const { theme, resolvedTheme, setThemeMode } = useTheme()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('mock_user')
|
localStorage.removeItem('mock_user')
|
||||||
window.location.href = '/auth/login'
|
window.location.href = '/auth/login'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const userMenuItems = [
|
||||||
<header className="h-16 bg-white border-b flex items-center justify-between px-6">
|
{
|
||||||
<div className="flex items-center gap-4">
|
label: 'Mi perfil',
|
||||||
<img src="/assets/logo-dark.png" alt="logo" className="w-36 h-auto object-contain" />
|
icon: <MdPerson size={18} />,
|
||||||
</div>
|
onClick: () => console.log('Ir a perfil')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ayuda',
|
||||||
|
icon: <MdHelpOutline size={18} />,
|
||||||
|
onClick: () => console.log('Ir a ayuda')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cerrar sesión',
|
||||||
|
icon: <MdLogout size={18} />,
|
||||||
|
onClick: handleLogout,
|
||||||
|
divider: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
return (
|
||||||
<div className="hidden sm:block">
|
<header className={styles.header}>
|
||||||
<button className="px-3 py-2 border rounded flex items-center gap-2"><i className="mdi mdi-help-circle-outline"></i> Ayuda</button>
|
<div></div> {/* Spacer */}
|
||||||
|
|
||||||
|
<div className={styles.headerActions}>
|
||||||
|
<button className={styles.planButton}>
|
||||||
|
Mejora tu plan
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Segmented theme control: Sistema / Claro / Oscuro */}
|
||||||
|
<div className={styles.segmentControl} role="tablist" aria-label="Tema">
|
||||||
|
<button
|
||||||
|
className={`${styles.segmentButton} ${theme === 'system' ? styles.activeSegment : ''}`}
|
||||||
|
onClick={() => setThemeMode('system')}
|
||||||
|
title="Usar tema del sistema"
|
||||||
|
>
|
||||||
|
<span className={styles.segmentIcon}>⚙</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.segmentButton} ${theme === 'light' ? styles.activeSegment : ''}`}
|
||||||
|
onClick={() => setThemeMode('light')}
|
||||||
|
title="Modo claro"
|
||||||
|
>
|
||||||
|
<MdLightMode size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.segmentButton} ${theme === 'dark' ? styles.activeSegment : ''}`}
|
||||||
|
onClick={() => setThemeMode('dark')}
|
||||||
|
title="Modo oscuro"
|
||||||
|
>
|
||||||
|
<MdDarkMode size={16} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center overflow-hidden">
|
<Tooltip content="Notificaciones">
|
||||||
<img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />
|
<button className={styles.notificationButton} title="Notificaciones">
|
||||||
</div>
|
<MdNotifications size={20} />
|
||||||
<div className="text-sm">Demo User</div>
|
<span className={styles.notificationDot}></span>
|
||||||
</div>
|
</button>
|
||||||
<button onClick={handleLogout} className="px-3 py-2 bg-red-50 text-red-600 border rounded">Cerrar sesión</button>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<div className={styles.userMenu}>
|
||||||
|
<span className={styles.userEmail}>nextv.stream@gmail.com</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
items={userMenuItems}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import styles from './NewTransmissionModal.module.css'
|
||||||
import type { Transmission } from '../types'
|
import type { Transmission } from '../types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -26,30 +27,58 @@ const NewTransmissionModal: React.FC<Props> = ({ open, onClose, onCreate }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div className={styles.modalOverlay} onClick={onClose}>
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-lg">
|
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
|
||||||
<h3 className="text-lg font-semibold mb-4">Nueva transmisión</h3>
|
<div className={styles.modalHeader}>
|
||||||
<form onSubmit={handleSubmit} className="space-y-3">
|
<h3 className={styles.modalTitle}>Crear transmisión en vivo</h3>
|
||||||
<div>
|
<button className={styles.closeButton} onClick={onClose}>×</button>
|
||||||
<label className="block text-sm">Título</label>
|
</div>
|
||||||
<input value={title} onChange={(e) => setTitle(e.target.value)} className="w-full p-2 border rounded" required />
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.modalBody}>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.formLabel}>Título de la transmisión</label>
|
||||||
|
<input
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
className={styles.formInput}
|
||||||
|
placeholder="Ej: Mi primera transmisión"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.formLabel}>Plataforma</label>
|
||||||
|
<select
|
||||||
|
value={platform}
|
||||||
|
onChange={(e) => setPlatform(e.target.value)}
|
||||||
|
className={styles.formSelect}
|
||||||
|
>
|
||||||
|
<option value="YouTube">YouTube</option>
|
||||||
|
<option value="Facebook">Facebook</option>
|
||||||
|
<option value="Twitch">Twitch</option>
|
||||||
|
<option value="LinkedIn">LinkedIn</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.formLabel}>Fecha y hora programada (opcional)</label>
|
||||||
|
<input
|
||||||
|
value={scheduled}
|
||||||
|
onChange={(e) => setScheduled(e.target.value)}
|
||||||
|
placeholder="YYYY-MM-DD HH:mm"
|
||||||
|
className={styles.formInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm">Plataforma</label>
|
<div className={styles.modalActions}>
|
||||||
<select value={platform} onChange={(e) => setPlatform(e.target.value)} className="w-full p-2 border rounded">
|
<button type="button" onClick={onClose} className={styles.cancelButton}>
|
||||||
<option>YouTube</option>
|
Cancelar
|
||||||
<option>Facebook</option>
|
</button>
|
||||||
<option>Twitch</option>
|
<button type="submit" className={styles.submitButton}>
|
||||||
<option>LinkedIn</option>
|
Crear transmisión
|
||||||
</select>
|
</button>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm">Fecha y hora</label>
|
|
||||||
<input value={scheduled} onChange={(e) => setScheduled(e.target.value)} placeholder="YYYY-MM-DD HH:mm" className="w-full p-2 border rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end gap-3">
|
|
||||||
<button type="button" onClick={onClose} className="px-4 py-2 border rounded">Cancelar</button>
|
|
||||||
<button type="submit" className="px-4 py-2 bg-indigo-600 text-white rounded">Crear</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
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 Sidebar from './Sidebar'
|
||||||
import Header from './Header'
|
import Header from './Header'
|
||||||
import TransmissionsTable from './TransmissionsTable'
|
import TransmissionsTable from './TransmissionsTable'
|
||||||
@ -10,23 +14,30 @@ const STORAGE_KEY = 'broadcast_transmissions'
|
|||||||
const PageContainer: React.FC = () => {
|
const PageContainer: React.FC = () => {
|
||||||
const [transmissions, setTransmissions] = useState<Transmission[]>([])
|
const [transmissions, setTransmissions] = useState<Transmission[]>([])
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
// Simular carga de datos
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
setTimeout(() => {
|
||||||
if (raw) setTransmissions(JSON.parse(raw))
|
try {
|
||||||
} catch (e) {
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
console.error('Failed to load transmissions', e)
|
if (raw) setTransmissions(JSON.parse(raw))
|
||||||
}
|
} catch (e) {
|
||||||
|
console.error('Failed to load transmissions', e)
|
||||||
|
}
|
||||||
|
setIsLoading(false)
|
||||||
|
}, 800)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
if (!isLoading) {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(transmissions))
|
try {
|
||||||
} catch (e) {
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(transmissions))
|
||||||
console.error('Failed to save transmissions', e)
|
} catch (e) {
|
||||||
|
console.error('Failed to save transmissions', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [transmissions])
|
}, [transmissions, isLoading])
|
||||||
|
|
||||||
const handleCreate = (t: Transmission) => {
|
const handleCreate = (t: Transmission) => {
|
||||||
setTransmissions(prev => [t, ...prev])
|
setTransmissions(prev => [t, ...prev])
|
||||||
@ -42,37 +53,72 @@ const PageContainer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex bg-[#f7f8fa]">
|
<ThemeProvider>
|
||||||
<Sidebar />
|
<div className={styles.pageContainer}>
|
||||||
<div className="flex-1 flex flex-col">
|
<Sidebar activeLink="inicio" />
|
||||||
<Header />
|
<div className={styles.mainContent}>
|
||||||
<main className="p-8">
|
<Header />
|
||||||
<div className="max-w-6xl mx-auto">
|
<main className={styles.contentWrapper}>
|
||||||
<div className="mb-6 flex items-center justify-between">
|
{/* Sección Crear */}
|
||||||
<h1 className="text-2xl font-semibold">Transmisiones</h1>
|
<section style={{ marginBottom: '40px' }}>
|
||||||
<div className="flex items-center gap-3">
|
<h2 style={{ fontSize: '22px', fontWeight: 600, marginBottom: '18px' }}>Crear</h2>
|
||||||
<button onClick={() => setIsModalOpen(true)} className="px-4 py-2 bg-indigo-600 text-white rounded">Nueva transmisión</button>
|
{isLoading ? (
|
||||||
<button className="px-3 py-2 border rounded">Importar</button>
|
<div className={styles.createGrid}>
|
||||||
</div>
|
<SkeletonCard />
|
||||||
</div>
|
<SkeletonCard />
|
||||||
|
<SkeletonCard />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.createGrid}>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
className={styles.createCard}
|
||||||
|
>
|
||||||
|
<div className={styles.createIconBox}>
|
||||||
|
<MdVideocam size={20} style={{ color: 'var(--primary-blue)' }} />
|
||||||
|
</div>
|
||||||
|
<span>Transmisión en vivo</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="bg-white border rounded p-4 shadow-sm">
|
<button className={styles.createCard}>
|
||||||
|
<div className={styles.createIconBox}>
|
||||||
|
<MdFiberManualRecord size={20} style={{ color: '#ea4335' }} />
|
||||||
|
</div>
|
||||||
|
<span>Grabación</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className={styles.createCard}>
|
||||||
|
<div className={styles.createIconBox}>
|
||||||
|
<MdSchool size={20} style={{ color: '#34a853' }} />
|
||||||
|
</div>
|
||||||
|
<span>Seminario web On-Air</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Sección Transmisiones y grabaciones */}
|
||||||
|
<section>
|
||||||
|
<h2 style={{ fontSize: '22px', fontWeight: 600, marginBottom: '24px' }}>
|
||||||
|
Transmisiones y grabaciones
|
||||||
|
</h2>
|
||||||
<TransmissionsTable
|
<TransmissionsTable
|
||||||
transmissions={transmissions}
|
transmissions={transmissions}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<NewTransmissionModal
|
<NewTransmissionModal
|
||||||
open={isModalOpen}
|
open={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
onCreate={handleCreate}
|
onCreate={handleCreate}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
178
packages/broadcast-panel/src/components/Sidebar.module.css
Normal file
178
packages/broadcast-panel/src/components/Sidebar.module.css
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,50 +1,73 @@
|
|||||||
import React from 'react'
|
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<SidebarProps> = ({ activeLink = 'inicio' }) => {
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ id: 'dashboard', label: 'Inicio' },
|
{ id: 'inicio', label: 'Inicio', icon: <MdHome size={20} /> },
|
||||||
{ id: 'create', label: 'Crear' },
|
{ id: 'biblioteca', label: 'Biblioteca', icon: <MdVideoLibrary size={20} /> },
|
||||||
{ id: 'transmissions', label: 'Transmisiones' },
|
{ id: 'destinos', label: 'Destinos', icon: <MdLink size={20} /> },
|
||||||
{ id: 'recordings', label: 'Grabaciones' },
|
{ id: 'miembros', label: 'Miembros', icon: <MdPeople size={20} /> },
|
||||||
{ id: 'settings', label: 'Ajustes' },
|
]
|
||||||
|
|
||||||
|
const secondaryNavItems = [
|
||||||
|
{ id: 'referidos', label: 'Referidos', icon: <MdCardGiftcard size={20} /> },
|
||||||
|
{ id: 'configuracion', label: 'Configuración del equipo', icon: <MdSettings size={20} /> },
|
||||||
|
{ id: 'sistema', label: 'Estado del sistema', icon: <MdAssessment size={20} /> },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-72 bg-white border-r shadow-sm flex flex-col min-h-screen">
|
<aside className={styles.sidebar}>
|
||||||
<div className="p-4 border-b flex items-center gap-3">
|
{/* Logo Section */}
|
||||||
<img src="/assets/logo-light.png" alt="logo" className="w-10 h-10 object-contain" />
|
<div className={styles.logoSection}>
|
||||||
<div>
|
<img src="/assets/logo-icon.png" alt="Logo" className={styles.logoIcon} />
|
||||||
<div className="text-lg font-semibold">AvanzaCast</div>
|
<span className={styles.logoText}>AvanzaCast</span>
|
||||||
<div className="text-sm text-slate-500">Cuenta Demo</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="p-4 flex-1 overflow-y-auto">
|
{/* Main Navigation */}
|
||||||
<ul className="space-y-1">
|
<nav className={styles.navMenu}>
|
||||||
|
<ul className={styles.navList}>
|
||||||
{navItems.map(item => (
|
{navItems.map(item => (
|
||||||
<li key={item.id}>
|
<li key={item.id} className={activeLink === item.id ? styles.activeLink : styles.navItem}>
|
||||||
<a href="#" className="flex items-center gap-3 px-3 py-2 rounded hover:bg-slate-50 text-sm text-slate-700">
|
<a href={`#${item.id}`} className={styles.navLink}>
|
||||||
<span className="w-9 h-9 flex items-center justify-center rounded bg-slate-100 text-slate-500">{item.label.charAt(0)}</span>
|
<span className={styles.navIcon}>{item.icon}</span>
|
||||||
<span className="flex-1">{item.label}</span>
|
<span>{item.label}</span>
|
||||||
<i className="mdi mdi-chevron-right text-slate-300"></i>
|
</a>
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{/* Secondary Navigation moved closer to storage */}
|
||||||
|
<div className={styles.secondaryNavGroup}>
|
||||||
|
<ul className={styles.navList}>
|
||||||
|
{secondaryNavItems.map(item => (
|
||||||
|
<li key={item.id} className={activeLink === item.id ? styles.activeLink : styles.navItem}>
|
||||||
|
<a href={`#${item.id}`} className={styles.navLink}>
|
||||||
|
<span className={styles.navIcon}>{item.icon}</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="mt-auto p-4 border-t bg-white">
|
{/* Storage Info */}
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className={styles.storageInfo}>
|
||||||
<div className="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center overflow-hidden">
|
<div className={styles.storageTitle}>
|
||||||
<img src="/assets/logo-icon.png" alt="avatar" className="w-full h-full object-cover" />
|
Almacenamiento
|
||||||
</div>
|
<span className={styles.infoIcon}>?</span>
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium">Demo User</div>
|
|
||||||
<div className="text-xs text-slate-500">demo@avanzacast.test</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-slate-600 mb-3">Almacenamiento: <strong>0</strong> de 5 GB</div>
|
<div className={styles.progressBarContainer}>
|
||||||
<button className="mt-3 w-full py-2 bg-indigo-600 text-white rounded">Comprar más</button>
|
<div className={styles.progressBarFill} style={{ width: '0%' }}></div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.storageUsage}>0 de 5 horas</div>
|
||||||
|
<a href="#agregar" className={styles.addMoreLink}>Agregar más</a>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
)
|
)
|
||||||
|
|||||||
45
packages/broadcast-panel/src/components/Skeleton.module.css
Normal file
45
packages/broadcast-panel/src/components/Skeleton.module.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--skeleton-base) 0%,
|
||||||
|
var(--skeleton-highlight) 50%,
|
||||||
|
var(--skeleton-base) 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonCard {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonTable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeletonRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
47
packages/broadcast-panel/src/components/Skeleton.tsx
Normal file
47
packages/broadcast-panel/src/components/Skeleton.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './Skeleton.module.css';
|
||||||
|
|
||||||
|
interface SkeletonProps {
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
borderRadius?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Skeleton: React.FC<SkeletonProps> = ({
|
||||||
|
width = '100%',
|
||||||
|
height = '20px',
|
||||||
|
borderRadius = '4px',
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.skeleton} ${className}`}
|
||||||
|
style={{ width, height, borderRadius }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SkeletonCard: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.skeletonCard}>
|
||||||
|
<Skeleton height="60px" borderRadius="8px" />
|
||||||
|
<Skeleton width="60%" height="16px" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SkeletonTable: React.FC<{ rows?: number }> = ({ rows = 5 }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.skeletonTable}>
|
||||||
|
{Array.from({ length: rows }).map((_, index) => (
|
||||||
|
<div key={index} className={styles.skeletonRow}>
|
||||||
|
<Skeleton width="150px" height="16px" />
|
||||||
|
<Skeleton width="100px" height="16px" />
|
||||||
|
<Skeleton width="120px" height="16px" />
|
||||||
|
<Skeleton width="80px" height="32px" borderRadius="6px" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
62
packages/broadcast-panel/src/components/ThemeProvider.tsx
Normal file
62
packages/broadcast-panel/src/components/ThemeProvider.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
resolvedTheme: 'light' | 'dark';
|
||||||
|
setThemeMode: (t: Theme) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [theme, setTheme] = useState<Theme>(() => {
|
||||||
|
const saved = localStorage.getItem('avanzacast-theme-mode');
|
||||||
|
return (saved as Theme) || 'system';
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSystem = () => (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
||||||
|
|
||||||
|
const apply = (mode: Theme) => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const resolved = mode === 'system' ? getSystem() : mode;
|
||||||
|
root.setAttribute('data-theme', resolved);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apply(theme);
|
||||||
|
localStorage.setItem('avanzacast-theme-mode', theme);
|
||||||
|
|
||||||
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
const handler = () => {
|
||||||
|
// only react if mode is system
|
||||||
|
if (theme === 'system') apply('system');
|
||||||
|
};
|
||||||
|
if (mq.addEventListener) mq.addEventListener('change', handler);
|
||||||
|
else mq.addListener(handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mq.removeEventListener) mq.removeEventListener('change', handler);
|
||||||
|
else mq.removeListener(handler);
|
||||||
|
};
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const setThemeMode = (t: Theme) => setTheme(t);
|
||||||
|
|
||||||
|
const resolvedTheme = theme === 'system' ? getSystem() : theme;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, resolvedTheme, setThemeMode }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useTheme debe usarse dentro de ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
92
packages/broadcast-panel/src/components/Tooltip.module.css
Normal file
92
packages/broadcast-panel/src/components/Tooltip.module.css
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
.tooltipWrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: fadeIn 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.top {
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.bottom {
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.left {
|
||||||
|
right: calc(100% + 8px);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip.right {
|
||||||
|
left: calc(100% + 8px);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top .arrow {
|
||||||
|
bottom: -4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 4px 4px 0 4px;
|
||||||
|
border-color: #1f2937 transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom .arrow {
|
||||||
|
top: -4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 0 4px 4px 4px;
|
||||||
|
border-color: transparent transparent #1f2937 transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .arrow {
|
||||||
|
right: -4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-width: 4px 0 4px 4px;
|
||||||
|
border-color: transparent transparent transparent #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .arrow {
|
||||||
|
left: -4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-width: 4px 4px 4px 0;
|
||||||
|
border-color: transparent #1f2937 transparent transparent;
|
||||||
|
}
|
||||||
28
packages/broadcast-panel/src/components/Tooltip.tsx
Normal file
28
packages/broadcast-panel/src/components/Tooltip.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import styles from './Tooltip.module.css';
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
content: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip: React.FC<TooltipProps> = ({ content, children, position = 'bottom' }) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.tooltipWrapper}
|
||||||
|
onMouseEnter={() => setIsVisible(true)}
|
||||||
|
onMouseLeave={() => setIsVisible(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{isVisible && (
|
||||||
|
<div className={`${styles.tooltip} ${styles[position]}`}>
|
||||||
|
{content}
|
||||||
|
<div className={styles.arrow} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
.transmissionsSection {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabButton {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabButton:hover {
|
||||||
|
color: var(--primary-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabButton.activeTab {
|
||||||
|
color: var(--primary-blue);
|
||||||
|
border-bottom-color: var(--primary-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper {
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transmissionsTable {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableHeader {
|
||||||
|
background: var(--bg-muted);
|
||||||
|
text-align: left;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRow {
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRow:hover {
|
||||||
|
background-color: var(--active-bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableCell {
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableRow:last-child .tableCell {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleCellContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platformAvatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--active-bg-light);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transmissionTitle {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platformIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsCell {
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableCard {
|
||||||
|
background: var(--surface-color);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterStudioButton {
|
||||||
|
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;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterStudioButton:hover {
|
||||||
|
background-color: var(--primary-blue);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moreOptionsButton {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moreOptionsButton:hover {
|
||||||
|
background-color: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.noDataCell {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 20px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tableWrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableCell {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enterStudioButton {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,54 +1,127 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { MdMoreVert, MdVideocam } from 'react-icons/md'
|
||||||
|
import { FaYoutube, FaFacebook, FaTwitch, FaLinkedin } from 'react-icons/fa'
|
||||||
|
import { SkeletonTable } from './Skeleton'
|
||||||
|
import styles from './TransmissionsTable.module.css'
|
||||||
import type { Transmission } from '../types'
|
import type { Transmission } from '../types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
transmissions: Transmission[]
|
transmissions: Transmission[]
|
||||||
onDelete: (id: string) => void
|
onDelete: (id: string) => void
|
||||||
onUpdate: (t: Transmission) => void
|
onUpdate: (t: Transmission) => void
|
||||||
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate }) => {
|
const platformIcons: Record<string, React.ReactNode> = {
|
||||||
|
'YouTube': <FaYoutube size={16} color="#FF0000" />,
|
||||||
|
'Facebook': <FaFacebook size={16} color="#1877F2" />,
|
||||||
|
'Twitch': <FaTwitch size={16} color="#9146FF" />,
|
||||||
|
'LinkedIn': <FaLinkedin size={16} color="#0A66C2" />,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate, isLoading }) => {
|
||||||
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
|
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming')
|
||||||
|
|
||||||
const filtered = transmissions // filtering by tab can be implemented later
|
// Filtrado por fechas
|
||||||
|
const filtered = transmissions.filter(t => {
|
||||||
|
if (!t.scheduled) return activeTab === 'upcoming'
|
||||||
|
|
||||||
|
const scheduledDate = new Date(t.scheduled)
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
if (activeTab === 'upcoming') {
|
||||||
|
return scheduledDate >= now
|
||||||
|
} else {
|
||||||
|
return scheduledDate < now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if (!filtered || filtered.length === 0) {
|
if (isLoading) {
|
||||||
return <div className="p-4">No hay transmisiones todavía.</div>
|
return (
|
||||||
|
<div className={styles.transmissionsSection}>
|
||||||
|
<div className={styles.tabContainer}>
|
||||||
|
<button className={`${styles.tabButton} ${styles.activeTab}`}>
|
||||||
|
Próximamente
|
||||||
|
</button>
|
||||||
|
<button className={styles.tabButton}>
|
||||||
|
Anteriores
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<SkeletonTable rows={5} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.transmissionsSection}>
|
||||||
<div className="mb-4 flex items-center gap-3">
|
<div className={styles.tabContainer}>
|
||||||
<button onClick={() => setActiveTab('upcoming')} className={`px-3 py-1 rounded ${activeTab==='upcoming' ? 'bg-indigo-600 text-white' : 'bg-white border'}`}>Próximamente</button>
|
<button
|
||||||
<button onClick={() => setActiveTab('past')} className={`px-3 py-1 rounded ${activeTab==='past' ? 'bg-indigo-600 text-white' : 'bg-white border'}`}>Anteriores</button>
|
onClick={() => setActiveTab('upcoming')}
|
||||||
|
className={`${styles.tabButton} ${activeTab === 'upcoming' ? styles.activeTab : ''}`}
|
||||||
|
>
|
||||||
|
Próximamente
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('past')}
|
||||||
|
className={`${styles.tabButton} ${activeTab === 'past' ? styles.activeTab : ''}`}
|
||||||
|
>
|
||||||
|
Anteriores
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white border rounded">
|
{!filtered || filtered.length === 0 ? (
|
||||||
<table className="w-full">
|
<div className={styles.tableWrapper}>
|
||||||
<thead className="bg-gray-50">
|
<div className={styles.noDataCell}>
|
||||||
<tr>
|
{activeTab === 'upcoming'
|
||||||
<th className="text-left p-3">Título</th>
|
? 'No hay transmisiones programadas todavía.'
|
||||||
<th className="text-left p-3">Plataforma</th>
|
: 'No hay transmisiones anteriores.'
|
||||||
<th className="text-left p-3">Fecha</th>
|
}
|
||||||
<th className="text-right p-3">Acciones</th>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
) : (
|
||||||
<tbody>
|
<div className={styles.tableWrapper}>
|
||||||
{filtered.map(t => (
|
<table className={styles.transmissionsTable}>
|
||||||
<tr key={t.id} className="border-t">
|
<thead>
|
||||||
<td className="p-3">{t.title}</td>
|
<tr>
|
||||||
<td className="p-3">{t.platform}</td>
|
<th className={styles.tableHeader}>Título</th>
|
||||||
<td className="p-3">{t.scheduled || '-'}</td>
|
<th className={styles.tableHeader}>Creado</th>
|
||||||
<td className="p-3 text-right">
|
<th className={styles.tableHeader}>Programado</th>
|
||||||
<button className="px-3 py-1 bg-indigo-600 text-white rounded mr-2">Entrar</button>
|
<th className={styles.tableHeader} style={{ textAlign: 'right' }}></th>
|
||||||
<button onClick={() => onDelete(t.id)} className="px-3 py-1 bg-red-500 text-white rounded mr-2">Eliminar</button>
|
|
||||||
<button onClick={() => onUpdate(t)} className="px-3 py-1 border rounded">Editar</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{filtered.map(t => (
|
||||||
</div>
|
<tr key={t.id} className={styles.tableRow}>
|
||||||
|
<td className={styles.tableCell} colSpan={4}>
|
||||||
|
<div className={styles.tableCard}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<div className={styles.platformAvatar}>
|
||||||
|
<div className={styles.platformIcon}>{platformIcons[t.platform] || platformIcons['YouTube']}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={styles.transmissionTitle}>{t.title}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{t.scheduled || '—'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actionsCell}>
|
||||||
|
<button aria-label={`Entrar al estudio ${t.title}`} className={styles.enterStudioButton} onClick={() => {/* enter studio logic placeholder */}}>
|
||||||
|
Entrar al estudio
|
||||||
|
</button>
|
||||||
|
<button aria-label={`Más opciones ${t.title}`} className={styles.moreOptionsButton}>
|
||||||
|
<MdMoreVert size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,100 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body { font-family: Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; }
|
/* Variables CSS Globales - Light & Dark Theme */
|
||||||
|
:root {
|
||||||
|
--primary-blue: #4f46e5;
|
||||||
|
--primary-blue-hover: #4338ca;
|
||||||
|
--background-color: #f7f8fa;
|
||||||
|
--surface-color: #ffffff;
|
||||||
|
--text-primary: #1f2937;
|
||||||
|
--text-secondary: #6b7280;
|
||||||
|
--border-light: #e5e7eb;
|
||||||
|
--active-bg-light: #eef2ff;
|
||||||
|
--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);
|
||||||
|
--skeleton-base: #e5e7eb;
|
||||||
|
--skeleton-highlight: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-blue: #6366f1;
|
||||||
|
--primary-blue-hover: #4f46e5;
|
||||||
|
--background-color: #0f172a;
|
||||||
|
--surface-color: #1e293b;
|
||||||
|
--text-primary: #f1f5f9;
|
||||||
|
--text-secondary: #cbd5e1;
|
||||||
|
--border-light: #334155;
|
||||||
|
--active-bg-light: #312e81;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-md: 0 1px 3px 0 rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-lg: 0 4px 6px -1px rgba(0, 0, 0, 0.5);
|
||||||
|
--skeleton-base: #334155;
|
||||||
|
--skeleton-highlight: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skeleton Loading Animation */
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -1000px 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 1000px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
animation: shimmer 2s infinite linear;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--skeleton-base) 4%,
|
||||||
|
var(--skeleton-highlight) 25%,
|
||||||
|
var(--skeleton-base) 36%
|
||||||
|
);
|
||||||
|
background-size: 1000px 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar personalizado */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|||||||
4
packages/broadcast-panel/src/vite-env.d.ts
vendored
Normal file
4
packages/broadcast-panel/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: { [key: string]: string }
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user