Акция 1+1=3 для магазинов на Тильде

Сделал удобный скрипт для этой акции в интернет-магазинах на Тильде. Пользуйтесь!

Подготовил модификацию в двух вариантах:
1) акция для всех товаров в каталоге;
2) акция только для выбранных товаров
  • Применяется к товару с наименьшей стомостью!

  • В обеих модификациях предусмотрен симпатичный баннер с уведомлением о акции:
1) Скрипт для всех товаров в каталоге
Он хорошо подходит для распродажи, короткой сезонной акции или магазина с небольшим каталогом, где нет необходимости исключать отдельные позиции.

В начале каждого скрипта есть блок настроек. В нём можно изменить:
1. Цену третьего товара после применения акции. Вместо 1 можно поставить, например, 100, если подарок должен быть не бесплатным, а продаваться за символическую сумму.
2. Текст баннера в корзине
Копировать код
<!--
  Скрипт от Арсена: акция 1+1=3 для ВСЕХ товаров каталога.
  Вставьте код целиком в Footer или в блок T123 перед </body>.
-->
<style>
  .promo113-notice { box-sizing: border-box; width: 100%; margin: 15px 0; padding: 14px 16px; color: #1f5132; background: #eef8f1; border: 1px solid #b9ddc4; border-radius: 6px; font-size: 14px; line-height: 1.45; }
  .promo113-notice strong { font-weight: 700; }
  .promo113-notice__title { display: block; margin-bottom: 4px; font-size: 15px; }
</style>

<script>
(function () {
  'use strict';

  /* ==================== НАСТРОЙКИ ==================== */

  // УЧАСТВУЮТ ВСЕ ТОВАРЫ В КОРЗИНЕ

  // 1. ЦЕНА третьего товара после применения акции.
  const PROMO_PRODUCT_PRICE = 1;
  const CURRENCY_LABEL = '₽';
  const MONEY_LOCALE = 'ru-RU';

  // 2. ТЕКСТ БАННЕРА В КОРЗИНЕ.
  //    {product}, {price} и {currency} скрипт подставит сам.
  const BANNER_TITLE = 'Акция 1+1=3 применена';
  const BANNER_LINE = 'Третий товар: {product} за {price} {currency}';
  const BANNER_FOOTER = 'Акция действует один раз на заказ. Промокоды не суммируются.';

  // 3. true - скрывать и сбрасывать промокод, пока акция активна.
  const BLOCK_PROMOCODES = true;

  /* ==================================================== */

  let promoActive = false;
  let promoDiscount = 0;
  let promoItem = null;
  let runTimer = null;
  let running = false;

  function getProducts() {
    return window.tcart && Array.isArray(window.tcart.products)
      ? window.tcart.products
      : [];
  }

  function rememberOriginalPrice(product) {
    if (product._promo113OriginalPrice === undefined) {
      product._promo113OriginalPrice = Number(product.price || 0);
    }
  }

  function resetPreviousPromo(products) {
    products.forEach(function (product) {
      if (product._promo113OriginalPrice === undefined) return;
      const quantity = Math.max(1, Number(product.quantity || 1));
      const price = Number(product._promo113OriginalPrice || 0);
      product.price = price;
      product.amount = price * quantity;
    });
  }

  function collectPromoItems(products) {
    const items = [];
    products.forEach(function (product, productIndex) {
      // Все товары каталога участвуют в акции.
      rememberOriginalPrice(product);
      const quantity = Math.max(1, Number(product.quantity || 1));
      const price = Number(product._promo113OriginalPrice || 0);
      for (let i = 0; i < quantity; i++) {
        items.push({ productIndex: productIndex, name: product.name || 'Товар', price: price });
      }
    });
    return items;
  }

  function getDiscountItem(items) {
    if (items.length < 3) return null;
    // Акция срабатывает только ОДИН раз: выбираем самый дешёвый товар.
    return items.slice().sort(function (a, b) {
      return a.price - b.price;
    })[0];
  }

  function applyDiscount(products, item) {
    if (!item || !products[item.productIndex]) return;
    const product = products[item.productIndex];
    const discount = Math.max(0, Number(item.price || 0) - PROMO_PRODUCT_PRICE);
    product.amount = Math.max(PROMO_PRODUCT_PRICE, Number(product.amount || 0) - discount);
    if (Number(product.quantity || 1) === 1) product.price = Number(product.amount || 0);
  }

  function promocodeInputs() {
    return Array.from(document.querySelectorAll([
      'input[name="promocode"]', 'input[name="promo"]',
      'input[name*="promocode" i]', '.t-inputpromocode',
      '.t706__cartwin-promocode-input'
    ].join(',')));
  }

  function setPromocodeState(blocked) {
    if (!BLOCK_PROMOCODES) return;
    if (blocked && window.tcart) {
      ['promocode', 'promocodecode', 'promocodevalue', 'promocode_name', 'coupon'].forEach(function (key) {
        if (key in window.tcart) window.tcart[key] = '';
      });
      ['discount', 'discountsum', 'promocode_discount', 'promocode_discountsum', 'coupon_discount', 'coupon_discountsum'].forEach(function (key) {
        if (key in window.tcart) window.tcart[key] = 0;
      });
    }
    promocodeInputs().forEach(function (input) {
      const wrapper = input.closest('.t-input-group, .t-form__promocode, .t706__cartwin-promocode, .t706__cartwin-promocode-wrapper');
      input.value = blocked ? '' : input.value;
      input.disabled = blocked;
      if (wrapper) wrapper.style.display = blocked ? 'none' : '';
    });
  }

  function formatMoney(value) {
    return new Intl.NumberFormat(MONEY_LOCALE).format(Math.round(Number(value || 0)));
  }

  function renderNotice() {
    document.querySelectorAll('.promo113-notice').forEach(function (node) { node.remove(); });
    if (!promoActive || !promoItem || promoDiscount <= 0) return;
    const anchor = document.querySelector('.t706__cartwin-prodamount-wrap, .t706__cartwin-totalamount-wrap, .t706__cartwin-orderform');
    const parent = anchor && anchor.parentNode
      ? anchor.parentNode
      : document.querySelector('.t706__cartwin-bottom, .t706__cartwin-content, .t706__cartwin');
    if (!parent) return;
    const text = function (value) {
      return value.replace('{product}', promoItem.name).replace('{price}', formatMoney(PROMO_PRODUCT_PRICE)).replace('{currency}', CURRENCY_LABEL);
    };
    const notice = document.createElement('div');
    notice.className = 'promo113-notice';
    notice.innerHTML = '<strong class="promo113-notice__title">' + BANNER_TITLE + '</strong>' + text(BANNER_LINE) + '<br>Ваша скидка: −' + formatMoney(promoDiscount) + ' ' + CURRENCY_LABEL + '<br>' + text(BANNER_FOOTER);
    if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(notice, anchor);
    else parent.appendChild(notice);
  }

  function redrawCart() {
    ['tcart__updateTotalProductsinCartObj', 'tcart__reDrawProducts', 'tcart__reDrawTotal', 'tcart__saveLocalObj'].forEach(function (name) {
      if (typeof window[name] === 'function') window[name]();
    });
  }

  function applyPromo() {
    if (running) return;
    const products = getProducts();
    if (!products.length) {
      promoActive = false;
      promoDiscount = 0;
      promoItem = null;
      setPromocodeState(false);
      renderNotice();
      return;
    }
    running = true;
    try {
      resetPreviousPromo(products);
      const item = getDiscountItem(collectPromoItems(products));
      promoActive = Boolean(item);
      promoItem = item;
      promoDiscount = item ? Math.max(0, Number(item.price || 0) - PROMO_PRODUCT_PRICE) : 0;
      if (promoActive) applyDiscount(products, item);
      setPromocodeState(promoActive);
      redrawCart();
      renderNotice();
      setTimeout(renderNotice, 300);
    } finally {
      running = false;
    }
  }

  function schedulePromo() {
    clearTimeout(runTimer);
    runTimer = setTimeout(applyPromo, 350);
  }

  new MutationObserver(function () {
    if (promoActive && !document.querySelector('.promo113-notice')) {
      setTimeout(renderNotice, 50);
    }
  }).observe(document.documentElement, { childList: true, subtree: true });

  document.addEventListener('DOMContentLoaded', schedulePromo);
  window.addEventListener('load', schedulePromo);
  document.addEventListener('tilda-cart-opened', schedulePromo);
  document.addEventListener('tilda-cart-updated', schedulePromo);
  document.addEventListener('click', schedulePromo);
})();
</script>
2) Скрипт только для выбранных товаров
Этот вариант подходит, если в акции участвуют не все товары магазина, а конкретные позиции или отдельная категория.

