feat: implement theme customization and user preferences

- Added support for user-selected themes, allowing users to customize the appearance of the application.
- Introduced a new `temaPreferido` field in the user schema to store the preferred theme.
- Updated various components to apply the selected theme dynamically based on user preferences.
- Enhanced the UI to include a theme selection interface, enabling users to preview and save their theme choices.
- Implemented a polyfill for BlobBuilder to ensure compatibility across browsers, improving the functionality of the application.
This commit is contained in:
2025-11-22 22:05:52 -03:00
parent 58ac3a4f1b
commit 37d7318d5a
12 changed files with 1149 additions and 74 deletions

View File

@@ -29,7 +29,8 @@
CheckCircle,
ListChecks,
Info,
Fingerprint
Fingerprint,
Palette
} from 'lucide-svelte';
import RegistroPonto from '$lib/components/ponto/RegistroPonto.svelte';
import TicketCard from '$lib/components/chamados/TicketCard.svelte';
@@ -44,6 +45,7 @@
} from '$lib/utils/chamados';
import { useConvexWithAuth } from '$lib/hooks/useConvexWithAuth';
import type { Doc } from '@sgse-app/backend/convex/_generated/dataModel';
import { temasDisponiveis, aplicarTema, type Tema } from '$lib/utils/temas';
const client = useConvexClient();
// @ts-expect-error - Convex types issue with getCurrentUser
@@ -66,6 +68,7 @@
| 'aprovar-ferias'
| 'aprovar-ausencias'
| 'meu-ponto'
| 'aparencia'
>('meu-perfil');
let periodoSelecionado = $state<Id<'ferias'> | null>(null);
@@ -98,6 +101,12 @@
let erroMensagemChamado = $state<string | null>(null);
let sucessoMensagemChamado = $state<string | null>(null);
// Estados para Aparência
let temaSelecionado = $state<string | null>(null);
let salvandoTema = $state(false);
let sucessoSalvarTema = $state<string | null>(null);
let erroSalvarTema = $state<string | null>(null);
// Avatares padrão disponíveis
const defaultAvatars = [
'/avatars/avatar-1.png',
@@ -530,6 +539,57 @@
modoFoto = 'avatar';
mostrarModalFoto = true;
}
// Inicializar tema selecionado com o tema atual do usuário
$effect(() => {
if (currentUser?.data?.temaPreferido && !temaSelecionado) {
temaSelecionado = currentUser.data.temaPreferido;
} else if (!temaSelecionado && currentUser !== undefined) {
// Só definir padrão se o usuário já foi carregado (mesmo que seja null)
temaSelecionado = 'purple'; // Tema padrão
}
});
// Função para selecionar e aplicar tema
async function selecionarTema(temaId: string) {
temaSelecionado = temaId;
erroSalvarTema = null;
sucessoSalvarTema = null;
// Aplicar tema imediatamente (sem salvar ainda)
aplicarTema(temaId);
}
// Função para salvar tema preferido
async function salvarTema() {
if (!temaSelecionado) return;
try {
salvandoTema = true;
erroSalvarTema = null;
sucessoSalvarTema = null;
await client.mutation(api.usuarios.atualizarTema, {
temaPreferido: temaSelecionado
});
// Garantir que o tema continue aplicado após salvar
aplicarTema(temaSelecionado);
sucessoSalvarTema = 'Tema salvo com sucesso! Sua preferência será aplicada em acessos futuros.';
// Limpar mensagem após 3 segundos
setTimeout(() => {
sucessoSalvarTema = null;
}, 3000);
} catch (error) {
const mensagemErro =
error instanceof Error ? error.message : 'Erro ao salvar tema. Tente novamente.';
erroSalvarTema = mensagemErro;
} finally {
salvandoTema = false;
}
}
</script>
<ProtectedRoute>
@@ -797,6 +857,16 @@
<Fingerprint class="h-5 w-5" strokeWidth={2} />
Meu Ponto
</button>
<button
type="button"
role="tab"
class={`tab tab-lg gap-2 font-semibold transition-all duration-300 ${abaAtiva === 'aparencia' ? 'tab-active scale-105 bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg' : 'hover:bg-base-100'}`}
onclick={() => (abaAtiva = 'aparencia')}
>
<Palette class="h-5 w-5" strokeWidth={2} />
Aparência
</button>
</div>
<!-- Conteúdo das Abas -->
@@ -2283,6 +2353,149 @@
{/if}
</div>
</div>
{:else if abaAtiva === 'aparencia'}
<!-- Aparência -->
<div class="space-y-6">
<!-- Header -->
<div
class="card from-base-100 to-base-200 overflow-hidden border-t-4 border-purple-500 bg-gradient-to-br shadow-2xl"
>
<div class="card-body p-6">
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-pink-600 shadow-lg ring-2 ring-purple-500/20"
>
<Palette class="h-6 w-6 text-white" strokeWidth={2.5} />
</div>
<div>
<h2 class="text-base-content flex items-center gap-2 text-2xl font-bold">
Personalizar Aparência
</h2>
<p class="text-base-content/60 mt-0.5 text-sm">
Escolha um tema para personalizar a interface do SGSE
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Mensagens de Sucesso/Erro -->
{#if sucessoSalvarTema}
<div class="alert alert-success shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
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>{sucessoSalvarTema}</span>
</div>
{/if}
{#if erroSalvarTema}
<div class="alert alert-error shadow-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
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>{erroSalvarTema}</span>
</div>
{/if}
<!-- Grid de Temas -->
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each temasDisponiveis as tema (tema.id)}
<button
type="button"
class={`card from-base-100 to-base-200 hover:shadow-3xl overflow-hidden border-2 bg-gradient-to-br shadow-xl transition-all duration-300 hover:scale-105 ${
temaSelecionado === tema.id
? 'border-primary ring-4 ring-primary/20 scale-105'
: 'border-base-300 hover:border-primary/50'
}`}
onclick={() => selecionarTema(tema.id)}
>
<div class="card-body p-6">
<!-- Preview do Tema -->
<div
class="mb-4 h-24 w-full rounded-lg shadow-lg"
style="background: linear-gradient(135deg, {tema.corPrimaria} 0%, {tema.corSecundaria} 100%);"
>
<div class="flex h-full items-center justify-center">
<div
class="h-12 w-12 rounded-full bg-white/20 backdrop-blur-sm"
></div>
</div>
</div>
<!-- Informações do Tema -->
<div class="text-center">
<h3 class="text-base-content mb-1 text-lg font-bold">
{tema.nome}
</h3>
<p class="text-base-content/60 text-sm">{tema.descricao}</p>
</div>
<!-- Indicador de Seleção -->
{#if temaSelecionado === tema.id}
<div class="mt-4 flex items-center justify-center gap-2">
<CheckCircle class="text-primary h-5 w-5" strokeWidth={2} />
<span class="text-primary text-sm font-semibold">Tema Ativo</span>
</div>
{/if}
</div>
</button>
{/each}
</div>
<!-- Botão Salvar -->
<div class="flex justify-end">
<button
type="button"
class="btn btn-primary btn-lg gap-2 shadow-lg"
onclick={salvarTema}
disabled={salvandoTema || !temaSelecionado}
>
{#if salvandoTema}
<span class="loading loading-spinner loading-sm"></span>
Salvando...
{:else}
<CheckCircle class="h-5 w-5" strokeWidth={2} />
Salvar Tema
{/if}
</button>
</div>
<!-- Informação Adicional -->
<div class="alert alert-info shadow-lg">
<Info class="h-6 w-6 shrink-0" strokeWidth={2} />
<div>
<h3 class="font-bold">Como funciona?</h3>
<p class="text-sm">
Clique em um tema para visualizar a prévia. O tema será aplicado
imediatamente, mas você precisa clicar em "Salvar Tema" para que a
preferência seja mantida em acessos futuros.
</p>
</div>
</div>
</div>
{/if}
</div>