import React, { useState, useEffect, useRef, useMemo } from "react"; import { Crosshair, Zap, Users, Clock, CalendarDays, Phone, User, MapPin, Mail, Plus, Minus, Camera, ShieldCheck, ChevronDown, ChevronUp, X, Search, ArrowRight, Star, Trophy, Flame, Sparkles, CheckCircle2, ChevronLeft, ChevronRight, Menu, Calculator as CalcIcon, Target, PartyPopper, Gamepad2, Car, BadgeCheck, RotateCcw, Timer, MessageSquare, CreditCard, Flag, ZoomIn, ZoomOut, } from "lucide-react"; /* ============================== ASSETS ============================== */ const IMG = { hero: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/1df0746e5-8a45-40b7-b23e-ed716495c000.png", pb1: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/1c32c6d5f-8223-4593-bb3a-cdfb5bdf9d07.png", pb2: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/1e0adcff4-d2db-4485-afe5-35895d115696.png", pb3: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/14e3d5716-0e8e-4df2-92ff-63e48953ec44.png", ls1: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/1eaae03cd-734c-4eb0-a507-ddaf0ed0e6d2.png", ls2: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/1752b6208-b86d-4431-a420-b30e2403edb5.png", ls3: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/10769cb82-5a36-44e6-a6a1-d07c975948fd.png", corp1: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/1a62b4d46-0a47-4dfc-b1dc-5586ce3e4d5b.png", corp2: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/1c3cdda79-4cad-4033-af94-4e69f97c07f5.png", bday1: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/131164d65-0019-4da4-bd0a-2dcd406c11a7.png", bday2: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/138109b68-2c11-464e-b62d-5b94597f0786.png", map: "https://image.qwenlm.ai/public_source/15d7dfee-3ec5-4112-a6c9-1ea63b390155/15e7288cd-e141-483d-b887-a8e5e13e343b.png", }; const ORANGE = "#FF6B00"; const GREEN = "#9DFF00"; /* ============================== DATA ============================== */ const PACKAGES = { paintball: [ { id: "light", name: "Лайт", hours: 1, price: 1500, img: IMG.pb2, features: ["Маркер, маска, камуфляж", "300 шаров бойцу", "1 площадка на выбор", "Инструктаж и судья"] }, { id: "standard", name: "Стандарт", hours: 2, price: 2200, hot: true, img: IMG.pb1, features: ["Маркер, маска, камуфляж + защита", "500 шаров бойцу", "2 площадки + сценарии", "Судья и фото-отчёт", "Зона отдыха"] }, { id: "max", name: "Максимум", hours: 3, price: 2900, img: IMG.pb3, features: ["Всё снаряжение премиум-класса", "800 шаров бойцу", "Все площадки и сценарии", "Судья, фото и видео", "Беседка с мангалом", "Призы команде"] }, ], lasertag: [ { id: "light", name: "Лайт", hours: 1, price: 900, img: IMG.ls2, features: ["Бластер и жилет с датчиками", "Сессия 60 минут", "Режим «Командный бой»", "Инструктаж и оператор"] }, { id: "standard", name: "Стандарт", hours: 2, price: 1800, hot: true, img: IMG.ls1, features: ["Бластеры с LED-отдачей", "2 часа безлимитных раундов", "5 режимов + захват базы", "Статистика и рейтинг бойцов", "Оператор и световое шоу"] }, { id: "max", name: "Максимум", hours: 3, price: 2700, img: IMG.ls3, features: ["VIP-снаряжение и жилеты", "3 часа + приватная арена", "Все 12 режимов и турнирная сетка", "Музыка, дым и лазерное шоу", "Статистика, фото и награды"] }, ], }; const ADDONS = [ { id: "balls", name: "Шары +500 шт на команду", price: 1200, per: "team", icon: Target }, { id: "protection", name: "Доп. комплект защиты", price: 300, per: "person", icon: ShieldCheck }, { id: "judge", name: "Личный судья-инструктор", price: 1500, per: "team", icon: BadgeCheck }, { id: "photo", name: "Фотосъёмка игры", price: 3500, per: "team", icon: Camera }, { id: "bbq", name: "Беседка с мангалом", price: 2500, per: "team", icon: Flame }, { id: "pyro", name: "Дымовые гранаты / пиро", price: 2000, per: "team", icon: Sparkles }, { id: "birthday", name: "Сценарий дня рождения", price: 3000, per: "team", icon: PartyPopper }, { id: "transfer", name: "Трансфер от метро", price: 2000, per: "team", icon: Car }, ]; const WEEKEND_RATE = 0.2; const discountRateFor = (n) => (n >= 20 ? 0.15 : n >= 15 ? 0.1 : n >= 10 ? 0.05 : 0); const GAME_META = { paintball: { name: "Пейнтбол", icon: Crosshair, color: ORANGE, from: 1500 }, lasertag: { name: "Лазертаг", icon: Zap, color: GREEN, from: 900 }, }; const FAQ = [ { q: "Больно ли играть в пейнтбол?", a: "Ощущения сравнимы с щелчком полотенца. Выдаём полный комплект защиты: маску с двойным стеклом, камуфляж, перчатки и жилет. При соблюдении правил попадания абсолютно безопасны." }, { q: "С какого возраста можно играть в лазертаг?", a: "Лазертаг подходит детям с 6 лет: жилеты лёгкие, бластеры весят около 1 кг, попадания фиксируются датчиками без физического воздействия. Для детей до 12 лет действует сценарий с упрощёнными правилами." }, { q: "Что взять с собой на площадку?", a: "Только удобную закрытую обувь и хорошее настроение. Маркеры, маски, камуфляж, шары, жилеты и бластеры входят в пакет. Есть раздевалки, душ и зона отдыха с мангалами." }, { q: "Можно прийти компанией меньше минимальной?", a: "Да! Одиночных бойцов объединяем в сборные команды на открытых играх (каждые сб и вс в 12:00 и 16:00). Оплата — по тарифу за игрока, без аренды площадки." }, { q: "Как работает предоплата и можно ли перенести игру?", a: "Предоплата 30% фиксирует дату и время. Бесплатно перенести игру можно один раз, предупредив не позднее чем за 48 часов. При отмене за 72+ часов предоплата возвращается полностью." }, ]; /* ============================== HELPERS ============================== */ const scrollToId = (id) => document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" }); const rub = (n) => Math.round(n).toLocaleString("ru-RU"); const formatPhone = (raw) => { let d = raw.replace(/\D/g, ""); if (!d) return ""; if (d[0] === "8") d = "7" + d.slice(1); if (d[0] !== "7") d = "7" + d; d = d.slice(0, 11); const p = d.slice(1); let res = "+7"; if (p.length > 0) res += " (" + p.slice(0, 3); if (p.length >= 3) res += ")"; if (p.length > 3) res += " " + p.slice(3, 6); if (p.length > 6) res += "-" + p.slice(6, 8); if (p.length > 8) res += "-" + p.slice(8, 10); return res; }; /* ============================== REVEAL / COUNTERS ============================== */ function Reveal({ children, delay = 0, className = "" }) { const ref = useRef(null); const [shown, setShown] = useState(false); useEffect(() => { const node = ref.current; if (!node) return; const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setShown(true); obs.disconnect(); } }, { threshold: 0.12 }); obs.observe(node); return () => obs.disconnect(); }, []); return (
{children}
); } function CountUp({ to, decimals = 0, duration = 1800, suffix = "" }) { const [val, setVal] = useState(0); const ref = useRef(null); const started = useRef(false); useEffect(() => { const node = ref.current; if (!node) return; const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting && !started.current) { started.current = true; const t0 = performance.now(); const step = (t) => { const p = Math.min(1, (t - t0) / duration); const eased = 1 - Math.pow(1 - p, 3); setVal(to * eased); if (p < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); obs.disconnect(); } }, { threshold: 0.4 }); obs.observe(node); return () => obs.disconnect(); }, [to, duration]); return {val.toLocaleString("ru-RU", { minimumFractionDigits: decimals, maximumFractionDigits: decimals })}{suffix}; } function AnimatedNumber({ value }) { const [display, setDisplay] = useState(value); const current = useRef(value); useEffect(() => { const from = current.current, to = value; if (from === to) return; const t0 = performance.now(); let raf; const step = (t) => { const p = Math.min(1, (t - t0) / 450); const eased = 1 - Math.pow(1 - p, 3); const cur = Math.round(from + (to - from) * eased); current.current = cur; setDisplay(cur); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [value]); return {rub(display)}; } function RotatingWord({ words }) { const [i, setI] = useState(0); useEffect(() => { const t = setInterval(() => setI((p) => (p + 1) % words.length), 2400); return () => clearInterval(t); }, [words.length]); return {words[i]}; } /* ============================ BRAND ICONS ============================ */ const TgIcon = (p) => (); const VkIcon = (p) => (); const WaIcon = (p) => (); /* ============================== NAVBAR ============================== */ function Navbar() { const [scrolled, setScrolled] = useState(false); const [open, setOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 40); onScroll(); window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); }, []); const links = [ { id: "directions", label: "Направления" }, { id: "packages", label: "Пакеты" }, { id: "gallery", label: "Галерея" }, { id: "calculator", label: "Калькулятор" }, { id: "booking", label: "Бронирование" }, { id: "contacts", label: "Контакты" }, ]; return (
{links.map((l) => ( ))}
); } /* ============================== HERO ============================== */ const PARTICLES = Array.from({ length: 16 }, (_, i) => ({ left: (i * 6.7 + 4) % 96, bottom: 8 + ((i * 13) % 55), delay: (i * 0.65) % 7, dur: 5 + (i % 5) * 1.4, size: 3 + (i % 4) * 2.5, color: i % 3 === 0 ? ORANGE : GREEN, })); function nextOpenGame() { const now = new Date(); const d = new Date(now); const diff = (6 - d.getDay() + 7) % 7; d.setDate(d.getDate() + diff); d.setHours(12, 0, 0, 0); if (d <= now) d.setDate(d.getDate() + 7); return d; } function Countdown() { const target = useMemo(() => nextOpenGame(), []); const [left, setLeft] = useState(target - new Date()); useEffect(() => { const t = setInterval(() => setLeft(target - new Date()), 1000); return () => clearInterval(t); }, [target]); const s = Math.max(0, Math.floor(left / 1000)); const cells = [ { v: Math.floor(s / 86400), l: "дней" }, { v: Math.floor((s % 86400) / 3600), l: "часов" }, { v: Math.floor((s % 3600) / 60), l: "минут" }, { v: s % 60, l: "секунд" }, ]; return (
{cells.map((c) => (
{String(c.v).padStart(2, "0")}
{c.l}
))}
); } function Hero() { const bgScrollRef = useRef(null); const bgMouseRef = useRef(null); const contentRef = useRef(null); useEffect(() => { let raf = null; const onMove = (e) => { if (raf) return; raf = requestAnimationFrame(() => { raf = null; const mx = e.clientX / window.innerWidth - 0.5; const my = e.clientY / window.innerHeight - 0.5; if (bgMouseRef.current) bgMouseRef.current.style.transform = `translate3d(${mx * -22}px, ${my * -16}px, 0)`; if (contentRef.current) contentRef.current.style.transform = `translate3d(${mx * 12}px, ${my * 9}px, 0)`; }); }; const onScroll = () => { if (bgScrollRef.current) bgScrollRef.current.style.transform = `translate3d(0, ${window.scrollY * 0.22}px, 0)`; }; window.addEventListener("mousemove", onMove); window.addEventListener("scroll", onScroll, { passive: true }); return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("scroll", onScroll); }; }, []); const stats = [ { icon: Trophy, to: 12, suffix: "", label: "лет в строю" }, { icon: Flame, to: 4800, suffix: "+", label: "матчей сыграно" }, { icon: Users, to: 60000, suffix: "", label: "бойцов прошли" }, { icon: Star, to: 4.9, decimals: 1, suffix: "", label: "рейтинг клуба" }, ]; return (
Пейнтбол ночью
{PARTICLES.map((p, i) => ( ))}
Клуб экстремальных игр · Москва

Заряди адреналином! Пейнтбол и лазертаг нового уровня

6 гектаров лесного полигона и неоновый лабиринт 800 м². Про-инструкторы, турниры каждую субботу и экипировка топ-класса. Соберём сценарий под твой .

{stats.map((s, i) => { const StatIcon = s.icon; return (
{s.label}
); })}
Ближайшая открытая игра
live
Суббота · 12:00 · сборные команды
Свободно мест: 7 из 24
Позвонить
scroll
); } /* ============================= MARQUEE ============================= */ function Marquee() { const words = ["ПЕЙНТБОЛ", "ЛАЗЕРТАГ", "КОРПОРАТИВЫ", "ДНИ РОЖДЕНИЯ", "ТУРНИРЫ", "НОЧНЫЕ БОИ"]; const row = [...words, ...words]; return (
{row.map((w, i) => ( {w} ))}
); } /* ============================ DIRECTIONS ============================ */ function Directions() { const cards = [ { icon: Crosshair, accent: ORANGE, title: "Тактический пейнтбол", subtitle: "Лесной полигон 6 га · Реализм · Драйв", text: "Три площадки с укрытиями из брёвен, шин и надувных фигур. Маркеры Tippmann A5 и 98 Custom, шары премиум-класса с яркой краской.", features: ["Маркер, маска, камуфляж и защита в комплекте", "500 шаров каждому бойцу в базовом пакете", "Сценарии: захват флага, сопровождение, «зомби»", "Инструктор-судья на площадке всё время боя"], price: "от 1 500 ₽ за бойца", img: IMG.pb3, }, { icon: Zap, accent: GREEN, title: "Высокотехнологичный лазертаг", subtitle: "Неоновый лабиринт 800 м² · Безопасность", text: "Крытая арена с дым-машинами, зеркальными укрытиями и LED-подсветкой. Жилеты с виброоткликом и статистикой попаданий в реальном времени.", features: ["Без боли и синяков — новобранцам от 6 лет", "Электронная статистика и рейтинг команды", "Режимы: командный бой, соло-охота, захват базы", "Музыка, световое шоу и дым в каждом раунде"], price: "от 900 ₽ за бойца", img: IMG.ls1, }, ]; return (
// Блок 01 · Направления

Две арены — один адреналин

Выбирайте формат: классический пейнтбол на открытом полигоне или футуристичный лазертаг в неоновом лабиринте.

{cards.map((c, i) => { const CardIcon = c.icon; return (
{c.title}
{c.subtitle}

{c.title}

{c.text}

    {c.features.map((f) => (
  • {f}
  • ))}
{c.price}
); })}
); } /* ============================ PACKAGES ============================ */ function Packages({ game, onSelect }) { const list = PACKAGES[game]; return (
// Блок 02 · Тарифы

Пакеты игры

Цена указана за одного бойца со всем снаряжением. Выберите направление, чтобы увидеть тарифы.

{Object.keys(GAME_META).map((g) => ( ))}
{list.map((p, i) => (
{`${GAME_META[game].name}
{p.hours} ч игры {p.hot && ( Хит )}

{p.name}

{GAME_META[game].name}
{rub(p.price)} ₽ / боец
    {p.features.map((f) => (
  • {f}
  • ))}
))}
Одиночные бойцы? Каждые сб и вс в 12:00 и 16:00 — открытые игры, сборные команды по тарифу за игрока.
); } /* ============================= GALLERY ============================= */ const GALLERY = [ { src: IMG.hero, cats: ["paintball"], title: "Ночной матч «Захват базы»", aspect: "aspect-[4/5]" }, { src: IMG.pb1, cats: ["paintball"], title: "Снайперская дуэль, полигон №2", aspect: "aspect-[3/4]" }, { src: IMG.ls1, cats: ["lasertag"], title: "Неоновый лабиринт: бой до последнего HP", aspect: "aspect-[4/5]" }, { src: IMG.pb2, cats: ["paintball"], title: "Маска после финального штурма", aspect: "aspect-square" }, { src: IMG.corp2, cats: ["corporate", "lasertag"], title: "Корпоратив IT-компании, 24 бойца", aspect: "aspect-square" }, { src: IMG.pb3, cats: ["paintball", "corporate"], title: "Командный штурм на рассвете", aspect: "aspect-[4/3]" }, { src: IMG.bday1, cats: ["birthday", "lasertag"], title: "День рождения: 14 лет драйва", aspect: "aspect-[3/4]" }, { src: IMG.ls2, cats: ["lasertag"], title: "Кибер-солдат: режим «Арена»", aspect: "aspect-square" }, { src: IMG.corp1, cats: ["corporate", "paintball"], title: "Победа отдела продаж", aspect: "aspect-[3/4]" }, { src: IMG.ls3, cats: ["lasertag"], title: "HUD-статистика матча", aspect: "aspect-[4/3]" }, { src: IMG.bday2, cats: ["birthday", "paintball"], title: "Детская лига, юбилей клуба", aspect: "aspect-square" }, ]; const FILTERS = [ { id: "all", label: "Все" }, { id: "paintball", label: "Пейнтбол" }, { id: "lasertag", label: "Лазертаг" }, { id: "corporate", label: "Корпоративы" }, { id: "birthday", label: "Дни рождения" }, ]; function Gallery() { const [filter, setFilter] = useState("all"); const [lightbox, setLightbox] = useState(null); const [zoom, setZoom] = useState(1); const items = useMemo(() => (filter === "all" ? GALLERY : GALLERY.filter((g) => g.cats.includes(filter))), [filter]); useEffect(() => { setZoom(1); }, [lightbox]); useEffect(() => { if (lightbox === null) return; const onKey = (e) => { if (e.key === "Escape") setLightbox(null); if (e.key === "ArrowRight") setLightbox((p) => (p + 1) % items.length); if (e.key === "ArrowLeft") setLightbox((p) => (p - 1 + items.length) % items.length); if (e.key === "+" || e.key === "=") setZoom((z) => Math.min(3, z + 0.25)); if (e.key === "-") setZoom((z) => Math.max(1, z - 0.25)); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [lightbox, items.length]); const catLabel = (id) => FILTERS.find((f) => f.id === id)?.label || id; return ( ); } /* ============================ CALCULATOR ============================ */ function Calculator({ game, pkg, setGame, setPkg, onBook }) { const [players, setPlayers] = useState(10); const [day, setDay] = useState("weekday"); const [addons, setAddons] = useState({}); const pkgObj = PACKAGES[game].find((p) => p.id === pkg) || PACKAGES[game][1]; const gameCost = pkgObj.price * players; const weekendCost = day === "weekend" ? Math.round(gameCost * WEEKEND_RATE) : 0; const addonsCost = ADDONS.reduce((sum, a) => sum + (addons[a.id] ? (a.per === "person" ? a.price * players : a.price) : 0), 0); const rate = discountRateFor(players); const discount = Math.round((gameCost + weekendCost) * rate); const total = gameCost + weekendCost + addonsCost - discount; const perPerson = Math.round(total / players); const toggleAddon = (id) => setAddons((p) => ({ ...p, [id]: !p[id] })); const reset = () => { setPlayers(10); setDay("weekday"); setAddons({}); }; const nextTierHint = players >= 20 ? null : players >= 15 ? { need: 20 - players, next: "15%" } : players >= 10 ? { need: 15 - players, next: "10%" } : { need: 10 - players, next: "5%" }; const book = () => onBook({ gameName: GAME_META[game].name, pkgName: pkgObj.name, hours: pkgObj.hours, players, dayName: day === "weekend" ? "Выходные и праздники" : "Будни (пн–чт)", addonNames: ADDONS.filter((a) => addons[a.id]).map((a) => a.name), total, perPerson, }); const StepTitle = ({ n, icon: Icon, text }) => (
{n} {text}
); return (
// Блок 03 · Калькулятор

Соберите свою операцию

Настройте параметры — итоговая стоимость пересчитается мгновенно, с учётом скидок за отряд.

{Object.keys(GAME_META).map((g) => { const GameIcon = GAME_META[g].icon; return ( ); })}
{PACKAGES[game].map((p) => ( ))}
setPlayers(Number(e.target.value))} className="range-neon w-full" style={{ background: `linear-gradient(90deg, ${GREEN} ${((players - 2) / 58) * 100}%, rgba(255,255,255,.12) ${((players - 2) / 58) * 100}%)` }} />
2 бойца60 бойцов
{players}
{[{ n: 10, d: 5 }, { n: 15, d: 10 }, { n: 20, d: 15 }].map((t) => ( = t.n ? "bg-[#9DFF00]/15 border-[#9DFF00]/60 text-[#9DFF00] shadow-[0_0_16px_rgba(157,255,0,.25)]" : "border-white/10 text-white/40"}`}> {t.n}+ чел — {t.d}% ))}
{[{ id: "weekday", label: "Будни (пн–чт)", note: "базовая цена" }, { id: "weekend", label: "Выходные и праздники", note: `+${WEEKEND_RATE * 100}% к игре` }].map((d) => ( ))}
{ADDONS.map((a) => { const AddonIcon = a.icon; return ( ); })}
Смета операции
{GAME_META[game].name} · {pkgObj.name} ({pkgObj.hours} ч)
{rub(pkgObj.price)} ₽ × {players} бойцов {rub(gameCost)} ₽
{weekendCost > 0 && (
Выходные и праздники +20%+{rub(weekendCost)} ₽
)} {ADDONS.filter((a) => addons[a.id]).map((a) => { const RowIcon = a.icon; return (
{a.name} +{rub(a.per === "person" ? a.price * players : a.price)} ₽
); })} {discount > 0 && (
Скидка за отряд −{Math.round(rate * 100)}%−{rub(discount)} ₽
)}
{nextTierHint && (
Добавьте ещё {nextTierHint.need} бойц. — получите скидку {nextTierHint.next}
)}
Итого за отряд
{rub(perPerson)} ₽ с бойца
Или позвоните: +7 (495) 120-45-67

Скидки за отряд: 10+ — 5%, 15+ — 10%, 20+ — 15%. Применяются автоматически.

); } /* ============================ STEPS ============================ */ function Steps() { const steps = [ { icon: MessageSquare, t: "Оставляете заявку", d: "На сайте или звоните в штаб — дежурный на связи 09:00–23:00." }, { icon: CheckCircle2, t: "Подтверждаем детали", d: "Дата, время, пакет игры и размер отряда закрепляются за вами." }, { icon: CreditCard, t: "Предоплата 30%", d: "Полигон закреплён. Перенос бесплатно за 48 часов до игры." }, { icon: Flag, t: "В бой!", d: "Приезжаете за 20 минут: экипировка, инструктаж — и вперёд." }, ]; return (
// Порядок действий

От заявки до первого выстрела

{steps.map((s, i) => { const StepIcon = s.icon; return (
0{i + 1}
{s.t}

{s.d}

); })}
); } /* ============================ BOOKING FORM ============================ */ function FloatingField({ icon: Icon, label, type = "text", value, onChange, error, inputMode, min }) { const [focus, setFocus] = useState(false); const isDate = type === "date"; const floated = focus || String(value).length > 0 || isDate; return (
setFocus(true)} onBlur={() => setFocus(false)} placeholder={isDate ? " " : ""} className="w-full bg-transparent pl-12 pr-4 pt-7 pb-3 text-white font-semibold focus:outline-none [color-scheme:dark]" />
{error &&
{error}
}
); } const TIME_SLOTS = ["10:00", "12:30", "15:00", "17:30", "20:00", "22:30"]; function BookingForm({ prefill }) { const [name, setName] = useState(""); const [phone, setPhone] = useState(""); const [date, setDate] = useState(""); const [slot, setSlot] = useState(""); const [people, setPeople] = useState(10); const [combo, setCombo] = useState("paintball-standard"); const [comment, setComment] = useState(""); const [errors, setErrors] = useState({}); const [modal, setModal] = useState(false); const [sending, setSending] = useState(false); useEffect(() => { if (prefill) { setPeople(prefill.players); const g = prefill.gameName === "Пейнтбол" ? "paintball" : "lasertag"; const p = prefill.pkgName === "Лайт" ? "light" : prefill.pkgName === "Максимум" ? "max" : "standard"; setCombo(`${g}-${p}`); } }, [prefill]); const today = new Date().toISOString().split("T")[0]; const validate = () => { const e = {}; if (name.trim().length < 2) e.name = "Укажите имя (минимум 2 символа)"; if (phone.replace(/\D/g, "").length !== 11) e.phone = "Введите телефон полностью: +7 (___) ___-__-__"; if (!date) e.date = "Выберите дату игры"; else if (date < today) e.date = "Дата не может быть в прошлом"; if (!slot) e.slot = "Выберите время слота"; if (people < 2) e.people = "Минимум 2 бойца"; setErrors(e); return Object.keys(e).length === 0; }; const submit = (ev) => { ev.preventDefault(); if (!validate()) return; setSending(true); setTimeout(() => { setSending(false); setModal(true); }, 700); }; const comboGame = combo.split("-")[0]; const comboPkgId = combo.split("-")[1]; const comboObj = (PACKAGES[comboGame] || []).find((p) => p.id === comboPkgId); return (
// Блок 04 · Бронирование

Заявка на вылазку —
перезвоним за 15 минут

Дежурный штаба подтвердит дату, время и пакет игры, ответит на все вопросы.

    {[ { icon: Clock, t: "Ответ за 15 минут", d: "ежедневно с 09:00 до 23:00" }, { icon: ShieldCheck, t: "Предоплата 30%", d: "фиксирует дату; перенос бесплатно за 48 ч" }, { icon: PartyPopper, t: "Приезжайте за 20 минут", d: "экипировка, инструктаж — и вперёд!" }, ].map((b, i) => { const RowIcon = b.icon; return (
  • {b.t} {b.d}
  • ); })}
{prefill && (
Из калькулятора: {prefill.gameName} · {prefill.pkgName} ({prefill.hours} ч)· {prefill.players} бойцов·{prefill.dayName} {prefill.addonNames.length > 0 && <>·+услуг: {prefill.addonNames.length}} {rub(prefill.total)} ₽ · ≈{rub(prefill.perPerson)} ₽/боец
)} setName(e.target.value)} error={errors.name} /> setPhone(formatPhone(e.target.value))} error={errors.phone} />
setDate(e.target.value)} error={errors.date} />
Время слота *
{TIME_SLOTS.map((t) => ( ))}
{errors.slot &&
{errors.slot}
}
Количество человек * (мин. 2)
setPeople(Math.max(2, Number(e.target.value) || 2))} className="flex-1 bg-transparent text-center font-display text-2xl text-white focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" />
{errors.people &&
{errors.people}
}