feat: Integrar LiveKit y actualizar URLs en el panel de transmisión y API
This commit is contained in:
parent
f57ce90c11
commit
396a803b1c
1
package-lock.json
generated
1
package-lock.json
generated
@ -11875,6 +11875,7 @@
|
|||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"livekit-server-sdk": "^2.14.0",
|
||||||
"socket.io": "^4.6.2",
|
"socket.io": "^4.6.2",
|
||||||
"stripe": "^14.9.0",
|
"stripe": "^14.9.0",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"joi": "^17.11.0",
|
"joi": "^17.11.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"livekit-server-sdk": "^2.14.0",
|
||||||
"socket.io": "^4.6.2",
|
"socket.io": "^4.6.2",
|
||||||
"stripe": "^14.9.0",
|
"stripe": "^14.9.0",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
|
|||||||
@ -15,6 +15,9 @@ const allowedOrigins = process.env.FRONTEND_URLS?.split(',') || ['http://localho
|
|||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
if (!allowedOrigins.includes('http://localhost:3020')) allowedOrigins.push('http://localhost:3020')
|
if (!allowedOrigins.includes('http://localhost:3020')) allowedOrigins.push('http://localhost:3020')
|
||||||
if (!allowedOrigins.includes('http://localhost:3021')) allowedOrigins.push('http://localhost:3021')
|
if (!allowedOrigins.includes('http://localhost:3021')) allowedOrigins.push('http://localhost:3021')
|
||||||
|
if (!allowedOrigins.includes('http://localhost:5175')) allowedOrigins.push('http://localhost:5175')
|
||||||
|
if (!allowedOrigins.includes('https://avanzacast-studio.bfzqqk.easypanel.host')) allowedOrigins.push('https://avanzacast-studio.bfzqqk.easypanel.host')
|
||||||
|
if (!allowedOrigins.includes('https://avanzacast-broadcastpanel.bfzqqk.easypanel.host')) allowedOrigins.push('https://avanzacast-broadcastpanel.bfzqqk.easypanel.host')
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
@ -44,6 +47,56 @@ app.get('/api/v1', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// LiveKit token generation endpoint
|
||||||
|
app.get('/api/token', async (req, res) => {
|
||||||
|
const { room, username } = req.query;
|
||||||
|
|
||||||
|
if (!room || typeof room !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Room name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!username || typeof username !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Username is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement actual LiveKit token generation
|
||||||
|
// For now, return a placeholder response
|
||||||
|
const LIVEKIT_API_KEY = process.env.LIVEKIT_API_KEY;
|
||||||
|
const LIVEKIT_API_SECRET = process.env.LIVEKIT_API_SECRET;
|
||||||
|
|
||||||
|
if (!LIVEKIT_API_KEY || !LIVEKIT_API_SECRET) {
|
||||||
|
console.error('⚠️ LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set in environment variables');
|
||||||
|
return res.status(500).json({ error: 'LiveKit credentials not configured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Import AccessToken from livekit-server-sdk
|
||||||
|
const { AccessToken } = await import('livekit-server-sdk');
|
||||||
|
|
||||||
|
const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
|
||||||
|
identity: username,
|
||||||
|
name: username,
|
||||||
|
});
|
||||||
|
|
||||||
|
at.addGrant({
|
||||||
|
room,
|
||||||
|
roomJoin: true,
|
||||||
|
canPublish: true,
|
||||||
|
canSubscribe: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await at.toJwt();
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
token,
|
||||||
|
url: process.env.LIVEKIT_URL || 'ws://localhost:7880',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating LiveKit token:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to generate token' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Minimal LiveKit-related endpoints (placeholder implementation)
|
// Minimal LiveKit-related endpoints (placeholder implementation)
|
||||||
app.get('/api/v1/livekit/rooms', (req, res) => {
|
app.get('/api/v1/livekit/rooms', (req, res) => {
|
||||||
const roomName = typeof req.query.roomName === 'string' ? req.query.roomName : undefined;
|
const roomName = typeof req.query.roomName === 'string' ? req.query.roomName : undefined;
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const Studio: React.FC = () => {
|
|||||||
localStorage.setItem('avanzacast_room', roomName)
|
localStorage.setItem('avanzacast_room', roomName)
|
||||||
|
|
||||||
// Redirigir al studio-panel (puerto 3001)
|
// Redirigir al studio-panel (puerto 3001)
|
||||||
const studioUrl = `http://localhost:3001?user=${encodeURIComponent(userName)}&room=${encodeURIComponent(roomName)}`
|
const studioUrl = `https://avanzacast-studio.bfzqqk.easypanel.host?user=${encodeURIComponent(userName)}&room=${encodeURIComponent(roomName)}`
|
||||||
window.location.href = studioUrl
|
window.location.href = studioUrl
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,7 @@ const TransmissionsTable: React.FC<Props> = ({ transmissions, onDelete, onUpdate
|
|||||||
|
|
||||||
console.log('[BroadcastPanel] Solicitando token:', { room: decodeURIComponent(room), user: decodeURIComponent(user) })
|
console.log('[BroadcastPanel] Solicitando token:', { room: decodeURIComponent(room), user: decodeURIComponent(user) })
|
||||||
|
|
||||||
const TOKEN_SERVER = import.meta.env.VITE_TOKEN_SERVER_URL || 'https://avanzacast-studio.bfzqqk.easypanel.host'
|
const TOKEN_SERVER = import.meta.env.VITE_TOKEN_SERVER_URL || 'https://avanzacast-servertokens.bfzqqk.easypanel.host'
|
||||||
const tokenUrl = `${TOKEN_SERVER.replace(/\/$/, '')}/api/token?room=${room}&username=${user}`
|
const tokenUrl = `${TOKEN_SERVER.replace(/\/$/, '')}/api/token?room=${room}&username=${user}`
|
||||||
const tokenRes = await fetch(tokenUrl)
|
const tokenRes = await fetch(tokenUrl)
|
||||||
if (!tokenRes.ok) throw new Error('No se pudo obtener token')
|
if (!tokenRes.ok) throw new Error('No se pudo obtener token')
|
||||||
|
|||||||
@ -37,6 +37,11 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5175,
|
port: 5175,
|
||||||
host: true,
|
host: true,
|
||||||
|
allowedHosts: [
|
||||||
|
'localhost',
|
||||||
|
'.easypanel.host',
|
||||||
|
'avanzacast-broadcastpanel.bfzqqk.easypanel.host'
|
||||||
|
],
|
||||||
fs: {
|
fs: {
|
||||||
// Allow serving files from the shared folder when mounted in Docker
|
// Allow serving files from the shared folder when mounted in Docker
|
||||||
allow: [
|
allow: [
|
||||||
|
|||||||
@ -7,108 +7,307 @@ import {
|
|||||||
MdQrCode,
|
MdQrCode,
|
||||||
MdTimer,
|
MdTimer,
|
||||||
MdSettings,
|
MdSettings,
|
||||||
MdClose,
|
|
||||||
MdPeople,
|
MdPeople,
|
||||||
|
MdChat,
|
||||||
|
MdComment,
|
||||||
} from 'react-icons/md'
|
} from 'react-icons/md'
|
||||||
import { COLOR_THEMES, DEMO_OVERLAYS, DEMO_BACKGROUNDS, DEMO_SOUNDS } from '../config/demo'
|
import { COLOR_THEMES, DEMO_OVERLAYS, DEMO_BACKGROUNDS, DEMO_SOUNDS } from '../config/demo'
|
||||||
import ParticipantsPanel from './ParticipantsPanel'
|
import ParticipantsPanel from './ParticipantsPanel'
|
||||||
|
|
||||||
type TabType = 'brand' | 'multimedia' | 'participants' | 'sounds' | 'video' | 'qr' | 'countdown' | 'settings'
|
type TabType = 'comments' | 'banners' | 'brand' | 'style' | 'notes' | 'participants' | 'chat'
|
||||||
|
|
||||||
const StudioRightPanel = ({ roomName }: { roomName?: string }) => {
|
const StudioRightPanel = ({ roomName }: { roomName?: string }) => {
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('brand')
|
const [activeTab, setActiveTab] = useState<TabType>('brand')
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'brand' as TabType, icon: MdBrush, label: 'Marca' },
|
{ id: 'comments' as TabType, icon: MdComment, label: 'Comentarios', badge: 0 },
|
||||||
{ id: 'multimedia' as TabType, icon: MdImage, label: 'Multimedia' },
|
{ id: 'banners' as TabType, icon: MdImage, label: 'Banners', badge: 0 },
|
||||||
{ id: 'participants' as TabType, icon: MdPeople, label: 'Personas' },
|
{ id: 'brand' as TabType, icon: MdBrush, label: 'Activos multimedia', badge: 0 },
|
||||||
{ id: 'sounds' as TabType, icon: MdMusicNote, label: 'Sonidos' },
|
{ id: 'style' as TabType, icon: MdBrush, label: 'Estilo', badge: 0 },
|
||||||
{ id: 'video' as TabType, icon: MdVideoLibrary, label: 'Videos' },
|
{ id: 'notes' as TabType, icon: MdTimer, label: 'Notas', badge: 0 },
|
||||||
{ id: 'qr' as TabType, icon: MdQrCode, label: 'QR' },
|
{ id: 'participants' as TabType, icon: MdPeople, label: 'Personas', badge: 0 },
|
||||||
{ id: 'countdown' as TabType, icon: MdTimer, label: 'Cuenta regresiva' },
|
{ id: 'chat' as TabType, icon: MdChat, label: 'Chat privado', badge: 0 },
|
||||||
{ id: 'settings' as TabType, icon: MdSettings, label: 'Ajustes' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if (isCollapsed) {
|
return (
|
||||||
return (
|
<div className="flex h-full">
|
||||||
<div className="w-12 bg-gray-800 border-l border-gray-700 flex flex-col items-center py-4 gap-2">
|
|
||||||
|
|
||||||
|
{/* Content Panel - Aside como en Streamyard */}
|
||||||
|
<aside className="flex-1 bg-gray-800 flex flex-col" style={{ width: '320px' }}>
|
||||||
|
<div
|
||||||
|
id="right-panel-content"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby={`right-panel-tab-${activeTab}`}
|
||||||
|
className="flex-1 flex flex-col h-full"
|
||||||
|
>
|
||||||
|
{activeTab === 'comments' && <CommentsTab />}
|
||||||
|
{activeTab === 'banners' && <BannersTab />}
|
||||||
|
{activeTab === 'brand' && <BrandTab />}
|
||||||
|
{activeTab === 'style' && <StyleTab />}
|
||||||
|
{activeTab === 'notes' && <NotesTab />}
|
||||||
|
{activeTab === 'participants' && <ParticipantsPanel roomName={roomName} />}
|
||||||
|
{activeTab === 'chat' && <ChatTab />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</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) => {
|
{tabs.map((tab) => {
|
||||||
const Icon = tab.icon
|
const Icon = tab.icon
|
||||||
|
const isActive = activeTab === tab.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => {
|
role="tab"
|
||||||
setActiveTab(tab.id)
|
aria-selected={isActive}
|
||||||
setIsCollapsed(false)
|
aria-controls={`right-panel-content-${tab.id}`}
|
||||||
}}
|
id={`right-panel-tab-${tab.id}`}
|
||||||
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
|
onClick={() => setActiveTab(tab.id)}
|
||||||
title={tab.label}
|
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'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<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>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-80 h-full bg-gray-800 border-l border-gray-700 flex flex-col">
|
|
||||||
{/* Header con tabs */}
|
|
||||||
<div className="border-b border-gray-700">
|
|
||||||
<div className="flex items-center justify-between px-3 py-2">
|
|
||||||
<h3 className="text-white font-semibold text-sm">Configuración</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCollapsed(true)}
|
|
||||||
className="p-1 rounded hover:bg-gray-700 text-gray-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
<MdClose size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="flex overflow-x-auto scrollbar-thin scrollbar-thumb-gray-700">
|
|
||||||
{tabs.map((tab) => {
|
|
||||||
const Icon = tab.icon
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={tab.id}
|
|
||||||
onClick={() => setActiveTab(tab.id)}
|
|
||||||
className={`flex items-center gap-2 px-3 py-2 text-xs font-medium whitespace-nowrap border-b-2 transition-colors ${
|
|
||||||
activeTab === tab.id
|
|
||||||
? 'text-pink-500 border-pink-500'
|
|
||||||
: 'text-gray-400 border-transparent hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icon size={16} />
|
|
||||||
<span className="hidden xl:inline">{tab.label}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contenido de tabs */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
|
||||||
{activeTab === 'brand' && <BrandTab />}
|
|
||||||
{activeTab === 'multimedia' && <MultimediaTab />}
|
|
||||||
{activeTab === 'participants' && <ParticipantsPanel roomName={roomName} />}
|
|
||||||
{activeTab === 'sounds' && <SoundsTab />}
|
|
||||||
{activeTab === 'video' && <VideoTab />}
|
|
||||||
{activeTab === 'qr' && <QRTab />}
|
|
||||||
{activeTab === 'countdown' && <CountdownTab />}
|
|
||||||
{activeTab === 'settings' && <SettingsTab />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab de Marca
|
// Tab de Comentarios (Streamyard style)
|
||||||
const BrandTab = () => {
|
const CommentsTab = () => {
|
||||||
|
return (
|
||||||
|
<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 mb-2">Comentarios</h3>
|
||||||
|
<p className="text-gray-400 text-xs">
|
||||||
|
Puedes destacar comentarios importantes para que recuerdes comentarlos durante el programa.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
<div className="flex-1 flex items-center justify-center p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<MdComment size={48} className="mx-auto text-gray-600 mb-3" />
|
||||||
|
<p className="text-gray-400 text-sm">No hay comentarios destacados</p>
|
||||||
|
<button className="mt-3 text-pink-500 text-xs hover:underline">
|
||||||
|
Agrega un destino para publicar comentarios
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input area */}
|
||||||
|
<div className="p-4 border-t border-gray-700">
|
||||||
|
<div className="bg-gray-700 rounded-lg p-3">
|
||||||
|
<textarea
|
||||||
|
placeholder="Escribe un comentario..."
|
||||||
|
disabled
|
||||||
|
className="w-full bg-transparent text-gray-500 text-sm resize-none outline-none"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className="px-3 py-1.5 bg-gray-600 text-gray-400 rounded text-xs font-medium cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Enviar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab de Banners (Streamyard style)
|
||||||
|
const BannersTab = () => {
|
||||||
|
const [banners] = useState([
|
||||||
|
{ id: '1', text: 'Este es un ejemplo de banner. Haz clic en un banner para mostrarlo en la pantalla.' },
|
||||||
|
{ id: '2', text: 'Utiliza banners para resumir los temas de los que estás hablando y mostrar llamadas a la acción' },
|
||||||
|
{ id: '3', text: 'Banner' },
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
|
||||||
|
<h3 className="text-white font-semibold text-sm">Banners de ejemplo</h3>
|
||||||
|
<button className="p-1 rounded hover:bg-gray-700 text-gray-400 hover:text-white transition-colors">
|
||||||
|
<MdSettings size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Banners list */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
||||||
|
{banners.map((banner) => (
|
||||||
|
<div
|
||||||
|
key={banner.id}
|
||||||
|
className="bg-gray-700 rounded-lg p-3 hover:bg-gray-600 transition-colors cursor-pointer group relative"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<button className="flex-shrink-0 px-3 py-1 bg-pink-500 hover:bg-pink-600 text-white text-xs rounded font-medium transition-colors">
|
||||||
|
Mostrar
|
||||||
|
</button>
|
||||||
|
<p className="flex-1 text-gray-300 text-sm">{banner.text}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover actions */}
|
||||||
|
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button className="p-1.5 bg-gray-800 rounded hover:bg-gray-700 text-gray-400 hover:text-white">
|
||||||
|
<MdBrush size={14} />
|
||||||
|
</button>
|
||||||
|
<button className="p-1.5 bg-gray-800 rounded hover:bg-gray-700 text-red-400 hover:text-red-300">
|
||||||
|
<MdTimer size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add banner button */}
|
||||||
|
<div className="p-3 border-t border-gray-700">
|
||||||
|
<button className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-lg text-sm font-medium transition-colors">
|
||||||
|
<MdImage size={18} />
|
||||||
|
Crear un banner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab de Notas
|
||||||
|
const NotesTab = () => {
|
||||||
|
const [notes, setNotes] = useState('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">Notas</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notes editor */}
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<textarea
|
||||||
|
value={notes}
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
placeholder="Escribe tus notas aquí..."
|
||||||
|
className="w-full h-full bg-gray-700 text-white p-3 rounded-lg text-sm resize-none outline-none border border-gray-600 focus:border-pink-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab de Chat Privado
|
||||||
|
const ChatTab = () => {
|
||||||
|
return (
|
||||||
|
<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">Chat Privado</h3>
|
||||||
|
<p className="text-gray-400 text-xs mt-1">
|
||||||
|
Chatea con los participantes sin que se vea en el stream
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
<div className="flex-1 flex items-center justify-center p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<MdChat size={48} className="mx-auto text-gray-600 mb-3" />
|
||||||
|
<p className="text-gray-400 text-sm">No hay mensajes</p>
|
||||||
|
<p className="text-gray-500 text-xs mt-2">
|
||||||
|
Los mensajes aparecerán aquí cuando haya participantes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input area */}
|
||||||
|
<div className="p-4 border-t border-gray-700">
|
||||||
|
<div className="bg-gray-700 rounded-lg p-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Escribe un mensaje..."
|
||||||
|
disabled
|
||||||
|
className="w-full bg-transparent text-gray-500 text-sm outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab de Estilo (nuevo - reemplaza parte de Brand)
|
||||||
|
const StyleTab = () => {
|
||||||
const [selectedTheme, setSelectedTheme] = useState(COLOR_THEMES[0])
|
const [selectedTheme, setSelectedTheme] = useState(COLOR_THEMES[0])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">Estilo</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white text-sm font-semibold mb-3">Tema de Color</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{COLOR_THEMES.map((theme) => (
|
||||||
|
<button
|
||||||
|
key={theme.id}
|
||||||
|
onClick={() => setSelectedTheme(theme)}
|
||||||
|
className={`p-3 rounded-lg border-2 transition-all ${
|
||||||
|
selectedTheme.id === theme.id
|
||||||
|
? 'border-pink-500 bg-gray-700'
|
||||||
|
: 'border-gray-700 hover:border-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
style={{ backgroundColor: theme.primary }}
|
||||||
|
/>
|
||||||
|
<span className="text-white text-xs font-medium">{theme.name}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab de Marca/Multimedia (Brand) - Activos multimedia
|
||||||
|
const BrandTab = () => {
|
||||||
const [logoUrl, setLogoUrl] = useState<string | null>(null)
|
const [logoUrl, setLogoUrl] = useState<string | null>(null)
|
||||||
const [logoPosition, setLogoPosition] = useState('top-right')
|
|
||||||
|
|
||||||
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0]
|
||||||
@ -121,324 +320,87 @@ const BrandTab = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveLogo = () => {
|
|
||||||
setLogoUrl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-full">
|
||||||
<div>
|
{/* Header */}
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Tema de Color</h4>
|
<div className="p-4 border-b border-gray-700 flex items-center justify-between">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div>
|
||||||
{COLOR_THEMES.map((theme) => (
|
<h3 className="text-white font-semibold text-sm">Marca 1</h3>
|
||||||
<button
|
<p className="text-gray-400 text-xs mt-0.5">Activos multimedia</p>
|
||||||
key={theme.id}
|
|
||||||
onClick={() => setSelectedTheme(theme)}
|
|
||||||
className={`px-3 py-2 rounded-lg border-2 transition-all ${
|
|
||||||
selectedTheme.id === theme.id
|
|
||||||
? 'border-white bg-gray-700'
|
|
||||||
: 'border-gray-700 hover:border-gray-500'
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.primary + '20',
|
|
||||||
borderColor: selectedTheme.id === theme.id ? theme.primary : undefined
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="w-4 h-4 rounded-full"
|
|
||||||
style={{ backgroundColor: theme.primary }}
|
|
||||||
/>
|
|
||||||
<span className="text-white text-xs font-medium">{theme.name}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button className="p-1 rounded hover:bg-gray-700 text-gray-400 hover:text-white transition-colors">
|
||||||
|
<MdSettings size={18} />
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Logo</h4>
|
|
||||||
{logoUrl ? (
|
|
||||||
<div className="relative border-2 border-gray-700 rounded-lg p-4 bg-gray-700/50">
|
|
||||||
<img
|
|
||||||
src={logoUrl}
|
|
||||||
alt="Logo"
|
|
||||||
className="max-h-24 mx-auto object-contain"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleRemoveLogo}
|
|
||||||
className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white p-1.5 rounded-full transition-colors"
|
|
||||||
title="Eliminar logo"
|
|
||||||
>
|
|
||||||
<MdClose size={16} />
|
|
||||||
</button>
|
|
||||||
<p className="text-gray-400 text-xs text-center mt-2">Logo cargado</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<label className="block border-2 border-dashed border-gray-700 rounded-lg p-8 text-center hover:border-pink-500 transition-colors cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleLogoUpload}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<MdImage size={32} className="mx-auto text-gray-500 mb-2" />
|
|
||||||
<p className="text-gray-400 text-xs">Haz clic para subir logo</p>
|
|
||||||
<p className="text-gray-500 text-xs mt-1">PNG, JPG, SVG (máx. 2MB)</p>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Posición del Logo</h4>
|
|
||||||
<div className="grid grid-cols-3 gap-2">
|
|
||||||
{[
|
|
||||||
{ pos: 'top-left', label: 'Superior Izq.' },
|
|
||||||
{ pos: 'top-center', label: 'Superior Centro' },
|
|
||||||
{ pos: 'top-right', label: 'Superior Der.' },
|
|
||||||
{ pos: 'center-left', label: 'Centro Izq.' },
|
|
||||||
{ pos: 'center', label: 'Centro' },
|
|
||||||
{ pos: 'center-right', label: 'Centro Der.' },
|
|
||||||
{ pos: 'bottom-left', label: 'Inferior Izq.' },
|
|
||||||
{ pos: 'bottom-center', label: 'Inferior Centro' },
|
|
||||||
{ pos: 'bottom-right', label: 'Inferior Der.' }
|
|
||||||
].map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.pos}
|
|
||||||
onClick={() => setLogoPosition(item.pos)}
|
|
||||||
className={`aspect-square rounded-lg transition-all flex items-center justify-center ${
|
|
||||||
logoPosition === item.pos
|
|
||||||
? 'bg-pink-500/30 border-2 border-pink-500'
|
|
||||||
: 'bg-gray-700 border-2 border-transparent hover:bg-gray-600'
|
|
||||||
}`}
|
|
||||||
title={item.label}
|
|
||||||
>
|
|
||||||
<div className={`w-2 h-2 rounded-full ${
|
|
||||||
logoPosition === item.pos ? 'bg-pink-500' : 'bg-gray-400'
|
|
||||||
}`} />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{logoUrl && (
|
|
||||||
<p className="text-gray-400 text-xs mt-2 text-center">
|
|
||||||
Posición: {logoPosition.replace('-', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab de Multimedia
|
|
||||||
const MultimediaTab = () => {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Fondos</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{DEMO_BACKGROUNDS.map((bg) => (
|
|
||||||
<button
|
|
||||||
key={bg.id}
|
|
||||||
className="aspect-video rounded-lg hover:ring-2 hover:ring-pink-500 transition cursor-pointer overflow-hidden group relative"
|
|
||||||
style={{ background: bg.gradient }}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
|
||||||
<span className="text-white text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
{bg.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
<button className="aspect-video border-2 border-dashed border-gray-700 rounded-lg flex flex-col items-center justify-center gap-1 hover:border-pink-500 transition">
|
|
||||||
<MdImage size={24} className="text-gray-500" />
|
|
||||||
<span className="text-gray-500 text-xs">Subir fondo</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Overlays</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{DEMO_OVERLAYS.map((overlay) => (
|
|
||||||
<button
|
|
||||||
key={overlay.id}
|
|
||||||
className="w-full p-3 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-left group"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-white text-sm font-medium">{overlay.name}</p>
|
|
||||||
<p className="text-gray-400 text-xs">{overlay.type}</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-8 h-8 bg-pink-500/20 rounded flex items-center justify-center">
|
|
||||||
<MdImage className="text-pink-500" size={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab de Sonidos
|
|
||||||
const SoundsTab = () => {
|
|
||||||
const [volumes, setVolumes] = useState<Record<string, number>>(
|
|
||||||
DEMO_SOUNDS.reduce((acc, sound) => ({ ...acc, [sound.id]: 50 }), {})
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Efectos de Sonido</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{DEMO_SOUNDS.map((sound) => (
|
|
||||||
<div key={sound.id} className="bg-gray-700 rounded-lg p-3">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<button className="flex items-center gap-2 text-white hover:text-pink-500 transition-colors">
|
|
||||||
<div className="w-8 h-8 bg-pink-500/20 rounded-full flex items-center justify-center">
|
|
||||||
<MdMusicNote className="text-pink-500" size={16} />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium">{sound.name}</span>
|
|
||||||
</button>
|
|
||||||
<span className="text-gray-400 text-xs">{volumes[sound.id]}%</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
value={volumes[sound.id]}
|
|
||||||
onChange={(e) => setVolumes({ ...volumes, [sound.id]: parseInt(e.target.value) })}
|
|
||||||
className="w-full h-1.5 bg-gray-600 rounded-lg appearance-none cursor-pointer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Música de Fondo</h4>
|
|
||||||
<div className="border-2 border-dashed border-gray-700 rounded-lg p-6 text-center hover:border-pink-500 transition-colors cursor-pointer">
|
|
||||||
<MdMusicNote size={28} className="mx-auto text-gray-500 mb-2" />
|
|
||||||
<p className="text-gray-400 text-xs">Haz clic para agregar música</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab de Videos
|
|
||||||
const VideoTab = () => {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Clips de Video</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
{['Intro', 'Outro', 'Transición'].map((clip) => (
|
|
||||||
<div key={clip} className="aspect-video bg-gray-700 rounded-lg hover:ring-2 hover:ring-pink-500 transition cursor-pointer flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<MdVideoLibrary size={24} className="mx-auto text-gray-500 mb-1" />
|
|
||||||
<p className="text-gray-400 text-xs">{clip}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab de QR
|
|
||||||
const QRTab = () => {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Generar Código QR</h4>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Ingresa URL"
|
|
||||||
className="w-full bg-gray-700 text-white py-2 px-3 rounded-lg text-sm border border-gray-600 focus:border-pink-500 focus:outline-none"
|
|
||||||
/>
|
|
||||||
<button className="w-full mt-2 bg-pink-500 hover:bg-pink-600 text-white py-2 px-3 rounded-lg text-sm font-medium transition-colors">
|
|
||||||
Generar QR
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-700 rounded-lg p-4 aspect-square flex items-center justify-center">
|
{/* Content */}
|
||||||
<p className="text-gray-400 text-sm text-center">El código QR aparecerá aquí</p>
|
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||||
</div>
|
{/* Logo section */}
|
||||||
</div>
|
<div>
|
||||||
)
|
<h4 className="text-white text-sm font-semibold mb-3">Logo</h4>
|
||||||
}
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{logoUrl && (
|
||||||
// Tab de Cuenta Regresiva
|
<div className="aspect-square bg-gray-700 rounded-lg p-2 flex items-center justify-center border-2 border-pink-500">
|
||||||
const CountdownTab = () => {
|
<img src={logoUrl} alt="Logo" className="max-w-full max-h-full object-contain" />
|
||||||
return (
|
</div>
|
||||||
<div className="space-y-4">
|
)}
|
||||||
<div>
|
<label className="aspect-square border-2 border-dashed border-gray-700 rounded-lg flex flex-col items-center justify-center gap-1 hover:border-pink-500 transition cursor-pointer">
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Temporizadores Rápidos</h4>
|
<input
|
||||||
<div className="grid grid-cols-3 gap-2">
|
type="file"
|
||||||
{['30s', '1m', '5m', '10m', '15m', '30m'].map((time) => (
|
accept="image/*"
|
||||||
<button
|
onChange={handleLogoUpload}
|
||||||
key={time}
|
className="hidden"
|
||||||
className="bg-gray-700 hover:bg-pink-500 text-white py-2 px-3 rounded-lg text-sm font-medium transition-colors"
|
/>
|
||||||
>
|
<MdImage size={24} className="text-gray-500" />
|
||||||
{time}
|
<span className="text-gray-500 text-xs">Subir logo</span>
|
||||||
</button>
|
</label>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
{/* Fondos section */}
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Personalizado</h4>
|
<div>
|
||||||
<input
|
<h4 className="text-white text-sm font-semibold mb-3">Fondos</h4>
|
||||||
type="number"
|
<div className="grid grid-cols-2 gap-2">
|
||||||
placeholder="Minutos"
|
{DEMO_BACKGROUNDS.map((bg) => (
|
||||||
className="w-full bg-gray-700 text-white py-2 px-3 rounded-lg text-sm border border-gray-600 focus:border-pink-500 focus:outline-none"
|
<button
|
||||||
/>
|
key={bg.id}
|
||||||
</div>
|
className="aspect-video rounded-lg hover:ring-2 hover:ring-pink-500 transition cursor-pointer overflow-hidden group relative"
|
||||||
</div>
|
style={{ background: bg.gradient }}
|
||||||
)
|
>
|
||||||
}
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
||||||
|
<span className="text-white text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{bg.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button className="aspect-video border-2 border-dashed border-gray-700 rounded-lg flex flex-col items-center justify-center gap-1 hover:border-pink-500 transition">
|
||||||
|
<MdImage size={20} className="text-gray-500" />
|
||||||
|
<span className="text-gray-500 text-xs">Subir</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
// Tab de Ajustes
|
{/* Overlays section */}
|
||||||
const SettingsTab = () => {
|
<div>
|
||||||
return (
|
<h4 className="text-white text-sm font-semibold mb-3">Overlays</h4>
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
<div>
|
{DEMO_OVERLAYS.slice(0, 3).map((overlay) => (
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Calidad de Video</h4>
|
<button
|
||||||
<select className="w-full bg-gray-700 text-white py-2 px-3 rounded-lg text-sm border border-gray-600 focus:border-pink-500 focus:outline-none">
|
key={overlay.id}
|
||||||
<option>1080p (Full HD)</option>
|
className="w-full p-3 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors text-left"
|
||||||
<option>720p (HD)</option>
|
>
|
||||||
<option>480p (SD)</option>
|
<div className="flex items-center justify-between">
|
||||||
</select>
|
<div>
|
||||||
</div>
|
<p className="text-white text-sm font-medium">{overlay.name}</p>
|
||||||
|
<p className="text-gray-400 text-xs">{overlay.type}</p>
|
||||||
<div>
|
</div>
|
||||||
<h4 className="text-white text-sm font-semibold mb-3">Bitrate</h4>
|
<MdImage className="text-pink-500" size={20} />
|
||||||
<input
|
</div>
|
||||||
type="range"
|
</button>
|
||||||
min="1000"
|
))}
|
||||||
max="6000"
|
</div>
|
||||||
step="500"
|
</div>
|
||||||
className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
|
||||||
/>
|
|
||||||
<p className="text-gray-400 text-xs mt-2">3500 kbps</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="flex items-center justify-between">
|
|
||||||
<span className="text-gray-300 text-sm">Grabar automáticamente</span>
|
|
||||||
<input type="checkbox" className="form-checkbox h-4 w-4 text-pink-500 rounded" />
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center justify-between">
|
|
||||||
<span className="text-gray-300 text-sm">Mostrar chat en vivo</span>
|
|
||||||
<input type="checkbox" className="form-checkbox h-4 w-4 text-pink-500 rounded" defaultChecked />
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center justify-between">
|
|
||||||
<span className="text-gray-300 text-sm">Habilitar aplausos</span>
|
|
||||||
<input type="checkbox" className="form-checkbox h-4 w-4 text-pink-500 rounded" defaultChecked />
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { LoginCredentials, RegisterData, AuthResponse, ApiResponse } from '@avanzacast/shared-types';
|
import type { LoginCredentials, RegisterData, AuthResponse, ApiResponse } from '@avanzacast/shared-types';
|
||||||
import { getAuthHeader } from './auth';
|
import { getAuthHeader } from './auth';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000/api/v1';
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'https://avanzacast-servertokens.bfzqqk.easypanel.host/api/v1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cliente HTTP para hacer peticiones a la API
|
* Cliente HTTP para hacer peticiones a la API
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user