Подробная инструкция по версии 1.1.0: пороги и категории, распределение подарков по уровням, переадресация с карточки подарка, настраиваемая кнопка акции, минификация и установка кода в HEAD.
Код считает товары в корзине без бесплатного подарка.
Порог разблокирует доступные карточки подарков.
Можно посмотреть описание и галерею каждого подарка.
Выбранная позиция добавляется по цене 0 ₽ и в количестве 1.
Сначала настройте блок CFG по шагам ниже. Затем нажмите «Скопировать» и установите весь код целиком — вместе с тегами <style> и <script>.
<style>
#tilda-gift-picker-host {
margin: 18px 0 20px;
font-family: "Segoe UI", Arial, sans-serif;
}
.tgp-wrap {
background: #fff;
}
.tgp-head {
display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: 8px;
margin-bottom: 12px;
flex-direction: column;
}
.tgp-title {
margin: 0;
font-size: 18px;
line-height: 1.2;
font-weight: 700;
}
.tgp-note {
margin: 4px 0 0;
font-size: 13px;
line-height: 1.35;
opacity: 0.85;
}
.tgp-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.tgp-card {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 12px;
overflow: hidden;
background: #f1f1f1;
position: relative;
min-height: 220px;
}
.tgp-card.locked {
opacity: 0.88;
}
.tgp-lock-mask {
position: absolute;
inset: 0;
background: rgba(90, 90, 90, 0.45);
pointer-events: none;
z-index: 1;
}
.tgp-media {
position: relative;
background: #f4f4f4;
height: 100%;
isolation: isolate;
}
.tgp-photo {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
overflow: hidden;
z-index: 0;
}
.tgp-photo img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
image-rendering: auto;
-ms-interpolation-mode: bicubic;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
.tgp-radio {
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
position: absolute;
top: 8px;
left: 8px;
width: 26px;
height: 26px;
border-radius: 50%;
border: 2px solid #fff;
background: rgba(0, 0, 0, 0.28);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.24);
cursor: pointer;
padding: 0;
z-index: 2;
outline: none;
display: block;
}
.tgp-radio:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.tgp-card.selected .tgp-radio {
background: #fff;
border-color: #fff;
}
.tgp-card.selected .tgp-radio::after {
content: "";
display: block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #111;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.tgp-info-float {
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
position: absolute;
right: 8px;
top: 8px;
border: 0;
border-radius: 50%;
background: rgba(0, 0, 0, 0.72);
color: #fff;
font-size: 12px;
line-height: 1;
width: 6px;
height: 6px;
padding: 9px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 4;
outline: none;
box-sizing: content-box;
font-weight: 700;
}
.tgp-caption {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 12px 10px 10px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0.06));
color: #fff;
pointer-events: none;
z-index: 3;
}
.tgp-card-title {
margin: 0;
font-size: 11px;
line-height: 1.3;
font-weight: 600;
color: #fff;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
min-height: 28px;
max-height: 28px;
}
.tgp-lock-text {
margin: 0;
font-size: 11px;
line-height: 1.2;
font-weight: 400;
margin-top: 5px;
opacity: 0.98;
}
.tgp-promo {
margin-top: 16px;
display: flex;
justify-content: center;
}
.tgp-promo-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 176px;
min-height: 32px;
padding: 6px 12px;
background: #5f5f5f;
border-radius: 9px;
color: #fff !important;
text-decoration: none;
font-size: 12px;
line-height: 1;
font-weight: 500;
transition: background-color 0.2s ease;
}
.tgp-promo-link:hover {
background: #4f4f4f;
}
.tgp-foot {
margin: 6px 0 0;
font-size: 12px;
line-height: 1.35;
opacity: 0.75;
}
.tgp-empty {
font-size: 14px;
line-height: 1.35;
opacity: 0.8;
padding: 6px 0;
}
.tgp-modal {
position: fixed;
inset: 0;
z-index: 999999;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.66);
}
.tgp-modal.open {
display: flex;
}
.tgp-dialog {
width: min(940px, 94vw);
max-height: min(88vh, 780px);
background: #fff;
border-radius: 14px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.tgp-dialog-head {
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
padding: 12px 14px;
}
.tgp-dialog-title {
margin: 0;
font-size: 18px;
line-height: 1.2;
font-weight: 700;
flex: 1;
}
.tgp-dialog-close {
border: 0;
background: transparent;
cursor: pointer;
font-size: 28px;
line-height: 1;
width: 34px;
height: 34px;
}
.tgp-dialog-body {
overflow: auto;
padding: 14px;
}
.tgp-gallery-main {
width: 100%;
border-radius: 10px;
background: #f4f4f4;
overflow: hidden;
}
.tgp-gallery-main img {
width: 100%;
max-height: 56vh;
object-fit: contain;
display: block;
background: #f4f4f4;
cursor: zoom-in;
}
.tgp-gallery-thumbs {
margin-top: 10px;
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 2px;
}
.tgp-thumb {
width: 84px;
min-width: 84px;
height: 84px;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 0;
cursor: pointer;
overflow: hidden;
background: #fff;
}
.tgp-thumb.active {
border-color: #111;
}
.tgp-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.tgp-full-desc {
margin: 14px 0 0;
font-size: 15px;
line-height: 1.45;
white-space: pre-wrap;
}
.tgp-lightbox {
position: fixed;
inset: 0;
z-index: 1000000;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.92);
padding: 12px;
}
.tgp-lightbox.open {
display: flex;
}
.tgp-lightbox img {
max-width: 96vw;
max-height: 96vh;
width: auto;
height: auto;
object-fit: contain;
display: block;
}
.tgp-lightbox-close {
position: absolute;
top: 8px;
right: 8px;
border: 0;
background: rgba(255, 255, 255, 0.2);
color: #fff;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 24px;
line-height: 1;
cursor: pointer;
}
@supports (-webkit-touch-callout: none) {
.tgp-photo img {
transform: none;
-webkit-transform: none;
backface-visibility: visible;
-webkit-backface-visibility: visible;
}
}
.tgp-cart-gift .t706__product-plusminus {
display: none !important;
}
@media (max-width: 767px) {
.t706__sidebar-content.t706__sidebar-content_showed #tilda-gift-picker-host {
padding-left: 20px;
padding-right: 20px;
box-sizing: border-box;
}
.tgp-head {
margin-bottom: 10px;
}
.tgp-grid {
display: flex;
overflow-x: auto;
gap: 10px;
padding-bottom: 4px;
scroll-snap-type: x proximity;
scroll-behavior: smooth;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: auto;
}
.tgp-card {
min-width: 60%;
min-height: 320px;
scroll-snap-align: start;
scroll-snap-stop: always;
flex: 0 0 60%;
}
.tgp-card-title {
font-size: 13px;
min-height: 20px;
max-height: 20px;
}
.tgp-dialog {
width: 100vw;
max-height: 100%;
height: 100vh;
height: 100dvh;
min-height: 100svh;
border-radius: 0;
}
}
</style>
<script>
(function () {
"use strict";
const CFG = {
DEBUG: false,
POLL_MS: 300,
CACHE_KEY: "tilda_gift_picker_catalog_v2",
CACHE_TTL_MS: 6 * 60 * 60 * 1000,
HOST_ID: "tilda-gift-picker-host",
MODAL_ID: "tilda-gift-picker-modal",
GIFT_SKU_PREFIX: "GIFT_PICKER_",
THRESHOLDS: {
1: 10000,
2: 15000,
3: 20000,
},
// Заполните ваши реальные ID разделов каталога с подарками.
// Логика: 3 товара для каждого уровня, итого 9 товаров.
GIFT_PARTS: [
{ tier: 1, storepartuid: 995491995622 },
{ tier: 2, storepartuid: 995491995622 },
{ tier: 3, storepartuid: 995491995622 },
],
// Если одна общая категория подарков, задайте UID подарков по уровням.
// Тогда уровень будет определяться по этим спискам, а не по tier в GIFT_PARTS.
// Пример: в каждом массиве по 3 uid.
GIFT_UIDS_BY_TIER: {
1: [
788376359312, 792960335582, 445978033312
],
2: [
150485787322, 166462998072, 192163347642
],
3: [
844229180832, 889114485142, 154243224612
],
},
TEXTS: {
title: "Подарки за сумму заказа",
choose: "Выбрать",
chosen: "Выбрано",
details: "Подробнее",
unavailable: "Недоступно",
loading: "Загружаем подарки...",
noGifts: "Подарки не найдены. Проверьте CFG.GIFT_PARTS и CFG.GIFT_UIDS_BY_TIER.",
noConfig: "Не заданы ID разделов подарков.",
tip: "Выберите 1 подарок из списка ниже",
needMorePrefix: "Добавьте до порога: ",
yourSumPrefix: "Сумма товаров в корзине: ",
levelPrefix: "Доступный уровень подарков: ",
lockedSuffix: "от",
alertNoCart: "Функции корзины Tilda недоступны.",
},
};
const CART_OPEN_CLASSES = [
"t706__body_cartsidebarshowed",
"t706__body_cartwinshowed",
"t706__body_cartpageshowed",
];
const state = {
gifts: [],
byUid: new Map(),
loading: false,
loaded: false,
loadError: "",
timer: null,
isCartOpen: false,
lastRenderKey: "",
modal: {
uid: "",
imageIndex: 0,
},
syncingGiftQty: false,
};
const log = (...args) => {
if (CFG.DEBUG) console.log("[GIFT-PICKER]", ...args);
};
const warn = (...args) => {
console.warn("[GIFT-PICKER]", ...args);
};
function safeJsonParse(value) {
try {
return JSON.parse(value);
} catch (_) {
return null;
}
}
function num(value) {
if (value == null) return 0;
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
const str = String(value)
.trim()
.replace(/\u00A0/g, " ")
.replace(/[^\d,.\- ]+/g, "")
.replace(/\s+/g, "")
.replace(",", ".");
const parsed = parseFloat(str);
return Number.isFinite(parsed) ? parsed : 0;
}
function formatMoney(value) {
const n = Math.round(num(value));
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " ₽";
}
function formatMoneyShortR(value) {
const n = Math.round(num(value));
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " р";
}
function escapeHtml(text) {
return String(text || "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function onReady(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 0);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
function stripHtml(raw) {
return String(raw || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}
function shortText(text, maxLen) {
const clean = String(text || "").trim();
if (!clean) return "";
if (clean.length <= maxLen) return clean;
return clean.slice(0, maxLen).trim() + "...";
}
function parseGallery(galleryRaw) {
const parsed = safeJsonParse(galleryRaw);
if (!Array.isArray(parsed)) return [];
return parsed
.map((item) => (item && item.img ? String(item.img) : ""))
.filter(Boolean);
}
function getConfiguredParts() {
const rows = (CFG.GIFT_PARTS || []).filter((item) => Number(item && item.storepartuid) > 0);
const seen = new Set();
return rows.filter((item) => {
const key = String(Number(item.storepartuid));
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function buildGiftUidTierIndex() {
const source = CFG.GIFT_UIDS_BY_TIER || {};
const tiers = [1, 2, 3];
const map = new Map();
tiers.forEach((tier) => {
const list = Array.isArray(source[tier]) ? source[tier] : [];
list.forEach((rawUid) => {
const uid = Number(rawUid);
if (!Number.isFinite(uid) || uid <= 0) return;
const key = String(uid);
const tierList = map.get(key) || [];
if (!tierList.includes(tier)) tierList.push(tier);
tierList.sort((a, b) => a - b);
map.set(key, tierList);
});
});
return map;
}
function getMinTierForUid(uid, giftUidTierIndex) {
if (!giftUidTierIndex || !giftUidTierIndex.size) return null;
const list = giftUidTierIndex.get(String(uid));
if (!Array.isArray(list) || !list.length) return null;
return Math.min.apply(null, list);
}
function isUidAllowedForUnlockedTier(uid, unlockedTier, giftUidTierIndex) {
if (!giftUidTierIndex || !giftUidTierIndex.size) return null;
const list = giftUidTierIndex.get(String(uid));
if (!Array.isArray(list) || !list.length) return false;
return list.some((tier) => tier <= unlockedTier);
}
function getCache() {
const raw = localStorage.getItem(CFG.CACHE_KEY);
if (!raw) return null;
const parsed = safeJsonParse(raw);
if (!parsed || !parsed.ts || !Array.isArray(parsed.gifts)) return null;
if (Date.now() - parsed.ts > CFG.CACHE_TTL_MS) return null;
return parsed.gifts;
}
function saveCache(gifts) {
try {
localStorage.setItem(CFG.CACHE_KEY, JSON.stringify({ ts: Date.now(), gifts }));
} catch (_) {}
}
async function apiGetProductsList(storepartuid, slice) {
const url = `https://store.tildaapi.com/api/getproductslist/?size=500&storepartuid=${storepartuid}&slice=${slice}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`API ${response.status} for storepartuid=${storepartuid}`);
}
return response.json();
}
async function loadAllFromPart(part, giftUidTierIndex) {
let slice = 1;
const rows = [];
const hasUidTierRules = giftUidTierIndex && giftUidTierIndex.size > 0;
while (slice) {
const data = await apiGetProductsList(part.storepartuid, slice);
const products = Array.isArray(data.products) ? data.products : [];
products.forEach((product) => {
const uid = Number(product && product.uid);
if (!Number.isFinite(uid)) return;
const key = String(uid);
const images = parseGallery(product.gallery);
const descr = stripHtml(product.descr || product.text || "");
const minTier = getMinTierForUid(key, giftUidTierIndex);
const gift = {
uid,
tier: hasUidTierRules ? Number(minTier || 99) : (Number(part.tier) || 1),
partuid: Number(part.storepartuid),
title: String(product.title || `UID ${uid}`),
shortDescr: shortText(descr, 140),
fullDescr: descr,
images,
cover: images[0] || "",
url: String(product.url || ""),
sku: String(product.sku || ""),
partuids: String(product.partuids || ""),
};
rows.push(gift);
});
slice = data.nextslice ? data.nextslice : null;
}
return rows;
}
function dedupeByUid(gifts) {
const map = new Map();
gifts.forEach((gift) => {
if (!gift || !gift.uid) return;
const key = String(gift.uid);
if (!map.has(key)) {
map.set(key, gift);
return;
}
const existing = map.get(key);
if ((gift.tier || 99) < (existing.tier || 99)) {
map.set(key, gift);
}
});
return Array.from(map.values());
}
async function ensureCatalogLoaded() {
if (state.loaded || state.loading) return;
const configuredParts = getConfiguredParts();
const giftUidTierIndex = buildGiftUidTierIndex();
const hasUidTierRules = giftUidTierIndex.size > 0;
if (!configuredParts.length) {
state.loaded = true;
state.loadError = CFG.TEXTS.noConfig;
return;
}
state.loading = true;
try {
const cached = getCache();
if (cached) {
const cachedGifts = (hasUidTierRules
? cached
.map((gift) => ({
...gift,
tier: Number(getMinTierForUid(gift.uid, giftUidTierIndex) || 99),
}))
: cached
).sort((a, b) => (a.tier - b.tier) || a.title.localeCompare(b.title, "ru"));
state.gifts = cachedGifts;
state.byUid = new Map(cachedGifts.map((gift) => [String(gift.uid), gift]));
state.loaded = true;
state.loadError = cachedGifts.length ? "" : CFG.TEXTS.noGifts;
return;
}
const loadedChunks = await Promise.all(
configuredParts.map((part) => loadAllFromPart(part, giftUidTierIndex))
);
const gifts = dedupeByUid(loadedChunks.flat())
.sort((a, b) => (a.tier - b.tier) || a.title.localeCompare(b.title, "ru"));
state.gifts = gifts;
state.byUid = new Map(gifts.map((gift) => [String(gift.uid), gift]));
state.loaded = true;
state.loadError = gifts.length ? "" : CFG.TEXTS.noGifts;
saveCache(gifts);
} catch (error) {
warn(error);
state.loaded = true;
state.loadError = "Ошибка загрузки подарков из каталога";
} finally {
state.loading = false;
}
}
function getCartProducts() {
if (!window.tcart || !Array.isArray(window.tcart.products)) return [];
return window.tcart.products;
}
function getProductUid(product) {
const value = product && (product.uid || product.productid || product.id || product.gen_uid || "");
return String(value || "");
}
function isGiftProduct(product) {
if (!product) return false;
const sku = String(product.sku || "");
if (sku.startsWith(CFG.GIFT_SKU_PREFIX)) return true;
const uid = getProductUid(product);
if (!uid) return false;
if (state.byUid.has(uid) && num(product.price) === 0) return true;
return false;
}
function getLineAmount(product) {
const byAmount = num(product && product.amount);
if (byAmount > 0) return byAmount;
const price = num(product && product.price);
const qty = Math.max(1, num(product && product.quantity));
return price * qty;
}
function getSubtotalWithoutGift() {
const cartSubtotal = num(window.tcart && window.tcart.prodamount);
if (cartSubtotal > 0) return cartSubtotal;
// Fallback: ручной пересчет, если tcart.prodamount еще не инициализирован.
return getCartProducts()
.filter((product) => num(product.quantity) > 0)
.filter((product) => !isGiftProduct(product))
.reduce((sum, product) => sum + getLineAmount(product), 0);
}
function getUnlockedTier(subtotal) {
if (subtotal >= CFG.THRESHOLDS[3]) return 3;
if (subtotal >= CFG.THRESHOLDS[2]) return 2;
if (subtotal >= CFG.THRESHOLDS[1]) return 1;
return 0;
}
function getSelectedGiftUidInCart() {
const products = getCartProducts();
for (let i = 0; i < products.length; i += 1) {
const product = products[i];
if (!isGiftProduct(product)) continue;
const sku = String(product.sku || "");
if (sku.startsWith(CFG.GIFT_SKU_PREFIX)) {
return sku.slice(CFG.GIFT_SKU_PREFIX.length);
}
const uid = getProductUid(product);
if (uid && state.byUid.has(uid)) return uid;
}
return "";
}
function lockGiftQuantityControlsInCart() {
const products = getCartProducts();
if (!products.length) return;
const productRows = Array.from(
document.querySelectorAll(
".t706__cartwin-products .t706__product, .t706__sidebar-products .t706__product, .t706__cartpage-products .t706__product"
)
);
if (!productRows.length) return;
let quantityChanged = false;
products.forEach((product) => {
if (!isGiftProduct(product)) return;
if (num(product.quantity) !== 1) {
product.quantity = 1;
quantityChanged = true;
}
});
productRows.forEach((row, index) => {
const product = products[index];
const isGift = !!product && isGiftProduct(product);
row.classList.toggle("tgp-cart-gift", isGift);
if (!isGift) return;
const plusMinus = row.querySelector(".t706__product-plusminus");
if (plusMinus) plusMinus.style.display = "none";
row.querySelectorAll(".t706__product-plus, .t706__product-minus").forEach((button) => {
button.style.pointerEvents = "none";
});
});
if (quantityChanged && !state.syncingGiftQty) {
state.syncingGiftQty = true;
try {
redrawCartUi();
} finally {
state.syncingGiftQty = false;
}
}
}
function redrawCartUi() {
try {
if (typeof window.tcart__saveLocalObj === "function") window.tcart__saveLocalObj();
} catch (_) {}
try {
if (typeof window.tcart__reDrawProducts === "function") window.tcart__reDrawProducts();
} catch (_) {}
try {
if (typeof window.tcart__reDrawTotal === "function") window.tcart__reDrawTotal();
} catch (_) {}
try {
if (typeof window.tcart__reDrawCartIcon === "function") window.tcart__reDrawCartIcon();
} catch (_) {}
try {
if (typeof window.tcart__updateTotalProductsinCartObj === "function") window.tcart__updateTotalProductsinCartObj();
} catch (_) {}
setTimeout(() => {
lockGiftQuantityControlsInCart();
}, 0);
}
function removeGiftProductsFromCart() {
const products = getCartProducts();
if (!products.length) return;
for (let index = products.length - 1; index >= 0; index -= 1) {
const product = products[index];
if (!isGiftProduct(product)) continue;
let removed = false;
if (typeof window.tcart__delProduct === "function") {
try {
window.tcart__delProduct(index);
removed = true;
} catch (_) {}
}
if (!removed && typeof window.tcart__removeProduct === "function") {
try {
window.tcart__removeProduct(index);
removed = true;
} catch (_) {}
}
if (!removed) {
products.splice(index, 1);
}
}
redrawCartUi();
}
function addGiftToCart(gift) {
if (typeof window.tcart__addProduct !== "function") {
alert(CFG.TEXTS.alertNoCart);
return;
}
const productData = {
name: gift.title,
price: 0,
img: gift.cover,
gen_uid: String(gift.uid),
lid: String(gift.uid),
pack_label: "",
pack_m: "",
pack_x: "",
pack_y: "",
pack_z: "",
sku: gift.sku || "",
part_uids: gift.partuids || "",
uid: String(gift.uid),
productid: String(gift.uid),
id: String(gift.uid),
url: gift.url || location.href,
quantity: 1,
};
window.tcart__addProduct(productData);
redrawCartUi();
}
function isGiftSelectableForTier(giftUid, unlockedTier, giftUidTierIndex) {
const byRules = isUidAllowedForUnlockedTier(giftUid, unlockedTier, giftUidTierIndex);
if (byRules !== null) return byRules;
const gift = state.byUid.get(String(giftUid));
return !!(gift && gift.tier <= unlockedTier);
}
function chooseGift(uid, unlockedTier, giftUidTierIndex) {
const gift = state.byUid.get(String(uid));
if (!gift) return;
if (!isGiftSelectableForTier(uid, unlockedTier, giftUidTierIndex)) return;
removeGiftProductsFromCart();
addGiftToCart(gift);
forceRender();
}
function getGiftRequiredTier(giftUid, giftUidTierIndex) {
const fromRules = getMinTierForUid(giftUid, giftUidTierIndex);
if (Number.isFinite(fromRules) && fromRules > 0) return fromRules;
const gift = state.byUid.get(String(giftUid));
const tier = Number(gift && gift.tier);
if (Number.isFinite(tier) && tier > 0) return tier;
return 1;
}
function resolveGiftFromText(requiredTier) {
const safeTier = Math.max(1, Math.min(3, Number(requiredTier) || 1));
const threshold = CFG.THRESHOLDS[safeTier] || CFG.THRESHOLDS[1] || 0;
return `от ${formatMoneyShortR(threshold)}`;
}
function getCurrentThresholdFloor(subtotal) {
if (subtotal >= CFG.THRESHOLDS[3]) return CFG.THRESHOLDS[3];
if (subtotal >= CFG.THRESHOLDS[2]) return CFG.THRESHOLDS[2];
if (subtotal >= CFG.THRESHOLDS[1]) return CFG.THRESHOLDS[1];
return CFG.THRESHOLDS[1];
}
function getLevelLabel(unlockedTier) {
if (unlockedTier <= 0) return "0";
return `${unlockedTier} из 3`;
}
function getMinLockedThreshold(unlockedTier) {
if (unlockedTier < 1) return CFG.THRESHOLDS[1];
if (unlockedTier < 2) return CFG.THRESHOLDS[2];
if (unlockedTier < 3) return CFG.THRESHOLDS[3];
return 0;
}
function getCartProductsRoot() {
return (
document.querySelector(".t706__cartwin-products") ||
document.querySelector(".t706__sidebar-products") ||
document.querySelector(".t706__cartpage-products")
);
}
function getCartBottomAnchor() {
const openedCart = document.querySelector(".t706__cartwin.t706__cartwin_showed");
if (openedCart) {
const openedBottom = openedCart.querySelector(".t706__cartwin-bottom");
if (openedBottom) return openedBottom;
}
return (
document.querySelector(".t706__cartwin_showed .t706__cartwin-bottom") ||
document.querySelector(".t706__cartwin .t706__cartwin-bottom") ||
document.querySelector(".t706__cartpage .t706__cartwin-bottom") ||
document.querySelector(".t706__cartwin-bottom")
);
}
function isElementVisible(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden") return false;
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
}
function getCartPageTotalsAnchor() {
const totals = Array.from(document.querySelectorAll(".t706__cartpage .t706__cartpage-totals"));
for (let i = 0; i < totals.length; i += 1) {
if (isElementVisible(totals[i])) return totals[i];
}
return null;
}
function ensureHost() {
const cartPageTotalsAnchor = getCartPageTotalsAnchor();
const anchor = getCartBottomAnchor();
const productsRoot = getCartProductsRoot();
let host = document.getElementById(CFG.HOST_ID);
if (!host) {
host = document.createElement("div");
host.id = CFG.HOST_ID;
}
if (cartPageTotalsAnchor && cartPageTotalsAnchor.parentElement) {
if (
host.parentElement !== cartPageTotalsAnchor.parentElement ||
host.nextElementSibling !== cartPageTotalsAnchor
) {
cartPageTotalsAnchor.insertAdjacentElement("beforebegin", host);
}
return host;
}
if (anchor && anchor.parentElement) {
if (host.parentElement !== anchor.parentElement || host.previousElementSibling !== anchor) {
anchor.insertAdjacentElement("afterend", host);
}
return host;
}
if (!productsRoot || !productsRoot.parentElement) return null;
if (host.parentElement !== productsRoot.parentElement || host.previousElementSibling !== productsRoot) {
productsRoot.insertAdjacentElement("afterend", host);
}
return host;
}
function ensureModal() {
let modal = document.getElementById(CFG.MODAL_ID);
if (modal) return modal;
modal = document.createElement("div");
modal.id = CFG.MODAL_ID;
modal.className = "tgp-modal";
modal.innerHTML = `
<div class="tgp-dialog" role="dialog" aria-modal="true" aria-label="Подарок">
<div class="tgp-dialog-head">
<h3 class="tgp-dialog-title"></h3>
<button type="button" class="tgp-dialog-close" aria-label="Закрыть">×</button>
</div>
<div class="tgp-dialog-body">
<div class="tgp-gallery-main"><img alt=""></div>
<div class="tgp-gallery-thumbs"></div>
<div class="tgp-full-desc"></div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
modal.querySelector(".tgp-dialog-close").addEventListener("click", closeModal);
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
const lightbox = ensureLightbox();
if (lightbox.classList.contains("open")) {
closeLightbox();
return;
}
if (modal.classList.contains("open")) closeModal();
});
return modal;
}
function ensureLightbox() {
let lightbox = document.getElementById("tgp-lightbox");
if (lightbox) return lightbox;
lightbox = document.createElement("div");
lightbox.id = "tgp-lightbox";
lightbox.className = "tgp-lightbox";
lightbox.innerHTML = `
<button type="button" class="tgp-lightbox-close" aria-label="Закрыть">×</button>
<img alt="">
`;
document.body.appendChild(lightbox);
lightbox.addEventListener("click", (event) => {
if (event.target === lightbox) closeLightbox();
});
lightbox.querySelector(".tgp-lightbox-close").addEventListener("click", closeLightbox);
return lightbox;
}
function openLightbox(src, alt) {
if (!src) return;
const lightbox = ensureLightbox();
const img = lightbox.querySelector("img");
img.src = String(src);
img.alt = String(alt || "");
lightbox.classList.add("open");
}
function closeLightbox() {
const lightbox = ensureLightbox();
lightbox.classList.remove("open");
}
function renderModalContent() {
const modal = ensureModal();
const gift = state.byUid.get(String(state.modal.uid));
if (!gift) return;
const titleNode = modal.querySelector(".tgp-dialog-title");
const mainImg = modal.querySelector(".tgp-gallery-main img");
const thumbs = modal.querySelector(".tgp-gallery-thumbs");
const descNode = modal.querySelector(".tgp-full-desc");
const images = Array.isArray(gift.images) && gift.images.length ? gift.images : [gift.cover].filter(Boolean);
const index = Math.min(Math.max(0, state.modal.imageIndex), Math.max(0, images.length - 1));
state.modal.imageIndex = index;
titleNode.textContent = gift.title;
mainImg.src = images[index] || "";
mainImg.alt = gift.title;
mainImg.onclick = () => openLightbox(images[index] || "", gift.title);
descNode.textContent = gift.fullDescr || "Описание отсутствует";
thumbs.innerHTML = images
.map((img, i) => {
const activeClass = i === index ? " active" : "";
return `<button class="tgp-thumb${activeClass}" type="button" data-img-index="${i}"><img src="${escapeHtml(img)}" alt="${escapeHtml(gift.title)}"></button>`;
})
.join("");
thumbs.querySelectorAll(".tgp-thumb").forEach((button) => {
button.addEventListener("click", () => {
state.modal.imageIndex = Number(button.getAttribute("data-img-index")) || 0;
renderModalContent();
});
});
}
function openModal(uid) {
if (!state.byUid.has(String(uid))) return;
state.modal.uid = String(uid);
state.modal.imageIndex = 0;
const modal = ensureModal();
renderModalContent();
modal.classList.add("open");
}
function closeModal() {
const modal = ensureModal();
modal.classList.remove("open");
}
function renderCards(gifts, selectedUid, unlockedTier, subtotal, giftUidTierIndex) {
return gifts
.map((gift) => {
const selected = String(selectedUid) === String(gift.uid);
const unlocked = isGiftSelectableForTier(gift.uid, unlockedTier, giftUidTierIndex);
const requiredTier = getGiftRequiredTier(gift.uid, giftUidTierIndex);
const fromText = resolveGiftFromText(requiredTier);
const cover = gift.cover || "";
return `
<article class="tgp-card${selected ? " selected" : ""}${unlocked ? "" : " locked"}" data-gift-uid="${gift.uid}">
<div class="tgp-media">
<div class="tgp-photo">
<img src="${escapeHtml(cover)}" alt="${escapeHtml(gift.title)}">
</div>
${unlocked ? "" : `<span class="tgp-lock-mask" aria-hidden="true"></span>`}
${unlocked ? `<button class="tgp-radio js-gift-pick" type="button" aria-label="Выбрать подарок" data-gift-uid="${gift.uid}"></button>` : ""}
<button class="tgp-info-float js-gift-open" type="button" data-gift-uid="${gift.uid}">i</button>
<div class="tgp-caption">
<h4 class="tgp-card-title">${escapeHtml(gift.title)}</h4>
<p class="tgp-lock-text">${escapeHtml(fromText)}</p>
</div>
</div>
</article>
`;
})
.join("");
}
function renderPromoButton() {
const cfg = CFG.PROMO_BUTTON || {};
if (!cfg.enabled || !cfg.url) return "";
return `
<div class="tgp-promo">
<a class="tgp-promo-link" href="${escapeHtml(cfg.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(cfg.text || "Подробнее")}</a>
</div>
`;
}
function renderHost(host, context) {
const { subtotal, unlockedTier, selectedUid, giftUidTierIndex } = context;
if (state.loading && !state.loaded) {
host.innerHTML = `<div class="tgp-wrap"><div class="tgp-empty">${escapeHtml(CFG.TEXTS.loading)}</div></div>`;
return;
}
if (state.loadError) {
host.innerHTML = `<div class="tgp-wrap"><div class="tgp-empty">${escapeHtml(state.loadError)}</div></div>`;
return;
}
if (!state.gifts.length) {
host.innerHTML = `<div class="tgp-wrap"><div class="tgp-empty">${escapeHtml(CFG.TEXTS.noGifts)}</div></div>`;
return;
}
const firstThreshold = CFG.THRESHOLDS[1] || 0;
const isBelowFirstThreshold = subtotal < firstThreshold;
const titleFrom = getCurrentThresholdFloor(subtotal);
const headTitle = isBelowFirstThreshold
? `Добавьте в корзину еще товары до ${formatMoneyShortR(firstThreshold)} и получите подарок`
: `${CFG.TEXTS.title} от ${formatMoneyShortR(titleFrom)}`;
const tipHtml = isBelowFirstThreshold
? ""
: `<p class="tgp-note">${escapeHtml(CFG.TEXTS.tip)}</p>`;
host.innerHTML = `
<section class="tgp-wrap">
<div class="tgp-head">
<div>
<h3 class="tgp-title">${escapeHtml(headTitle)}</h3>
${tipHtml}
</div>
</div>
<div class="tgp-grid">
${renderCards(state.gifts, selectedUid, unlockedTier, subtotal, giftUidTierIndex)}
</div>
${renderPromoButton()}
</section>
`;
host.querySelectorAll(".js-gift-open").forEach((button) => {
button.addEventListener("click", () => {
const uid = button.getAttribute("data-gift-uid");
openModal(uid);
});
});
host.querySelectorAll(".js-gift-pick").forEach((button) => {
button.addEventListener("click", () => {
if (button.disabled) return;
const uid = button.getAttribute("data-gift-uid");
chooseGift(uid, unlockedTier, giftUidTierIndex);
});
});
host.querySelectorAll(".tgp-card").forEach((card) => {
card.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (target.closest(".js-gift-open") || target.closest(".js-gift-pick")) return;
const uid = card.getAttribute("data-gift-uid");
if (!uid) return;
chooseGift(uid, unlockedTier, giftUidTierIndex);
});
});
}
function shouldCartBeOpen() {
const className = document.body.className || "";
const openedByClass = CART_OPEN_CLASSES.some((token) => className.includes(token));
const cartPageVisible = !!document.querySelector(".t706__cartpage");
return openedByClass || cartPageVisible;
}
function renderCycle() {
if (!window.tcart || !Array.isArray(window.tcart.products)) return;
const host = ensureHost();
if (!host) return;
const subtotal = getSubtotalWithoutGift();
const unlockedTier = getUnlockedTier(subtotal);
const giftUidTierIndex = buildGiftUidTierIndex();
const selectedUid = getSelectedGiftUidInCart();
if (selectedUid) {
const selectedGift = state.byUid.get(String(selectedUid));
if (!selectedGift || !isGiftSelectableForTier(selectedUid, unlockedTier, giftUidTierIndex)) {
removeGiftProductsFromCart();
}
}
const freshSelectedUid = getSelectedGiftUidInCart();
const renderKey = [
Math.round(subtotal),
unlockedTier,
freshSelectedUid,
state.gifts.length,
state.loading ? 1 : 0,
state.loadError,
].join("|");
lockGiftQuantityControlsInCart();
if (renderKey === state.lastRenderKey) return;
state.lastRenderKey = renderKey;
renderHost(host, {
subtotal,
unlockedTier,
selectedUid: freshSelectedUid,
giftUidTierIndex,
});
}
function forceRender() {
state.lastRenderKey = "";
renderCycle();
}
function startLoop() {
if (state.timer) return;
state.timer = setInterval(() => {
ensureCatalogLoaded().catch((error) => warn(error));
renderCycle();
}, CFG.POLL_MS);
ensureCatalogLoaded().catch((error) => warn(error));
forceRender();
}
function stopLoop() {
if (!state.timer) return;
clearInterval(state.timer);
state.timer = null;
}
function observeCartVisibility() {
const observer = new MutationObserver(() => {
const currentlyOpen = shouldCartBeOpen();
if (currentlyOpen && !state.isCartOpen) {
state.isCartOpen = true;
startLoop();
return;
}
if (!currentlyOpen && state.isCartOpen) {
state.isCartOpen = false;
stopLoop();
return;
}
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ["class"],
});
state.isCartOpen = shouldCartBeOpen();
if (state.isCartOpen) startLoop();
}
onReady(() => {
ensureModal();
observeCartVisibility();
log("init complete");
});
})();
</script>
Сначала решите, сколько будет порогов суммы (в версии 1.0.0 их три) и откуда будут подгружаться подарки для каждого уровня.
Впишите три суммы порогов в THRESHOLDS — по возрастанию, без пробелов и знака ₽.
1.1Перейдите в Каталог товаров Tilda.
1.2Создайте категорию «Подарки» либо используйте любую уже существующую — и заведите в ней подарочные товары (или добавьте туда уже существующие).
1.3Находясь внутри этой категории, скопируйте её числовой ID — он отображается прямо в шапке категории в каталоге Тильды. Это и есть storepartuid.
1.4Пропишите этот ID в GIFT_PARTS для каждого уровня — можно использовать одну и ту же категорию для всех трёх уровней, а можно завести разные.
THRESHOLDS: {
1: 10000,
2: 15000,
3: 20000,
},
GIFT_PARTS: [
{ tier: 1, storepartuid: 702249631482 },
{ tier: 2, storepartuid: 702249631482 },
{ tier: 3, storepartuid: 702249631482 },
],Где tier — номер уровня порога, а storepartuid — категория, откуда будут подгружаться подарки для этого уровня. В примере выше это одна и та же категория «Подарки» — можно завести и разные категории под каждый уровень, если так удобнее для структуры каталога.



Теперь укажите, какие именно товары доступны на каждом из трёх уровней — через GIFT_UIDS_BY_TIER.
Откройте карточку товара-подарка в каталоге — числовой ID (product ID) указан в нижней части карточки.
2.2Впишите ID нужных товаров в массив соответствующего уровня — 1, 2 или 3.
GIFT_UIDS_BY_TIER: {
1: [
788376359312, 792960335582, 445978033312
],
2: [
150485787322, 166462998072, 192163347642
],
3: [
844229180832, 889114485142, 154243224612
],
},
Все подписи интерфейса собраны в объекте TEXTS. Меняйте только текст между кавычками, сохраняя имена параметров и запятые.
TEXTS: {
title: "Подарки за сумму заказа",
choose: "Выбрать",
chosen: "Выбрано",
details: "Подробнее",
unavailable: "Недоступно",
loading: "Загружаем подарки...",
noGifts: "Подарки не найдены.",
noConfig: "Не заданы ID разделов подарков.",
tip: "Выберите 1 подарок из списка ниже",
needMorePrefix: "Добавьте до порога: ",
yourSumPrefix: "Сумма товаров в корзине: ",
levelPrefix: "Доступный уровень подарков: ",
lockedSuffix: "от",
alertNoCart: "Функции корзины Tilda недоступны.",
},Тильда часто выводит в стандартных блоках каталога вообще все товары раздела — включая подарочные, даже с ценой 0 ₽. Отключить показ таких товаров штатно нельзя, поэтому лучше для каждого подарочного товара настроить переадресацию с его собственной страницы на общую страницу каталога.
Откройте карточку товара-подарка в каталоге → раздел с настройками страницы товара.
4.2Включите переадресацию (редирект) и укажите ссылкой страницу вашего каталога — так покупатель, случайно перешедший на страницу подарка напрямую, попадёт на витрину товаров, а не на отдельную карточку с ценой 0 ₽.

Кнопка со ссылкой на условия акции настраивается через PROMO_BUTTON — по умолчанию она скрыта.
PROMO_BUTTON: {
enabled: false,
url: "https://web4life.ru/inform",
text: "Подробнее об акции",
},Если кнопка нужна — включите её и укажите свою ссылку и текст:
PROMO_BUTTON: {
enabled: true,
url: "https://web4life.ru/inform",
text: "Подробнее об акции",
},Скопируйте настроенный код между тегами <script> и </script> (без самих тегов) и вставьте на сервис minify-js.com. Нажмите «Minify JS», скопируйте результат и вставьте его вместо полной версии кода между тегами <script></script>.

Так же поступите с содержимым между тегами <style> и </style> — но уже на другом сервисе: toptal.com/developers/cssminifier. Вставьте стили, нажмите «Minify», скопируйте результат и вставьте его между тегами <style></style>.

Перейдите в Настройки сайта → Ещё → Вставка кода → HTML-код для вставки внутрь HEAD, нажмите «Редактировать код» и вставьте настроенную версию целиком — обычную или минифицированную.



Убедитесь, что на сайте установлен стандартный блок корзины Tilda.
6.2Сохраните настройки HEAD и переопубликуйте все страницы, на которых есть каталог или корзина.
6.3Проверяйте работу только на опубликованном сайте, а не внутри редактора Tilda.
Тестируйте в новой вкладке или режиме инкогнито, чтобы старая версия каталога не осталась в кэше браузера.
Карточки видны, но выбор заблокирован; заголовок предлагает добавить товары.
Открываются подарки уровня 1, выбор добавляет одну позицию по цене 0 ₽.
Предыдущий подарок удаляется, новый занимает его место.
Сумма и доступный уровень пересчитываются без перезагрузки страницы.
Если выбранный подарок больше не доступен на новом уровне, он удаляется автоматически.
Открывается окно с названием, описанием, галереей и просмотром фото.
Сетка, модальное окно, выбор и закрытие корректно работают на телефоне.
Прямой переход на страницу товара-подарка уводит на страницу каталога, а не показывает отдельную карточку.
В заказ передаётся выбранный подарок с названием, изображением, UID и ценой 0 ₽.
Каталог сохраняется в браузере на шесть часов. Если вы добавили товар, заменили фотографии или изменили категории, выполните команду в консоли браузера и перезагрузите страницу.
localStorage.removeItem("tilda_gift_picker_catalog_v2");
location.reload();Блок выбора вообще не появился. Проверьте наличие корзины Tilda, публикацию всех страниц и отсутствие второй копии кода.
2Показывается «Подарки не найдены». Проверьте storepartuid, наличие товаров в категории и распределение UID. Затем очистите кэш.
3Все карточки заблокированы. Сверьте суммы в THRESHOLDS и убедитесь, что UID подарков находятся в массивах нужных уровней.
4В общей категории отображается лишний товар. Перенесите его из служебной категории либо добавьте в нужный уровень. Нераспределённые товары не скрываются автоматически.
5После изменения каталога остались старые фото. Сбросьте localStorage по команде выше или дождитесь окончания шестичасового кэша.
6Подарок дублируется. Удалите повторную установку решения из T123, HEAD страницы или старой версии в настройках сайта.
7Подарок исчез после удаления обычного товара. Это штатное поведение, если сумма стала ниже порога доступности выбранного подарка.
8Нет изображения или описания. Заполните галерею и описание в карточке подарка, очистите кэш и переоткройте корзину.
9Товар-подарок виден в обычном каталоге или в поиске по сайту. Проверьте, настроена ли переадресация с его карточки на страницу каталога — см. шаг 4.
10Кнопка «Подробнее об акции» не появляется. Проверьте, что в PROMO_BUTTON указано enabled: true и заполнено поле url.
Временно включите журнал в начале объекта CFG и откройте консоль браузера. После диагностики верните значение false.
const CFG = {
DEBUG: true,
// остальные настройки
};Решение ищет корзину-окно, боковую корзину и отдельную страницу корзины Tilda.
Каждая категория загружается страницами через slice, пока API Tilda не вернёт весь список.
При новом выборе старый подарок удаляется, а изменение его количества блокируется.
Подарок имеет цену 0 ₽. В промокодах и акции 3=2 исключите его UID, чтобы правила не учитывали бесплатную позицию.
Если используете решение автодобавления товаров, не назначайте UID подарков родителями или дополнительными товарами. После совместной установки обязательно повторите весь тест-лист корзины.
Поддержка включена в подписку: помогу проверить пороги, ID категорий, распределение подарков и работу корзины после публикации.
Версия 1.1.0 · три уровня подарков · настраиваемая кнопка акции · автоматическая загрузка каталога через slice