311 lines
12 KiB
TypeScript

import { useState, useEffect } from "react";
import {
LiveKitRoom,
ControlBar,
useTracks,
useLocalParticipant
} from "@livekit/components-react";
import "@livekit/components-styles";
import StudioHeader from "./StudioHeader";
import StudioLeftSidebar from "./StudioLeftSidebar";
import StudioRightPanel, { TabsColumn, TabType } from "./StudioRightPanel";
import { SceneProvider } from "../context/SceneContext";
import StreamView from "./broadcast/StreamView";
import ControlPanel from "./broadcast/ControlPanel";
function Studio() {
const [token, setToken] = useState<string>("");
const [serverUrl, setServerUrl] = useState<string>(
"wss://avanzacast-test-0kl2kzjr.livekit.cloud",
);
const [roomName, setRoomName] = useState<string>("");
const [userName, setUserName] = useState<string>("");
const [showLeftPanel, setShowLeftPanel] = useState(true);
const [showRightPanel, setShowRightPanel] = useState(true);
const [activeRightTab, setActiveRightTab] = useState<TabType>("comments");
const [needsUserName, setNeedsUserName] = useState(false);
const [inputUserName, setInputUserName] = useState("");
const [isConnecting, setIsConnecting] = useState(false);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const urlToken = params.get("token");
const urlRoom = params.get("room");
const urlUser = params.get("user");
const savedToken =
urlToken || localStorage.getItem("avanzacast_studio_token") || "";
const savedRoom =
urlRoom || localStorage.getItem("avanzacast_studio_room") || "";
const savedServerUrl =
localStorage.getItem("avanzacast_studio_serverUrl") ||
"wss://avanzacast-test-0kl2kzjr.livekit.cloud";
const savedUserName =
urlUser || localStorage.getItem("avanzacast_studio_userName") || "";
setToken(savedToken);
setRoomName(savedRoom);
setServerUrl(savedServerUrl);
if (savedUserName) {
setUserName(savedUserName);
setNeedsUserName(false);
} else if (savedToken && savedRoom) {
setNeedsUserName(true);
}
if (savedToken) localStorage.setItem("avanzacast_studio_token", savedToken);
if (savedRoom) localStorage.setItem("avanzacast_studio_room", savedRoom);
if (savedServerUrl)
localStorage.setItem("avanzacast_studio_serverUrl", savedServerUrl);
}, []);
const handleSubmitUserName = (e: React.FormEvent) => {
e.preventDefault();
if (inputUserName.trim()) {
const finalUserName = inputUserName.trim();
setUserName(finalUserName);
localStorage.setItem("avanzacast_studio_userName", finalUserName);
setNeedsUserName(false);
}
};
if (needsUserName) {
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-900 via-purple-900/20 to-gray-900">
<div className="w-full max-w-md mx-4">
<div className="bg-gray-800/50 backdrop-blur-xl rounded-2xl shadow-2xl border border-gray-700/50 p-8">
<div className="flex justify-center mb-6">
<div className="flex items-center gap-2">
<div className="w-10 h-10 bg-gradient-to-br from-pink-500 to-purple-600 rounded-lg flex items-center justify-center shadow-lg">
<span className="text-white text-xl font-bold">A</span>
</div>
<span className="text-2xl font-bold text-white">
AvanzaCast
</span>
</div>
</div>
<h2 className="text-xl font-semibold text-white text-center mb-2">
Bienvenido al Estudio
</h2>
<p className="text-gray-400 text-center mb-6">
Ingresa tu nombre para unirte a la transmisión
</p>
<form onSubmit={handleSubmitUserName} className="space-y-4">
<div>
<label
htmlFor="userName"
className="block text-sm font-medium text-gray-300 mb-2"
>
Nombre de usuario
</label>
<input
id="userName"
type="text"
value={inputUserName}
onChange={(e) => setInputUserName(e.target.value)}
placeholder="Tu nombre"
className="w-full px-4 py-3 bg-gray-900/50 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
autoFocus
required
/>
</div>
<button
type="submit"
className="w-full py-3 bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transform hover:scale-[1.02] transition-all duration-200"
>
Entrar al Estudio
</button>
</form>
</div>
</div>
</div>
);
}
if (isConnecting) {
return (
<div className="flex items-center justify-center h-screen bg-gray-900">
<div className="text-center">
<div className="mb-4">
<div className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
</div>
<div className="text-white text-xl">Conectando al estudio...</div>
</div>
</div>
);
}
if (!token || !roomName) {
return (
<div className="flex items-center justify-center h-screen bg-gradient-to-br from-gray-900 via-purple-900/20 to-gray-900">
<div className="text-center max-w-md mx-4">
<div className="mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-pink-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg mx-auto mb-4">
<span className="text-white text-2xl font-bold">A</span>
</div>
<h1 className="text-3xl font-bold text-white mb-2">
AvanzaCast Studio
</h1>
</div>
<p className="text-gray-400 text-lg mb-4">
No hay datos de conexión disponibles.
</p>
<p className="text-gray-500 text-sm">
Para acceder al estudio, debes iniciar una transmisión desde el
panel de broadcast.
</p>
</div>
</div>
);
}
return (
<LiveKitRoom
token={token}
serverUrl={serverUrl}
onDisconnected={() => console.log("[LiveKit] Desconectado.")}
onError={(e) => console.error("[LiveKit] Error:", e)}
data-lk-theme="default"
className="studio-container"
>
<SceneProvider>
<div className="flex flex-col h-screen bg-gray-900 overflow-hidden">
{/* Header */}
<div className="flex-none z-40">
<StudioHeader roomName={roomName} userName={userName} />
</div>
{/* Contenedor principal entre header y footer */}
<div className="flex-1 overflow-hidden relative min-h-0">
{/* Panel izquierdo */}
<div
className={`absolute left-0 top-0 bottom-0 z-20 transition-transform duration-300 ${showLeftPanel ? "translate-x-0" : "-translate-x-full"}`}
>
<StudioLeftSidebar />
</div>
{/* Botón toggle panel izquierdo */}
<button
onClick={() => setShowLeftPanel(!showLeftPanel)}
className="absolute top-1/2 -translate-y-1/2 z-30 transition-all duration-300"
style={{ left: showLeftPanel ? "256px" : "0px" }}
>
<svg
width="16"
height="101"
viewBox="0 0 16 101"
fill="none"
className="hover:opacity-80"
>
<path
d="M0 12C0 5.37258 5.37258 0 12 0H16V101H12C5.37258 101 0 95.6274 0 89V12Z"
fill="#1F2937"
/>
<rect
x="13"
y="42"
width="3"
height="17"
rx="1.5"
fill="white"
/>
<path
d={
showLeftPanel ? "M6 44L10 50.5L6 57" : "M10 44L6 50.5L10 57"
}
stroke="#FFF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{/* Contenedor del estudio (StreamView + ControlPanel) */}
<div
className="absolute top-0 bottom-0 transition-all duration-300 flex flex-col overflow-hidden"
style={{
left: showLeftPanel ? "256px" : "0px",
right: showRightPanel ? "480px" : "80px",
}}
>
<div className="flex-1 flex items-center justify-center px-4 pt-4 overflow-hidden min-h-0">
<div className="w-full h-full max-w-6xl flex items-center justify-center">
<div className="w-full relative">
<div className="absolute top-3 right-3 z-10 flex items-center gap-2 text-gray-400 text-xs bg-gray-900/80 backdrop-blur-sm px-3 py-1.5 rounded-lg">
<span>Producido con</span>
<div className="flex items-center gap-1">
<div className="w-5 h-5 bg-gradient-to-br from-pink-500 to-purple-600 rounded flex items-center justify-center">
<span className="text-white text-xs font-bold">
A
</span>
</div>
<span className="font-semibold text-white">
AvanzaCast
</span>
</div>
</div>
<StreamView />
</div>
</div>
</div>
<div className="flex-none px-4 pb-4">
<ControlPanel />
</div>
</div>
{/* Panel derecho: tabs siempre visibles en el extremo derecho, contenido se oculta */}
{/* Panel de contenido con animación de slide */}
<div
className={`absolute top-0 bottom-0 z-20 transition-transform duration-300 ${showRightPanel ? "translate-x-0" : "translate-x-full"}`}
style={{ right: "80px" }}
>
<StudioRightPanel
activeTab={activeRightTab}
onChangeTab={(t: TabType) => setActiveRightTab(t)}
/>
</div>
{/* TabsColumn siempre visible en el extremo derecho */}
<div className="absolute right-0 top-0 bottom-0 z-30">
<TabsColumn
activeTab={activeRightTab}
onChangeTab={(t: TabType) => {
setActiveRightTab(t);
if (!showRightPanel) {
setShowRightPanel(true);
}
}}
onTogglePanel={() => setShowRightPanel(!showRightPanel)}
isCollapsed={!showRightPanel}
/>
</div>
</div>
{/* ControlBar como footer fijo */}
<div
className="flex-none h-20 z-40 transition-all duration-300 bg-gray-900/95 backdrop-blur-sm border-t border-gray-800"
style={{
marginLeft: showLeftPanel ? "256px" : "0px",
marginRight: showRightPanel ? "480px" : "80px",
}}
>
<ControlBar
variation="verbose"
controls={{
microphone: true,
camera: true,
screenShare: true,
leave: true,
}}
style={{
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 1rem',
}}
/>
</div>
</div>
</SceneProvider>
</LiveKitRoom>
);
}
export default Studio;