Seção 'Receba as novidades da campanha' acima do rodapé: nome + WhatsApp obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado 'Quero fazer parte do time' gravado como subscription.campaignLevel (time|esporadico). CTA secundário do hero vira o atalho (#novidades, scroll suave). Contact + Subscription em withPayloadTransaction com consentimento novo fail-closed por chave estável 'campanha-novidades' — nada é gravado sem o texto aprovado no admin. Migration 20260819_192605 (state de contact nullable + coluna campaign_level). Impl plan, changelog, int + e2e.
181 lines
7.1 KiB
TypeScript
181 lines
7.1 KiB
TypeScript
'use client'
|
|
|
|
import { submitCampaignNewsletter } from '@/app/(frontend)/actions/submitCampaignNewsletter'
|
|
import { CitySelect } from '@/components/CitySelect'
|
|
import { EmailInput } from '@/components/EmailInput'
|
|
import { NameInput } from '@/components/NameInput'
|
|
import { PhoneInput } from '@/components/PhoneInput'
|
|
import { StateSelect } from '@/components/StateSelect'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Checkbox } from '@/components/ui/Checkbox'
|
|
import { Field, FieldDescription, FieldError, FieldGroup } from '@/components/ui/field'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { CampaignNewsletterInput, campaignNewsletterSchema } from '@/lib/schemas/campaignNewsletter'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { CheckIcon } from 'lucide-react'
|
|
import { useState, useTransition } from 'react'
|
|
import { FormProvider, SubmitHandler, useForm, useWatch } from 'react-hook-form'
|
|
|
|
interface CampaignNewsletterFormProps {
|
|
onSubmit: () => void
|
|
}
|
|
|
|
/**
|
|
* S9 — public capture of campaign "novidades" on the home: name + WhatsApp
|
|
* required, email/state/city/comment optional, engagement-level toggle
|
|
* pre-selected ("Quero fazer parte do time"). Success swaps the form for an
|
|
* in-place confirmation (the seam where S10 will fire `Lead`).
|
|
*/
|
|
const CampaignNewsletterForm = ({ onSubmit }: CampaignNewsletterFormProps) => {
|
|
const methods = useForm<CampaignNewsletterInput>({
|
|
resolver: zodResolver(campaignNewsletterSchema),
|
|
defaultValues: {
|
|
name: '',
|
|
phone: '',
|
|
email: '',
|
|
state: undefined,
|
|
city: '',
|
|
comment: '',
|
|
campaignLevel: 'time',
|
|
},
|
|
})
|
|
|
|
const [isSubmitting, startTransition] = useTransition()
|
|
|
|
const handleSubmit: SubmitHandler<CampaignNewsletterInput> = (input) => {
|
|
startTransition(async () => {
|
|
try {
|
|
await submitCampaignNewsletter(input)
|
|
methods.reset()
|
|
onSubmit()
|
|
} catch (error) {
|
|
console.log(error)
|
|
methods.setError('root', {
|
|
message: 'Falha ao enviar. Tente novamente.',
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const campaignLevel = useWatch({ control: methods.control, name: 'campaignLevel' })
|
|
|
|
return (
|
|
<FormProvider {...methods}>
|
|
<form
|
|
id="campanha-novidades"
|
|
onSubmit={methods.handleSubmit(handleSubmit)}
|
|
className="mx-auto max-w-3xl"
|
|
>
|
|
<FieldGroup>
|
|
<Field>
|
|
<NameInput placeholder="Nome completo*" />
|
|
<FieldError errors={[methods.formState.errors.name]} />
|
|
</Field>
|
|
<Field>
|
|
<PhoneInput placeholder="WhatsApp (com DDD)*" />
|
|
<FieldError errors={[methods.formState.errors.phone]} />
|
|
</Field>
|
|
<Field>
|
|
<EmailInput required={false} placeholder="E-mail (opcional)" />
|
|
</Field>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Field>
|
|
<StateSelect required={false} placeholder="Estado (opcional)" />
|
|
</Field>
|
|
<Field>
|
|
<CitySelect required={false} placeholder="Cidade (opcional)" />
|
|
</Field>
|
|
</div>
|
|
<Field>
|
|
<Textarea
|
|
id="comment"
|
|
{...methods.register('comment')}
|
|
placeholder="Comentário (opcional)"
|
|
className="min-h-22"
|
|
/>
|
|
</Field>
|
|
<Field>
|
|
<div className="rounded-lg border border-(--campaign-line) bg-white p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Checkbox
|
|
id="campaign-level"
|
|
checked={campaignLevel === 'time'}
|
|
onCheckedChange={(checked) =>
|
|
methods.setValue('campaignLevel', checked ? 'time' : 'esporadico')
|
|
}
|
|
aria-describedby="campaign-level-help"
|
|
className="mt-0.5 size-5 rounded border-(--pt-red) text-white data-[state=checked]:border-(--pt-red) data-[state=checked]:bg-(--pt-red)"
|
|
/>
|
|
<label
|
|
htmlFor="campaign-level"
|
|
className="cursor-pointer text-sm leading-snug text-(--campaign-ink)"
|
|
>
|
|
<strong>Quero fazer parte do time:</strong> receber novidades com frequência, ser
|
|
adicionado(a) aos grupos de WhatsApp da campanha e participar das ações.
|
|
</label>
|
|
</div>
|
|
<p
|
|
id="campaign-level-help"
|
|
className="m-0 mt-2 text-xs leading-snug text-(--campaign-muted)"
|
|
>
|
|
Sem marcar, você recebe apenas comunicações esporádicas da campanha — sem grupos e
|
|
sem envios frequentes.
|
|
</p>
|
|
</div>
|
|
</Field>
|
|
<Field>
|
|
{methods.formState.errors.root?.message ? (
|
|
<FieldDescription className="text-destructive">
|
|
{methods.formState.errors.root.message}
|
|
</FieldDescription>
|
|
) : null}
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="h-12 w-full rounded-lg bg-(--pt-red) text-sm font-extrabold text-white hover:brightness-95"
|
|
>
|
|
{isSubmitting ? 'ENVIANDO...' : 'QUERO RECEBER NOVIDADES'}
|
|
</Button>
|
|
</Field>
|
|
</FieldGroup>
|
|
</form>
|
|
<p className="mx-auto mt-4 max-w-xl text-center text-xs leading-relaxed text-(--campaign-muted)">
|
|
Ao enviar, você demonstra apoio à campanha e escolhe como quer acompanhar: comunicações
|
|
frequentes e grupos de WhatsApp (marcando a opção acima) ou comunicações esporádicas —
|
|
conforme a Política de Privacidade (LGPD).
|
|
</p>
|
|
</FormProvider>
|
|
)
|
|
}
|
|
|
|
/** S9 — in-place success shown after the capture is recorded. */
|
|
const CampaignNewsletterSuccess = () => (
|
|
<div className="mx-auto max-w-3xl rounded-xl border border-(--campaign-line) bg-white px-6 py-12 text-center">
|
|
<div className="mx-auto flex size-10 items-center justify-center rounded-full border-2 border-(--pt-red) text-(--pt-red)">
|
|
<CheckIcon className="size-5" />
|
|
</div>
|
|
<h3 className="mt-5 font-[family-name:var(--font-exo2)] text-2xl font-black tracking-[-0.02em] text-(--campaign-ink)">
|
|
Inscrição confirmada
|
|
</h3>
|
|
<p className="mx-auto mt-4 max-w-md text-sm leading-relaxed text-(--campaign-muted)">
|
|
Você vai receber as novidades da campanha. Enquanto isso, conheça as bandeiras da campanha.
|
|
</p>
|
|
<a
|
|
href="#bandeiras"
|
|
className="mt-6 inline-flex h-11 items-center justify-center rounded-lg border-2 border-(--campaign-line) bg-white px-6 text-sm font-extrabold text-(--pt-red) no-underline transition-colors hover:bg-(--campaign-band) focus-visible:ring-2 focus-visible:ring-(--pt-red) focus-visible:ring-offset-2 focus-visible:outline-none"
|
|
>
|
|
Ver bandeiras
|
|
</a>
|
|
</div>
|
|
)
|
|
|
|
/** S9 — wrapper owning the form ↔ success swap on the home section. */
|
|
export const CampaignNewsletterCapture = () => {
|
|
const [submitted, setSubmitted] = useState(false)
|
|
|
|
return submitted ? (
|
|
<CampaignNewsletterSuccess />
|
|
) : (
|
|
<CampaignNewsletterForm onSubmit={() => setSubmitted(true)} />
|
|
)
|
|
}
|