// ===== Event placeholder emoji ===== const EVENT_EMOJI = { event_1: '🩰', event_2: '🎻', event_3: '🐱', event_4: '🌻', event_5: '🎷', event_6: '🎪', event_7: '🦢', event_8: '🎨' }; const CAT_BG = { theatre: 'bg-theatre', concerts: 'bg-concerts', exhibitions: 'bg-exhibitions', kids: 'bg-kids' }; // ===== Router ===== function getRoute() { return window.location.hash.slice(1) || '/'; } function navigate(path) { window.location.hash = path; } function router() { const route = getRoute(); const app = document.getElementById('app'); window.scrollTo(0, 0); if (route === '/') renderHomePage(app); else if (route.startsWith('/category/')) renderCategoryPage(app, route.split('/')[2]); else if (route.startsWith('/event/')) renderEventPage(app, route.split('/')[2]); else if (route.startsWith('/tickets/')) renderTicketsPage(app, route.split('/')[2]); else if (route === '/checkout') renderCheckoutPage(app); else if (route === '/payment-success') renderPaymentSuccessPage(app); else renderHomePage(app); updateActiveNav(); } function updateActiveNav() { const route = getRoute(); document.querySelectorAll('.header__nav-btn').forEach(btn => { btn.classList.remove('active'); const cat = btn.dataset.category; if (cat && route === `/category/${cat}`) btn.classList.add('active'); }); } // ===== Card Components ===== // Hero card (large, like bilet.mos.ru carousel) function heroCardHTML(event) { return `
${event.name}
Вернём 10%
от ${formatPrice(event.price)}
${event.categoryName} · ${event.age_restriction}
${event.name}
${event.date} · ${event.venue}
`; } // Horizontal card (like "Главные концерты сезона") function eventCardHHTML(event) { return `
${event.name}
${event.categoryName} · ${event.age_restriction}
${event.name}
${event.date} · ${event.venue}
от ${formatPrice(event.price)}
`; } // Grid card (for category pages) function eventCardGridHTML(event) { return `
${event.name} ${EVENT_EMOJI[event.id] || '🎫'}
Вернём 10%
от ${formatPrice(event.price)}
${event.categoryName} · ${event.age_restriction}
${event.name}
${event.date} · ${event.venue}
`; } // ===== Date Picker HTML ===== function datepickerHTML() { const days = []; const now = new Date(); const monthNames = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь']; const dayNames = ['вс','пн','вт','ср','чт','пт','сб']; let currentMonth = -1; for (let i = 0; i < 30; i++) { const d = new Date(now); d.setDate(d.getDate() + i); const m = d.getMonth(); if (m !== currentMonth) { currentMonth = m; days.push(`
${monthNames[m]}
`); } const dow = d.getDay(); const isWeekend = dow === 0 || dow === 6; days.push(`
${d.getDate()} ${dayNames[dow]}
`); } return `
${days.join('')}
`; } // ===== Pages ===== function renderHomePage(app) { trackEvent('page_view', { page_type: 'homepage' }); const heroEvents = EVENTS.slice(0, 3); const concertEvents = EVENTS.filter(e => e.category === 'concerts' || e.category === 'theatre'); const theatreEvents = EVENTS.filter(e => e.category === 'theatre'); const recommendedEvents = EVENTS.slice(0, 4); app.innerHTML = `

Афиша Москвы

${datepickerHTML()}
Все события ▾
Цена, ₽ ▾
Дата ▾
Только бесплатные
Метро ▾
Площадки ▾
Для детей
Пушкинская карта
Рекомендуем вам
Персонализация CDP
${recommendedEvents.map(e => { trackEvent('block_view', { block_name: 'recommendations', event_id: e.id }); return eventCardHHTML(e); }).join('')}
${concertEvents.slice(0, 4).map(e => eventCardHHTML(e)).join('')}
${[...theatreEvents, ...EVENTS.filter(e => e.category === 'kids')].slice(0, 3).map(e => eventCardGridHTML(e)).join('')}
Все площадки ›
Большой театр
Московский зоопарк
Театр Маяковского
ГМИИ им. Пушкина
`; } function renderCategoryPage(app, categoryId) { const category = getCategoryById(categoryId); if (!category) { navigate('/'); return; } const events = getEventsByCategory(categoryId); trackEvent('category_view', { category_name: categoryId }); trackEvent('page_view', { page_type: 'category', category_name: categoryId }); // Hero events for the category const heroEvents = events.slice(0, 3); // Remaining for grid const gridEvents = events; app.innerHTML = `
${category.name}
${datepickerHTML()}
Цена, ₽ ▾
Дата ▾
Только бесплатные
Метро ▾
Площадки ▾
Для детей
Пушкинская карта
${gridEvents.map(e => eventCardGridHTML(e)).join('')}
`; } function renderEventPage(app, eventId) { const event = getEventById(eventId); if (!event) { navigate('/'); return; } trackEvent('event_view', { event_id: event.id, event_name: event.name, event_category: event.category, event_subcategory: event.subcategory, event_date: event.date, venue: event.venue, price: event.price, age_restriction: event.age_restriction }); trackEvent('page_view', { page_type: 'event', event_id: event.id }); const similar = EVENTS.filter(e => e.category === event.category && e.id !== event.id).slice(0, 3); app.innerHTML = `
Вернём 10%
· ${event.age_restriction}

${event.name}

