- GroupRequestForm: full form with group type/size/date, POST /api/leads - BirthdayBookingForm: birthday booking with package preselect from ?package=, POST /api/leads - Leads collection: added message, groupSize, preferredDate, packageSlug fields - GoogleAnalytics + BinotelWidget: Script-based, loaded from SiteSettings global - layout.tsx: generateMetadata from SiteSettings, GA4 + Binotel wired - Footer: full render — logo, nav links, contacts, socials from CMS - LocationsSlider: BtnGradient href fixed to /kvytky?category=slug - utm.ts: added getUtmParams() client helper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
210 lines
6.6 KiB
TypeScript
210 lines
6.6 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useTransition } from 'react'
|
||
import { getUtmParams } from '@/lib/utm'
|
||
|
||
const FONT = 'var(--font-montserrat, Montserrat), sans-serif'
|
||
|
||
const GROUP_TYPES = [
|
||
{ value: 'school', label: 'Шкільна екскурсія' },
|
||
{ value: 'kindergarten', label: 'Дитячий садок' },
|
||
{ value: 'corporate', label: 'Корпоратив' },
|
||
{ value: 'other', label: 'Інше' },
|
||
]
|
||
|
||
export function GroupRequestForm() {
|
||
const [name, setName] = useState('')
|
||
const [phone, setPhone] = useState('')
|
||
const [email, setEmail] = useState('')
|
||
const [groupSize, setGroupSize] = useState('')
|
||
const [preferredDate, setPreferredDate] = useState('')
|
||
const [groupType, setGroupType] = useState('')
|
||
const [message, setMessage] = useState('')
|
||
const [success, setSuccess] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [isPending, startTransition] = useTransition()
|
||
|
||
function handleSubmit(e: React.FormEvent) {
|
||
e.preventDefault()
|
||
setError(null)
|
||
|
||
startTransition(async () => {
|
||
try {
|
||
const utm = getUtmParams()
|
||
const res = await fetch('/api/leads', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name,
|
||
phone,
|
||
email: email || undefined,
|
||
formSource: 'group-request',
|
||
groupSize: groupSize ? Number(groupSize) : undefined,
|
||
preferredDate: preferredDate || undefined,
|
||
message: [groupType ? `Тип групи: ${groupType}` : '', message].filter(Boolean).join('\n') || undefined,
|
||
...utm,
|
||
}),
|
||
})
|
||
|
||
const data = (await res.json()) as { ok?: boolean; error?: string }
|
||
if (!res.ok) {
|
||
setError(data.error ?? 'Щось пішло не так. Спробуйте ще раз.')
|
||
return
|
||
}
|
||
setSuccess(true)
|
||
} catch {
|
||
setError('Помилка мережі. Перевірте з\'єднання та спробуйте ще раз.')
|
||
}
|
||
})
|
||
}
|
||
|
||
if (success) {
|
||
return (
|
||
<div className="flex flex-col items-center gap-4 py-10 text-center">
|
||
<div className="text-[48px]">✅</div>
|
||
<h3 className="text-[24px] font-bold text-white" style={{ fontFamily: FONT }}>
|
||
Заявку отримано!
|
||
</h3>
|
||
<p className="text-white/70 text-[16px]" style={{ fontFamily: FONT }}>
|
||
Менеджер зателефонує вам протягом 30 хвилин для уточнення деталей.
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||
<Field label="Ваше ім'я *" htmlFor="grp-name">
|
||
<input
|
||
id="grp-name"
|
||
type="text"
|
||
required
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
placeholder="Іван Іванов"
|
||
className={INPUT_CLS}
|
||
style={{ fontFamily: FONT }}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Телефон *" htmlFor="grp-phone">
|
||
<input
|
||
id="grp-phone"
|
||
type="tel"
|
||
required
|
||
value={phone}
|
||
onChange={(e) => setPhone(e.target.value)}
|
||
placeholder="+38 (0__) ___-__-__"
|
||
className={INPUT_CLS}
|
||
style={{ fontFamily: FONT }}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Email" htmlFor="grp-email">
|
||
<input
|
||
id="grp-email"
|
||
type="email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder="your@email.com"
|
||
className={INPUT_CLS}
|
||
style={{ fontFamily: FONT }}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Кількість учасників *" htmlFor="grp-size">
|
||
<input
|
||
id="grp-size"
|
||
type="number"
|
||
required
|
||
min={10}
|
||
max={500}
|
||
value={groupSize}
|
||
onChange={(e) => setGroupSize(e.target.value)}
|
||
placeholder="30"
|
||
className={INPUT_CLS}
|
||
style={{ fontFamily: FONT }}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Бажана дата" htmlFor="grp-date">
|
||
<input
|
||
id="grp-date"
|
||
type="date"
|
||
value={preferredDate}
|
||
onChange={(e) => setPreferredDate(e.target.value)}
|
||
className={INPUT_CLS}
|
||
style={{ fontFamily: FONT }}
|
||
/>
|
||
</Field>
|
||
|
||
<Field label="Тип групи" htmlFor="grp-type">
|
||
<select
|
||
id="grp-type"
|
||
value={groupType}
|
||
onChange={(e) => setGroupType(e.target.value)}
|
||
className={INPUT_CLS}
|
||
style={{ fontFamily: FONT }}
|
||
>
|
||
<option value="">Оберіть тип</option>
|
||
{GROUP_TYPES.map((t) => (
|
||
<option key={t.value} value={t.label}>
|
||
{t.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</Field>
|
||
</div>
|
||
|
||
<Field label="Повідомлення" htmlFor="grp-msg">
|
||
<textarea
|
||
id="grp-msg"
|
||
rows={4}
|
||
value={message}
|
||
onChange={(e) => setMessage(e.target.value)}
|
||
placeholder="Додаткові побажання або запитання..."
|
||
className={INPUT_CLS + ' resize-none'}
|
||
style={{ fontFamily: FONT }}
|
||
/>
|
||
</Field>
|
||
|
||
{error && (
|
||
<div className="rounded-xl border border-red-400 bg-red-900/30 px-4 py-3 text-[14px] text-red-300">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isPending}
|
||
className="mt-2 inline-flex items-center justify-center rounded-[64px] bg-[#f28b4a] px-10 py-4 text-[16px] font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a] disabled:opacity-60"
|
||
style={{ fontFamily: FONT }}
|
||
>
|
||
{isPending ? 'Надсилаємо...' : 'Надіслати заявку'}
|
||
</button>
|
||
</form>
|
||
)
|
||
}
|
||
|
||
function Field({
|
||
label,
|
||
htmlFor,
|
||
children,
|
||
}: {
|
||
label: string
|
||
htmlFor: string
|
||
children: React.ReactNode
|
||
}) {
|
||
return (
|
||
<div className="flex flex-col gap-2">
|
||
<label htmlFor={htmlFor} className="text-[14px] font-medium text-white/80" style={{ fontFamily: FONT }}>
|
||
{label}
|
||
</label>
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const INPUT_CLS =
|
||
'w-full rounded-[12px] border-2 border-white/20 bg-white/10 px-4 py-3 text-[15px] text-white placeholder-white/40 focus:border-[#f28b4a] focus:outline-none'
|