Files
supabase/apps/www/components/Forms/TalkToPartnershipTeamForm.tsx
Gildas GarciaandIvan Vasilov 96d43099bb chore: refactor Button API so that it can be used a standard button (#46880)
## Problem

Our `<Button>` component breaks the default `button` contract by
redefining the `type` prop to set its variant (`primary`, `default`,
etc) instead of the button type (`submit`, `button`, etc).
This is confusing and forces to write more code when using it with
shadcn components that expect/inject the standard button props.

## Solution

- rename the `type` prop to `variant`
- rename the `htmlType` prop to `type`
- propagate the changes where necessary
- format code

## How to test

As this is just prop renaming, if it builds it's ok

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
2026-06-16 23:59:58 +02:00

283 lines
8.2 KiB
TypeScript

import { useSendTelemetryEvent } from '~/lib/telemetry'
import { CircleAlert } from 'lucide-react'
import Link from 'next/link'
import { FC, useEffect, useState } from 'react'
import { Button, cn, Input, Label, Separator, TextArea } from 'ui'
import { Alert } from 'ui/src/components/shadcn/ui/alert'
interface FormData {
firstName: string
secondName: string
companyEmail: string
}
interface FormItem {
type: 'text' | 'textarea'
label: string
placeholder: string
required: boolean
className?: string
component: typeof TextArea | typeof Input
}
type FormConfig = {
[K in keyof FormData]: FormItem
}
interface Props {
className?: string
}
const formConfig: FormConfig = {
firstName: {
type: 'text',
label: 'First Name',
placeholder: 'First Name',
required: true,
className: 'col-span-full',
component: Input,
},
secondName: {
type: 'text',
label: 'Last Name',
placeholder: 'Last Name',
required: true,
className: 'col-span-full',
component: Input,
},
companyEmail: {
type: 'text',
label: 'Company Email',
placeholder: 'Company Email',
required: true,
className: '',
component: Input,
},
}
const isValidEmail = (email: string): boolean => {
const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/
return emailPattern.test(email)
}
const personalEmailDomains = [
'@gmail.com',
'@yahoo.com',
'@hotmail.',
'@outlook.com',
'@aol.com',
'@icloud.com',
'@live.com',
'@protonmail.com',
'@mail.com',
'@example.com',
]
const isCompanyEmail = (email: string): boolean => {
for (const domain of personalEmailDomains) {
if (email.includes(domain)) {
return false
}
}
return true
}
const defaultFormValue: FormData = {
firstName: '',
secondName: '',
companyEmail: '',
}
const TalkToPartnershipTeamForm: FC<Props> = ({ className }) => {
const [formData, setFormData] = useState<FormData>(defaultFormValue)
const [honeypot, setHoneypot] = useState<string>('') // field to prevent spam
const [errors, setErrors] = useState<{ [key: string]: string }>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [success, setSuccess] = useState<string | null>(null)
const [startTime, setStartTime] = useState<number>(0)
const sendTelemetryEvent = useSendTelemetryEvent()
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setErrors({})
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleReset = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault()
setFormData(defaultFormValue)
setSuccess(null)
setErrors({})
}
const validate = (): boolean => {
const newErrors: { [key in keyof FormData]?: string } = {}
// Check required fields
for (const key in formConfig) {
if (formConfig[key as keyof FormData].required && !formData[key as keyof FormData]) {
newErrors[key as keyof FormData] = `This field is required`
}
}
// Validate email
if (formData.companyEmail && !isValidEmail(formData.companyEmail)) {
newErrors.companyEmail = 'Invalid email address'
}
// Validate company email
if (formData.companyEmail && !isCompanyEmail(formData.companyEmail)) {
newErrors.companyEmail = 'Please use a company email address'
}
setErrors(newErrors)
// Return validation status, also check if honeypot is filled (indicating a bot)
return Object.keys(newErrors).length === 0 && honeypot === ''
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const currentTime = Date.now()
const timeElapsed = (currentTime - startTime) / 1000
// Spam prevention: Reject form if submitted too quickly (less than 3 seconds)
if (timeElapsed < 3) {
setErrors({ general: 'Submission too fast. Please fill the form correctly.' })
return
}
if (!validate()) {
return
}
setIsSubmitting(true)
setSuccess(null)
try {
const response = await fetch('/api-v2/submit-form-talk-to-partnership', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
})
if (response.ok) {
setSuccess('Thank you for your submission!')
setFormData({ firstName: '', secondName: '', companyEmail: '' })
} else {
const errorData = await response.json()
setErrors({ general: `Submission failed: ${errorData.message}` })
}
} catch (error) {
setErrors({ general: 'An unexpected error occurred. Please try again.' })
} finally {
setIsSubmitting(false)
}
}
useEffect(() => {
setStartTime(Date.now())
}, [])
return (
<div
className={cn(
'flex flex-col gap-4 w-full items-center justify-center min-h-[300px]',
className
)}
>
<div className="border rounded-xl bg-surface-75 p-4 md:p-6 w-full lg:max-w-lg min-h-[200px] flex flex-col">
{success ? (
<div className="flex flex-col flex-1 w-full gap-4 items-center justify-center animate-fade-in">
<p className="text-center text-sm">{success}</p>
<Button onClick={handleReset}>Reset</Button>
</div>
) : (
<form
id="support-form"
className={cn('flex flex-col lg:grid lg:grid-cols-2 gap-4')}
onSubmit={handleSubmit}
>
{Object.entries(formConfig).map(([key, { component: Component, ...fieldValue }]) => {
const fieldKey = key as keyof FormData
return (
<div
key={key}
className={cn('flex flex-col col-span-full gap-y-2', fieldValue.className)}
>
<Label htmlFor={fieldKey} className="text-foreground-light flex justify-between">
{fieldValue.label}
<div
className={cn(
'flex flex-nowrap text-right gap-1 items-center text-xs leading-none transition-opacity opacity-0 text-foreground-muted',
errors[key as keyof FormData] && 'opacity-100 animate-fade-in'
)}
>
{errors[fieldKey]}
</div>
</Label>
<Component
type="text"
id={fieldKey}
name={fieldKey}
value={formData[fieldKey]}
onChange={handleChange}
placeholder={fieldValue.placeholder}
/>
</div>
)
})}
{/* Spam prevention */}
<input
type="text"
name="honeypot"
value={honeypot}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setHoneypot(e.target.value)}
style={{ display: 'none' }}
aria-hidden="true"
/>
<Separator className="col-span-full" />
<Button
block
type="submit"
size="small"
className="col-span-full"
disabled={isSubmitting}
loading={isSubmitting}
onClick={() =>
sendTelemetryEvent({
action: 'request_demo_button_clicked',
properties: { buttonLocation: 'Enterprise Request Demo Form' },
})
}
>
Submit
</Button>
<p className="text-foreground-lighter text-sm col-span-full">
By submitting this form, I confirm that I have read and understood the{' '}
<Link href="/privacy" className="text-foreground hover:underline">
Privacy Policy
</Link>
.
</p>
{errors.general && (
<Alert className="flex gap-2 text-foreground text-sm col-span-full">
<CircleAlert className="w-3 h-3" /> <span>{errors.general}</span>
</Alert>
)}
</form>
)}
</div>
</div>
)
}
export default TalkToPartnershipTeamForm