${event.description}

${event.date}
${event.venue}
${event.name} ${EVENT_EMOJI[event.id] || '🎫'}
Расписание
19
мар / чт
19:00
от ${formatPrice(event.price)}
25
апр / сб
18:00
от ${formatPrice(Math.round(event.price * 1.2))}
16
май / сб
18:00
от ${formatPrice(event.price)}
${similar.length > 0 ? `
Похожие события
${similar.map(e => eventCardHHTML(e)).join('')}
` : ''}
`; } function renderTicketsPage(app, eventId) { const event = getEventById(eventId); if (!event) { navigate('/'); return; } trackEvent('page_view', { page_type: 'tickets', event_id: event.id }); trackEvent('ticket_selection', { event_id: event.id, ticket_price: event.price, ticket_quantity: 2, ticket_type: 'standard' }); app.innerHTML = `

Выбор билета

${event.name}
${event.venue} · ${event.date}
Билет (Стандарт) × 2 ${formatPrice(event.price * 2)}
Сервисный сбор 0 ₽
Итого ${formatPrice(event.price * 2)}
`; } function updateTicketSummary() { const typeSelect = document.getElementById('ticket-type'); const qtySelect = document.getElementById('ticket-qty'); const opt = typeSelect.options[typeSelect.selectedIndex]; const price = parseInt(opt.dataset.price); const qty = parseInt(qtySelect.value); const name = typeSelect.selectedIndex === 0 ? 'Стандарт' : 'VIP'; const total = price * qty; document.getElementById('ticket-summary').innerHTML = `
Билет (${name}) × ${qty}${formatPrice(price * qty)}
Сервисный сбор0 ₽
Итого${formatPrice(total)}
`; } function handleTicketSelection(eventId) { const typeSelect = document.getElementById('ticket-type'); const qtySelect = document.getElementById('ticket-qty'); const opt = typeSelect.options[typeSelect.selectedIndex]; const price = parseInt(opt.dataset.price); const qty = parseInt(qtySelect.value); trackEvent('ticket_selection', { event_id: eventId, ticket_price: price, ticket_quantity: qty, ticket_type: typeSelect.value }); trackEvent('checkout_start', { event_id: eventId, ticket_price: price, ticket_quantity: qty, ticket_type: typeSelect.value }); sessionStorage.setItem('checkout_data', JSON.stringify({ event_id: eventId, ticket_price: price, ticket_quantity: qty, ticket_type: typeSelect.value })); navigate('/checkout'); } function renderCheckoutPage(app) { const data = JSON.parse(sessionStorage.getItem('checkout_data') || '{}'); const event = getEventById(data.event_id) || EVENTS[0]; const qty = data.ticket_quantity || 2; const price = data.ticket_price || event.price; const total = price * qty; const typeName = data.ticket_type === 'vip' ? 'VIP' : 'Стандарт'; trackEvent('page_view', { page_type: 'checkout', event_id: event.id }); app.innerHTML = `

Оформление заказа

Ваш заказ
${event.name}
${event.date}, ${event.time}
${event.venue}
${typeName} × ${qty}${formatPrice(total)}
Итого${formatPrice(total)}
`; } function handleCheckoutSubmit(eventId) { const email = document.getElementById('checkout-email').value; const phone = document.getElementById('checkout-phone').value; const lastname = document.getElementById('checkout-lastname').value; const firstname = document.getElementById('checkout-firstname').value; if (!email || !phone || !lastname || !firstname) { alert('Пожалуйста, заполните все поля'); return; } trackEvent('checkout_form_submit', { event_id: eventId, email, phone }); trackEvent('payment_start', { event_id: eventId, email, phone }); setTimeout(() => { trackEvent('payment_success', { event_id: eventId, email }); navigate('/payment-success'); }, 800); } function renderPaymentSuccessPage(app) { trackEvent('page_view', { page_type: 'payment_success' }); trackEvent('payment_success', {}); app.innerHTML = `

Спасибо за покупку!

Ваш заказ успешно оформлен. Билеты отправлены на указанный email.
Номер заказа: MB-${Date.now().toString().slice(-6)}

`; } // ===== Event Tracking (dataLayer for tag manager) ===== window.dataLayer = window.dataLayer || []; function trackEvent(eventName, params) { const eventData = { event: eventName, ...params, timestamp: new Date().toISOString() }; window.dataLayer.push(eventData); console.log('[DataLayer]', eventName, params); } function trackUserAttributes() { window.dataLayer.push({ event: 'user_attributes', cookie_id: getCookieId(), session_id: getSessionId(), device_type: getDeviceType(), geo: 'Moscow' }); } function getCookieId() { let id = localStorage.getItem('mosbilet_cookie_id'); if (!id) { id = 'ck_' + Math.random().toString(36).substr(2, 12); localStorage.setItem('mosbilet_cookie_id', id); } return id; } function getSessionId() { let id = sessionStorage.getItem('mosbilet_session_id'); if (!id) { id = 'ss_' + Math.random().toString(36).substr(2, 12); sessionStorage.setItem('mosbilet_session_id', id); } return id; } function getDeviceType() { const w = window.innerWidth; if (w < 768) return 'mobile'; if (w < 1024) return 'tablet'; return 'desktop'; } // ===== Init ===== window.addEventListener('hashchange', router); window.addEventListener('DOMContentLoaded', () => { trackUserAttributes(); router(); });