diff --git a/public/avatars/avatar_1.png b/public/avatars/avatar_1.png new file mode 100644 index 0000000..5380607 Binary files /dev/null and b/public/avatars/avatar_1.png differ diff --git a/public/avatars/avatar_10.png b/public/avatars/avatar_10.png new file mode 100644 index 0000000..c0d01e3 Binary files /dev/null and b/public/avatars/avatar_10.png differ diff --git a/public/avatars/avatar_11.png b/public/avatars/avatar_11.png new file mode 100644 index 0000000..66e7f4e Binary files /dev/null and b/public/avatars/avatar_11.png differ diff --git a/public/avatars/avatar_12.png b/public/avatars/avatar_12.png new file mode 100644 index 0000000..f64354b Binary files /dev/null and b/public/avatars/avatar_12.png differ diff --git a/public/avatars/avatar_13.png b/public/avatars/avatar_13.png new file mode 100644 index 0000000..732fe6f Binary files /dev/null and b/public/avatars/avatar_13.png differ diff --git a/public/avatars/avatar_14.png b/public/avatars/avatar_14.png new file mode 100644 index 0000000..8ad1480 Binary files /dev/null and b/public/avatars/avatar_14.png differ diff --git a/public/avatars/avatar_15.png b/public/avatars/avatar_15.png new file mode 100644 index 0000000..d00d6b0 Binary files /dev/null and b/public/avatars/avatar_15.png differ diff --git a/public/avatars/avatar_16.png b/public/avatars/avatar_16.png new file mode 100644 index 0000000..1228c87 Binary files /dev/null and b/public/avatars/avatar_16.png differ diff --git a/public/avatars/avatar_17.png b/public/avatars/avatar_17.png new file mode 100644 index 0000000..bb40bb6 Binary files /dev/null and b/public/avatars/avatar_17.png differ diff --git a/public/avatars/avatar_18.png b/public/avatars/avatar_18.png new file mode 100644 index 0000000..1dcc343 Binary files /dev/null and b/public/avatars/avatar_18.png differ diff --git a/public/avatars/avatar_19.png b/public/avatars/avatar_19.png new file mode 100644 index 0000000..818faa6 Binary files /dev/null and b/public/avatars/avatar_19.png differ diff --git a/public/avatars/avatar_2.png b/public/avatars/avatar_2.png new file mode 100644 index 0000000..13bafea Binary files /dev/null and b/public/avatars/avatar_2.png differ diff --git a/public/avatars/avatar_20.png b/public/avatars/avatar_20.png new file mode 100644 index 0000000..88cead8 Binary files /dev/null and b/public/avatars/avatar_20.png differ diff --git a/public/avatars/avatar_3.png b/public/avatars/avatar_3.png new file mode 100644 index 0000000..cc2435d Binary files /dev/null and b/public/avatars/avatar_3.png differ diff --git a/public/avatars/avatar_4.png b/public/avatars/avatar_4.png new file mode 100644 index 0000000..8f1118c Binary files /dev/null and b/public/avatars/avatar_4.png differ diff --git a/public/avatars/avatar_5.png b/public/avatars/avatar_5.png new file mode 100644 index 0000000..cafeb74 Binary files /dev/null and b/public/avatars/avatar_5.png differ diff --git a/public/avatars/avatar_6.png b/public/avatars/avatar_6.png new file mode 100644 index 0000000..1b7dd67 Binary files /dev/null and b/public/avatars/avatar_6.png differ diff --git a/public/avatars/avatar_7.png b/public/avatars/avatar_7.png new file mode 100644 index 0000000..b713d4a Binary files /dev/null and b/public/avatars/avatar_7.png differ diff --git a/public/avatars/avatar_8.png b/public/avatars/avatar_8.png new file mode 100644 index 0000000..9f23d15 Binary files /dev/null and b/public/avatars/avatar_8.png differ diff --git a/public/avatars/avatar_9.png b/public/avatars/avatar_9.png new file mode 100644 index 0000000..31552ef Binary files /dev/null and b/public/avatars/avatar_9.png differ diff --git a/src/pages/MeetingRoom.tsx b/src/pages/MeetingRoom.tsx index b14cfd0..ff85766 100644 --- a/src/pages/MeetingRoom.tsx +++ b/src/pages/MeetingRoom.tsx @@ -43,7 +43,8 @@ import { toast } from 'sonner'; import { Users, MessageSquare, Mic, MicOff, Home, Send, ArrowRight, ChevronLeft, ChevronRight, Share2, LogOut, - RotateCw, Award, ShieldAlert, Maximize2, Minimize2, Box + RotateCw, Award, ShieldAlert, Maximize2, Minimize2, Box, + Settings, Check } from 'lucide-react'; // Interfaces tipadas para evitar any @@ -75,6 +76,7 @@ interface PeerData { cargo: string; empresa: string; presence_ref: string; + allowedAvatars?: number[]; } // Detector nativo de suporte ao WebGL @@ -112,18 +114,28 @@ const CHAIRS = [ ]; const AVATAR_PRESETS: AvatarPreset[] = [ - // Homens (5) - { id: 1, name: 'Carlos', desc: 'Estilo corporativo masculino clássico', url: 'https://models.readyplayer.me/65851410d54020a5605d8f63.glb?bodyType=halfbody', color: '#ef4444' }, - { id: 2, name: 'Lucas', desc: 'Estilo moderno masculino com barba', url: 'https://models.readyplayer.me/6585144b248a3064be486a4f.glb?bodyType=halfbody', color: '#b45309' }, - { id: 3, name: 'Gabriel', desc: 'Jovem e sorridente com cabelo escuro', url: 'https://models.readyplayer.me/65851480f2d52f672d6199bc.glb?bodyType=halfbody', color: '#eab308' }, - { id: 4, name: 'Marcos', desc: 'Visual executivo com óculos de grau', url: 'https://models.readyplayer.me/658514b8d54020a5605d92df.glb?bodyType=halfbody', color: '#3b82f6' }, - { id: 5, name: 'Ricardo', desc: 'Design casual moderno e cabelos grisalhos', url: 'https://models.readyplayer.me/658514ec248a3064be486f0b.glb?bodyType=halfbody', color: '#f97316' }, - // Mulheres (5) - { id: 6, name: 'Juliana', desc: 'Visual feminino clássico executivo', url: 'https://models.readyplayer.me/65851336d54020a5605d820f.glb?bodyType=halfbody', color: '#10b981' }, - { id: 7, name: 'Fernanda', desc: 'Cabelo longo escuro e roupa corporativa', url: 'https://models.readyplayer.me/65851375248a3064be485d1d.glb?bodyType=halfbody', color: '#78350f' }, - { id: 8, name: 'Camila', desc: 'Estilo moderno e cabelos cacheados', url: 'https://models.readyplayer.me/658513a9f2d52f672d61937f.glb?bodyType=halfbody', color: '#1e293b' }, - { id: 9, name: 'Beatriz', desc: 'Cabelo ruivo e trajes executivos finos', url: 'https://models.readyplayer.me/658513dbf2d52f672d6195be.glb?bodyType=halfbody', color: '#a855f7' }, - { id: 10, name: 'Amanda', desc: 'Visual jovem loira e roupas elegantes', url: 'https://models.readyplayer.me/65851403248a3064be48651a.glb?bodyType=halfbody', color: '#ca8a04' }, + // Masculinos (1 a 10) + { id: 1, name: 'Leo', desc: 'Negro, cabelo curto preto', url: '/avatars/avatar_1.png', color: '#ef4444' }, + { id: 2, name: 'Marcus', desc: 'Loiro, cabelo curto', url: '/avatars/avatar_2.png', color: '#b45309' }, + { id: 3, name: 'Arthur', desc: 'Ruivo com barba', url: '/avatars/avatar_3.png', color: '#eab308' }, + { id: 4, name: 'Jin', desc: 'Asiático, cabelo preto liso', url: '/avatars/avatar_4.png', color: '#3b82f6' }, + { id: 5, name: 'Mateo', desc: 'Castanho, cabelo cacheado', url: '/avatars/avatar_5.png', color: '#f97316' }, + { id: 6, name: 'Malik', desc: 'Negro, careca com barba', url: '/avatars/avatar_6.png', color: '#84cc16' }, + { id: 7, name: 'Oliver', desc: 'Cabelo grisalho com óculos', url: '/avatars/avatar_7.png', color: '#06b6d4' }, + { id: 8, name: 'Carlos', desc: 'Latino com bigode', url: '/avatars/avatar_8.png', color: '#6366f1' }, + { id: 9, name: 'Yuki', desc: 'Cabelo espetado azul escuro', url: '/avatars/avatar_9.png', color: '#a855f7' }, + { id: 10, name: 'Elijah', desc: 'Negro com dreadlocks', url: '/avatars/avatar_10.png', color: '#ec4899' }, + // Femininos (11 a 20) + { id: 11, name: 'Sophia', desc: 'Morena, cabelo longo castanho', url: '/avatars/avatar_11.png', color: '#10b981' }, + { id: 12, name: 'Aisha', desc: 'Negra com turbante/lenço', url: '/avatars/avatar_12.png', color: '#78350f' }, + { id: 13, name: 'Isabella', desc: 'Loira, cabelo longo ondulado', url: '/avatars/avatar_13.png', color: '#1e293b' }, + { id: 14, name: 'Chloe', desc: 'Ruiva, corte canal/bob', url: '/avatars/avatar_14.png', color: '#ca8a04' }, + { id: 15, name: 'Mei', desc: 'Asiática, cabelo com franja', url: '/avatars/avatar_15.png', color: '#f43f5e' }, + { id: 16, name: 'Elena', desc: 'Castanha, cabelo afro volumoso', url: '/avatars/avatar_16.png', color: '#14b8a6' }, + { id: 17, name: 'Zara', desc: 'Cabelo roxo moderno', url: '/avatars/avatar_17.png', color: '#0ea5e9' }, + { id: 18, name: 'Naomi', desc: 'Negra de óculos e rabo de cavalo', url: '/avatars/avatar_18.png', color: '#4f46e5' }, + { id: 19, name: 'Harper', desc: 'Corte pixie rosa', url: '/avatars/avatar_19.png', color: '#d946ef' }, + { id: 20, name: 'Amara', desc: 'Negra com tranças', url: '/avatars/avatar_20.png', color: '#f59e0b' } ]; const SKIN_COLORS = ['#fed7aa', '#fdba74', '#f59e0b', '#d97706', '#9a3412', '#ffedd5']; @@ -136,13 +148,23 @@ const getAvatarBgClass = (avatarId: number): string => { 'bg-yellow-500/20 text-yellow-400', 'bg-blue-500/20 text-blue-400', 'bg-orange-500/20 text-orange-400', - 'bg-emerald-500/20 text-emerald-400', - 'bg-amber-900/20 text-amber-600', - 'bg-slate-700/20 text-slate-400', + 'bg-lime-500/20 text-lime-400', + 'bg-cyan-500/20 text-cyan-400', + 'bg-indigo-500/20 text-indigo-400', 'bg-purple-500/20 text-purple-400', - 'bg-yellow-600/20 text-yellow-500' + 'bg-pink-500/20 text-pink-400', + 'bg-emerald-500/20 text-emerald-400', + 'bg-amber-950/40 text-amber-600', + 'bg-slate-700/20 text-slate-300', + 'bg-yellow-600/20 text-yellow-500', + 'bg-rose-500/20 text-rose-400', + 'bg-teal-500/20 text-teal-400', + 'bg-sky-500/20 text-sky-400', + 'bg-indigo-600/20 text-indigo-500', + 'bg-fuchsia-500/20 text-fuchsia-400', + 'bg-amber-500/20 text-amber-400' ]; - return classes[(avatarId - 1) % 10] || classes[0]; + return classes[(avatarId - 1) % classes.length] || classes[0]; }; // --- Subcomponentes 3D --- @@ -178,9 +200,9 @@ function MiiAvatarBackup({ }, [isLastChatSender]); const avatarUrl = useMemo(() => { - const seed = `${username || 'user'}_${avatarId}`; - return `https://api.dicebear.com/9.x/toon-head/png?seed=${encodeURIComponent(seed)}`; - }, [username, avatarId]); + const preset = AVATAR_PRESETS.find((p) => p.id === avatarId); + return preset ? preset.url : `/avatars/avatar_${avatarId}.png`; + }, [avatarId]); useFrame((state) => { if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) { @@ -261,7 +283,6 @@ function MiiAvatarBackup({ src={avatarUrl} alt={username} className="w-full h-full object-contain drop-shadow-md" - style={{ minWidth: '64px', minHeight: '64px' }} onError={(e) => { (e.currentTarget as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(username || 'U')}&background=475569&color=fff&size=128&rounded=true`; }} @@ -312,6 +333,13 @@ function MiiAvatar(props: { scale?: number; isLastChatSender?: boolean; } & Record) { + const preset = AVATAR_PRESETS.find((p) => p.id === props.avatarId); + const isPng = preset?.url.endsWith('.png'); + + if (isPng) { + return ; + } + return ( }> }> @@ -832,6 +860,29 @@ export default function MeetingRoom() { // Estados da Sala (Conexão Supabase) const [peers, setPeers] = useState>({}); + + // Lista de avatares permitidos (IDs de 1 a 20). Por padrão, todos estão ativos. + const [allowedAvatars, setAllowedAvatars] = useState(() => { + return Array.from({ length: 20 }, (_, i) => i + 1); + }); + const [isAvatarConfigOpen, setIsAvatarConfigOpen] = useState(false); + + const updateAllowedAvatars = async (newAllowed: number[]) => { + setAllowedAvatars(newAllowed); + if (isPresenter && channelRef.current) { + console.log("⚙️ [MeetingRoom] Atualizando metadados de Presence com avatares permitidos:", newAllowed); + await channelRef.current.track({ + username: name.substring(0, 10), + avatarId: selectedAvatar, + chairId: selectedChair, + role: isPresenter ? 'presenter' : 'guest', + cargo: cargo.trim() || 'Colaborador', + empresa: empresa.trim() || 'SteelXR Corp', + joinedAt: new Date().toISOString(), + allowedAvatars: newAllowed + }); + } + }; const mergedPeers = useMemo(() => { if (!inRoom) return peers; @@ -941,6 +992,7 @@ export default function MeetingRoom() { const state = tempChannel.presenceState(); console.log("👥 [Lobby Watcher Sync] Estado de presença bruto:", state); const formattedPeers: Record = {}; + let remoteAllowed: number[] | null = null; Object.keys(state).forEach((key) => { if (key.startsWith('lobby_watcher_')) return; @@ -955,6 +1007,10 @@ export default function MeetingRoom() { empresa: presence.empresa || 'SteelXR Corp', role: presence.role || 'guest' }; + + if (presence.role === 'presenter' && Array.isArray(presence.allowedAvatars)) { + remoteAllowed = presence.allowedAvatars; + } } }); } @@ -962,6 +1018,11 @@ export default function MeetingRoom() { console.log("👥 [Lobby Watcher Sync] Peers formatados:", formattedPeers); setPeers(formattedPeers); + + if (remoteAllowed) { + console.log("⚙️ [Lobby Watcher Sync] Sincronizados avatares permitidos remotos:", remoteAllowed); + setAllowedAvatars(remoteAllowed); + } }) .subscribe(); @@ -1054,7 +1115,7 @@ export default function MeetingRoom() { const state = channel.presenceState(); console.log("👥 [Presence Sync] Estado de presença bruto da reunião:", state); const formattedPeers: Record = {}; - let count = 0; + let remoteAllowed: number[] | null = null; Object.keys(state).forEach((key) => { if (key.startsWith('lobby_watcher_')) return; @@ -1069,13 +1130,21 @@ export default function MeetingRoom() { empresa: presence.empresa || 'SteelXR Corp', role: presence.role || 'guest' }; - count++; + + if (presence.role === 'presenter' && Array.isArray(presence.allowedAvatars)) { + remoteAllowed = presence.allowedAvatars; + } } }); } }); console.log("👥 [Presence Sync] Peers formatados:", formattedPeers); setPeers(formattedPeers); + + if (!isPresenter && remoteAllowed) { + console.log("⚙️ [Presence Sync] Sincronizando avatares permitidos remotos na sala conectada:", remoteAllowed); + setAllowedAvatars(remoteAllowed); + } }) .on('presence', { event: 'join' }, ({ key, newPresences }) => { if (key.startsWith('lobby_watcher_')) return; @@ -1139,6 +1208,7 @@ export default function MeetingRoom() { cargo: cargo.trim() || 'Colaborador', empresa: empresa.trim() || 'SteelXR Corp', joinedAt: new Date().toISOString(), + allowedAvatars: allowedAvatars }); // Se for o apresentador, propaga o estado inicial de apresentação (vinda do Viewer) para todos @@ -1214,11 +1284,34 @@ export default function MeetingRoom() { return Object.values(peers).find((p) => p.avatarId === selectedAvatar); }, [peers, selectedAvatar]); + // Lista de avatares disponíveis para este usuário escolher + const availableAvatars = useMemo(() => { + if (isPresenter) { + return AVATAR_PRESETS.map((p) => p.id); + } + // Para convidados, filtra apenas os IDs permitidos pelo apresentador + return AVATAR_PRESETS.map((p) => p.id).filter((id) => allowedAvatars.includes(id)); + }, [isPresenter, allowedAvatars]); + + // Se o selectedAvatar atual não estiver na lista de permitidos, selecionamos o primeiro disponível + useEffect(() => { + if (availableAvatars.length > 0 && !availableAvatars.includes(selectedAvatar)) { + setSelectedAvatar(availableAvatars[0]); + } + }, [availableAvatars, selectedAvatar]); + const nextAvatar = () => { - setSelectedAvatar((prev) => (prev === 10 ? 1 : prev + 1)); + if (availableAvatars.length === 0) return; + const currentIndex = availableAvatars.indexOf(selectedAvatar); + const nextIndex = (currentIndex + 1) % availableAvatars.length; + setSelectedAvatar(availableAvatars[nextIndex]); }; + const prevAvatar = () => { - setSelectedAvatar((prev) => (prev === 1 ? 10 : prev - 1)); + if (availableAvatars.length === 0) return; + const currentIndex = availableAvatars.indexOf(selectedAvatar); + const prevIndex = (currentIndex - 1 + availableAvatars.length) % availableAvatars.length; + setSelectedAvatar(availableAvatars[prevIndex]); }; // --- Renderização do Lobby --- @@ -1289,11 +1382,24 @@ export default function MeetingRoom() { -
+
{currentAvatar.name}

