AvanzaCast/docs/broadcast_panel.md

1184 lines
44 KiB
Markdown

**INFORME TÉCNICO COMPLETO**
**I. Estructura y Componentes Lógicos:**
La página presenta una estructura de panel de control (`dashboard`) dividida en un diseño de dos columnas principales: una barra lateral de navegación fija a la izquierda y un área de contenido principal dinámica a la derecha.
* **Componentes Lógicos Modulares:**
1. **`PageContainer.tsx`:** Actúa como el contenedor principal, organizando la disposición global.
* **Jerarquía DOM:** Un `div` principal que encapsula una `aside` para la barra lateral y una `main` para el contenido principal.
* **Medidas Clave:**
* Layout: `display: flex;` para posicionar la barra lateral y el contenido.
* Ancho de `aside` (barra lateral): Aproximadamente `260px` (fijo).
* `main` (contenido principal): `flex-grow: 1;` para ocupar el espacio restante.
* El contenido dentro de `main` tiene un `padding` horizontal de aproximadamente `32px`.
* No se observa un `max-width` global para toda la aplicación; el diseño se adapta al ancho de la ventana, con el contenido interno centrado o con padding.
2. **`Sidebar.tsx`:** Contiene la navegación principal de la aplicación.
* **Jerarquía DOM:** `aside` (contenedor) > `div` (logo y título "StreamYard") > `nav` (para enlaces de navegación) > `ul` > `li` (elementos de menú) > `div` (sección de almacenamiento) > `div` (sección de configuración y ayuda).
* **Medidas Clave:**
* Ancho: `260px` (fijo).
* Altura: `100vh` (altura completa de la ventana).
* Fondo: Blanco.
* Items de menú: `padding` vertical de `10px` y horizontal de `20px`.
* Elemento activo: `background-color` (azul claro) y `border-left` (azul primario) de `4px`.
* Separación entre grupos de enlaces y secciones: `margin-top` de `20px` a `30px`.
3. **`Header.tsx`:** La barra superior del área de contenido principal.
* **Jerarquía DOM:** `header` (contenedor) > `div` (botones "Mejora tu plan") > `div` (grupo de iconos y "Mi cuenta").
* **Medidas Clave:**
* Altura: Aproximadamente `64px` (fija).
* Fondo: Blanco.
* Layout: `display: flex; justify-content: flex-end; align-items: center;` (elementos alineados a la derecha, con espacio entre ellos). `padding` horizontal de `32px`.
* Borde inferior sutil: `border-bottom: 1px solid` con color gris claro.
4. **`HeroSection.tsx`:** La sección "Crear", que presenta las opciones principales al usuario.
* **Jerarquía DOM:** `section` (contenedor) > `h2` (título "Crear") > `div` (contenedor de tarjetas) > `div` (tarjeta individual, por ejemplo, "Transmisión en vivo").
* **Medidas Clave:**
* Sistema de Rejilla: `display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;` (para escritorio).
* Tarjetas: `min-height` de aproximadamente `120px` - `150px`. `padding: 20px;`.
* Radio de borde: `border-radius: 8px`.
* Separación: `margin-bottom` de `40px` después de la sección.
5. **`TransmissionsTable.tsx`:** La sección de "Transmisiones y grabaciones".
* **Jerarquía DOM:** `section` (contenedor) > `h2` (título "Transmisiones y grabaciones") > `div` (contenedor de pestañas) > `button` (pestañas "Próximamente" y "Anteriores") > `div` (contenedor de la tabla) > `table` > `thead`, `tbody`, `tr`, `th`, `td`.
* **Medidas Clave:**
* Pestañas: `display: flex;` con `padding: 10px 0; margin-right: 20px;` para cada botón. La pestaña activa tiene `border-bottom: 3px solid` (azul primario).
* Tabla: `width: 100%; border-collapse: collapse;`.
* Celdas de tabla: `padding: 12px 15px;`.
* Filas de tabla: `border-bottom: 1px solid` (gris claro).
* Separación: `margin-bottom` de `30px` después del título de la sección, `margin-bottom` de `20px` después de las pestañas.
**II. Métricas Pixel-Perfect:**
* **Tipografía:**
* **Familia de Fuentes:** 'Inter', 'Roboto', sans-serif (estimado visualmente). Se utilizará 'Inter' como principal.
* **Títulos (H1/H2, por ejemplo, "Crear", "Transmisiones y grabaciones"):**
* `font-size`: `22px`
* `font-weight`: `600` (Semi-bold)
* `line-height`: `1.4`
* **Subtítulos (ej. "Próximamente", "Anteriores"):**
* `font-size`: `16px`
* `font-weight`: `500` (Medium)
* `line-height`: `1.5`
* **Texto Principal (P, elementos de menú, contenido de tabla):**
* `font-size`: `14px`
* `font-weight`: `400` (Regular)
* `line-height`: `1.6`
* **Texto Secundario/Pequeño (ej. "0 de 5 horas", "Agregar más"):**
* `font-size`: `12px`
* `font-weight`: `400` (Regular)
* `line-height`: `1.5`
* **Paleta de Colores (Códigos HEX/RGB estimados):**
* **Fondo de Página (Main Background):** `#F7F8FA` (`rgb(247, 248, 250)`) - Gris muy claro.
* **Fondo de Componentes/Tarjetas/Sidebar/Header:** `#FFFFFF` (`rgb(255, 255, 255)`) - Blanco.
* **Texto Principal (Dark Grey):** `#212121` (`rgb(33, 33, 33)`) - Casi negro.
* **Texto Secundario (Medium Grey):** `#6B7280` (`rgb(107, 114, 128)`) - Gris medio para subtítulos y texto menos prominente.
* **Color de Acento (Primary Blue):** `#1876F2` (`rgb(24, 118, 242)`) - Azul vibrante para elementos interactivos.
* **Fondo Activo de Sidebar/Hover:** `#EBF3FF` (`rgb(235, 243, 255)`) - Azul muy claro.
* **Bordes/Divisores (Light Grey):** `#E5E7EB` (`rgb(229, 231, 235)`) - Gris claro.
* **Sombras (Box Shadow):** `0px 1px 3px rgba(0, 0, 0, 0.08)` (sutil para tarjetas y botón flotante).
* **Color de Iconos (Grey):** `#6B7280`
* **Espaciado y Bordeado:**
* **Separación vertical entre secciones:** `margin-top: 32px;` o `padding-top: 32px;`.
* **Padding horizontal global en contenido principal:** `32px`.
* **Padding vertical en Header:** `16px`.
* **Radius de Borde:**
* Botones ("Mejora tu plan", "Entrar al estudio", "Ayuda"): `border-radius: 6px`.
* Tarjetas ("Crear" section): `border-radius: 8px`.
* Alternadores (Theme Toggle): `border-radius: 9999px` (píldora).
**III. Funcionalidad y Scripts Internos:**
* **Funcionalidad del Aplicativo Web:**
1. **Navegación Interactiva del Sidebar:** Los elementos del menú en la barra lateral son clicables. El elemento actualmente seleccionado ("Inicio") se destaca con un fondo azul claro y un borde azul a la izquierda para indicar su estado activo.
2. **Toggle de Tema (Modo Claro/Oscuro):** En la esquina superior derecha del área de contenido principal, hay un control que permite al usuario alternar entre modos de visualización (claro y oscuro), representado por iconos de sol y luna. El modo "claro" (sol) está activo por defecto.
3. **Pestañas de Contenido (Transmisiones y Grabaciones):** La sección "Transmisiones y grabaciones" contiene dos pestañas ("Próximamente" y "Anteriores"). Al hacer clic en cada pestaña, el contenido de la tabla subyacente debe actualizarse para mostrar las transmisiones correspondientes a la selección. La pestaña "Próximamente" está activa.
4. **Botón de Acción Flotante ("Ayuda"):** Un botón persistente en la parte inferior derecha de la pantalla que ofrece acceso rápido a la ayuda o soporte. Este botón tiene una sombra para darle un efecto elevado.
5. **Estados de Interacción (Hover/Active):** Botones, enlaces de navegación y tarjetas presentan cambios visuales (ej. cambio de color de fondo, elevación de sombra) al pasar el cursor (hover) y al hacer clic (active) para proporcionar retroalimentación al usuario.
* **Scripts Internos (Comportamiento):**
1. **Manejo de Estado para Navegación Activa:** Se utilizará `useState` en el componente `Sidebar` para controlar el `id` o la `ruta` del elemento de navegación activo, aplicando dinámicamente las clases CSS (`styles.activeLink`) al elemento seleccionado.
2. **Lógica de Toggle de Tema:** Un `useState` en `PageContainer` o un contexto global para gestionar el estado del tema (`light`/`dark`). Se aplicará una clase CSS (`.theme-dark` o similar) al `body` o al contenedor principal para modificar los colores a través de variables CSS (ej. `--background-color`, `--text-color`).
3. **Manejo de Estado para Pestañas:** Dentro del componente `TransmissionsTable`, se utilizará `useState` para mantener el control de la pestaña activa. Esto determinará qué conjunto de datos de transmisiones se renderiza en la tabla.
4. **Renderizado Condicional y Mapeo de Datos:** El contenido de la tabla en `TransmissionsTable` se renderizará condicionalmente o mediante `.map()` basándose en el estado de la pestaña activa y un array de objetos de transmisiones.
5. **Event Handlers:** Funciones de `onClick` serán implementadas en los elementos interactivos (enlaces de navegación, botones de tema, pestañas) para actualizar los estados correspondientes y desencadenar los cambios en la UI.
6. **Desplazamiento Suave (Scroll Behavior):** Aunque no es explícitamente interactivo, la barra lateral debe permanecer fija mientras el contenido principal es desplazable. El botón "Ayuda" también se fijará en la esquina inferior derecha (`position: fixed`).
---
**GENERACIÓN DE CÓDIGO**
A continuación, se presenta el código para los componentes React/TypeScript y sus módulos CSS, siguiendo las métricas y la funcionalidad descritas en el informe técnico. Se incluirá un componente `Sidebar` adicional por su relevancia estructural en el diseño.
**1. `src/components/PageContainer.tsx`**
```tsx
import React, { useState } from 'react';
import styles from './PageContainer.module.css';
import Sidebar from './Sidebar/Sidebar'; // Assuming Sidebar component
import Header from './Header/Header'; // Assuming Header component
import HeroSection from './HeroSection/HeroSection'; // Assuming HeroSection component
import TransmissionsTable from './TransmissionsTable/TransmissionsTable'; // Assuming TransmissionsTable component
const PageContainer: React.FC = () => {
// Estado para manejar el tema (claro/oscuro)
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
// Aplica la clase del tema al body o al contenedor principal
// En un proyecto real, esto podría hacerse con un contexto o CSS variables
// document.body.className = theme === 'dark' ? 'theme-dark' : 'theme-light';
return (
<div className={`${styles.pageContainer} ${theme === 'dark' ? styles.themeDark : styles.themeLight}`}>
{/* Sidebar - componente lateral de navegación */}
<Sidebar activeLink="Inicio" />
{/* Main Content Area */}
<div className={styles.mainContent}>
{/* Header - barra superior del contenido principal */}
<Header currentTheme={theme} onToggleTheme={toggleTheme} />
<div className={styles.contentWrapper}>
{/* Hero Section - sección "Crear" */}
<HeroSection />
{/* Transmissions Table - sección de tablas */}
<TransmissionsTable />
</div>
{/* Botón flotante de Ayuda */}
<button className={styles.helpButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-help-circle">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
Ayuda
</button>
</div>
</div>
);
};
export default PageContainer;
```
**2. `src/components/PageContainer.module.css`**
```css
/* Variables CSS para el tema */
:root {
--background-color: #f7f8fa; /* Gris muy claro para el fondo de la página */
--surface-color: #ffffff; /* Blanco para componentes/tarjetas */
--text-primary: #212121; /* Gris oscuro casi negro */
--text-secondary: #6b7280; /* Gris medio para texto secundario */
--primary-blue: #1876f2; /* Azul vibrante */
--active-bg-light: #ebf3ff; /* Azul muy claro para activo/hover */
--border-light: #e5e7eb; /* Gris claro para bordes */
--shadow-light: rgba(0, 0, 0, 0.08); /* Sombra sutil */
}
/* Tema oscuro (ejemplo, no completamente implementado en la imagen) */
.themeDark {
--background-color: #1a1a1a;
--surface-color: #2c2c2c;
--text-primary: #f0f0f0;
--text-secondary: #a0a0a0;
--border-light: #444;
/* Otros colores oscuros */
}
.pageContainer {
display: flex;
min-height: 100vh; /* Asegura que ocupe al menos la altura de la ventana */
background-color: var(--background-color);
font-family: 'Inter', 'Roboto', sans-serif; /* Familia de fuentes */
color: var(--text-primary);
}
.mainContent {
flex-grow: 1; /* Ocupa el espacio restante */
display: flex;
flex-direction: column;
position: relative; /* Para el botón flotante */
background-color: var(--background-color); /* Fondo del área de contenido */
overflow-y: auto; /* Permite el scroll vertical en el contenido principal */
}
.contentWrapper {
padding: 32px; /* Padding global para el contenido principal */
flex-grow: 1;
}
.helpButton {
position: fixed;
bottom: 24px;
right: 24px;
background-color: var(--primary-blue);
color: #ffffff;
padding: 12px 20px;
border: none;
border-radius: 6px;
box-shadow: 0px 4px 12px var(--shadow-light); /* Sombra para elevarlo */
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
z-index: 1000; /* Asegura que esté por encima de otros elementos */
}
.helpButton:hover {
background-color: #146bdc; /* Azul un poco más oscuro al hacer hover */
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.pageContainer {
flex-direction: column; /* La barra lateral se apilará en móviles */
}
.mainContent {
width: 100%; /* El contenido principal ocupa todo el ancho */
}
.contentWrapper {
padding: 20px; /* Reducir padding en móviles */
}
.helpButton {
bottom: 16px;
right: 16px;
padding: 10px 16px;
}
}
```
**3. `src/components/Sidebar/Sidebar.tsx`**
```tsx
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
activeLink: string;
}
const Sidebar: React.FC<SidebarProps> = ({ activeLink }) => {
const navItems = [
{ id: 'Inicio', label: 'Inicio', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg> },
{ id: 'Biblioteca', label: 'Biblioteca', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-video"><path d="M23 7l-7 5V2l7 5z"></path><path d="M22 17H7a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h15v16z"></path></svg> },
{ id: 'Destinos', label: 'Destinos', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-share-2"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg> },
{ id: 'Miembros', label: 'Miembros', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> },
];
const secondaryItems = [
{ id: 'Referidos', label: 'Referidos', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-gift"><polyline points="20 12 20 22 4 22 4 12"></polyline><rect x="2" y="7" width="20" height="5"></rect><line x1="12" y1="22" x2="12" y2="7"></line><path d="M12 7H7.5a2.5 2 0 0 1 0-5C11 2 12 7 12 7z"></path><path d="M12 7h4.5a2.5 2 0 0 0 0-5C13 2 12 7 12 7z"></path></svg> },
{ id: 'Configuracion', label: 'Configuración del equipo', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0-.33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> },
{ id: 'Estado', label: 'Estado del sistema', icon: <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-bar-chart-2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg> },
];
return (
<aside className={styles.sidebar}>
<div className={styles.logoSection}>
{/* Usar una imagen real o un SVG para el logo de StreamYard */}
<img src="https://static.streamyard.com/images/logo-icon.svg" alt="StreamYard Logo" className={styles.logoIcon} />
<span className={styles.logoText}>StreamYard</span>
</div>
<nav className={styles.navMenu}>
<ul className={styles.navList}>
{navItems.map((item) => (
<li key={item.id} className={`${styles.navItem} ${activeLink === item.id ? styles.activeLink : ''}`}>
<a href="#" onClick={(e) => e.preventDefault()} className={styles.navLink}>
{item.icon}
<span>{item.label}</span>
</a>
</li>
))}
</ul>
<div className={styles.secondaryNavGroup}>
<ul className={styles.navList}>
{secondaryItems.map((item) => (
<li key={item.id} className={`${styles.navItem} ${activeLink === item.id ? styles.activeLink : ''}`}>
<a href="#" onClick={(e) => e.preventDefault()} className={styles.navLink}>
{item.icon}
<span>{item.label}</span>
</a>
</li>
))}
</ul>
</div>
</nav>
<div className={styles.storageInfo}>
<h4 className={styles.storageTitle}>Almacenamiento <span className={styles.infoIcon}>?</span></h4>
<div className={styles.progressBarContainer}>
<div className={styles.progressBarFill} style={{ width: '0%' }}></div> {/* Simula el progreso */}
</div>
<p className={styles.storageUsage}>0 de 5 horas</p>
<a href="#" className={styles.addMoreLink}>Agregar más</a>
</div>
</aside>
);
};
export default Sidebar;
```
**4. `src/components/Sidebar/Sidebar.module.css`**
```css
.sidebar {
width: 260px; /* Ancho fijo */
min-width: 260px;
height: 100vh; /* Ocupa toda la altura */
background-color: var(--surface-color);
padding: 20px 0; /* Padding vertical */
box-shadow: 1px 0 5px rgba(0, 0, 0, 0.05); /* Sombra sutil a la derecha */
display: flex;
flex-direction: column;
overflow-y: auto; /* Para permitir scroll si el contenido es largo */
}
.logoSection {
display: flex;
align-items: center;
padding: 0 20px 20px; /* Padding bottom para separación */
border-bottom: 1px solid var(--border-light); /* Separador sutil */
margin-bottom: 20px;
}
.logoIcon {
width: 32px;
height: 32px;
margin-right: 10px;
}
.logoText {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.navMenu {
flex-grow: 1; /* Permite que el menú crezca y ocupe el espacio */
}
.navList {
list-style: none;
padding: 0;
margin: 0;
}
.navItem {
margin-bottom: 4px; /* Pequeña separación entre items */
}
.navLink {
display: flex;
align-items: center;
padding: 10px 20px;
color: var(--text-secondary); /* Color por defecto */
text-decoration: none;
font-size: 14px;
font-weight: 500;
border-left: 4px solid transparent; /* Borde transparente por defecto */
transition: all 0.2s ease-in-out;
gap: 10px;
}
.navLink svg {
color: var(--text-secondary); /* Color de icono por defecto */
}
.navLink:hover {
background-color: var(--active-bg-light); /* Fondo azul claro al hover */
color: var(--primary-blue); /* Texto azul al hover */
}
.navLink:hover svg {
color: var(--primary-blue); /* Icono azul al hover */
}
.activeLink .navLink {
background-color: var(--active-bg-light); /* Fondo azul claro para activo */
color: var(--primary-blue); /* Texto azul para activo */
border-left-color: var(--primary-blue); /* Borde azul para activo */
}
.activeLink .navLink svg {
color: var(--primary-blue); /* Icono azul para activo */
}
.secondaryNavGroup {
margin-top: 30px; /* Separación para el segundo grupo */
padding-top: 20px;
border-top: 1px solid var(--border-light);
}
.storageInfo {
padding: 20px;
margin-top: auto; /* Empuja el almacenamiento al final */
border-top: 1px solid var(--border-light); /* Separador */
}
.storageTitle {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 4px;
}
.infoIcon {
background-color: var(--border-light);
border-radius: 50%;
width: 16px;
height: 16px;
display: flex;
justify-content: center;
align-items: center;
font-size: 10px;
color: var(--text-secondary);
font-weight: 500;
}
.progressBarContainer {
width: 100%;
height: 6px;
background-color: var(--border-light);
border-radius: 3px;
margin-bottom: 8px;
}
.progressBarFill {
height: 100%;
background-color: var(--primary-blue);
border-radius: 3px;
width: 0%; /* Se controlará con JS para el progreso real */
}
.storageUsage {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.addMoreLink {
font-size: 12px;
color: var(--primary-blue);
text-decoration: none;
font-weight: 500;
}
.addMoreLink:hover {
text-decoration: underline;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.sidebar {
width: 100%; /* Ocupa todo el ancho en móviles */
height: auto; /* Altura automática */
padding: 10px 0;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.05); /* Sombra inferior */
position: relative;
z-index: 100; /* Asegura que esté encima de otros elementos */
}
.navMenu {
display: none; /* Ocultar el menú de navegación principal en móvil por defecto */
/* En un caso real, aquí iría la lógica para un menú hamburguesa */
}
.logoSection {
padding: 10px 20px;
border-bottom: none;
margin-bottom: 0;
justify-content: center; /* Centrar logo en móvil */
}
.storageInfo {
display: none; /* Ocultar info de almacenamiento en móvil */
}
}
```
**5. `src/components/Header/Header.tsx`**
```tsx
import React from 'react';
import styles from './Header.module.css';
interface HeaderProps {
currentTheme: 'light' | 'dark';
onToggleTheme: () => void;
}
const Header: React.FC<HeaderProps> = ({ currentTheme, onToggleTheme }) => {
return (
<header className={styles.header}>
<div className={styles.headerActions}>
<button className={styles.planButton}>Mejora tu plan</button>
<div className={styles.themeToggleGroup}>
<button
className={`${styles.themeToggleButton} ${currentTheme === 'light' ? styles.active : ''}`}
onClick={onToggleTheme}
aria-label="Toggle light theme"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<button
className={`${styles.themeToggleButton} ${currentTheme === 'dark' ? styles.active : ''}`}
onClick={onToggleTheme}
aria-label="Toggle dark theme"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-moon">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</div>
<button className={styles.notificationButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-bell">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
<span className={styles.notificationDot}></span> {/* Punto de notificación */}
</button>
<div className={styles.userMenu}>
<span>Mi cuenta</span>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-chevron-down">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</div>
</header>
);
};
export default Header;
```
**6. `src/components/Header/Header.module.css`**
```css
.header {
height: 64px; /* Altura fija del header */
background-color: var(--surface-color);
border-bottom: 1px solid var(--border-light); /* Borde inferior sutil */
display: flex;
justify-content: flex-end; /* Alinea el contenido a la derecha */
align-items: center;
padding: 0 32px; /* Padding horizontal */
flex-shrink: 0; /* Previene que el header se encoja */
}
.headerActions {
display: flex;
align-items: center;
gap: 20px; /* Espacio entre los grupos de elementos */
}
.planButton {
background-color: transparent;
color: var(--primary-blue);
border: 1px solid var(--primary-blue);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease-in-out;
}
.planButton:hover {
background-color: var(--primary-blue);
color: #ffffff;
}
.themeToggleGroup {
display: flex;
background-color: var(--background-color); /* Fondo gris claro */
border-radius: 9999px; /* Forma de píldora */
padding: 4px;
gap: 4px;
}
.themeToggleButton {
background-color: transparent;
border: none;
border-radius: 9999px; /* Forma redonda */
padding: 8px;
cursor: pointer;
color: var(--text-secondary); /* Icono gris por defecto */
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.themeToggleButton.active {
background-color: var(--surface-color); /* Fondo blanco para el activo */
color: var(--text-primary); /* Icono oscuro para el activo */
box-shadow: 0px 1px 3px var(--shadow-light); /* Sombra sutil para el activo */
}
.themeToggleButton:hover:not(.active) {
background-color: var(--border-light); /* Fondo ligeramente más oscuro al hover */
}
.notificationButton {
background-color: transparent;
border: none;
cursor: pointer;
color: var(--text-secondary);
position: relative;
padding: 0; /* Eliminar padding para que el icono se centre mejor */
display: flex; /* Asegura que el SVG se centre */
align-items: center;
justify-content: center;
height: 36px; /* Ajusta la altura para que coincida con otros elementos interactivos */
width: 36px;
border-radius: 50%;
transition: background-color 0.2s ease-in-out;
}
.notificationButton:hover {
background-color: var(--border-light);
}
.notificationDot {
position: absolute;
top: 6px;
right: 6px;
width: 8px;
height: 8px;
background-color: var(--primary-blue); /* Color del punto de notificación */
border-radius: 50%;
border: 1px solid var(--surface-color); /* Pequeño borde blanco */
}
.userMenu {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
}
.userMenu svg {
color: var(--text-secondary);
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.header {
padding: 0 16px;
height: 56px;
justify-content: space-between; /* Alinear elementos al principio y al final */
}
.headerActions {
gap: 10px;
}
.planButton {
padding: 6px 12px;
font-size: 13px;
}
.themeToggleGroup {
display: none; /* Ocultar el selector de tema en móviles para simplificar */
}
.notificationButton, .userMenu {
/* Ajustes menores si es necesario */
}
}
```
**7. `src/components/HeroSection/HeroSection.tsx`**
```tsx
import React from 'react';
import styles from './HeroSection.module.css';
interface CardProps {
icon: React.ReactNode;
title: string;
}
const CreateCard: React.FC<CardProps> = ({ icon, title }) => {
return (
<div className={styles.createCard}>
<div className={styles.cardIcon}>{icon}</div>
<p className={styles.cardTitle}>{title}</p>
</div>
);
};
const HeroSection: React.FC = () => {
return (
<section className={styles.heroSection}>
<h2 className={styles.sectionTitle}>Crear</h2>
<div className={styles.cardGrid}>
<CreateCard
icon={<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-video"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg>}
title="Transmisión en vivo"
/>
<CreateCard
icon={<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg>}
title="Grabación"
/>
<CreateCard
icon={<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-monitor"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>}
title="Seminario web On-Air"
/>
</div>
</section>
);
};
export default HeroSection;
```
**8. `src/components/HeroSection/HeroSection.module.css`**
```css
.heroSection {
margin-bottom: 40px; /* Separación con la siguiente sección */
}
.sectionTitle {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 24px; /* Espacio debajo del título */
}
.cardGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); /* Adaptativo */
gap: 20px; /* Espacio entre las tarjetas */
}
.createCard {
background-color: var(--surface-color);
border: 1px solid var(--border-light);
border-radius: 8px;
padding: 20px;
display: flex;
flex-direction: column;
align-items: flex-start; /* Alinea los elementos a la izquierda */
cursor: pointer;
transition: all 0.2s ease-in-out;
min-height: 120px; /* Altura mínima para la tarjeta */
box-shadow: 0px 1px 3px var(--shadow-light); /* Sombra sutil */
}
.createCard:hover {
border-color: var(--primary-blue); /* Borde azul al hover */
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.12); /* Sombra más pronunciada */
}
.cardIcon {
color: var(--primary-blue); /* Color del icono */
margin-bottom: 12px;
width: 32px;
height: 32px;
}
.cardTitle {
font-size: 16px;
font-weight: 500;
color: var(--text-primary);
margin: 0;
line-height: 1.5;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.heroSection {
margin-bottom: 30px;
}
.sectionTitle {
font-size: 20px;
margin-bottom: 16px;
}
.cardGrid {
grid-template-columns: 1fr; /* Una columna en móviles */
gap: 16px;
}
.createCard {
padding: 16px;
min-height: 100px;
}
.cardIcon {
width: 28px;
height: 28px;
margin-bottom: 8px;
}
.cardTitle {
font-size: 15px;
}
}
```
**9. `src/components/TransmissionsTable/TransmissionsTable.tsx`**
```tsx
import React, { useState } from 'react';
import styles from './TransmissionsTable.module.css';
interface Transmission {
id: string;
title: string;
platformIcon: React.ReactNode;
created: string;
scheduled: string;
}
const dummyTransmissions: Transmission[] = [
{
id: '1',
title: 'Transmision',
platformIcon: <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#FF0000" stroke="none" className="youtube-icon"><path d="M2.5 17.5V6.5L12 12.5L2.5 17.5Z"></path><path d="M22.5 6.5C22.5 6.5 22.5 4.5 20 4.5C17.5 4.5 12.5 4.5 12.5 4.5L12.5 4.5C12.5 4.5 7.5 4.5 5 4.5C2.5 4.5 2.5 6.5 2.5 6.5V17.5C2.5 17.5 2.5 19.5 5 19.5C7.5 19.5 12.5 19.5 12.5 19.5L12.5 19.5C12.5 19.5 17.5 19.5 20 19.5C22.5 19.5 22.5 17.5 22.5 17.5V6.5Z" stroke="#FF0000" strokeWidth="1.5" strokeLinejoin="round"></path></svg>,
created: '01:56',
scheduled: '-',
},
// Más datos de transmisiones pueden ir aquí
];
const TransmissionsTable: React.FC = () => {
const [activeTab, setActiveTab] = useState<'upcoming' | 'past'>('upcoming');
// Datos simulados para las pestañas
const upcomingTransmissions: Transmission[] = dummyTransmissions; // De momento, los mismos datos
const pastTransmissions: Transmission[] = []; // Sin datos pasados por ahora
const displayedTransmissions = activeTab === 'upcoming' ? upcomingTransmissions : pastTransmissions;
return (
<section className={styles.transmissionsSection}>
<h2 className={styles.sectionTitle}>Transmisiones y grabaciones</h2>
<div className={styles.tabContainer}>
<button
className={`${styles.tabButton} ${activeTab === 'upcoming' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('upcoming')}
>
Próximamente
</button>
<button
className={`${styles.tabButton} ${activeTab === 'past' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('past')}
>
Anteriores
</button>
</div>
<div className={styles.tableWrapper}>
<table className={styles.transmissionsTable}>
<thead>
<tr>
<th className={styles.tableHeader}>Título</th>
<th className={styles.tableHeader}>Creada</th>
<th className={styles.tableHeader}>Programado</th>
<th className={styles.tableHeader}>Acciones</th>
</tr>
</thead>
<tbody>
{displayedTransmissions.length > 0 ? (
displayedTransmissions.map((transmission) => (
<tr key={transmission.id} className={styles.tableRow}>
<td className={styles.tableCell}>
<div className={styles.titleCellContent}>
<span className={styles.platformAvatar}>
{/* Avatar simulado con "a" o imagen de usuario */}
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="#E5E7EB" stroke="none" className="avatar"><circle cx="12" cy="12" r="10"></circle><text x="50%" y="50%" dominantBaseline="middle" textAnchor="middle" fill="#6B7280" fontSize="12px">a</text></svg>
</span>
<div className={styles.titleTextContent}>
<span className={styles.transmissionTitle}>{transmission.title}</span>
<span className={styles.transmissionPlatformIcon}>
{transmission.platformIcon}
</span>
</div>
</div>
</td>
<td className={styles.tableCell}>{transmission.created}</td>
<td className={styles.tableCell}>{transmission.scheduled}</td>
<td className={`${styles.tableCell} ${styles.actionsCell}`}>
<button className={styles.enterStudioButton}>Entrar al estudio</button>
<button className={styles.moreOptionsButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-more-horizontal">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
</button>
</td>
</tr>
))
) : (
<tr className={styles.tableRow}>
<td colSpan={4} className={styles.noDataCell}>No hay transmisiones en esta categoría.</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
);
};
export default TransmissionsTable;
```
**10. `src/components/TransmissionsTable/TransmissionsTable.module.css`**
```css
.transmissionsSection {
margin-bottom: 40px; /* Separación con el pie de página o final */
}
.sectionTitle {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 24px;
}
.tabContainer {
display: flex;
border-bottom: 1px solid var(--border-light); /* Borde debajo de las pestañas */
margin-bottom: 20px; /* Espacio debajo del contenedor de pestañas */
}
.tabButton {
background-color: transparent;
border: none;
padding: 10px 0;
margin-right: 20px; /* Espacio entre pestañas */
font-size: 16px;
font-weight: 500;
color: var(--text-secondary);
cursor: pointer;
position: relative;
transition: color 0.2s ease-in-out;
}
.tabButton:hover {
color: var(--primary-blue);
}
.tabButton.activeTab {
color: var(--primary-blue);
border-bottom: 3px solid var(--primary-blue); /* Borde azul para la pestaña activa */
padding-bottom: 7px; /* Ajustar padding para compensar el borde */
}
.tableWrapper {
overflow-x: auto; /* Permite el scroll horizontal en tablas grandes */
background-color: var(--surface-color);
border-radius: 8px;
box-shadow: 0px 1px 3px var(--shadow-light);
border: 1px solid var(--border-light);
}
.transmissionsTable {
width: 100%;
border-collapse: collapse; /* Elimina los espacios entre bordes de celda */
}
.tableHeader {
text-align: left;
padding: 12px 15px;
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
border-bottom: 1px solid var(--border-light);
}
.tableRow {
transition: background-color 0.2s ease-in-out;
}
.tableRow:hover {
background-color: var(--active-bg-light); /* Fondo suave al hover en la fila */
}
.tableCell {
padding: 12px 15px;
font-size: 14px;
color: var(--text-primary);
border-bottom: 1px solid var(--border-light);
}
.tableRow:last-child .tableCell {
border-bottom: none; /* Elimina el borde inferior de la última fila */
}
.titleCellContent {
display: flex;
align-items: center;
gap: 12px;
}
.platformAvatar {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--border-light); /* Fondo para el avatar genérico */
flex-shrink: 0; /* Evita que el avatar se encoja */
}
.transmissionTitle {
font-weight: 500;
color: var(--text-primary);
}
.transmissionPlatformIcon {
margin-left: 8px; /* Espacio entre el título y el icono de la plataforma */
display: inline-flex; /* Para alinear el SVG correctamente */
vertical-align: middle;
}
.transmissionPlatformIcon svg {
width: 16px;
height: 16px;
}
.actionsCell {
white-space: nowrap; /* Evita que los botones se rompan en varias líneas */
text-align: right;
}
.enterStudioButton {
background-color: transparent;
color: var(--primary-blue);
border: 1px solid var(--primary-blue);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease-in-out;
margin-right: 8px;
}
.enterStudioButton:hover {
background-color: var(--primary-blue);
color: #ffffff;
}
.moreOptionsButton {
background-color: transparent;
border: none;
padding: 8px;
border-radius: 6px;
cursor: pointer;
color: var(--text-secondary);
transition: background-color 0.2s ease-in-out;
}
.moreOptionsButton:hover {
background-color: var(--border-light);
}
.noDataCell {
text-align: center;
padding: 40px;
color: var(--text-secondary);
font-style: italic;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.sectionTitle {
font-size: 20px;
margin-bottom: 16px;
}
.tabButton {
font-size: 15px;
margin-right: 15px;
padding-bottom: 5px;
}
.tabButton.activeTab {
padding-bottom: 2px; /* Ajustar por el borde */
}
.tableWrapper {
/* El overflow-x: auto ya maneja el responsive */
}
.tableHeader, .tableCell {
padding: 10px 12px;
font-size: 13px;
}
.titleCellContent {
gap: 8px;
}
.platformAvatar {
width: 28px;
height: 28px;
}
.platformAvatar text {
font-size: 10px;
}
.transmissionTitle {
font-size: 13px;
}
.enterStudioButton {
padding: 6px 12px;
font-size: 13px;
margin-right: 4px;
}
.moreOptionsButton {
padding: 6px;
}
}
```