templates/product.html.twig line 1

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