Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { Loader2, Maximize, Minimize, AlertCircle, Eye } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { joinAsViewer, type ViewerHandle } from '@/lib/webrtc/viewer';
|
||||
|
||||
type ConnState = 'checking' | 'connecting' | 'waiting' | 'live' | 'error' | 'not-found';
|
||||
|
||||
export default function Watch() {
|
||||
const { code = '' } = useParams();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const handleRef = useRef<ViewerHandle | null>(null);
|
||||
const [state, setState] = useState<ConnState>('checking');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
const { data, error: dbErr } = await supabase
|
||||
.from('share_rooms')
|
||||
.select('code, is_active, expires_at')
|
||||
.eq('code', code.toUpperCase())
|
||||
.maybeSingle();
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (dbErr) {
|
||||
setError(dbErr.message);
|
||||
setState('error');
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
setState('not-found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!videoRef.current) return;
|
||||
try {
|
||||
const handle = await joinAsViewer(code.toUpperCase(), videoRef.current, (s, detail) => {
|
||||
setState(s as ConnState);
|
||||
if (detail) setError(detail);
|
||||
});
|
||||
handleRef.current = handle;
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setState('error');
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (handleRef.current) handleRef.current.stop();
|
||||
};
|
||||
}, [code]);
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
const el = videoRef.current;
|
||||
if (!el) return;
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen();
|
||||
setFullscreen(false);
|
||||
} else {
|
||||
await el.requestFullscreen();
|
||||
setFullscreen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<header className="border-b bg-card/50 backdrop-blur px-4 py-3 flex items-center justify-between">
|
||||
<Link to="/" className="font-mono text-sm font-bold text-foreground">
|
||||
TrackSteel<span className="text-primary">XR</span>
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-[10px] text-muted-foreground">SALA</span>
|
||||
<span className="font-mono text-sm font-bold tracking-widest text-primary">
|
||||
{code.toUpperCase()}
|
||||
</span>
|
||||
{state === 'live' && (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500" />
|
||||
</span>
|
||||
<span className="font-mono text-[10px] text-red-500">AO VIVO</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 relative bg-black flex items-center justify-center">
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
|
||||
{(state === 'checking' || state === 'connecting' || state === 'waiting') && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/80">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<p className="font-mono text-xs text-muted-foreground">
|
||||
{state === 'checking' && 'Verificando sala…'}
|
||||
{state === 'connecting' && 'Conectando…'}
|
||||
{state === 'waiting' && 'Aguardando o inspetor iniciar…'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'not-found' && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/90 px-6 text-center">
|
||||
<AlertCircle className="h-10 w-10 text-destructive" />
|
||||
<h2 className="font-mono text-base font-bold">Sala não encontrada</h2>
|
||||
<p className="font-mono text-xs text-muted-foreground max-w-sm">
|
||||
A sala <span className="text-primary">{code.toUpperCase()}</span> não existe,
|
||||
expirou ou foi encerrada pelo inspetor.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'error' && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/90 px-6 text-center">
|
||||
<AlertCircle className="h-10 w-10 text-destructive" />
|
||||
<h2 className="font-mono text-base font-bold">Erro de conexão</h2>
|
||||
<p className="font-mono text-xs text-muted-foreground max-w-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state === 'live' && (
|
||||
<Button
|
||||
onClick={toggleFullscreen}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="absolute bottom-4 right-4 h-9 w-9 bg-card/80 backdrop-blur"
|
||||
>
|
||||
{fullscreen ? <Minimize className="h-4 w-4" /> : <Maximize className="h-4 w-4" />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="border-t bg-card/50 backdrop-blur px-4 py-2 flex items-center justify-center gap-2">
|
||||
<Eye className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-mono text-[10px] text-muted-foreground">
|
||||
Modo somente visualização · TrackSteelXR
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user