Сейчас узнаем, какой подарок выпадет именно Вам
Крутите колесо
Как создать продажу билетов через интерактивную сцену в Тильда
Схема зала
Ложа амфитеатра левая
3000
Ложа бенуар
3500
Ложа бельетаж левая
2500
Амфитеатр
2800
Ложа амфитеатра правая
3000
Партер
3500
Ложа бельетаж правая
2500

Как создать продажу билетов через интерактивную сцену в Тильда

Пример позволяет создать интерактивную сцену для продажи билетов, используя каталог.
Рассмотрен способ создания сцены через ZeroBlock и способ создания через Figma и SVG
Mo-ti Level Up
Видео инструкции по добавлению кода и работе с Zero Block.
Как создать продажу билетов через интерактивную сцену
Фрагмент видео
Библиотека для примера
Код для ZeroBlock
<style>
@media screen and (min-width: 640px) {
    .uc-seats-map [data-seat]:hover .tn-atom {
        background-color: #e2e2e2 !important; 
    }
}

.uc-seats-map [data-seat].active-chair .tn-atom {
    background-color: #aaec5d !important;
}

.uc-seats-map [data-seat].sold-out .tn-atom {
    background-color: #ffb49c !important;
    opacity: 0.4;
}



.uc-seats-map {
    position: relative;
}

.uc-seats-map [data-seat] {
    cursor: pointer;
    pointer-events: all;
}

