1 Commits
v0.0.1 ... main

Author SHA1 Message Date
c23b2690d1 fix: room layout fixed viewport + dashboard room delete
- Room: overflow-hidden + contain:strict on video area prevents layout
  from growing beyond viewport when sidebar opens
- Room: absolute positioning for GridLayout/FocusLayout ensures video
  fills all available space
- Dashboard: delete button on room cards (hover-visible, confirm dialog)
- Dashboard: active rooms cannot be deleted (button hidden)
2026-03-24 12:51:06 +03:00
2 changed files with 54 additions and 16 deletions

View File

@@ -36,6 +36,7 @@ export default function DashboardPage() {
const { data: session, isPending } = useSession(); const { data: session, isPending } = useSession();
const [rooms, setRooms] = useState<Room[]>([]); const [rooms, setRooms] = useState<Room[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [deleting, setDeleting] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (!isPending && !session) { if (!isPending && !session) {
@@ -63,6 +64,24 @@ export default function DashboardPage() {
} }
}, [session]); }, [session]);
async function handleDelete(e: React.MouseEvent, room: Room) {
e.preventDefault(); // prevent Link navigation
e.stopPropagation();
if (room.status === "ACTIVE") return;
if (!confirm(`Удалить комнату "${room.name}"? Это действие необратимо.`)) return;
setDeleting(room.id);
try {
const res = await fetch(`/api/rooms/${room.id}`, { method: "DELETE" });
if (res.ok) {
setRooms((prev) => prev.filter((r) => r.id !== room.id));
}
} catch {
// silent
} finally {
setDeleting(null);
}
}
if (isPending) { if (isPending) {
return ( return (
<main className="min-h-screen bg-surface-0 flex items-center justify-center"> <main className="min-h-screen bg-surface-0 flex items-center justify-center">
@@ -157,15 +176,33 @@ export default function DashboardPage() {
<Link <Link
key={room.id} key={room.id}
href={`/room/${room.code}`} href={`/room/${room.code}`}
className="block bg-surface-1 border border-border-subtle rounded-xl p-5 hover:border-border-default transition-colors cursor-pointer group" className="block bg-surface-1 border border-border-subtle rounded-xl p-5 hover:border-border-default transition-colors cursor-pointer group relative"
> >
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<h2 className="font-semibold text-base truncate pr-3 group-hover:text-white transition-colors"> <h2 className="font-semibold text-base truncate pr-3 group-hover:text-white transition-colors">
{room.name} {room.name}
</h2> </h2>
<div className={`flex items-center gap-1.5 shrink-0 text-xs font-medium ${statusTextColors[room.status]}`}> <div className="flex items-center gap-2 shrink-0">
<span className={`h-2 w-2 rounded-full ${statusDotColors[room.status]}`} /> <div className={`flex items-center gap-1.5 text-xs font-medium ${statusTextColors[room.status]}`}>
{statusLabels[room.status]} <span className={`h-2 w-2 rounded-full ${statusDotColors[room.status]}`} />
{statusLabels[room.status]}
</div>
{room.status !== "ACTIVE" && (
<button
onClick={(e) => handleDelete(e, room)}
disabled={deleting === room.id}
className="opacity-0 group-hover:opacity-100 p-1.5 rounded-lg text-text-muted hover:text-danger hover:bg-danger/10 transition-all disabled:opacity-50"
title="Удалить комнату"
>
{deleting === room.id ? (
<div className="w-4 h-4 border-2 border-danger border-t-transparent rounded-full animate-spin" />
) : (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
)}
</button>
)}
</div> </div>
</div> </div>
<div className="flex items-center gap-3 text-sm"> <div className="flex items-center gap-3 text-sm">

View File

@@ -153,13 +153,14 @@ export default function RoomPage() {
serverUrl={LIVEKIT_URL} serverUrl={LIVEKIT_URL}
token={token} token={token}
connect={true} connect={true}
className="h-screen flex flex-col bg-surface-0" data-lk-theme="default"
className="h-screen max-h-screen flex flex-col bg-surface-0 overflow-hidden"
onDisconnected={() => router.push(isHost ? "/dashboard" : "/")} onDisconnected={() => router.push(isHost ? "/dashboard" : "/")}
> >
<RoomAudioRenderer /> <RoomAudioRenderer />
{/* Top bar — minimal, just room info */} {/* Top bar — minimal, just room info */}
<header className="flex items-center px-4 h-12 bg-surface-1/80 backdrop-blur-sm border-b border-border-subtle shrink-0"> <header className="flex items-center px-4 h-12 bg-surface-1/80 backdrop-blur-sm border-b border-border-subtle shrink-0 z-10">
<h1 className="text-white font-semibold text-sm">{room?.name ?? "Комната"}</h1> <h1 className="text-white font-semibold text-sm">{room?.name ?? "Комната"}</h1>
{room && ( {room && (
<span className="text-[11px] text-text-muted font-mono bg-surface-2 px-2 py-0.5 rounded-md border border-border-subtle ml-3"> <span className="text-[11px] text-text-muted font-mono bg-surface-2 px-2 py-0.5 rounded-md border border-border-subtle ml-3">
@@ -168,28 +169,28 @@ export default function RoomPage() {
)} )}
</header> </header>
{/* Main content area */} {/* Main content area — fixed between header and footer */}
<div className="flex flex-1 min-h-0"> <div className="flex flex-1 min-h-0 overflow-hidden">
{/* Video area */} {/* Video area */}
<div className="flex-1 min-w-0 p-2"> <div className="flex-1 min-w-0 min-h-0 relative" style={{ contain: "strict" }}>
<VideoArea /> <VideoArea />
</div> </div>
{/* Sidebar */} {/* Sidebar */}
{sidebarOpen && ( {sidebarOpen && (
<aside className="w-80 shrink-0 border-l border-border-default flex flex-col bg-surface-1"> <aside className="w-80 shrink-0 border-l border-border-default flex flex-col bg-surface-1 overflow-hidden">
{isHost && showLobby && room && ( {isHost && showLobby && room && (
<div className="border-b border-border-default"> <div className="border-b border-border-default shrink-0">
<LobbyManager roomId={room.id} /> <LobbyManager roomId={room.id} />
</div> </div>
)} )}
{isHost && showModeration && room && ( {isHost && showModeration && room && (
<div className="border-b border-border-default"> <div className="border-b border-border-default shrink-0">
<ModerationPanel roomId={room.id} /> <ModerationPanel roomId={room.id} />
</div> </div>
)} )}
{showChat && room && ( {showChat && room && (
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0 overflow-hidden">
<ChatPanel <ChatPanel
roomId={room.id} roomId={room.id}
sessionId={userId ?? "anonymous"} sessionId={userId ?? "anonymous"}
@@ -202,7 +203,7 @@ export default function RoomPage() {
</div> </div>
{/* Bottom control bar — Google Meet style */} {/* Bottom control bar — Google Meet style */}
<footer className="flex items-center justify-between px-4 h-16 bg-surface-1 border-t border-border-default shrink-0"> <footer className="flex items-center justify-between px-4 h-16 bg-surface-1 border-t border-border-default shrink-0 z-10">
{/* Left: room time or empty */} {/* Left: room time or empty */}
<div className="w-48" /> <div className="w-48" />
@@ -298,7 +299,7 @@ function VideoArea() {
// If someone is screen sharing, use focus layout // If someone is screen sharing, use focus layout
if (screenShareTracks.length > 0) { if (screenShareTracks.length > 0) {
return ( return (
<FocusLayoutContainer className="h-full"> <FocusLayoutContainer className="absolute inset-0">
<CarouselLayout tracks={cameraTracks} className="h-24"> <CarouselLayout tracks={cameraTracks} className="h-24">
<ParticipantTile /> <ParticipantTile />
</CarouselLayout> </CarouselLayout>
@@ -308,7 +309,7 @@ function VideoArea() {
} }
return ( return (
<GridLayout tracks={cameraTracks} className="h-full"> <GridLayout tracks={cameraTracks} className="absolute inset-0">
<ParticipantTile /> <ParticipantTile />
</GridLayout> </GridLayout>
); );