Shumiland/src/components/forms/GroupRequestForm.tsx
Vadym Samoilenko 557bf5a1b5
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat: forms, analytics, footer, LocationsSlider fix
- 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>
2026-05-10 21:50:06 +01:00

210 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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'