feat: implement professional avatar system with 30 3D realistic avatars inspired by cinema; enhance upload functionality and user experience with instant updates and improved UI components
This commit is contained in:
@@ -4,12 +4,36 @@
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
import SolicitarFerias from "$lib/components/SolicitarFerias.svelte";
|
||||
import AprovarFerias from "$lib/components/AprovarFerias.svelte";
|
||||
|
||||
import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars";
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let abaAtiva = $state<"meu-perfil" | "minhas-ferias" | "aprovar-ferias">("meu-perfil");
|
||||
let mostrarFormSolicitar = $state(false);
|
||||
let solicitacaoSelecionada = $state<any>(null);
|
||||
let mostrarModalFoto = $state(false);
|
||||
let uploadandoFoto = $state(false);
|
||||
let erroUpload = $state("");
|
||||
let modoFoto = $state<"upload" | "avatar">("avatar");
|
||||
let avatarSelecionado = $state<string>("");
|
||||
let mostrarBotaoCamera = $state(false);
|
||||
|
||||
// Estados locais para atualização imediata
|
||||
let fotoPerfilLocal = $state<string | null>(null);
|
||||
let avatarLocal = $state<string | null>(null);
|
||||
|
||||
// Galeria de avatares (30 avatares profissionais 3D realistas)
|
||||
const avatarGallery = generateAvatarGallery(30);
|
||||
|
||||
// Sincronizar com authStore
|
||||
$effect(() => {
|
||||
if (authStore.usuario?.fotoPerfilUrl !== undefined) {
|
||||
fotoPerfilLocal = authStore.usuario.fotoPerfilUrl;
|
||||
}
|
||||
if (authStore.usuario?.avatar !== undefined) {
|
||||
avatarLocal = authStore.usuario.avatar;
|
||||
}
|
||||
});
|
||||
|
||||
// Queries
|
||||
const funcionarioQuery = $derived(
|
||||
@@ -82,29 +106,208 @@
|
||||
};
|
||||
return textos[status] || status;
|
||||
}
|
||||
|
||||
async function handleUploadFoto(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
// Validar tipo de arquivo
|
||||
if (!file.type.startsWith("image/")) {
|
||||
erroUpload = "Por favor, selecione uma imagem válida";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar tamanho (max 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
erroUpload = "A imagem deve ter no máximo 5MB";
|
||||
return;
|
||||
}
|
||||
|
||||
uploadandoFoto = true;
|
||||
erroUpload = "";
|
||||
|
||||
try {
|
||||
// 1. Gerar URL de upload (NOME CORRETO DA FUNÇÃO!)
|
||||
const uploadUrl = await client.mutation(api.usuarios.uploadFotoPerfil, {});
|
||||
|
||||
// 2. Upload do arquivo
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": file.type },
|
||||
body: file,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Falha no upload da imagem");
|
||||
}
|
||||
|
||||
const { storageId } = await response.json();
|
||||
|
||||
// 3. Atualizar perfil com o novo storageId
|
||||
await client.mutation(api.usuarios.atualizarPerfil, {
|
||||
fotoPerfil: storageId,
|
||||
avatar: undefined, // Remove avatar se colocar foto
|
||||
});
|
||||
|
||||
// 4. Atualizar authStore para obter a URL da foto
|
||||
await authStore.refresh();
|
||||
|
||||
// 5. Atualizar localmente IMEDIATAMENTE com a URL do authStore
|
||||
if (authStore.usuario?.fotoPerfilUrl) {
|
||||
fotoPerfilLocal = authStore.usuario.fotoPerfilUrl;
|
||||
avatarLocal = null;
|
||||
}
|
||||
|
||||
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>Foto de perfil atualizada!</span>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
} catch (e: any) {
|
||||
erroUpload = e.message || "Erro ao fazer upload da foto";
|
||||
} finally {
|
||||
uploadandoFoto = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelecionarAvatar(avatarUrl: string) {
|
||||
uploadandoFoto = true;
|
||||
erroUpload = "";
|
||||
|
||||
try {
|
||||
// 1. Atualizar localmente IMEDIATAMENTE (antes mesmo da API)
|
||||
avatarLocal = avatarUrl;
|
||||
fotoPerfilLocal = null;
|
||||
|
||||
// 2. Salvar avatar selecionado no backend
|
||||
await client.mutation(api.usuarios.atualizarPerfil, {
|
||||
avatar: avatarUrl,
|
||||
fotoPerfil: undefined, // Remove foto se colocar avatar
|
||||
});
|
||||
|
||||
// 3. Atualizar authStore em background
|
||||
authStore.refresh();
|
||||
|
||||
mostrarModalFoto = false;
|
||||
|
||||
// Toast de sucesso mais discreto
|
||||
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!</span>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
} catch (e: any) {
|
||||
erroUpload = e.message || "Erro ao salvar avatar";
|
||||
// Reverter mudança local se houver erro
|
||||
avatarLocal = authStore.usuario?.avatar || null;
|
||||
fotoPerfilLocal = authStore.usuario?.fotoPerfilUrl || null;
|
||||
} finally {
|
||||
uploadandoFoto = false;
|
||||
}
|
||||
}
|
||||
|
||||
function abrirModalFoto() {
|
||||
erroUpload = "";
|
||||
modoFoto = "avatar";
|
||||
avatarSelecionado = "";
|
||||
mostrarModalFoto = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container mx-auto px-4 py-6 max-w-7xl">
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-primary text-primary-content rounded-full w-16">
|
||||
<span class="text-2xl">{authStore.usuario?.nome.substring(0, 2).toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-primary">{authStore.usuario?.nome}</h1>
|
||||
<p class="text-base-content/70">{authStore.usuario?.email}</p>
|
||||
{#if meuTime}
|
||||
<div class="badge badge-outline mt-1" style="border-color: {meuTime.cor}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{meuTime.nome}
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Avatar com botão de edição -->
|
||||
<div
|
||||
class="relative"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onmouseenter={() => mostrarBotaoCamera = true}
|
||||
onmouseleave={() => mostrarBotaoCamera = false}
|
||||
>
|
||||
<button type="button" class="avatar cursor-pointer p-0 border-0 bg-transparent" onclick={abrirModalFoto}>
|
||||
<div class="w-24 h-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 transition-all hover:ring-4">
|
||||
{#if fotoPerfilLocal}
|
||||
<img src={fotoPerfilLocal} alt="Foto de perfil" />
|
||||
{:else if avatarLocal}
|
||||
<img src={avatarLocal} alt="Avatar" />
|
||||
{:else}
|
||||
<div class="bg-primary text-primary-content flex items-center justify-center">
|
||||
<span class="text-3xl font-bold">{authStore.usuario?.nome.substring(0, 2).toUpperCase()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Botão de editar foto -->
|
||||
<button
|
||||
type="button"
|
||||
class={`absolute bottom-0 right-0 btn btn-circle btn-sm btn-primary shadow-xl transition-all duration-300 ${mostrarBotaoCamera ? 'opacity-100 scale-100' : 'opacity-0 scale-90'}`}
|
||||
onclick={abrirModalFoto}
|
||||
aria-label="Editar foto de perfil"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dica visual -->
|
||||
{#if mostrarBotaoCamera}
|
||||
<div class="absolute -bottom-8 left-1/2 -translate-x-1/2 text-xs text-center whitespace-nowrap bg-base-300 px-2 py-1 rounded shadow-lg">
|
||||
Clique para alterar
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Informações do usuário -->
|
||||
<div class="flex-1">
|
||||
<h1 class="text-3xl font-bold text-primary">{authStore.usuario?.nome}</h1>
|
||||
|
||||
{#if funcionario?.descricaoCargo}
|
||||
<p class="text-lg font-semibold text-base-content/80 mt-1">
|
||||
{funcionario.descricaoCargo}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<p class="text-base-content/70 mt-1">{authStore.usuario?.email}</p>
|
||||
|
||||
<div class="flex items-center gap-2 mt-2 flex-wrap">
|
||||
<div class="badge badge-primary">{authStore.usuario?.role?.nome || "Usuário"}</div>
|
||||
|
||||
{#if meuTime}
|
||||
<div class="badge badge-outline" style="border-color: {meuTime.cor}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{meuTime.nome}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if funcionario?.statusFerias === "em_ferias"}
|
||||
<div class="badge badge-warning">🏖️ Em Férias</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,7 +352,7 @@
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo das Abas -->
|
||||
{#if abaAtiva === "meu-perfil"}
|
||||
@@ -177,10 +380,10 @@
|
||||
<span class="label-text font-semibold">Perfil</span>
|
||||
</span>
|
||||
<div class="badge badge-primary">{authStore.usuario?.role?.nome || "Usuário"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informações de Funcionário -->
|
||||
{#if funcionario}
|
||||
@@ -213,8 +416,8 @@
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-base-content/50">Não atribuído a um time</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<span class="label">
|
||||
<span class="label-text font-semibold">Status</span>
|
||||
@@ -249,17 +452,17 @@
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</svg>
|
||||
<span class="text-sm font-semibold">{time.membros?.length || 0} membros</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if abaAtiva === "minhas-ferias"}
|
||||
<!-- Minhas Férias -->
|
||||
@@ -284,7 +487,7 @@
|
||||
</div>
|
||||
|
||||
{#if mostrarFormSolicitar}
|
||||
<div class="divider"></div>
|
||||
<div class="divider"></div>
|
||||
{#if funcionario}
|
||||
<SolicitarFerias funcionarioId={funcionario._id} onSucesso={recarregar} />
|
||||
{:else}
|
||||
@@ -296,7 +499,7 @@
|
||||
<h3 class="font-bold">Perfil de funcionário não encontrado</h3>
|
||||
<div class="text-xs">Seu usuário ainda não está associado a um cadastro de funcionário. Entre em contato com o RH.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -347,7 +550,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if abaAtiva === "aprovar-ferias"}
|
||||
<!-- Aprovar Férias (Gestores) -->
|
||||
@@ -401,20 +604,20 @@
|
||||
</td>
|
||||
<td>
|
||||
{#if solicitacao.status === "aguardando_aprovacao"}
|
||||
<button
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={() => selecionarSolicitacao(solicitacao._id)}
|
||||
>
|
||||
Analisar
|
||||
</button>
|
||||
{:else}
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => selecionarSolicitacao(solicitacao._id)}
|
||||
>
|
||||
Detalhes
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -438,10 +641,166 @@
|
||||
onCancelar={() => solicitacaoSelecionada = null}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={() => solicitacaoSelecionada = null} aria-label="Fechar modal">Fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
|
||||
<!-- Modal de Upload de Foto / Escolher Avatar -->
|
||||
{#if mostrarModalFoto}
|
||||
<dialog class="modal modal-open">
|
||||
<div class="modal-box max-w-4xl">
|
||||
<h3 class="font-bold text-2xl mb-6 text-center">Alterar Foto de Perfil</h3>
|
||||
|
||||
<!-- Preview da foto atual -->
|
||||
<div class="flex justify-center mb-6">
|
||||
<div class="avatar">
|
||||
<div class="w-32 h-32 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
|
||||
{#if fotoPerfilLocal}
|
||||
<img src={fotoPerfilLocal} alt="Foto atual" class="object-cover" />
|
||||
{:else if avatarLocal}
|
||||
<img src={avatarLocal} alt="Avatar atual" class="object-cover" />
|
||||
{:else}
|
||||
<div class="bg-primary text-primary-content flex items-center justify-center">
|
||||
<span class="text-4xl font-bold">{authStore.usuario?.nome.substring(0, 2).toUpperCase()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs: Avatar ou Upload -->
|
||||
<div role="tablist" class="tabs tabs-boxed mb-6 bg-base-200">
|
||||
<button
|
||||
role="tab"
|
||||
class={`tab ${modoFoto === "avatar" ? "tab-active" : ""}`}
|
||||
onclick={() => modoFoto = "avatar"}
|
||||
disabled={uploadandoFoto}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Escolher Avatar
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
class={`tab ${modoFoto === "upload" ? "tab-active" : ""}`}
|
||||
onclick={() => modoFoto = "upload"}
|
||||
disabled={uploadandoFoto}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Enviar Foto
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo das Tabs -->
|
||||
{#if modoFoto === "avatar"}
|
||||
<!-- Galeria de Avatares -->
|
||||
<div class="mb-4">
|
||||
<p class="text-center text-base-content/70 mb-4">
|
||||
Escolha um dos <strong>30 avatares profissionais</strong> para seu perfil
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-3 md:grid-cols-5 lg:grid-cols-6 gap-4 p-4 bg-base-200 rounded-lg max-h-[500px] overflow-y-auto">
|
||||
{#each avatarGallery as avatar}
|
||||
<button
|
||||
type="button"
|
||||
class={`avatar cursor-pointer transition-all hover:scale-105 ${avatarSelecionado === avatar.url ? 'ring-4 ring-primary' : 'hover:ring-2 hover:ring-primary/50'}`}
|
||||
onclick={() => avatarSelecionado = avatar.url}
|
||||
ondblclick={() => handleSelecionarAvatar(avatar.url)}
|
||||
disabled={uploadandoFoto}
|
||||
aria-label="Selecionar avatar {avatar.name}"
|
||||
>
|
||||
<div class="w-20 h-20 rounded-full">
|
||||
<img src={avatar.url} alt={avatar.name} loading="lazy" />
|
||||
</div>
|
||||
<div class="text-xs text-center mt-1 truncate w-20">{avatar.name}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
||||
<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>
|
||||
<span class="text-sm">
|
||||
<strong>Dica:</strong> Clique uma vez para selecionar, clique duas vezes para aplicar imediatamente!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if avatarSelecionado}
|
||||
<div class="flex justify-center gap-2 mt-4">
|
||||
<button
|
||||
class="btn btn-primary btn-lg gap-2"
|
||||
onclick={() => handleSelecionarAvatar(avatarSelecionado)}
|
||||
disabled={uploadandoFoto}
|
||||
>
|
||||
{#if uploadandoFoto}
|
||||
<span class="loading loading-spinner"></span>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{/if}
|
||||
{uploadandoFoto ? "Salvando..." : "Confirmar Avatar"}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<!-- Upload de nova foto -->
|
||||
<div class="form-control">
|
||||
<label class="label" for="foto-upload">
|
||||
<span class="label-text font-semibold">Selecionar nova foto</span>
|
||||
</label>
|
||||
<input
|
||||
id="foto-upload"
|
||||
type="file"
|
||||
class="file-input file-input-bordered w-full"
|
||||
accept="image/*"
|
||||
onchange={handleUploadFoto}
|
||||
disabled={uploadandoFoto}
|
||||
/>
|
||||
<div class="label">
|
||||
<span class="label-text-alt text-base-content/70">Formatos aceitos: JPG, PNG, GIF. Tamanho máximo: 5MB</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if erroUpload}
|
||||
<div class="alert alert-error mt-4">
|
||||
<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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{erroUpload}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if uploadandoFoto && modoFoto === "upload"}
|
||||
<div class="flex justify-center items-center gap-2 mt-4">
|
||||
<span class="loading loading-spinner loading-md text-primary"></span>
|
||||
<span>Enviando foto...</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="modal-action">
|
||||
<button
|
||||
class="btn"
|
||||
onclick={() => mostrarModalFoto = false}
|
||||
disabled={uploadandoFoto}
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={() => mostrarModalFoto = false} aria-label="Fechar modal" disabled={uploadandoFoto}>Fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user