.uc-seats-map:not(.load-map):after {
    content: "";
    width: 100%;
    height: 100%;
    background-image: url(https://static.tildacdn.com/tild6136-3066-4630-a462-376665323265/nopic_image.gif);
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 150px;
    opacity: 0.5;
    pointer-events: none;
}

.uc-seats-map.load-map:after {
    display: none;
}

.uc-seats-map>div {
    opacity: 0.3;
}

.uc-seats-map.load-map>div {
    opacity: 1;
}

.uc-seats-map [data-seat].sold-out {
    pointer-events: none;
    cursor: not-allowed;
}

.t706__carticon-imgwrap {
    box-shadow: none;
}

.t706__product-imgdiv {
    background-color: transparent;
}

.t706__cartwin-totalamount-wrap {
    display: none !important;
}

.t706__bubble {
    display: none !important;
}

.t706__carticon-counter.js-carticon-counter:after {
    content: " бил.";
}

.t706__carticon-counter.js-carticon-counter {
    font-size: 14px;
}

.uc-seats-list {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    opacity: 0;
    z-index: -50;
    pointer-events: none;
}

.product-section-widget {
    display: none;
    max-width: 1200px;
    margin: 0 auto;
    background: #fff;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}

.widget-title {
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 20px;
    text-align: center;
}

.products-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 20px;
}

.product-card {
    border: 1px solid #e0e0e0;
    border-radius: 12px;
    padding: 16px;
    background: #fafafa;
    display: flex;
    flex-direction: column;
    gap: 12px;
    transition: box-shadow 0.2s, transform 0.2s;
}

.product-card:hover {
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    transform: translateY(-2px);
}

.product-card.out-of-stock {
    opacity: 0.75;
}

.card-image {
    width: 100%;
    height: 200px;
    background: #fff;
    border-radius: 8px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.card-image img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.card-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.card-sku {
    font-size: 12px;
    color: #666;
    background: #e0e0e0;
    padding: 2px 8px;
    border-radius: 4px;
    width: fit-content;
}

.card-title {
    font-size: 15px;
    font-weight: 500;
    line-height: 1.3;
    min-height: 40px;
}

.card-mark {
    font-size: 12px;
    color: #666;
}

.card-price {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 4px 0;
}

.card-price-old {
    text-decoration: line-through;
    color: #999;
    font-size: 13px;
}

.card-price-current {
    font-size: 18px;
    font-weight: 600;
    color: #000;
}

.card-specs {
    list-style: none;
    font-size: 12px;
    color: #555;
    margin: 0;
    padding: 0;
}

.card-specs li {
    margin: 2px 0;
}

.card-inventory {
    font-size: 12px;
    padding: 4px 8px;
    border-radius: 6px;
    width: fit-content;
}

.card-inventory.in-stock {
    background: #e8f5e9;
    color: #2e7d32;
}

.card-inventory.out-stock {
    background: #ffebee;
    color: #c62828;
}

.card-inventory.unlimited {
    background: #e3f2fd;
    color: #1565c0;
}

.btn-buy {
    box-sizing: border-box;
    margin-top: auto;
    padding: 10px 20px;
    background: #000;
    color: #fff;
    border: none;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    width: 100%;
}

.btn-buy:hover:not(:disabled) {
    background: #333;
}

.btn-buy:disabled {
    background: #ccc;
    cursor: not-allowed;
}

.btn-buy.added {
    background: #4caf50;
}

.btn-buy .spinner {
    display: none;
    width: 14px;
    height: 14px;
    border: 2px solid rgba(255,255,255,0.3);
    border-top-color: #fff;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

.btn-buy.loading .spinner {
    display: block;
}

.btn-buy.loading .btn-text {
    display: none;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

.message {
    padding: 12px 16px;
    border-radius: 10px;
    margin-bottom: 16px;
    font-size: 14px;
    display: none;
    text-align: center;
}

.message.error {
    background: #ffebee;
    color: #c62828;
    border: 1px solid #ffcdd2;
}

.message.success {
    background: #e8f5e9;
    color: #2e7d32;
    border: 1px solid #c8e6c9;
}

.message.visible {
    display: block;
    animation: slideIn 0.3s ease;
}

@keyframes slideIn {
    from {
        opacity: 0;
        transform: translateY(10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.loader {
    text-align: center;
    padding: 40px;
    color: #666;
    font-size: 16px;
}

@media (max-width: 640px) {
    .products-grid {
        grid-template-columns: 1fr;
    }
}
</style>

<div class="product-section-widget">
    <h2 class="widget-title">🛍️ Наши товары</h2>
    <div class="message" id="message"></div>
    <div class="loader" id="loader">Загрузка товаров...</div>
    <div class="products-grid" id="productsGrid"></div>
</div>

<script>
(function() {
    'use strict';
    
    const STORE_PART_UID = '378598258843';
    const PRODUCTS_PER_REQUEST = 100;
    
    const messageEl = document.getElementById('message');
    const loaderEl = document.getElementById('loader');
    const productsGrid = document.getElementById('productsGrid');
    const seatsMap = document.querySelector('.uc-seats-map');
    
    const state = {
        products: [],
        productsBySku: {},
        isMapLoaded: false,
        historyProductsArr: []
    };
    
    const safeJsonParse = (str, fallback = null) => {
        if (!str || typeof str !== 'string') return fallback;
        try { return JSON.parse(str); } catch { return fallback; }
    };
    
    const parsePartUids = (input) => {
        if (Array.isArray(input)) return input.map(String);
        if (!input || typeof input !== 'string') return [];
        try {
            const parsed = JSON.parse(input);
            return Array.isArray(parsed) ? parsed.map(String) : [];
        } catch { return [String(input)].filter(Boolean); }
    };
    
    const extractRecid = (product) => {
        if (product.recid) return String(product.recid);
        if (product.externalid) return String(product.externalid);
        if (product.url) {
            const match = product.url.match(/\/tproduct\/(\d+)-/);
            if (match) return match[1];
        }
        return String(product.uid);
    };
    
    const getInventory = (product) => {
        const editions = safeJsonParse(product.editions, []);
        if (Array.isArray(editions) && editions[0]?.quantity !== undefined && editions[0].quantity !== "") {
            const inv = parseInt(editions[0].quantity);
            if (!isNaN(inv) && inv >= 0) return inv;
        }
        if (product.quantity !== undefined && product.quantity !== "") {
            const inv = parseInt(product.quantity);
            if (!isNaN(inv) && inv >= 0) return inv;
        }
        return null;
    };
    
    const getImageUrl = (product) => {
        const editions = safeJsonParse(product.editions, []);
        if (editions?.[0]?.img) return editions[0].img;
        const gallery = safeJsonParse(product.gallery, []);
        if (gallery?.[0]?.img) return gallery[0].img;
        return product.img || 'https://static.tildacdn.com/img/tilda-no-photo-560x560.png';
    };
    
    const formatPrice = (price) => {
        const num = parseFloat(price);
        if (isNaN(num)) return price;
        return new Intl.NumberFormat('ru-RU', { 
            style: 'currency', currency: 'RUB', minimumFractionDigits: 0 
        }).format(num);
    };
    
    const escapeHtml = (str) => {
        if (!str) return '';
        const div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    };
    
    const showMessage = (text, type = 'error') => {
        messageEl.textContent = text;
        messageEl.className = `message ${type} visible`;
    };
    
    const buildCartItem = (product) => {
        const inv = getInventory(product);
        return {
            name: product.title || 'Товар',
            title: product.title || 'Товар',
            uid: String(product.uid),
            lid: String(product.uid),
            gen_uid: String(product.uid),
            recid: extractRecid(product),
            price: parseFloat(product.price) || 0,
            priceold: product.priceold ? parseFloat(product.priceold) : null,
            img: getImageUrl(product),
            sku: product.sku || '',
            url: product.url || window.location.href,
            inv: inv,
            pack_label: product.pack_label || '',
            pack_x: String(product.pack_x || 0),
            pack_y: String(product.pack_y || 0),
            pack_z: String(product.pack_z || 0),
            pack_m: String(product.pack_m || 0),
            part_uids: parsePartUids(product.partuids),
            quantity: 1,
            amount: parseFloat(product.price) || 0,
            ts: Date.now()
        };
    };
    
    const addToCart = async (product, btnEl) => {
        const waitForCart = (maxAttempts = 50) => new Promise((resolve, reject) => {
            if (typeof window.tcart__addProduct === 'function') {
                resolve();
                return;
            }
            let attempts = 0;
            const check = () => {
                if (typeof window.tcart__addProduct === 'function') {
                    resolve();
                } else if (attempts < maxAttempts) {
                    attempts++;
                    setTimeout(check, 100);
                } else {
                    reject(new Error('cart not loaded'));
                }
            };
            check();
        });
        
        try {
            if (btnEl) {
                btnEl.classList.add('loading');
                btnEl.disabled = true;
            }
            
            const cartItem = buildCartItem(product);
            await waitForCart();
            window.tcart__addProduct(cartItem);
            
            if (btnEl) {
                const textEl = btnEl.querySelector('.btn-text');
                const originalText = textEl.textContent;
                textEl.textContent = 'В корзине';
                btnEl.classList.add('added');
                
                setTimeout(() => {
                    textEl.textContent = originalText;
                    btnEl.classList.remove('added');
                }, 2000);
            }
            
            return true;
        } catch (err) {
            showMessage('Ошибка добавления в корзину: ' + err.message, 'error');
            return false;
        } finally {
            if (btnEl) {
                btnEl.classList.remove('loading');
                btnEl.disabled = false;
            }
        }
    };
    
    const removeFromCartBySku = (sku) => {
        const productsInCart = document.querySelectorAll(".t706__product");
        productsInCart.forEach(function(product) {
            const skuOption = product.querySelector('.t706__product-title__option');
            if (skuOption && skuOption.innerText === sku) {
                const delBtn = product.querySelector('.t706__product-del');
                if (delBtn) delBtn.click();
            }
        });
    };
    
    const renderCard = (product) => {
        const characteristics = Array.isArray(product.characteristics) ? product.characteristics : [];
        const inv = getInventory(product);
        const hasStock = inv === null || inv > 0;
        
        const card = document.createElement('div');
        card.className = `product-card ${!hasStock ? 'out-of-stock' : ''}`;
        card.dataset.uid = product.uid;
        card.dataset.sku = product.sku || '';
        
        const specsHtml = characteristics.slice(0, 3).map(spec => 
            `<li><strong>${escapeHtml(spec.title)}:</strong> ${escapeHtml(spec.value)}</li>`
        ).join('');
        
        const inventoryText = inv !== null 
            ? (inv > 0 ? `В наличии: ${inv} шт.` : 'Нет в наличии')
            : '∞ Без ограничений';
        const inventoryClass = inv !== null 
            ? (inv > 0 ? 'in-stock' : 'out-stock')
            : 'unlimited';
        
        const priceOldHtml = product.priceold && parseFloat(product.priceold) > 0
            ? `<span class="card-price-old">${formatPrice(product.priceold)}</span>`
            : '';
        
        card.innerHTML = `
            <div class="card-image">
                <img src="${escapeHtml(getImageUrl(product))}" alt="${escapeHtml(product.title)}" loading="lazy">
            </div>
            <div class="card-content">
                ${product.sku ? `<span class="card-sku">${escapeHtml(product.sku)}</span>` : ''}
                <h3 class="card-title">${escapeHtml(product.title || 'Товар')}</h3>
                ${product.mark ? `<div class="card-mark">${escapeHtml(product.mark)}</div>` : ''}
                <div class="card-price">
                    ${priceOldHtml}
                    <span class="card-price-current">${formatPrice(product.price)}</span>
                </div>
                ${specsHtml ? `<ul class="card-specs">${specsHtml}</ul>` : ''}
                <div class="card-inventory ${inventoryClass}">${inventoryText}</div>
                <button class="btn-buy" data-uid="${product.uid}" data-sku="${product.sku || ''}">
                    <span class="btn-text">${hasStock ? 'Купить товар' : 'Нет в наличии'}</span>
                    <span class="spinner"></span>
                </button>
            </div>
        `;
        
        const buyBtn = card.querySelector('.btn-buy');
        if (hasStock) {
            buyBtn.addEventListener('click', (e) => {
                e.preventDefault();
                addToCart(product, buyBtn);
            });
        } else {
            buyBtn.disabled = true;
        }
        
        return card;
    };
    
    const loadAllProducts = async () => {
        let slice = 1;
        let hasMore = true;
        
        while (hasMore) {
            try {
                const url = new URL('https://store.tildaapi.com/api/getproductslist/');
                url.searchParams.set('storepartuid', STORE_PART_UID);
                url.searchParams.set('c', Date.now());
                url.searchParams.set('slice', slice);
                url.searchParams.set('size', String(PRODUCTS_PER_REQUEST));
                
                const resp = await fetch(url.toString());
                if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
                
                const data = await resp.json();
                const products = data?.products || [];
                
                if (products.length === 0) break;
                
                products.forEach(product => {
                    state.products.push(product);
                    if (product.sku) {
                        state.productsBySku[product.sku] = product;
                    }
                    
                    productsGrid.appendChild(renderCard(product));
                });
                
                hasMore = 'nextslice' in data;
                if (hasMore) slice++;
                
            } catch (err) {
                showMessage('Ошибка загрузки: ' + err.message, 'error');
                break;
            }
        }
        
        loaderEl.style.display = 'none';
        
        const totalLoaded = productsGrid.querySelectorAll('.product-card').length;
        if (totalLoaded === 0) {
            showMessage('Товары не найдены', 'error');
        }
        
        initSeatsMap();
    };
    
    function initSeatsMap() {
        if (state.isMapLoaded) return;
        if (!seatsMap) return;
        
        setTimeout(function() {
            markSoldOutSeats();
            seatsMap.classList.add('load-map');
            attachClickHandlers();
            
            setTimeout(function() {
                checkCartHistory();
            }, 300);
            
            state.isMapLoaded = true;
        }, 300);
    }
    
    function markSoldOutSeats() {
        const foundSkus = new Set();
        
        state.products.forEach(function(product) {
            const sku = product.sku || '';
            const quant = getInventory(product);
            
            if (sku) {
                foundSkus.add(sku);
                
                if (quant === 0) {
                    const seatEl = seatsMap.querySelector('[data-seat="' + sku + '"]');
                    if (seatEl) setSoldOut(seatEl);
                }
            }
        });
        
        const allSeats = seatsMap.querySelectorAll('[data-seat]');
        allSeats.forEach(function(seatEl) {
            const sku = seatEl.getAttribute('data-seat');
            if (sku && !foundSkus.has(sku)) {
                setSoldOut(seatEl);
            }
        });
    }
    
    function setSoldOut(seatEl) {
        seatEl.classList.add('sold-out');
        const atom = seatEl.querySelector('.tn-atom');
        if (atom) atom.classList.add('sold-out');
    }
    
    function attachClickHandlers() {
        seatsMap.addEventListener('click', function(e) {
            const seatEl = e.target.closest('[data-seat]');
            if (!seatEl) return;
            handleSeatClick(seatEl);
        });
    }
    
    function handleSeatClick(el) {
        const chairId = el.getAttribute('data-seat');
        if (!chairId) return;
        
        if (el.classList.contains('sold-out')) return;
        
        const atom = el.querySelector('.tn-atom');
        
        if (el.classList.contains('active-chair')) {
            el.classList.remove('active-chair');
            if (atom) atom.classList.remove('active-chair');
            removeFromCartBySku(chairId);
        } else {
            addPlaceToCart(el, chairId);
        }
    }
    
    function addPlaceToCart(em, seatId) {
        const product = state.productsBySku[seatId];
        
        if (!product) return;
        
        const quant = getInventory(product);
        if (quant === 0) return;
        
        addToCart(product).then(function(success) {
            if (success) {
                em.classList.add('active-chair');
                const atom = em.querySelector('.tn-atom');
                if (atom) atom.classList.add('active-chair');
                
                setTimeout(function() {
                    if (typeof tcart__reDrawProducts === 'function') {
                        tcart__reDrawProducts();
                    }
                }, 200);
            }
        });
    }
    
    function checkCartHistory() {
        if (typeof tcart === 'undefined' || tcart.total <= 0) return;
        
        if (typeof tcart__reDrawProducts === 'function') {
            tcart__reDrawProducts();
        }
        
        const productsInCart = document.querySelectorAll(".t706__product");
        
        productsInCart.forEach(function(product) {
            let sku = product.querySelector('.t706__product-title__option')?.innerText || '';
            
            if (sku) {
                const seatEl = seatsMap.querySelector('[data-seat="' + sku + '"]');
                const prod = state.productsBySku[sku];
                const quant = prod ? getInventory(prod) : null;
                
                if (seatEl) {
                    if (quant === 0) {
                        state.historyProductsArr.push(sku);
                    } else {
                        seatEl.classList.add('active-chair');
                        const atom = seatEl.querySelector('.tn-atom');
                        if (atom) atom.classList.add('active-chair');
                    }
                }
            }
        });
        
        if (state.historyProductsArr.length > 0) {
            removeHistoryProducts();
        }
    }
    
    function removeHistoryProducts() {
        for (let i = 0; i < state.historyProductsArr.length; i++) {
            const productsInCart = document.querySelectorAll(".t706__product");
            productsInCart.forEach(function(product) {
                const sku = product.querySelector('.t706__product-title__option')?.innerText || '';
                if (sku === state.historyProductsArr[i]) {
                    const delBtn = product.querySelector('.t706__product-del');
                    if (delBtn) delBtn.click();
                }
            });
        }
        state.historyProductsArr = [];
    }
    
    document.addEventListener('click', function(event) {
        if (event.target.closest('.t706__product-del')) {
            const cartProduct = event.target.closest('.t706__product');
            if (!cartProduct) return;
            
            let sku = cartProduct.querySelector('.t706__product-title__option')?.innerText || '';
            if (!sku) return;
            
            const seatEl = seatsMap.querySelector('[data-seat="' + sku + '"]');
            if (seatEl) {
                seatEl.classList.remove('active-chair');
                const atom = seatEl.querySelector('.tn-atom');
                if (atom) atom.classList.remove('active-chair');
            }
        }
    });
    
    loadAllProducts();
})();
</script>
Код для SVG
<div id="svg-viewport" style="width:100%; overflow:hidden; position:relative; background:#f5f5f5; cursor:grab;">
  <div id="svg-container" style="width:100%; position:relative;"></div>
</div>


<div class="zoom-controls" id="zoom-controls">
  <button class="zoom-btn zoom-in" title="Приблизить"></button>
  <button class="zoom-btn zoom-out" title="Отдалить"></button>
  <button class="zoom-btn zoom-reset" title="Сбросить"></button>
</div>


<script>
document.addEventListener('DOMContentLoaded', function() {

    const CONFIG = {
        seatPrefixes: ['sts_'],
        svgUrl: 'https://845c2bcc-da72-4821-bf54-953d3f4e3581.selstorage.ru/scene/sheme.svg',
        storePartUid: '334902715593',
        productsPerRequest: 100,
        zoomLevels: [100, 200, 300, 600],
    };

    const state = {
        historyProductsArr: [],
        isMapLoaded: false,
        svgLoaded: false,
        productsLoaded: false,

        zoomIndex: 0,

     
        isDragging: false,
        dragStartX: 0,
        dragStartY: 0,
        scrollStartX: 0,
        scrollStartY: 0,
        dragTracking: false,

     
        products: [],
        productsBySku: {},
    };

    const viewport = document.getElementById('svg-viewport');
    const container = document.getElementById('svg-container');
    const zoomControls = document.getElementById('zoom-controls');
    let svgElement = null;

    
    const safeJsonParse = (str, fallback) => {
        if (!str || typeof str !== 'string') return fallback;
        try { return JSON.parse(str); } catch (e) { return fallback; }
    };

    const parsePartUids = (input) => {
        if (Array.isArray(input)) return input.map(String);
        if (!input || typeof input !== 'string') return [];
        try {
            const parsed = JSON.parse(input);
            return Array.isArray(parsed) ? parsed.map(String) : [];
        } catch (e) { return [String(input)].filter(Boolean); }
    };

    const extractRecid = (product) => {
        if (product.recid) return String(product.recid);
        if (product.externalid) return String(product.externalid);
        if (product.url) {
            const match = product.url.match(/\/tproduct\/(\d+)-/);
            if (match) return match[1];
        }
        return String(product.uid);
    };

    const getInventory = (product) => {
        const editions = safeJsonParse(product.editions, []);
        if (Array.isArray(editions) && editions[0] && editions[0].quantity !== undefined && editions[0].quantity !== "") {
            const inv = parseInt(editions[0].quantity);
            if (!isNaN(inv) && inv >= 0) return inv;
        }
        if (product.quantity !== undefined && product.quantity !== "") {
            const inv = parseInt(product.quantity);
            if (!isNaN(inv) && inv >= 0) return inv;
        }
        return null;
    };

    const getImageUrl = (product) => {
        const editions = safeJsonParse(product.editions, []);
        if (editions && editions[0] && editions[0].img) return editions[0].img;
        const gallery = safeJsonParse(product.gallery, []);
        if (gallery && gallery[0] && gallery[0].img) return gallery[0].img;
        return product.img || 'https://static.tildacdn.com/img/tilda-no-photo-560x560.png';
    };

    const buildCartItem = (product) => {
        const inv = getInventory(product);
        return {
            name: product.title || 'Товар',
            title: product.title || 'Товар',
            uid: String(product.uid),
            lid: String(product.uid),
            gen_uid: String(product.uid),
            recid: extractRecid(product),
            price: parseFloat(product.price) || 0,
            priceold: product.priceold ? parseFloat(product.priceold) : null,
            img: getImageUrl(product),
            sku: product.sku || '',
            url: product.url || window.location.href,
            inv: inv,
            pack_label: product.pack_label || '',
            pack_x: String(product.pack_x || 0),
            pack_y: String(product.pack_y || 0),
            pack_z: String(product.pack_z || 0),
            pack_m: String(product.pack_m || 0),
            part_uids: parsePartUids(product.partuids),
            quantity: 1,
            amount: parseFloat(product.price) || 0,
            ts: Date.now()
        };
    };

    const addToCart = async (product) => {
        const waitForCart = (maxAttempts) => {
            maxAttempts = maxAttempts || 50;
            return new Promise((resolve, reject) => {
                if (typeof window.tcart__addProduct === 'function') {
                    resolve();
                    return;
                }
                let attempts = 0;
                const check = () => {
                    if (typeof window.tcart__addProduct === 'function') {
                        resolve();
                    } else if (attempts < maxAttempts) {
                        attempts++;
                        setTimeout(check, 100);
                    } else {
                        reject(new Error('cart not loaded'));
                    }
                };
                check();
            });
        };

        try {
            const cartItem = buildCartItem(product);
            await waitForCart();
            window.tcart__addProduct(cartItem);
            return true;
        } catch (err) {
            console.error('[SEATS] Cart error:', err);
            return false;
        }
    };

  

    async function loadAllProducts() {
        let slice = 1;
        let hasMore = true;

        while (hasMore) {
            try {
                const url = new URL('https://store.tildaapi.com/api/getproductslist/');
                url.searchParams.set('storepartuid', CONFIG.storePartUid);
                url.searchParams.set('c', Date.now());
                url.searchParams.set('slice', slice);
                url.searchParams.set('size', String(CONFIG.productsPerRequest));

                const resp = await fetch(url.toString());
                if (!resp.ok) throw new Error('HTTP ' + resp.status);

                const data = await resp.json();
                const products = data && data.products ? data.products : [];

                if (products.length === 0) break;

                products.forEach(function(product) {
                    state.products.push(product);
                    if (product.sku) {
                        state.productsBySku[product.sku] = product;
                    }
                });

                hasMore = 'nextslice' in data;
                if (hasMore) slice++;

            } catch (err) {
                console.error('[SEATS] Products load error:', err);
                break;
            }
        }

        state.productsLoaded = true;
        tryInitMap();
    }

    function loadSVG() {
        fetch(CONFIG.svgUrl)
            .then(function(response) {
                if (!response.ok) throw new Error('SVG не загрузился ' + response.status);
                return response.text();
            })
            .then(function(svgText) {
                container.insertAdjacentHTML('beforeend', svgText);
                svgElement = container.querySelector('svg');

                if (svgElement) {
                    var vb = svgElement.getAttribute('viewBox');
                    svgElement.setAttribute('viewBox', vb || '0 0 800 600');
                    svgElement.style.width = '100%';
                    svgElement.style.height = 'auto';
                    svgElement.style.display = 'block';
                }

                state.svgLoaded = true;
                tryInitMap();
            })
            .catch(function(err) {
                console.error('[SEATS] ERROR', err);
                container.innerHTML = '<p style="color:red; text-align:center; padding:40px;">Не удалось загрузить схему</p>';
            });
    }

  
    function tryInitMap() {
        if (!state.svgLoaded) return;
        if (!state.productsLoaded) return;
        initSeatsMap();
    }

  
    function initSeatsMap() {
        if (state.isMapLoaded) return;

        setTimeout(function() {
            markSoldOutSeats();
            container.classList.add('load-map');
            attachClickHandlers();
            initZoom();
            initDrag();
            initScrollObserver();

            setTimeout(function() {
                checkCartHistory();
            }, 300);

            state.isMapLoaded = true;
        }, 300);
    }

    function markSoldOutSeats() {
        const foundSkus = new Set();

        state.products.forEach(function(product) {
            const sku = product.sku || '';
            const quant = getInventory(product);

            if (sku) {
                foundSkus.add(sku);

                if (quant === 0) {
                    const el = container.querySelector('#' + sku);
                    if (el) el.classList.add('sold-out');
                }
            }
        });

        const allSeats = container.querySelectorAll('[id]');
        allSeats.forEach(function(el) {
            const id = el.getAttribute('id');
            if (id && CONFIG.seatPrefixes.some(function(prefix) { return id.indexOf(prefix) === 0; })) {
                if (!foundSkus.has(id)) {
                    el.classList.add('sold-out');
                }
            }
        });
    }

    function findSeatParent(startElement) {
        let el = startElement;
        while (el && el !== container) {
            if (el.id && CONFIG.seatPrefixes.some(function(prefix) { return el.id.indexOf(prefix) === 0; })) {
                return el;
            }
            el = el.parentElement;
        }
        return null;
    }

    function attachClickHandlers() {
        container.addEventListener('click', function(e) {
  
            if (state.isDragging) return;

            const seatEl = findSeatParent(e.target);
            if (!seatEl) return;
            handleSeatClick(seatEl);
        });
    }

    function handleSeatClick(el) {
        const chairId = el.getAttribute('id');
        var chairClass = el.getAttribute('class') || '';

        if (chairClass.indexOf('sold-out') !== -1) return;

        if (chairClass.indexOf('active-chair') !== -1) {
            el.classList.remove('active-chair');
            removeFromCartBySku(chairId);
        } else {
            addPlaceToCart(el, chairId);
        }
    }


    function addPlaceToCart(em, seatId) {
        const product = state.productsBySku[seatId];

        if (!product) return;

        const quant = getInventory(product);
        if (quant === 0) return;

        addToCart(product).then(function(success) {
            if (success) {
                em.classList.add('active-chair');

                setTimeout(function() {
                    if (typeof tcart__reDrawProducts === 'function') {
                        tcart__reDrawProducts();
                    }
                }, 200);
            }
        });
    }

    function removeFromCartBySku(sku) {
        const productsInCart = document.querySelectorAll('.t706__product');

        productsInCart.forEach(function(product) {
            const skuOption = product.querySelector('.t706__product-title__option');
            if (skuOption && skuOption.innerText === sku) {
                const delBtn = product.querySelector('.t706__product-del');
                if (delBtn) delBtn.click();
            }
        });
    }

    function checkCartHistory() {
        if (typeof tcart === 'undefined' || !tcart || tcart.total <= 0) return;

        if (typeof tcart__reDrawProducts === 'function') {
            tcart__reDrawProducts();
        }

        const productsInCart = document.querySelectorAll('.t706__product');

        productsInCart.forEach(function(product) {
            var skuEl = product.querySelector('.t706__product-title__option');
            var sku = skuEl ? skuEl.innerText : '';

            if (sku) {
                const chair = container.querySelector('#' + sku);
                const prod = state.productsBySku[sku];
                const quant = prod ? getInventory(prod) : null;

                if (chair) {
                    if (quant === 0) {
                        state.historyProductsArr.push(sku);
                    } else {
                        chair.classList.add('active-chair');
                    }
                }
            }
        });

        if (state.historyProductsArr.length > 0) {
            removeHistoryProducts();
        }
    }

    function removeHistoryProducts() {
        for (let i = 0; i < state.historyProductsArr.length; i++) {
            const productsInCart = document.querySelectorAll('.t706__product');
            productsInCart.forEach(function(product) {
                var skuEl = product.querySelector('.t706__product-title__option');
                var sku = skuEl ? skuEl.innerText : '';
                if (sku === state.historyProductsArr[i]) {
                    const delBtn = product.querySelector('.t706__product-del');
                    if (delBtn) delBtn.click();
                }
            });
        }
        state.historyProductsArr = [];
    }

 
    document.addEventListener('click', function(event) {
        if (event.target.closest('.t706__product-del')) {
            const cartProduct = event.target.closest('.t706__product');
            if (!cartProduct) return;

            var skuEl = cartProduct.querySelector('.t706__product-title__option');
            var sku = skuEl ? skuEl.innerText : '';
            if (!sku) return;

            const chair = container.querySelector('#' + sku);
            if (chair) chair.classList.remove('active-chair');
        }
    });

   
    function initZoom() {
        const btnIn = zoomControls.querySelector('.zoom-in');
        const btnOut = zoomControls.querySelector('.zoom-out');
        const btnReset = zoomControls.querySelector('.zoom-reset');

        btnIn.addEventListener('click', function(e) {
            e.stopPropagation();
            zoomIn();
        });

        btnOut.addEventListener('click', function(e) {
            e.stopPropagation();
            zoomOut();
        });

        btnReset.addEventListener('click', function(e) {
            e.stopPropagation();
            zoomReset();
        });
    }

    function zoomIn() {
        if (state.zoomIndex < CONFIG.zoomLevels.length - 1) {
            state.zoomIndex++;
            applyZoom();
        }
    }

    function zoomOut() {
        if (state.zoomIndex > 0) {
            state.zoomIndex--;
            applyZoom();
        }
    }

    function zoomReset() {
        state.zoomIndex = 0;
        applyZoom();
    }

    function applyZoom() {
        const width = CONFIG.zoomLevels[state.zoomIndex];
        container.style.width = width + '%';
        centerScroll();
    }

    function centerScroll() {
        const scrollLeft = (container.scrollWidth - viewport.clientWidth) / 2;
        viewport.scrollLeft = Math.max(0, scrollLeft);

        const scrollTop = (container.scrollHeight - viewport.clientHeight) / 2;
        viewport.scrollTop = Math.max(0, scrollTop);
    }

    function initScrollObserver() {
        let isVisible = false;
    
        function checkVisibility() {
            const rect = viewport.getBoundingClientRect();
            const viewportHeight = window.innerHeight;
    
          
            const threshold = rect.height * 0.2;
    
            const topInView = rect.top < viewportHeight - threshold;
            const bottomInView = rect.bottom > +threshold;
    
            const shouldShow = topInView && bottomInView;
    
            if (shouldShow && !isVisible) {
                zoomControls.classList.add('visible');
                isVisible = true;
            } else if (!shouldShow && isVisible) {
                zoomControls.classList.remove('visible');
                isVisible = false;
            }
        }
    
        window.addEventListener('scroll', checkVisibility, { passive: true });
        window.addEventListener('resize', checkVisibility, { passive: true });
    
        checkVisibility();
    }

   
    function initDrag() {

        viewport.addEventListener('mousedown', onDragStart);
        document.addEventListener('mousemove', onDragMove);
        document.addEventListener('mouseup', onDragEnd);

    
        viewport.addEventListener('touchstart', onDragStart, { passive: false });
        document.addEventListener('touchmove', onDragMove, { passive: false });
        document.addEventListener('touchend', onDragEnd);
    }

    function onDragStart(e) {
       
        if (e.target.closest && e.target.closest('.zoom-btn')) return;
        if (findSeatParent(e.target)) return;

        e.preventDefault();

        state.isDragging = false; 

        const clientX = e.touches ? e.touches[0].clientX : e.clientX;
        const clientY = e.touches ? e.touches[0].clientY : e.clientY;

        state.dragStartX = clientX;
        state.dragStartY = clientY;
        state.scrollStartX = viewport.scrollLeft;
        state.scrollStartY = viewport.scrollTop;

     
        state.dragTracking = true;

        viewport.style.cursor = 'grabbing';
    }

    function onDragMove(e) {
        if (!state.dragTracking) return;

        const clientX = e.touches ? e.touches[0].clientX : e.clientX;
        const clientY = e.touches ? e.touches[0].clientY : e.clientY;

        const dx = clientX - state.dragStartX;
        const dy = clientY - state.dragStartY;

       
        if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
            state.isDragging = true;
        }

        if (state.isDragging) {
            e.preventDefault();
            viewport.scrollLeft = state.scrollStartX - dx;
            viewport.scrollTop = state.scrollStartY - dy;
        }
    }

    function onDragEnd(e) {
        if (!state.dragTracking) return;

        state.dragTracking = false;
        viewport.style.cursor = 'grab';

       
        setTimeout(function() {
            state.isDragging = false;
        }, 50);
    }

  
    loadSVG();
    loadAllProducts();

});
</script>


<style>

#svg-viewport {
    width: 100%;
    overflow: hidden;
    position: relative;
    background: #f5f5f5;
    cursor: grab;
    overflow-x: auto;
    overflow-y: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch; 
}

#svg-viewport::-webkit-scrollbar {
    display: none;
}

#svg-viewport:active {
    cursor: grabbing;
}


#svg-container {
    width: 100%;
    position: relative;
  
}

#svg-container > svg {
    width: 100%;
    height: auto;
    display: block;
    shape-rendering: geometricPrecision;
    opacity: 0.2;
}


#svg-container.load-map > svg{
    opacity: 1;
}


