diff --git a/frontends/nextjs/src/components/examples/contact-form/FormConfig.ts b/frontends/nextjs/src/components/examples/contact-form/FormConfig.ts new file mode 100644 index 000000000..6fb66cf33 --- /dev/null +++ b/frontends/nextjs/src/components/examples/contact-form/FormConfig.ts @@ -0,0 +1,60 @@ +export type ContactFormFieldType = 'text' | 'email' | 'textarea' + +export interface ContactFormField { + name: 'name' | 'email' | 'message' + label: string + placeholder: string + type: ContactFormFieldType + required?: boolean + helperText?: string +} + +export interface ContactFormConfig { + title: string + description: string + submitLabel: string + successTitle: string + successMessage: string + fields: ContactFormField[] +} + +export const contactFormConfig: ContactFormConfig = { + title: 'Contact form', + description: 'Collect a name, email, and short message with simple validation.', + submitLabel: 'Send message', + successTitle: 'Message sent', + successMessage: 'Thanks for reaching out. We will get back to you shortly.', + fields: [ + { + name: 'name', + label: 'Name', + placeholder: 'Your name', + type: 'text', + required: true, + }, + { + name: 'email', + label: 'Email', + placeholder: 'you@example.com', + type: 'email', + required: true, + helperText: 'We will only use this to reply to your note.', + }, + { + name: 'message', + label: 'Message', + placeholder: 'How can we help?', + type: 'textarea', + required: true, + }, + ], +} + +export type ContactFormState = Record + +export function createInitialContactFormState(): ContactFormState { + return contactFormConfig.fields.reduce((state, field) => { + state[field.name] = '' + return state + }, {} as ContactFormState) +} diff --git a/frontends/nextjs/src/components/examples/contact-form/Preview.tsx b/frontends/nextjs/src/components/examples/contact-form/Preview.tsx new file mode 100644 index 000000000..b097be962 --- /dev/null +++ b/frontends/nextjs/src/components/examples/contact-form/Preview.tsx @@ -0,0 +1,145 @@ +import { ChangeEvent, FormEvent, useMemo, useState } from 'react' +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, + Textarea, +} from '@/components/ui' + +import { + contactFormConfig, + ContactFormField, + ContactFormState, + createInitialContactFormState, +} from './FormConfig' + +type ValidationErrors = Partial> + +function validateContactForm(values: ContactFormState): ValidationErrors { + const errors: ValidationErrors = {} + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + + contactFormConfig.fields.forEach(field => { + const value = values[field.name]?.trim() ?? '' + + if (field.required && !value) { + errors[field.name] = `${field.label} is required` + return + } + + if (field.type === 'email' && value && !emailPattern.test(value)) { + errors[field.name] = 'Enter a valid email address' + } + }) + + return errors +} + +export function ContactFormPreview() { + const [formValues, setFormValues] = useState( + createInitialContactFormState() + ) + const [errors, setErrors] = useState({}) + const [submitted, setSubmitted] = useState(false) + + const hasErrors = useMemo(() => Object.keys(errors).length > 0, [errors]) + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + + const validationErrors = validateContactForm(formValues) + + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors) + setSubmitted(false) + return + } + + setErrors({}) + setSubmitted(true) + setFormValues(createInitialContactFormState()) + setTimeout(() => setSubmitted(false), 3200) + } + + const renderField = (field: ContactFormField) => { + const commonProps = { + id: field.name, + name: field.name, + value: formValues[field.name], + onChange: (event: ChangeEvent) => { + const { value } = event.target + setFormValues(current => ({ ...current, [field.name]: value })) + }, + 'aria-describedby': errors[field.name] ? `${field.name}-error` : undefined, + placeholder: field.placeholder, + } + + if (field.type === 'textarea') { + return ( +