feat: replace dynamic avatar generation with static image assets

This commit is contained in:
2025-11-20 15:05:17 -03:00
parent 51e2efa07e
commit 0af8daa901
17 changed files with 146 additions and 576 deletions

View File

@@ -9,7 +9,7 @@
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { Menu, User, Home, UserPlus, XCircle, LogIn, Tag, Plus, Check } from 'lucide-svelte';
import { authClient } from '$lib/auth';
import { resolve } from '$app/paths';
@@ -33,8 +33,8 @@
return currentUser.data.avatar;
}
// Fallback: gerar avatar baseado no nome
return getAvatarUrl(currentUser.data.nome);
// Fallback: retornar null para usar o ícone User do Lucide
return null;
});
// Função para gerar classes do menu ativo
@@ -328,8 +328,9 @@
>Contato</a
>
<span class="text-base-content/30"></span>
<a href={resolve('/abrir-chamado')} class="link link-hover hover:text-primary transition-colors"
>Suporte</a
<a
href={resolve('/abrir-chamado')}
class="link link-hover hover:text-primary transition-colors">Suporte</a
>
<span class="text-base-content/30"></span>
<a href={resolve('/')} class="link link-hover hover:text-primary transition-colors"
@@ -365,7 +366,7 @@
<span>Dashboard</span>
</a>
</li>
{#each setores as s}
{#each setores as s (s.link)}
{@const isActive = currentPath.startsWith(s.link)}
<li class="rounded-xl">
<a
@@ -400,7 +401,7 @@
<!-- Botão de fechar moderno -->
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute top-4 right-4 z-10 hover:bg-error/20 hover:text-error transition-all duration-200"
class="btn btn-sm btn-circle btn-ghost hover:bg-error/20 hover:text-error absolute top-4 right-4 z-10 transition-all duration-200"
onclick={closeLoginModal}
aria-label="Fechar modal"
>
@@ -408,28 +409,20 @@
</button>
<!-- Decoração de fundo -->
<div
class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-primary/10 blur-3xl"
></div>
<div
class="absolute -bottom-20 -left-20 h-40 w-40 rounded-full bg-primary/5 blur-3xl"
></div>
<div class="bg-primary/10 absolute -top-20 -right-20 h-40 w-40 rounded-full blur-3xl"></div>
<div class="bg-primary/5 absolute -bottom-20 -left-20 h-40 w-40 rounded-full blur-3xl"></div>
<div class="relative z-10 p-8">
<!-- Header com logo e título -->
<div class="mb-8 text-center">
<div class="avatar mb-5 mx-auto">
<div class="avatar mx-auto mb-5">
<div
class="group relative w-24 overflow-hidden rounded-2xl bg-white p-4 shadow-xl ring-2 ring-primary/20 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
class="group ring-primary/20 relative w-24 overflow-hidden rounded-2xl bg-white p-4 shadow-xl ring-2 transition-all duration-300 hover:scale-105 hover:shadow-2xl"
>
<div
class="absolute inset-0 bg-gradient-to-br from-primary/10 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
class="from-primary/10 absolute inset-0 bg-gradient-to-br to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
></div>
<img
src={logo}
alt="Logo SGSE"
class="relative z-10 h-full w-full object-contain"
/>
<img src={logo} alt="Logo SGSE" class="relative z-10 h-full w-full object-contain" />
</div>
</div>
<h3 class="text-primary mb-2 text-4xl font-bold tracking-tight">Login</h3>
@@ -441,7 +434,7 @@
<!-- Mensagem de erro -->
{#if erroLogin}
<div
class="alert alert-error mb-6 border-error/30 bg-error/10 shadow-lg backdrop-blur-sm"
class="alert alert-error border-error/30 bg-error/10 mb-6 shadow-lg backdrop-blur-sm"
>
<XCircle class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2.5} />
<span class="font-medium">{erroLogin}</span>
@@ -453,16 +446,14 @@
<!-- Campo Matrícula/E-mail -->
<div class="form-control">
<label class="label pb-2" for="login-matricula">
<span class="text-primary label-text text-sm font-semibold"
>Matrícula ou E-mail</span
>
<span class="text-primary label-text text-sm font-semibold">Matrícula ou E-mail</span>
</label>
<div class="relative">
<input
id="login-matricula"
type="text"
placeholder="Digite sua matrícula ou e-mail"
class="input input-bordered input-primary w-full border-2 transition-all duration-200 focus:border-primary focus:shadow-lg focus:shadow-primary/20 disabled:opacity-50"
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
bind:value={matricula}
required
disabled={carregandoLogin}
@@ -481,7 +472,7 @@
id="login-password"
type="password"
placeholder="Digite sua senha"
class="input input-bordered input-primary w-full border-2 transition-all duration-200 focus:border-primary focus:shadow-lg focus:shadow-primary/20 disabled:opacity-50"
class="input input-bordered input-primary focus:border-primary focus:shadow-primary/20 w-full border-2 transition-all duration-200 focus:shadow-lg disabled:opacity-50"
bind:value={senha}
required
disabled={carregandoLogin}
@@ -494,7 +485,7 @@
<div class="form-control pt-2">
<button
type="submit"
class="btn btn-primary btn-lg group relative w-full overflow-hidden border-0 bg-gradient-to-r from-primary via-primary to-primary/90 shadow-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl disabled:opacity-50"
class="btn btn-primary btn-lg group from-primary via-primary to-primary/90 relative w-full overflow-hidden border-0 bg-gradient-to-r shadow-xl transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl disabled:opacity-50"
disabled={carregandoLogin}
>
<!-- Efeito de brilho animado -->
@@ -506,14 +497,17 @@
<span class="loading loading-spinner loading-sm"></span>
<span class="font-semibold">Entrando...</span>
{:else}
<LogIn class="h-5 w-5 transition-transform duration-300 group-hover:scale-110" strokeWidth={2.5} />
<LogIn
class="h-5 w-5 transition-transform duration-300 group-hover:scale-110"
strokeWidth={2.5}
/>
<span class="font-semibold">Entrar</span>
{/if}
</button>
</div>
<!-- Links auxiliares -->
<div class="pt-4 space-y-3 text-center">
<div class="space-y-3 pt-4 text-center">
<a
href={resolve('/abrir-chamado')}
class="link link-primary block text-sm font-medium transition-all duration-200 hover:scale-105"
@@ -548,7 +542,7 @@
>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2 z-10 hover:bg-base-300"
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 absolute top-2 right-2 z-10"
onclick={closeAboutModal}
>
@@ -558,7 +552,7 @@
<!-- Logo e Título -->
<div class="flex flex-col items-center gap-3">
<div class="avatar">
<div class="w-20 rounded-xl bg-white p-3 shadow-lg ring-2 ring-primary/20">
<div class="ring-primary/20 w-20 rounded-xl bg-white p-3 shadow-lg ring-2">
<img src={logo} alt="Logo SGSE" class="h-full w-full object-contain" />
</div>
</div>
@@ -574,10 +568,12 @@
<div class="divider my-1"></div>
<!-- Informações de Versão -->
<div class="bg-gradient-to-br from-primary/10 to-primary/5 space-y-2 rounded-xl border border-primary/10 p-4 shadow-sm">
<div
class="from-primary/10 to-primary/5 border-primary/10 space-y-2 rounded-xl border bg-gradient-to-br p-4 shadow-sm"
>
<div class="flex items-center justify-center gap-2">
<Tag class="text-primary h-4 w-4" strokeWidth={2} />
<p class="text-base-content/60 text-xs font-medium uppercase tracking-wide">Versão</p>
<p class="text-base-content/60 text-xs font-medium tracking-wide uppercase">Versão</p>
</div>
<p class="text-primary text-2xl font-bold tracking-tight">1.0 11_2025</p>
<div class="badge badge-warning badge-sm gap-1.5 px-3 py-1.5 text-xs">
@@ -588,7 +584,9 @@
<!-- Desenvolvido por -->
<div class="space-y-1.5">
<p class="text-base-content/50 text-xs font-medium uppercase tracking-wide">Desenvolvido por</p>
<p class="text-base-content/50 text-xs font-medium tracking-wide uppercase">
Desenvolvido por
</p>
<p class="text-primary text-sm font-semibold">Secretaria de Esportes de Pernambuco</p>
</div>
@@ -597,12 +595,16 @@
<!-- Informações Adicionais -->
<div class="grid grid-cols-2 gap-3">
<div class="bg-base-200/60 rounded-lg border border-base-300/50 p-3 shadow-sm transition-all hover:shadow-md">
<p class="text-primary mb-1 text-xs font-semibold uppercase tracking-wide">Governo</p>
<div
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
>
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Governo</p>
<p class="text-base-content/60 text-xs font-medium">Estado de Pernambuco</p>
</div>
<div class="bg-base-200/60 rounded-lg border border-base-300/50 p-3 shadow-sm transition-all hover:shadow-md">
<p class="text-primary mb-1 text-xs font-semibold uppercase tracking-wide">Ano</p>
<div
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
>
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Ano</p>
<p class="text-base-content/60 text-xs font-medium">2025</p>
</div>
</div>

View File

@@ -14,7 +14,7 @@
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import ChatList from './ChatList.svelte';
import ChatWindow from './ChatWindow.svelte';
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { SvelteSet } from 'svelte/reactivity';
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
@@ -38,11 +38,8 @@
return usuario.fotoPerfilUrl;
}
if (usuario.avatar) {
return getAvatarUrl(usuario.avatar);
}
// Fallback: gerar avatar baseado no nome
return getAvatarUrl(usuario.nome);
// Fallback: retornar null para usar o ícone User do Lucide
return null;
});
// Posição do widget (arrastável)

