Files
supabase/apps/www/components/Forms/TalkToPartnershipTeamForm.tsx
Danny White 1131e3e2ce fix(ui): default Button variant to default instead of primary (#50160)
## What kind of change does this PR introduce?

Bug fix / design-system alignment for the legacy `Button` from `ui`.

## What is the current behavior?

Omitting `variant` on the legacy `Button` falls back to brand-green
`primary`. That makes accidental greens easy, and it is hard to spot the
real main action on busy pages.

## What is the new behavior?

- Legacy `Button` now defaults to neutral `default`
- Intentional primary CTAs (create, save, submit, marketing CTAs, and
matching `ButtonTooltip` usages) now set `variant="primary"` so their
appearance is unchanged
- Neutral actions that previously relied on the old fallback (cancel,
close, back, dashboard nav, and similar) become grey/white
- Design-system docs updated; regression tests cover the new default

`Button_Shadcn_` is unchanged. It already uses its own CVA default.

This is PR 1 of 2 in a stack. PR 2 drops now-redundant
`variant="default"` props.

## To test

Studio (http://localhost:8082):

- `/sign-in`: Sign in stays green
- Open a project → Database → Tables: New table stays green
- Auth → Users → Invite: Invite user stays green; Cancel / dismiss
controls stay neutral
- Project Settings → General: edit a field so Cancel and Save appear.
Cancel is neutral, Save is green

Design system (http://localhost:3003):

- Components → Button: default demo is neutral; primary demo is green;
featured preview is the default variant

Marketing (optional):

- www header: Start your project stays green; logged-in Dashboard is
neutral

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Style**
- Buttons now default to a neutral style, while primary actions across
Studio, documentation, marketing pages, forms, dialogs, and error states
use prominent primary styling.
- Updated button examples and previews clarify the distinction between
default and primary variants.
  - Event registration now includes a directional arrow icon.

- **Tests**
- Added coverage confirming default button styling and explicit primary
styling behave as expected.
- Updated related test fixtures to use primary styling where
appropriate.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-10 11:23:17 +10:00

283 lines
8.1 KiB
TypeScript

import { useSendTelemetryEvent } from '~/lib/telemetry'
import { CircleAlert } from 'lucide-react'
import Link from 'next/link'
import { FC, useEffect, useState } from 'react'
import { Alert, Button, cn, Input, Label, Separator, TextArea } from 'ui'
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
variant="primary"
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