Files

219 lines
6.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}