Layout 100%, Notificações, SetupWizard
This commit is contained in:
@@ -2,137 +2,149 @@
|
||||
<Toast />
|
||||
|
||||
<!-- Sentinel -->
|
||||
<div ref="headerSentinelRef" class="extlink-sentinel" />
|
||||
<div ref="headerSentinelRef" class="h-px" />
|
||||
|
||||
<!-- Hero sticky -->
|
||||
<div ref="headerEl" class="extlink-hero mx-3 md:mx-5 mb-4" :class="{ 'extlink-hero--stuck': headerStuck }">
|
||||
<div class="extlink-hero__blobs" aria-hidden="true">
|
||||
<div class="extlink-hero__blob extlink-hero__blob--1" />
|
||||
<div class="extlink-hero__blob extlink-hero__blob--2" />
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
HERO sticky
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<section
|
||||
ref="headerEl"
|
||||
class="sticky mx-3 md:mx-4 mb-3 z-20 overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] px-3 py-2.5 transition-[border-radius] duration-200"
|
||||
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
|
||||
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
|
||||
>
|
||||
<!-- Blobs -->
|
||||
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
||||
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-indigo-500/10" />
|
||||
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-emerald-400/[0.08]" />
|
||||
</div>
|
||||
|
||||
<!-- Row 1 -->
|
||||
<div class="extlink-hero__row1">
|
||||
<div class="extlink-hero__brand">
|
||||
<div class="extlink-hero__icon"><i class="pi pi-link text-lg" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="extlink-hero__title">Link de Cadastro</div>
|
||||
<div class="extlink-hero__sub">Compartilhe com o paciente para preencher o pré-cadastro com calma e segurança</div>
|
||||
<div class="relative z-[1] flex items-center gap-3">
|
||||
|
||||
<!-- Brand -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500">
|
||||
<i class="pi pi-link text-base" />
|
||||
</div>
|
||||
<div class="min-w-0 hidden lg:block">
|
||||
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Link de Cadastro</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)]">Compartilhe com o paciente para preencher o pré-cadastro</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop (≥1200px) -->
|
||||
<div class="hidden xl:flex items-center gap-2 shrink-0">
|
||||
<!-- Status + link rápido — desktop -->
|
||||
<div class="hidden xl:flex flex-1 min-w-0 mx-2 items-center gap-3">
|
||||
<!-- Badge de status -->
|
||||
<span
|
||||
class="inline-flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border transition-colors"
|
||||
class="inline-flex items-center gap-1.5 text-[0.75rem] px-2.5 py-1 rounded-full border flex-shrink-0 transition-colors"
|
||||
:class="inviteToken
|
||||
? 'border-emerald-200 text-emerald-700 bg-emerald-50'
|
||||
: 'border-[var(--surface-border)] text-[var(--text-color-secondary)] bg-[var(--surface-ground)]'"
|
||||
>
|
||||
<span
|
||||
class="h-2 w-2 rounded-full"
|
||||
:class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'"
|
||||
/>
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'" />
|
||||
{{ inviteToken ? 'Link ativo' : 'Gerando…' }}
|
||||
</span>
|
||||
<Button
|
||||
label="Gerar novo link"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full"
|
||||
:loading="rotating"
|
||||
@click="rotateLink"
|
||||
/>
|
||||
|
||||
<!-- Link inline -->
|
||||
<div v-if="!inviteToken" class="flex items-center gap-2 text-[1rem] text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-spin pi-spinner" /> Gerando link…
|
||||
</div>
|
||||
<InputGroup v-else class="max-w-xl">
|
||||
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
|
||||
<InputText readonly :value="publicUrl" class="font-mono text-[0.75rem]" />
|
||||
<Button icon="pi pi-copy" severity="secondary" title="Copiar link" @click="copyLink" />
|
||||
<Button icon="pi pi-external-link" severity="secondary" title="Abrir no navegador" @click="openLink" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<!-- Mobile (<1200px) -->
|
||||
<div class="flex xl:hidden items-center shrink-0">
|
||||
<!-- Ações desktop -->
|
||||
<div class="hidden xl:flex items-center gap-1 flex-shrink-0">
|
||||
<Button label="Gerar novo link" icon="pi pi-refresh" severity="secondary" outlined class="rounded-full" :loading="rotating" @click="rotateLink" />
|
||||
</div>
|
||||
|
||||
<!-- Mobile -->
|
||||
<div class="flex xl:hidden items-center gap-1 flex-shrink-0 ml-auto">
|
||||
<Button label="Ações" icon="pi pi-ellipsis-v" severity="secondary" size="small" class="rounded-full" @click="(e) => mobileMenuRef.toggle(e)" />
|
||||
<Menu ref="mobileMenuRef" :model="mobileMenuItems" :popup="true" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Divider -->
|
||||
<Divider class="extlink-hero__divider my-2" />
|
||||
<!-- ══════════════════════════════════════════════════════
|
||||
CONTEÚDO
|
||||
═══════════════════════════════════════════════════════ -->
|
||||
<div class="flex flex-col lg:flex-row gap-3 px-3 md:px-4 pb-5">
|
||||
|
||||
<!-- Row 2: link rápido (oculto no mobile) -->
|
||||
<div class="extlink-hero__row2">
|
||||
<div v-if="!inviteToken" class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-spin pi-spinner text-xs" /> Gerando link…
|
||||
</div>
|
||||
<InputGroup v-else class="max-w-2xl">
|
||||
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
|
||||
<InputText readonly :value="publicUrl" class="font-mono text-xs" />
|
||||
<Button icon="pi pi-copy" severity="secondary" title="Copiar link" @click="copyLink" />
|
||||
<Button icon="pi pi-external-link" severity="secondary" title="Abrir no navegador" @click="openLink" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conteúdo -->
|
||||
<div class="flex flex-col lg:flex-row gap-4 px-3 md:px-5 mb-5">
|
||||
|
||||
<!-- Esquerda: ações do link -->
|
||||
<div class="flex-1 min-w-0 flex flex-col gap-4">
|
||||
<!-- ── ESQUERDA: link + mensagem ──────────────────── -->
|
||||
<div class="flex-1 min-w-0 flex flex-col gap-3">
|
||||
|
||||
<!-- Card principal: link -->
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<div class="p-5 border-b border-[var(--surface-border)] flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
|
||||
|
||||
<!-- Header da seção -->
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap px-4 pt-3.5 pb-3 border-b border-[var(--surface-border,#e2e8f0)]">
|
||||
<div>
|
||||
<div class="font-semibold text-[var(--text-color)]">Seu link público</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Envie ao paciente por WhatsApp, e-mail ou mensagem direta</div>
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Envie ao paciente por WhatsApp, e-mail ou mensagem direta</div>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border"
|
||||
class="inline-flex items-center gap-1.5 text-[0.75rem] px-2.5 py-1 rounded-full border flex-shrink-0"
|
||||
:class="inviteToken
|
||||
? 'border-emerald-200 text-emerald-700 bg-emerald-50'
|
||||
: 'border-[var(--surface-border)] text-[var(--text-color-secondary)] bg-[var(--surface-ground)]'"
|
||||
>
|
||||
<span class="h-2 w-2 rounded-full" :class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'" />
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'" />
|
||||
{{ inviteToken ? 'Ativo' : 'Gerando…' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="p-4 flex flex-col gap-4">
|
||||
<!-- Skeleton -->
|
||||
<div v-if="!inviteToken" class="space-y-3">
|
||||
<div class="h-10 rounded-xl bg-[var(--surface-ground)] animate-pulse" />
|
||||
<div class="h-10 rounded-xl bg-[var(--surface-ground)] animate-pulse" />
|
||||
<div v-if="!inviteToken" class="flex flex-col gap-3">
|
||||
<div class="h-10 rounded-md bg-[var(--surface-ground,#f8fafc)] animate-pulse" />
|
||||
<div class="h-10 rounded-md bg-[var(--surface-ground,#f8fafc)] animate-pulse" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<!-- Link com ações -->
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<!-- InputGroup do link -->
|
||||
<InputGroup>
|
||||
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
|
||||
<InputText readonly :value="publicUrl" class="font-mono text-xs" />
|
||||
<InputText readonly :value="publicUrl" class="font-mono text-[0.75rem]" />
|
||||
<Button icon="pi pi-copy" severity="secondary" title="Copiar" @click="copyLink" />
|
||||
<Button icon="pi pi-external-link" severity="secondary" title="Abrir" @click="openLink" />
|
||||
</InputGroup>
|
||||
|
||||
<div class="text-xs text-[var(--text-color-secondary)]">
|
||||
Token: <span class="font-mono select-all">{{ inviteToken }}</span>
|
||||
<div class="text-[0.75rem] text-[var(--text-color-secondary)]">
|
||||
Token: <span class="font-mono select-all opacity-60">{{ inviteToken }}</span>
|
||||
</div>
|
||||
|
||||
<!-- CTAs rápidas -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<button class="extlink-cta-btn" @click="copyLink">
|
||||
<div class="extlink-cta-btn__icon bg-[color-mix(in_srgb,var(--p-primary-500,#6366f1)_12%,transparent)] text-[var(--p-primary-500,#6366f1)]">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||
<!-- Copiar link -->
|
||||
<button
|
||||
class="flex items-center gap-3 px-3.5 py-3 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] cursor-pointer text-left transition-[background,box-shadow,transform] duration-150 hover:bg-[var(--surface-hover,#f1f5f9)] hover:shadow-[0_2px_12px_rgba(0,0,0,0.06)] hover:-translate-y-px active:translate-y-0"
|
||||
@click="copyLink"
|
||||
>
|
||||
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500">
|
||||
<i class="pi pi-copy" />
|
||||
</div>
|
||||
<div class="text-left min-w-0">
|
||||
<div class="font-semibold text-sm text-[var(--text-color)]">Copiar link</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)]">Cole no WhatsApp ou e-mail</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-[1rem] text-[var(--text-color)]">Copiar link</div>
|
||||
<div class="text-[0.75rem] text-[var(--text-color-secondary)]">Cole no WhatsApp ou e-mail</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button class="extlink-cta-btn" @click="copyInviteMessage">
|
||||
<div class="extlink-cta-btn__icon bg-emerald-500/10 text-emerald-600">
|
||||
<!-- Copiar mensagem pronta -->
|
||||
<button
|
||||
class="flex items-center gap-3 px-3.5 py-3 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] cursor-pointer text-left transition-[background,box-shadow,transform] duration-150 hover:bg-[var(--surface-hover,#f1f5f9)] hover:shadow-[0_2px_12px_rgba(0,0,0,0.06)] hover:-translate-y-px active:translate-y-0"
|
||||
@click="copyInviteMessage"
|
||||
>
|
||||
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-emerald-500/10 text-emerald-600">
|
||||
<i class="pi pi-comment" />
|
||||
</div>
|
||||
<div class="text-left min-w-0">
|
||||
<div class="font-semibold text-sm text-[var(--text-color)]">Copiar mensagem pronta</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)]">Texto formatado com o link incluso</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-[1rem] text-[var(--text-color)]">Copiar mensagem pronta</div>
|
||||
<div class="text-[0.75rem] text-[var(--text-color-secondary)]">Texto formatado com o link incluso</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -146,84 +158,66 @@
|
||||
</div>
|
||||
|
||||
<!-- Mensagem pronta -->
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-4">
|
||||
<div class="font-semibold text-[var(--text-color)] flex items-center gap-2 mb-1">
|
||||
<i class="pi pi-comment text-sm text-[var(--text-color-secondary)]" />
|
||||
<i class="pi pi-comment text-[1rem] text-[var(--text-color-secondary)]" />
|
||||
Mensagem pronta para envio
|
||||
</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mb-3">Copie e cole ao enviar o link ao paciente:</div>
|
||||
<div class="rounded-xl bg-[var(--surface-ground)] border border-[var(--surface-border)] p-4 text-sm text-[var(--text-color)] leading-relaxed">
|
||||
<div class="text-[1rem] text-[var(--text-color-secondary)] mb-3">Copie e cole ao enviar o link ao paciente:</div>
|
||||
<div class="rounded-md bg-[var(--surface-ground,#f8fafc)] border border-[var(--surface-border,#e2e8f0)] p-4 text-[1rem] text-[var(--text-color)] leading-relaxed">
|
||||
Olá! Segue o link para seu pré-cadastro. Preencha com calma — campos opcionais podem ficar em branco:
|
||||
<span class="block mt-2 font-mono text-xs break-all text-[var(--text-color-secondary)]">{{ publicUrl || '…aguardando link…' }}</span>
|
||||
<span class="block mt-2 font-mono text-[0.72rem] break-all text-[var(--text-color-secondary)]">{{ publicUrl || '…aguardando link…' }}</span>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Button
|
||||
icon="pi pi-copy"
|
||||
label="Copiar mensagem"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full"
|
||||
:disabled="!publicUrl"
|
||||
@click="copyInviteMessage"
|
||||
/>
|
||||
<Button icon="pi pi-copy" label="Copiar mensagem" severity="secondary" outlined class="rounded-full" :disabled="!publicUrl" @click="copyInviteMessage" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Direita: instruções -->
|
||||
<div class="lg:w-80 shrink-0 flex flex-col gap-4">
|
||||
<!-- ── DIREITA: instruções ────────────────────────── -->
|
||||
<div class="w-full lg:w-[272px] lg:flex-shrink-0 flex flex-col gap-3">
|
||||
|
||||
<!-- Como funciona -->
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<div class="p-5 border-b border-[var(--surface-border)]">
|
||||
<div class="font-semibold text-[var(--text-color)]">Como funciona</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Simples e sem fricção para o paciente</div>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<ol class="space-y-4">
|
||||
<li class="flex gap-3">
|
||||
<div class="extlink-step shrink-0">1</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-sm text-[var(--text-color)]">Você envia o link</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Por WhatsApp, e-mail ou mensagem direta.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex gap-3">
|
||||
<div class="extlink-step shrink-0">2</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-sm text-[var(--text-color)]">O paciente preenche</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Campos opcionais podem ficar em branco. Menos fricção, mais adesão.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex gap-3">
|
||||
<div class="extlink-step shrink-0">3</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-sm text-[var(--text-color)]">Você recebe e converte</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">O cadastro aparece em "Cadastros recebidos". Revise e converta em paciente quando quiser.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
|
||||
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2.5 border-b border-[var(--surface-border,#f1f5f9)]">
|
||||
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-indigo-500/10 text-indigo-500">
|
||||
<i class="pi pi-list-check text-[0.9rem]" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-[1rem] font-bold text-[var(--text-color)]">Como funciona</span>
|
||||
<span class="block text-[0.72rem] text-[var(--text-color-secondary)]">Simples e sem fricção para o paciente</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol class="flex flex-col divide-y divide-[var(--surface-border,#f1f5f9)]">
|
||||
<li v-for="step in howItWorks" :key="step.n" class="flex items-start gap-3 px-3.5 py-3">
|
||||
<div class="grid place-items-center w-7 h-7 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500 text-[0.75rem] font-bold mt-px">
|
||||
{{ step.n }}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-[1rem] text-[var(--text-color)]">{{ step.title }}</div>
|
||||
<div class="text-[0.75rem] text-[var(--text-color-secondary)] mt-0.5 leading-relaxed">{{ step.desc }}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- Boas práticas -->
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
|
||||
<div class="font-semibold text-[var(--text-color)] flex items-center gap-2 mb-3">
|
||||
<i class="pi pi-shield text-sm text-[var(--text-color-secondary)]" />
|
||||
Boas práticas
|
||||
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
|
||||
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2.5 border-b border-[var(--surface-border,#f1f5f9)]">
|
||||
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-emerald-500/10 text-emerald-600">
|
||||
<i class="pi pi-shield text-[0.9rem]" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-[1rem] font-bold text-[var(--text-color)]">Boas práticas</span>
|
||||
<span class="block text-[0.72rem] text-[var(--text-color-secondary)]">Segurança e privacidade</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="space-y-2.5">
|
||||
<li class="flex gap-2 text-sm text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-check text-emerald-500 mt-0.5 shrink-0" />
|
||||
<span>Gere um novo link se suspeitar que ele foi repassado indevidamente.</span>
|
||||
</li>
|
||||
<li class="flex gap-2 text-sm text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-check text-emerald-500 mt-0.5 shrink-0" />
|
||||
<span>Informe o paciente que campos opcionais podem ficar em branco.</span>
|
||||
</li>
|
||||
<li class="flex gap-2 text-sm text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-check text-emerald-500 mt-0.5 shrink-0" />
|
||||
<span>Evite divulgar em público; é um link para compartilhamento individual.</span>
|
||||
|
||||
<ul class="flex flex-col divide-y divide-[var(--surface-border,#f1f5f9)]">
|
||||
<li v-for="tip in goodPractices" :key="tip" class="flex items-start gap-2.5 px-3.5 py-2.5">
|
||||
<i class="pi pi-check text-emerald-500 mt-0.5 flex-shrink-0 text-[1rem]" />
|
||||
<span class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">{{ tip }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -241,26 +235,39 @@ import { supabase } from '@/lib/supabase/client'
|
||||
const toast = useToast()
|
||||
|
||||
const inviteToken = ref('')
|
||||
const rotating = ref(false)
|
||||
const rotating = ref(false)
|
||||
|
||||
// ── Hero sticky ────────────────────────────────────────────
|
||||
const headerEl = ref(null)
|
||||
// ── Hero sticky ───────────────────────────────────────────
|
||||
const headerEl = ref(null)
|
||||
const headerSentinelRef = ref(null)
|
||||
const headerStuck = ref(false)
|
||||
const headerStuck = ref(false)
|
||||
let _observer = null
|
||||
|
||||
// ── Mobile menu ────────────────────────────────────────────
|
||||
// ── Mobile menu ───────────────────────────────────────────
|
||||
const mobileMenuRef = ref(null)
|
||||
|
||||
const mobileMenuItems = computed(() => [
|
||||
{ label: 'Copiar link', icon: 'pi pi-copy', command: () => copyLink(), disabled: !inviteToken.value },
|
||||
{ label: 'Copiar mensagem', icon: 'pi pi-comment', command: () => copyInviteMessage(), disabled: !inviteToken.value },
|
||||
{ label: 'Abrir no navegador', icon: 'pi pi-external-link', command: () => openLink(), disabled: !inviteToken.value },
|
||||
{ label: 'Copiar link', icon: 'pi pi-copy', command: () => copyLink(), disabled: !inviteToken.value },
|
||||
{ label: 'Copiar mensagem', icon: 'pi pi-comment', command: () => copyInviteMessage(), disabled: !inviteToken.value },
|
||||
{ label: 'Abrir no navegador', icon: 'pi pi-external-link', command: () => openLink(), disabled: !inviteToken.value },
|
||||
{ separator: true },
|
||||
{ label: 'Gerar novo link', icon: 'pi pi-refresh', command: () => rotateLink() }
|
||||
{ label: 'Gerar novo link', icon: 'pi pi-refresh', command: () => rotateLink() }
|
||||
])
|
||||
|
||||
// ── URL base ────────────────────────────────────────────────
|
||||
// ── Conteúdo estático ─────────────────────────────────────
|
||||
const howItWorks = [
|
||||
{ n: 1, title: 'Você envia o link', desc: 'Por WhatsApp, e-mail ou mensagem direta.' },
|
||||
{ n: 2, title: 'O paciente preenche', desc: 'Campos opcionais podem ficar em branco. Menos fricção, mais adesão.' },
|
||||
{ n: 3, title: 'Você recebe e converte', desc: 'O cadastro aparece em "Cadastros recebidos". Revise e converta em paciente quando quiser.' },
|
||||
]
|
||||
|
||||
const goodPractices = [
|
||||
'Gere um novo link se suspeitar que ele foi repassado indevidamente.',
|
||||
'Informe o paciente que campos opcionais podem ficar em branco.',
|
||||
'Evite divulgar em público; é um link para compartilhamento individual.',
|
||||
]
|
||||
|
||||
// ── URL base ──────────────────────────────────────────────
|
||||
const PUBLIC_BASE_URL = ''
|
||||
|
||||
const origin = computed(() => {
|
||||
@@ -273,13 +280,13 @@ const publicUrl = computed(() => {
|
||||
return `${origin.value}/cadastro/paciente?t=${encodeURIComponent(inviteToken.value)}`
|
||||
})
|
||||
|
||||
// ── Token helpers ───────────────────────────────────────────
|
||||
function newToken() {
|
||||
// ── Token helpers ─────────────────────────────────────────
|
||||
function newToken () {
|
||||
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID()
|
||||
return 'tok_' + Math.random().toString(36).slice(2) + Date.now().toString(36)
|
||||
}
|
||||
|
||||
async function requireUserId() {
|
||||
async function requireUserId () {
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
const uid = data?.user?.id
|
||||
@@ -287,7 +294,7 @@ async function requireUserId() {
|
||||
return uid
|
||||
}
|
||||
|
||||
async function loadOrCreateInvite() {
|
||||
async function loadOrCreateInvite () {
|
||||
const uid = await requireUserId()
|
||||
|
||||
const { data, error } = await supabase
|
||||
@@ -301,10 +308,7 @@ async function loadOrCreateInvite() {
|
||||
if (error) throw error
|
||||
|
||||
const token = data?.[0]?.token
|
||||
if (token) {
|
||||
inviteToken.value = token
|
||||
return
|
||||
}
|
||||
if (token) { inviteToken.value = token; return }
|
||||
|
||||
const t = newToken()
|
||||
const { error: insErr } = await supabase
|
||||
@@ -315,19 +319,18 @@ async function loadOrCreateInvite() {
|
||||
inviteToken.value = t
|
||||
}
|
||||
|
||||
async function rotateLink() {
|
||||
async function rotateLink () {
|
||||
rotating.value = true
|
||||
try {
|
||||
const uid = await requireUserId()
|
||||
const t = newToken()
|
||||
const t = newToken()
|
||||
|
||||
const rpc = await supabase.rpc('rotate_patient_invite_token', { p_new_token: t })
|
||||
if (rpc.error) {
|
||||
const { error: e1 } = await supabase
|
||||
.from('patient_invites')
|
||||
.update({ active: false, updated_at: new Date().toISOString() })
|
||||
.eq('owner_id', uid)
|
||||
.eq('active', true)
|
||||
.eq('owner_id', uid).eq('active', true)
|
||||
if (e1) throw e1
|
||||
|
||||
const { error: e2 } = await supabase
|
||||
@@ -345,7 +348,7 @@ async function rotateLink() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
async function copyLink () {
|
||||
try {
|
||||
if (!publicUrl.value) return
|
||||
await navigator.clipboard.writeText(publicUrl.value)
|
||||
@@ -355,12 +358,12 @@ async function copyLink() {
|
||||
}
|
||||
}
|
||||
|
||||
function openLink() {
|
||||
function openLink () {
|
||||
if (!publicUrl.value) return
|
||||
window.open(publicUrl.value, '_blank', 'noopener')
|
||||
}
|
||||
|
||||
async function copyInviteMessage() {
|
||||
async function copyInviteMessage () {
|
||||
try {
|
||||
if (!publicUrl.value) return
|
||||
const msg = `Olá! Segue o link para seu pré-cadastro. Preencha com calma — campos opcionais podem ficar em branco:\n${publicUrl.value}`
|
||||
@@ -387,96 +390,4 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => { _observer?.disconnect() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ── Sentinel ─────────────────────────────────────── */
|
||||
.extlink-sentinel { height: 1px; }
|
||||
|
||||
/* ── Hero ─────────────────────────────────────────── */
|
||||
.extlink-hero {
|
||||
position: sticky;
|
||||
top: var(--layout-sticky-top, 56px);
|
||||
z-index: 20;
|
||||
overflow: hidden;
|
||||
border-radius: 1.75rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-card);
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
.extlink-hero--stuck {
|
||||
margin-left: 0; margin-right: 0;
|
||||
border-top-left-radius: 0; border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Blobs decorativos */
|
||||
.extlink-hero__blobs { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
|
||||
.extlink-hero__blob { position: absolute; border-radius: 50%; filter: blur(70px); }
|
||||
.extlink-hero__blob--1 { width: 18rem; height: 18rem; top: -4rem; right: -3rem; background: rgba(99,102,241,0.10); }
|
||||
.extlink-hero__blob--2 { width: 20rem; height: 20rem; top: 0.5rem; left: -5rem; background: rgba(16,185,129,0.08); }
|
||||
|
||||
/* Linha 1 */
|
||||
.extlink-hero__row1 {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
}
|
||||
.extlink-hero__brand {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
flex: 1; min-width: 0;
|
||||
}
|
||||
.extlink-hero__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2.5rem; height: 2.5rem; border-radius: 0.875rem; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
}
|
||||
.extlink-hero__title { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.02em; color: var(--text-color); }
|
||||
.extlink-hero__sub { font-size: 0.78rem; color: var(--text-color-secondary); margin-top: 2px; }
|
||||
|
||||
/* Linha 2 */
|
||||
.extlink-hero__row2 {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.extlink-hero__divider,
|
||||
.extlink-hero__row2 { display: none; }
|
||||
}
|
||||
|
||||
/* ── CTA button ───────────────────────────────────── */
|
||||
.extlink-cta-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.875rem;
|
||||
padding: 0.875rem 1rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
|
||||
text-align: left;
|
||||
}
|
||||
.extlink-cta-btn:hover {
|
||||
background: var(--surface-hover);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.extlink-cta-btn:active { transform: translateY(0); }
|
||||
.extlink-cta-btn__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2.25rem; height: 2.25rem;
|
||||
border-radius: 0.75rem; flex-shrink: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* ── Step numbers ─────────────────────────────────── */
|
||||
.extlink-step {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem;
|
||||
border-radius: 0.625rem;
|
||||
font-size: 0.8rem; font-weight: 700;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
Reference in New Issue
Block a user