mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 14:05:05 +08:00
feat(marketing): add reusable MarketingForm with multi-channel fan-out
Extract form rendering from FormSection into a standalone MarketingForm component exported from the marketing package. The form fans out submissions to HubSpot, Customer.io, and Notion in parallel via the existing submitFormAction. Replace the legacy HubSpotFormEmbed iframe on the AWS Activate page with the new form, and delete the old embed.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { MarketingForm } from 'marketing'
|
||||
import type { GoPageInput } from 'marketing'
|
||||
|
||||
import HubSpotFormEmbed from './components/HubSpotFormEmbed'
|
||||
|
||||
const page: GoPageInput = {
|
||||
template: 'lead-gen',
|
||||
slug: 'aws-activate-offer',
|
||||
@@ -110,8 +109,61 @@ const page: GoPageInput = {
|
||||
id: 'form',
|
||||
title: 'Apply for your credits',
|
||||
children: (
|
||||
<div className="mx-auto w-full max-w-2xl border border-muted rounded-2xl p-6 sm:p-8">
|
||||
<HubSpotFormEmbed portalId="19953346" formId="db2718f8-1f23-4fe1-aaab-b4924dc4ca54" />
|
||||
<div className="mx-auto w-full max-w-2xl">
|
||||
<MarketingForm
|
||||
fields={[
|
||||
{
|
||||
type: 'text',
|
||||
name: 'firstName',
|
||||
label: 'First name',
|
||||
placeholder: 'First name',
|
||||
required: true,
|
||||
half: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'lastName',
|
||||
label: 'Last name',
|
||||
placeholder: 'Last name',
|
||||
required: true,
|
||||
half: true,
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
name: 'email',
|
||||
label: 'Work email',
|
||||
placeholder: 'you@company.com',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
placeholder: 'Company name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'awsAccountId',
|
||||
label: 'AWS account ID',
|
||||
placeholder: '12-digit AWS account ID',
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
submitLabel="Apply for credits"
|
||||
successMessage="Thanks! We've received your application and will review it within 5 business days."
|
||||
disclaimer="By submitting this form, I confirm that I have read and understood the [Privacy Policy](https://supabase.com/privacy)."
|
||||
crm={{
|
||||
hubspot: {
|
||||
formGuid: 'db2718f8-1f23-4fe1-aaab-b4924dc4ca54',
|
||||
fieldMap: {
|
||||
firstName: 'firstname',
|
||||
lastName: 'lastname',
|
||||
awsAccountId: 'aws_account_id',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useId } from 'react'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
hbspt?: {
|
||||
forms: {
|
||||
create: (config: { portalId: string; formId: string; target: string }) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HUBSPOT_SCRIPT_SRC = 'https://js.hsforms.net/forms/embed/v2.js'
|
||||
|
||||
function loadHubSpotScript(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.hbspt) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const existing = document.querySelector<HTMLScriptElement>(
|
||||
`script[src="${HUBSPOT_SCRIPT_SRC}"]`
|
||||
)
|
||||
if (existing) {
|
||||
existing.addEventListener('load', () => resolve(), { once: true })
|
||||
existing.addEventListener(
|
||||
'error',
|
||||
() => reject(new Error('Failed to load HubSpot form script')),
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = HUBSPOT_SCRIPT_SRC
|
||||
script.async = true
|
||||
script.defer = true
|
||||
script.onload = () => resolve()
|
||||
script.onerror = () => reject(new Error('Failed to load HubSpot form script'))
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
export default function HubSpotFormEmbed({
|
||||
portalId,
|
||||
formId,
|
||||
}: {
|
||||
portalId: string
|
||||
formId: string
|
||||
}) {
|
||||
const targetId = `hubspot-form-${useId().replace(/:/g, '-')}`
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const mountForm = async () => {
|
||||
try {
|
||||
await loadHubSpotScript()
|
||||
if (cancelled || !window.hbspt) return
|
||||
|
||||
const target = document.getElementById(targetId)
|
||||
if (!target) return
|
||||
|
||||
// Reset target to avoid duplicate forms on remounts.
|
||||
while (target.firstChild) {
|
||||
target.removeChild(target.firstChild)
|
||||
}
|
||||
|
||||
window.hbspt.forms.create({
|
||||
portalId,
|
||||
formId,
|
||||
target: `#${targetId}`,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[go/hubspot] Failed to initialize HubSpot form embed', error)
|
||||
}
|
||||
}
|
||||
|
||||
mountForm()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [formId, portalId, targetId])
|
||||
|
||||
return <div id={targetId} />
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './src/go'
|
||||
export * from './src/forms'
|
||||
|
||||
282
packages/marketing/src/forms/MarketingForm.tsx
Normal file
282
packages/marketing/src/forms/MarketingForm.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import {
|
||||
Button,
|
||||
Input_Shadcn_,
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
TextArea_Shadcn_,
|
||||
} from 'ui'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { submitFormAction } from '../go/actions/submitForm'
|
||||
import { formCrmConfigSchema, formFieldSchema } from '../go/schemas'
|
||||
|
||||
/** Input-shape field type — fields with Zod defaults (`half`, `required`) are optional here. */
|
||||
export type MarketingFormField = z.input<typeof formFieldSchema>
|
||||
export type MarketingFormCrmConfig = z.input<typeof formCrmConfigSchema>
|
||||
|
||||
export interface MarketingFormProps {
|
||||
/** Form fields. The submit handler builds the payload from these by `name`. */
|
||||
fields: MarketingFormField[]
|
||||
/** Submit button label. */
|
||||
submitLabel: string
|
||||
/** Optional title shown above the form. */
|
||||
title?: string
|
||||
/** Optional description shown above the form. */
|
||||
description?: string
|
||||
/** Optional markdown disclaimer shown beneath the submit button. */
|
||||
disclaimer?: string
|
||||
/** Message shown after a successful submission. Ignored when `successRedirect` is set. */
|
||||
successMessage?: string
|
||||
/** URL to redirect the user to after a successful submission. Overrides `successMessage`. */
|
||||
successRedirect?: string
|
||||
/** CRM fan-out config — submits to HubSpot, Customer.io, and/or Notion in parallel. */
|
||||
crm?: MarketingFormCrmConfig
|
||||
/** Wraps the form in a styled card (border + padding). Defaults to `true`. */
|
||||
card?: boolean
|
||||
/** Extra class names applied to the outer wrapper. */
|
||||
className?: string
|
||||
}
|
||||
|
||||
type SubmitState = 'idle' | 'loading' | 'success' | 'error'
|
||||
|
||||
function FormField({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
field: MarketingFormField
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'email':
|
||||
return (
|
||||
<Input_Shadcn_
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextArea_Shadcn_
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
rows={field.rows}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
case 'select':
|
||||
return (
|
||||
<Select_Shadcn_ value={value} onValueChange={onChange} required={field.required}>
|
||||
<SelectTrigger_Shadcn_>
|
||||
<SelectValue_Shadcn_ placeholder={field.placeholder} />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{field.options.map((opt) => (
|
||||
<SelectItem_Shadcn_ key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
)
|
||||
default: {
|
||||
const _exhaustive: never = field
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function MarketingForm({
|
||||
fields,
|
||||
submitLabel,
|
||||
title,
|
||||
description,
|
||||
disclaimer,
|
||||
successMessage,
|
||||
successRedirect,
|
||||
crm,
|
||||
card = true,
|
||||
className,
|
||||
}: MarketingFormProps) {
|
||||
const [values, setValues] = useState<Record<string, string>>(() =>
|
||||
Object.fromEntries(fields.map((f) => [f.name, '']))
|
||||
)
|
||||
const [submitState, setSubmitState] = useState<SubmitState>('idle')
|
||||
const [errorMessages, setErrorMessages] = useState<string[]>([])
|
||||
|
||||
const handleChange = (name: string, value: string) => {
|
||||
setValues((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!crm) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[marketing/form] No CRM configured — form values:', values)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitState('loading')
|
||||
setErrorMessages([])
|
||||
|
||||
const pageUri = typeof window !== 'undefined' ? window.location.href : undefined
|
||||
const pageName = typeof document !== 'undefined' ? document.title : undefined
|
||||
|
||||
try {
|
||||
const result = await submitFormAction(crm, values, { pageUri, pageName })
|
||||
|
||||
if (result.success) {
|
||||
if (successRedirect) {
|
||||
window.location.href = successRedirect
|
||||
} else {
|
||||
setSubmitState('success')
|
||||
}
|
||||
} else {
|
||||
setSubmitState('error')
|
||||
setErrorMessages(result.errors)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[marketing/form] Form submission failed:', err)
|
||||
setSubmitState('error')
|
||||
setErrorMessages(['Something went wrong. Please try again.'])
|
||||
}
|
||||
}
|
||||
|
||||
// Group fields into rows: half-width fields pair up, full-width fields get their own row
|
||||
const rows: MarketingFormField[][] = []
|
||||
let pendingHalf: MarketingFormField | null = null
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.half) {
|
||||
if (pendingHalf) {
|
||||
rows.push([pendingHalf, field])
|
||||
pendingHalf = null
|
||||
} else {
|
||||
pendingHalf = field
|
||||
}
|
||||
} else {
|
||||
if (pendingHalf) {
|
||||
rows.push([pendingHalf])
|
||||
pendingHalf = null
|
||||
}
|
||||
rows.push([field])
|
||||
}
|
||||
}
|
||||
if (pendingHalf) {
|
||||
rows.push([pendingHalf])
|
||||
}
|
||||
|
||||
if (submitState === 'success') {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
className={
|
||||
card
|
||||
? 'border border-muted rounded-2xl p-6 sm:p-8 flex flex-col items-center gap-4 text-center'
|
||||
: 'flex flex-col items-center gap-4 text-center'
|
||||
}
|
||||
>
|
||||
<p className="text-lg font-medium">Thank you!</p>
|
||||
<p className="text-foreground-light">
|
||||
{successMessage ?? "We've received your submission and will be in touch soon."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{(title || description) && (
|
||||
<div className="flex flex-col items-center gap-4 text-center text-balance mb-10">
|
||||
{title && (
|
||||
<h2 className="text-2xl md:text-3xl lg:text-4xl leading-tight tracking-tight">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{description && <p className="text-foreground-light text-lg max-w-xl">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className={
|
||||
card
|
||||
? 'border border-muted rounded-2xl p-6 sm:p-8 flex flex-col gap-6'
|
||||
: 'flex flex-col gap-6'
|
||||
}
|
||||
>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className={row.length > 1 ? 'grid grid-cols-1 sm:grid-cols-2 gap-4' : undefined}
|
||||
>
|
||||
{row.map((field) => (
|
||||
<div key={field.name} className="flex flex-col gap-2">
|
||||
<label className="text-sm text-foreground font-medium">{field.label}</label>
|
||||
<FormField
|
||||
field={field}
|
||||
value={values[field.name] ?? ''}
|
||||
onChange={(v) => handleChange(field.name, v)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{submitState === 'error' && errorMessages.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{errorMessages.map((msg, i) => (
|
||||
<p key={i} className="text-sm text-destructive">
|
||||
{msg}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="border-muted" />
|
||||
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={submitState === 'loading'}
|
||||
>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
|
||||
{disclaimer && (
|
||||
<div className="text-xs text-foreground-lighter leading-relaxed [&_a]:text-brand-link [&_a]:decoration-brand-link">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
p: ({ children }) => <p>{children}</p>,
|
||||
a: ({ href, children }) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{disclaimer}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
6
packages/marketing/src/forms/index.ts
Normal file
6
packages/marketing/src/forms/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as MarketingForm } from './MarketingForm'
|
||||
export type {
|
||||
MarketingFormCrmConfig,
|
||||
MarketingFormField,
|
||||
MarketingFormProps,
|
||||
} from './MarketingForm'
|
||||
@@ -1,240 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import {
|
||||
Button,
|
||||
Input_Shadcn_,
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
TextArea_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
import { submitFormAction } from '../actions/submitForm'
|
||||
import type { GoFormField, GoFormSection } from '../schemas'
|
||||
|
||||
function FormField({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
field: GoFormField
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'email':
|
||||
return (
|
||||
<Input_Shadcn_
|
||||
type={field.type}
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextArea_Shadcn_
|
||||
placeholder={field.placeholder}
|
||||
required={field.required}
|
||||
rows={field.rows}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
case 'select':
|
||||
return (
|
||||
<Select_Shadcn_ value={value} onValueChange={onChange} required={field.required}>
|
||||
<SelectTrigger_Shadcn_>
|
||||
<SelectValue_Shadcn_ placeholder={field.placeholder} />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{field.options.map((opt) => (
|
||||
<SelectItem_Shadcn_ key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
)
|
||||
default: {
|
||||
const _exhaustive: never = field
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SubmitState = 'idle' | 'loading' | 'success' | 'error'
|
||||
import MarketingForm from '../../forms/MarketingForm'
|
||||
import type { GoFormSection } from '../schemas'
|
||||
|
||||
export default function FormSection({ section }: { section: GoFormSection }) {
|
||||
const [values, setValues] = useState<Record<string, string>>(() =>
|
||||
Object.fromEntries(section.fields.map((f) => [f.name, '']))
|
||||
)
|
||||
const [submitState, setSubmitState] = useState<SubmitState>('idle')
|
||||
const [errorMessages, setErrorMessages] = useState<string[]>([])
|
||||
|
||||
const handleChange = (name: string, value: string) => {
|
||||
setValues((prev) => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!section.crm) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[go/form] No CRM configured — form values:', values)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitState('loading')
|
||||
setErrorMessages([])
|
||||
|
||||
const pageUri = typeof window !== 'undefined' ? window.location.href : undefined
|
||||
const pageName = typeof document !== 'undefined' ? document.title : undefined
|
||||
|
||||
try {
|
||||
const result = await submitFormAction(section.crm, values, { pageUri, pageName })
|
||||
|
||||
if (result.success) {
|
||||
if (section.successRedirect) {
|
||||
window.location.href = section.successRedirect
|
||||
} else {
|
||||
setSubmitState('success')
|
||||
}
|
||||
} else {
|
||||
setSubmitState('error')
|
||||
setErrorMessages(result.errors)
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Unexpected client-side error (network failure, server action crash, etc.)
|
||||
console.error('[go/form] Form submission failed:', err)
|
||||
setSubmitState('error')
|
||||
setErrorMessages(['Something went wrong. Please try again.'])
|
||||
}
|
||||
}
|
||||
|
||||
// Group fields into rows: half-width fields pair up, full-width fields get their own row
|
||||
const rows: GoFormField[][] = []
|
||||
let pendingHalf: GoFormField | null = null
|
||||
|
||||
for (const field of section.fields) {
|
||||
if (field.half) {
|
||||
if (pendingHalf) {
|
||||
rows.push([pendingHalf, field])
|
||||
pendingHalf = null
|
||||
} else {
|
||||
pendingHalf = field
|
||||
}
|
||||
} else {
|
||||
if (pendingHalf) {
|
||||
rows.push([pendingHalf])
|
||||
pendingHalf = null
|
||||
}
|
||||
rows.push([field])
|
||||
}
|
||||
}
|
||||
if (pendingHalf) {
|
||||
rows.push([pendingHalf])
|
||||
}
|
||||
|
||||
if (submitState === 'success') {
|
||||
return (
|
||||
<section>
|
||||
<div className="max-w-2xl mx-auto px-8">
|
||||
<div className="border border-muted rounded-2xl p-6 sm:p-8 flex flex-col items-center gap-4 text-center">
|
||||
<p className="text-lg font-medium">Thank you!</p>
|
||||
<p className="text-foreground-light">
|
||||
{section.successMessage ??
|
||||
"We've received your submission and will be in touch soon."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="max-w-2xl mx-auto px-8">
|
||||
{(section.title || section.description) && (
|
||||
<div className="flex flex-col items-center gap-4 text-center text-balance mb-10">
|
||||
{section.title && (
|
||||
<h2 className="text-2xl md:text-3xl lg:text-4xl leading-tight tracking-tight">
|
||||
{section.title}
|
||||
</h2>
|
||||
)}
|
||||
{section.description && (
|
||||
<p className="text-foreground-light text-lg max-w-xl">{section.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="border border-muted rounded-2xl p-6 sm:p-8 flex flex-col gap-6"
|
||||
>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className={row.length > 1 ? 'grid grid-cols-1 sm:grid-cols-2 gap-4' : undefined}
|
||||
>
|
||||
{row.map((field) => (
|
||||
<div key={field.name} className="flex flex-col gap-2">
|
||||
<label className="text-sm text-foreground font-medium">{field.label}</label>
|
||||
<FormField
|
||||
field={field}
|
||||
value={values[field.name] ?? ''}
|
||||
onChange={(v) => handleChange(field.name, v)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{submitState === 'error' && errorMessages.length > 0 && (
|
||||
<div className="flex flex-col gap-1">
|
||||
{errorMessages.map((msg, i) => (
|
||||
<p key={i} className="text-sm text-destructive">
|
||||
{msg}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<hr className="border-muted" />
|
||||
|
||||
<Button
|
||||
htmlType="submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={submitState === 'loading'}
|
||||
>
|
||||
{section.submitLabel}
|
||||
</Button>
|
||||
|
||||
{section.disclaimer && (
|
||||
<div className="text-xs text-foreground-lighter leading-relaxed [&_a]:text-brand-link [&_a]:decoration-brand-link">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
p: ({ children }) => <p>{children}</p>,
|
||||
a: ({ href, children }) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{section.disclaimer}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
<MarketingForm
|
||||
fields={section.fields}
|
||||
submitLabel={section.submitLabel}
|
||||
title={section.title}
|
||||
description={section.description}
|
||||
disclaimer={section.disclaimer}
|
||||
successMessage={section.successMessage}
|
||||
successRedirect={section.successRedirect}
|
||||
crm={section.crm}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user