mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Merge pull request #250 from johndoe6345789/codex/add-contact-form-example-components
Add contact form example config and preview
This commit is contained in:
@@ -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<ContactFormField['name'], string>
|
||||
|
||||
export function createInitialContactFormState(): ContactFormState {
|
||||
return contactFormConfig.fields.reduce<ContactFormState>((state, field) => {
|
||||
state[field.name] = ''
|
||||
return state
|
||||
}, {} as ContactFormState)
|
||||
}
|
||||
@@ -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<Record<ContactFormField['name'], string>>
|
||||
|
||||
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<ContactFormState>(
|
||||
createInitialContactFormState()
|
||||
)
|
||||
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
const hasErrors = useMemo(() => Object.keys(errors).length > 0, [errors])
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
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<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<Textarea
|
||||
rows={4}
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
type={field.type}
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="max-w-xl">
|
||||
<CardHeader>
|
||||
<CardTitle>{contactFormConfig.title}</CardTitle>
|
||||
<CardDescription>{contactFormConfig.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
{contactFormConfig.fields.map(field => (
|
||||
<div key={field.name} className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label className="text-sm font-medium" htmlFor={field.name}>
|
||||
{field.label}
|
||||
{field.required && <span className="text-red-600">*</span>}
|
||||
</label>
|
||||
{field.helperText && (
|
||||
<span className="text-xs text-muted-foreground">{field.helperText}</span>
|
||||
)}
|
||||
</div>
|
||||
{renderField(field)}
|
||||
{errors[field.name] && (
|
||||
<p
|
||||
id={`${field.name}-error`}
|
||||
className="text-xs text-red-600"
|
||||
role="alert"
|
||||
>
|
||||
{errors[field.name]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button className="w-full" type="submit">
|
||||
{contactFormConfig.submitLabel}
|
||||
</Button>
|
||||
{submitted && !hasErrors && (
|
||||
<p className="rounded-md bg-emerald-50 px-3 py-2 text-sm text-emerald-700">
|
||||
<strong className="mr-1">{contactFormConfig.successTitle}.</strong>
|
||||
{contactFormConfig.successMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user