🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 00:34:30
This commit is contained in:
+240
-86
@@ -12,11 +12,11 @@ import { toast } from 'sonner';
|
|||||||
import {
|
import {
|
||||||
Users, MessageSquare, Mic, MicOff, Home, Send,
|
Users, MessageSquare, Mic, MicOff, Home, Send,
|
||||||
ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut,
|
ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut,
|
||||||
RotateCw, Award
|
RotateCw, Award, ShieldAlert
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Interfaces tipadas para evitar any
|
// Interfaces tipadas para evitar any
|
||||||
interface MiiPreset {
|
interface AvatarPreset {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
desc: 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] },
|
{ id: '8', name: 'Assento 8', pos: [-1.2, 0.95, 1.45], lookAt: [0, 0.95, 0] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const AVATAR_PRESETS: MiiPreset[] = [
|
const AVATAR_PRESETS: AvatarPreset[] = [
|
||||||
{ id: 1, name: 'Mii Cyber', desc: 'Cabelo curto preto e óculos vermelhos', color: '#ef4444' },
|
{ id: 1, name: '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: 2, name: '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: 3, name: '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: 4, name: '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: 5, name: '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: 6, name: '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: 7, name: '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: 8, name: 'Sun', desc: 'Cabelo raspado e óculos escuros pretos', color: '#1e293b' },
|
||||||
{ id: 9, name: 'Mii Synth', desc: 'Cabelo roxo ondulado moderno', color: '#a855f7' },
|
{ id: 9, name: 'Synth', desc: 'Cabelo roxo ondulado moderno', color: '#a855f7' },
|
||||||
{ id: 10, name: 'Mii Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' },
|
{ id: 10, name: 'Straw', desc: 'Chapéu de palha do campo clássico', color: '#ca8a04' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5'];
|
const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5'];
|
||||||
@@ -88,7 +88,7 @@ const getAvatarBgClass = (avatarId: number): string => {
|
|||||||
|
|
||||||
// --- Subcomponentes 3D ---
|
// --- 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>) {
|
function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: number; username: string; isTalking?: boolean } & Record<string, unknown>) {
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
|
|||||||
capColor: '#3b82f6',
|
capColor: '#3b82f6',
|
||||||
hasHat: avatarId === 10,
|
hasHat: avatarId === 10,
|
||||||
hatColor: '#b45309',
|
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,
|
beard: avatarId === 7,
|
||||||
};
|
};
|
||||||
}, [avatarId]);
|
}, [avatarId]);
|
||||||
@@ -149,18 +149,30 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
|
|||||||
</mesh>
|
</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]}>
|
<mesh position={[0, 0.1, 0]} scale={[1, 1.15, 1]}>
|
||||||
<sphereGeometry args={[0.08, 32, 32]} />
|
<sphereGeometry args={[0.08, 32, 32]} />
|
||||||
<meshStandardMaterial color={mii.skin} roughness={0.6} />
|
<meshStandardMaterial color={mii.skin} roughness={0.6} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* CABELO */}
|
{/* CABELO MELHORADO */}
|
||||||
{mii.hairStyle === 'short' && (
|
{mii.hairStyle === 'short' && (
|
||||||
<mesh position={[0, 0.15, -0.015]} scale={[1.05, 0.8, 1.05]}>
|
<group>
|
||||||
<sphereGeometry args={[0.08, 16, 16]} />
|
{/* Topo do cabelo */}
|
||||||
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
|
<mesh position={[0, 0.15, -0.015]} scale={[1.05, 0.8, 1.05]}>
|
||||||
</mesh>
|
<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' && (
|
{mii.hairStyle === 'long' && (
|
||||||
@@ -170,13 +182,42 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
|
|||||||
<sphereGeometry args={[0.08, 16, 16]} />
|
<sphereGeometry args={[0.08, 16, 16]} />
|
||||||
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
|
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* Cabelo lateral caindo */}
|
{/* Franja frontal */}
|
||||||
<mesh position={[-0.075, 0.08, -0.02]} scale={[0.4, 1.2, 0.6]}>
|
<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]} />
|
<sphereGeometry args={[0.04, 16, 16]} />
|
||||||
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
|
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0.075, 0.08, -0.02]} scale={[0.4, 1.2, 0.6]}>
|
{/* Costeletas longas */}
|
||||||
<sphereGeometry args={[0.04, 16, 16]} />
|
<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} />
|
<meshStandardMaterial color={mii.hairColor} roughness={0.8} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
@@ -214,22 +255,35 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
|
|||||||
</group>
|
</group>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* OLHOS */}
|
{/* OLHOS MELHORADOS (Mais expressivos e com brilho) */}
|
||||||
<mesh position={[-0.028, 0.11, 0.07]} rotation={[0.08, -0.15, 0]}>
|
<group position={[-0.028, 0.11, 0.07]} rotation={[0.08, -0.15, 0]}>
|
||||||
<sphereGeometry args={[0.01, 8, 8]} />
|
<mesh>
|
||||||
<meshBasicMaterial color="#18181b" />
|
<sphereGeometry args={[0.011, 16, 16]} />
|
||||||
</mesh>
|
<meshBasicMaterial color="#111827" />
|
||||||
<mesh position={[0.028, 0.11, 0.07]} rotation={[0.08, 0.15, 0]}>
|
</mesh>
|
||||||
<sphereGeometry args={[0.01, 8, 8]} />
|
<mesh position={[0.004, 0.004, 0.008]}>
|
||||||
<meshBasicMaterial color="#18181b" />
|
<sphereGeometry args={[0.004, 8, 8]} />
|
||||||
</mesh>
|
<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 */}
|
{/* 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]} />
|
<boxGeometry args={[0.028, 0.005, 0.005]} />
|
||||||
<meshBasicMaterial color="#18181b" />
|
<meshBasicMaterial color="#18181b" />
|
||||||
</mesh>
|
</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]} />
|
<boxGeometry args={[0.028, 0.005, 0.005]} />
|
||||||
<meshBasicMaterial color="#18181b" />
|
<meshBasicMaterial color="#18181b" />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -240,9 +294,19 @@ function MiiAvatar({ avatarId, username, isTalking, ...props }: { avatarId: numb
|
|||||||
<meshStandardMaterial color={mii.skin} roughness={0.6} />
|
<meshStandardMaterial color={mii.skin} roughness={0.6} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* BOCA */}
|
{/* BOCHECHAS ROSADAS (Deixa o avatar super amigável e feliz) */}
|
||||||
<mesh position={[0, 0.065, 0.073]} rotation={[0.1, 0, 0]} scale={[1, 0.75, 1]}>
|
<mesh position={[-0.042, 0.08, 0.071]} scale={[1, 0.6, 0.2]}>
|
||||||
<torusGeometry args={[0.015, 0.004, 4, 16, Math.PI]} />
|
<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" />
|
<meshBasicMaterial color="#b91c1c" />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
@@ -539,7 +603,7 @@ export default function MeetingRoom() {
|
|||||||
const [selectedChair, setSelectedChair] = useState('1');
|
const [selectedChair, setSelectedChair] = useState('1');
|
||||||
const [inputRoomId, setInputRoomId] = useState(roomId || '');
|
const [inputRoomId, setInputRoomId] = useState(roomId || '');
|
||||||
|
|
||||||
// Estados da Sala
|
// Estados da Sala (Conexão Supabase)
|
||||||
const [peers, setPeers] = useState<Record<string, PeerData>>({});
|
const [peers, setPeers] = useState<Record<string, PeerData>>({});
|
||||||
const [chatMessages, setChatMessages] = useState<{ id: string; sender: string; text: string; time: string }[]>([]);
|
const [chatMessages, setChatMessages] = useState<{ id: string; sender: string; text: string; time: string }[]>([]);
|
||||||
const [newMessage, setNewMessage] = useState('');
|
const [newMessage, setNewMessage] = useState('');
|
||||||
@@ -562,13 +626,13 @@ export default function MeetingRoom() {
|
|||||||
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
|
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
|
||||||
const chatScrollRef = useRef<HTMLDivElement>(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(() => {
|
useEffect(() => {
|
||||||
setIsPresenter(!roomId);
|
setIsPresenter(!roomId);
|
||||||
setInputRoomId(roomId || '');
|
setInputRoomId(roomId || '');
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
// Simulação simples de detecção de áudio
|
// Simulação de detecção de áudio
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inRoom || isMuted) {
|
if (!inRoom || isMuted) {
|
||||||
setIsTalking(false);
|
setIsTalking(false);
|
||||||
@@ -586,6 +650,47 @@ export default function MeetingRoom() {
|
|||||||
}
|
}
|
||||||
}, [chatMessages]);
|
}, [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
|
// Envia as atualizações de apresentação via Broadcast para a sala inteira
|
||||||
const handlePresentationChange = (updatedFields: Partial<PresentationState>) => {
|
const handlePresentationChange = (updatedFields: Partial<PresentationState>) => {
|
||||||
if (!isPresenter || !channelRef.current) return;
|
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) => {
|
const connectToRoom = (targetRoomId: string) => {
|
||||||
if (!name.trim()) {
|
if (!name.trim()) {
|
||||||
toast.error('Digite seu nome primeiro');
|
toast.error('Digite seu nome primeiro');
|
||||||
return;
|
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();
|
const finalRoomId = targetRoomId.trim().toUpperCase() || Math.random().toString(36).substring(3, 8).toUpperCase();
|
||||||
setRoomCode(finalRoomId);
|
setRoomCode(finalRoomId);
|
||||||
|
|
||||||
@@ -628,6 +747,7 @@ export default function MeetingRoom() {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
Object.keys(state).forEach((chairKey) => {
|
Object.keys(state).forEach((chairKey) => {
|
||||||
|
if (chairKey.startsWith('lobby_watcher_')) return;
|
||||||
const presences = state[chairKey] as unknown as PeerData[];
|
const presences = state[chairKey] as unknown as PeerData[];
|
||||||
if (presences && presences.length > 0) {
|
if (presences && presences.length > 0) {
|
||||||
formattedPeers[chairKey] = presences[0];
|
formattedPeers[chairKey] = presences[0];
|
||||||
@@ -638,9 +758,11 @@ export default function MeetingRoom() {
|
|||||||
setActiveUsersCount(count);
|
setActiveUsersCount(count);
|
||||||
})
|
})
|
||||||
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
|
.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}`);
|
toast.info(`"${(newPresences[0] as unknown as PeerData)?.username}" sentou-se no assento ${key}`);
|
||||||
})
|
})
|
||||||
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
|
.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}`);
|
toast.info(`"${(leftPresences[0] as unknown as PeerData)?.username}" liberou o assento ${key}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -712,9 +834,14 @@ export default function MeetingRoom() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const occupiedChairs = useMemo(() => {
|
const occupiedChairs = useMemo(() => {
|
||||||
return Object.keys(peers);
|
return Object.keys(peers).filter(k => !k.startsWith('lobby_watcher_'));
|
||||||
}, [peers]);
|
}, [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 = () => {
|
const nextAvatar = () => {
|
||||||
setSelectedAvatar((prev) => (prev === 10 ? 1 : prev + 1));
|
setSelectedAvatar((prev) => (prev === 10 ? 1 : prev + 1));
|
||||||
};
|
};
|
||||||
@@ -726,6 +853,8 @@ export default function MeetingRoom() {
|
|||||||
|
|
||||||
if (!inRoom) {
|
if (!inRoom) {
|
||||||
const currentAvatar = AVATAR_PRESETS[selectedAvatar - 1];
|
const currentAvatar = AVATAR_PRESETS[selectedAvatar - 1];
|
||||||
|
const canJoin = name.trim().length > 0 && !isAvatarInUse && !occupiedChairs.includes(selectedChair);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center bg-background grid-industrial p-6">
|
<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">
|
<div className="absolute top-6 left-6 flex items-center gap-2">
|
||||||
@@ -741,98 +870,113 @@ export default function MeetingRoom() {
|
|||||||
Lobby de <span className="text-primary">Reunião Virtual</span>
|
Lobby de <span className="text-primary">Reunião Virtual</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-1 text-xs font-mono uppercase tracking-wider text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-12">
|
<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">
|
<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">
|
<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} />
|
<ambientLight intensity={0.8} />
|
||||||
<directionalLight position={[2, 3, 2]} intensity={1.5} />
|
<directionalLight position={[2, 3, 2]} intensity={1.5} />
|
||||||
<pointLight position={[0, 0.15, 0.2]} intensity={1} color={currentAvatar.color} />
|
<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>
|
</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" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</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" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 text-center">
|
<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}
|
{currentAvatar.name}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-[9px] text-muted-foreground mt-0.5">{currentAvatar.desc}</p>
|
<p className="text-[9px] text-muted-foreground mt-0.5">{currentAvatar.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nome */}
|
{/* Formulário de Identificação: Nome, Cargo e Empresa agrupados sequencialmente na esquerda */}
|
||||||
<div className="mt-4 w-full space-y-2">
|
<div className="mt-4 w-full space-y-3">
|
||||||
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground">Seu Nome (Máx 10 letras)</label>
|
<div className="space-y-1">
|
||||||
<Input
|
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground block">Seu Nome (Máx 10 letras)</label>
|
||||||
type="text"
|
<Input
|
||||||
maxLength={10}
|
type="text"
|
||||||
placeholder="Nome do usuário"
|
maxLength={10}
|
||||||
value={name}
|
placeholder="Nome do usuário"
|
||||||
onChange={(e) => setName(e.target.value)}
|
value={name}
|
||||||
className="font-semibold text-center uppercase font-mono tracking-wider text-xs h-9"
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
className="font-semibold text-center uppercase font-mono tracking-wider text-xs h-9"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Credenciais, Assento e Conexão (Lado Direito - 7 colunas) */}
|
<div className="space-y-1">
|
||||||
<div className="flex flex-col justify-between md:col-span-7 space-y-5">
|
<label className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground block">Cargo / Função</label>
|
||||||
{/* 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>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Ex: Eng. Civil"
|
placeholder="Ex: Eng. Civil"
|
||||||
value={cargo}
|
value={cargo}
|
||||||
onChange={(e) => setCargo(e.target.value)}
|
onChange={(e) => setCargo(e.target.value)}
|
||||||
className="text-xs h-9"
|
className="text-xs h-9 text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Ex: Steel Co."
|
placeholder="Ex: Steel Co."
|
||||||
value={empresa}
|
value={empresa}
|
||||||
onChange={(e) => setEmpresa(e.target.value)}
|
onChange={(e) => setEmpresa(e.target.value)}
|
||||||
className="text-xs h-9"
|
className="text-xs h-9 text-center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="space-y-2">
|
||||||
<span className="font-mono text-[9px] uppercase tracking-wider text-muted-foreground">Selecione seu Assento na Mesa</span>
|
<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) => {
|
{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;
|
const isSelected = selectedChair === chair.id;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={chair.id}
|
key={chair.id}
|
||||||
variant={isSelected ? 'default' : 'outline'}
|
variant={isSelected ? 'default' : 'outline'}
|
||||||
className={`h-11 flex-col p-1 gap-0.5 text-[9px] font-mono ${
|
className={`h-12 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' : ''
|
isOccupied ? 'border-destructive/40 text-destructive bg-destructive/10 cursor-not-allowed hover:bg-destructive/10' : ''
|
||||||
}`}
|
}`}
|
||||||
disabled={isOccupied}
|
disabled={isOccupied}
|
||||||
onClick={() => setSelectedChair(chair.id)}
|
onClick={() => setSelectedChair(chair.id)}
|
||||||
>
|
>
|
||||||
<span className="font-semibold">{chair.name}</span>
|
<span className="font-semibold">{chair.name}</span>
|
||||||
<span className="text-[8px] opacity-75">
|
<span className={`text-[8px] truncate max-w-full font-bold uppercase ${
|
||||||
{isOccupied ? 'Ocupado' : isSelected ? 'Escolhido' : 'Livre'}
|
isOccupied ? 'text-destructive' : isSelected ? 'text-primary-foreground' : 'text-muted-foreground'
|
||||||
|
}`}>
|
||||||
|
{isOccupied ? occupier.username : isSelected ? 'Escolhido' : 'Livre'}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -840,7 +984,7 @@ export default function MeetingRoom() {
|
|||||||
</div>
|
</div>
|
||||||
</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="rounded-xl border bg-muted/20 p-4 space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -871,10 +1015,20 @@ export default function MeetingRoom() {
|
|||||||
<Button variant="outline" className="flex-1 font-mono text-xs h-10" onClick={() => navigate('/')}>
|
<Button variant="outline" className="flex-1 font-mono text-xs h-10" onClick={() => navigate('/')}>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</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" />
|
Acessar Reunião <ArrowRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isAvatarInUse && (
|
||||||
|
<p className="text-[10px] text-destructive font-mono text-center">
|
||||||
|
* Escolha outro avatar para liberar o acesso.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 }}>
|
<Canvas camera={{ position: [0, 2.5, 2.8], fov: 45 }} shadows gl={{ localClippingEnabled: true }}>
|
||||||
<ThreeMeetingRoomScene currentActiveModel={activeModel} presentationState={presentationState} />
|
<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) => {
|
{Object.keys(peers).map((chairKey) => {
|
||||||
const peer = peers[chairKey];
|
const peer = peers[chairKey];
|
||||||
const chairConf = CHAIRS.find((c) => c.id === peer.chairId);
|
const chairConf = CHAIRS.find((c) => c.id === peer.chairId);
|
||||||
|
|||||||
Reference in New Issue
Block a user