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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user