В начале кода нужно заменить тестовые значения на реальные ID товаров из Tilda. Так можно ограничить акцию, например, только средствами одного бренда, конкретными наборами или сезонной подборкой.
Копировать код
<!--
  Скрипт от Арсена: акция 1+1=3 по ID товаров.
  Вставьте код целиком в Footer или в блок T123 перед </body>.
-->
<style>
  .promo113-notice { box-sizing: border-box; width: 100%; margin: 15px 0; padding: 14px 16px; color: #1f5132; background: #eef8f1; border: 1px solid #b9ddc4; border-radius: 6px; font-size: 14px; line-height: 1.45; }
  .promo113-notice strong { font-weight: 700; }
  .promo113-notice__title { display: block; margin-bottom: 4px; font-size: 15px; }
</style>

<script>
(function () {
  'use strict';

  /* ==================== НАСТРОЙКИ ==================== */

  // 1. ВПИШИТЕ ID ТОВАРОВ, УЧАСТВУЮЩИХ В АКЦИИ.
  //    ID берётся из настроек товара в каталоге.
  const PROMO_PRODUCT_IDS = [
    'ID-ПЕРВОГО-ТОВАРА',
    'ID-ВТОРОГО-ТОВАРА',
    'ID-ТРЕТЬЕГО-ТОВАРА'
  ];

  // 2. ЦЕНА третьего товара после применения акции.
  const PROMO_PRODUCT_PRICE = 1;
  const CURRENCY_LABEL = '₽';
  const MONEY_LOCALE = 'ru-RU';

  // 3. ТЕКСТ БАННЕРА В КОРЗИНЕ.
  //    {product}, {price} и {currency} скрипт подставит сам.
  const BANNER_TITLE = 'Акция 1+1=3 применена';
  const BANNER_LINE = 'Третий товар: {product} за {price} {currency}';
  const BANNER_FOOTER = 'Акция действует один раз на заказ. Промокоды не суммируются.';

  // 4. true - скрывать и сбрасывать промокод, пока акция активна.
  const BLOCK_PROMOCODES = true;

  /* ==================================================== */

  const promoProductIds = new Set(PROMO_PRODUCT_IDS.map(String));
  let promoActive = false;
  let promoDiscount = 0;
  let promoItem = null;
  let runTimer = null;
  let running = false;

  function getProducts() {
    return window.tcart && Array.isArray(window.tcart.products)
      ? window.tcart.products
      : [];
  }

  function getProductId(product) {
    return String(product.uid || product.lid || product.gen_uid || '');
  }

  function isPromoProduct(product) {
    return promoProductIds.has(getProductId(product));
  }

  function rememberOriginalPrice(product) {
    if (product._promo113OriginalPrice === undefined) {
      product._promo113OriginalPrice = Number(product.price || 0);
    }
  }

  function resetPreviousPromo(products) {
    products.forEach(function (product) {
      if (product._promo113OriginalPrice === undefined) return;
      const quantity = Math.max(1, Number(product.quantity || 1));
      const price = Number(product._promo113OriginalPrice || 0);
      product.price = price;
      product.amount = price * quantity;
    });
  }

  function collectPromoItems(products) {
    const items = [];
    products.forEach(function (product, productIndex) {
      if (!isPromoProduct(product)) return;
      rememberOriginalPrice(product);
      const quantity = Math.max(1, Number(product.quantity || 1));
      const price = Number(product._promo113OriginalPrice || 0);
      for (let i = 0; i < quantity; i++) {
        items.push({ productIndex: productIndex, name: product.name || 'Товар', price: price });
      }
    });
    return items;
  }

  function getDiscountItem(items) {
    if (items.length < 3) return null;
    // Акция срабатывает только ОДИН раз: выбираем самый дешёвый товар.
    return items.slice().sort(function (a, b) {
      return a.price - b.price;
    })[0];
  }

  function applyDiscount(products, item) {
    if (!item || !products[item.productIndex]) return;
    const product = products[item.productIndex];
    const discount = Math.max(0, Number(item.price || 0) - PROMO_PRODUCT_PRICE);
    product.amount = Math.max(PROMO_PRODUCT_PRICE, Number(product.amount || 0) - discount);
    if (Number(product.quantity || 1) === 1) product.price = Number(product.amount || 0);
  }

  function promocodeInputs() {
    return Array.from(document.querySelectorAll([
      'input[name="promocode"]', 'input[name="promo"]',
      'input[name*="promocode" i]', '.t-inputpromocode',
      '.t706__cartwin-promocode-input'
    ].join(',')));
  }

  function setPromocodeState(blocked) {
    if (!BLOCK_PROMOCODES) return;
    if (blocked && window.tcart) {
      ['promocode', 'promocodecode', 'promocodevalue', 'promocode_name', 'coupon'].forEach(function (key) {
        if (key in window.tcart) window.tcart[key] = '';
      });
      ['discount', 'discountsum', 'promocode_discount', 'promocode_discountsum', 'coupon_discount', 'coupon_discountsum'].forEach(function (key) {
        if (key in window.tcart) window.tcart[key] = 0;
      });
    }
    promocodeInputs().forEach(function (input) {
      const wrapper = input.closest('.t-input-group, .t-form__promocode, .t706__cartwin-promocode, .t706__cartwin-promocode-wrapper');
      input.value = blocked ? '' : input.value;
      input.disabled = blocked;
      if (wrapper) wrapper.style.display = blocked ? 'none' : '';
    });
  }

  function formatMoney(value) {
    return new Intl.NumberFormat(MONEY_LOCALE).format(Math.round(Number(value || 0)));
  }

  function renderNotice() {
    document.querySelectorAll('.promo113-notice').forEach(function (node) { node.remove(); });
    if (!promoActive || !promoItem || promoDiscount <= 0) return;
    const anchor = document.querySelector('.t706__cartwin-prodamount-wrap, .t706__cartwin-totalamount-wrap, .t706__cartwin-orderform');
    const parent = anchor && anchor.parentNode
      ? anchor.parentNode
      : document.querySelector('.t706__cartwin-bottom, .t706__cartwin-content, .t706__cartwin');
    if (!parent) return;
    const text = function (value) {
      return value.replace('{product}', promoItem.name).replace('{price}', formatMoney(PROMO_PRODUCT_PRICE)).replace('{currency}', CURRENCY_LABEL);
    };
    const notice = document.createElement('div');
    notice.className = 'promo113-notice';
    notice.innerHTML = '<strong class="promo113-notice__title">' + BANNER_TITLE + '</strong>' + text(BANNER_LINE) + '<br>Ваша скидка: −' + formatMoney(promoDiscount) + ' ' + CURRENCY_LABEL + '<br>' + text(BANNER_FOOTER);
    if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(notice, anchor);
    else parent.appendChild(notice);
  }

  function redrawCart() {
    ['tcart__updateTotalProductsinCartObj', 'tcart__reDrawProducts', 'tcart__reDrawTotal', 'tcart__saveLocalObj'].forEach(function (name) {
      if (typeof window[name] === 'function') window[name]();
    });
  }

  function applyPromo() {
    if (running) return;
    const products = getProducts();
    if (!products.length) {
      promoActive = false;
      promoDiscount = 0;
      promoItem = null;
      setPromocodeState(false);
      renderNotice();
      return;
    }
    running = true;
    try {
      resetPreviousPromo(products);
      const item = getDiscountItem(collectPromoItems(products));
      promoActive = Boolean(item);
      promoItem = item;
      promoDiscount = item ? Math.max(0, Number(item.price || 0) - PROMO_PRODUCT_PRICE) : 0;
      if (promoActive) applyDiscount(products, item);
      setPromocodeState(promoActive);
      redrawCart();
      renderNotice();
      setTimeout(renderNotice, 300);
    } finally {
      running = false;
    }
  }

  function schedulePromo() {
    clearTimeout(runTimer);
    runTimer = setTimeout(applyPromo, 350);
  }

  new MutationObserver(function () {
    if (promoActive && !document.querySelector('.promo113-notice')) {
      setTimeout(renderNotice, 50);
    }
  }).observe(document.documentElement, { childList: true, subtree: true });

  document.addEventListener('DOMContentLoaded', schedulePromo);
  window.addEventListener('load', schedulePromo);
  document.addEventListener('tilda-cart-opened', schedulePromo);
  document.addEventListener('tilda-cart-updated', schedulePromo);
  document.addEventListener('click', schedulePromo);
})();
</script>