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

@@ -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">