feat: enhance landing page, Telegram webhook, SEO suite, and cookie consent banner

This commit is contained in:
ayrisdev
2026-08-22 22:57:03 +03:00
parent 5ac5988a3a
commit 61dd85133c
26 changed files with 3264 additions and 896 deletions
+201 -30
View File
@@ -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>
)
+97
View File
@@ -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>
)
}