58 KiB
INFORME TÉCNICO COMPLEMENTARIO (Análisis de UX del Video)
I. Estructura y Componentes Lógicos (Actualización):
Se confirman los componentes previamente definidos (PageContainer, Sidebar, Header, HeroSection, TransmissionsTable) y se añaden los siguientes componentes lógicos clave basados en la interactividad observada:
Modal.tsx(Componente Reutilizable): Un componente genérico para la superposición de modales.- Jerarquía DOM: Un
divprincipal para la capa de superposición (overlay), y undivanidado para el contenido del modal (modal-content). Un botón de cierre (x) y unh2para el título. - Medidas Clave:
- Overlay:
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 2000; display: flex; justify-content: center; align-items: center;. - Modal Content:
background-color: var(--surface-color); border-radius: 8px; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.15); max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; position: relative; padding: 24px;. - Botón de cierre (x):
position: absolute; top: 16px; right: 16px;.
- Overlay:
- Jerarquía DOM: Un
DropdownMenu.tsx(Componente Reutilizable): Para el menú "Mi cuenta".- Jerarquía DOM: Un
divprincipal que se posiciona absolutamente.ulconlipara los elementos del menú. - Medidas Clave:
- Contenedor:
position: absolute; top: 100%; right: 0; background-color: var(--surface-color); border-radius: 8px; box-shadow: 0px 4px 12px var(--shadow-light); min-width: 250px; z-index: 1500;. - Items:
padding: 10px 16px; font-size: 14px;.
- Contenedor:
- Jerarquía DOM: Un
ToggleSwitch.tsx(Componente Reutilizable): Para las opciones de encendido/apagado.- Jerarquía DOM: Un
labelque contiene uninput[type="checkbox"]oculto y unspanpara el "slider" visual. - Medidas Clave:
width: 40px; height: 22px; border-radius: 11px;para el slider.circleohandlede18pxde diámetro.
- Jerarquía DOM: Un
TabbedContent.tsx(Componente Lógico para Pestañas): Se observa en "Configuración del equipo" y dentro del modal "Agregar destino".- Jerarquía DOM: Contenedor de
buttonpara las pestañas y undivpara el contenido activo.
- Jerarquía DOM: Contenedor de
FormInput.tsx,FormDropdown.tsx,FormCheckbox.tsx(Componentes de Formulario): Se utilizarán para estandarizar los campos de entrada en los modales y páginas de configuración.
II. Métricas Pixel-Perfect (Actualización):
- Tipografía Adicional:
- Títulos de Modal (h2):
font-size: 20px; font-weight: 600;. - Texto dentro de Modales/Formularios:
- Labels:
font-size: 14px; font-weight: 500;. - Input Text:
font-size: 14px; font-weight: 400;.
- Labels:
- Elementos de Dropdown:
font-size: 14px; font-weight: 400;.
- Títulos de Modal (h2):
- Paleta de Colores Adicional:
- Overlay de Modal:
rgba(0, 0, 0, 0.5)- Negro semitransparente. - Fondo de Toggle (off):
var(--border-light)(gris claro). - Fondo de Toggle (on):
var(--primary-blue).
- Overlay de Modal:
- Espaciado y Bordeado Adicional:
- Padding de Modal:
24pxalrededor del contenido. - Input Fields:
padding: 10px 12px; border-radius: 6px; border: 1px solid var(--border-light);. - Toggle Switch: Dimensiones ya mencionadas. El "handle" del toggle tiene
border-radius: 50%; background-color: white; box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);. - Dropdown Separadores:
border-bottom: 1px solid var(--border-light);para separar grupos de elementos en el menú "Mi cuenta". - Tooltip:
background-color: var(--text-primary); color: var(--surface-color); padding: 6px 10px; border-radius: 4px; font-size: 12px;.
- Padding de Modal:
III. Funcionalidad y Scripts Internos (Actualización y Expansión):
Se confirma y expande la funcionalidad descrita previamente, añadiendo los siguientes puntos clave:
-
Manejo Centralizado de Modales:
- Se implementará un estado global (posiblemente a través de
useStateenPageContainero un contexto simple si se requiere compartir estado entre más componentes) para controlar la visibilidad y el tipo de modal a mostrar. - Cada modal tendrá su propia lógica interna para manejar formularios, opciones y acciones, pero su apertura y cierre serán gestionados por el componente padre o el contexto.
- Se observó que los modales pueden tener contenido complejo como tabs (
Agregar destinomodal) y videos (¡Obtén StreamYard On-Air!). - Cierre de Modal: Los modales se cierran al hacer clic en el botón 'x' o al presionar la tecla
Escape.
- Se implementará un estado global (posiblemente a través de
-
Manejo de Dropdown "Mi cuenta":
- El componente
Headergestionará el estado de visibilidad del dropdown "Mi cuenta" utilizandouseState. - Un
useEffectcon un event listener paraclickendocumentse usará para cerrar el dropdown cuando se haga clic fuera de él. - Los elementos del dropdown son enlaces de navegación que pueden cambiar la ruta o abrir sub-secciones dentro de la aplicación.
- El componente
-
Implementación de Toggle Switches:
- Los componentes
ToggleSwitchserán reutilizables y gestionarán su propio estadochecked(useState). - Se pasarán props
onChangepara que los componentes padres puedan reaccionar a los cambios de estado (ej. "Eliminar grabaciones automáticas", "Grabaciones locales").
- Los componentes
-
Navegación en el Sidebar y Pestañas de Contenido:
- La barra lateral sigue un patrón de "enlaces activos", que se actualizará al hacer clic en ellos, reflejando el cambio de ruta en la URL o el contenido cargado en el área principal.
- En secciones como "Configuración del equipo" y el modal "Agregar destino", se utilizará
useStatepara gestionar la pestaña activa, cambiando el contenido renderizado (renderizado condicional).
-
Formularios Interactivos:
- Modales como "Crear transmisión en vivo" incluyen campos de texto, radio buttons y dropdowns. Estos necesitarán gestión de estado (
useState) para sus valores y validación básica(inline validation, if complex). - El campo de destino en "Crear transmisión en vivo" muestra avatares de plataformas y un botón de "añadir", que abre otro modal (
Agregar destino). Esto implica una interacción entre modales o un flujo de apertura en cadena. - Tooltips al Hover: El video muestra un tooltip al pasar el cursor sobre los avatares de destino, indicando el nombre de la plataforma.
- Modales como "Crear transmisión en vivo" incluyen campos de texto, radio buttons y dropdowns. Estos necesitarán gestión de estado (
-
Redirecciones Externas:
- El enlace "Estado del sistema" en el sidebar y el icono de notificación en el header (
Mi cuenta) redirigen a URLs externas, lo que se implementará contarget="_blank" rel="noopener noreferrer"para abrir en una nueva pestaña.
- El enlace "Estado del sistema" en el sidebar y el icono de notificación en el header (
-
Estado de Carga (Loading States):
- Aunque breve, se observa un spinner de carga al navegar a la "Biblioteca", sugiriendo la necesidad de implementar estados de carga para mejorar la experiencia del usuario durante la recuperación de datos.
-
Gestión de Rutas/Vistas:
- Dada la navegación entre "Inicio", "Biblioteca", "Destinos", "Miembros", "Configuración del equipo", "Referidos" y "Estado del sistema", la aplicación requerirá un sistema de enrutamiento (ej. React Router DOM) para manejar las diferentes vistas y mantener el estado de la URL. Esto no se implementará directamente en los componentes dados, pero es una consideración crucial para la aplicación completa.
GENERACIÓN DE CÓDIGO (Componentes Adicionales y Modificaciones)
Para integrar la nueva funcionalidad, se necesitarán algunos ajustes y componentes adicionales.
1. src/components/PageContainer.tsx (Actualización - Añadir control de Modal)
import React, { useState } from 'react';
import styles from './PageContainer.module.css';
import Sidebar from './Sidebar/Sidebar';
import Header from './Header/Header';
import HeroSection from './HeroSection/HeroSection';
import TransmissionsTable from './TransmissionsTable/TransmissionsTable';
import Modal from '../components/Modal/Modal'; // Nuevo componente Modal
import CreateRecordingModal from '../components/Modals/CreateRecordingModal'; // Modal específico
import StreamYardOnAirModal from '../components/Modals/StreamYardOnAirModal'; // Modal específico
import CreateLiveStreamModal from '../components/Modals/CreateLiveStreamModal'; // Modal específico
// Definir tipos para el estado del modal
type ModalType = 'createRecording' | 'streamYardOnAir' | 'createLiveStream' | null;
const PageContainer: React.FC = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [activeModal, setActiveModal] = useState<ModalType>(null);
const [activeSidebarLink, setActiveSidebarLink] = useState<string>('Inicio'); // Para simular navegación
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
const openModal = (modalType: ModalType) => {
setActiveModal(modalType);
};
const closeModal = () => {
setActiveModal(null);
};
const handleSidebarLinkClick = (linkId: string) => {
setActiveSidebarLink(linkId);
// En una aplicación real, esto manejaría el enrutamiento (ej. history.push('/${linkId.toLowerCase()}'))
// Para esta simulación, solo cambiaremos el contenido si es "Inicio"
if (linkId !== 'Inicio') {
console.log(`Navegando a: ${linkId}`);
// Simular carga de contenido diferente aquí si es necesario para otros links
}
};
return (
<div className={`${styles.pageContainer} ${theme === 'dark' ? styles.themeDark : styles.themeLight}`}>
<Sidebar activeLink={activeSidebarLink} onLinkClick={handleSidebarLinkClick} />
<div className={styles.mainContent}>
<Header currentTheme={theme} onToggleTheme={toggleTheme} />
<div className={styles.contentWrapper}>
{/* Renderizado condicional del contenido principal basado en el link activo */}
{activeSidebarLink === 'Inicio' && (
<>
{/* Se pasa la función openModal a HeroSection */}
<HeroSection onOpenModal={openModal} />
<TransmissionsTable />
</>
)}
{activeSidebarLink === 'Configuracion' && (
<div>
<h2 className={styles.tempSectionTitle}>Configuración del equipo</h2>
{/* Aquí iría el componente de configuración con tabs General/Facturación */}
</div>
)}
{activeSidebarLink === 'Referidos' && (
<div>
<h2 className={styles.tempSectionTitle}>Referidos</h2>
{/* Aquí iría el componente de referidos */}
</div>
)}
{activeSidebarLink === 'Biblioteca' && (
<div>
<h2 className={styles.tempSectionTitle}>Biblioteca</h2>
<p className={styles.noContentMessage}>Actualmente no tienes grabaciones.</p>
<p className={styles.noContentMessage}>Tus grabaciones aparecerán aquí</p>
</div>
)}
{activeSidebarLink === 'Destinos' && (
<div>
<h2 className={styles.tempSectionTitle}>Destinos</h2>
{/* Aquí iría el componente de destinos, con botón para abrir modal "Agregar destino" */}
</div>
)}
{activeSidebarLink === 'Miembros' && (
<div>
<h2 className={styles.tempSectionTitle}>Miembros</h2>
{/* Aquí iría el componente de miembros */}
</div>
)}
{/* Otros contenidos para el sidebar */}
</div>
<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>
{/* Renderizado de modales condicional */}
{activeModal === 'createRecording' && (
<Modal title="Crea una grabación" onClose={closeModal}>
<CreateRecordingModal />
</Modal>
)}
{activeModal === 'streamYardOnAir' && (
<Modal title="¡Obtén StreamYard On-Air!" onClose={closeModal}>
<StreamYardOnAirModal />
</Modal>
)}
{activeModal === 'createLiveStream' && (
<Modal title="Crear transmisión en vivo" onClose={closeModal}>
<CreateLiveStreamModal />
</Modal>
)}
</div>
);
};
export default PageContainer;
2. src/components/PageContainer.module.css (Actualización - Añadir estilos genéricos)
/* ... (estilos existentes) ... */
.tempSectionTitle {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 24px;
}
.noContentMessage {
font-size: 16px;
color: var(--text-secondary);
text-align: center;
margin-top: 50px;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
/* ... (estilos existentes) ... */
}
3. src/components/Sidebar/Sidebar.tsx (Actualización - Añadir onLinkClick prop)
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
activeLink: string;
onLinkClick: (linkId: string) => void;
}
const Sidebar: React.FC<SidebarProps> = ({ activeLink, onLinkClick }) => {
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 del sistema', 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}>
<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();
onLinkClick(item.id);
}}
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 : ''}`}>
{/* Condicional para enlaces externos */}
{item.id === 'Estado del sistema' ? (
<a
href="https://status.streamyard.com/"
target="_blank"
rel="noopener noreferrer"
className={styles.navLink}
>
{item.icon}
<span>{item.label}</span>
</a>
) : (
<a
href="#"
onClick={(e) => {
e.preventDefault();
onLinkClick(item.id);
}}
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>
</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/Header/Header.tsx (Actualización - Añadir DropdownMenu y Tooltip)
import React, { useState, useRef, useEffect } from 'react';
import styles from './Header.module.css';
import DropdownMenu from './DropdownMenu'; // Nuevo componente DropdownMenu
interface HeaderProps {
currentTheme: 'light' | 'dark';
onToggleTheme: () => void;
}
const Header: React.FC<HeaderProps> = ({ currentTheme, onToggleTheme }) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const userMenuRef = useRef<HTMLDivElement>(null);
// Cierra el dropdown si se hace clic fuera
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
userMenuRef.current &&
!userMenuRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleUserMenuClick = () => {
setIsDropdownOpen((prev) => !prev);
};
const handleNotificationClick = () => {
window.open('https://new.streamyard.com/', '_blank', 'noopener noreferrer');
};
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"
data-tooltip={currentTheme === 'light' ? 'Modo claro' : 'Modo automático'} // Tooltip dinámico
>
<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>
<span className={styles.tooltip}>Modo claro</span> {/* Tooltip */}
</button>
<button
className={`${styles.themeToggleButton} ${currentTheme === 'dark' ? styles.active : ''}`}
onClick={onToggleTheme}
aria-label="Toggle dark theme"
data-tooltip={currentTheme === 'dark' ? 'Modo oscuro' : 'Modo automático'} // Tooltip dinámico
>
<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>
<span className={styles.tooltip}>Modo oscuro</span> {/* Tooltip */}
</button>
</div>
<button className={styles.notificationButton} onClick={handleNotificationClick}>
<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>
</button>
<div className={styles.userMenuWrapper}>
<div className={styles.userMenu} onClick={handleUserMenuClick} ref={userMenuRef}>
<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>
{isDropdownOpen && <DropdownMenu ref={dropdownRef} />}
</div>
</div>
</header>
);
};
export default Header;
5. src/components/Header/Header.module.css (Actualización - Añadir estilos para Dropdown y Tooltip)
/* ... (estilos existentes) ... */
.themeToggleButton {
/* ... (estilos existentes) ... */
position: relative; /* Para el tooltip */
}
.themeToggleButton .tooltip {
visibility: hidden;
background-color: var(--text-primary);
color: var(--surface-color);
text-align: center;
border-radius: 4px;
padding: 6px 10px;
position: absolute;
z-index: 1;
bottom: -35px; /* Posicionar debajo del botón */
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
white-space: nowrap; /* Evita que el texto se rompa */
}
.themeToggleButton:hover .tooltip {
visibility: visible;
opacity: 1;
}
.userMenuWrapper {
position: relative; /* Contenedor para posicionar el dropdown */
}
.userMenu {
/* ... (estilos existentes) ... */
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
/* ... (estilos existentes) ... */
.themeToggleGroup {
display: none; /* Ocultar el selector de tema en móviles */
}
/* Ajustes para el tooltip en móviles si se decide mostrar */
.themeToggleButton .tooltip {
display: none; /* Ocultar tooltips en móviles por simplicidad */
}
}
6. src/components/Header/DropdownMenu.tsx (Nuevo componente DropdownMenu)
import React, { forwardRef } from 'react';
import styles from './DropdownMenu.module.css';
const DropdownMenu = forwardRef<HTMLDivElement>((props, ref) => {
return (
<div className={styles.dropdownMenu} ref={ref}>
<ul className={styles.menuList}>
<li className={styles.menuItemHeader}>nexttv.stream@gmail.com</li>
<li className={styles.menuItem}><a href="#">Referidos</a></li>
<li className={styles.menuItem}><a href="#">Configuración del equipo</a></li>
<li className={styles.menuItem}><a href="#">Facturación</a></li>
<li className={styles.menuItem}><a href="#">Configuración de la cuenta</a></li>
<li className={styles.menuItemSeparator}></li> {/* Separador visual */}
<li className={styles.menuItem}><a href="#">Contacto</a></li>
<li className={styles.menuItem}><a href="#">Mis tickets de soporte</a></li>
<li className={styles.menuItem}><a href="#">Centro de ayuda</a></li>
<li className={styles.menuItem}><a href="#">Recursos para socios</a></li>
<li className={styles.menuItemSeparator}></li> {/* Separador visual */}
<li className={styles.menuItem}><a href="#">Términos de servicio</a></li>
<li className={styles.menuItem}><a href="#">Política de privacidad</a></li>
<li className={styles.menuItemSeparator}></li> {/* Separador visual */}
<li className={styles.menuItem}><a href="#">Cerrar sesión</a></li>
<li className={styles.menuItemButton}>
<button className={styles.helpButtonDropdown}>
<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-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>
</li>
</ul>
</div>
);
});
export default DropdownMenu;
7. src/components/Header/DropdownMenu.module.css (Nuevo CSS para DropdownMenu)
.dropdownMenu {
position: absolute;
top: 100%; /* Posiciona debajo del elemento padre */
right: 0;
background-color: var(--surface-color);
border-radius: 8px;
box-shadow: 0px 4px 12px var(--shadow-light); /* Sombra para el dropdown */
min-width: 250px;
z-index: 1500;
padding: 8px 0; /* Padding interno */
margin-top: 10px; /* Separación del botón "Mi cuenta" */
}
.menuList {
list-style: none;
padding: 0;
margin: 0;
}
.menuItemHeader {
padding: 10px 16px;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 8px; /* Espacio antes del primer separador */
}
.menuItem a {
display: block;
padding: 10px 16px;
font-size: 14px;
color: var(--text-primary);
text-decoration: none;
transition: background-color 0.15s ease-in-out;
}
.menuItem a:hover {
background-color: var(--active-bg-light); /* Fondo azul claro al hover */
}
.menuItemSeparator {
height: 1px;
background-color: var(--border-light);
margin: 8px 0;
}
.menuItemButton {
padding: 8px 16px;
text-align: center;
}
.helpButtonDropdown {
background-color: var(--primary-blue);
color: #ffffff;
padding: 10px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
width: 100%; /* Ocupa todo el ancho */
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.helpButtonDropdown:hover {
background-color: #146bdc; /* Azul un poco más oscuro al hacer hover */
}
8. src/components/HeroSection/HeroSection.tsx (Actualización - Añadir onOpenModal prop)
import React from 'react';
import styles from './HeroSection.module.css';
import { ModalType } from '../../components/PageContainer'; // Importar ModalType
interface CardProps {
icon: React.ReactNode;
title: string;
onClick: () => void; // Añadir prop onClick
}
const CreateCard: React.FC<CardProps> = ({ icon, title, onClick }) => {
return (
<div className={styles.createCard} onClick={onClick}> {/* Añadir onClick */}
<div className={styles.cardIcon}>{icon}</div>
<p className={styles.cardTitle}>{title}</p>
</div>
);
};
interface HeroSectionProps {
onOpenModal: (modalType: ModalType) => void;
}
const HeroSection: React.FC<HeroSectionProps> = ({ onOpenModal }) => {
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"
onClick={() => onOpenModal('createLiveStream')} // Abrir modal específico
/>
<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"
onClick={() => onOpenModal('createRecording')} // Abrir modal específico
/>
<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"
onClick={() => onOpenModal('streamYardOnAir')} // Abrir modal específico
/>
</div>
</section>
);
};
export default HeroSection;
9. src/components/Modal/Modal.tsx (Nuevo componente genérico de Modal)
import React, { useEffect } from 'react';
import styles from './Modal.module.css';
interface ModalProps {
title: string;
onClose: () => void;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ title, onClose, children }) => {
// Cierra el modal al presionar la tecla Escape
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
return (
<div className={styles.modalOverlay} onClick={onClose}>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}> {/* Evita cerrar al hacer clic dentro */}
<button className={styles.closeButton} onClick={onClose} aria-label="Cerrar">
<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-x">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<h2 className={styles.modalTitle}>{title}</h2>
<div className={styles.modalBody}>
{children}
</div>
</div>
</div>
);
};
export default Modal;
10. src/components/Modal/Modal.module.css (Nuevo CSS para Modal)
.modalOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Overlay semitransparente */
display: flex;
justify-content: center;
align-items: center;
z-index: 2000; /* Asegura que esté por encima de todo */
backdrop-filter: blur(2px); /* Efecto de desenfoque */
}
.modalContent {
background-color: var(--surface-color);
border-radius: 8px;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.15); /* Sombra más prominente */
max-width: 500px; /* Ancho máximo */
width: 90%; /* Ajuste responsivo */
max-height: 90vh; /* Altura máxima con scroll */
overflow-y: auto;
position: relative;
padding: 24px;
}
.closeButton {
position: absolute;
top: 16px;
right: 16px;
background-color: transparent;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 0;
width: 32px;
height: 32px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
transition: background-color 0.2s ease-in-out;
}
.closeButton:hover {
background-color: var(--border-light);
}
.modalTitle {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 24px;
margin-top: 0;
padding-right: 40px; /* Espacio para el botón de cerrar */
}
.modalBody {
/* Estilos para el contenido dentro del modal */
font-size: 14px;
color: var(--text-primary);
line-height: 1.6;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.modalContent {
width: 95%;
padding: 16px;
}
.modalTitle {
font-size: 18px;
margin-bottom: 16px;
}
.closeButton {
top: 10px;
right: 10px;
width: 28px;
height: 28px;
}
}
11. src/components/Forms/ToggleSwitch.tsx (Nuevo componente ToggleSwitch)
import React from 'react';
import styles from './ToggleSwitch.module.css';
interface ToggleSwitchProps {
id: string;
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
description?: string;
}
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({ id, label, checked, onChange, description }) => {
return (
<div className={styles.toggleWrapper}>
<label htmlFor={id} className={styles.toggleLabel}>
{label}
{description && <span className={styles.toggleDescription}>{description}</span>}
<input
type="checkbox"
id={id}
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className={styles.toggleInput}
/>
<span className={styles.toggleSlider}></span>
</label>
</div>
);
};
export default ToggleSwitch;
12. src/components/Forms/ToggleSwitch.module.css (Nuevo CSS para ToggleSwitch)
.toggleWrapper {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.toggleLabel {
display: flex;
align-items: center;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
position: relative;
}
.toggleDescription {
font-size: 12px;
color: var(--text-secondary);
margin-left: 8px;
flex-basis: 100%; /* Permite que la descripción ocupe su propio espacio si es larga */
}
.toggleInput {
opacity: 0;
width: 0;
height: 0;
}
.toggleSlider {
position: relative;
display: inline-block;
width: 40px; /* Ancho del slider */
height: 22px; /* Altura del slider */
background-color: var(--border-light); /* Fondo cuando está OFF */
border-radius: 11px; /* Forma de píldora */
transition: background-color 0.2s ease;
margin-left: 12px;
flex-shrink: 0; /* Evita que el slider se encoja */
}
.toggleSlider:before {
content: "";
position: absolute;
height: 18px; /* Diámetro del "handle" */
width: 18px; /* Diámetro del "handle" */
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 50%;
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); /* Sombra del handle */
}
.toggleInput:checked + .toggleSlider {
background-color: var(--primary-blue); /* Fondo cuando está ON */
}
.toggleInput:checked + .toggleSlider:before {
transform: translateX(18px); /* Mueve el handle a la derecha */
}
13. src/components/Modals/CreateRecordingModal.tsx (Nuevo componente para modal "Crea una grabación")
import React, { useState } from 'react';
import styles from './ModalContent.module.css'; // Usar un CSS genérico para contenido de modal
import ToggleSwitch from '../Forms/ToggleSwitch';
const CreateRecordingModal: React.FC = () => {
const [recordingTitle, setRecordingTitle] = useState('');
const [localRecordings, setLocalRecordings] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Crear grabación:', { recordingTitle, localRecordings });
// Lógica para crear grabación
};
return (
<form onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<label htmlFor="recordingTitle" className={styles.formLabel}>Título de la grabación</label>
<input
type="text"
id="recordingTitle"
className={styles.formInput}
value={recordingTitle}
onChange={(e) => setRecordingTitle(e.target.value)}
placeholder="Mi nueva grabación"
required
/>
</div>
<ToggleSwitch
id="localRecordings"
label="Grabaciones locales?"
description="Grabaciones de la mejor calidad con archivos de audio y video individuales para cada participante, necesario para las grabaciones en 4K Ultra HD."
checked={localRecordings}
onChange={setLocalRecordings}
/>
<div className={styles.modalActions}>
<button type="submit" className={styles.primaryButton}>Crear grabación</button>
</div>
</form>
);
};
export default CreateRecordingModal;
14. src/components/Modals/StreamYardOnAirModal.tsx (Nuevo componente para modal "Obtén StreamYard On-Air!")
import React from 'react';
import styles from './ModalContent.module.css';
const StreamYardOnAirModal: React.FC = () => {
return (
<div>
<p className={styles.modalDescription}>
Organizo un seminario web, una transmisión en vivo o un evento en StreamYard, o insértalo en tu sitio web. Además, tienes la opción de recopilar direcciones de correo electrónico a través de un formulario de registro.
</p>
{/* Simulación del video */}
<div className={styles.videoPlaceholder}>
<img src="https://static.streamyard.com/images/hero_img.svg" alt="StreamYard On-Air Video" className={styles.videoThumbnail} />
{/* Aquí iría el reproductor de video real */}
</div>
<h3 className={styles.planFeatureTitle}>Advanced</h3>
<p className={styles.planFeatureDescription}>Para profesionales que quieren llevar su contenido a otro nivel.</p>
<ul className={styles.featureList}>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Webinars en vivo
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Alta definición completa (1080p)
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Grabaciones locales en 4K (2160p)
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Logos, Superposiciones, Clips de video, Fondos
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Transmisión múltiple - 8 destinos
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Transmisión y grabación ilimitados
</li>
<li className={styles.featureItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
y mucho más
</li>
</ul>
<div className={styles.modalActions}>
<button className={styles.primaryButton}>Continuar</button>
<a href="#" className={styles.secondaryLink}>Mostrar otros planes</a>
<p className={styles.commitmentText}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={styles.checkIcon}><polyline points="20 6 9 17 4 12"></polyline></svg>
Sin compromisos y fácil de cancelar.
</p>
</div>
</div>
);
};
export default StreamYardOnAirModal;
15. src/components/Modals/CreateLiveStreamModal.tsx (Nuevo componente para modal "Crear transmisión en vivo")
import React, { useState } from 'react';
import styles from './ModalContent.module.css'; // Usar CSS genérico para contenido de modal
const CreateLiveStreamModal: React.FC = () => {
const [source, setSource] = useState<'studio' | 'preRecorded'>('studio');
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [addReferenceMessage, setAddReferenceMessage] = useState(false);
const [privacy, setPrivacy] = useState('Publica');
const [category, setCategory] = useState('');
const [scheduleLater, setScheduleLater] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Crear transmisión en vivo:', {
source, title, description, addReferenceMessage, privacy, category, scheduleLater
});
// Lógica para crear transmisión en vivo
};
return (
<form onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Fuente <span className={styles.infoIcon}>?</span></label>
<div className={styles.radioGroup}>
<label className={styles.radioLabel}>
<input
type="radio"
name="source"
value="studio"
checked={source === 'studio'}
onChange={() => setSource('studio')}
className={styles.radioInput}
/>
Estudio
</label>
<label className={styles.radioLabel}>
<input
type="radio"
name="source"
value="preRecorded"
checked={source === 'preRecorded'}
onChange={() => setSource('preRecorded')}
className={styles.radioInput}
/>
Video pregrabado
</label>
</div>
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Selecciona los destinos</label>
<div className={styles.destinationsRow}>
{/* Avatar de destino simulado */}
<div className={styles.destinationAvatar} data-tooltip="Da Nation Freedom">
<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 className={styles.tooltip}>Da Nation Freedom</span> {/* Tooltip */}
</div>
<button type="button" className={styles.addDestinationButton}>
<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-plus">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<a href="#" className={styles.secondaryLink}>Omitir por ahora</a>
</div>
</div>
<div className={styles.formGroup}>
<label htmlFor="streamTitle" className={styles.formLabel}>Título</label>
<input
type="text"
id="streamTitle"
className={styles.formInput}
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<span className={styles.charCount}>0/100</span>
</div>
<div className={styles.formGroup}>
<label htmlFor="streamDescription" className={styles.formLabel}>Descripción</label>
<textarea
id="streamDescription"
className={styles.formTextarea}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Cuéntanos un poco sobre esta transmisión en vivo"
></textarea>
<span className={styles.charCount}>0/5000</span>
</div>
<div className={styles.formGroup}>
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={addReferenceMessage}
onChange={(e) => setAddReferenceMessage(e.target.checked)}
className={styles.checkboxInput}
/>
Agrega el mensaje de referencia a la descripción <span className={styles.infoIcon}>?</span>
<p className={styles.checkboxDescription}>Gana $25 por cada crédito de referencia exitoso.</p>
</label>
</div>
<div className={styles.formGroup}>
<label htmlFor="privacy" className={styles.formLabel}>Privacidad</label>
<select
id="privacy"
className={styles.formSelect}
value={privacy}
onChange={(e) => setPrivacy(e.target.value)}
>
<option value="Publica">Pública</option>
<option value="Privada">Privada</option>
<option value="NoListada">No listada</option>
</select>
</div>
<div className={styles.formGroup}>
<label htmlFor="category" className={styles.formLabel}>Categoría</label>
<select
id="category"
className={styles.formSelect}
value={category}
onChange={(e) => setCategory(e.target.value)}
>
<option value="">Seleccionar categoría</option>
{/* Opciones de categoría */}
</select>
</div>
<div className={styles.formGroup}>
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={scheduleLater}
onChange={(e) => setScheduleLater(e.target.checked)}
className={styles.checkboxInput}
/>
Programar para más tarde
</label>
</div>
<div className={styles.modalActions}>
<button type="submit" className={styles.primaryButton}>Crear transmisión en vivo</button>
<p className={styles.smallText}>Esta transmisión no se grabará en StreamYard. Para grabar, tendrás que <a href="#" className={styles.secondaryLink}>pasarte a un plan superior</a>.</p>
</div>
</form>
);
};
export default CreateLiveStreamModal;
16. src/components/Modals/ModalContent.module.css (Nuevo CSS genérico para el contenido de los modales de formulario)
.formGroup {
margin-bottom: 20px;
}
.formLabel {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 8px;
display: flex; /* Para el icono de info */
align-items: center;
gap: 4px;
}
.formInput,
.formTextarea,
.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);
box-sizing: border-box; /* Asegura que padding y borde no aumenten el ancho */
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.formInput:focus,
.formTextarea:focus,
.formSelect:focus {
border-color: var(--primary-blue);
box-shadow: 0 0 0 2px rgba(24, 118, 242, 0.2); /* Sombra de foco azul */
outline: none;
}
.formTextarea {
min-height: 80px;
resize: vertical;
}
.charCount {
display: block;
font-size: 12px;
color: var(--text-secondary);
text-align: right;
margin-top: 4px;
}
.radioGroup {
display: flex;
gap: 20px;
}
.radioLabel {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
}
.radioInput {
/* Estilos básicos, se puede personalizar con CSS avanzado para radios custom */
transform: scale(1.1); /* Ligeramente más grande */
accent-color: var(--primary-blue); /* Color del radio button */
}
.checkboxLabel {
display: flex;
align-items: flex-start; /* Alinea el texto con el checkbox */
gap: 8px;
cursor: pointer;
font-size: 14px;
color: var(--text-primary);
}
.checkboxInput {
margin-top: 3px; /* Ajuste visual */
transform: scale(1.1);
accent-color: var(--primary-blue);
flex-shrink: 0; /* Evita que el checkbox se encoja */
}
.checkboxDescription {
font-size: 12px;
color: var(--text-secondary);
margin: 0;
line-height: 1.4;
flex-grow: 1; /* Permite que la descripción ocupe el espacio restante */
}
.modalActions {
margin-top: 30px;
text-align: right;
border-top: 1px solid var(--border-light);
padding-top: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.primaryButton {
background-color: var(--primary-blue);
color: #ffffff;
padding: 12px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease-in-out;
width: auto; /* Para que no ocupe todo el ancho */
align-self: flex-end; /* Alinea a la derecha en columna */
}
.primaryButton:hover {
background-color: #146bdc;
}
.secondaryLink {
color: var(--primary-blue);
text-decoration: none;
font-size: 14px;
font-weight: 500;
align-self: flex-end;
}
.secondaryLink:hover {
text-decoration: underline;
}
.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;
cursor: help; /* Indica que hay más información */
}
.modalDescription {
font-size: 15px;
line-height: 1.6;
color: var(--text-primary);
margin-bottom: 20px;
}
.videoPlaceholder {
background-color: #000; /* Fondo negro para simular el video */
width: 100%;
padding-bottom: 56.25%; /* Ratio 16:9 */
position: relative;
border-radius: 8px;
overflow: hidden; /* Asegura que la imagen no se salga */
margin-bottom: 20px;
}
.videoThumbnail {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.planFeatureTitle {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-top: 24px;
margin-bottom: 8px;
}
.planFeatureDescription {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 20px;
}
.featureList {
list-style: none;
padding: 0;
margin: 0 0 20px 0;
}
.featureItem {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--text-primary);
margin-bottom: 8px;
}
.checkIcon {
color: #28a745; /* Verde para los checks */
flex-shrink: 0;
}
.commitmentText {
font-size: 13px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
align-self: flex-end; /* Alinea a la derecha en columna */
}
.destinationsRow {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.destinationAvatar {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: var(--border-light);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative; /* Para el tooltip */
cursor: pointer;
}
/* Tooltip para avatares de destino */
.destinationAvatar .tooltip {
visibility: hidden;
background-color: var(--text-primary);
color: var(--surface-color);
text-align: center;
border-radius: 4px;
padding: 6px 10px;
position: absolute;
z-index: 10;
bottom: 120%; /* Posicionar encima del avatar */
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
white-space: nowrap;
}
.destinationAvatar:hover .tooltip {
visibility: visible;
opacity: 1;
}
.addDestinationButton {
background-color: transparent;
border: 1px solid var(--border-light);
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease-in-out;
}
.addDestinationButton:hover {
border-color: var(--primary-blue);
color: var(--primary-blue);
background-color: var(--active-bg-light);
}
.smallText {
font-size: 12px;
color: var(--text-secondary);
text-align: right;
}
/* Media Queries para responsividad */
@media (max-width: 768px) {
.formLabel {
font-size: 13px;
}
.formInput,
.formTextarea,
.formSelect,
.radioLabel,
.checkboxLabel {
font-size: 13px;
}
.radioGroup {
flex-direction: column; /* Apilar radios en móviles */
gap: 10px;
}
.charCount, .checkboxDescription, .smallText {
font-size: 11px;
}
.modalActions {
padding-top: 15px;
gap: 10px;
}
.primaryButton, .secondaryLink, .commitmentText {
align-self: center; /* Centrar botones y enlaces en columna en móvil */
width: 100%; /* Ocupar todo el ancho */
text-align: center;
}
.primaryButton {
padding: 10px 15px;
font-size: 13px;
}
.secondaryLink {
font-size: 13px;
}
.featureList {
font-size: 13px;
}
.destinationAvatar {
width: 28px;
height: 28px;
}
.destinationAvatar .tooltip {
display: none; /* Ocultar tooltips en móviles */
}
.addDestinationButton {
width: 32px;
height: 32px;
}
}