🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 00:34:30
This commit is contained in:
+224
-70
@@ -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 já 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);
|
||||
|
||||
Reference in New Issue
Block a user