feat: Mejorar el panel de participantes y agregar funcionalidad de pestañas en el panel derecho
This commit is contained in:
parent
396a803b1c
commit
d3122d64f8
@ -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>
|
||||
)
|
||||
|
||||
@ -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)' }}>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user