52c34cf63a
- edge function send-welcome-email: e-mail de boas-vindas ao DONO do tenant recem-provisionado (destinatario do JWT, SMTP global/sistema, defaults Mailpit). Best-effort, disparada fire-and-forget no OnboardingPage so no provisionamento novo. - vitrine: seed plan_public + bullets dos planos free (cartao "Gratis"); Landingpage passa a mostrar "Gratis para sempre" (isFreePlan) em vez de "—". - build OK Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
589 lines
35 KiB
Vue
589 lines
35 KiB
Vue
<!--
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Criado e desenvolvido por Leonardo Nohama
|
|
|
|
|
| Tecnologia aplicada à escuta.
|
|
| Estrutura para o cuidado.
|
|
|
|
|
| Arquivo: src/views/pages/public/Landingpage-v1.vue
|
|
| Data: 2026
|
|
| Local: São Carlos/SP — Brasil
|
|
|--------------------------------------------------------------------------
|
|
| © 2026 — Todos os direitos reservados
|
|
|--------------------------------------------------------------------------
|
|
-->
|
|
<script setup>
|
|
import { computed, ref, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { supabase } from '@/lib/supabase/client';
|
|
|
|
import Chip from 'primevue/chip';
|
|
import Accordion from 'primevue/accordion';
|
|
import AccordionTab from 'primevue/accordiontab';
|
|
|
|
const router = useRouter();
|
|
|
|
const brandName = 'Agência PSI'; // ajuste para o nome final do produto
|
|
const year = computed(() => new Date().getFullYear());
|
|
|
|
function go(path) {
|
|
router.push(path);
|
|
}
|
|
|
|
function scrollTo(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
const features = ref([
|
|
{ title: 'Agenda inteligente', desc: 'Configure semana, encaixes, bloqueios e visão por dia/semana.', icon: 'pi pi-calendar' },
|
|
{ title: 'Autoagendamento (PRO)', desc: 'Página para o paciente confirmar, agendar e reagendar sem fricção.', icon: 'pi pi-globe', pro: true },
|
|
{ title: 'Prontuário e sessões', desc: 'Registro por paciente, histórico por sessão e linha do tempo.', icon: 'pi pi-file-edit' },
|
|
{ title: 'Financeiro integrado', desc: 'Receitas e despesas conectadas ao que acontece na agenda.', icon: 'pi pi-wallet' },
|
|
{ title: 'Pacientes e tags', desc: 'Segmentação por grupos, etiquetas e filtros para achar rápido.', icon: 'pi pi-users' },
|
|
{ title: 'Clínica / multi-profissional', desc: 'Múltiplos profissionais, papéis, convites e visão gerencial.', icon: 'pi pi-building' }
|
|
]);
|
|
|
|
/** PRICING dinâmico do SaaS */
|
|
const billingInterval = ref('year'); // 'month' | 'year'
|
|
const pricing = ref([]);
|
|
const loadingPricing = ref(false);
|
|
|
|
const featuredPlanKey = computed(() => {
|
|
const list = Array.isArray(pricing.value) ? pricing.value : [];
|
|
const featured = list.find((p) => p && p.is_featured && p.is_visible);
|
|
return featured?.plan_key || null;
|
|
});
|
|
|
|
function goStart() {
|
|
if (featuredPlanKey.value) {
|
|
router.push(`/auth/signup?plan=${encodeURIComponent(featuredPlanKey.value)}&interval=${billingInterval.value}`);
|
|
return;
|
|
}
|
|
router.push('/auth/signup');
|
|
}
|
|
|
|
function formatBRLFromCents(cents) {
|
|
if (cents == null) return '—';
|
|
const v = Number(cents) / 100;
|
|
if (!Number.isFinite(v)) return '—';
|
|
return v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
|
}
|
|
|
|
function priceFor(p) {
|
|
if (!p) return null;
|
|
const cents = billingInterval.value === 'year' ? p.yearly_cents : p.monthly_cents;
|
|
// fallback: se não existir anual, mostra mensal (e vice-versa)
|
|
if (cents == null) return billingInterval.value === 'year' ? p.monthly_cents : p.yearly_cents;
|
|
return cents;
|
|
}
|
|
|
|
// plano gratuito: por chave (_free) ou preço zero/ausente
|
|
function isFreePlan(p) {
|
|
const k = String(p?.plan_key || '').toLowerCase();
|
|
if (k.endsWith('_free') || k === 'free') return true;
|
|
const cents = priceFor(p);
|
|
return cents == null || Number(cents) === 0;
|
|
}
|
|
|
|
async function fetchPricing() {
|
|
loadingPricing.value = true;
|
|
try {
|
|
const { data, error } = await supabase.from('v_public_pricing').select('*').eq('is_visible', true).order('sort_order', { ascending: true });
|
|
|
|
if (!error) pricing.value = data || [];
|
|
} finally {
|
|
loadingPricing.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(fetchPricing);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="min-h-screen bg-[var(--surface-ground)] text-[var(--text-color)]">
|
|
<!-- TOPBAR -->
|
|
<div class="sticky top-0 z-40 border-b border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_12%)] backdrop-blur">
|
|
<div class="mx-auto max-w-7xl px-4 md:px-6 py-3 flex items-center justify-between gap-3">
|
|
<button class="flex items-center gap-3 min-w-0" @click="scrollTo('top')">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] grid place-items-center shadow-sm">
|
|
<i class="pi pi-sparkles text-lg opacity-80" />
|
|
</div>
|
|
<div class="min-w-0 text-left">
|
|
<div class="font-semibold leading-tight truncate">{{ brandName }}</div>
|
|
<div class="text-xs text-[var(--text-color-secondary)] truncate">Gestão clínica sem ruído.</div>
|
|
</div>
|
|
</button>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<Button label="Entrar" icon="pi pi-sign-in" severity="secondary" outlined @click="go('/auth/login')" />
|
|
<Button label="Começar" icon="pi pi-bolt" @click="goStart()" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- HERO -->
|
|
<section id="top" class="relative overflow-hidden">
|
|
<!-- background: noir grid + glow -->
|
|
<div class="pointer-events-none absolute inset-0">
|
|
<div class="hero-grid absolute inset-0 opacity-[0.35]" />
|
|
<div class="absolute -top-28 -left-28 h-96 w-96 rounded-full blur-3xl opacity-60 bg-indigo-400/10" />
|
|
<div class="absolute top-20 -right-24 h-[28rem] w-[28rem] rounded-full blur-3xl opacity-60 bg-emerald-400/10" />
|
|
<div class="absolute -bottom-40 left-1/3 h-[34rem] w-[34rem] rounded-full blur-3xl opacity-60 bg-fuchsia-400/10" />
|
|
<div class="hero-noise absolute inset-0 opacity-[0.12]" />
|
|
</div>
|
|
|
|
<div class="mx-auto max-w-7xl px-4 md:px-6 pt-10 md:pt-16 pb-10 md:pb-14 relative">
|
|
<div class="grid grid-cols-12 gap-6 items-center">
|
|
<div class="col-span-12 lg:col-span-7">
|
|
<div class="flex flex-wrap items-center gap-2 mb-4">
|
|
<Chip label="Para psicólogos e clínicas" icon="pi pi-shield" />
|
|
<span class="text-xs text-[var(--text-color-secondary)]"> • menos dispersão • mais presença • mais previsibilidade </span>
|
|
</div>
|
|
|
|
<h1 class="text-3xl md:text-5xl font-semibold leading-tight">Um sistema que <span class="hero-underline">reduz ruído</span> — sem roubar seu método.</h1>
|
|
|
|
<p class="mt-4 text-base md:text-lg text-[var(--text-color-secondary)] max-w-2xl leading-relaxed">
|
|
Centralize a rotina clínica em um lugar só: pacientes, sessões, lembretes e indicadores. O objetivo não é "burocratizar": é deixar o consultório respirável.
|
|
</p>
|
|
|
|
<div class="mt-6 flex flex-col sm:flex-row gap-2">
|
|
<Button label="Criar conta grátis" icon="pi pi-arrow-right" class="w-full sm:w-auto" @click="goStart()" />
|
|
<Button label="Ver planos" icon="pi pi-credit-card" severity="secondary" outlined class="w-full sm:w-auto" @click="scrollTo('pricing')" />
|
|
<Button label="Como funciona" icon="pi pi-compass" severity="secondary" text class="w-full sm:w-auto" @click="scrollTo('how')" />
|
|
</div>
|
|
|
|
<div class="mt-6 flex flex-wrap gap-2">
|
|
<Tag severity="secondary" value="Agenda online (PRO)" />
|
|
<Tag severity="secondary" value="Controle de sessões" />
|
|
<Tag severity="secondary" value="Financeiro integrado" />
|
|
<Tag severity="secondary" value="Clínica / multi-profissional" />
|
|
<Tag severity="secondary" value="Separação por tenant + RLS" />
|
|
</div>
|
|
|
|
<div class="mt-6 text-xs text-[var(--text-color-secondary)]">"A diferença entre ter uma agenda e ter um sistema mora nos detalhes."</div>
|
|
</div>
|
|
|
|
<div class="col-span-12 lg:col-span-5">
|
|
<Card class="overflow-hidden rounded-[2rem] border border-[var(--surface-border)]">
|
|
<template #content>
|
|
<div class="p-1">
|
|
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div class="font-semibold text-lg">Painel de hoje</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)]">Um recorte: o essencial, sem excesso.</div>
|
|
</div>
|
|
<i class="pi pi-chart-line opacity-70" />
|
|
</div>
|
|
|
|
<Divider class="my-4" />
|
|
|
|
<div class="grid grid-cols-12 gap-3">
|
|
<div class="col-span-12 md:col-span-6">
|
|
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-3">
|
|
<div class="text-xs text-[var(--text-color-secondary)]">Sessões</div>
|
|
<div class="text-2xl font-semibold mt-1">6</div>
|
|
<div class="text-xs text-[var(--text-color-secondary)] mt-1">com lembretes automáticos</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-span-12 md:col-span-6">
|
|
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-3">
|
|
<div class="text-xs text-[var(--text-color-secondary)]">Recebimentos</div>
|
|
<div class="text-2xl font-semibold mt-1">R$ 840</div>
|
|
<div class="text-xs text-[var(--text-color-secondary)] mt-1">visão clara do mês</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-span-12">
|
|
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-3">
|
|
<div class="flex items-center justify-between">
|
|
<div class="min-w-0">
|
|
<div class="text-xs text-[var(--text-color-secondary)]">Prontuário</div>
|
|
<div class="font-semibold mt-1 truncate">Anotações e histórico</div>
|
|
<div class="text-xs text-[var(--text-color-secondary)] mt-1">por paciente • por sessão • linha do tempo</div>
|
|
</div>
|
|
<i class="pi pi-file-edit opacity-70" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 text-xs text-[var(--text-color-secondary)]">* Ilustração conceitual do produto.</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
|
|
<div class="mt-4 grid grid-cols-12 gap-3">
|
|
<div class="col-span-12 sm:col-span-6">
|
|
<div class="rounded-2xl border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_10%)] p-4">
|
|
<div class="text-xs text-[var(--text-color-secondary)]">Promessa</div>
|
|
<div class="font-semibold mt-1">Organizar sem invadir.</div>
|
|
<div class="text-xs text-[var(--text-color-secondary)] mt-1">Você define o método. O sistema remove ruído.</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-span-12 sm:col-span-6">
|
|
<div class="rounded-2xl border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_10%)] p-4">
|
|
<div class="text-xs text-[var(--text-color-secondary)]">Arquitetura</div>
|
|
<div class="font-semibold mt-1">Multi-tenant de verdade.</div>
|
|
<div class="text-xs text-[var(--text-color-secondary)] mt-1">Menus + guards + RLS alinhados.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- VALUE STRIP -->
|
|
<section class="mx-auto max-w-7xl px-4 md:px-6 pb-10">
|
|
<div class="grid grid-cols-12 gap-4">
|
|
<div class="col-span-12 md:col-span-4">
|
|
<Card class="h-full rounded-[2rem]">
|
|
<template #content>
|
|
<div class="flex items-start gap-3">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
|
|
<i class="pi pi-calendar opacity-80" />
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">Agenda e autoagendamento</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1">O paciente confirma, agenda e reagenda com autonomia (PRO).</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<div class="col-span-12 md:col-span-4">
|
|
<Card class="h-full rounded-[2rem]">
|
|
<template #content>
|
|
<div class="flex items-start gap-3">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
|
|
<i class="pi pi-wallet opacity-80" />
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">Financeiro integrado</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1">Receita/despesa junto da agenda — sem planilhas espalhadas.</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<div class="col-span-12 md:col-span-4">
|
|
<Card class="h-full rounded-[2rem]">
|
|
<template #content>
|
|
<div class="flex items-start gap-3">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
|
|
<i class="pi pi-lock opacity-80" />
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">Prontuário e sessões</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1">Registro e histórico acessíveis, com organização e backup.</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 text-xs text-[var(--text-color-secondary)]">Módulos: agenda, prontuário/sessões, financeiro e gestão multi-profissional.</div>
|
|
</section>
|
|
|
|
<!-- HOW IT WORKS -->
|
|
<section id="how" class="mx-auto max-w-7xl px-4 md:px-6 pb-12 scroll-mt-24">
|
|
<div class="flex items-end justify-between gap-3 mb-4">
|
|
<div>
|
|
<div class="text-2xl md:text-3xl font-semibold">Como funciona</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1">Três movimentos: preparar, atender, acompanhar — sem fricção.</div>
|
|
</div>
|
|
<Button label="Ver planos" severity="secondary" outlined icon="pi pi-arrow-down" @click="scrollTo('pricing')" />
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-4">
|
|
<div class="col-span-12 md:col-span-4">
|
|
<Card class="h-full rounded-[2rem]">
|
|
<template #content>
|
|
<div class="flex items-start gap-3">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
|
|
<i class="pi pi-sliders-h opacity-80" />
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">1) Preparar</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1 leading-relaxed">Configure agenda, encaixes e regras. Se quiser, habilite autoagendamento (PRO).</div>
|
|
<div class="mt-2">
|
|
<Tag severity="secondary" value="Bloqueios" />
|
|
<Tag class="ml-2" severity="secondary" value="Encaixes" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<div class="col-span-12 md:col-span-4">
|
|
<Card class="h-full rounded-[2rem]">
|
|
<template #content>
|
|
<div class="flex items-start gap-3">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
|
|
<i class="pi pi-comments opacity-80" />
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">2) Atender</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1 leading-relaxed">Sessão acontece. Registro fica onde precisa ficar: no prontuário, no tempo certo.</div>
|
|
<div class="mt-2">
|
|
<Tag severity="secondary" value="Sessões" />
|
|
<Tag class="ml-2" severity="secondary" value="Prontuário" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<div class="col-span-12 md:col-span-4">
|
|
<Card class="h-full rounded-[2rem]">
|
|
<template #content>
|
|
<div class="flex items-start gap-3">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
|
|
<i class="pi pi-chart-bar opacity-80" />
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold">3) Acompanhar</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1 leading-relaxed">Financeiro e indicadores acompanham o movimento. Menos "cadê?", mais previsibilidade.</div>
|
|
<div class="mt-2">
|
|
<Tag severity="secondary" value="Recebimentos" />
|
|
<Tag class="ml-2" severity="secondary" value="Indicadores" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider class="my-8" />
|
|
|
|
<Accordion :activeIndex="0">
|
|
<AccordionTab header="Como fica o fluxo na prática?">
|
|
<div class="text-sm text-[var(--text-color-secondary)] leading-relaxed">
|
|
Você abre a agenda, a sessão acontece, o registro vai para o prontuário, e o financeiro acompanha. O sistema existe para manter o consultório respirando — não para virar uma burocracia nova.
|
|
</div>
|
|
</AccordionTab>
|
|
<AccordionTab header="E para clínica (multi-profissionais)?">
|
|
<div class="text-sm text-[var(--text-color-secondary)] leading-relaxed">Perfis por função, agendas separadas, visão gerencial e convites (Modelo B). Você cresce sem quebrar a estrutura.</div>
|
|
</AccordionTab>
|
|
<AccordionTab header="Privacidade e segurança">
|
|
<div class="text-sm text-[var(--text-color-secondary)] leading-relaxed">Separação por clínica/tenant, controle de acesso e políticas no banco (RLS). (Quando quiser, a gente cria uma página dedicada de LGPD/Segurança.)</div>
|
|
</AccordionTab>
|
|
</Accordion>
|
|
</section>
|
|
|
|
<!-- FEATURES -->
|
|
<section class="mx-auto max-w-7xl px-4 md:px-6 pb-12">
|
|
<div class="flex items-end justify-between gap-3 mb-4">
|
|
<div>
|
|
<div class="text-2xl md:text-3xl font-semibold">Recursos que sustentam a rotina</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1">O foco é tirar fricção — sem invadir o que é do seu método.</div>
|
|
</div>
|
|
<Button label="Ver planos" severity="secondary" outlined icon="pi pi-arrow-down" @click="scrollTo('pricing')" />
|
|
</div>
|
|
|
|
<div class="grid grid-cols-12 gap-4">
|
|
<div v-for="f in features" :key="f.title" class="col-span-12 md:col-span-6 lg:col-span-4">
|
|
<Card class="h-full rounded-[2rem]">
|
|
<template #content>
|
|
<div class="flex items-start gap-3">
|
|
<div class="h-10 w-10 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
|
|
<i :class="f.icon" class="opacity-80" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<div class="font-semibold">{{ f.title }}</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1 leading-relaxed">
|
|
{{ f.desc }}
|
|
</div>
|
|
<div v-if="f.pro" class="mt-2">
|
|
<Tag severity="warning" value="PRO" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- PRICING (dinâmico do SaaS) -->
|
|
<section id="pricing" class="mx-auto max-w-7xl px-4 md:px-6 pb-14 scroll-mt-24">
|
|
<div class="text-3xl md:text-4xl font-semibold text-center">Planos</div>
|
|
<div class="text-base md:text-lg text-[var(--text-color-secondary)] mt-2 text-center">Comece simples. Suba para PRO quando a agenda pedir automação.</div>
|
|
|
|
<!-- toggle -->
|
|
<div class="flex flex-col items-center text-center mt-6">
|
|
<div class="inline-flex items-center rounded-xl border border-[var(--surface-border)] bg-[color-mix(in_srgb,var(--surface-card),transparent_10%)] p-1">
|
|
<Button label="Mensal" size="small" :severity="billingInterval === 'month' ? 'success' : 'secondary'" :outlined="billingInterval !== 'month'" @click="billingInterval = 'month'" />
|
|
<Button label="Anual" size="small" :severity="billingInterval === 'year' ? 'success' : 'secondary'" :outlined="billingInterval !== 'year'" class="ml-1" @click="billingInterval = 'year'" />
|
|
</div>
|
|
|
|
<div v-if="billingInterval === 'year'" class="mt-2">
|
|
<Tag severity="success" value="Economize até 20%" />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loadingPricing" class="mt-8">
|
|
<div class="grid grid-cols-12 gap-4">
|
|
<div v-for="i in 3" :key="i" class="col-span-12 md:col-span-4">
|
|
<div class="rounded-[2rem] border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
|
|
<div class="h-4 w-24 bg-[color-mix(in_srgb,var(--surface-200),transparent_40%)] rounded mb-3 animate-pulse" />
|
|
<div class="h-7 w-40 bg-[color-mix(in_srgb,var(--surface-200),transparent_40%)] rounded mb-4 animate-pulse" />
|
|
<div class="h-10 w-48 bg-[color-mix(in_srgb,var(--surface-200),transparent_40%)] rounded mb-4 animate-pulse" />
|
|
<div class="space-y-2">
|
|
<div class="h-3 w-full bg-[color-mix(in_srgb,var(--surface-200),transparent_40%)] rounded animate-pulse" />
|
|
<div class="h-3 w-11/12 bg-[color-mix(in_srgb,var(--surface-200),transparent_40%)] rounded animate-pulse" />
|
|
<div class="h-3 w-10/12 bg-[color-mix(in_srgb,var(--surface-200),transparent_40%)] rounded animate-pulse" />
|
|
</div>
|
|
<div class="h-10 w-full bg-[color-mix(in_srgb,var(--surface-200),transparent_40%)] rounded-xl mt-6 animate-pulse" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="mt-8">
|
|
<div v-if="!pricing.length" class="rounded-[2rem] border border-[var(--surface-border)] bg-[var(--surface-card)] p-6 text-center">
|
|
<div class="text-lg font-semibold">Planos em preparação</div>
|
|
<div class="text-sm text-[var(--text-color-secondary)] mt-1">Ainda não há itens visíveis na vitrine pública. Publique no painel SaaS para aparecer aqui.</div>
|
|
<div class="mt-4 flex justify-center gap-2 flex-wrap">
|
|
<Button label="Entrar" severity="secondary" outlined icon="pi pi-sign-in" @click="go('/auth/login')" />
|
|
<Button label="Criar conta" icon="pi pi-bolt" @click="goStart()" />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="grid grid-cols-12 gap-4">
|
|
<div v-for="p in pricing" :key="p.plan_id" class="col-span-12 md:col-span-4">
|
|
<div
|
|
class="h-full rounded-[2rem] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden transition-transform duration-300"
|
|
:class="p.is_featured ? 'ring-1 ring-emerald-500/30 md:-translate-y-2 md:scale-[1.02]' : 'hover:-translate-y-1'"
|
|
>
|
|
<div class="relative p-5">
|
|
<div v-if="p.is_featured" class="pointer-events-none absolute inset-0 opacity-70">
|
|
<div class="absolute -top-20 -right-24 h-72 w-72 rounded-full bg-emerald-400/10 blur-3xl" />
|
|
<div class="absolute -bottom-24 left-10 h-72 w-72 rounded-full bg-indigo-400/10 blur-3xl" />
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="min-w-0">
|
|
<div class="text-sm text-[var(--text-color-secondary)]">
|
|
{{ p.badge || 'Plano' }}
|
|
</div>
|
|
<div class="text-xl font-semibold truncate">
|
|
{{ p.public_name || p.plan_name || p.plan_key }}
|
|
</div>
|
|
</div>
|
|
|
|
<Tag v-if="p.is_featured" severity="success" value="Popular" />
|
|
</div>
|
|
|
|
<div class="mt-4 text-3xl font-semibold leading-none">
|
|
<template v-if="isFreePlan(p)">
|
|
Grátis<span class="text-sm font-normal text-[var(--text-color-secondary)]"> para sempre </span>
|
|
</template>
|
|
<template v-else>
|
|
{{ formatBRLFromCents(priceFor(p)) }}
|
|
<span class="text-sm font-normal text-[var(--text-color-secondary)]"> /{{ billingInterval === 'month' ? 'mês' : 'ano' }} </span>
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="!isFreePlan(p) && billingInterval === 'year'" class="text-xs text-emerald-500 mt-1 font-medium">Melhor custo-benefício</div>
|
|
|
|
<div class="mt-2 text-sm text-[var(--text-color-secondary)] min-h-[44px] leading-relaxed">
|
|
{{ p.public_description || '—' }}
|
|
</div>
|
|
|
|
<Divider class="my-4" />
|
|
|
|
<ul v-if="p.bullets?.length" class="space-y-2 text-sm">
|
|
<li v-for="b in p.bullets" :key="b.id" class="flex items-start gap-2">
|
|
<i class="pi pi-check mt-1 text-emerald-500"></i>
|
|
<span :class="b.highlight ? 'font-semibold' : 'text-[var(--text-color-secondary)]'">
|
|
{{ b.text }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
|
|
<div v-else class="text-sm text-[var(--text-color-secondary)]">Benefícios em breve.</div>
|
|
|
|
<div class="mt-5">
|
|
<Button
|
|
label="Começar"
|
|
class="w-full"
|
|
:severity="p.is_featured ? 'success' : 'secondary'"
|
|
:outlined="!p.is_featured"
|
|
icon="pi pi-arrow-right"
|
|
@click="go(`/auth/signup?plan=${encodeURIComponent(p.plan_key)}&interval=${billingInterval}`)"
|
|
/>
|
|
</div>
|
|
|
|
<div class="mt-3 text-xs text-[var(--text-color-secondary)]">Plano vem da vitrine SaaS — sem mexer no código.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 text-xs text-[var(--text-color-secondary)]">Dica: esses planos podem mapear diretamente para entitlements (FREE/PRO) e features (plan_features).</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- FOOTER -->
|
|
<footer class="border-t border-[var(--surface-border)]">
|
|
<div class="mx-auto max-w-7xl px-4 md:px-6 py-8 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
|
<div>
|
|
<div class="font-semibold">{{ brandName }}</div>
|
|
<div class="text-xs text-[var(--text-color-secondary)] mt-1">© {{ year }} — Todos os direitos reservados.</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
<Button label="Entrar" severity="secondary" outlined icon="pi pi-sign-in" @click="go('/auth/login')" />
|
|
<Button label="Criar conta" icon="pi pi-bolt" @click="goStart()" />
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.hero-grid {
|
|
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.06) 1px, transparent 1px), linear-gradient(to bottom, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
|
|
background-size: 44px 44px;
|
|
mask-image: radial-gradient(circle at 30% 20%, black 0%, transparent 65%);
|
|
}
|
|
|
|
.hero-noise {
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.35'/%3E%3C/svg%3E");
|
|
background-size: 180px 180px;
|
|
}
|
|
|
|
.hero-underline {
|
|
position: relative;
|
|
display: inline-block;
|
|
white-space: nowrap;
|
|
}
|
|
.hero-underline::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: -2%;
|
|
right: -2%;
|
|
bottom: 0.12em;
|
|
height: 0.5em;
|
|
background: linear-gradient(90deg, rgba(16, 185, 129, 0), rgba(16, 185, 129, 0.22), rgba(99, 102, 241, 0.18), rgba(16, 185, 129, 0));
|
|
border-radius: 999px;
|
|
z-index: -1;
|
|
filter: blur(0.2px);
|
|
}
|
|
</style>
|