templates/product.html.twig line 1

Open in your IDE?
  1. {% extends 'base.html.twig' %}
  2. {% set productData = product|default(null) %}
  3. {% set selectedVariant = productData and productData.selected_variant|default(null) ? productData.selected_variant : null %}
  4. {% set productVariants = productData and productData.variants|default([])|length ? productData.variants : [] %}
  5. {% set productName = productData ? productData.name : 'Forza Horizon 6 · Standard / Deluxe / Premium · STEAM RU' %}
  6. {% set productMobileName = productData ? productData.name : 'DOOM 2016 · STEAM RU' %}
  7. {% set productImage = productData and productData.image_url ? productData.image_url : './assets/product-doom.jpg' %}
  8. {% set productPrice = selectedVariant ? selectedVariant.price_label : (productData and productData.price_label ? productData.price_label : '5 299 ₽') %}
  9. {% set productRegion = selectedVariant ? selectedVariant.context_label|default(selectedVariant.label) : (productData and productData.region ? productData.region : 'Россия · Standard') %}
  10. {% set productDiscountLabel = selectedVariant ? selectedVariant.discount_label|default('') : (productData and productData.discount_label|default('') ? productData.discount_label : '') %}
  11. {% set productGallery = productData ? productData.gallery|default([]) : [] %}
  12. {% set productGalleryCount = productGallery|length %}
  13. {% set productPreviewGallery = productGallery|slice(0, 10) %}
  14. {% set productPreviewGalleryCount = productPreviewGallery|length %}
  15. {% set productHeroImage = productPreviewGalleryCount ? productPreviewGallery[0].url|default(productImage) : productImage %}
  16. {% set relatedProducts = productData and productData.related_products|default([])|length ? productData.related_products : [] %}
  17. {% set productRegionRestriction = selectedVariant and selectedVariant.region_restriction|default(null) ? selectedVariant.region_restriction : (productData and productData.region_restriction|default(null) ? productData.region_restriction : null) %}
  18. {% set productSpecs = productData and productData.specs|default([])|length ? productData.specs : [
  19.   {'key': 'platform', 'label': 'ПЛАТФОРМА', 'value': 'Steam · PC'},
  20.   {'key': 'activation_region', 'label': 'РЕГИОН АКТИВАЦИИ', 'value': 'Россия'},
  21.   {'key': 'language', 'label': 'ЯЗЫК', 'value': 'Русский, English'},
  22.   {'key': 'genre', 'label': 'ЖАНР', 'value': 'Игра'},
  23.   {'key': 'release_date', 'label': 'ДАТА ВЫХОДА', 'value': 'Уточняется'},
  24.   {'key': 'publisher', 'label': 'ИЗДАТЕЛЬ', 'value': 'Уточняется'}
  25. ] %}
  26. {% set productDelivery = productData and productData.delivery|default(null) ? productData.delivery : {
  27.   'type': 'steam_gift',
  28.   'label': 'Подарок Steam',
  29.   'short_label': 'Подарок Steam',
  30.   'badge': 'Gift',
  31.   'description': 'Отправляется подарком на Steam-аккаунт по ссылке-приглашению.',
  32.   'form_hint': 'Понадобится ссылка-приглашение Steam.'
  33. } %}
  34. {% set productDeliveryShortLabel = productDelivery.short_label|default(productDelivery.label) %}
  35. {% set checkoutForms = selectedVariant ? selectedVariant.forms|default(productData.forms|default([])) : (productData ? productData.forms|default([]) : []) %}
  36. {% set checkoutProductId = selectedVariant ? selectedVariant.id : (productData ? productData.id : '') %}
  37. {% block title %}{{ productName }} — PlayPapa{% endblock %}
  38. {% block body_class %}product-page{% endblock %}
  39. {% block stylesheets %}
  40.       <link rel="stylesheet" href="/product.css" />
  41. {% endblock %}
  42. {% block content %}
  43. <section class="shell product-hero" aria-label="Карточка товара">
  44.         <nav class="product-breadcrumbs" aria-label="Навигация">
  45.           <a href="./index.html">Главная</a>
  46.           <span aria-hidden="true">›</span>
  47.           <a href="/catalog">Игры</a>
  48.           <span aria-hidden="true">›</span>
  49.           <b>{{ productName }}</b>
  50.         </nav>
  51.         <div class="product-overview">
  52.           <div class="product-gallery">
  53.             <div class="product-cover-large">
  54.               <img src="{{ productHeroImage }}" alt="{{ productName }}" data-product-cover />
  55.               {% if productDiscountLabel %}
  56.                 <span class="product-sale-badge">{{ productDiscountLabel }}</span>
  57.               {% endif %}
  58.             </div>
  59.           </div>
  60.           <section class="product-summary" aria-labelledby="productTitle">
  61.             <h1 id="productTitle">
  62.               <span class="desktop-title">{{ productName }}</span>
  63.               <span class="mobile-title">{{ productMobileName }}</span>
  64.             </h1>
  65.             <div class="product-meta">
  66.               <span class="product-stars" aria-label="Рейтинг 4.9">★★★★★ <b>4.9</b></span>
  67.               <span>Продано 5 744</span>
  68.               <a href="#reviews" data-product-tab-link="reviews">896 отзывов</a>
  69.             </div>
  70.             <div class="mobile-price-row" aria-hidden="true">
  71.               <strong>{{ productPrice }}</strong>
  72.               {% if productDiscountLabel %}<b>{{ productDiscountLabel }}</b>{% endif %}
  73.             </div>
  74.             <div class="variant-block">
  75.               <p class="variant-title">Выберите регион и издание <span>*</span></p>
  76.               <div class="variant-grid" role="group" aria-label="Регион и издание">
  77.                 {% if productVariants %}
  78.                   {% for variant in productVariants %}
  79.                     {% set selected = variant.id == productData.selected_variant_id %}
  80.                     <button
  81.                       class="variant-option {{ selected ? 'is-selected' : '' }} {{ variant.in_stock ? '' : 'is-disabled' }}"
  82.                       type="button"
  83.                       data-variant-option
  84.                       data-price="{{ variant.price_label }}"
  85.                       data-context="{{ variant.context_label|default(variant.label) }}"
  86.                       data-region-label="{{ variant.region }}"
  87.                       data-region-restriction-title="{{ variant.region_restriction.title|default('') }}"
  88.                       data-region-restriction-text="{{ variant.region_restriction.text|default('') }}"
  89.                       data-region-restriction-countries="{{ variant.region_restriction.countries|default('') }}"
  90.                       data-name="{{ variant.name }}"
  91.                       data-url="{{ variant.url }}"
  92.                       data-delivery-label="{{ variant.delivery.label|default(productDelivery.label) }}"
  93.                       data-delivery-short-label="{{ variant.delivery.short_label|default(productDeliveryShortLabel) }}"
  94.                       data-delivery-description="{{ variant.delivery.description|default(productDelivery.description) }}"
  95.                       data-delivery-type="{{ variant.delivery.type|default(productDelivery.type|default('digital_item')) }}"
  96.                       data-product-id="{{ variant.id }}"
  97.                       data-forms="{{ variant.forms|default([])|json_encode|e('html_attr') }}"
  98.                       data-discount-label="{{ variant.discount_label|default('') }}"
  99.                       {{ variant.in_stock ? '' : 'disabled' }}
  100.                     >
  101.                       <span>{{ variant.label }}</span>
  102.                       <small>{{ selected ? 'Выбрано' : (variant.in_stock ? variant.price_label : 'Нет в наличии') }}</small>
  103.                     </button>
  104.                   {% endfor %}
  105.                 {% else %}
  106.                   <button class="variant-option is-selected" type="button">
  107.                     <span>{{ productRegion }}</span>
  108.                     <small>Выбрано</small>
  109.                   </button>
  110.                   <button class="variant-option" type="button">
  111.                     <span>Россия · Deluxe</span>
  112.                     <small>+2 500 ₽</small>
  113.                   </button>
  114.                   <button class="variant-option" type="button">
  115.                     <span>Россия · Premium</span>
  116.                     <small>+4 300 ₽</small>
  117.                   </button>
  118.                   <button class="variant-option is-discount" type="button">
  119.                     <span><span class="desktop-label">Казахстан · Standard</span><span class="mobile-label">Казахстан</span></span>
  120.                     <small>−400 ₽</small>
  121.                   </button>
  122.                 {% endif %}
  123.               </div>
  124.               {% if productVariants|length > 4 %}
  125.                 <button class="show-regions" type="button">Всего вариантов: {{ productVariants|length }}</button>
  126.               {% elseif not productVariants %}
  127.                 <button class="show-regions" type="button">Показать все регионы ›</button>
  128.               {% endif %}
  129.             </div>
  130.             <div class="product-media-tabs">
  131.               <div class="product-media-head" aria-label="Галерея товара">
  132.                 <p>Изображения{% if productPreviewGalleryCount %} <span>{{ productPreviewGalleryCount }}</span>{% endif %}</p>
  133.               </div>
  134.               <div class="thumb-row-product" aria-label="Миниатюры">
  135.                 {% if productPreviewGallery %}
  136.                   {% for image in productPreviewGallery %}
  137.                     {% set fullImage = image.url|default(productImage) %}
  138.                     <button
  139.                       class="product-thumb {{ loop.first ? 'is-active' : '' }}"
  140.                       type="button"
  141.                       aria-label="Изображение {{ loop.index }}"
  142.                       data-product-thumb
  143.                       data-full-image="{{ fullImage }}"
  144.                     >
  145.                       <img src="{{ image.thumbnail_url|default(fullImage) }}" alt="" />
  146.                     </button>
  147.                   {% endfor %}
  148.                 {% else %}
  149.                   <button class="product-thumb is-active" type="button" aria-label="Изображение 1" data-product-thumb data-full-image="{{ productImage }}">
  150.                     <img src="{{ productImage }}" alt="" />
  151.                   </button>
  152.                 {% endif %}
  153.               </div>
  154.             </div>
  155.           </section>
  156.           <aside class="purchase-column" aria-label="Покупка">
  157.             <section class="purchase-card">
  158.               <div class="price-main">
  159.                 <strong>{{ productPrice }}</strong>
  160.                 {% if not productData %}<del>6 499 ₽</del>{% endif %}
  161.               </div>
  162.               <div class="buy-context">
  163.                 <span>{{ productRegion }}</span>
  164.                 <b data-discount-label-target {{ productDiscountLabel ? '' : 'hidden' }}>{{ productDiscountLabel }}</b>
  165.               </div>
  166.               <div class="delivery-method" data-delivery-method>
  167.                 <span>Способ получения</span>
  168.                 <strong data-delivery-label-target>{{ productDelivery.label }}</strong>
  169.                 <small data-delivery-description-target>{{ productDelivery.description }}</small>
  170.               </div>
  171.               <button class="buy-now" type="button" data-checkout-open aria-haspopup="dialog" aria-controls="checkoutModal">
  172.                 <img src="./assets/cart-white.svg" alt="" />
  173.                 Купить сейчас
  174.               </button>
  175.               <div class="promo-widget" data-promo-widget>
  176.                 <button class="promo-link" type="button" aria-expanded="false">
  177.                   Ввести промокод <img src="./assets/promo-arrow.svg" alt="" />
  178.                 </button>
  179.                 <form class="promo-form" hidden>
  180.                   <div class="promo-control-row">
  181.                     <input type="text" placeholder="Промокод" autocomplete="off" />
  182.                     <button class="promo-apply" type="submit">
  183.                       <span>OK</span>
  184.                       <img src="./assets/auth-check.svg" alt="" />
  185.                     </button>
  186.                   </div>
  187.                   <p class="promo-help">Введите промокод - скидка применится автоматически.</p>
  188.                   <p class="promo-success" hidden>Промокод применён - скидка 15%</p>
  189.                   <p class="promo-error" hidden>× Промокод недействителен или истёк</p>
  190.                 </form>
  191.               </div>
  192.               <ul class="buy-features">
  193.                 <li>Безопасная сделка</li>
  194.                 <li>Мгновенная доставка</li>
  195.                 <li>Гарантия возврата</li>
  196.               </ul>
  197.             </section>
  198.             <section class="seller-card" aria-label="Продавец">
  199.               <img class="seller-logo" src="./assets/blogger-playpapa-logo.svg" alt="" />
  200.               <div>
  201.                 <strong>PlayPapa</strong>
  202.                 <span>20 218 продаж</span>
  203.               </div>
  204.               <b class="seller-rating">★ 4.9</b>
  205.             </section>
  206.           </aside>
  207.         </div>
  208.       </section>
  209.       <section class="shell product-info" aria-label="Описание товара">
  210.         <nav class="product-page-tabs" aria-label="Разделы товара">
  211.           <button class="is-active" type="button" data-product-tab="description">Описание</button>
  212.           <button id="reviews" type="button" data-product-tab="reviews">Отзывы <span class="tab-count">896</span></button>
  213.           <button type="button" data-product-tab="comments">Комментарии <span class="tab-count">28</span></button>
  214.           <button type="button" data-product-tab="images">Изображения <span class="tab-count">{{ productGalleryCount ?: 1 }}</span></button>
  215.         </nav>
  216.         <div class="product-description product-tab-panel is-active is-collapsed" data-product-panel="description">
  217.           <div class="description-copy">
  218.             {% if productData and productData.description_html %}
  219.               {{ productData.description_html|raw }}
  220.             {% else %}
  221.               <p>
  222.                 DOOM 2016 - перезапуск легендарной серии от id Software. Стремительный, агрессивный шутер с культовым
  223.                 арсеналом, добиваниями Glory Kill и саундтреком, ставшим эталоном жанра. Ключ активируется в Steam, регион
  224.                 РФ + СНГ.
  225.               </p>
  226.             {% endif %}
  227.           </div>
  228.           {% if productSpecs %}
  229.             <div class="spec-grid product-spec-grid" aria-label="Характеристики товара">
  230.               {% for spec in productSpecs %}
  231.                 <div class="spec-card">
  232.                   <small>{{ spec.label }}</small>
  233.                   <strong{% if spec.key|default('') == 'activation_region' %} data-spec-region{% endif %}>{{ spec.value }}</strong>
  234.                 </div>
  235.               {% endfor %}
  236.             </div>
  237.           {% endif %}
  238.           <div class="region-restriction-note" data-region-restriction {% if not productRegionRestriction %}hidden{% endif %}>
  239.             <strong data-region-restriction-title>{{ productRegionRestriction.title|default('') }}</strong>
  240.             <p data-region-restriction-text>{{ productRegionRestriction.text|default('') }}</p>
  241.             <code data-region-restriction-countries>{{ productRegionRestriction.countries|default('') }}</code>
  242.           </div>
  243.           <div class="product-description-extra" id="descriptionDetails" data-description-extra hidden>
  244.             {% if productData and productData.short_description_html %}
  245.               {{ productData.short_description_html|raw }}
  246.             {% else %}
  247.             <ul>
  248.               <li>Автоматическая выдача 24/7 - активация не дольше 2 минут.</li>
  249.               <li>Оплата через СБП - комиссия 0%.</li>
  250.               <li>Отправляется как подарок (gift) на ваш аккаунт Steam.</li>
  251.               <li>Цены для разных регионов могут отличаться - смотрите при выборе.</li>
  252.               <li>Куплено официально через Steam. Действует гарантия возврата.</li>
  253.             </ul>
  254.             <h2>Подробнее об игре</h2>
  255.             <p>
  256.               Вы - Палач Рока, пробудившийся на марсианской базе, захваченной силами Ада. Никаких перезарядок-передышек:
  257.               игра поощряет постоянное движение и агрессию. Собирайте руны, улучшайте броню Praetor и оружие, исследуйте
  258.               секреты уровней и уничтожайте демонов всеми доступными способами.
  259.             </p>
  260.             {% endif %}
  261.           </div>
  262.           <button class="description-toggle" type="button" data-description-toggle aria-expanded="false" aria-controls="descriptionDetails">
  263.             <span>Показать полностью</span>
  264.             <img src="./assets/promo-arrow.svg" alt="" />
  265.           </button>
  266.         </div>
  267.         <section class="product-reviews-block product-tab-panel" data-product-panel="reviews" aria-label="Отзывы о товаре" hidden>
  268.           <div class="reviews-score-card">
  269.             <div class="reviews-score-main">
  270.               <strong>4.9</strong>
  271.               <span aria-label="Рейтинг 4.9 из 5">★★★★★</span>
  272.               <b>896 отзывов</b>
  273.             </div>
  274.             <div class="reviews-score-bars" aria-label="Распределение оценок">
  275.               <div class="score-row"><span>5</span><i><b style="--score: 92%"></b></i><em>824</em></div>
  276.               <div class="score-row"><span>4</span><i><b style="--score: 7%"></b></i><em>52</em></div>
  277.               <div class="score-row"><span>3</span><i><b style="--score: 2%"></b></i><em>14</em></div>
  278.               <div class="score-row"><span>2</span><i><b style="--score: 1%"></b></i><em>4</em></div>
  279.               <div class="score-row"><span>1</span><i><b style="--score: 1%"></b></i><em>2</em></div>
  280.             </div>
  281.           </div>
  282.           <div class="review-form-grid">
  283.             <article class="review-form-wrap">
  284.               <form class="product-review-form" data-review-form>
  285.                 <div class="review-form-head">
  286.                   <h3><span class="desktop-label">Поделитесь впечатлением</span><span class="mobile-label">Ваш отзыв</span></h3>
  287.                 </div>
  288.                 <div class="review-form-rating" role="radiogroup" aria-label="Поставьте оценку">
  289.                   <button type="button" aria-label="1 звезда" data-rating="1">★</button>
  290.                   <button type="button" aria-label="2 звезды" data-rating="2">★</button>
  291.                   <button type="button" aria-label="3 звезды" data-rating="3">★</button>
  292.                   <button type="button" aria-label="4 звезды" data-rating="4">★</button>
  293.                   <button type="button" aria-label="5 звезд" data-rating="5">★</button>
  294.                   <b>Поставьте оценку</b>
  295.                 </div>
  296.                 <div class="review-tags" aria-label="Быстрые теги">
  297.                   <button type="button">Быстрая выдача</button>
  298.                   <button type="button">Всё честно</button>
  299.                   <button type="button">Приятная цена</button>
  300.                 </div>
  301.                 <label class="review-textarea">
  302.                   <textarea placeholder="Расскажите, как прошла покупка и активация..."></textarea>
  303.                 </label>
  304.                 <p class="review-error" hidden>Пожалуйста, напишите текст перед отправкой</p>
  305.                 <p class="review-success" hidden>Отправлено, спасибо! Появится после модерации</p>
  306.                 <button class="review-submit" type="submit">Опубликовать отзыв</button>
  307.               </form>
  308.             </article>
  309.           </div>
  310.           <div class="product-review-list">
  311.             <article class="product-review-item">
  312.               <div class="review-avatar"></div>
  313.               <div class="review-item-body">
  314.                 <div class="review-item-head">
  315.                   <div>
  316.                     <strong>Bizidin</strong>
  317.                     <span aria-label="5 из 5">★★★★★</span>
  318.                   </div>
  319.                   <time>Сегодня, 11:48</time>
  320.                 </div>
  321.                 <b class="review-product-chip">DOOM 2016</b>
  322.                 <p>Отзывчивый и приятный продавец, помог не только с товаром, но и с другим вопросом. Рекомендую!</p>
  323.                 <div class="review-actions">
  324.                   <button class="like-action is-liked" type="button" aria-label="13 лайков">
  325.                     <img src="./assets/like-filled.svg" alt="" />
  326.                     <span>13</span>
  327.                   </button>
  328.                   <button type="button">Ответить</button>
  329.                 </div>
  330.               </div>
  331.             </article>
  332.             <article class="product-review-item">
  333.               <div class="review-avatar"></div>
  334.               <div class="review-item-body">
  335.                 <div class="review-item-head">
  336.                   <div>
  337.                     <strong>maxx_88</strong>
  338.                     <span aria-label="4 из 5">★★★★<i>★</i></span>
  339.                   </div>
  340.                   <time>2 дня назад</time>
  341.                 </div>
  342.                 <b class="review-product-chip">DOOM Eternal</b>
  343.                 <p>Немного подождал ответа поддержки, но в итоге всё ок. Цена приятная.</p>
  344.                 <div class="review-actions">
  345.                   <button class="like-action" type="button" aria-label="3 лайка">
  346.                     <img src="./assets/like-outline.svg" alt="" />
  347.                     <span>3</span>
  348.                   </button>
  349.                   <button type="button">Ответить</button>
  350.                 </div>
  351.               </div>
  352.             </article>
  353.           </div>
  354.         </section>
  355.         <section class="product-questions-block product-tab-panel" data-product-panel="comments" aria-label="Вопросы о товаре" hidden>
  356.           <div class="question-form-grid">
  357.             <form class="product-question-form" data-question-form>
  358.               <h3>Задайте вопрос о товаре</h3>
  359.               <label class="question-textarea">
  360.                 <textarea placeholder="Ваш вопрос по товару..."></textarea>
  361.               </label>
  362.               <div class="question-form-footer">
  363.                 <p class="question-error" hidden>Напишите вопрос перед отправкой</p>
  364.                 <p class="question-success" hidden>Отправлено - ответит продавец или покупатели</p>
  365.                 <button class="question-submit" type="submit">Отправить</button>
  366.               </div>
  367.             </form>
  368.           </div>
  369.           <div class="product-question-list">
  370.             <article class="product-question-item">
  371.               <div class="question-main">
  372.                 <div class="review-avatar question-avatar"></div>
  373.                 <div class="question-item-body">
  374.                   <div class="review-item-head">
  375.                     <strong>Vladislav</strong>
  376.                     <time>Сегодня, 10:12</time>
  377.                   </div>
  378.                   <p>А регион точно РФ? Активируется на российском аккаунте?</p>
  379.                   <div class="review-actions question-actions">
  380.                     <button class="like-action" type="button" aria-label="4 лайка">
  381.                       <img src="./assets/like-outline.svg" alt="" />
  382.                       <span>4</span>
  383.                     </button>
  384.                     <button type="button">Ответить</button>
  385.                   </div>
  386.                   <div class="question-answer">
  387.                     <div class="review-avatar seller-answer-avatar"></div>
  388.                     <div class="question-answer-body">
  389.                       <div class="review-item-head">
  390.                         <strong>PlayPapa <em>· Продавец</em></strong>
  391.                         <time>Сегодня, 10:20</time>
  392.                       </div>
  393.                       <p>Да, ключ для региона РФ+СНГ, активируется на российском аккаунте Steam без VPN.</p>
  394.                       <div class="review-actions question-actions">
  395.                         <button class="like-action is-liked" type="button" aria-label="9 лайков">
  396.                           <img src="./assets/like-filled.svg" alt="" />
  397.                           <span>9</span>
  398.                         </button>
  399.                       </div>
  400.                     </div>
  401.                   </div>
  402.                 </div>
  403.               </div>
  404.             </article>
  405.           </div>
  406.         </section>
  407.         <section class="product-images-block product-tab-panel" data-product-panel="images" aria-label="Изображения товара" hidden>
  408.           <div class="product-images-grid">
  409.             {% if productGallery %}
  410.               {% for image in productGallery %}
  411.                 <button class="product-image-preview" type="button" aria-label="Открыть изображение {{ loop.index }}">
  412.                   <img src="{{ image.url|default(productImage) }}" alt="{{ productName }} изображение {{ loop.index }}" />
  413.                 </button>
  414.               {% endfor %}
  415.             {% else %}
  416.               <button class="product-image-preview" type="button" aria-label="Открыть изображение 1">
  417.                 <img src="{{ productImage }}" alt="{{ productName }} скриншот 1" />
  418.               </button>
  419.             {% endif %}
  420.           </div>
  421.         </section>
  422.       </section>
  423.       <section class="shell product-section product-recommendations">
  424.         <div class="section-head">
  425.           <h2>С этим товаром смотрят</h2>
  426.           <a class="show-all" href="/catalog">Показать все</a>
  427.         </div>
  428.         <div class="product-grid product-recommend-grid">
  429.           {% for related in relatedProducts %}
  430.             <article class="product-card">
  431.               <div class="product-cover">
  432.                 <img src="{{ related.image_url|default('./assets/product-pubg.png') }}" alt="" />
  433.                 {% if related.discount_label|default('') %}
  434.                   <span class="discount red">{{ related.discount_label }}</span>
  435.                 {% endif %}
  436.               </div>
  437.               <div class="product-body">
  438.                 <h3>{{ related.name|default('Товар PlayPapa') }}</h3>
  439.                 <div class="price-line"><strong>{{ related.price_label|default('Цена уточняется') }}</strong></div>
  440.                 <a href="{{ related.url|default('/catalog') }}">Купить</a>
  441.               </div>
  442.             </article>
  443.           {% else %}
  444.             <article class="product-card">
  445.               <div class="product-cover">
  446.                 <img src="./assets/product-pubg.png" alt="" />
  447.               </div>
  448.               <div class="product-body">
  449.                 <h3>Популярные товары PlayPapa</h3>
  450.                 <div class="price-line"><strong>Каталог</strong></div>
  451.                 <a href="/catalog">Смотреть</a>
  452.               </div>
  453.             </article>
  454.           {% endfor %}
  455.         </div>
  456.       </section>
  457. {% endblock %}
  458. {% block body_extras %}
  459. <aside class="sticky-purchase-bar" aria-label="Быстрая покупка">
  460.       <div class="sticky-purchase-inner">
  461.         <img class="sticky-purchase-thumb" src="{{ productImage }}" alt="" />
  462.         <strong class="sticky-purchase-title">{{ productName }}</strong>
  463.         <div class="sticky-purchase-price">
  464.           <strong>{{ productPrice }}</strong>
  465.           {% if not productData %}<del>6 499 ₽</del>{% endif %}
  466.         </div>
  467.         <button class="sticky-purchase-button" type="button" data-checkout-open aria-haspopup="dialog" aria-controls="checkoutModal">Купить сейчас</button>
  468.       </div>
  469.     </aside>
  470. {% include 'components/modals/checkout_product.html.twig' %}
  471. {% include 'components/modals/image_lightbox.html.twig' %}
  472. {% endblock %}
  473. {% block scripts %}
  474. <script>
  475.       const root = document.documentElement;
  476.       const themeSwitch = document.querySelector(".theme-switch");
  477.       const themeButtons = document.querySelectorAll(".theme-btn[data-theme]");
  478.       const setTheme = (theme, animate = false) => {
  479.         if (animate) {
  480.           document.body.classList.remove("theme-animating");
  481.           void document.body.offsetWidth;
  482.           document.body.classList.add("theme-animating");
  483.         }
  484.         root.dataset.theme = theme;
  485.         try {
  486.           localStorage.setItem("playpapa-theme", theme);
  487.         } catch {}
  488.         themeButtons.forEach((button) => {
  489.           const active = button.dataset.theme === theme;
  490.           button.classList.toggle("is-active", active);
  491.           button.setAttribute("aria-pressed", active ? "true" : "false");
  492.         });
  493.         if (animate) window.setTimeout(() => document.body.classList.remove("theme-animating"), 560);
  494.       };
  495.       setTheme(root.dataset.theme === "dark" ? "dark" : "light");
  496.       themeButtons.forEach((button) => button.addEventListener("click", () => setTheme(button.dataset.theme, true)));
  497.       themeSwitch?.addEventListener("click", (event) => {
  498.         if (event.target.closest(".theme-btn")) return;
  499.         setTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
  500.       });
  501.       const profileMenu = document.querySelector(".profile-menu");
  502.       const profileButton = profileMenu?.querySelector(".profile-btn");
  503.       const profilePopover = document.getElementById("profilePopover");
  504.       const setProfileOpen = (open, returnFocus = false) => {
  505.         if (!profileMenu || !profilePopover || !profileButton) return;
  506.         profileMenu.classList.toggle("is-open", open);
  507.         profileButton.setAttribute("aria-expanded", open ? "true" : "false");
  508.         profilePopover.setAttribute("aria-hidden", open ? "false" : "true");
  509.         profilePopover.toggleAttribute("inert", !open);
  510.         document.body.classList.toggle("profile-open", open);
  511.         if (returnFocus) profileButton.focus();
  512.       };
  513.       profileButton?.addEventListener("click", (event) => {
  514.         event.stopPropagation();
  515.         setProfileOpen(!profileMenu?.classList.contains("is-open"));
  516.       });
  517.       document.addEventListener("pointerdown", (event) => {
  518.         if (!profileMenu?.classList.contains("is-open") || profileMenu.contains(event.target)) return;
  519.         setProfileOpen(false);
  520.       });
  521.       document.addEventListener("keydown", (event) => {
  522.         if (event.key !== "Escape" || !profileMenu?.classList.contains("is-open")) return;
  523.         setProfileOpen(false, true);
  524.       });
  525.       const catalogLayer = document.getElementById("catalogMegaMenu");
  526.       const catalogButton = document.querySelector(".catalog-btn:not([href])");
  527.       const catalogSideLinks = document.querySelectorAll(".catalog-side-link");
  528.       const catalogFeaturedItems = [
  529.         ["service-steam.png", "Пополнение Steam"],
  530.         ["service-brawl.png", "Game Pass"],
  531.         ["service-steam.png", "Пополнение PSN"],
  532.         ["service-brawl.png", "PS Plus"],
  533.         ["service-pubg.png", "Grand Theft Auto V"],
  534.         ["service-roblox.png", "Minecraft"],
  535.         ["service-playstation.png", "Diablo 4"],
  536.         ["service-mobile.png", "ARC Raiders"],
  537.         ["service-chatgpt.png", "Dead by Daylight"],
  538.       ];
  539.       const catalogNewItems = [
  540.         ["article-paradise.png", "Gothic 1 Remake"],
  541.         ["article-sale.png", "Forza Horizon 6"],
  542.         ["product-pubg.png", "Assassin's Creed: Black..."],
  543.         ["article-mobile.png", "007 First Light"],
  544.         ["article-sale.png", "Call of Duty: Modern..."],
  545.         ["product-pubg.png", "Marvel's Wolverine"],
  546.         ["article-paradise.png", "UFC 6"],
  547.         ["article-sale.png", "Subnautica 2"],
  548.         ["product-pubg.png", "SILENT HILL: Townfall"],
  549.       ];
  550.       const catalogItemMarkup = ([image, label]) => `
  551.         <a class="catalog-showcase-item" href="#" aria-label="${label}">
  552.           <img src="./assets/${image}" alt="">
  553.           <span>${label}</span>
  554.         </a>
  555.       `;
  556.       const catalogFeatured = document.getElementById("catalogFeatured");
  557.       const catalogNew = document.getElementById("catalogNew");
  558.       if (catalogFeatured) catalogFeatured.innerHTML = catalogFeaturedItems.map(catalogItemMarkup).join("");
  559.       if (catalogNew) catalogNew.innerHTML = catalogNewItems.map(catalogItemMarkup).join("");
  560.       const setCatalogOpen = (open) => {
  561.         if (!catalogLayer || !catalogButton) return;
  562.         catalogLayer.classList.toggle("is-open", open);
  563.         catalogLayer.setAttribute("aria-hidden", open ? "false" : "true");
  564.         catalogLayer.toggleAttribute("inert", !open);
  565.         document.body.classList.toggle("catalog-open", open);
  566.         catalogButton.setAttribute("aria-expanded", open ? "true" : "false");
  567.       };
  568.       catalogButton?.addEventListener("click", (event) => {
  569.         event.preventDefault();
  570.         setProfileOpen(false);
  571.         setCatalogOpen(!catalogLayer?.classList.contains("is-open"));
  572.       });
  573.       catalogSideLinks.forEach((link) => {
  574.         link.addEventListener("click", () => {
  575.           catalogSideLinks.forEach((item) => item.classList.toggle("is-active", item === link));
  576.         });
  577.       });
  578.       catalogLayer?.querySelectorAll('a[href="#"]').forEach((link) => {
  579.         link.addEventListener("click", (event) => event.preventDefault());
  580.       });
  581.       document.addEventListener("pointerdown", (event) => {
  582.         if (!catalogLayer?.classList.contains("is-open")) return;
  583.         if (catalogLayer.contains(event.target) || catalogButton?.contains(event.target)) return;
  584.         setCatalogOpen(false);
  585.       });
  586.       document.addEventListener("keydown", (event) => {
  587.         if (event.key !== "Escape" || !catalogLayer?.classList.contains("is-open")) return;
  588.         setCatalogOpen(false);
  589.         catalogButton?.focus();
  590.       });
  591.       const checkoutModal = document.getElementById("checkoutModal");
  592.       const checkoutDialog = checkoutModal?.querySelector(".checkout-modal-dialog");
  593.       const checkoutOpenButtons = document.querySelectorAll("[data-checkout-open]");
  594.       const checkoutCloseButtons = checkoutModal?.querySelectorAll("[data-checkout-close]");
  595.       const checkoutPaymentButtons = checkoutModal?.querySelectorAll(".checkout-payment-option");
  596.       const checkoutPaymentInput = checkoutModal?.querySelector('input[name="payment"]');
  597.       const checkoutEmailInput = checkoutModal?.querySelector('input[name="email"]');
  598.       const checkoutEmailField = checkoutEmailInput?.closest(".checkout-field");
  599.       const checkoutEmailError = checkoutEmailField?.querySelector("small");
  600.       const checkoutProductIdInput = checkoutModal?.querySelector("[data-checkout-product-id]");
  601.       const checkoutDynamicFields = checkoutModal?.querySelector("[data-checkout-fields]");
  602.       const checkoutResult = checkoutModal?.querySelector("[data-checkout-result]");
  603.       const checkoutEndpoint = "{{ path('payment_cryptobot_create') }}";
  604.       const checkoutStatusEndpointBase = "/checkout/galaxylink/test/";
  605.       const checkoutRequiresAuth = {{ user ? 'false' : 'true' }};
  606.       let checkoutReturnFocus = null;
  607.       let checkoutPollTimer = 0;
  608.       const checkoutAuthReturnPath = () => `${window.location.pathname}${window.location.search}${window.location.hash}`;
  609.       const openCheckoutAuth = (trigger = null) => {
  610.         const returnPath = checkoutAuthReturnPath();
  611.         if (window.PlayPapaAuth?.openModal?.("login", trigger, returnPath)) return;
  612.         const url = new URL("/", window.location.origin);
  613.         url.searchParams.set("auth", "login");
  614.         url.searchParams.set("redirect", returnPath);
  615.         url.searchParams.set("reason", "checkout");
  616.         window.location.assign(`${url.pathname}${url.search}`);
  617.       };
  618.       const setCheckoutEmailError = (show) => {
  619.         checkoutEmailField?.classList.toggle("is-invalid", show);
  620.         if (checkoutEmailError) checkoutEmailError.hidden = !show;
  621.       };
  622.       const checkoutEscape = (value) =>
  623.         String(value ?? "")
  624.           .replaceAll("&", "&amp;")
  625.           .replaceAll("<", "&lt;")
  626.           .replaceAll(">", "&gt;")
  627.           .replaceAll('"', "&quot;")
  628.           .replaceAll("'", "&#039;");
  629.       const checkoutFieldMeta = {
  630.         invite_url: ["Ссылка-приглашение Steam", "https://s.team/p/xxxx/yyyyyyyy", "url"],
  631.         user_id: ["User ID", "ID пользователя", "text"],
  632.         server_id: ["Сервер", "Europe", "text"],
  633.         region_id: ["Регион", "Europe", "text"],
  634.         ip_address: ["IP-адрес", "Автоматически, если оставить пустым", "text"],
  635.         login: ["Логин аккаунта", "login@example.com", "text"],
  636.         password: ["Пароль аккаунта", "Пароль", "password"],
  637.         backup_codes: ["Backup codes", "Коды через пробел", "text"],
  638.         order_comment: ["Комментарий", "Комментарий для оператора", "text"],
  639.         roblox_username: ["Roblox username", "PlayerOne", "text"],
  640.         roblox_password: ["Roblox password", "Пароль Roblox", "password"],
  641.       };
  642.       const parseCheckoutForms = (raw) => {
  643.         try {
  644.           const forms = JSON.parse(raw || "[]");
  645.           return Array.isArray(forms) ? forms.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean) : [];
  646.         } catch {
  647.           return [];
  648.         }
  649.       };
  650.       const checkoutFormsFor = (forms, deliveryType) => {
  651.         const result = Array.from(new Set(forms));
  652.         if (deliveryType === "steam_gift" && !result.includes("invite_url")) result.push("invite_url");
  653.         return result.filter((field) => checkoutFieldMeta[field]);
  654.       };
  655.       const renderCheckoutFields = (forms, deliveryType) => {
  656.         if (!checkoutDynamicFields) return;
  657.         const fields = checkoutFormsFor(forms, deliveryType);
  658.         checkoutDynamicFields.hidden = fields.length === 0;
  659.         checkoutDynamicFields.innerHTML = fields.map((field) => {
  660.           const [label, placeholder, type] = checkoutFieldMeta[field];
  661.           const required = field === "ip_address" || field === "order_comment" ? "" : "required";
  662.           return `
  663.             <label class="checkout-field checkout-extra-field">
  664.               <span>${checkoutEscape(label)}</span>
  665.               <input type="${type}" name="${checkoutEscape(field)}" placeholder="${checkoutEscape(placeholder)}" autocomplete="off" ${required} data-checkout-extra-input />
  666.               <small hidden>Заполните поле</small>
  667.             </label>
  668.           `;
  669.         }).join("");
  670.       };
  671.       const collectCheckoutFields = () => {
  672.         const fields = {};
  673.         const inputs = Array.from(checkoutDynamicFields?.querySelectorAll("[data-checkout-extra-input]") || []);
  674.         for (const input of inputs) {
  675.           if (!input.checkValidity()) {
  676.             input.closest(".checkout-field")?.classList.add("is-invalid");
  677.             input.focus();
  678.             return null;
  679.           }
  680.           input.closest(".checkout-field")?.classList.remove("is-invalid");
  681.           fields[input.name] = input.value.trim();
  682.         }
  683.         return fields;
  684.       };
  685.       const renderCheckoutResult = (order, error = "") => {
  686.         if (!checkoutResult) return;
  687.         checkoutResult.hidden = false;
  688.         checkoutResult.classList.toggle("is-error", Boolean(error));
  689.         checkoutResult.classList.toggle("is-success", !error && order?.status === "completed");
  690.         if (error) {
  691.           checkoutResult.innerHTML = `<strong>Не получилось создать оплату</strong><p>${checkoutEscape(error)}</p>`;
  692.           return;
  693.         }
  694.         const items = Array.isArray(order?.items) ? order.items : [];
  695.         const codes = items.map((item) => `
  696.           <li>
  697.             <span>${checkoutEscape(item.label || "Код")}</span>
  698.             <code>${checkoutEscape(item.code || "")}</code>
  699.           </li>
  700.         `).join("");
  701.         const verification = order?.verification?.url
  702.           ? `<a href="${checkoutEscape(order.verification.url)}" target="_blank" rel="noreferrer">Открыть verification-ссылку</a>`
  703.           : "";
  704.         checkoutResult.innerHTML = `
  705.           <strong>${checkoutEscape(order?.status_label || "Заказ создан")}</strong>
  706.           <p>${checkoutEscape(order?.message || "")}</p>
  707.           <dl>
  708.             <div><dt>GalaxyLink</dt><dd>${checkoutEscape(order?.provider_order_id || "ожидаем")}</dd></div>
  709.             <div><dt>Partner ID</dt><dd>${checkoutEscape(order?.partner_order_id || "")}</dd></div>
  710.             ${order?.amount_label ? `<div><dt>Списание</dt><dd>${checkoutEscape(order.amount_label)}</dd></div>` : ""}
  711.           </dl>
  712.           ${codes ? `<ul>${codes}</ul>` : ""}
  713.           ${verification}
  714.         `;
  715.       };
  716.       const refreshCheckoutOrder = async (orderId) => {
  717.         const response = await fetch(`${checkoutStatusEndpointBase}${encodeURIComponent(orderId)}`, {
  718.           headers: { Accept: "application/json" },
  719.         });
  720.         const payload = await response.json();
  721.         if (!response.ok || !payload.ok) throw new Error(payload.error || `HTTP ${response.status}`);
  722.         renderCheckoutResult(payload.order);
  723.         return payload.order;
  724.       };
  725.       const pollCheckoutOrder = (order) => {
  726.         window.clearInterval(checkoutPollTimer);
  727.         if (!order?.id || order.terminal || (Array.isArray(order.items) && order.items.length > 0)) return;
  728.         let attempts = 0;
  729.         checkoutPollTimer = window.setInterval(async () => {
  730.           attempts += 1;
  731.           try {
  732.             const nextOrder = await refreshCheckoutOrder(order.id);
  733.             if (nextOrder.terminal || (Array.isArray(nextOrder.items) && nextOrder.items.length > 0) || attempts >= 8) {
  734.               window.clearInterval(checkoutPollTimer);
  735.             }
  736.           } catch (error) {
  737.             window.clearInterval(checkoutPollTimer);
  738.           }
  739.         }, 3500);
  740.       };
  741.       const setCheckoutOpen = (open, returnFocus = false) => {
  742.         if (!checkoutModal) return;
  743.         if (open) {
  744.           window.clearInterval(checkoutPollTimer);
  745.           if (checkoutResult) {
  746.             checkoutResult.hidden = true;
  747.             checkoutResult.innerHTML = "";
  748.           }
  749.           checkoutReturnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
  750.           checkoutModal.hidden = false;
  751.           checkoutModal.removeAttribute("inert");
  752.           checkoutModal.setAttribute("aria-hidden", "false");
  753.           document.body.classList.add("checkout-modal-open");
  754.           setCatalogOpen(false);
  755.           setProfileOpen(false);
  756.           requestAnimationFrame(() => checkoutModal.classList.add("is-open"));
  757.           window.setTimeout(() => checkoutModal.querySelector("[data-checkout-close]")?.focus(), 80);
  758.           return;
  759.         }
  760.         checkoutModal.classList.remove("is-open");
  761.         checkoutModal.setAttribute("aria-hidden", "true");
  762.         checkoutModal.setAttribute("inert", "");
  763.         document.body.classList.remove("checkout-modal-open");
  764.         window.setTimeout(() => {
  765.           if (!checkoutModal.classList.contains("is-open")) checkoutModal.hidden = true;
  766.         }, 200);
  767.         if (returnFocus) checkoutReturnFocus?.focus();
  768.       };
  769.       checkoutOpenButtons.forEach((button) => {
  770.         button.addEventListener("click", () => {
  771.           if (checkoutRequiresAuth) {
  772.             openCheckoutAuth(button);
  773.             return;
  774.           }
  775.           setCheckoutOpen(true);
  776.         });
  777.       });
  778.       checkoutCloseButtons?.forEach((button) => {
  779.         button.addEventListener("click", () => setCheckoutOpen(false, true));
  780.       });
  781.       checkoutModal?.addEventListener("pointerdown", (event) => {
  782.         if (event.target !== checkoutModal || checkoutDialog?.contains(event.target)) return;
  783.         setCheckoutOpen(false, true);
  784.       });
  785.       checkoutPaymentButtons?.forEach((button) => {
  786.         button.addEventListener("click", () => {
  787.           checkoutPaymentButtons.forEach((item) => {
  788.             const selected = item === button;
  789.             item.classList.toggle("is-selected", selected);
  790.             item.setAttribute("aria-checked", selected ? "true" : "false");
  791.           });
  792.           if (checkoutPaymentInput) checkoutPaymentInput.value = button.dataset.paymentMethod || "cryptobot";
  793.         });
  794.       });
  795.       checkoutEmailInput?.addEventListener("input", () => setCheckoutEmailError(false));
  796.       renderCheckoutFields(
  797.         parseCheckoutForms(checkoutDynamicFields?.dataset.initialForms || "[]"),
  798.         checkoutDynamicFields?.dataset.initialDeliveryType || "digital_item"
  799.       );
  800.       checkoutDialog?.addEventListener("submit", async (event) => {
  801.         event.preventDefault();
  802.         if (!checkoutEmailInput?.checkValidity()) {
  803.           setCheckoutEmailError(true);
  804.           checkoutEmailInput?.focus();
  805.           return;
  806.         }
  807.         const fields = collectCheckoutFields();
  808.         if (fields === null) {
  809.           renderCheckoutResult(null, "Заполните обязательные поля для выдачи товара.");
  810.           return;
  811.         }
  812.         setCheckoutEmailError(false);
  813.         const submitButton = checkoutDialog.querySelector(".checkout-submit");
  814.         const originalText = submitButton?.textContent || "";
  815.         if (submitButton) {
  816.           submitButton.disabled = true;
  817.           submitButton.textContent = "Создаем счёт...";
  818.         }
  819.         renderCheckoutResult({ status_label: "Создаем счёт CryptoBot", message: "Подготовили заказ и создаём ссылку на оплату." });
  820.         try {
  821.           const response = await fetch(checkoutEndpoint, {
  822.             method: "POST",
  823.             headers: {
  824.               Accept: "application/json",
  825.               "Content-Type": "application/json",
  826.             },
  827.             body: JSON.stringify({
  828.               product_id: checkoutProductIdInput?.value || "",
  829.               email: checkoutEmailInput.value,
  830.               payment: checkoutPaymentInput?.value || "cryptobot",
  831.               fields,
  832.             }),
  833.           });
  834.           const payload = await response.json();
  835.           if (response.status === 401 && payload.auth_required) {
  836.             setCheckoutOpen(false, true);
  837.             openCheckoutAuth(submitButton);
  838.             return;
  839.           }
  840.           if (!response.ok || !payload.ok) throw new Error(payload.error || `HTTP ${response.status}`);
  841.           if (payload.status_url) {
  842.             renderCheckoutResult({ status_label: "Переходим к заказу", message: "Открываем страницу статуса. Там будет кнопка оплаты и автообновление выдачи." });
  843.             window.location.href = payload.status_url;
  844.             return;
  845.           }
  846.           if (payload.redirect_url) {
  847.             renderCheckoutResult({ status_label: "Переходим к оплате", message: "Открываем CryptoBot." });
  848.             window.location.href = payload.redirect_url;
  849.             return;
  850.           }
  851.           renderCheckoutResult(payload.order);
  852.           pollCheckoutOrder(payload.order);
  853.         } catch (error) {
  854.           renderCheckoutResult(null, error.message || "Неизвестная ошибка оплаты.");
  855.         } finally {
  856.           if (submitButton) {
  857.             submitButton.disabled = false;
  858.             submitButton.textContent = originalText;
  859.           }
  860.         }
  861.       });
  862.       document.addEventListener("keydown", (event) => {
  863.         if (event.key !== "Escape" || !checkoutModal?.classList.contains("is-open")) return;
  864.         setCheckoutOpen(false, true);
  865.       });
  866.       document.querySelectorAll(".variant-option").forEach((button) => {
  867.         button.addEventListener("click", () => {
  868.           if (button.disabled) return;
  869.           document.querySelectorAll(".variant-option").forEach((item) => item.classList.remove("is-selected"));
  870.           document.querySelectorAll(".variant-option small").forEach((item) => {
  871.             const option = item.closest(".variant-option");
  872.             if (!option || option === button) return;
  873.             item.textContent = option.dataset.price || item.textContent;
  874.           });
  875.           button.classList.add("is-selected");
  876.           const note = button.querySelector("small");
  877.           if (note) note.textContent = "Выбрано";
  878.           const price = button.dataset.price || "";
  879.           const context = button.dataset.context || "";
  880.           const regionLabel = button.dataset.regionLabel || "";
  881.           const regionRestrictionTitle = button.dataset.regionRestrictionTitle || "";
  882.           const regionRestrictionText = button.dataset.regionRestrictionText || "";
  883.           const regionRestrictionCountries = button.dataset.regionRestrictionCountries || "";
  884.           const name = button.dataset.name || "";
  885.           const deliveryLabel = button.dataset.deliveryLabel || "";
  886.           const deliveryShortLabel = button.dataset.deliveryShortLabel || deliveryLabel;
  887.           const deliveryDescription = button.dataset.deliveryDescription || "";
  888.           const deliveryType = button.dataset.deliveryType || "digital_item";
  889.           const forms = parseCheckoutForms(button.dataset.forms || "[]");
  890.           const discountLabel = button.dataset.discountLabel || "";
  891.           if (checkoutProductIdInput && button.dataset.productId) checkoutProductIdInput.value = button.dataset.productId;
  892.           renderCheckoutFields(forms, deliveryType);
  893.           if (checkoutResult) checkoutResult.hidden = true;
  894.           if (price) {
  895.             document.querySelectorAll(".price-main strong, .mobile-price-row strong, .sticky-purchase-price strong, .checkout-product-price strong, [data-checkout-total-price]").forEach((node) => {
  896.               node.textContent = price;
  897.             });
  898.             document.querySelectorAll(".checkout-submit").forEach((node) => {
  899.               node.textContent = `Оплатить ${price}`;
  900.             });
  901.           }
  902.           if (context) {
  903.             document.querySelectorAll(".buy-context span").forEach((node) => {
  904.               node.textContent = context;
  905.             });
  906.             const checkoutContext = document.querySelector(".checkout-product span");
  907.             if (checkoutContext) checkoutContext.textContent = `${context} · ${deliveryShortLabel || "мгновенно"}`;
  908.           }
  909.           if (deliveryLabel) {
  910.             document.querySelectorAll("[data-delivery-label-target]").forEach((node) => {
  911.               node.textContent = deliveryLabel;
  912.             });
  913.           }
  914.           if (deliveryDescription) {
  915.             document.querySelectorAll("[data-delivery-description-target]").forEach((node) => {
  916.               node.textContent = deliveryDescription;
  917.             });
  918.           }
  919.           if (regionLabel) {
  920.             document.querySelectorAll("[data-spec-region]").forEach((node) => {
  921.               node.textContent = regionLabel;
  922.             });
  923.           }
  924.           document.querySelectorAll("[data-region-restriction]").forEach((node) => {
  925.             const hasRestriction = regionRestrictionTitle !== "" || regionRestrictionText !== "" || regionRestrictionCountries !== "";
  926.             node.hidden = !hasRestriction;
  927.             const title = node.querySelector("[data-region-restriction-title]");
  928.             const text = node.querySelector("[data-region-restriction-text]");
  929.             const countries = node.querySelector("[data-region-restriction-countries]");
  930.             if (title) title.textContent = regionRestrictionTitle;
  931.             if (text) text.textContent = regionRestrictionText;
  932.             if (countries) countries.textContent = regionRestrictionCountries;
  933.           });
  934.           document.querySelectorAll("[data-discount-label-target]").forEach((node) => {
  935.             node.textContent = discountLabel;
  936.             node.hidden = discountLabel === "";
  937.           });
  938.           if (name) {
  939.             document.querySelectorAll(".sticky-purchase-title, .checkout-product strong").forEach((node) => {
  940.               node.textContent = name;
  941.             });
  942.           }
  943.           if (button.dataset.url && window.history?.replaceState) {
  944.             window.history.replaceState(null, "", button.dataset.url);
  945.           }
  946.         });
  947.       });
  948.       const productTabButtons = document.querySelectorAll("[data-product-tab]");
  949.       const productPanels = document.querySelectorAll("[data-product-panel]");
  950.       const setProductTab = (target, scrollToPanel = false) => {
  951.         const targetPanel = document.querySelector(`[data-product-panel="${target}"]`);
  952.         if (!targetPanel) return;
  953.         productPanels.forEach((panel) => {
  954.           const active = panel.dataset.productPanel === target;
  955.           panel.classList.toggle("is-active", active);
  956.           panel.hidden = !active;
  957.         });
  958.         productTabButtons.forEach((button) => {
  959.           button.classList.toggle("is-active", button.dataset.productTab === target);
  960.         });
  961.         if (scrollToPanel) {
  962.           document.querySelector(".product-info")?.scrollIntoView({ block: "start", behavior: "smooth" });
  963.         }
  964.       };
  965.       productTabButtons.forEach((button) => {
  966.         button.addEventListener("click", () => {
  967.           setProductTab(button.dataset.productTab, false);
  968.         });
  969.       });
  970.       document.querySelectorAll("[data-product-tab-link]").forEach((link) => {
  971.         link.addEventListener("click", (event) => {
  972.           event.preventDefault();
  973.           setProductTab(link.dataset.productTabLink, true);
  974.         });
  975.       });
  976.       const productCover = document.querySelector("[data-product-cover]");
  977.       const productThumbs = Array.from(document.querySelectorAll("[data-product-thumb]"));
  978.       let productGalleryTimer = null;
  979.       let productCoverSwapTimer = null;
  980.       const setProductCover = (button, scrollThumb = false) => {
  981.         if (!button) return;
  982.         const src = button.dataset.fullImage || button.querySelector("img")?.src;
  983.         if (!src) return;
  984.         const group = button.parentElement;
  985.         group?.querySelectorAll(".is-active").forEach((item) => item.classList.remove("is-active"));
  986.         button.classList.add("is-active");
  987.         if (productCover) {
  988.           if (productCoverSwapTimer) window.clearTimeout(productCoverSwapTimer);
  989.           productCover.classList.add("is-changing");
  990.           productCoverSwapTimer = window.setTimeout(() => {
  991.             productCover.src = src;
  992.             productCover.alt = button.getAttribute("aria-label") || productCover.alt;
  993.             productCover.classList.remove("is-changing");
  994.           }, 120);
  995.         }
  996.         if (scrollThumb && group && group.scrollWidth > group.clientWidth) {
  997.           group.scrollTo({ left: Math.max(0, button.offsetLeft - 12), behavior: "smooth" });
  998.         }
  999.       };
  1000.       const restartProductGalleryAutoplay = () => {
  1001.         if (productGalleryTimer) window.clearInterval(productGalleryTimer);
  1002.         if (productThumbs.length < 2) return;
  1003.         productGalleryTimer = window.setInterval(() => {
  1004.           const activeIndex = Math.max(0, productThumbs.findIndex((button) => button.classList.contains("is-active")));
  1005.           setProductCover(productThumbs[(activeIndex + 1) % productThumbs.length], true);
  1006.         }, 4500);
  1007.       };
  1008.       productThumbs.forEach((button) => {
  1009.         button.addEventListener("click", () => {
  1010.           setProductCover(button, true);
  1011.           restartProductGalleryAutoplay();
  1012.         });
  1013.       });
  1014.       restartProductGalleryAutoplay();
  1015.       document.querySelectorAll("[data-description-toggle]").forEach((button) => {
  1016.         const description = button.closest(".product-description");
  1017.         const extra = description?.querySelector("[data-description-extra]");
  1018.         const label = button.querySelector("span");
  1019.         const mobileDescription = window.matchMedia("(max-width: 767px)");
  1020.         const setDescriptionOpen = (isOpen) => {
  1021.           if (!description || !extra || !label) return;
  1022.           description.classList.toggle("is-collapsed", !isOpen);
  1023.           extra.hidden = !isOpen;
  1024.           button.setAttribute("aria-expanded", isOpen ? "true" : "false");
  1025.           label.textContent = isOpen ? (mobileDescription.matches ? "Свернуть" : "Скрыть полностью") : "Показать полностью";
  1026.         };
  1027.         button.addEventListener("click", () => {
  1028.           setDescriptionOpen(button.getAttribute("aria-expanded") !== "true");
  1029.         });
  1030.         const refreshDescriptionLabel = () => setDescriptionOpen(button.getAttribute("aria-expanded") === "true");
  1031.         if (mobileDescription.addEventListener) {
  1032.           mobileDescription.addEventListener("change", refreshDescriptionLabel);
  1033.         } else {
  1034.           mobileDescription.addListener(refreshDescriptionLabel);
  1035.         }
  1036.       });
  1037.       document.querySelectorAll("[data-review-form]").forEach((form) => {
  1038.         const textarea = form.querySelector("textarea");
  1039.         const error = form.querySelector(".review-error");
  1040.         const success = form.querySelector(".review-success");
  1041.         const ratingLabel = form.querySelector(".review-form-rating b");
  1042.         const ratingButtons = form.querySelectorAll("[data-rating]");
  1043.         const setRating = (rating) => {
  1044.           form.dataset.rating = rating;
  1045.           form.querySelector(".review-form-rating")?.classList.add("has-value");
  1046.           ratingButtons.forEach((button) => button.classList.toggle("is-active", Number(button.dataset.rating) <= rating));
  1047.           if (ratingLabel) ratingLabel.textContent = `${rating}.0`;
  1048.         };
  1049.         ratingButtons.forEach((button) => {
  1050.           button.addEventListener("click", () => setRating(Number(button.dataset.rating)));
  1051.         });
  1052.         form.querySelectorAll(".review-tags button").forEach((button) => {
  1053.           button.addEventListener("click", () => button.classList.toggle("is-active"));
  1054.         });
  1055.         textarea?.addEventListener("input", () => {
  1056.           form.classList.remove("has-error");
  1057.           if (error) error.hidden = true;
  1058.         });
  1059.         form.addEventListener("submit", (event) => {
  1060.           event.preventDefault();
  1061.           const text = textarea?.value.trim() || "";
  1062.           if (!text) {
  1063.             setRating(Number(form.dataset.rating || 5));
  1064.             form.classList.add("has-error");
  1065.             form.classList.remove("is-sent");
  1066.             if (error) error.hidden = false;
  1067.             if (success) success.hidden = true;
  1068.             textarea?.focus();
  1069.             return;
  1070.           }
  1071.           setRating(Number(form.dataset.rating || 5));
  1072.           form.classList.remove("has-error");
  1073.           form.classList.add("is-sent");
  1074.           if (error) error.hidden = true;
  1075.           if (success) success.hidden = false;
  1076.         });
  1077.       });
  1078.       document.querySelectorAll("[data-question-form]").forEach((form) => {
  1079.         const textarea = form.querySelector("textarea");
  1080.         const error = form.querySelector(".question-error");
  1081.         const success = form.querySelector(".question-success");
  1082.         textarea?.addEventListener("input", () => {
  1083.           form.classList.remove("has-error");
  1084.           if (error) error.hidden = true;
  1085.         });
  1086.         form.addEventListener("submit", (event) => {
  1087.           event.preventDefault();
  1088.           const text = textarea?.value.trim() || "";
  1089.           if (!text) {
  1090.             form.classList.add("has-error");
  1091.             form.classList.remove("is-sent");
  1092.             if (error) error.hidden = false;
  1093.             if (success) success.hidden = true;
  1094.             textarea?.focus();
  1095.             return;
  1096.           }
  1097.           form.classList.remove("has-error");
  1098.           form.classList.add("is-sent");
  1099.           if (error) error.hidden = true;
  1100.           if (success) success.hidden = false;
  1101.         });
  1102.       });
  1103.       document.querySelectorAll(".like-action").forEach((button) => {
  1104.         button.addEventListener("click", () => {
  1105.           const wasLiked = button.classList.contains("is-liked");
  1106.           const count = button.querySelector("span");
  1107.           const icon = button.querySelector("img");
  1108.           button.classList.toggle("is-liked", !wasLiked);
  1109.           if (icon) icon.src = wasLiked ? "./assets/like-outline.svg" : "./assets/like-filled.svg";
  1110.           if (count) count.textContent = String(Math.max(0, Number(count.textContent || 0) + (wasLiked ? -1 : 1)));
  1111.         });
  1112.       });
  1113.       document.querySelectorAll("[data-promo-widget]").forEach((widget) => {
  1114.         const toggle = widget.querySelector(".promo-link");
  1115.         const form = widget.querySelector(".promo-form");
  1116.         const input = widget.querySelector("input");
  1117.         const help = widget.querySelector(".promo-help");
  1118.         const success = widget.querySelector(".promo-success");
  1119.         const error = widget.querySelector(".promo-error");
  1120.         const setPromoState = (state) => {
  1121.           widget.classList.toggle("is-success", state === "success");
  1122.           widget.classList.toggle("is-error", state === "error");
  1123.           if (help) help.hidden = state !== "idle";
  1124.           if (success) success.hidden = state !== "success";
  1125.           if (error) error.hidden = state !== "error";
  1126.         };
  1127.         toggle?.addEventListener("click", () => {
  1128.           const isOpen = widget.classList.toggle("is-open");
  1129.           if (form) form.hidden = !isOpen;
  1130.           toggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
  1131.           if (isOpen) {
  1132.             setPromoState("idle");
  1133.             window.setTimeout(() => input?.focus(), 0);
  1134.           }
  1135.         });
  1136.         input?.addEventListener("input", () => setPromoState("idle"));
  1137.         form?.addEventListener("submit", (event) => {
  1138.           event.preventDefault();
  1139.           const code = input?.value.trim().toUpperCase() || "";
  1140.           if (code === "PAPA15") {
  1141.             if (input) input.value = "PAPA15";
  1142.             setPromoState("success");
  1143.             return;
  1144.           }
  1145.           setPromoState("error");
  1146.         });
  1147.       });
  1148.       const lightbox = document.querySelector("[data-image-lightbox]");
  1149.       const lightboxImage = lightbox?.querySelector("img");
  1150.       const closeLightbox = () => {
  1151.         if (!lightbox) return;
  1152.         lightbox.hidden = true;
  1153.         document.body.classList.remove("image-lightbox-open");
  1154.       };
  1155.       document.querySelectorAll(".product-image-preview").forEach((button) => {
  1156.         button.addEventListener("click", () => {
  1157.           const image = button.querySelector("img");
  1158.           if (!lightbox || !lightboxImage || !image) return;
  1159.           lightboxImage.src = image.currentSrc || image.src;
  1160.           lightboxImage.alt = image.alt;
  1161.           lightbox.hidden = false;
  1162.           document.body.classList.add("image-lightbox-open");
  1163.         });
  1164.       });
  1165.       lightbox?.querySelectorAll(".image-lightbox-backdrop, .image-lightbox-close").forEach((button) => {
  1166.         button.addEventListener("click", closeLightbox);
  1167.       });
  1168.       document.addEventListener("keydown", (event) => {
  1169.         if (event.key === "Escape") closeLightbox();
  1170.       });
  1171.       document.querySelectorAll('a[href="#"]').forEach((link) => {
  1172.         link.addEventListener("click", (event) => event.preventDefault());
  1173.       });
  1174.       document.querySelector(".search-form")?.addEventListener("submit", (event) => event.preventDefault());
  1175.     </script>
  1176. {% endblock %}