Подарок на выбор Закрытая страница · материалы доступны по подписке ← Все решения Telegram MAX
Доступ активен

Подарок на выбор от суммы корзины в Tilda

Подробная инструкция по версии 1.1.0: пороги и категории, распределение подарков по уровням, переадресация с карточки подарка, настраиваемая кнопка акции, минификация и установка кода в HEAD.

1Сумма растёт

Код считает товары в корзине без бесплатного подарка.

2Открывается уровень

Порог разблокирует доступные карточки подарков.

3Покупатель выбирает

Можно посмотреть описание и галерею каждого подарка.

4Подарок в корзине

Выбранная позиция добавляется по цене 0 ₽ и в количестве 1.

Материалы

Код решения 1.1.0

Сначала настройте блок 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;
    }
  }
&lt;/style&gt;

&lt;script&gt;
(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, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#039;");
  }

  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>
Важно: на сайте должен остаться только один экземпляр решения. Не дублируйте код одновременно в HEAD, блоке T123 и настройках отдельных страниц.
01

Пороги и категории: заполните THRESHOLDS и GIFT_PARTS

Сначала решите, сколько будет порогов суммы (в версии 1.0.0 их три) и откуда будут подгружаться подарки для каждого уровня.

1.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 — категория, откуда будут подгружаться подарки для этого уровня. В примере выше это одна и та же категория «Подарки» — можно завести и разные категории под каждый уровень, если так удобнее для структуры каталога.

Создание категории Подарки в каталоге Tilda
Категория «Подарки» с подарочными товарами в каталоге
Скрытие категории Подарки из навигации и фильтров каталога
В настройках категории скройте её из навигации и фильтров каталога
storepartuid категории отображается в шапке категории Tilda
ID категории (storepartuid) виден прямо в шапке страницы категории
Обязательно ставьте цену 0 ₽ для каждого подарочного товара в карточке каталога.
Порогов ровно три — это структура версии 1.0.0. Товаров на каждом уровне может быть сколько угодно, но сетка лучше всего выглядит при количестве, кратном трём.
02

Распределите ID товаров-подарков по уровням

Теперь укажите, какие именно товары доступны на каждом из трёх уровней — через GIFT_UIDS_BY_TIER.

2.1

Откройте карточку товара-подарка в каталоге — числовой 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
  ],
},
Product ID товара-подарка в карточке каталога Tilda
ID товара (product ID) — в нижней части карточки товара
Количество подарков на уровень может быть любым: 1, 2, 3 и больше — просто перечислите нужные ID через запятую в массиве уровня.
Учтите, что на первом уровне может быть, например, 1 подарок, на втором — уже 2 на выбор, на третьем — 3 и так далее. Но в финальной вёрстке в корзине карточки подарков выводятся по 3 в ряд, поэтому распределяйте итоговое количество так, чтобы сетка выглядела аккуратно.
03

При необходимости скорректируйте тексты

Все подписи интерфейса собраны в объекте TEXTS. Меняйте только текст между кавычками, сохраняя имена параметров и запятые.

TEXTS: {
  title: "Подарки за сумму заказа",
  choose: "Выбрать",
  chosen: "Выбрано",
  details: "Подробнее",
  unavailable: "Недоступно",
  loading: "Загружаем подарки...",
  noGifts: "Подарки не найдены.",
  noConfig: "Не заданы ID разделов подарков.",
  tip: "Выберите 1 подарок из списка ниже",
  needMorePrefix: "Добавьте до порога: ",
  yourSumPrefix: "Сумма товаров в корзине: ",
  levelPrefix: "Доступный уровень подарков: ",
  lockedSuffix: "от",
  alertNoCart: "Функции корзины Tilda недоступны.",
},
Этот шаг можно пропустить: тексты по умолчанию уже готовы к работе.
04

Настройте переадресацию с карточки подарка

Тильда часто выводит в стандартных блоках каталога вообще все товары раздела — включая подарочные, даже с ценой 0 ₽. Отключить показ таких товаров штатно нельзя, поэтому лучше для каждого подарочного товара настроить переадресацию с его собственной страницы на общую страницу каталога.

4.1

Откройте карточку товара-подарка в каталоге → раздел с настройками страницы товара.

4.2

Включите переадресацию (редирект) и укажите ссылкой страницу вашего каталога — так покупатель, случайно перешедший на страницу подарка напрямую, попадёт на витрину товаров, а не на отдельную карточку с ценой 0 ₽.

Настройка переадресации с карточки товара-подарка на страницу каталога Tilda
Переадресация с карточки подарка на страницу каталога
Без этого шага подарочные товары по 0 ₽ могут случайно попасться покупателю в обычных блоках каталога или в поиске по сайту.
05

