Files
LiveServer-M1/src/components/lobby/LobbyManager.tsx
joylessorchid 3846e3e00d feat: LiveServer-M1 v1 — educational video conferencing platform
Full MVP implementation:
- Next.js 16 + React 19 + TypeScript + Tailwind CSS v4
- LiveKit integration (rooms, tokens, webhooks, moderation)
- better-auth (email/password, sessions, roles)
- Prisma 7 + PostgreSQL (9 models, 3 enums)
- 15 API routes (auth, rooms, lobby, chat, files, moderation, hand-raise)
- 7 pages (landing, auth, dashboard, join, video room)
- SSE-based waiting room with host approval flow
- Security: PIN rate limiting, session fingerprint bans, chat/files auth
- Python AI Agent (Deepgram STT + OpenAI summarization)
- Docker Compose (local + production with Traefik + Let's Encrypt)
- Interactive setup script (setup.sh)
- Dev protection middleware (DEV_ACCESS_KEY, ALLOWED_IPS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 13:57:53 +03:00

96 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState, useCallback, useRef } from "react";
interface LobbyManagerProps {
roomId: string;
}
type LobbyEntry = {
id: string;
sessionId: string;
displayName: string;
createdAt: string;
};
export default function LobbyManager({ roomId }: LobbyManagerProps) {
const [entries, setEntries] = useState<LobbyEntry[]>([]);
const [acting, setActing] = useState<string | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
const fetchEntries = useCallback(async () => {
try {
const res = await fetch(`/api/rooms/${roomId}/lobby`);
if (res.ok) {
const data = await res.json();
setEntries(data);
}
} catch {
// silent
}
}, [roomId]);
useEffect(() => {
fetchEntries();
pollRef.current = setInterval(fetchEntries, 3000);
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, [fetchEntries]);
async function handleAction(entry: LobbyEntry, action: "APPROVED" | "REJECTED") {
setActing(entry.id);
try {
await fetch(`/api/rooms/${roomId}/lobby`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lobbyEntryId: entry.id, action }),
});
setEntries((prev) => prev.filter((e) => e.id !== entry.id));
} catch {
// silent
} finally {
setActing(null);
}
}
return (
<div className="p-4">
<h3 className="text-sm font-semibold text-neutral-400 uppercase tracking-wider mb-3">
Зал ожидания ({entries.length})
</h3>
{entries.length === 0 ? (
<p className="text-sm text-neutral-500">Никто не ожидает</p>
) : (
<div className="space-y-2">
{entries.map((entry) => (
<div
key={entry.id}
className="flex items-center justify-between bg-neutral-800 rounded-lg px-3 py-2"
>
<span className="text-sm truncate mr-2">{entry.displayName}</span>
<div className="flex gap-1 shrink-0">
<button
onClick={() => handleAction(entry, "APPROVED")}
disabled={acting === entry.id}
className="px-2 py-1 text-xs bg-green-600 hover:bg-green-700 disabled:opacity-50 rounded font-medium transition-colors"
>
Принять
</button>
<button
onClick={() => handleAction(entry, "REJECTED")}
disabled={acting === entry.id}
className="px-2 py-1 text-xs bg-red-600/20 text-red-400 hover:bg-red-600/30 disabled:opacity-50 rounded font-medium transition-colors"
>
Отклонить
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}