275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState } from 'react'
|
||
import { CheckCircle2, AlertCircle, ArrowRight, RotateCcw } from 'lucide-react'
|
||
|
||
type FormState = 'idle' | 'loading' | 'success' | 'error'
|
||
|
||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||
const ACCENT = '#E61919'
|
||
const ACCENT_DARK = '#B8140F'
|
||
const ACCENT_SOFT = '#FDEEEC'
|
||
|
||
export default function ContactForm() {
|
||
const [state, setState] = useState<FormState>('idle')
|
||
const [errorMessage, setErrorMessage] = useState<string>('')
|
||
const [form, setForm] = useState({ name: '', org: '', email: '', phone: '', _hp: '' })
|
||
const [touched, setTouched] = useState<{ [key: string]: boolean }>({})
|
||
const [fieldErrors, setFieldErrors] = useState<{ [key: string]: string }>({})
|
||
|
||
const validate = () => {
|
||
const errors: { [key: string]: string } = {}
|
||
if (!form.name.trim() || form.name.trim().length < 2) {
|
||
errors.name = 'Lütfen ad ve soyadınızı belirtin (en az 2 karakter).'
|
||
}
|
||
if (!form.email.trim() || !EMAIL_REGEX.test(form.email.trim())) {
|
||
errors.email = 'Lütfen geçerli bir e-posta adresi girin.'
|
||
}
|
||
setFieldErrors(errors)
|
||
return Object.keys(errors).length === 0
|
||
}
|
||
|
||
const handleBlur = (field: string) => () => {
|
||
setTouched(prev => ({ ...prev, [field]: true }))
|
||
validate()
|
||
}
|
||
|
||
const set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const value = e.target.value
|
||
setForm(p => ({ ...p, [k]: value }))
|
||
if (fieldErrors[k]) {
|
||
setFieldErrors(prev => ({ ...prev, [k]: '' }))
|
||
}
|
||
}
|
||
|
||
const resetForm = () => {
|
||
setForm({ name: '', org: '', email: '', phone: '', _hp: '' })
|
||
setTouched({})
|
||
setFieldErrors({})
|
||
setState('idle')
|
||
setErrorMessage('')
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setTouched({ name: true, org: true, email: true, phone: true })
|
||
if (form._hp) return
|
||
|
||
if (!validate()) {
|
||
return
|
||
}
|
||
|
||
setState('loading')
|
||
setErrorMessage('')
|
||
|
||
try {
|
||
const res = await fetch('/api/contact', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: form.name.trim(),
|
||
email: form.email.trim(),
|
||
org: form.org.trim(),
|
||
phone: form.phone.trim(),
|
||
_hp: form._hp,
|
||
}),
|
||
})
|
||
|
||
const data = await res.json().catch(() => ({}))
|
||
|
||
if (res.ok && data.success) {
|
||
setState('success')
|
||
} else {
|
||
setState('error')
|
||
setErrorMessage(data.error || 'Talep iletilirken bir sorun oluştu. Lütfen tekrar deneyin.')
|
||
}
|
||
} catch {
|
||
setState('error')
|
||
setErrorMessage('Bağlantı hatası oluştu. Lütfen internet bağlantınızı kontrol edip tekrar deneyin.')
|
||
}
|
||
}
|
||
|
||
if (state === 'success') {
|
||
return (
|
||
<div
|
||
role="status"
|
||
aria-live="polite"
|
||
className="rounded-3xl border border-zinc-200/80 p-8 sm:p-12 text-center shadow-[0_20px_60px_-30px_rgba(24,24,27,0.15)] bg-white"
|
||
>
|
||
<div
|
||
className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-full text-white shadow-sm"
|
||
style={{ background: ACCENT }}
|
||
>
|
||
<CheckCircle2 size={26} strokeWidth={2.5} aria-hidden="true" />
|
||
</div>
|
||
<h3 className="text-2xl font-extrabold tracking-tight text-zinc-950">
|
||
Demo Talebiniz Alındı
|
||
</h3>
|
||
<p className="mt-3 max-w-[44ch] mx-auto text-sm leading-relaxed text-zinc-600">
|
||
Kurucu ekibimiz 24 saat içinde sizinle iletişime geçerek 15 dakikalık birebir canlı demo oturumunuzu planlayacaktır.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={resetForm}
|
||
className="mt-7 inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-5 py-2.5 text-sm font-semibold text-zinc-800 transition-colors hover:bg-zinc-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950"
|
||
>
|
||
<RotateCcw size={14} aria-hidden="true" /> Yeni Talep Gönder
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const baseInput =
|
||
'w-full min-h-[46px] rounded-xl border bg-white px-4 py-2.5 text-sm text-zinc-900 placeholder:text-zinc-400 outline-none transition-all'
|
||
|
||
return (
|
||
<form
|
||
onSubmit={handleSubmit}
|
||
className="grid grid-cols-1 gap-5 rounded-3xl border border-zinc-200/80 bg-white p-6 sm:p-10 shadow-[0_20px_60px_-30px_rgba(24,24,27,0.2)] sm:grid-cols-2"
|
||
noValidate
|
||
>
|
||
<div aria-hidden="true" className="hidden">
|
||
<input
|
||
type="text"
|
||
name="_hp"
|
||
value={form._hp}
|
||
onChange={set('_hp')}
|
||
autoComplete="off"
|
||
tabIndex={-1}
|
||
/>
|
||
</div>
|
||
|
||
<div className="sm:col-span-1">
|
||
<label className="mb-1.5 block text-xs font-bold text-zinc-900" htmlFor="af_name">
|
||
Ad Soyad <span style={{ color: ACCENT }}>*</span>
|
||
</label>
|
||
<input
|
||
id="af_name"
|
||
type="text"
|
||
required
|
||
autoComplete="name"
|
||
placeholder="Av. Ayşe Yılmaz"
|
||
value={form.name}
|
||
onChange={set('name')}
|
||
onBlur={handleBlur('name')}
|
||
aria-invalid={!!fieldErrors.name}
|
||
aria-describedby={fieldErrors.name ? 'af_name_error' : undefined}
|
||
className={`${baseInput} ${
|
||
fieldErrors.name && touched.name
|
||
? 'border-red-500 focus-visible:border-red-600 focus-visible:ring-2 focus-visible:ring-red-500/20'
|
||
: 'border-zinc-200 hover:border-zinc-300 focus-visible:border-[#E61919] focus-visible:ring-2 focus-visible:ring-[#E61919]/20'
|
||
}`}
|
||
/>
|
||
{fieldErrors.name && touched.name && (
|
||
<p id="af_name_error" className="mt-1.5 text-xs font-semibold text-red-600 flex items-center gap-1">
|
||
<AlertCircle size={12} aria-hidden="true" /> {fieldErrors.name}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-xs font-bold text-zinc-900" htmlFor="af_org">
|
||
Büro / Kurum
|
||
</label>
|
||
<input
|
||
id="af_org"
|
||
type="text"
|
||
autoComplete="organization"
|
||
placeholder="Yılmaz Hukuk Bürosu"
|
||
value={form.org}
|
||
onChange={set('org')}
|
||
className={`${baseInput} border-zinc-200 hover:border-zinc-300 focus-visible:border-[#E61919] focus-visible:ring-2 focus-visible:ring-[#E61919]/20`}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-xs font-bold text-zinc-900" htmlFor="af_email">
|
||
E-posta <span style={{ color: ACCENT }}>*</span>
|
||
</label>
|
||
<input
|
||
id="af_email"
|
||
type="email"
|
||
required
|
||
autoComplete="email"
|
||
placeholder="avukat@buro.com"
|
||
value={form.email}
|
||
onChange={set('email')}
|
||
onBlur={handleBlur('email')}
|
||
aria-invalid={!!fieldErrors.email}
|
||
aria-describedby={fieldErrors.email ? 'af_email_error' : undefined}
|
||
className={`${baseInput} ${
|
||
fieldErrors.email && touched.email
|
||
? 'border-red-500 focus-visible:border-red-600 focus-visible:ring-2 focus-visible:ring-red-500/20'
|
||
: 'border-zinc-200 hover:border-zinc-300 focus-visible:border-[#E61919] focus-visible:ring-2 focus-visible:ring-[#E61919]/20'
|
||
}`}
|
||
/>
|
||
{fieldErrors.email && touched.email && (
|
||
<p id="af_email_error" className="mt-1.5 text-xs font-semibold text-red-600 flex items-center gap-1">
|
||
<AlertCircle size={12} aria-hidden="true" /> {fieldErrors.email}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1.5 block text-xs font-bold text-zinc-900" htmlFor="af_phone">
|
||
Telefon <span className="text-zinc-500 font-normal">(opsiyonel)</span>
|
||
</label>
|
||
<input
|
||
id="af_phone"
|
||
type="tel"
|
||
autoComplete="tel"
|
||
placeholder="0532 000 00 00"
|
||
value={form.phone}
|
||
onChange={set('phone')}
|
||
className={`${baseInput} border-zinc-200 hover:border-zinc-300 focus-visible:border-[#E61919] focus-visible:ring-2 focus-visible:ring-[#E61919]/20`}
|
||
/>
|
||
</div>
|
||
|
||
{state === 'error' && (
|
||
<div
|
||
role="alert"
|
||
aria-live="assertive"
|
||
className="rounded-xl border border-red-200 bg-red-50/80 p-3.5 text-sm font-medium text-red-800 sm:col-span-2 flex items-start gap-2.5"
|
||
>
|
||
<AlertCircle size={17} className="text-red-600 shrink-0 mt-0.5" aria-hidden="true" />
|
||
<div>
|
||
<span>{errorMessage}</span>
|
||
<span className="block mt-1 text-xs text-red-700">
|
||
Alternatif olarak doğrudan{' '}
|
||
<a href="mailto:info@ayris.tech" className="underline font-semibold hover:text-red-900">
|
||
info@ayris.tech
|
||
</a>{' '}
|
||
adresine e-posta gönderebilirsiniz.
|
||
</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={state === 'loading'}
|
||
aria-busy={state === 'loading'}
|
||
className="mt-2 flex items-center justify-center gap-2 rounded-full py-4 text-sm font-bold text-white transition-all disabled:cursor-not-allowed disabled:opacity-60 sm:col-span-2 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-[#E61919]/40 active:scale-[0.99]"
|
||
style={{ background: ACCENT }}
|
||
onMouseEnter={e => {
|
||
if (state !== 'loading') (e.currentTarget as HTMLElement).style.background = ACCENT_DARK
|
||
}}
|
||
onMouseLeave={e => {
|
||
if (state !== 'loading') (e.currentTarget as HTMLElement).style.background = ACCENT
|
||
}}
|
||
>
|
||
{state === 'loading' ? (
|
||
<span className="inline-flex items-center gap-2">
|
||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||
Talebiniz İletiliyor…
|
||
</span>
|
||
) : (
|
||
<span className="inline-flex items-center gap-2">
|
||
15 Dk Canlı Demo Talep Edin <ArrowRight size={16} strokeWidth={2.5} aria-hidden="true" />
|
||
</span>
|
||
)}
|
||
</button>
|
||
</form>
|
||
)
|
||
}
|