first commit
ci / build (push) Failing after 23s
deploy / deploy (push) Failing after 10m12s

This commit is contained in:
soroush.asadi
2026-05-31 12:47:02 +03:30
commit add78d8460
100 changed files with 15221 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
'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>
);
}