feat: enhance landing page, Telegram webhook, SEO suite, and cookie consent banner
This commit is contained in:
+201
-30
@@ -1,102 +1,273 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle2 } from 'lucide-react'
|
||||
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 set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setForm(p => ({ ...p, [k]: e.target.value }))
|
||||
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, email: form.email, barNo: form.org, message: form.phone }),
|
||||
body: JSON.stringify({
|
||||
name: form.name.trim(),
|
||||
email: form.email.trim(),
|
||||
org: form.org.trim(),
|
||||
phone: form.phone.trim(),
|
||||
_hp: form._hp,
|
||||
}),
|
||||
})
|
||||
setState(res.ok ? 'success' : 'error')
|
||||
|
||||
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 className="rounded-3xl border border-zinc-200/70 p-10 text-center" style={{ background: ACCENT_SOFT }}>
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full text-white" style={{ background: ACCENT }}>
|
||||
<CheckCircle2 size={22} strokeWidth={2.5} />
|
||||
<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-xl font-extrabold tracking-tight text-zinc-950">Talebiniz alındı!</h3>
|
||||
<p className="mt-2 text-sm text-zinc-500">Kurucu ekipten kısa süre içinde size dönüş yapılacak.</p>
|
||||
<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 inputClass = 'w-full min-h-[46px] rounded-xl border-[1.5px] border-zinc-200 bg-white px-4 py-2.5 text-sm text-zinc-900 outline-none transition-colors focus:border-[var(--accent)]'
|
||||
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-4 rounded-3xl border border-zinc-200/70 bg-white p-8 shadow-[0_20px_60px_-30px_rgba(24,24,27,0.2)] sm:grid-cols-2"
|
||||
style={{ ['--accent' as string]: ACCENT }}
|
||||
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} />
|
||||
<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-[13px] font-semibold text-zinc-900" htmlFor="af_name">
|
||||
<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 placeholder="Av. Ayşe Yılmaz" value={form.name} onChange={set('name')} className={inputClass} />
|
||||
<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-[13px] font-semibold text-zinc-900" htmlFor="af_org">Büro / Kurum</label>
|
||||
<input id="af_org" type="text" placeholder="Yılmaz Hukuk Bürosu" value={form.org} onChange={set('org')} className={inputClass} />
|
||||
<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-[13px] font-semibold text-zinc-900" htmlFor="af_email">
|
||||
<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 placeholder="ayse@buro.com" value={form.email} onChange={set('email')} className={inputClass} />
|
||||
<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-[13px] font-semibold text-zinc-900" htmlFor="af_phone">Telefon</label>
|
||||
<input id="af_phone" type="tel" placeholder="0532 000 00 00" value={form.phone} onChange={set('phone')} className={inputClass} />
|
||||
<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 className="text-[13px] text-red-600 sm:col-span-2">
|
||||
Bir hata oluştu. Lütfen tekrar deneyin veya{' '}
|
||||
<a href="mailto:info@ayris.tech" className="underline">info@ayris.tech</a> adresine yazın.
|
||||
<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'}
|
||||
className="mt-1 flex items-center justify-center gap-2 rounded-full py-3.5 text-[15px] font-bold text-white transition-colors disabled:cursor-not-allowed disabled:opacity-70 sm:col-span-2"
|
||||
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 }}
|
||||
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' ? 'Gönderiliyor…' : 'Demo Talep Et →'}
|
||||
{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>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Cookie, X, ShieldCheck } from 'lucide-react'
|
||||
|
||||
export default function CookieBanner() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [visible, setVisible] = useState(false)
|
||||
const params = useParams()
|
||||
const locale = (params?.locale as string) || 'tr'
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
const consent = localStorage.getItem('ayris_cookie_consent')
|
||||
if (!consent) {
|
||||
// Sayfa yüklendikten kısa bir süre sonra pürüzsüzce aç
|
||||
const timer = setTimeout(() => {
|
||||
setVisible(true)
|
||||
}, 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
localStorage.setItem('ayris_cookie_consent', 'accepted_all')
|
||||
setVisible(false)
|
||||
}
|
||||
|
||||
const handleAcceptEssential = () => {
|
||||
localStorage.setItem('ayris_cookie_consent', 'essential_only')
|
||||
setVisible(false)
|
||||
}
|
||||
|
||||
if (!mounted || !visible) return null
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label="Çerez ve Gizlilik Bildirimi"
|
||||
role="region"
|
||||
className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-[540px] animate-in fade-in slide-in-from-bottom-5 duration-300 sm:bottom-6 sm:right-6 sm:left-auto"
|
||||
>
|
||||
<div className="rounded-2xl border border-zinc-200/90 bg-white/95 p-5 shadow-[0_20px_50px_-15px_rgba(24,24,27,0.2)] backdrop-blur-md">
|
||||
<div className="flex items-start gap-3.5">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#FDEEEC] text-[#B8140F]">
|
||||
<Cookie size={18} strokeWidth={2.2} aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-bold text-zinc-950 flex items-center gap-1.5">
|
||||
Çerez ve Veri Politikası
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAcceptEssential}
|
||||
aria-label="Bildirimi Kapat"
|
||||
className="rounded-lg p-1 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700 transition-colors"
|
||||
>
|
||||
<X size={15} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mt-1.5 text-xs leading-relaxed text-zinc-600">
|
||||
Sitemizde gezinme deneyiminizi iyileştirmek, temel işlevleri sunmak ve güvenliği sağlamak amacıyla çerezler kullanılmaktadır. Ayrıntılı bilgi için{' '}
|
||||
<Link
|
||||
href={`/${locale}/privacy`}
|
||||
className="font-semibold text-zinc-900 underline underline-offset-2 hover:text-[#E61919]"
|
||||
>
|
||||
Gizlilik Politikası
|
||||
</Link>
|
||||
'nı inceleyebilirsiniz.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAcceptAll}
|
||||
className="flex-1 rounded-full bg-[#E61919] px-4 py-2 text-xs font-bold text-white shadow-sm transition-all hover:bg-[#B8140F] active:scale-[0.98] sm:flex-initial"
|
||||
>
|
||||
Tümünü Kabul Et
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAcceptEssential}
|
||||
className="flex-1 rounded-full border border-zinc-200 bg-white px-3.5 py-2 text-xs font-semibold text-zinc-700 transition-all hover:bg-zinc-50 hover:text-zinc-950 active:scale-[0.98] sm:flex-initial"
|
||||
>
|
||||
Yalnızca Zorunlular
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user