🚀 Auto-deploy: melhoria no snap e medição AR em 25/05/2026 16:51:02
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -43,7 +43,8 @@ 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, ShieldAlert, Maximize2, Minimize2, Box
|
RotateCw, Award, ShieldAlert, Maximize2, Minimize2, Box,
|
||||||
|
Settings, Check
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
// Interfaces tipadas para evitar any
|
// Interfaces tipadas para evitar any
|
||||||
@@ -75,6 +76,7 @@ interface PeerData {
|
|||||||
cargo: string;
|
cargo: string;
|
||||||
empresa: string;
|
empresa: string;
|
||||||
presence_ref: string;
|
presence_ref: string;
|
||||||
|
allowedAvatars?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detector nativo de suporte ao WebGL
|
// Detector nativo de suporte ao WebGL
|
||||||
@@ -112,18 +114,28 @@ const CHAIRS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const AVATAR_PRESETS: AvatarPreset[] = [
|
const AVATAR_PRESETS: AvatarPreset[] = [
|
||||||
// Homens (5)
|
// Masculinos (1 a 10)
|
||||||
{ id: 1, name: 'Carlos', desc: 'Estilo corporativo masculino clássico', url: 'https://models.readyplayer.me/65851410d54020a5605d8f63.glb?bodyType=halfbody', color: '#ef4444' },
|
{ id: 1, name: 'Leo', desc: 'Negro, cabelo curto preto', url: '/avatars/avatar_1.png', color: '#ef4444' },
|
||||||
{ id: 2, name: 'Lucas', desc: 'Estilo moderno masculino com barba', url: 'https://models.readyplayer.me/6585144b248a3064be486a4f.glb?bodyType=halfbody', color: '#b45309' },
|
{ id: 2, name: 'Marcus', desc: 'Loiro, cabelo curto', url: '/avatars/avatar_2.png', 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: 3, name: 'Arthur', desc: 'Ruivo com barba', url: '/avatars/avatar_3.png', 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: 4, name: 'Jin', desc: 'Asiático, cabelo preto liso', url: '/avatars/avatar_4.png', color: '#3b82f6' },
|
||||||
{ id: 5, name: 'Ricardo', desc: 'Design casual moderno e cabelos grisalhos', url: 'https://models.readyplayer.me/658514ec248a3064be486f0b.glb?bodyType=halfbody', color: '#f97316' },
|
{ id: 5, name: 'Mateo', desc: 'Castanho, cabelo cacheado', url: '/avatars/avatar_5.png', color: '#f97316' },
|
||||||
// Mulheres (5)
|
{ id: 6, name: 'Malik', desc: 'Negro, careca com barba', url: '/avatars/avatar_6.png', color: '#84cc16' },
|
||||||
{ id: 6, name: 'Juliana', desc: 'Visual feminino clássico executivo', url: 'https://models.readyplayer.me/65851336d54020a5605d820f.glb?bodyType=halfbody', color: '#10b981' },
|
{ id: 7, name: 'Oliver', desc: 'Cabelo grisalho com óculos', url: '/avatars/avatar_7.png', color: '#06b6d4' },
|
||||||
{ 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: 'Carlos', desc: 'Latino com bigode', url: '/avatars/avatar_8.png', color: '#6366f1' },
|
||||||
{ id: 8, name: 'Camila', desc: 'Estilo moderno e cabelos cacheados', url: 'https://models.readyplayer.me/658513a9f2d52f672d61937f.glb?bodyType=halfbody', color: '#1e293b' },
|
{ id: 9, name: 'Yuki', desc: 'Cabelo espetado azul escuro', url: '/avatars/avatar_9.png', color: '#a855f7' },
|
||||||
{ 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: 'Elijah', desc: 'Negro com dreadlocks', url: '/avatars/avatar_10.png', color: '#ec4899' },
|
||||||
{ id: 10, name: 'Amanda', desc: 'Visual jovem loira e roupas elegantes', url: 'https://models.readyplayer.me/65851403248a3064be48651a.glb?bodyType=halfbody', color: '#ca8a04' },
|
// 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'];
|
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-yellow-500/20 text-yellow-400',
|
||||||
'bg-blue-500/20 text-blue-400',
|
'bg-blue-500/20 text-blue-400',
|
||||||
'bg-orange-500/20 text-orange-400',
|
'bg-orange-500/20 text-orange-400',
|
||||||
'bg-emerald-500/20 text-emerald-400',
|
'bg-lime-500/20 text-lime-400',
|
||||||
'bg-amber-900/20 text-amber-600',
|
'bg-cyan-500/20 text-cyan-400',
|
||||||
'bg-slate-700/20 text-slate-400',
|
'bg-indigo-500/20 text-indigo-400',
|
||||||
'bg-purple-500/20 text-purple-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 ---
|
// --- Subcomponentes 3D ---
|
||||||
@@ -178,9 +200,9 @@ function MiiAvatarBackup({
|
|||||||
}, [isLastChatSender]);
|
}, [isLastChatSender]);
|
||||||
|
|
||||||
const avatarUrl = useMemo(() => {
|
const avatarUrl = useMemo(() => {
|
||||||
const seed = `${username || 'user'}_${avatarId}`;
|
const preset = AVATAR_PRESETS.find((p) => p.id === avatarId);
|
||||||
return `https://api.dicebear.com/9.x/toon-head/png?seed=${encodeURIComponent(seed)}`;
|
return preset ? preset.url : `/avatars/avatar_${avatarId}.png`;
|
||||||
}, [username, avatarId]);
|
}, [avatarId]);
|
||||||
|
|
||||||
useFrame((state) => {
|
useFrame((state) => {
|
||||||
if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) {
|
if (groupRef.current && props.position && Array.isArray(props.position) && props.position.length >= 2) {
|
||||||
@@ -261,7 +283,6 @@ function MiiAvatarBackup({
|
|||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={username}
|
alt={username}
|
||||||
className="w-full h-full object-contain drop-shadow-md"
|
className="w-full h-full object-contain drop-shadow-md"
|
||||||
style={{ minWidth: '64px', minHeight: '64px' }}
|
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.currentTarget as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(username || 'U')}&background=475569&color=fff&size=128&rounded=true`;
|
(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;
|
scale?: number;
|
||||||
isLastChatSender?: boolean;
|
isLastChatSender?: boolean;
|
||||||
} & Record<string, unknown>) {
|
} & Record<string, unknown>) {
|
||||||
|
const preset = AVATAR_PRESETS.find((p) => p.id === props.avatarId);
|
||||||
|
const isPng = preset?.url.endsWith('.png');
|
||||||
|
|
||||||
|
if (isPng) {
|
||||||
|
return <MiiAvatarBackup {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AvatarErrorBoundary fallback={<MiiAvatarBackup {...props} />}>
|
<AvatarErrorBoundary fallback={<MiiAvatarBackup {...props} />}>
|
||||||
<Suspense fallback={<MiiAvatarBackup {...props} />}>
|
<Suspense fallback={<MiiAvatarBackup {...props} />}>
|
||||||
@@ -833,6 +861,29 @@ export default function MeetingRoom() {
|
|||||||
// Estados da Sala (Conexão Supabase)
|
// Estados da Sala (Conexão Supabase)
|
||||||
const [peers, setPeers] = useState<Record<string, PeerData>>({});
|
const [peers, setPeers] = useState<Record<string, PeerData>>({});
|
||||||
|
|
||||||
|
// Lista de avatares permitidos (IDs de 1 a 20). Por padrão, todos estão ativos.
|
||||||
|
const [allowedAvatars, setAllowedAvatars] = useState<number[]>(() => {
|
||||||
|
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(() => {
|
const mergedPeers = useMemo(() => {
|
||||||
if (!inRoom) return peers;
|
if (!inRoom) return peers;
|
||||||
|
|
||||||
@@ -941,6 +992,7 @@ export default function MeetingRoom() {
|
|||||||
const state = tempChannel.presenceState();
|
const state = tempChannel.presenceState();
|
||||||
console.log("👥 [Lobby Watcher Sync] Estado de presença bruto:", state);
|
console.log("👥 [Lobby Watcher Sync] Estado de presença bruto:", state);
|
||||||
const formattedPeers: Record<string, PeerData> = {};
|
const formattedPeers: Record<string, PeerData> = {};
|
||||||
|
let remoteAllowed: number[] | null = null;
|
||||||
|
|
||||||
Object.keys(state).forEach((key) => {
|
Object.keys(state).forEach((key) => {
|
||||||
if (key.startsWith('lobby_watcher_')) return;
|
if (key.startsWith('lobby_watcher_')) return;
|
||||||
@@ -955,6 +1007,10 @@ export default function MeetingRoom() {
|
|||||||
empresa: presence.empresa || 'SteelXR Corp',
|
empresa: presence.empresa || 'SteelXR Corp',
|
||||||
role: presence.role || 'guest'
|
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);
|
console.log("👥 [Lobby Watcher Sync] Peers formatados:", formattedPeers);
|
||||||
setPeers(formattedPeers);
|
setPeers(formattedPeers);
|
||||||
|
|
||||||
|
if (remoteAllowed) {
|
||||||
|
console.log("⚙️ [Lobby Watcher Sync] Sincronizados avatares permitidos remotos:", remoteAllowed);
|
||||||
|
setAllowedAvatars(remoteAllowed);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
@@ -1054,7 +1115,7 @@ export default function MeetingRoom() {
|
|||||||
const state = channel.presenceState();
|
const state = channel.presenceState();
|
||||||
console.log("👥 [Presence Sync] Estado de presença bruto da reunião:", state);
|
console.log("👥 [Presence Sync] Estado de presença bruto da reunião:", state);
|
||||||
const formattedPeers: Record<string, PeerData> = {};
|
const formattedPeers: Record<string, PeerData> = {};
|
||||||
let count = 0;
|
let remoteAllowed: number[] | null = null;
|
||||||
|
|
||||||
Object.keys(state).forEach((key) => {
|
Object.keys(state).forEach((key) => {
|
||||||
if (key.startsWith('lobby_watcher_')) return;
|
if (key.startsWith('lobby_watcher_')) return;
|
||||||
@@ -1069,13 +1130,21 @@ export default function MeetingRoom() {
|
|||||||
empresa: presence.empresa || 'SteelXR Corp',
|
empresa: presence.empresa || 'SteelXR Corp',
|
||||||
role: presence.role || 'guest'
|
role: presence.role || 'guest'
|
||||||
};
|
};
|
||||||
count++;
|
|
||||||
|
if (presence.role === 'presenter' && Array.isArray(presence.allowedAvatars)) {
|
||||||
|
remoteAllowed = presence.allowedAvatars;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("👥 [Presence Sync] Peers formatados:", formattedPeers);
|
console.log("👥 [Presence Sync] Peers formatados:", formattedPeers);
|
||||||
setPeers(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 }) => {
|
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
|
||||||
if (key.startsWith('lobby_watcher_')) return;
|
if (key.startsWith('lobby_watcher_')) return;
|
||||||
@@ -1139,6 +1208,7 @@ export default function MeetingRoom() {
|
|||||||
cargo: cargo.trim() || 'Colaborador',
|
cargo: cargo.trim() || 'Colaborador',
|
||||||
empresa: empresa.trim() || 'SteelXR Corp',
|
empresa: empresa.trim() || 'SteelXR Corp',
|
||||||
joinedAt: new Date().toISOString(),
|
joinedAt: new Date().toISOString(),
|
||||||
|
allowedAvatars: allowedAvatars
|
||||||
});
|
});
|
||||||
|
|
||||||
// Se for o apresentador, propaga o estado inicial de apresentação (vinda do Viewer) para todos
|
// 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);
|
return Object.values(peers).find((p) => p.avatarId === selectedAvatar);
|
||||||
}, [peers, 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 = () => {
|
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 = () => {
|
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 ---
|
// --- Renderização do Lobby ---
|
||||||
@@ -1289,11 +1382,24 @@ export default function MeetingRoom() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 text-center">
|
<div className="mt-3 text-center w-full px-2">
|
||||||
<span className="font-mono text-xs font-bold uppercase tracking-wider text-primary">
|
<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>
|
||||||
|
|
||||||
|
{isPresenter && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsAvatarConfigOpen(true)}
|
||||||
|
className="mt-3 w-full font-mono text-[9px] uppercase tracking-wider gap-1 border-primary/40 hover:bg-primary/10 text-primary-foreground bg-primary/5 h-8"
|
||||||
|
>
|
||||||
|
<Settings className="h-3.5 w-3.5" />
|
||||||
|
Gerenciar Avatares ({allowedAvatars.length} ativos)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Formulário de Identificação: Nome, Cargo e Empresa agrupados sequencialmente na esquerda */}
|
{/* Formulário de Identificação: Nome, Cargo e Empresa agrupados sequencialmente na esquerda */}
|
||||||
@@ -1415,6 +1521,108 @@ export default function MeetingRoom() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal de Configuração de Avatares Permitidos (Apenas para o Apresentador) */}
|
||||||
|
{isAvatarConfigOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 backdrop-blur-sm p-4 animate-in fade-in duration-200">
|
||||||
|
<div className="w-full max-w-3xl rounded-xl border border-primary/30 bg-slate-900/90 p-6 md:p-8 shadow-2xl backdrop-blur-md flex flex-col max-h-[85vh] animate-in zoom-in-95 duration-200">
|
||||||
|
<div className="flex justify-between items-start border-b border-slate-800 pb-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-primary font-mono uppercase tracking-wider">Avatares da Reunião</h3>
|
||||||
|
<p className="text-[10px] text-muted-foreground font-mono mt-0.5 uppercase tracking-wide">
|
||||||
|
Selecione entre 4 e 10 avatares que estarão disponíveis para a reunião
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-mono text-xs px-2 py-0.5 rounded-full bg-slate-800 border text-slate-300">
|
||||||
|
{allowedAvatars.length} Selecionados
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid dos Avatares para Seleção */}
|
||||||
|
<div className="flex-1 overflow-y-auto pr-1 py-1 grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 gap-3">
|
||||||
|
{AVATAR_PRESETS.map((preset) => {
|
||||||
|
const isSelected = allowedAvatars.includes(preset.id);
|
||||||
|
const isMale = preset.id <= 10;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={preset.id}
|
||||||
|
onClick={() => {
|
||||||
|
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 */}
|
||||||
|
<span className={`absolute top-1 right-1.5 font-mono text-[7px] px-1 rounded uppercase font-bold ${
|
||||||
|
isMale ? 'bg-blue-500/20 text-blue-400' : 'bg-pink-500/20 text-pink-400'
|
||||||
|
}`}>
|
||||||
|
{isMale ? 'M' : 'F'}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Miniatura da Imagem Local */}
|
||||||
|
<div className="w-14 h-14 bg-slate-900/60 rounded-full border overflow-hidden flex items-center justify-center mb-2">
|
||||||
|
<img
|
||||||
|
src={preset.url}
|
||||||
|
alt={preset.name}
|
||||||
|
className="w-12 h-12 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="font-mono text-[10px] font-bold text-slate-200">{preset.name}</span>
|
||||||
|
<span className="text-[8px] text-slate-400 text-center mt-0.5 line-clamp-1 leading-snug">{preset.desc}</span>
|
||||||
|
|
||||||
|
{/* Check Icon */}
|
||||||
|
{isSelected && (
|
||||||
|
<div className="absolute -bottom-1.5 -right-1 bg-primary text-primary-foreground rounded-full p-0.5 shadow-md">
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Botão de Fechar / Salvar */}
|
||||||
|
<div className="border-t border-slate-800 pt-4 mt-4 flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
onClick={() => {
|
||||||
|
if (allowedAvatars.length < 4 || allowedAvatars.length > 10) {
|
||||||
|
toast.error('Selecione entre 4 e 10 avatares antes de continuar.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsAvatarConfigOpen(false);
|
||||||
|
toast.success('Configuração de avatares atualizada com sucesso!');
|
||||||
|
}}
|
||||||
|
className="font-mono text-xs glow-primary px-6 animate-pulse"
|
||||||
|
>
|
||||||
|
Salvar Escolhas
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||