172 lines
5.5 KiB
TypeScript
172 lines
5.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { useLocale } from '@/lib/i18n/locale-context';
|
|
import { SectionHeader } from '@/components/ui/SectionHeader';
|
|
import { SERVICE_IDS } from '@/lib/i18n/dictionaries';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
type Status = 'idle' | 'sending' | 'sent' | 'error';
|
|
|
|
export function Contact() {
|
|
const { t, locale } = useLocale();
|
|
const [status, setStatus] = useState<Status>('idle');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
setStatus('sending');
|
|
setError(null);
|
|
const form = e.currentTarget;
|
|
const data = Object.fromEntries(new FormData(form).entries());
|
|
try {
|
|
const res = await fetch('/api/contact', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ ...data, locale }),
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw new Error(body?.error ?? `HTTP ${res.status}`);
|
|
}
|
|
setStatus('sent');
|
|
form.reset();
|
|
} catch (err) {
|
|
setStatus('error');
|
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section id="contact" className="relative px-5 py-28 sm:px-8">
|
|
<div className="mx-auto max-w-5xl">
|
|
<SectionHeader
|
|
align="center"
|
|
eyebrow={t.contact.eyebrow}
|
|
title={t.contact.title}
|
|
sub={t.contact.sub}
|
|
/>
|
|
|
|
<motion.form
|
|
onSubmit={onSubmit}
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: '-60px' }}
|
|
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
|
|
className="glass mt-14 grid grid-cols-1 gap-5 p-7 sm:grid-cols-2 sm:p-9"
|
|
noValidate
|
|
>
|
|
<Field label={t.contact.fields.name} htmlFor="name">
|
|
<input
|
|
id="name"
|
|
name="name"
|
|
type="text"
|
|
required
|
|
placeholder={t.contact.placeholders.name}
|
|
className={inputCls}
|
|
/>
|
|
</Field>
|
|
|
|
<Field label={t.contact.fields.company} htmlFor="company">
|
|
<input
|
|
id="company"
|
|
name="company"
|
|
type="text"
|
|
placeholder={t.contact.placeholders.company}
|
|
className={inputCls}
|
|
/>
|
|
</Field>
|
|
|
|
<Field label={t.contact.fields.service} htmlFor="service">
|
|
<select id="service" name="service" defaultValue="" className={inputCls} required>
|
|
<option value="" disabled>
|
|
—
|
|
</option>
|
|
{t.services.items.map((s, i) => (
|
|
<option key={SERVICE_IDS[i]} value={SERVICE_IDS[i]}>
|
|
{s.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
|
|
<Field label={t.contact.fields.budget} htmlFor="budget">
|
|
<select id="budget" name="budget" defaultValue="" className={inputCls} required>
|
|
<option value="" disabled>
|
|
—
|
|
</option>
|
|
{t.contact.budgets.map((b) => (
|
|
<option key={b} value={b}>
|
|
{b}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
|
|
<Field
|
|
label={t.contact.fields.message}
|
|
htmlFor="message"
|
|
className="sm:col-span-2"
|
|
>
|
|
<textarea
|
|
id="message"
|
|
name="message"
|
|
rows={5}
|
|
required
|
|
placeholder={t.contact.placeholders.message}
|
|
className={cn(inputCls, 'resize-y')}
|
|
/>
|
|
</Field>
|
|
|
|
<div className="sm:col-span-2 flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="font-mono text-[0.72rem] uppercase tracking-wider text-slate-500">
|
|
{t.contact.note}
|
|
</p>
|
|
<button
|
|
type="submit"
|
|
disabled={status === 'sending'}
|
|
className="btn-primary disabled:cursor-not-allowed disabled:opacity-60"
|
|
>
|
|
{status === 'sending' ? '…' : t.contact.submit}
|
|
</button>
|
|
</div>
|
|
|
|
{status === 'sent' && (
|
|
<p className="sm:col-span-2 rounded-lg border border-emerald/30 bg-emerald/5 px-4 py-3 text-sm text-emerald">
|
|
✓ {locale === 'fa' ? 'پیام شما ارسال شد.' : 'Your message was sent.'}
|
|
</p>
|
|
)}
|
|
{status === 'error' && (
|
|
<p className="sm:col-span-2 rounded-lg border border-magenta/30 bg-magenta/5 px-4 py-3 text-sm text-magenta">
|
|
{locale === 'fa' ? 'خطا در ارسال:' : 'Send failed:'} {error}
|
|
</p>
|
|
)}
|
|
</motion.form>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const inputCls =
|
|
'w-full rounded-xl border border-white/10 bg-base-800/60 px-4 py-3 text-sm text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-electric/60 focus:bg-base-800';
|
|
|
|
function Field({
|
|
label,
|
|
htmlFor,
|
|
className,
|
|
children,
|
|
}: {
|
|
label: string;
|
|
htmlFor: string;
|
|
className?: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<label htmlFor={htmlFor} className={cn('flex flex-col gap-2', className)}>
|
|
<span className="label-mono">{label}</span>
|
|
{children}
|
|
</label>
|
|
);
|
|
}
|