#svg-container:not(.load-map):after {
    content: '';
    width: 100%;
    height: 100%;
    background-image: url(https://static.tildacdn.com/tild6136-3066-4630-a462-376665323265/nopic_image.gif);
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 150px;
    opacity: 0.5;
    pointer-events: none;
}

#svg-container.load-map:after {
    display: none;
}

.zoom-controls {
    position: fixed;
    top: 75px;
    left: 15px;
    z-index: 100;
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: fit-content;

    opacity: 0;
    visibility: hidden;
    transform: translateY(-10px);
    transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
    pointer-events: none;
}

.zoom-controls.visible {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    pointer-events: auto;
}

.zoom-btn {
    width: 30px;
    height: 30px;
    border: none;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.95);
    color: #333;
    font-size: 22px;
    font-weight: 600;
    cursor: pointer;
    box-shadow: 0 2px 10px rgba(0,0,0,0.15);
    transition: all 0.15s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    background-size: 38%;
    background-repeat: no-repeat;
    background-position: center;
}

.zoom-btn:hover {
    transform: scale(1.08);
    box-shadow: 0 4px 14px rgba(0,0,0,0.2);
}

button.zoom-btn.zoom-in {
    background-image: url(https://static.tildacdn.com/tild3238-3735-4233-a339-653462626239/zoom-in.svg);
}

button.zoom-btn.zoom-out {
    background-image: url(https://static.tildacdn.com/tild3937-6531-4533-b039-323732613933/zoom-out.svg);
}

button.zoom-btn.zoom-reset {
    background-image: url(https://static.tildacdn.com/tild3166-6165-4466-b738-626334353666/reload.svg);
}


.zoom-btn:active {
    transform: scale(0.95);
}

.zoom-reset {
    font-size: 18px;
}

#svg-container svg [id^="sts_"]
{
    cursor: pointer;
    pointer-events: all;
}

@media screen and (min-width: 640px) {
    #svg-container svg [id^="sts_"]:hover
    {
        fill: #fff;
    }
}

#svg-container svg .active-chair {
    fill: #aaec5d !important;
}

#svg-container svg .sold-out {
    fill: #ffb49c !important;
    opacity: 0.4;
    pointer-events: none;
    cursor: not-allowed;
}


.t706__bubble {
    display: none !important;
}

.t706__carticon-counter.js-carticon-counter:after {
    content: " бил.";
}

.t706__carticon-counter.js-carticon-counter {
    font-size: 14px;
}


@media screen and (max-width: 640px) {
    .zoom-controls {
        top: 10px;
        left: 10px;
    }

    .zoom-btn {
        width: 40px;
        height: 40px;
        font-size: 20px;
    }

    .zoom-reset {
        font-size: 16px;
    }
}
</style>
Made on
Tilda