44 KiB
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:
-
PageContainer.tsx: Actúa como el contenedor principal, organizando la disposición global.- Jerarquía DOM: Un
divprincipal que encapsula unaasidepara la barra lateral y unamainpara el contenido principal. - Medidas Clave:
- Layout:
display: flex;para posicionar la barra lateral y el contenido. - Ancho de
aside(barra lateral): Aproximadamente260px(fijo). main(contenido principal):flex-grow: 1;para ocupar el espacio restante.- El contenido dentro de
maintiene unpaddinghorizontal de aproximadamente32px. - No se observa un
max-widthglobal para toda la aplicación; el diseño se adapta al ancho de la ventana, con el contenido interno centrado o con padding.
- Layout:
- Jerarquía DOM: Un
-
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ú:
paddingvertical de10pxy horizontal de20px. - Elemento activo:
background-color(azul claro) yborder-left(azul primario) de4px. - Separación entre grupos de enlaces y secciones:
margin-topde20pxa30px.
- Ancho:
- Jerarquía DOM:
-
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).paddinghorizontal de32px. - Borde inferior sutil:
border-bottom: 1px solidcon color gris claro.
- Altura: Aproximadamente
- Jerarquía DOM:
-
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-heightde aproximadamente120px-150px.padding: 20px;. - Radio de borde:
border-radius: 8px. - Separación:
margin-bottomde40pxdespués de la sección.
- Sistema de Rejilla:
- Jerarquía DOM:
-
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;conpadding: 10px 0; margin-right: 20px;para cada botón. La pestaña activa tieneborder-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-bottomde30pxdespués del título de la sección,margin-bottomde20pxdespués de las pestañas.
- Pestañas:
- Jerarquía DOM:
-
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:22pxfont-weight:600(Semi-bold)line-height:1.4
- Subtítulos (ej. "Próximamente", "Anteriores"):
font-size:16pxfont-weight:500(Medium)line-height:1.5
- Texto Principal (P, elementos de menú, contenido de tabla):
font-size:14pxfont-weight:400(Regular)line-height:1.6
- Texto Secundario/Pequeño (ej. "0 de 5 horas", "Agregar más"):
font-size:12pxfont-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
- Fondo de Página (Main Background):
-
Espaciado y Bordeado:
- Separación vertical entre secciones:
margin-top: 32px;opadding-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).
- Botones ("Mejora tu plan", "Entrar al estudio", "Ayuda"):
- Separación vertical entre secciones:
III. Funcionalidad y Scripts Internos:
-
Funcionalidad del Aplicativo Web:
- 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.
- 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.
- 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.
- 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.
- 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):
- Manejo de Estado para Navegación Activa: Se utilizará
useStateen el componenteSidebarpara controlar elido larutadel elemento de navegación activo, aplicando dinámicamente las clases CSS (styles.activeLink) al elemento seleccionado. - Lógica de Toggle de Tema: Un
useStateenPageContainero un contexto global para gestionar el estado del tema (light/dark). Se aplicará una clase CSS (.theme-darko similar) albodyo al contenedor principal para modificar los colores a través de variables CSS (ej.--background-color,--text-color). - Manejo de Estado para Pestañas: Dentro del componente
TransmissionsTable, se utilizaráuseStatepara mantener el control de la pestaña activa. Esto determinará qué conjunto de datos de transmisiones se renderiza en la tabla. - Renderizado Condicional y Mapeo de Datos: El contenido de la tabla en
TransmissionsTablese renderizará condicionalmente o mediante.map()basándose en el estado de la pestaña activa y un array de objetos de transmisiones. - Event Handlers: Funciones de
onClickserá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. - 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).
- Manejo de Estado para Navegación Activa: Se utilizará
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
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
/* 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
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
.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
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
.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
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
.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
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
.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;
}
}