View File

@@ -9,7 +9,7 @@
import UserAvatar from './UserAvatar.svelte';
import ScheduleMessageModal from './ScheduleMessageModal.svelte';
import SalaReuniaoManager from './SalaReuniaoManager.svelte';
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
interface Props {
@@ -59,10 +59,7 @@
const c = conversa();
if (!c) return '💬';
if (c.tipo === 'grupo') {
return c.avatar || '👥';
}
if (c.outroUsuario?.avatar) {
return c.outroUsuario.avatar;
return '👥';
}
return '👤';
}
@@ -138,7 +135,6 @@
<div class="relative shrink-0">
{#if conversa() && conversa()?.tipo === 'individual' && conversa()?.outroUsuario}
<UserAvatar
avatar={conversa()?.outroUsuario?.avatar}
fotoPerfilUrl={conversa()?.outroUsuario?.fotoPerfilUrl}
nome={conversa()?.outroUsuario?.nome || 'Usuário'}
size="md"
@@ -195,18 +191,12 @@
alt={participante.nome}
class="h-full w-full object-cover"
/>
{:else if participante.avatar}
<img
src={getAvatarUrl(participante.avatar)}
alt={participante.nome}
class="h-full w-full object-cover"
/>
{:else}
<img
src={getAvatarUrl(participante.nome)}
alt={participante.nome}
class="h-full w-full object-cover"
/>
<div
class="bg-base-200 flex h-full w-full items-center justify-center text-xs font-semibold"
>
{participante.nome.substring(0, 2).toUpperCase()}
</div>
{/if}
</div>
{/each}

View File

@@ -1,41 +1,43 @@
<script lang="ts">
import { getAvatarUrl as generateAvatarUrl } from "$lib/utils/avatarGenerator";
interface Props {
avatar?: string;
fotoPerfilUrl?: string | null;
nome: string;
size?: "xs" | "sm" | "md" | "lg";
}
import { User } from 'lucide-svelte';
let { avatar, fotoPerfilUrl, nome, size = "md" }: Props = $props();
interface Props {
fotoPerfilUrl?: string | null;
nome: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
}
const sizeClasses = {
xs: "w-8 h-8",
sm: "w-10 h-10",
md: "w-12 h-12",
lg: "w-16 h-16",
};
let { fotoPerfilUrl, nome, size = 'md' }: Props = $props();
function getAvatarUrl(avatarId: string): string {
// Usar gerador local ao invés da API externa
return generateAvatarUrl(avatarId);
}
const sizeClasses = {
xs: 'w-8 h-8',
sm: 'w-10 h-10',
md: 'w-12 h-12',
lg: 'w-16 h-16',
xl: 'w-32 h-32'
};
const avatarUrlToShow = $derived(() => {
if (fotoPerfilUrl) return fotoPerfilUrl;
if (avatar) return getAvatarUrl(avatar);
return getAvatarUrl(nome); // Fallback usando o nome
});
const iconSizes = {
xs: 16,
sm: 20,
md: 24,
lg: 32,
xl: 64
};
</script>
<div class="avatar">
<div class={`${sizeClasses[size]} rounded-full bg-base-200 overflow-hidden`}>
<img
src={avatarUrlToShow()}
alt={`Avatar de ${nome}`}
class="w-full h-full object-cover"
/>
</div>
<div class="avatar placeholder">
<div
class={`${sizeClasses[size]} bg-base-200 text-base-content/50 flex items-center justify-center overflow-hidden rounded-full`}
>
{#if fotoPerfilUrl}
<img
src={fotoPerfilUrl}
alt={`Foto de perfil de ${nome}`}
class="h-full w-full object-cover"
/>
{:else}
<User size={iconSizes[size]} />
{/if}
</div>
</div>

View File

@@ -1,63 +0,0 @@
// Mapa de seeds para os 32 avatares
const avatarSeeds: Record<string, string> = {
// Masculinos (16)
"avatar-m-1": "John",
"avatar-m-2": "Peter",
"avatar-m-3": "Michael",
"avatar-m-4": "David",
"avatar-m-5": "James",
"avatar-m-6": "Robert",
"avatar-m-7": "William",
"avatar-m-8": "Joseph",
"avatar-m-9": "Thomas",
"avatar-m-10": "Charles",
"avatar-m-11": "Daniel",
"avatar-m-12": "Matthew",
"avatar-m-13": "Anthony",
"avatar-m-14": "Mark",
"avatar-m-15": "Donald",
"avatar-m-16": "Steven",
// Femininos (16)
"avatar-f-1": "Maria",
"avatar-f-2": "Ana",
"avatar-f-3": "Patricia",
"avatar-f-4": "Jennifer",
"avatar-f-5": "Linda",
"avatar-f-6": "Barbara",
"avatar-f-7": "Elizabeth",
"avatar-f-8": "Jessica",
"avatar-f-9": "Sarah",
"avatar-f-10": "Karen",
"avatar-f-11": "Nancy",
"avatar-f-12": "Betty",
"avatar-f-13": "Helen",
"avatar-f-14": "Sandra",
"avatar-f-15": "Ashley",
"avatar-f-16": "Kimberly",
};
/**
* Gera URL do avatar usando API DiceBear com parâmetros simples
*/
export function getAvatarUrl(avatarId: string): string {
const seed = avatarSeeds[avatarId] || avatarId || "default";
// Usar avataarstyle do DiceBear com parâmetros mínimos
// API v7 suporta apenas parâmetros específicos
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(seed)}`;
}
/**
* Lista todos os IDs de avatares disponíveis
*/
export function getAllAvatarIds(): string[] {
return Object.keys(avatarSeeds);
}
/**
* Verifica se um avatarId é válido
*/
export function isValidAvatarId(avatarId: string): boolean {
return avatarId in avatarSeeds;
}

View File

@@ -1,283 +0,0 @@
// Galeria de avatares inspirados em artistas do cinema
// Usando DiceBear API com estilos variados para aparência cinematográfica
export interface Avatar {
id: string;
name: string;
url: string;
seed: string;
style: string;
}
// Avatares inspirados em artistas do cinema (30 avatares estilizados)
const cinemaArtistsAvatars = [
// 15 Masculinos - Inspirados em grandes atores
{
id: 'avatar-male-1',
name: 'Leonardo DiCaprio',
seed: 'Leonardo',
style: 'adventurer',
bgColor: 'C5CAE9',
},
{
id: 'avatar-male-2',
name: 'Brad Pitt',
seed: 'Bradley',
style: 'adventurer',
bgColor: 'B2DFDB',
},
{
id: 'avatar-male-3',
name: 'Tom Hanks',
seed: 'Thomas',
style: 'adventurer-neutral',
bgColor: 'DCEDC8',
},
{
id: 'avatar-male-4',
name: 'Morgan Freeman',
seed: 'Morgan',
style: 'adventurer',
bgColor: 'F0F4C3',
},
{
id: 'avatar-male-5',
name: 'Robert De Niro',
seed: 'Robert',
style: 'adventurer-neutral',
bgColor: 'E0E0E0',
},
{
id: 'avatar-male-6',
name: 'Al Pacino',
seed: 'Alfredo',
style: 'adventurer',
bgColor: 'FFCCBC',
},
{
id: 'avatar-male-7',
name: 'Johnny Depp',
seed: 'John',
style: 'adventurer',
bgColor: 'D1C4E9',
},
{
id: 'avatar-male-8',
name: 'Denzel Washington',
seed: 'Denzel',
style: 'adventurer-neutral',
bgColor: 'B3E5FC',
},
{
id: 'avatar-male-9',
name: 'Will Smith',
seed: 'Willard',
style: 'adventurer',
bgColor: 'FFF9C4',
},
{
id: 'avatar-male-10',
name: 'Tom Cruise',
seed: 'TomC',
style: 'adventurer-neutral',
bgColor: 'CFD8DC',
},
{
id: 'avatar-male-11',
name: 'Samuel L Jackson',
seed: 'Samuel',
style: 'adventurer',
bgColor: 'F8BBD0',
},
{
id: 'avatar-male-12',
name: 'Harrison Ford',
seed: 'Harrison',
style: 'adventurer-neutral',
bgColor: 'C8E6C9',
},
{
id: 'avatar-male-13',
name: 'Keanu Reeves',
seed: 'Keanu',
style: 'adventurer',
bgColor: 'BBDEFB',
},
{
id: 'avatar-male-14',
name: 'Matt Damon',
seed: 'Matthew',
style: 'adventurer-neutral',
bgColor: 'FFE0B2',
},
{
id: 'avatar-male-15',
name: 'Christian Bale',
seed: 'Christian',
style: 'adventurer',
bgColor: 'E1BEE7',
},
// 15 Femininos - Inspiradas em grandes atrizes
{
id: 'avatar-female-1',
name: 'Meryl Streep',
seed: 'Meryl',
style: 'lorelei',
bgColor: 'F8BBD0',
},
{
id: 'avatar-female-2',
name: 'Scarlett Johansson',
seed: 'Scarlett',
style: 'lorelei',
bgColor: 'FFCCBC',
},
{
id: 'avatar-female-3',
name: 'Jennifer Lawrence',
seed: 'Jennifer',
style: 'lorelei-neutral',
bgColor: 'E1BEE7',
},
{
id: 'avatar-female-4',
name: 'Angelina Jolie',
seed: 'Angelina',
style: 'lorelei',
bgColor: 'C5CAE9',
},
{
id: 'avatar-female-5',
name: 'Cate Blanchett',
seed: 'Catherine',
style: 'lorelei-neutral',
bgColor: 'B2DFDB',
},
{
id: 'avatar-female-6',
name: 'Nicole Kidman',
seed: 'Nicole',
style: 'lorelei',
bgColor: 'DCEDC8',
},
{
id: 'avatar-female-7',
name: 'Julia Roberts',
seed: 'Julia',
style: 'lorelei-neutral',
bgColor: 'FFF9C4',
},
{
id: 'avatar-female-8',
name: 'Emma Stone',
seed: 'Emma',
style: 'lorelei',
bgColor: 'CFD8DC',
},
{
id: 'avatar-female-9',
name: 'Natalie Portman',
seed: 'Natalie',
style: 'lorelei-neutral',
bgColor: 'F0F4C3',
},
{
id: 'avatar-female-10',
name: 'Charlize Theron',
seed: 'Charlize',
style: 'lorelei',
bgColor: 'E0E0E0',
},
{
id: 'avatar-female-11',
name: 'Kate Winslet',
seed: 'Kate',
style: 'lorelei-neutral',
bgColor: 'D1C4E9',
},
{
id: 'avatar-female-12',
name: 'Sandra Bullock',
seed: 'Sandra',
style: 'lorelei',
bgColor: 'B3E5FC',
},
{
id: 'avatar-female-13',
name: 'Halle Berry',
seed: 'Halle',
style: 'lorelei-neutral',
bgColor: 'C8E6C9',
},
{
id: 'avatar-female-14',
name: 'Anne Hathaway',
seed: 'Anne',
style: 'lorelei',
bgColor: 'BBDEFB',
},
{
id: 'avatar-female-15',
name: 'Amy Adams',
seed: 'Amy',
style: 'lorelei-neutral',
bgColor: 'FFE0B2',
},
];
/**
* Gera uma galeria de avatares inspirados em artistas do cinema
* Usa DiceBear API com estilos cinematográficos
* @param count Número de avatares a gerar (padrão: 30)
* @returns Array de objetos com id, name, url, seed e style
*/
export function generateAvatarGallery(count: number = 30): Avatar[] {
const avatars: Avatar[] = [];
for (let i = 0; i < Math.min(count, cinemaArtistsAvatars.length); i++) {
const avatar = cinemaArtistsAvatars[i];
// URL do DiceBear com estilo cinematográfico
const url = `https://api.dicebear.com/7.x/${avatar.style}/svg?seed=${encodeURIComponent(avatar.seed)}&backgroundColor=${avatar.bgColor}&radius=50&size=200`;
avatars.push({
id: avatar.id,
name: avatar.name,
url,
seed: avatar.seed,
style: avatar.style,
});
}
return avatars;
}
/**
* Obter URL do avatar por ID
* @param avatarId ID do avatar (ex: "avatar-male-1")
* @returns URL do avatar ou string vazia se não encontrado
*/
export function getAvatarUrl(avatarId: string): string {
const gallery = generateAvatarGallery();
const avatar = gallery.find(a => a.id === avatarId);
return avatar?.url || '';
}
/**
* Gerar avatar aleatório da galeria
* @returns Avatar aleatório
*/
export function getRandomAvatar(): Avatar {
const gallery = generateAvatarGallery();
const randomIndex = Math.floor(Math.random() * gallery.length);
return gallery[randomIndex];
}
/**
* Salvar avatar selecionado (retorna o ID para salvar no backend)
* @param avatarId ID do avatar selecionado
* @returns ID do avatar
*/
export function saveAvatarSelection(avatarId: string): string {
return avatarId;
}

