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