feat: Mejorar el panel de participantes y agregar funcionalidad de pestañas en el panel derecho

This commit is contained in:
Cesar Mendivil 2025-11-07 17:34:24 -07:00
parent 396a803b1c
commit d3122d64f8
3 changed files with 170 additions and 107 deletions

View File

@ -57,62 +57,79 @@ const ParticipantsPanel: React.FC<ParticipantsPanelProps> = ({ roomName }) => {
const isConnected = (identity: string) => liveParticipants.some((p: any) => p.identity === identity)
return (
<div className="space-y-4">
<div>
<h4 className="text-white text-sm font-semibold mb-2">Conectados</h4>
{liveParticipants.length === 0 ? (
<p className="text-gray-400 text-xs">No hay participantes conectados.</p>
) : (
<div className="space-y-2">
{liveParticipants.map((p: any) => (
<div key={p.sid} className="flex items-center justify-between bg-gray-700 p-2 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-pink-500 to-red-500 flex items-center justify-center text-white">
{p.identity?.charAt(0)?.toUpperCase()}
</div>
<div>
<p className="text-sm text-white truncate">{p.identity}</p>
<p className="text-xs text-gray-400">{p.isLocal ? 'Tú (presentador)' : 'Invitado'}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button className="text-xs px-2 py-1 bg-gray-600 hover:bg-gray-500 rounded text-white">Silenciar</button>
<button className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 rounded text-white">Remover</button>
</div>
</div>
))}
</div>
)}
<div className="flex flex-col h-full">
{/* Header */}
<div className="p-4 border-b border-gray-700">
<h3 className="text-white font-semibold text-sm">Participantes</h3>
<p className="text-gray-400 text-xs mt-1">
Gestiona quién está en el stream
</p>
</div>
<div>
<h4 className="text-white text-sm font-semibold mb-2">Invitados</h4>
{loading ? (
<p className="text-gray-400 text-xs">Cargando...</p>
) : error ? (
<p className="text-red-400 text-xs">Error: {error}</p>
) : invited.length === 0 ? (
<p className="text-gray-400 text-xs">No hay invitados registrados.</p>
) : (
<div className="space-y-2">
{invited.map((inv) => (
<div key={inv.identity} className={`flex items-center justify-between bg-gray-700 p-2 rounded-lg ${isConnected(inv.identity) ? 'border-2 border-green-500' : ''}`}>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gray-600 flex items-center justify-center text-white">
{inv.identity?.charAt(0)?.toUpperCase()}
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div>
<h4 className="text-white text-sm font-semibold mb-3">Conectados</h4>
{liveParticipants.length === 0 ? (
<p className="text-gray-400 text-xs">No hay participantes conectados.</p>
) : (
<div className="space-y-2">
{liveParticipants.map((p: any) => (
<div key={p.sid} className="flex items-center justify-between bg-gray-700 p-3 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-pink-500 to-red-500 flex items-center justify-center text-white font-medium text-sm">
{p.identity?.charAt(0)?.toUpperCase()}
</div>
<div>
<p className="text-sm text-white font-medium truncate">{p.identity}</p>
<p className="text-xs text-gray-400">{p.isLocal ? 'Tú (presentador)' : 'Invitado'}</p>
</div>
</div>
<div>
<p className="text-sm text-white truncate">{inv.identity}</p>
<p className="text-xs text-gray-400">{isConnected(inv.identity) ? 'Conectado' : 'Esperando'}</p>
<div className="flex items-center gap-2">
<button className="text-xs px-2 py-1 bg-gray-600 hover:bg-gray-500 rounded text-white transition-colors">
Silenciar
</button>
<button className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 rounded text-white transition-colors">
Remover
</button>
</div>
</div>
<div className="flex items-center gap-2">
<button className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 rounded text-white">Invitar</button>
))}
</div>
)}
</div>
<div>
<h4 className="text-white text-sm font-semibold mb-3">Invitados</h4>
{loading ? (
<p className="text-gray-400 text-xs">Cargando...</p>
) : error ? (
<p className="text-red-400 text-xs">Error: {error}</p>
) : invited.length === 0 ? (
<p className="text-gray-400 text-xs">No hay invitados registrados.</p>
) : (
<div className="space-y-2">
{invited.map((inv) => (
<div key={inv.identity} className={`flex items-center justify-between bg-gray-700 p-3 rounded-lg ${isConnected(inv.identity) ? 'ring-2 ring-green-500' : ''}`}>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-gray-600 flex items-center justify-center text-white font-medium text-sm">
{inv.identity?.charAt(0)?.toUpperCase()}
</div>
<div>
<p className="text-sm text-white font-medium truncate">{inv.identity}</p>
<p className="text-xs text-gray-400">{isConnected(inv.identity) ? 'Conectado' : 'Esperando'}</p>
</div>
</div>
<div className="flex items-center gap-2">
<button className="text-xs px-2 py-1 bg-blue-600 hover:bg-blue-700 rounded text-white transition-colors">
Invitar
</button>
</div>
</div>
</div>
))}
</div>
)}
))}
</div>
)}
</div>
</div>
</div>
)

View File

@ -4,7 +4,7 @@ import '@livekit/components-styles'
import StudioHeader from './StudioHeader'
import StudioLeftSidebar from './StudioLeftSidebar'
import StudioVideoArea from './StudioVideoArea'
import StudioRightPanel from './StudioRightPanel'
import StudioRightPanel, { TabsColumn, TabType } from './StudioRightPanel'
import StudioControls from './StudioControls'
import PresentationPanel from './PresentationPanel'
import { DEMO_TOKEN } from '../config/demo'
@ -25,6 +25,7 @@ const Studio: React.FC<StudioProps> = ({ userName, roomName }) => {
const [mode, setMode] = useState<'video' | 'audio'>('video')
const [showLeftPanel, setShowLeftPanel] = useState(true)
const [showRightPanel, setShowRightPanel] = useState(true)
const [activeRightTab, setActiveRightTab] = useState<TabType>('brand')
// Utility: heurística ligera para validar token en cliente
const isTokenLikelyValid = (t?: string) => {
@ -332,18 +333,34 @@ const Studio: React.FC<StudioProps> = ({ userName, roomName }) => {
{/* Panel derecho - Ajustes (posición absoluta derecha) */}
<div
className={`absolute right-0 top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
className={`absolute top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
showRightPanel ? 'translate-x-0' : 'translate-x-full'
}`}
style={{ right: '80px' }}
>
<StudioRightPanel roomName={(localStorage.getItem('avanzacast_studio_room') || roomName) as string} />
<StudioRightPanel
roomName={(localStorage.getItem('avanzacast_studio_room') || roomName) as string}
activeTab={activeRightTab}
onChangeTab={(t) => setActiveRightTab(t)}
/>
</div>
{/* Tabs Column externo (modo demo): permanece visible incluso cuando el panel se oculta */}
<div
className="absolute top-0 bottom-0 z-30 pointer-events-auto"
style={{ right: '0px' }}
>
<div className="h-full flex items-start">
<TabsColumn activeTab={activeRightTab} onChangeTab={(t) => setActiveRightTab(t)} />
</div>
</div>
{/* Botón toggle derecho (en el borde izquierdo del panel derecho) */}
{/* Botón toggle derecho (se mueve con el panel) */}
<button
onClick={() => setShowRightPanel(!showRightPanel)}
className="absolute top-1/2 -translate-y-1/2 z-30 transition-all duration-300"
style={{ right: showRightPanel ? '320px' : '0px' }}
style={{ right: showRightPanel ? '400px' : '80px' }}
title={showRightPanel ? 'Ocultar panel derecho' : 'Mostrar panel derecho'}
>
<svg width="16" height="101" viewBox="0 0 16 101" fill="none" xmlns="http://www.w3.org/2000/svg" className="hover:opacity-80 transition-opacity" style={{ transform: 'scaleX(-1)' }}>
@ -465,20 +482,38 @@ const Studio: React.FC<StudioProps> = ({ userName, roomName }) => {
<StudioVideoArea isDemoMode={false} />
</div>
{/* Panel derecho - Ajustes (posición absoluta derecha) */}
<div
className={`absolute right-0 top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
className={`absolute top-0 bottom-0 z-20 transition-transform duration-300 ease-in-out ${
showRightPanel ? 'translate-x-0' : 'translate-x-full'
}`}
style={{ right: '80px' }}
>
<StudioRightPanel roomName={(localStorage.getItem('avanzacast_studio_room') || roomName) as string} />
<StudioRightPanel
roomName={(localStorage.getItem('avanzacast_studio_room') || roomName) as string}
activeTab={activeRightTab}
onChangeTab={(t) => setActiveRightTab(t)}
/>
</div>
{/* Tabs Column externo: permanece visible incluso cuando el panel se oculta */}
<div
className="absolute top-0 bottom-0 z-30 pointer-events-auto"
style={{ right: '0px' }}
>
<div className="h-full flex items-start">
<TabsColumn activeTab={activeRightTab} onChangeTab={(t) => setActiveRightTab(t)} />
</div>
</div>
{/* Botón toggle derecho (en el borde izquierdo del panel derecho) */}
{/* Botón toggle derecho (se mueve con el panel) */}
<button
onClick={() => setShowRightPanel(!showRightPanel)}
className="absolute top-1/2 -translate-y-1/2 z-30 transition-all duration-300"
style={{ right: showRightPanel ? '320px' : '0px' }}
style={{ right: showRightPanel ? '400px' : '80px' }}
title={showRightPanel ? 'Ocultar panel derecho' : 'Mostrar panel derecho'}
>
<svg width="16" height="101" viewBox="0 0 16 101" fill="none" xmlns="http://www.w3.org/2000/svg" className="hover:opacity-80 transition-opacity" style={{ transform: 'scaleX(1)' }}>

View File

@ -14,20 +14,60 @@ import {
import { COLOR_THEMES, DEMO_OVERLAYS, DEMO_BACKGROUNDS, DEMO_SOUNDS } from '../config/demo'
import ParticipantsPanel from './ParticipantsPanel'
type TabType = 'comments' | 'banners' | 'brand' | 'style' | 'notes' | 'participants' | 'chat'
export type TabType = 'comments' | 'banners' | 'brand' | 'style' | 'notes' | 'participants' | 'chat'
const StudioRightPanel = ({ roomName }: { roomName?: string }) => {
const [activeTab, setActiveTab] = useState<TabType>('brand')
// tabs shared entre el panel y el elemento externo (TabsColumn)
const tabs: { id: TabType; icon: any; label: string; badge: number }[] = [
{ id: 'comments', icon: MdComment, label: 'Comentarios', badge: 0 },
{ id: 'banners', icon: MdImage, label: 'Banners', badge: 0 },
{ id: 'brand', icon: MdBrush, label: 'Activos multimedia', badge: 0 },
{ id: 'style', icon: MdBrush, label: 'Estilo', badge: 0 },
{ id: 'notes', icon: MdTimer, label: 'Notas', badge: 0 },
{ id: 'participants', icon: MdPeople, label: 'Personas', badge: 0 },
{ id: 'chat', icon: MdChat, label: 'Chat privado', badge: 0 },
]
const tabs = [
{ id: 'comments' as TabType, icon: MdComment, label: 'Comentarios', badge: 0 },
{ id: 'banners' as TabType, icon: MdImage, label: 'Banners', badge: 0 },
{ id: 'brand' as TabType, icon: MdBrush, label: 'Activos multimedia', badge: 0 },
{ id: 'style' as TabType, icon: MdBrush, label: 'Estilo', badge: 0 },
{ id: 'notes' as TabType, icon: MdTimer, label: 'Notas', badge: 0 },
{ id: 'participants' as TabType, icon: MdPeople, label: 'Personas', badge: 0 },
{ id: 'chat' as TabType, icon: MdChat, label: 'Chat privado', badge: 0 },
]
// Componente reutilizable para renderizar la columna de tabs (puede usarse fuera del panel)
export const TabsColumn: React.FC<{ activeTab: TabType; onChangeTab: (t: TabType) => void }> = ({ activeTab, onChangeTab }) => {
return (
<div role="tablist" className="w-20 bg-gray-800 border-l border-gray-700 flex flex-col">
{tabs.map((tab) => {
const Icon = tab.icon
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
role="tab"
aria-selected={isActive}
aria-controls={`right-panel-content-${tab.id}`}
id={`right-panel-tab-${tab.id}`}
onClick={() => onChangeTab(tab.id)}
className={`relative flex flex-col items-center gap-1 py-4 px-2 border-r-2 transition-all ${
isActive
? 'bg-gray-700 border-pink-500 text-pink-500'
: 'border-transparent text-gray-400 hover:text-white hover:bg-gray-750'
}`}
>
<div className="relative">
<Icon size={24} />
{tab.badge > 0 && (
<div className="absolute -top-1 -right-1 w-4 h-4 bg-pink-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs font-bold">{tab.badge}</span>
</div>
)}
</div>
<span className="text-xs font-medium text-center leading-tight">{tab.label}</span>
</button>
)
})}
</div>
)
}
const StudioRightPanel = ({ roomName, activeTab: activeTabProp, onChangeTab }: { roomName?: string; activeTab?: TabType; onChangeTab?: (t: TabType) => void }) => {
const [internalActiveTab, setInternalActiveTab] = useState<TabType>('brand')
const activeTab = activeTabProp ?? internalActiveTab
const setActiveTab = onChangeTab ?? setInternalActiveTab
return (
<div className="flex h-full">
@ -52,41 +92,7 @@ const StudioRightPanel = ({ roomName }: { roomName?: string }) => {
</aside>
{/* Tabs Column - Vertical tabs como en Streamyard */}
<div role="tablist" className="w-20 bg-gray-800 border-l border-gray-700 flex flex-col">
{tabs.map((tab) => {
const Icon = tab.icon
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
role="tab"
aria-selected={isActive}
aria-controls={`right-panel-content-${tab.id}`}
id={`right-panel-tab-${tab.id}`}
onClick={() => setActiveTab(tab.id)}
className={`relative flex flex-col items-center gap-1 py-4 px-2 border-r-2 transition-all ${
isActive
? 'bg-gray-700 border-pink-500 text-pink-500'
: 'border-transparent text-gray-400 hover:text-white hover:bg-gray-750'
}`}
>
<div className="relative">
<Icon size={24} />
{tab.badge > 0 && (
<div className="absolute -top-1 -right-1 w-4 h-4 bg-pink-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs font-bold">{tab.badge}</span>
</div>
)}
</div>
<span className="text-xs font-medium text-center leading-tight">
{tab.label}
</span>
</button>
)
})}
</div>
</div>
)
}
@ -218,7 +224,12 @@ const NotesTab = () => {
<div className="p-4 border-t border-gray-700">
<div className="flex items-center justify-between text-xs text-gray-400">
<span>{notes.length} caracteres</span>
<button className="text-pink-500 hover:underline">Limpiar</button>
<button
onClick={() => setNotes('')}
className="text-pink-500 hover:underline"
>
Limpiar
</button>
</div>
</div>
</div>