View File

@@ -7,7 +7,6 @@
import WizardSolicitacaoAusencia from '$lib/components/ausencias/WizardSolicitacaoAusencia.svelte';
import AprovarAusencias from '$lib/components/AprovarAusencias.svelte';
import CalendarioAusencias from '$lib/components/ausencias/CalendarioAusencias.svelte';
import { generateAvatarGallery } from '$lib/utils/avatars';
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import type { FunctionReturnType } from 'convex/server';
@@ -75,7 +74,6 @@
let uploadandoFoto = $state(false);
let erroUpload = $state('');
let modoFoto = $state<'upload' | 'avatar'>('avatar');
let avatarSelecionado = $state<string>('');
let mostrarBotaoCamera = $state(false);
// Estados para Minhas Férias
@@ -100,8 +98,16 @@
let erroMensagemChamado = $state<string | null>(null);
let sucessoMensagemChamado = $state<string | null>(null);
// Galeria de avatares (30 avatares profissionais 3D realistas)
const avatarGallery = generateAvatarGallery(30);
// Avatares padrão disponíveis
const defaultAvatars = [
'/avatars/avatar-1.png',
'/avatars/avatar-2.png',
'/avatars/avatar-3.png',
'/avatars/avatar-4.png',
'/avatars/avatar-5.png',
'/avatars/avatar-6.png',
'/avatars/avatar-7.png'
];
// FuncionarioId disponível diretamente do usuário atual
const funcionarioIdDisponivel = $derived(currentUser?.data?.funcionarioId ?? null);
@@ -441,6 +447,30 @@
return;
}
await processarUploadFoto(file);
}
async function handleEscolherAvatarPadrao(avatarPath: string) {
try {
uploadandoFoto = true;
erroUpload = '';
// Buscar a imagem
const response = await fetch(avatarPath);
const blob = await response.blob();
const file = new File([blob], avatarPath.split('/').pop() || 'avatar.png', {
type: 'image/png'
});
await processarUploadFoto(file);
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e);
erroUpload = errorMessage || 'Erro ao processar avatar padrão';
uploadandoFoto = false;
}
}
async function processarUploadFoto(file: File) {
uploadandoFoto = true;
erroUpload = '';
@@ -463,16 +493,12 @@
// 4. Atualizar perfil com o novo storageId
await client.mutation(api.usuarios.atualizarPerfil, {
fotoPerfil: storageId,
avatar: undefined // Remove avatar se colocar foto
fotoPerfil: storageId
});
// 5. Aguardar um pouco para garantir que o backend processou
await new Promise((resolve) => setTimeout(resolve, 300));
// 8. Limpar o input para permitir novo upload
input.value = '';
// 9. Fechar modal após sucesso
mostrarModalFoto = false;
@@ -496,45 +522,9 @@
uploadandoFoto = false;
}
async function handleSelecionarAvatar(avatarUrl: string) {
uploadandoFoto = true;
erroUpload = '';
try {
// 2. Salvar avatar selecionado no backend
await client.mutation(api.usuarios.atualizarPerfil, {
avatar: avatarUrl,
fotoPerfil: undefined // Remove foto se colocar avatar
});
// 6. Fechar modal após sucesso
mostrarModalFoto = false;
// Toast de sucesso
const toast = document.createElement('div');
toast.className = 'toast toast-top toast-end';
toast.innerHTML = `
<div class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Avatar atualizado com sucesso!</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e);
erroUpload = errorMessage || 'Erro ao salvar avatar';
} finally {
uploadandoFoto = false;
}
}
function abrirModalFoto() {
erroUpload = '';
modoFoto = 'avatar';
avatarSelecionado = '';
mostrarModalFoto = true;
}
</script>
@@ -571,22 +561,16 @@
onclick={abrirModalFoto}
>
<div
class="animate-float h-40 w-40 rounded-full shadow-2xl ring-4 ring-white ring-offset-4 ring-offset-transparent transition-all duration-300 hover:scale-105 hover:ring-8"
class="animate-float flex h-40 w-40 items-center justify-center overflow-hidden rounded-full bg-white shadow-2xl ring-4 ring-white ring-offset-4 ring-offset-transparent transition-all duration-300 hover:scale-105 hover:ring-8"
>
{#if currentUser.data?.fotoPerfilUrl}
<img
src={currentUser.data.fotoPerfilUrl}
alt="Foto de perfil"
class="object-cover"
class="h-full w-full object-cover"
/>
{:else if currentUser.data?.avatar}
<img src={currentUser.data.avatar} alt="Avatar" class="object-cover" />
{:else}
<div class="flex items-center justify-center bg-white text-purple-700">
<span class="text-5xl font-black"
>{currentUser.data?.nome.substring(0, 2).toUpperCase()}</span
>
</div>
<User class="h-20 w-20 text-purple-700" />
{/if}
</div>
</button>
@@ -2338,18 +2322,16 @@
<div class="mb-8 flex justify-center">
<div class="avatar">
<div
class="ring-primary ring-offset-base-100 h-32 w-32 rounded-full shadow-2xl ring-4 ring-offset-4"
class="ring-primary ring-offset-base-100 bg-base-200 flex h-32 w-32 items-center justify-center overflow-hidden rounded-full shadow-2xl ring-4 ring-offset-4"
>
{#if currentUser.data?.fotoPerfilUrl}
<img src={currentUser.data.fotoPerfilUrl} alt="Foto atual" class="object-cover" />
{:else if currentUser.data?.avatar}
<img src={currentUser.data.avatar} alt="Avatar atual" class="object-cover" />
<img
src={currentUser.data.fotoPerfilUrl}
alt="Foto atual"
class="h-full w-full object-cover"
/>
{:else}
<div class="bg-primary text-primary-content flex items-center justify-center">
<span class="text-4xl font-bold"
>{currentUser.data?.nome.substring(0, 2).toUpperCase()}</span
>
</div>
<User class="text-base-content/50 h-16 w-16" />
{/if}
</div>
</div>
@@ -2413,82 +2395,37 @@
<!-- Galeria de Avatares -->
<div class="mb-4">
<p class="text-base-content/70 mb-6 text-center text-lg">
Escolha um dos <strong class="text-primary">30 avatares profissionais</strong> para seu
Escolha um dos <strong class="text-primary">avatares profissionais</strong> para seu
perfil
</p>
<div
class="bg-base-200 grid max-h-[500px] grid-cols-3 gap-4 overflow-y-auto rounded-2xl p-6 shadow-inner md:grid-cols-5 lg:grid-cols-6"
class="bg-base-200 grid max-h-[500px] grid-cols-3 gap-4 overflow-y-auto rounded-2xl p-6 shadow-inner md:grid-cols-4 lg:grid-cols-5"
>
{#each avatarGallery as avatar (avatar.id)}
{#each defaultAvatars as avatarPath, i (avatarPath)}
<button
type="button"
class={`flex cursor-pointer flex-col items-center rounded-xl p-2 transition-all hover:scale-110 ${avatarSelecionado === avatar.url ? 'ring-primary bg-primary/10 scale-105 ring-4' : 'hover:ring-primary/50 hover:bg-base-100 hover:ring-2'}`}
onclick={() => (avatarSelecionado = avatar.url)}
ondblclick={() => handleSelecionarAvatar(avatar.url)}
class="hover:ring-primary/50 hover:bg-base-100 flex cursor-pointer flex-col items-center rounded-xl p-2 transition-all hover:scale-110 hover:ring-2"
onclick={() => handleEscolherAvatarPadrao(avatarPath)}
disabled={uploadandoFoto}
aria-label="Selecionar avatar {avatar.name}"
aria-label="Selecionar avatar {i + 1}"
>
<div class="avatar">
<div class="h-20 w-20 rounded-full shadow-lg">
<img src={avatar.url} alt={avatar.name} loading="lazy" />
<img src={avatarPath} alt="Avatar {i + 1}" loading="lazy" />
</div>
</div>
<div class="mt-2 w-full truncate text-center text-[10px] font-semibold">
{avatar.name}
</div>
</button>
{/each}
</div>
<div class="alert alert-info mt-6 shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<Info class="h-6 w-6 shrink-0" />
<span>
<strong>Dica:</strong> Clique uma vez para selecionar, clique duas vezes para aplicar
imediatamente!
<strong>Dica:</strong> Clique em um avatar para defini-lo como sua foto de perfil.
</span>
</div>
</div>
{#if avatarSelecionado}
<div class="mt-6 flex justify-center gap-3">
<button
type="button"
class="btn btn-lg gap-2 shadow-xl transition-all hover:scale-105"
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;"
onclick={() => handleSelecionarAvatar(avatarSelecionado)}
disabled={uploadandoFoto}
>
{#if uploadandoFoto}
<span class="loading loading-spinner"></span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{/if}
{uploadandoFoto ? 'Salvando...' : 'Confirmar Avatar'}
</button>
</div>
{/if}
{:else}
<!-- Upload de nova foto -->
<div class="form-control">

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB