🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 00:34:30

This commit is contained in:
2026-05-25 00:34:30 +00:00
parent 3c903c1ff1
commit 2e7402691a
+224 -70
View File
@@ -12,11 +12,11 @@ import { toast } from 'sonner';
import {
Users, MessageSquare, Mic, MicOff, Home, Send,
ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut,
RotateCw, Award
RotateCw, Award, ShieldAlert
} from 'lucide-react';
// Interfaces tipadas para evitar any
interface MiiPreset {
interface AvatarPreset {
id: number;
name: string;
desc: string;
@@ -54,17 +54,17 @@ const CHAIRS = [
{ id: '8', name: 'Assento 8', pos: [-1.2, 0.95, 1.45], lookAt: [0, 0.95, 0] },
];
const AVATAR_PRESETS: MiiPreset[] = [
{ id: 1, name: 'Mii Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' },
{ id: 2, name: 'Mii Classic', desc: 'Pele média com Cabelo castanho clássico', color: '#b45309' },
{ id: 3, name: 'Mii Goldie', desc: 'Cabelo loiro longo com olhos azuis expressivos', color: '#eab308' },
{ id: 4, name: 'Mii Sporty', desc: 'Boné azul virado para trás e sorriso aberto', color: '#3b82f6' },
{ id: 5, name: 'Mii Ginger', desc: 'Cabelo ruivo longo ondulado e expressivo', color: '#f97316' },
{ id: 6, name: 'Mii Sleek', desc: 'Pele parda com cabelo curto liso lateral', color: '#10b981' },
{ id: 7, name: 'Mii Beard', desc: 'Careca estilosa com barba e bigode', color: '#78350f' },
{ id: 8, name: 'Mii Sun', desc: 'Cabelo raspado e óculos escuros pretos', color: '#1e293b' },
{ id: 9, name: 'Mii Synth', desc: 'Cabelo roxo ondulado moderno', color: '#a855f7' },
{ id: 10, name: 'Mii Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' },
const AVATAR_PRESETS: AvatarPreset[] = [
{ id: 1, name: 'Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' },
{ id: 2, name: 'Classic', desc: 'Pele média com Cabelo castanho clássico', color: '#b45309' },
{ id: 3, name: 'Goldie', desc: 'Cabelo loiro longo com olhos azuis expressivos', color: '#eab308' },
{ id: 4, name: 'Sporty', desc: 'Boné azul virado para trás e sorriso aberto', color: '#3b82f6' },
{ id: 5, name: 'Ginger', desc: 'Cabelo ruivo longo ondulado e expressivo', color: '#f97316' },
{ id: 6, name: 'Sleek', desc: 'Pele parda com cabelo curto liso lateral', color: '#10b981' },
{ id: 7, name: 'Beard', desc: 'Careca estilosa com barba e bigode', color: '#78350f' },
{ id: 8, name: 'Sun', desc: 'Cabelo raspado e óculos escuros pretos', color: '#1e293b' },
{ id: 9, name: 'Synth', desc: 'Cabelo roxo ondulado moderno', color: '#a855f7' },
{ id: 10, name: 'Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' },
];
const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5'];
@@ -88,7 +88,7 @@ const getAvatarBgClass = (avatarId: number): string => {
// --- Subcomponentes 3D ---
// Avatar estilo Mii (Nintendo Wii)
// Avatar estilo Mii (Nintendo Wii) - Versão Melhorada (Mais Feliz e Expressivo)
function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: number; username: string; isTalking?: boolean } & Record<string, unknown>) {
const groupRef = useRef<THREE.Group>(null);
@@ -103,7 +103,7 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
capColor: '#3b82f6',
hasHat: avatarId === 10,
hatColor: '#b45309',
hairStyle: avatarId === 7 ? 'bald' : (avatarId % 3 === 0 ? 'long' : 'short'),
hairStyle: avatarId === 7 ? 'bald' : (avatarId === 9 ? 'modern' : (avatarId % 3 === 0 ? 'long' : 'short')),
beard: avatarId === 7,
};
}, [avatarId]);
@@ -149,18 +149,30 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
</mesh>
)}
{/* CABEÇA MII (Esfera ovalada de pele) */}
{/* CABEÇA AVATAR (Esfera ovalada de pele) */}
<mesh position={[0, 0.1, 0]} scale={[1, 1.15, 1]}>
<sphereGeometry args={[0.08, 32, 32]} />
<meshStandardMaterial color={mii.skin} roughness={0.6} />
</mesh>
{/* CABELO */}
{/* CABELO MELHORADO */}
{mii.hairStyle === 'short' && (
<group>
{/* Topo do cabelo */}
<mesh position={[0, 0.15, -0.015]} scale={[1.05, 0.8, 1.05]}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Costeletas laterais */}
<mesh position={[-0.078, 0.11, 0.02]} scale={[0.2, 0.5, 0.4]}>
<sphereGeometry args={[0.03, 8, 8]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
<mesh position={[0.078, 0.11, 0.02]} scale={[0.2, 0.5, 0.4]}>
<sphereGeometry args={[0.03, 8, 8]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
</group>
)}
{mii.hairStyle === 'long' && (
@@ -170,13 +182,42 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
<sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Cabelo lateral caindo */}
<mesh position={[-0.075, 0.08, -0.02]} scale={[0.4, 1.2, 0.6]}>
{/* Franja frontal */}
<mesh position={[0, 0.16, 0.045]} scale={[1.3, 0.3, 0.4]} rotation={[0.1, 0, 0]}>
<sphereGeometry args={[0.04, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
<mesh position={[0.075, 0.08, -0.02]} scale={[0.4, 1.2, 0.6]}>
<sphereGeometry args={[0.04, 16, 16]} />
{/* Costeletas longas */}
<mesh position={[-0.076, 0.05, 0.01]} scale={[0.3, 1.6, 0.6]}>
<sphereGeometry args={[0.038, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
<mesh position={[0.076, 0.05, 0.01]} scale={[0.3, 1.6, 0.6]}>
<sphereGeometry args={[0.038, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
</group>
)}
{mii.hairStyle === 'modern' && (
<group>
{/* Cabelo Superior */}
<mesh position={[0, 0.16, -0.01]} scale={[1.05, 0.7, 1.05]}>
<sphereGeometry args={[0.08, 16, 16]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Topete Moderno */}
<mesh position={[0, 0.19, 0.02]} rotation={[-0.3, 0, 0]} scale={[0.8, 1.2, 0.8]}>
<coneGeometry args={[0.04, 0.08, 4]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
{/* Costeletas */}
<mesh position={[-0.076, 0.09, 0.01]} scale={[0.3, 1.0, 0.5]}>
<sphereGeometry args={[0.03, 8, 8]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
<mesh position={[0.076, 0.09, 0.01]} scale={[0.3, 1.0, 0.5]}>
<sphereGeometry args={[0.03, 8, 8]} />
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
</mesh>
</group>
@@ -214,22 +255,35 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
</group>
)}
{/* OLHOS */}
<mesh position={[-0.028, 0.11, 0.07]} rotation={[0.08, -0.15, 0]}>
<sphereGeometry args={[0.01, 8, 8]} />
<meshBasicMaterial color="#18181b" />
{/* OLHOS MELHORADOS (Mais expressivos e com brilho) */}
<group position={[-0.028, 0.11, 0.07]} rotation={[0.08, -0.15, 0]}>
<mesh>
<sphereGeometry args={[0.011, 16, 16]} />
<meshBasicMaterial color="#111827" />
</mesh>
<mesh position={[0.028, 0.11, 0.07]} rotation={[0.08, 0.15, 0]}>
<sphereGeometry args={[0.01, 8, 8]} />
<meshBasicMaterial color="#18181b" />
<mesh position={[0.004, 0.004, 0.008]}>
<sphereGeometry args={[0.004, 8, 8]} />
<meshBasicMaterial color="#ffffff" />
</mesh>
</group>
<group position={[0.028, 0.11, 0.07]} rotation={[0.08, 0.15, 0]}>
<mesh>
<sphereGeometry args={[0.011, 16, 16]} />
<meshBasicMaterial color="#111827" />
</mesh>
<mesh position={[0.004, 0.004, 0.008]}>
<sphereGeometry args={[0.004, 8, 8]} />
<meshBasicMaterial color="#ffffff" />
</mesh>
</group>
{/* SOBRANCELHAS */}
<mesh position={[-0.028, 0.125, 0.072]} rotation={[0.05, -0.15, 0.08]}>
<mesh position={[-0.028, 0.128, 0.072]} rotation={[0.05, -0.15, 0.08]}>
<boxGeometry args={[0.028, 0.005, 0.005]} />
<meshBasicMaterial color="#18181b" />
</mesh>
<mesh position={[0.028, 0.125, 0.072]} rotation={[0.05, 0.15, -0.08]}>
<mesh position={[0.028, 0.128, 0.072]} rotation={[0.05, 0.15, -0.08]}>
<boxGeometry args={[0.028, 0.005, 0.005]} />
<meshBasicMaterial color="#18181b" />
</mesh>
@@ -240,9 +294,19 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
<meshStandardMaterial color={mii.skin} roughness={0.6} />
</mesh>
{/* BOCA */}
<mesh position={[0, 0.065, 0.073]} rotation={[0.1, 0, 0]} scale={[1, 0.75, 1]}>
<torusGeometry args={[0.015, 0.004, 4, 16, Math.PI]} />
{/* BOCHECHAS ROSADAS (Deixa o avatar super amigável e feliz) */}
<mesh position={[-0.042, 0.08, 0.071]} scale={[1, 0.6, 0.2]}>
<sphereGeometry args={[0.01, 16, 16]} />
<meshBasicMaterial color="#f43f5e" transparent opacity={0.5} />
</mesh>
<mesh position={[0.042, 0.08, 0.071]} scale={[1, 0.6, 0.2]}>
<sphereGeometry args={[0.01, 16, 16]} />
<meshBasicMaterial color="#f43f5e" transparent opacity={0.5} />
</mesh>
{/* BOCA FELIZ E SORRIDENTE (Sem aparência de triste) */}
<mesh position={[0, 0.062, 0.073]} rotation={[0, 0, Math.PI]}>
<torusGeometry args={[0.014, 0.003, 8, 24, Math.PI]} />
<meshBasicMaterial color="#b91c1c" />
</mesh>
@@ -539,7 +603,7 @@ export default function MeetingRoom() {
const [selectedChair, setSelectedChair] = useState('1');
const [inputRoomId, setInputRoomId] = useState(roomId || '');
// Estados da Sala
// Estados da Sala (Conexão Supabase)
const [peers, setPeers] = useState<Record<string, PeerData>>({});
const [chatMessages, setChatMessages] = useState<{ id: string; sender: string; text: string; time: string }[]>([]);
const [newMessage, setNewMessage] = useState('');
@@ -562,13 +626,13 @@ export default function MeetingRoom() {
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
const chatScrollRef = useRef<HTMLDivElement>(null);
// Se o roomId mudar na url, atualiza o checkbox isPresenter
// Se o roomId mudar na url, atualiza o checkbox isPresenter e o inputRoomId
useEffect(() => {
setIsPresenter(!roomId);
setInputRoomId(roomId || '');
}, [roomId]);
// Simulação simples de detecção de áudio
// Simulação de detecção de áudio
useEffect(() => {
if (!inRoom || isMuted) {
setIsTalking(false);
@@ -586,6 +650,47 @@ export default function MeetingRoom() {
}
}, [chatMessages]);
// --- ESCUTA DO LOBBY EM TEMPO REAL ---
// Conecta ao canal antes do lobby finalizar para ler assentos e avatares ocupados na sala digitada
useEffect(() => {
if (!inputRoomId || inputRoomId.trim().length < 3) {
setPeers({});
return;
}
const roomIdUpper = inputRoomId.trim().toUpperCase();
console.log(`🔌 [Lobby Watcher] Escutando sala: ${roomIdUpper}`);
const tempChannel = supabase.channel(`meeting_room_${roomIdUpper}`, {
config: {
presence: { key: 'lobby_watcher_' + Math.random().toString(36).substring(3, 8) }
}
});
tempChannel
.on('presence', { event: 'sync' }, () => {
const state = tempChannel.presenceState();
const formattedPeers: Record<string, PeerData> = {};
Object.keys(state).forEach((chairKey) => {
// Ignora outros watchers do lobby
if (chairKey.startsWith('lobby_watcher_')) return;
const presences = state[chairKey] as unknown as PeerData[];
if (presences && presences.length > 0) {
formattedPeers[chairKey] = presences[0];
}
});
setPeers(formattedPeers);
})
.subscribe();
return () => {
tempChannel.unsubscribe();
};
}, [inputRoomId, inRoom]);
// Envia as atualizações de apresentação via Broadcast para a sala inteira
const handlePresentationChange = (updatedFields: Partial<PresentationState>) => {
if (!isPresenter || !channelRef.current) return;
@@ -600,12 +705,26 @@ export default function MeetingRoom() {
});
};
// Inicializa o Supabase Presence & Broadcast para conexões
// Inicializa o Supabase Presence & Broadcast para conexões oficiais
const connectToRoom = (targetRoomId: string) => {
if (!name.trim()) {
toast.error('Digite seu nome primeiro');
return;
}
// Validar se o avatar já está em uso por outra pessoa
const avatarInUse = Object.values(peers).find((p) => p.avatarId === selectedAvatar);
if (avatarInUse) {
toast.error(`O avatar "${AVATAR_PRESETS[selectedAvatar - 1].name}" já está em uso por ${avatarInUse.username}. Escolha outro!`);
return;
}
// Validar se a cadeira está ocupada
if (Object.keys(peers).includes(selectedChair)) {
toast.error('O assento selecionado já está ocupado por outro participante. Por favor, selecione outro assento.');
return;
}
const finalRoomId = targetRoomId.trim().toUpperCase() || Math.random().toString(36).substring(3, 8).toUpperCase();
setRoomCode(finalRoomId);
@@ -628,6 +747,7 @@ export default function MeetingRoom() {
let count = 0;
Object.keys(state).forEach((chairKey) => {
if (chairKey.startsWith('lobby_watcher_')) return;
const presences = state[chairKey] as unknown as PeerData[];
if (presences && presences.length > 0) {
formattedPeers[chairKey] = presences[0];
@@ -638,9 +758,11 @@ export default function MeetingRoom() {
setActiveUsersCount(count);
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
if (key.startsWith('lobby_watcher_')) return;
toast.info(`"${(newPresences[0] as unknown as PeerData)?.username}" sentou-se no assento ${key}`);
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
if (key.startsWith('lobby_watcher_')) return;
toast.info(`"${(leftPresences[0] as unknown as PeerData)?.username}" liberou o assento ${key}`);
});
@@ -712,9 +834,14 @@ export default function MeetingRoom() {
};
const occupiedChairs = useMemo(() => {
return Object.keys(peers);
return Object.keys(peers).filter(k => !k.startsWith('lobby_watcher_'));
}, [peers]);
// Checar se o avatar selecionado no lobby está em uso
const isAvatarInUse = useMemo(() => {
return Object.values(peers).find((p) => p.avatarId === selectedAvatar);
}, [peers, selectedAvatar]);
const nextAvatar = () => {
setSelectedAvatar((prev) => (prev === 10 ? 1 : prev + 1));
};
@@ -726,6 +853,8 @@ export default function MeetingRoom() {
if (!inRoom) {
const currentAvatar = AVATAR_PRESETS[selectedAvatar - 1];
const canJoin = name.trim().length > 0 && !isAvatarInUse && !occupiedChairs.includes(selectedChair);
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-background grid-industrial p-6">
<div className="absolute top-6 left-6 flex items-center gap-2">
@@ -741,41 +870,54 @@ export default function MeetingRoom() {
Lobby de <span className="text-primary">Reunião Virtual</span>
</h2>
<p className="mt-1 text-xs font-mono uppercase tracking-wider text-muted-foreground">
Configure suas credenciais e escolha seu visual Mii
Configure suas credenciais e escolha seu avatar
</p>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-12">
{/* Avatar Mii Selector (Lado Esquerdo - 5 colunas) */}
{/* Seletor de Avatar (Lado Esquerdo - 5 colunas) */}
<div className="flex flex-col items-center justify-center rounded-xl border bg-muted/30 p-5 md:col-span-5 relative">
<span className="absolute top-3 left-4 font-mono text-[9px] uppercase tracking-widest text-muted-foreground">Visual Mii</span>
<span className="absolute top-3 left-4 font-mono text-[9px] uppercase tracking-widest text-muted-foreground">Visual Avatar</span>
{/* Enquadramento centralizado perfeito (Camera apontando e fov adequado) */}
<div className="h-40 w-full flex items-center justify-center relative bg-slate-900/60 rounded-lg overflow-hidden border border-border">
<Canvas camera={{ position: [0, 0.16, 0.28], fov: 40 }}>
<Canvas camera={{ position: [0, 0.11, 0.28], fov: 42 }}>
<ambientLight intensity={0.8} />
<directionalLight position={[2, 3, 2]} intensity={1.5} />
<pointLight position={[0, 0.15, 0.2]} intensity={1} color={currentAvatar.color} />
<MiiAvatar avatarId={selectedAvatar} username={name || 'Mii'} position={[0, 0, 0]} isTalking={true} />
<MiiAvatar avatarId={selectedAvatar} username={name || 'Você'} position={[0, 0, 0]} isTalking={true} />
</Canvas>
<Button variant="ghost" size="icon" className="absolute left-2 h-8 w-8 rounded-full border bg-background/50 hover:bg-background" onClick={prevAvatar}>
{/* Aviso se o avatar estiver em uso na sala */}
{isAvatarInUse && (
<div className="absolute inset-0 bg-black/75 backdrop-blur-sm flex flex-col items-center justify-center text-center p-3 animate-in fade-in duration-200">
<ShieldAlert className="h-7 w-7 text-destructive animate-bounce mb-1" />
<span className="font-mono text-[10px] font-bold text-destructive uppercase tracking-wider">Avatar em Uso</span>
<span className="text-[9px] text-slate-300 mt-1 max-w-xs px-2 leading-snug">
Este visual está ocupado por <strong className="text-primary uppercase block truncate">{isAvatarInUse.username}</strong>
</span>
</div>
)}
<Button variant="ghost" size="icon" className="absolute left-2 h-8 w-8 rounded-full border bg-background/50 hover:bg-background z-10" onClick={prevAvatar}>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="absolute right-2 h-8 w-8 rounded-full border bg-background/50 hover:bg-background" onClick={nextAvatar}>
<Button variant="ghost" size="icon" className="absolute right-2 h-8 w-8 rounded-full border bg-background/50 hover:bg-background z-10" onClick={nextAvatar}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="mt-3 text-center">
<span className="font-mono text-xs font-bold uppercase tracking-wider text-foreground">
<span className="font-mono text-xs font-bold uppercase tracking-wider text-primary">
{currentAvatar.name}
</span>
<p className="text-[9px] text-muted-foreground mt-0.5">{currentAvatar.desc}</p>
</div>
{/* Nome */}
<div className="mt-4 w-full space-y-2">
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground">Seu Nome (Máx 10 letras)</label>
{/* Formulário de Identificação: Nome, Cargo e Empresa agrupados sequencialmente na esquerda */}
<div className="mt-4 w-full space-y-3">
<div className="space-y-1">
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground block">Seu Nome (Máx 10 letras)</label>
<Input
type="text"
maxLength={10}
@@ -785,54 +927,56 @@ export default function MeetingRoom() {
className="font-semibold text-center uppercase font-mono tracking-wider text-xs h-9"
/>
</div>
</div>
{/* Credenciais, Assento e Conexão (Lado Direito - 7 colunas) */}
<div className="flex flex-col justify-between md:col-span-7 space-y-5">
{/* Form de Cargo e Empresa */}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground">Cargo / Função</label>
<div className="space-y-1">
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground block">Cargo / Função</label>
<Input
type="text"
placeholder="Ex: Eng. Civil"
value={cargo}
onChange={(e) => setCargo(e.target.value)}
className="text-xs h-9"
className="text-xs h-9 text-center"
/>
</div>
<div className="space-y-1.5">
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground">Empresa</label>
<div className="space-y-1">
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground block">Empresa</label>
<Input
type="text"
placeholder="Ex: Steel Co."
value={empresa}
onChange={(e) => setEmpresa(e.target.value)}
className="text-xs h-9"
className="text-xs h-9 text-center"
/>
</div>
</div>
</div>
{/* Seleção do Assento */}
{/* Seleção do Assento, Host e Conexão (Lado Direito - 7 colunas) */}
<div className="flex flex-col justify-between md:col-span-7 space-y-5">
{/* Seleção do Assento (Exibe quem está ocupando em tempo real) */}
<div className="space-y-2">
<span className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground">Selecione seu Assento na Mesa</span>
<div className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{CHAIRS.map((chair) => {
const isOccupied = occupiedChairs.includes(chair.id);
const occupier = Object.values(peers).find((p) => p.chairId === chair.id);
const isOccupied = !!occupier;
const isSelected = selectedChair === chair.id;
return (
<Button
key={chair.id}
variant={isSelected ? 'default' : 'outline'}
className={`h-11 flex-col p-1 gap-0.5 text-[9px] font-mono ${
isOccupied ? 'border-destructive/30 text-destructive bg-destructive/5 cursor-not-allowed hover:bg-destructive/5' : ''
className={`h-12 flex-col p-1 gap-0.5 text-[9px] font-mono ${
isOccupied ? 'border-destructive/40 text-destructive bg-destructive/10 cursor-not-allowed hover:bg-destructive/10' : ''
}`}
disabled={isOccupied}
onClick={() => setSelectedChair(chair.id)}
>
<span className="font-semibold">{chair.name}</span>
<span className="text-[8px] opacity-75">
{isOccupied ? 'Ocupado' : isSelected ? 'Escolhido' : 'Livre'}
<span className={`text-[8px] truncate max-w-full font-bold uppercase ${
isOccupied ? 'text-destructive' : isSelected ? 'text-primary-foreground' : 'text-muted-foreground'
}`}>
{isOccupied ? occupier.username : isSelected ? 'Escolhido' : 'Livre'}
</span>
</Button>
);
@@ -840,7 +984,7 @@ export default function MeetingRoom() {
</div>
</div>
{/* Papel e Conexão */}
{/* Papel e Código de Conexão */}
<div className="rounded-xl border bg-muted/20 p-4 space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
@@ -871,10 +1015,20 @@ export default function MeetingRoom() {
<Button variant="outline" className="flex-1 font-mono text-xs h-10" onClick={() => navigate('/')}>
Cancelar
</Button>
<Button className="flex-1 glow-primary font-mono text-xs h-10 gap-2" onClick={() => connectToRoom(inputRoomId)}>
<Button
className="flex-1 glow-primary font-mono text-xs h-10 gap-2"
disabled={!canJoin}
onClick={() => connectToRoom(inputRoomId)}
>
Acessar Reunião <ArrowRight className="h-4 w-4" />
</Button>
</div>
{isAvatarInUse && (
<p className="text-[10px] text-destructive font-mono text-center">
* Escolha outro avatar para liberar o acesso.
</p>
)}
</div>
</div>
</div>
@@ -892,7 +1046,7 @@ export default function MeetingRoom() {
<Canvas camera={{ position: [0, 2.5, 2.8], fov: 45 }} shadows gl={{ localClippingEnabled: true }}>
<ThreeMeetingRoomScene currentActiveModel={activeModel} presentationState={presentationState} />
{/* Renderizar avatares de TODOS na sala (incluindo o próprio usuário para ele se ver sentado na cadeira) */}
{/* Renderizar avatares de TODOS na sala */}
{Object.keys(peers).map((chairKey) => {
const peer = peers[chairKey];
const chairConf = CHAIRS.find((c) => c.id === peer.chairId);