{currentAvatar.desc}

+ + {isPresenter && ( + + )}
{/* Formulário de Identificação: Nome, Cargo e Empresa agrupados sequencialmente na esquerda */} @@ -1415,6 +1521,108 @@ export default function MeetingRoom() {
+ + {/* Modal de Configuração de Avatares Permitidos (Apenas para o Apresentador) */} + {isAvatarConfigOpen && ( +
+
+
+
+

Avatares da Reunião

+

+ Selecione entre 4 e 10 avatares que estarão disponíveis para a reunião +

+
+
+ + {allowedAvatars.length} Selecionados + +
+
+ + {/* Grid dos Avatares para Seleção */} +
+ {AVATAR_PRESETS.map((preset) => { + const isSelected = allowedAvatars.includes(preset.id); + const isMale = preset.id <= 10; + + return ( +
{ + if (isSelected) { + // Se desmarcar, verifica se fica com menos de 4 + if (allowedAvatars.length <= 4) { + toast.warning('A reunião deve conter pelo menos 4 avatares disponíveis.'); + return; + } + updateAllowedAvatars(allowedAvatars.filter(id => id !== preset.id)); + } else { + // Se marcar, verifica se excede 10 + if (allowedAvatars.length >= 10) { + toast.warning('Você pode selecionar no máximo 10 avatares.'); + return; + } + updateAllowedAvatars([...allowedAvatars, preset.id]); + } + }} + className={`relative flex flex-col items-center justify-center p-3 rounded-lg border-2 cursor-pointer transition-all duration-200 hover:scale-105 select-none ${ + isSelected + ? 'border-primary bg-primary/10 shadow-[0_0_10px_rgba(59,130,246,0.3)]' + : 'border-slate-800 bg-slate-950/40 hover:border-slate-700 hover:bg-slate-950/70' + }`} + > + {/* Badge Sexo */} + + {isMale ? 'M' : 'F'} + + + {/* Miniatura da Imagem Local */} +
+ {preset.name} +
+ + {preset.name} + {preset.desc} + + {/* Check Icon */} + {isSelected && ( +
+ +
+ )} +
+ ); + })} +
+ + {/* Botão de Fechar / Salvar */} +
+ +
+
+
+ )} ); }