565 lines
24 KiB
Vue
565 lines
24 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { supabase } from '@/lib/supabase/client'
|
|
import { useToast } from 'primevue/usetoast'
|
|
import { useConfirm } from 'primevue/useconfirm'
|
|
import Editor from 'primevue/editor'
|
|
|
|
const toast = useToast()
|
|
const confirm = useConfirm()
|
|
|
|
// ─── Estado ───────────────────────────────────────────────────────────────────
|
|
const slides = ref([])
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const previewIdx = ref(0)
|
|
|
|
const dialogOpen = ref(false)
|
|
const editingSlide = ref(null) // null = novo
|
|
|
|
const form = ref({ title: '', body: '', icon: '', ordem: 0, ativo: true })
|
|
|
|
// ─── Ícones disponíveis (subset PrimeIcons relevantes) ────────────────────────
|
|
const ICONS = [
|
|
{ value: 'pi-calendar-clock', label: 'Agenda' },
|
|
{ value: 'pi-users', label: 'Equipe' },
|
|
{ value: 'pi-globe', label: 'Online' },
|
|
{ value: 'pi-shield', label: 'Segurança' },
|
|
{ value: 'pi-heart-fill', label: 'Saúde' },
|
|
{ value: 'pi-chart-line', label: 'Estatísticas' },
|
|
{ value: 'pi-bell', label: 'Notificações' },
|
|
{ value: 'pi-lock', label: 'Privacidade' },
|
|
{ value: 'pi-mobile', label: 'Mobile' },
|
|
{ value: 'pi-sync', label: 'Sincronização' },
|
|
{ value: 'pi-star', label: 'Destaque' },
|
|
{ value: 'pi-check-circle', label: 'Aprovação' },
|
|
{ value: 'pi-comments', label: 'Comunicação' },
|
|
{ value: 'pi-file-edit', label: 'Prontuário' },
|
|
{ value: 'pi-briefcase', label: 'Profissional' },
|
|
{ value: 'pi-bolt', label: 'Performance' },
|
|
]
|
|
|
|
// ─── Computed ─────────────────────────────────────────────────────────────────
|
|
const slidesAtivos = computed(() => slides.value.filter(s => s.ativo).sort((a, b) => a.ordem - b.ordem))
|
|
const previewSlide = computed(() => slidesAtivos.value[previewIdx.value] ?? slidesAtivos.value[0] ?? null)
|
|
|
|
// ─── Supabase ─────────────────────────────────────────────────────────────────
|
|
async function load () {
|
|
loading.value = true
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('login_carousel_slides')
|
|
.select('*')
|
|
.order('ordem', { ascending: true })
|
|
if (error) throw error
|
|
slides.value = data || []
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao carregar slides.', life: 4000 })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function stripHtml (s) {
|
|
return String(s || '').replace(/<[^>]+>/g, '').trim()
|
|
}
|
|
|
|
async function saveSlide () {
|
|
if (!stripHtml(form.value.title) || !stripHtml(form.value.body)) {
|
|
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Título e conteúdo são obrigatórios.', life: 3000 })
|
|
return
|
|
}
|
|
saving.value = true
|
|
try {
|
|
const payload = {
|
|
title: form.value.title,
|
|
body: form.value.body,
|
|
icon: form.value.icon || 'pi-star',
|
|
ordem: form.value.ordem,
|
|
ativo: form.value.ativo,
|
|
}
|
|
if (editingSlide.value) {
|
|
const { error } = await supabase
|
|
.from('login_carousel_slides')
|
|
.update(payload)
|
|
.eq('id', editingSlide.value.id)
|
|
if (error) throw error
|
|
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Slide atualizado com sucesso.', life: 3000 })
|
|
} else {
|
|
const maxOrdem = slides.value.length ? Math.max(...slides.value.map(s => s.ordem)) + 1 : 0
|
|
payload.ordem = maxOrdem
|
|
const { error } = await supabase
|
|
.from('login_carousel_slides')
|
|
.insert(payload)
|
|
if (error) throw error
|
|
toast.add({ severity: 'success', summary: 'Criado', detail: 'Slide adicionado com sucesso.', life: 3000 })
|
|
}
|
|
dialogOpen.value = false
|
|
await load()
|
|
previewIdx.value = 0
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao salvar.', life: 4000 })
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function toggleAtivo (slide) {
|
|
try {
|
|
const { error } = await supabase
|
|
.from('login_carousel_slides')
|
|
.update({ ativo: !slide.ativo })
|
|
.eq('id', slide.id)
|
|
if (error) throw error
|
|
slide.ativo = !slide.ativo
|
|
toast.add({ severity: 'info', summary: slide.ativo ? 'Ativado' : 'Desativado', detail: `"${slide.title}"`, life: 2500 })
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
|
|
}
|
|
}
|
|
|
|
async function deleteSlide (slide) {
|
|
confirm.require({
|
|
message: `Remover o slide "${slide.title}"? Esta ação não pode ser desfeita.`,
|
|
header: 'Confirmar remoção',
|
|
icon: 'pi pi-exclamation-triangle',
|
|
acceptClass: 'p-button-danger',
|
|
acceptLabel: 'Remover',
|
|
rejectLabel: 'Cancelar',
|
|
accept: async () => {
|
|
try {
|
|
const { error } = await supabase.from('login_carousel_slides').delete().eq('id', slide.id)
|
|
if (error) throw error
|
|
toast.add({ severity: 'success', summary: 'Removido', detail: `Slide removido.`, life: 2500 })
|
|
await load()
|
|
previewIdx.value = 0
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
async function moveSlide (slide, dir) {
|
|
const sorted = [...slides.value].sort((a, b) => a.ordem - b.ordem)
|
|
const idx = sorted.findIndex(s => s.id === slide.id)
|
|
const swapIdx = idx + dir
|
|
if (swapIdx < 0 || swapIdx >= sorted.length) return
|
|
|
|
const a = sorted[idx]
|
|
const b = sorted[swapIdx]
|
|
const tempOrdem = a.ordem
|
|
|
|
try {
|
|
await supabase.from('login_carousel_slides').update({ ordem: b.ordem }).eq('id', a.id)
|
|
await supabase.from('login_carousel_slides').update({ ordem: tempOrdem }).eq('id', b.id)
|
|
await load()
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
|
|
}
|
|
}
|
|
|
|
// ─── Dialog helpers ───────────────────────────────────────────────────────────
|
|
function openNew () {
|
|
editingSlide.value = null
|
|
form.value = { title: '', body: '', icon: 'pi-calendar-clock', ordem: 0, ativo: true }
|
|
dialogOpen.value = true
|
|
}
|
|
|
|
function openEdit (slide) {
|
|
editingSlide.value = slide
|
|
form.value = { title: slide.title, body: slide.body, icon: slide.icon || 'pi-star', ordem: slide.ordem, ativo: slide.ativo }
|
|
dialogOpen.value = true
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<Toast />
|
|
<ConfirmDialog />
|
|
|
|
<!-- Sentinel -->
|
|
<div class="h-px" />
|
|
|
|
<!-- Hero sticky -->
|
|
<div
|
|
class="sticky mx-3 md:mx-4 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
|
|
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
|
>
|
|
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
|
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-20 bg-indigo-400/10" />
|
|
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-emerald-400/10" />
|
|
<div class="absolute rounded-full blur-[70px] w-72 h-72 -bottom-20 right-24 bg-fuchsia-400/10" />
|
|
</div>
|
|
<div class="relative z-10 flex items-center justify-between gap-3 flex-wrap">
|
|
<div class="min-w-0">
|
|
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)] flex items-center gap-2">
|
|
<i class="pi pi-images text-indigo-500" />
|
|
Carrossel do Login
|
|
</div>
|
|
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">
|
|
Gerencie os slides exibidos na tela de login do sistema
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
severity="secondary"
|
|
outlined
|
|
title="Recarregar"
|
|
:loading="loading"
|
|
@click="load"
|
|
/>
|
|
<Button
|
|
icon="pi pi-plus"
|
|
label="Novo slide"
|
|
@click="openNew"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
|
|
|
|
<div class="grid grid-cols-1 xl:grid-cols-[1fr_340px] gap-6 items-start">
|
|
|
|
<!-- ── Tabela de slides ──────────────────────────────────────────────── -->
|
|
<div class="bg-[var(--surface-card,#fff)] rounded-md border border-[var(--surface-border)] overflow-hidden">
|
|
|
|
<!-- Loading skeleton -->
|
|
<div v-if="loading" class="flex flex-col divide-y divide-[var(--surface-border)]">
|
|
<div v-for="i in 4" :key="i" class="flex items-center gap-4 px-5 py-4 animate-pulse">
|
|
<div class="w-10 h-10 rounded-md bg-[var(--surface-ground)] flex-shrink-0" />
|
|
<div class="flex-1 space-y-2">
|
|
<div class="h-3.5 w-40 rounded bg-[var(--surface-ground)]" />
|
|
<div class="h-3 w-64 rounded bg-[var(--surface-ground)]" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lista vazia -->
|
|
<div v-else-if="!slides.length" class="flex flex-col items-center justify-center py-16 gap-3 text-[var(--text-color-secondary)]">
|
|
<i class="pi pi-images text-4xl opacity-30" />
|
|
<span class="text-[1rem]">Nenhum slide cadastrado ainda.</span>
|
|
<Button label="Criar primeiro slide" size="small" @click="openNew" />
|
|
</div>
|
|
|
|
<!-- Rows -->
|
|
<div v-else class="divide-y divide-[var(--surface-border)]">
|
|
<!-- Header -->
|
|
<div class="grid grid-cols-[auto_1fr_auto_auto] items-center gap-4 px-5 py-2.5 bg-[var(--surface-ground)] text-xs font-bold uppercase tracking-widest text-[var(--text-color-secondary)]">
|
|
<span class="w-10" />
|
|
<span>Slide</span>
|
|
<span class="text-center w-[60px]">Status</span>
|
|
<span class="w-[96px]" />
|
|
</div>
|
|
|
|
<div
|
|
v-for="(slide, i) in [...slides].sort((a,b) => a.ordem - b.ordem)"
|
|
:key="slide.id"
|
|
class="grid grid-cols-[auto_1fr_auto_auto] items-center gap-4 px-5 py-3.5 transition-colors duration-100 hover:bg-[var(--surface-ground,#f8fafc)] group"
|
|
>
|
|
<!-- Ícone + ordem -->
|
|
<div class="relative flex-shrink-0">
|
|
<div
|
|
class="w-10 h-10 rounded-md flex items-center justify-center text-lg"
|
|
:class="slide.ativo ? 'bg-indigo-500/10 text-indigo-500' : 'bg-[var(--surface-ground)] text-[var(--text-color-secondary)]'"
|
|
>
|
|
<i :class="['pi', slide.icon || 'pi-star']" />
|
|
</div>
|
|
<span class="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-[var(--surface-border)] text-[0.58rem] font-bold flex items-center justify-center text-[var(--text-color-secondary)]">
|
|
{{ slide.ordem + 1 }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Conteúdo -->
|
|
<div class="min-w-0">
|
|
<div class="text-[1rem] font-semibold text-[var(--text-color)] truncate [&_*]:inline" :class="!slide.ativo && 'opacity-40 line-through'" v-html="slide.title" />
|
|
<div class="text-[1rem] text-[var(--text-color-secondary)] truncate mt-0.5 [&_*]:inline" :class="!slide.ativo && 'opacity-40'" v-html="slide.body" />
|
|
</div>
|
|
|
|
<!-- Toggle ativo -->
|
|
<div class="flex justify-center w-[60px]">
|
|
<InputSwitch
|
|
:modelValue="slide.ativo"
|
|
@update:modelValue="() => toggleAtivo(slide)"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Ações -->
|
|
<div class="flex items-center gap-1 w-[96px] justify-end opacity-0 group-hover:opacity-100 transition-opacity duration-150">
|
|
<button
|
|
class="w-7 h-7 rounded-md flex items-center justify-center text-[var(--text-color-secondary)] hover:bg-[var(--surface-border)] hover:text-[var(--text-color)] transition-colors duration-100 disabled:opacity-30"
|
|
:disabled="i === 0"
|
|
title="Mover para cima"
|
|
@click="moveSlide(slide, -1)"
|
|
>
|
|
<i class="pi pi-chevron-up text-xs" />
|
|
</button>
|
|
<button
|
|
class="w-7 h-7 rounded-md flex items-center justify-center text-[var(--text-color-secondary)] hover:bg-[var(--surface-border)] hover:text-[var(--text-color)] transition-colors duration-100 disabled:opacity-30"
|
|
:disabled="i === slides.length - 1"
|
|
title="Mover para baixo"
|
|
@click="moveSlide(slide, 1)"
|
|
>
|
|
<i class="pi pi-chevron-down text-xs" />
|
|
</button>
|
|
<button
|
|
class="w-7 h-7 rounded-md flex items-center justify-center text-[var(--text-color-secondary)] hover:bg-indigo-50 hover:text-indigo-600 transition-colors duration-100"
|
|
title="Editar"
|
|
@click="openEdit(slide)"
|
|
>
|
|
<i class="pi pi-pencil text-xs" />
|
|
</button>
|
|
<button
|
|
class="w-7 h-7 rounded-md flex items-center justify-center text-[var(--text-color-secondary)] hover:bg-red-50 hover:text-red-500 transition-colors duration-100"
|
|
title="Remover"
|
|
@click="deleteSlide(slide)"
|
|
>
|
|
<i class="pi pi-trash text-xs" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Preview ──────────────────────────────────────────────────────── -->
|
|
<div class="sticky top-6 flex flex-col gap-3">
|
|
<div class="text-xs font-bold uppercase tracking-widest text-[var(--text-color-secondary)] flex items-center gap-1.5 px-1">
|
|
<i class="pi pi-eye" /> Pré-visualização
|
|
</div>
|
|
|
|
<!-- Mock da tela de login — lado esquerdo -->
|
|
<div class="relative overflow-hidden rounded-md aspect-[9/16] max-h-[480px] w-full select-none shadow-xl">
|
|
<div class="absolute inset-0 bg-gradient-to-br from-indigo-600 via-violet-600 to-purple-700" />
|
|
|
|
<!-- Grade decorativa -->
|
|
<div
|
|
class="absolute inset-0 opacity-[0.08]"
|
|
style="background-image: linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px); background-size: 32px 32px;"
|
|
/>
|
|
|
|
<!-- Orbs -->
|
|
<div class="absolute -top-20 -left-20 h-64 w-64 rounded-full bg-white/10 blur-3xl pointer-events-none" />
|
|
<div class="absolute bottom-0 right-0 h-48 w-48 rounded-full bg-violet-300/20 blur-3xl pointer-events-none" />
|
|
|
|
<div class="relative z-10 flex flex-col h-full p-6">
|
|
<!-- Brand mock -->
|
|
<div class="flex items-center gap-2">
|
|
<div class="grid h-7 w-7 place-items-center rounded-lg bg-white/20 border border-white/20">
|
|
<i class="pi pi-heart-fill text-white text-[0.6rem]" />
|
|
</div>
|
|
<span class="text-white/90 font-bold text-xs tracking-tight">Agência PSI</span>
|
|
</div>
|
|
|
|
<!-- Slide content -->
|
|
<div class="flex-1 flex flex-col justify-center gap-4">
|
|
<Transition name="prev-fade" mode="out-in">
|
|
<div v-if="previewSlide" :key="previewSlide.id ?? previewIdx" class="space-y-4">
|
|
<div class="grid h-11 w-11 place-items-center rounded-md bg-white/15 border border-white/20 shadow-lg">
|
|
<i :class="['pi', previewSlide.icon || 'pi-star', 'text-white text-lg']" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div class="text-xl font-bold text-white leading-tight prose prose-invert prose-sm max-w-none" v-html="previewSlide.title" />
|
|
<div class="text-[1rem] text-white/70 leading-relaxed prose prose-invert prose-sm max-w-none" v-html="previewSlide.body" />
|
|
</div>
|
|
</div>
|
|
<div v-else class="flex flex-col items-center justify-center gap-2 text-white/30 text-xs">
|
|
<i class="pi pi-ban text-2xl" />
|
|
Nenhum slide ativo
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
|
|
<!-- Dots -->
|
|
<div class="flex items-center gap-1.5">
|
|
<button
|
|
v-for="(s, i) in slidesAtivos"
|
|
:key="s.id"
|
|
class="transition-all duration-300 rounded-full"
|
|
:class="i === previewIdx ? 'w-5 h-1.5 bg-white shadow' : 'w-1.5 h-1.5 bg-white/35 hover:bg-white/60'"
|
|
@click="previewIdx = i"
|
|
/>
|
|
<span v-if="slidesAtivos.length" class="ml-2 text-[0.6rem] text-white/40 tabular-nums">
|
|
{{ previewIdx + 1 }}/{{ slidesAtivos.length }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="rounded-lg border border-[var(--surface-border)] bg-[var(--surface-card)] px-4 py-3 text-xs text-[var(--text-color-secondary)] flex items-start gap-2">
|
|
<i class="pi pi-info-circle text-indigo-500 mt-px flex-shrink-0" />
|
|
<span>Clique nos pontos para navegar entre os slides ativos. A ordem e visibilidade refletem o que o usuário verá no login.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── SQL Helper ───────────────────────────────────────────────────────── -->
|
|
<div class="bg-[var(--surface-card)] rounded-md border border-[var(--surface-border)] px-5 py-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<i class="pi pi-database text-amber-500 text-[1rem]" />
|
|
<span class="text-xs font-bold text-[var(--text-color)] uppercase tracking-widest">SQL de referência</span>
|
|
<span class="ml-auto text-xs text-[var(--text-color-secondary)]">Execute no Supabase caso a tabela não exista</span>
|
|
</div>
|
|
<pre class="text-[0.7rem] bg-[var(--surface-ground)] rounded-lg p-3.5 overflow-x-auto text-[var(--text-color-secondary)] leading-relaxed whitespace-pre-wrap"><code>create table if not exists public.login_carousel_slides (
|
|
id uuid primary key default gen_random_uuid(),
|
|
title text not null,
|
|
body text not null,
|
|
icon text not null default 'pi-star',
|
|
ordem integer not null default 0,
|
|
ativo boolean not null default true,
|
|
created_at timestamptz default now(),
|
|
updated_at timestamptz default now()
|
|
);
|
|
|
|
-- RLS: apenas saas_admin pode gerenciar
|
|
alter table public.login_carousel_slides enable row level security;
|
|
|
|
create policy "saas_admin_full" on public.login_carousel_slides
|
|
for all using (
|
|
exists (
|
|
select 1 from public.profiles
|
|
where id = auth.uid() and role = 'saas_admin'
|
|
)
|
|
);
|
|
|
|
-- Leitura pública (login não tem usuário autenticado)
|
|
create policy "public_read" on public.login_carousel_slides
|
|
for select using (ativo = true);</code></pre>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- /px-3 content wrapper -->
|
|
|
|
<!-- ── Dialog: Criar / Editar slide ───────────────────────────────────────── -->
|
|
<Dialog
|
|
v-model:visible="dialogOpen"
|
|
modal
|
|
:header="editingSlide ? 'Editar slide' : 'Novo slide'"
|
|
:draggable="false"
|
|
:style="{ width: '46rem', maxWidth: '96vw' }"
|
|
>
|
|
<div class="flex flex-col gap-4 pt-1">
|
|
|
|
<!-- Título -->
|
|
<div class="flex flex-col gap-1.5">
|
|
<label class="text-[1rem] font-semibold text-[var(--text-color)]">Título <span class="text-red-500">*</span></label>
|
|
<Editor
|
|
v-model="form.title"
|
|
:pt="{ toolbar: { style: 'display:none' } }"
|
|
style="height: 72px"
|
|
editorStyle="font-size: 1rem; font-weight: 600;"
|
|
placeholder="Ex: Gestão clínica simplificada"
|
|
>
|
|
<template #toolbar>
|
|
<span class="ql-formats">
|
|
<button class="ql-bold" />
|
|
<button class="ql-italic" />
|
|
<button class="ql-underline" />
|
|
</span>
|
|
</template>
|
|
</Editor>
|
|
</div>
|
|
|
|
<!-- Conteúdo -->
|
|
<div class="flex flex-col gap-1.5">
|
|
<label class="text-[1rem] font-semibold text-[var(--text-color)]">Conteúdo <span class="text-red-500">*</span></label>
|
|
<Editor
|
|
v-model="form.body"
|
|
style="height: 160px"
|
|
editorStyle="font-size: 1rem;"
|
|
>
|
|
<template #toolbar>
|
|
<span class="ql-formats">
|
|
<button class="ql-bold" />
|
|
<button class="ql-italic" />
|
|
<button class="ql-underline" />
|
|
</span>
|
|
<span class="ql-formats">
|
|
<button class="ql-list" value="ordered" />
|
|
<button class="ql-list" value="bullet" />
|
|
</span>
|
|
<span class="ql-formats">
|
|
<button class="ql-link" />
|
|
<button class="ql-clean" />
|
|
</span>
|
|
</template>
|
|
</Editor>
|
|
</div>
|
|
|
|
<!-- Ícone -->
|
|
<div class="flex flex-col gap-1.5">
|
|
<label class="text-[1rem] font-semibold text-[var(--text-color)]">Ícone</label>
|
|
<div class="grid grid-cols-4 sm:grid-cols-8 gap-1.5">
|
|
<button
|
|
v-for="ic in ICONS"
|
|
:key="ic.value"
|
|
type="button"
|
|
class="flex flex-col items-center justify-center gap-1 py-2 rounded-lg border text-xs transition-all duration-100"
|
|
:class="form.icon === ic.value
|
|
? 'border-indigo-500 bg-indigo-50 text-indigo-600 shadow-sm'
|
|
: 'border-[var(--surface-border)] bg-[var(--surface-ground)] text-[var(--text-color-secondary)] hover:border-indigo-300 hover:text-indigo-500'"
|
|
:title="ic.label"
|
|
@click="form.icon = ic.value"
|
|
>
|
|
<i :class="['pi', ic.value, 'text-base']" />
|
|
<span class="text-[0.6rem] leading-none">{{ ic.label }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ativo -->
|
|
<div class="flex items-center gap-3">
|
|
<InputSwitch v-model="form.ativo" inputId="slide-ativo" />
|
|
<label for="slide-ativo" class="text-[1rem] text-[var(--text-color)] cursor-pointer select-none">
|
|
Slide ativo (visível no carrossel)
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Mini preview -->
|
|
<div
|
|
class="relative overflow-hidden rounded-md p-5 flex items-center gap-4"
|
|
style="background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)"
|
|
>
|
|
<div class="grid h-12 w-12 flex-shrink-0 place-items-center rounded-md bg-white/15 border border-white/20 shadow">
|
|
<i :class="['pi', form.icon || 'pi-star', 'text-white text-xl']" />
|
|
</div>
|
|
<div class="min-w-0 overflow-hidden">
|
|
<div class="text-[1rem] font-bold text-white line-clamp-2 prose prose-invert prose-sm max-w-none" v-html="stripHtml(form.title) ? form.title : 'Título do slide'" />
|
|
<div class="text-[1rem] text-white/70 mt-0.5 line-clamp-3 prose prose-invert prose-sm max-w-none" v-html="stripHtml(form.body) ? form.body : 'Conteúdo descritivo...'" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ações -->
|
|
<div class="flex justify-end gap-2 pt-1">
|
|
<Button label="Cancelar" severity="secondary" outlined :disabled="saving" @click="dialogOpen = false" />
|
|
<Button
|
|
:label="editingSlide ? 'Salvar alterações' : 'Criar slide'"
|
|
icon="pi pi-check"
|
|
:loading="saving"
|
|
@click="saveSlide"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
|
|
</template>
|
|
|
|
<style scoped>
|
|
.prev-fade-enter-active,
|
|
.prev-fade-leave-active {
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
}
|
|
.prev-fade-enter-from {
|
|
opacity: 0;
|
|
transform: translateY(12px);
|
|
}
|
|
.prev-fade-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-8px);
|
|
}
|
|
</style>
|