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 (
window.scrollTo({ top: 0, behavior: "smooth" })} className="flex items-center gap-2 group">
POLYGON.X
{links.map((l) => (
scrollToId(l.id)}
className="text-sm font-semibold text-white/70 hover:text-[#9DFF00] transition-colors relative after:absolute after:left-0 after:-bottom-1 after:h-[2px] after:w-0 hover:after:w-full after:bg-[#9DFF00] after:transition-all">
{l.label}
))}
scrollToId("booking")}
className="px-5 py-2.5 rounded-xl bg-[#9DFF00] text-black text-sm font-extrabold uppercase tracking-wide hover:bg-white hover:shadow-[0_0_28px_rgba(157,255,0,.6)] transition-all active:scale-95">
Играть
setOpen(!open)} aria-label="Меню">
{open ? : }
{links.map((l) => (
{ setOpen(false); scrollToId(l.id); }}
className="text-left px-4 py-3 rounded-xl text-white/80 font-semibold hover:bg-white/5 hover:text-[#9DFF00] transition-colors">
{l.label}
))}
{ setOpen(false); scrollToId("booking"); }} className="mt-2 px-4 py-3 rounded-xl bg-[#9DFF00] text-black font-extrabold uppercase">Играть
);
}
/* ============================== 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 м². Про-инструкторы, турниры каждую субботу
и экипировка топ-класса. Соберём сценарий под твой .
scrollToId("calculator")}
className="fu px-8 py-4 rounded-2xl bg-gradient-to-r from-[#FF6B00] to-[#ff9500] text-black font-extrabold uppercase tracking-wide text-sm sm:text-base shadow-[0_0_30px_rgba(255,107,0,.45)] hover:shadow-[0_0_55px_rgba(255,107,0,.8)] hover:-translate-y-1 transition-all active:scale-95 flex items-center justify-center gap-3"
style={{ animationDelay: ".95s" }}>
Рассчитать стоимость
scrollToId("booking")}
className="fu group px-8 py-4 rounded-2xl border border-[#9DFF00]/60 bg-white/5 backdrop-blur-md text-[#9DFF00] font-extrabold uppercase tracking-wide text-sm sm:text-base hover:bg-[#9DFF00] hover:text-black hover:shadow-[0_0_40px_rgba(157,255,0,.5)] hover:-translate-y-1 transition-all active:scale-95 flex items-center justify-center gap-3"
style={{ animationDelay: "1.1s" }}>
Забронировать игру
{stats.map((s, i) => {
const StatIcon = s.icon;
return (
);
})}
Ближайшая открытая игра
live
Суббота · 12:00 · сборные команды
Свободно мест: 7 из 24
scrollToId("booking")}
className="py-3.5 rounded-xl bg-[#9DFF00] text-black font-extrabold uppercase text-xs tracking-wide hover:bg-white hover:shadow-[0_0_30px_rgba(157,255,0,.5)] transition-all active:scale-95">
Занять место
Позвонить
);
}
/* ============================= 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.subtitle}
{c.title}
{c.text}
{c.features.map((f) => (
{f}
))}
{c.price}
scrollToId("packages")}
className="px-5 py-2.5 rounded-xl text-black text-sm font-extrabold uppercase hover:-translate-y-0.5 hover:shadow-lg transition-all active:scale-95"
style={{ background: c.accent, boxShadow: `0 0 20px ${c.accent}44` }}>
Тарифы
);
})}
);
}
/* ============================ PACKAGES ============================ */
function Packages({ game, onSelect }) {
const list = PACKAGES[game];
return (
// Блок 02 · Тарифы
Пакеты игры
Цена указана за одного бойца со всем снаряжением. Выберите направление, чтобы увидеть тарифы.
{Object.keys(GAME_META).map((g) => (
onSelect(g, null, true)}
className={`px-6 py-3 rounded-xl text-sm font-extrabold uppercase tracking-wide transition-all active:scale-95 ${game === g ? "text-black shadow-lg" : "text-white/60 hover:text-white"}`}
style={game === g ? { background: GAME_META[g].color, boxShadow: `0 0 24px ${GAME_META[g].color}66` } : {}}>
{GAME_META[g].name}
))}
{list.map((p, i) => (
{p.hours} ч игры
{p.hot && (
Хит
)}
{p.name}
{GAME_META[game].name}
{rub(p.price)}
₽ / боец
{p.features.map((f) => (
{f}
))}
onSelect(game, p.id)}
className={`py-3.5 rounded-xl font-extrabold uppercase text-xs tracking-wide transition-all active:scale-95 hover:-translate-y-0.5 ${p.hot ? "bg-gradient-to-r from-[#FF6B00] to-[#ff9500] text-black shadow-[0_0_26px_rgba(255,107,0,.45)] hover:shadow-[0_0_40px_rgba(255,107,0,.7)]" : "border border-[#9DFF00]/50 text-[#9DFF00] hover:bg-[#9DFF00] hover:text-black"}`}>
Выбрать пакет
scrollToId("booking")}
className="py-3.5 rounded-xl font-bold uppercase text-xs tracking-wide border border-white/15 text-white/70 hover:border-white/40 hover:text-white transition-all active:scale-95">
Забронировать
))}
Одиночные бойцы? Каждые сб и вс в 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 (
// Блок 05 · Галерея
Атмосфера наших арен
Живые кадры с площадок. Нажмите на фото, чтобы открыть на весь экран.
{FILTERS.map((f) => (
{ setFilter(f.id); setLightbox(null); }}
className={`px-5 py-2.5 rounded-full text-sm font-bold uppercase tracking-wide transition-all active:scale-95 border ${filter === f.id ? "bg-[#9DFF00] text-black border-[#9DFF00] shadow-[0_0_24px_rgba(157,255,0,.45)]" : "glass text-white/60 hover:text-white hover:border-[#9DFF00]/50"}`}>
{f.label}
))}
{items.map((g, idx) => (
setLightbox(idx)} className="group relative w-full overflow-hidden rounded-2xl border border-white/10 hover:border-[#9DFF00]/50 transition-colors text-left block">
{g.cats.map((c) => (
{catLabel(c)}
))}
{g.title}
))}
{lightbox !== null && items[lightbox] && (
setLightbox(null)}>
setLightbox(null)} aria-label="Закрыть">
{ e.stopPropagation(); setZoom((z) => Math.max(1, z - 0.25)); }} aria-label="Уменьшить">
{Math.round(zoom * 100)}%
{ e.stopPropagation(); setZoom((z) => Math.min(3, z + 0.25)); }} aria-label="Увеличить">
{ e.stopPropagation(); setLightbox((lightbox - 1 + items.length) % items.length); }} aria-label="Назад">
{ e.stopPropagation(); setLightbox((lightbox + 1) % items.length); }} aria-label="Вперёд">
e.stopPropagation()} onWheel={(e) => setZoom((z) => Math.min(3, Math.max(1, z + (e.deltaY < 0 ? 0.2 : -0.2))))}>
{items[lightbox].title}
Фото {lightbox + 1} / {items.length}
← → листать · Esc закрыть · колесо мыши — масштаб
)}
);
}
/* ============================ 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 (
setGame(g)} />
{game === g && }
{GAME_META[g].name}
от {rub(GAME_META[g].from)} ₽ / боец
);
})}
{PACKAGES[game].map((p) => (
setPkg(p.id)}
className={`relative rounded-2xl border p-4 text-left transition-all active:scale-95 overflow-hidden ${pkg === p.id ? "border-[#9DFF00] bg-[#9DFF00]/10 shadow-[0_0_26px_rgba(157,255,0,.2)]" : "border-white/10 bg-white/[0.03] hover:bg-white/[0.06]"}`}>
{p.hot &&
}
{p.name}
{p.hours} ч · {rub(p.price)} ₽/боец
{pkg === p.id ? "✓ выбран" : "выбрать"}
))}
setPlayers((p) => Math.max(2, p - 1))} className="w-12 h-12 rounded-xl bg-white/10 hover:bg-[#FF6B00] hover:text-black flex items-center justify-center transition-colors active:scale-90 shrink-0" aria-label="Меньше">
{players}
setPlayers((p) => Math.min(60, p + 1))} className="w-12 h-12 rounded-xl bg-white/10 hover:bg-[#9DFF00] hover:text-black flex items-center justify-center transition-colors active:scale-90 shrink-0" aria-label="Больше">
{[{ 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) => (
setDay(d.id)}
className={`rounded-2xl border p-4 text-left transition-all active:scale-95 ${day === d.id ? "border-[#FF6B00] bg-[#FF6B00]/10 shadow-[0_0_26px_rgba(255,107,0,.2)]" : "border-white/10 bg-white/[0.03] hover:bg-white/[0.06]"}`}>
{d.label}
{d.note}
))}
Смета операции
{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 (
);
})}
);
}
/* ============================ 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]" />
{label}
{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}
);
})}
{modal && (
setModal(false)}>
e.stopPropagation()}>
Заявка успешно отправлена!
{name.trim() || "Боец"}, дежурный штаба перезвонит на {phone} в течение 15 минут.
Бронь: {GAME_META[comboGame] ? GAME_META[comboGame].name : ""} · {comboObj ? comboObj.name : ""} , {date ? new Date(date + "T00:00:00").toLocaleDateString("ru-RU", { day: "numeric", month: "long" }) : ""} в {slot}, отряд {people} чел.
{prefill && (
Расчёт из калькулятора: {prefill.gameName} · {prefill.pkgName} — {rub(prefill.total)} ₽
)}
{comment.trim() && (
Комментарий: {comment}
)}
Позвонить
setModal(false)} className="py-4 rounded-2xl bg-[#9DFF00] text-black font-extrabold uppercase text-xs hover:shadow-[0_0_35px_rgba(157,255,0,.5)] transition-all active:scale-95">Отлично!
)}
);
}
/* ============================== FAQ ============================== */
function Faq() {
const [openIdx, setOpenIdx] = useState(0);
return (
// Блок 06 · Вопросы
Брифинг: частые вопросы
{FAQ.map((f, i) => {
const isOpen = openIdx === i;
return (
setOpenIdx(isOpen ? -1 : i)} className="w-full flex items-center justify-between gap-4 px-6 py-5 text-left">
{f.q}
);
})}
);
}
/* ============================== FOOTER ============================== */
function Footer() {
return (
);
}
/* ======================= STICKY CTA + BACK TO TOP ======================= */
function StickyCtaBar() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const onScroll = () => setVisible(window.scrollY > 700);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
scrollToId("calculator")} className="px-5 h-12 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#ff9500] text-black font-extrabold uppercase text-xs shrink-0">Рассчитать
);
}
function BackToTop() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const onScroll = () => setVisible(window.scrollY > 1200);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
window.scrollTo({ top: 0, behavior: "smooth" })} aria-label="Наверх"
className={`fixed bottom-24 lg:bottom-8 right-4 sm:right-6 z-40 w-12 h-12 rounded-full glass border-[#9DFF00]/40 flex items-center justify-center text-[#9DFF00] hover:bg-[#9DFF00] hover:text-black transition-all duration-300 ${visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6 pointer-events-none"}`}>
);
}
/* ============================== APP ============================== */
export default function App() {
const [calcGame, setCalcGame] = useState("paintball");
const [calcPkg, setCalcPkg] = useState("standard");
const [prefill, setPrefill] = useState(null);
const selectPackage = (game, pkgId, keepPkg = false) => {
setCalcGame(game);
if (pkgId) setCalcPkg(pkgId);
else if (!keepPkg) setCalcPkg("standard");
if (pkgId) setTimeout(() => scrollToId("calculator"), 60);
};
const handleBookFromCalc = (data) => {
setPrefill(data);
setTimeout(() => scrollToId("booking"), 60);
};
return (
);
}