219 lines
6.7 KiB
TypeScript
219 lines
6.7 KiB
TypeScript
'use client'
|
||
|
||
import React, { useMemo, useState } from 'react'
|
||
import { useForm } from 'react-hook-form'
|
||
import { z } from 'zod'
|
||
import { zodResolver } from '@hookform/resolvers/zod'
|
||
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Label } from '@/components/ui/label'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { useContent } from '@/components/layout/content-provider'
|
||
|
||
import { submitContact } from '@/app/contact/actions'
|
||
import type { ContactFormData } from '@/app/contact/actions'
|
||
|
||
export function ContactForm() {
|
||
const content = useContent()
|
||
const formContent = content.forms.contact
|
||
const schema = useMemo(
|
||
() =>
|
||
z.object({
|
||
name: z.string().min(2, { message: formContent.validation.nameMin }),
|
||
phone: z
|
||
.string()
|
||
.min(10, { message: formContent.validation.phoneValid })
|
||
.regex(/^[\d\s۰-۹]+$/, { message: formContent.validation.phoneNumbers }),
|
||
email: z.string().email({ message: formContent.validation.emailValid }).optional().or(z.literal('')),
|
||
subject: z.string().optional(),
|
||
message: z.string().max(1000).optional(),
|
||
}),
|
||
[formContent.validation]
|
||
)
|
||
type FormValues = z.infer<typeof schema>
|
||
|
||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||
|
||
const {
|
||
register,
|
||
handleSubmit,
|
||
formState: { errors },
|
||
setValue,
|
||
watch,
|
||
reset,
|
||
} = useForm<FormValues>({
|
||
resolver: zodResolver(schema),
|
||
defaultValues: {
|
||
name: '',
|
||
phone: '',
|
||
email: '',
|
||
subject: '',
|
||
message: '',
|
||
},
|
||
})
|
||
|
||
const subjectValue = watch('subject')
|
||
|
||
const onSubmit = async (data: FormValues) => {
|
||
setStatus('loading')
|
||
setErrorMessage(null)
|
||
const subject = data.subject?.trim() && data.subject !== '__empty__' ? data.subject : undefined
|
||
const payload: ContactFormData = {
|
||
name: data.name.trim(),
|
||
phone: data.phone.replace(/\s/g, ''),
|
||
email: data.email?.trim() || undefined,
|
||
subject,
|
||
message: data.message?.trim() || undefined,
|
||
}
|
||
const result = await submitContact(payload)
|
||
if (result.success) {
|
||
setStatus('success')
|
||
reset()
|
||
} else {
|
||
setStatus('error')
|
||
setErrorMessage(result.error ?? formContent.errorDefault)
|
||
}
|
||
}
|
||
|
||
if (status === 'success') {
|
||
return (
|
||
<div
|
||
className="rounded-xl border border-primary/20 bg-primary/5 p-8 text-center"
|
||
role="alert"
|
||
>
|
||
<h3 className="text-heading-3 font-semibold text-foreground">
|
||
{formContent.success.title}
|
||
</h3>
|
||
<p className="mt-2 text-body text-muted-foreground">
|
||
{formContent.success.description}
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<form
|
||
onSubmit={handleSubmit(onSubmit)}
|
||
className="mx-auto max-w-xl space-y-6 rounded-xl border bg-card p-6 shadow-sm md:p-8"
|
||
noValidate
|
||
>
|
||
<div className="grid gap-2 md:grid-cols-2">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="contact-name" className="text-body-sm font-medium">
|
||
{formContent.labels.name} <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
id="contact-name"
|
||
type="text"
|
||
placeholder={formContent.placeholders.name}
|
||
className="w-full"
|
||
autoComplete="name"
|
||
aria-invalid={!!errors.name}
|
||
{...register('name')}
|
||
/>
|
||
{errors.name && (
|
||
<p className="text-caption text-destructive">{errors.name.message}</p>
|
||
)}
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="contact-phone" className="text-body-sm font-medium">
|
||
{formContent.labels.phone} <span className="text-destructive">*</span>
|
||
</Label>
|
||
<Input
|
||
id="contact-phone"
|
||
type="tel"
|
||
placeholder={formContent.placeholders.phone}
|
||
className="w-full"
|
||
autoComplete="tel"
|
||
aria-invalid={!!errors.phone}
|
||
{...register('phone')}
|
||
/>
|
||
{errors.phone && (
|
||
<p className="text-caption text-destructive">{errors.phone.message}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="contact-email" className="text-body-sm font-medium">
|
||
{formContent.labels.email}
|
||
</Label>
|
||
<Input
|
||
id="contact-email"
|
||
type="email"
|
||
placeholder={formContent.placeholders.email}
|
||
className="w-full"
|
||
autoComplete="email"
|
||
aria-invalid={!!errors.email}
|
||
{...register('email')}
|
||
/>
|
||
{errors.email && (
|
||
<p className="text-caption text-destructive">{errors.email.message}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="text-body-sm font-medium">{formContent.labels.subject}</Label>
|
||
<Select
|
||
value={subjectValue || '__empty__'}
|
||
onValueChange={(v) => setValue('subject', v === '__empty__' ? '' : v)}
|
||
>
|
||
<SelectTrigger className="w-full">
|
||
<SelectValue placeholder={formContent.placeholders.subject} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="__empty__">{formContent.placeholders.subject}</SelectItem>
|
||
{formContent.subjectOptions.map((opt) => (
|
||
<SelectItem key={opt.value} value={opt.value}>
|
||
{opt.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="contact-message" className="text-body-sm font-medium">
|
||
{formContent.labels.message}
|
||
</Label>
|
||
<Textarea
|
||
id="contact-message"
|
||
placeholder={formContent.placeholders.message}
|
||
rows={4}
|
||
className="resize-none"
|
||
aria-invalid={!!errors.message}
|
||
{...register('message')}
|
||
/>
|
||
{errors.message && (
|
||
<p className="text-caption text-destructive">{errors.message.message}</p>
|
||
)}
|
||
</div>
|
||
|
||
{status === 'error' && errorMessage && (
|
||
<p className="rounded-md bg-destructive/10 p-3 text-body-sm text-destructive">
|
||
{errorMessage}
|
||
</p>
|
||
)}
|
||
|
||
<Button
|
||
type="submit"
|
||
size="lg"
|
||
variant="primary"
|
||
className="w-full"
|
||
disabled={status === 'loading'}
|
||
>
|
||
{status === 'loading' ? formContent.labels.submitting : formContent.labels.submit}
|
||
</Button>
|
||
</form>
|
||
)
|
||
}
|