Кнопка «Подробнее об акции» и минификация кода

Кнопка со ссылкой на условия акции настраивается через PROMO_BUTTON — по умолчанию она скрыта.

PROMO_BUTTON: {
  enabled: false,
  url: "https://web4life.ru/inform",
  text: "Подробнее об акции",
},

Если кнопка нужна — включите её и укажите свою ссылку и текст:

PROMO_BUTTON: {
  enabled: true,
  url: "https://web4life.ru/inform",
  text: "Подробнее об акции",
},

5.1 · Минифицируем JavaScript

Скопируйте настроенный код между тегами <script> и </script> (без самих тегов) и вставьте на сервис minify-js.com. Нажмите «Minify JS», скопируйте результат и вставьте его вместо полной версии кода между тегами <script></script>.

Сервис minify-js.com для минификации JavaScript
Вставляем код в minify-js.com и нажимаем «Minify JS»

5.2 · Минифицируем CSS

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

Сервис cssminifier для минификации CSS
Вставляем стили в cssminifier и нажимаем «Minify»
Минификация не обязательна, но даёт более компактную запись, которая грузится быстрее полной версии. Сохраните у себя и читаемую настроенную копию — в минифицированном коде сложнее менять пороги, ID и тексты при следующих правках.
06

Скопируйте код в HEAD проекта

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

Настройки сайта Tilda — переход к вставке кода
Настройки сайта
Раздел Вставка кода в настройках Tilda
Раздел «Вставка кода»
Вставка кода решения в HEAD проекта Tilda
«Редактировать код» → вставляете код → сохраняете
6.1

Убедитесь, что на сайте установлен стандартный блок корзины Tilda.

6.2

Сохраните настройки HEAD и переопубликуйте все страницы, на которых есть каталог или корзина.

6.3

Проверяйте работу только на опубликованном сайте, а не внутри редактора Tilda.

Код нельзя вставлять внутрь другого тега <script>: в готовом решении уже есть собственные теги <style> и <script>.
Перед запуском

Проверьте все состояния корзины

Тестируйте в новой вкладке или режиме инкогнито, чтобы старая версия каталога не осталась в кэше браузера.

1
Сумма ниже первого порога

Карточки видны, но выбор заблокирован; заголовок предлагает добавить товары.

2
Первый порог достигнут

Открываются подарки уровня 1, выбор добавляет одну позицию по цене 0 ₽.

3
Выбор другого подарка

Предыдущий подарок удаляется, новый занимает его место.

4
Изменение количества товара

Сумма и доступный уровень пересчитываются без перезагрузки страницы.

5
Сумма стала меньше

Если выбранный подарок больше не доступен на новом уровне, он удаляется автоматически.

6
Карточка «Подробнее»

Открывается окно с названием, описанием, галереей и просмотром фото.

7
Мобильная корзина

Сетка, модальное окно, выбор и закрытие корректно работают на телефоне.

8
Переадресация с карточки подарка

Прямой переход на страницу товара-подарка уводит на страницу каталога, а не показывает отдельную карточку.

9
Тестовая заявка

В заказ передаётся выбранный подарок с названием, изображением, UID и ценой 0 ₽.

Как сбросить кэш подарков после изменений

Каталог сохраняется в браузере на шесть часов. Если вы добавили товар, заменили фотографии или изменили категории, выполните команду в консоли браузера и перезагрузите страницу.

localStorage.removeItem("tilda_gift_picker_catalog_v2");
location.reload();
Открыть консоль: F12 → Console. Для быстрой проверки также можно открыть сайт в режиме инкогнито.
Диагностика

Если подарки не появились или работают неправильно

1

Блок выбора вообще не появился. Проверьте наличие корзины 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,
  // остальные настройки
};
Совместимость

Что уже предусмотрено в версии 1.1.0

Три вида корзины

Решение ищет корзину-окно, боковую корзину и отдельную страницу корзины Tilda.

Каталоги больше 500 товаров

Каждая категория загружается страницами через slice, пока API Tilda не вернёт весь список.

Только один подарок

При новом выборе старый подарок удаляется, а изменение его количества блокируется.

Другие механики скидок

Подарок имеет цену 0 ₽. В промокодах и акции 3=2 исключите его UID, чтобы правила не учитывали бесплатную позицию.

Если используете решение автодобавления товаров, не назначайте UID подарков родителями или дополнительными товарами. После совместной установки обязательно повторите весь тест-лист корзины.

Что-то не получается? Помогу с установкой

Поддержка включена в подписку: помогу проверить пороги, ID категорий, распределение подарков и работу корзины после публикации.

Версия 1.1.0 · три уровня подарков · настраиваемая кнопка акции · автоматическая загрузка каталога через slice