Template
82 lines
4.2 KiB
TypeScript
82 lines
4.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, type FormEvent } from 'react';
|
|
import { ArrowRight, Mail, MapPin } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
export type ContactFormProps = {
|
|
heading?: string;
|
|
description?: string;
|
|
email?: string;
|
|
location?: string;
|
|
nameLabel?: string;
|
|
emailLabel?: string;
|
|
companyLabel?: string;
|
|
messageLabel?: string;
|
|
submitLabel?: string;
|
|
successMessage?: string;
|
|
};
|
|
|
|
export default function ContactForm({
|
|
heading = '', description = '', email = '', location = '', nameLabel = 'Name', emailLabel = 'Email', companyLabel = 'Company', messageLabel = 'Message', submitLabel = 'Send message', successMessage = 'Thanks — we will be in touch soon.',
|
|
}: ContactFormProps) {
|
|
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
|
const [error, setError] = useState('');
|
|
|
|
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
setStatus('submitting');
|
|
setError('');
|
|
const form = event.currentTarget;
|
|
const body = Object.fromEntries(new FormData(form).entries());
|
|
|
|
try {
|
|
const response = await fetch('/api/contact', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
});
|
|
const result = await response.json() as { error?: string };
|
|
if (!response.ok) throw new Error(result.error || 'We could not send your message.');
|
|
form.reset();
|
|
setStatus('success');
|
|
} catch (cause) {
|
|
setError(cause instanceof Error ? cause.message : 'We could not send your message.');
|
|
setStatus('error');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<main className="bg-background py-16 sm:py-24">
|
|
<div className="section-shell grid gap-12 lg:grid-cols-[.8fr_1.2fr] lg:gap-20">
|
|
<div>
|
|
<h1 className="text-5xl font-normal leading-[0.96] tracking-[-0.05em] sm:text-6xl lg:text-7xl">{heading}</h1>
|
|
{description && <p className="mt-6 max-w-md text-lg leading-8 text-muted-foreground">{description}</p>}
|
|
<div className="mt-10 space-y-4 text-sm">
|
|
{email && <a href={`mailto:${email}`} className="flex items-center gap-3 hover:text-brand"><Mail className="size-4 text-muted-foreground" aria-hidden="true" />{email}</a>}
|
|
{location && <p className="flex items-center gap-3"><MapPin className="size-4 text-muted-foreground" aria-hidden="true" />{location}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="border-t pt-8">
|
|
<div className="grid gap-5 sm:grid-cols-2">
|
|
<Field label={nameLabel} htmlFor="contact-name"><Input id="contact-name" name="name" autoComplete="name" required /></Field>
|
|
<Field label={emailLabel} htmlFor="contact-email"><Input id="contact-email" name="email" type="email" autoComplete="email" required /></Field>
|
|
<Field label={companyLabel} htmlFor="contact-company" className="sm:col-span-2"><Input id="contact-company" name="company" autoComplete="organization" /></Field>
|
|
<Field label={messageLabel} htmlFor="contact-message" className="sm:col-span-2"><Textarea id="contact-message" name="message" rows={6} required /></Field>
|
|
</div>
|
|
<div className="sr-only" aria-hidden="true"><label htmlFor="contact-website">Website</label><input id="contact-website" name="website" tabIndex={-1} autoComplete="off" /></div>
|
|
<div className="mt-6 flex flex-col items-start gap-4 sm:flex-row sm:items-center">
|
|
<Button type="submit" size="lg" disabled={status === 'submitting'}>{status === 'submitting' ? 'Sending…' : submitLabel}<ArrowRight aria-hidden="true" /></Button>
|
|
<p aria-live="polite" className={`text-sm ${status === 'error' ? 'text-destructive' : 'text-muted-foreground'}`}>{status === 'success' ? successMessage : status === 'error' ? error : ''}</p>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function Field({ label, htmlFor, className = '', children }: { label: string; htmlFor: string; className?: string; children: React.ReactNode }) {
|
|
return <div className={className}><label htmlFor={htmlFor} className="mb-2 block text-sm font-medium">{label}</label>{children}</div>;
|
|
}
|