{% extends 'base.html.twig' %}
{% block title %}PlayPapa{% endblock %}
{% block body_class %}preloader-active{% endblock %}
{% block head_scripts %}
<script>
window.__preloaderStart = performance.now();
try {
if ("scrollRestoration" in history) history.scrollRestoration = "manual";
if (!window.location.hash || window.location.hash === "#") {
window.addEventListener("load", () => requestAnimationFrame(() => window.scrollTo(0, 0)), { once: true });
}
} catch {}
</script>
<script>
window.playpapaCatalog = {{ catalog_payload_json|default('{"products":[],"total":0}')|raw }};
</script>
<script>
tailwind = {
config: {
theme: {
extend: {
colors: {
papa: "#268bf3",
ink: "#202838",
soft: "#f2f4f8",
},
},
},
},
};
</script>
<script src="https://cdn.tailwindcss.com" defer></script>
{% endblock %}
{% block body_start %}
<div class="page-preloader" id="pagePreloader" role="status" aria-label="Загрузка">
<div class="preloader-inner">
<div class="preloader-dots" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<div class="preloader-text">Загрузка...</div>
</div>
</div>
{% endblock %}
{% block content %}
<section class="shell hero-section">
<picture class="hero-picture">
<source media="(max-width: 767px)" srcset="./assets/hero-steam-mobile.jpg" />
<img class="hero-img" src="./assets/hero-steam-cutout.jpg" alt="Пополнение Steam" />
</picture>
<div class="hero-arrows">
<button aria-label="Назад">
<svg viewBox="0 0 24 24" fill="none"><path d="M19 12H6m5-5-5 5 5 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" /></svg>
</button>
<button aria-label="Вперед">
<svg viewBox="0 0 24 24" fill="none"><path d="M5 12h13m-5-5 5 5-5 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" /></svg>
</button>
</div>
<div class="hero-dots" aria-hidden="true"><span></span><span></span><span></span><span></span><span></span><span></span></div>
</section>
<section class="shell services-card">
<div class="services-row" id="servicesRow"></div>
<div class="topup-row">
<div class="topup-info">
<img src="./assets/steam-topup.png" alt="" />
<div>
<div class="topup-title">
Пополнение Steam
<span>5%</span>
</div>
<button class="promo-small" type="button" aria-expanded="false" aria-controls="topupPromoSection">
<span class="promo-small-label">Ввести промокод</span>
<svg viewBox="0 0 16 16" fill="none"><path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" /></svg>
</button>
</div>
</div>
<label class="form-field login-field">
<img src="./assets/profile-gray.svg" alt="" />
<input type="text" name="steam-login" placeholder="Логин Steam" />
<span class="field-info" aria-hidden="true"><img src="./assets/info-i.svg" alt="" /></span>
</label>
<div class="form-field amount-field">
<div class="amount-field-shell">
<span class="currency-mark"><span class="currency-icon currency-icon-ruble" aria-hidden="true"></span></span>
<label class="amount-control">
<small>Сумма</small>
<span class="amount-value">
<input type="text" name="amount" inputmode="numeric" value="500" maxlength="6" aria-label="Сумма" aria-expanded="false" aria-controls="topupQuickPopover" />
<span class="currency-icon currency-icon-ruble" aria-hidden="true"></span>
</span>
</label>
<span class="currency-tabs" role="group" aria-label="Валюта">
<button type="button" aria-label="Доллар" aria-pressed="false" data-topup-currency="USD"><span class="currency-icon currency-icon-dollar" aria-hidden="true"></span></button>
<button type="button" aria-label="Тенге" aria-pressed="false" data-topup-currency="KZT"><span class="currency-icon currency-icon-tenge" aria-hidden="true"></span></button>
<button class="is-active" type="button" aria-label="Рубль" aria-pressed="true" data-topup-currency="RUB"><span class="currency-icon currency-icon-ruble" aria-hidden="true"></span></button>
</span>
</div>
<div class="topup-quick-popover" id="topupQuickPopover" aria-hidden="true" inert>
<span class="topup-quick-popover-label">Быстрый выбор суммы</span>
<div class="topup-quick-popover-buttons" aria-label="Быстрый выбор суммы">
<button class="is-active" type="button" data-quick-amount="500">500 ₽</button>
<button type="button" data-quick-amount="1000">1 000 ₽</button>
<button type="button" data-quick-amount="2500">2 500 ₽</button>
<button type="button" data-quick-amount="5000">5 000 ₽</button>
</div>
</div>
</div>
<button class="pay-btn" type="button" aria-expanded="false" aria-controls="topupPaymentSection">Оплатить 500<span class="currency-icon currency-icon-ruble" aria-hidden="true"></span></button>
<div class="topup-expand" id="topupDetails" aria-hidden="true" inert>
<div class="topup-expand-clip">
<div class="topup-expand-content">
<section class="topup-expand-section topup-promo-section" id="topupPromoSection" aria-hidden="true" inert>
<div class="topup-expand-section-inner">
<label class="topup-detail-label" for="topupPromoInput">Промокод</label>
<div class="topup-promo-row">
<label class="topup-promo-input">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4 10h16v10H4V10Zm0-4h16v4H4V6Zm8 0v14M12 6c-1.7-3.1-5.2-2.8-5.2-.5C6.8 7.2 9 7.5 12 6Zm0 0c1.7-3.1 5.2-2.8 5.2-.5C17.2 7.2 15 7.5 12 6Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
</svg>
<input id="topupPromoInput" type="text" placeholder="Введите промокод" autocomplete="off" />
</label>
<button class="topup-promo-apply" type="button">Применить</button>
</div>
</div>
</section>
<div class="topup-promo-benefit" aria-live="polite" aria-hidden="true">
<img src="./assets/discount-percent.png" alt="" />
<strong>Скидка <span>5%</span> — вы экономите <b data-promo-saving>25 ₽</b></strong>
<small><span>Вы платите примерно на 12% меньше,</span><span>чем на других площадках</span></small>
</div>
<section class="topup-expand-section topup-payment-section" id="topupPaymentSection" aria-hidden="true" inert>
<div class="topup-expand-section-inner">
<h3 class="topup-detail-label topup-payment-title">Способ оплаты</h3>
<div class="topup-payment-methods" role="radiogroup" aria-label="Способ оплаты">
<button class="topup-payment-method is-disabled" type="button" role="radio" aria-checked="false" aria-disabled="true" data-payment-method="sbp" data-fee="0" disabled>
<span class="topup-payment-icon"><img src="./assets/payment-sbp.svg" alt="" /></span>
<span class="topup-payment-copy"><b>СБП</b><small>Скоро</small></span>
<span class="topup-payment-check" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m5 10 3 3 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg></span>
</button>
<button class="topup-payment-method is-disabled" type="button" role="radio" aria-checked="false" aria-disabled="true" data-payment-method="mir" data-fee="2" disabled>
<span class="topup-payment-icon"><img src="./assets/payment-mir-option.svg" alt="" /></span>
<span class="topup-payment-copy"><b>Карта МИР</b><small>Скоро</small></span>
<span class="topup-payment-check" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m5 10 3 3 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg></span>
</button>
<button class="topup-payment-method is-selected" type="button" role="radio" aria-checked="true" data-payment-method="cryptobot" data-fee="0">
<span class="topup-payment-icon"><img src="./assets/payment-btc.svg" alt="" /></span>
<span class="topup-payment-copy"><b>CryptoBot</b><small>Telegram Crypto Bot</small></span>
<span class="topup-payment-check" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m5 10 3 3 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg></span>
</button>
</div>
<div class="topup-total-row">
<div class="topup-total-copy">
<small>К зачислению на Steam</small>
<span><strong data-topup-credit>500 ₽</strong><b data-topup-fee-badge>Без комиссии</b></span>
</div>
<button class="topup-final-pay" type="button">Оплатить 500₽</button>
</div>
<div class="topup-checkout-result" data-topup-result hidden></div>
</div>
</section>
</div>
</div>
</div>
</div>
</section>
<section class="shell product-section popular-section">
<div class="section-head compact-head">
<h2><img src="./assets/icon-popular.svg" alt="" />Популярные товары</h2>
<nav class="chip-row" aria-label="Категории">
<button class="chip is-active"><img src="./assets/cat-donate.svg" alt="" />Донат</button>
<button class="chip"><img src="./assets/cat-subscribes.svg" alt="" />Подписки</button>
<button class="chip"><img src="./assets/cat-items.svg" alt="" />Предметы</button>
<button class="chip"><img src="./assets/cat-accounts.svg" alt="" />Аккаунты</button>
<button class="chip"><img src="./assets/cat-keys.svg" alt="" />Ключи</button>
<button class="chip"><img src="./assets/cat-currency.svg" alt="" />Steam Gift</button>
<button class="chip"><img src="./assets/cat-other.svg" alt="" />Другое</button>
</nav>
</div>
<div class="product-grid" data-products="popular"></div>
<a class="show-all mobile-show-all" href="/catalog">Показать все</a>
</section>
<section class="shell product-section">
<div class="section-head">
<h2><img src="./assets/icon-recommended.svg" alt="" />Рекомендованные товары</h2>
<a class="show-all" href="/catalog">Показать все</a>
</div>
<div class="product-grid" data-products="recommended"></div>
</section>
<section class="shell blogger-section">
<div class="section-head">
<h2><img src="./assets/icon-recommended-alt.svg" alt="" />Подборка от блогера</h2>
<a class="show-all" href="/catalog">Показать все</a>
</div>
<div class="blogger-layout">
<article class="blogger-card">
<div class="blogger-card-head">
<div class="blogger-author">
<img src="./assets/polka.png" alt="" />
<strong>Рекомендует Polk Zone</strong>
</div>
<button class="promo-inside">
<img src="./assets/blogger-promo-blob.svg" alt="" />
<span>Промокод внутри</span>
</button>
</div>
<img class="blogger-banner" src="./assets/blogger-promo.png" alt="Личная подборка от блогера" />
<div class="blogger-stats">
<div class="coupon-stat">
<span class="stat-icon"><img src="./assets/blogger-icon-coupon.svg" alt="" /></span>
<span class="coupon-text">Промокод<br /><b>BLOGGER5</b></span>
<span class="coupon-copy" aria-hidden="true"><img src="./assets/blogger-icon-copy.svg" alt="" /></span>
</div>
<div class="stat-item"><span class="stat-icon"><img src="./assets/blogger-icon-cart.svg" alt="" /></span><span class="stat-text"><b>12 товаров</b><small>в подборке</small></span></div>
<div class="stat-item wide"><span class="stat-icon"><img src="./assets/blogger-icon-services.svg" alt="" /></span><span class="stat-text"><b>Steam · Roblox · PUBG</b><small>и другие сервисы</small></span></div>
</div>
</article>
<aside class="blogger-side">
<article class="side-card steam-side">
<div class="side-title">
<img src="./assets/steam-topup.png" alt="" />
<div><b>Пополнение Steam</b><span>Без комиссии</span></div>
</div>
<div class="side-features">
<div><img src="./assets/blogger-icon-wallet.svg" alt="" /><b>от 500 ₽</b></div>
<div><img src="./assets/blogger-icon-lightning.svg" alt="" /><b>Моментально</b></div>
</div>
<button>Пополнить</button>
</article>
<article class="side-card keys-side">
<div class="side-title">
<img src="./assets/blogger-playpapa-logo.svg" alt="" />
<div><b>Топ ключи игр</b><span>Выбор подписчиков</span></div>
</div>
<div class="thumb-row">
<img src="./assets/thumb-game-1.jpg" alt="" />
<img src="./assets/thumb-game-2.png" alt="" />
<img src="./assets/thumb-game-3.png" alt="" />
<span>Ещё 7</span>
</div>
<button>Смотреть</button>
</article>
</aside>
</div>
</section>
<section class="shell product-section other-products">
<div class="section-head">
<h2><img src="./assets/icon-recommended-alt.svg" alt="" />Другие товары</h2>
<a class="show-all" href="/catalog">Показать все</a>
</div>
<div class="product-grid" data-products="other"></div>
</section>
<section class="shell articles-section">
<div class="section-head">
<h2><img src="./assets/icon-articles.svg" alt="" />Интересные статьи</h2>
<a class="show-all" href="/catalog">Показать все</a>
</div>
<div class="article-grid">
<article class="article-card">
<div class="article-image"><img src="./assets/article-paradise.png" alt="" /><span>Статьи</span></div>
<h3>Тайные игры EGS: Paradise Killer — детектив, который можно забрать прямо сейчас</h3>
<div class="article-meta"><span><img src="./assets/article-calendar.svg" alt="" />13 июня 2026</span><span><img src="./assets/article-eye.svg" alt="" />4 892</span></div>
</article>
<article class="article-card">
<div class="article-image"><img src="./assets/article-sale.png" alt="" /><span>Статьи</span></div>
<h3>Тайные игры EGS: Paradise Killer — детектив, который можно забрать прямо сейчас</h3>
<div class="article-meta"><span><img src="./assets/article-calendar.svg" alt="" />13 июня 2026</span><span><img src="./assets/article-eye.svg" alt="" />4 892</span></div>
</article>
<article class="article-card">
<div class="article-image"><img src="./assets/article-mobile.png" alt="" /><span>Статьи</span></div>
<h3>Тайные игры EGS: Paradise Killer — детектив, который можно забрать прямо сейчас</h3>
<div class="article-meta"><span><img src="./assets/article-calendar.svg" alt="" />13 июня 2026</span><span><img src="./assets/article-eye.svg" alt="" />4 892</span></div>
</article>
</div>
</section>
<section class="shell reviews-section">
<div class="section-head reviews-head">
<div>
<h2><img src="./assets/icon-reviews.svg" alt="" />Последние отзывы</h2>
<p>Все отзывы взяты с независимой площадки</p>
</div>
<a class="show-all" href="/catalog">Показать все</a>
</div>
<div class="review-grid">
<article class="review-card">
<div class="review-top">
<img src="./assets/review-avatar.png" alt="" />
<div><b>Bizidin</b><span>★★★★★ <strong>5.0</strong></span></div>
<time>Сегодня в 11:48</time>
</div>
<p class="review-text">Отзывчивый и приятный продавец, помог не только с товаром но и с другим вопросом. Рекомендую!</p>
<div class="review-product">
<img src="./assets/product-pubg.png" alt="" />
<b>🌸 FunTime | Полностью готовый сервер под ключ ⚡</b>
<span>139₽</span>
</div>
</article>
<article class="review-card">
<div class="review-top">
<img src="./assets/review-avatar.png" alt="" />
<div><b>Bizidin</b><span>★★★★★ <strong>5.0</strong></span></div>
<time>Сегодня в 11:48</time>
</div>
<p class="review-text">Отзывчивый и приятный продавец, помог не только с товаром но и с другим вопросом. Рекомендую!</p>
<div class="review-product">
<img src="./assets/product-pubg.png" alt="" />
<b>🌸 FunTime | Полностью готовый сервер под ключ ⚡</b>
<span>139₽</span>
</div>
</article>
<article class="review-card">
<div class="review-top">
<img src="./assets/review-avatar.png" alt="" />
<div><b>Bizidin</b><span>★★★★★ <strong>5.0</strong></span></div>
<time>Сегодня в 11:48</time>
</div>
<p class="review-text">Отзывчивый и приятный продавец, помог не только с товаром но и с другим вопросом. Рекомендую!</p>
<div class="review-product">
<img src="./assets/product-pubg.png" alt="" />
<b>🌸 FunTime | Полностью готовый сервер под ключ ⚡</b>
<span>139₽</span>
</div>
</article>
</div>
</section>
{% endblock %}
{% block body_extras %}
{% include 'components/mobile_search.html.twig' %}
<div data-mobile-panel></div>
{% endblock %}
{% block scripts %}
<script src="/assets/mobile-panel.js"></script>
<script>
const root = document.documentElement;
const themeSwitch = document.querySelector(".theme-switch");
const themeButtons = document.querySelectorAll(".theme-btn[data-theme]");
const preloader = document.getElementById("pagePreloader");
if (preloader) {
const preloaderStart = window.__preloaderStart || performance.now();
const minPreloaderTime = 600;
const maxPreloaderTime = 3000;
let preloaderHidden = false;
const hidePreloader = () => {
if (preloaderHidden) return;
preloaderHidden = true;
preloader.classList.add("is-hiding");
document.body.classList.remove("preloader-active");
window.setTimeout(() => preloader.remove(), 260);
};
const requestPreloaderHide = () => {
const elapsed = performance.now() - preloaderStart;
window.setTimeout(hidePreloader, Math.max(minPreloaderTime - elapsed, 0));
};
window.setTimeout(hidePreloader, Math.max(maxPreloaderTime - (performance.now() - preloaderStart), 0));
if (document.readyState === "complete") {
requestPreloaderHide();
} else {
window.addEventListener("load", requestPreloaderHide, { once: true });
}
}
const setTheme = (theme, animate = false) => {
if (animate) {
document.body.classList.remove("theme-animating");
void document.body.offsetWidth;
document.body.classList.add("theme-animating");
}
root.dataset.theme = theme;
try {
localStorage.setItem("playpapa-theme", theme);
} catch {}
themeButtons.forEach((button) => {
const active = button.dataset.theme === theme;
button.classList.toggle("is-active", active);
button.setAttribute("aria-pressed", active ? "true" : "false");
});
if (animate) window.setTimeout(() => document.body.classList.remove("theme-animating"), 560);
};
setTheme(root.dataset.theme === "dark" ? "dark" : "light");
themeButtons.forEach((button) => {
button.addEventListener("click", () => setTheme(button.dataset.theme, true));
});
themeSwitch?.addEventListener("click", (event) => {
if (event.target.closest(".theme-btn")) return;
setTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
});
const mobileSearchLayer = document.getElementById("mobileSearch");
const mobileSearchTrigger = document.querySelector("[data-mobile-search-trigger]");
const mobileSearchBack = document.getElementById("mobileSearchBack");
const setMobileSearchOpen = (open) => {
if (!mobileSearchLayer) return;
mobileSearchLayer.classList.toggle("is-open", open);
mobileSearchLayer.setAttribute("aria-hidden", open ? "false" : "true");
mobileSearchLayer.toggleAttribute("inert", !open);
mobileSearchTrigger?.setAttribute("aria-expanded", open ? "true" : "false");
document.body.classList.toggle("mobile-search-open", open);
if (open) mobileSearchLayer.scrollTop = 0;
};
mobileSearchTrigger?.addEventListener("click", (event) => {
event.preventDefault();
setProfileOpen(false);
setCatalogOpen(false);
setMobileSearchOpen(true);
});
mobileSearchBack?.addEventListener("click", () => {
setMobileSearchOpen(false);
mobileSearchTrigger?.focus();
});
mobileSearchLayer?.querySelector(".mobile-search-form")?.addEventListener("submit", (event) => event.preventDefault());
mobileSearchLayer?.querySelectorAll('a[href="#"]').forEach((link) => {
link.addEventListener("click", (event) => event.preventDefault());
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || !mobileSearchLayer?.classList.contains("is-open")) return;
setMobileSearchOpen(false);
mobileSearchTrigger?.focus();
});
const profileMenu = document.querySelector(".profile-menu");
const profileButton = profileMenu?.querySelector(".profile-btn");
const profilePopover = document.getElementById("profilePopover");
const setProfileOpen = (open, returnFocus = false) => {
if (!profileMenu || !profilePopover || !profileButton) return;
profileMenu.classList.toggle("is-open", open);
profileButton.setAttribute("aria-expanded", open ? "true" : "false");
profilePopover.setAttribute("aria-hidden", open ? "false" : "true");
profilePopover.toggleAttribute("inert", !open);
document.body.classList.toggle("profile-open", open);
if (returnFocus) profileButton.focus();
};
profileButton?.addEventListener("click", (event) => {
event.stopPropagation();
const shouldOpen = !profileMenu?.classList.contains("is-open");
setProfileOpen(shouldOpen);
});
profilePopover?.querySelectorAll(".profile-menu-item:not(:disabled)").forEach((item) => {
item.addEventListener("click", () => setProfileOpen(false));
});
document.addEventListener("pointerdown", (event) => {
if (!profileMenu?.classList.contains("is-open") || profileMenu.contains(event.target)) return;
setProfileOpen(false);
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || !profileMenu?.classList.contains("is-open")) return;
setProfileOpen(false, true);
});
const catalogLayer = document.getElementById("catalogMegaMenu");
const catalogButton = document.querySelector(".catalog-btn:not([href])");
const catalogTriggers = [catalogButton, ...document.querySelectorAll("[data-catalog-trigger]")].filter(Boolean);
const catalogSideLinks = document.querySelectorAll(".catalog-side-link");
const catalogFeaturedItems = [
["service-steam.png", "Пополнение Steam"],
["service-brawl.png", "Game Pass"],
["service-steam.png", "Пополнение PSN"],
["service-brawl.png", "PS Plus"],
["service-pubg.png", "Grand Theft Auto V"],
["service-roblox.png", "Minecraft"],
["service-playstation.png", "Diablo 4"],
["service-mobile.png", "ARC Raiders"],
["service-chatgpt.png", "Dead by Daylight"],
];
const catalogNewItems = [
["article-paradise.png", "Gothic 1 Remake"],
["article-sale.png", "Forza Horizon 6"],
["product-pubg.png", "Assassin's Creed: Black..."],
["article-mobile.png", "007 First Light"],
["article-sale.png", "Call of Duty: Modern..."],
["product-pubg.png", "Marvel's Wolverine"],
["article-paradise.png", "UFC 6"],
["article-sale.png", "Subnautica 2"],
["product-pubg.png", "SILENT HILL: Townfall"],
];
const catalogItemMarkup = ([image, label]) => `
<a class="catalog-showcase-item" href="#" aria-label="${label}">
<img src="./assets/${image}" alt="">
<span>${label}</span>
</a>
`;
const catalogFeatured = document.getElementById("catalogFeatured");
const catalogNew = document.getElementById("catalogNew");
if (catalogFeatured) catalogFeatured.innerHTML = catalogFeaturedItems.map(catalogItemMarkup).join("");
if (catalogNew) catalogNew.innerHTML = catalogNewItems.map(catalogItemMarkup).join("");
const setCatalogOpen = (open) => {
if (!catalogLayer) return;
catalogLayer.classList.toggle("is-open", open);
catalogLayer.setAttribute("aria-hidden", open ? "false" : "true");
catalogLayer.toggleAttribute("inert", !open);
document.body.classList.toggle("catalog-open", open);
catalogTriggers.forEach((trigger) => trigger.setAttribute("aria-expanded", open ? "true" : "false"));
};
catalogTriggers.forEach((trigger) => {
trigger.addEventListener("click", (event) => {
event.preventDefault();
setProfileOpen(false);
setMobileSearchOpen(false);
setCatalogOpen(!catalogLayer?.classList.contains("is-open"));
});
});
catalogSideLinks.forEach((link) => {
link.addEventListener("click", () => {
catalogSideLinks.forEach((item) => item.classList.toggle("is-active", item === link));
});
});
catalogLayer?.querySelectorAll('a[href="#"]').forEach((link) => {
link.addEventListener("click", (event) => event.preventDefault());
});
document.addEventListener("pointerdown", (event) => {
if (!catalogLayer?.classList.contains("is-open")) return;
if (catalogLayer.contains(event.target) || catalogTriggers.some((trigger) => trigger.contains(event.target))) return;
setCatalogOpen(false);
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || !catalogLayer?.classList.contains("is-open")) return;
setCatalogOpen(false);
catalogButton?.focus();
});
const authModal = document.getElementById("authModal");
const authDialog = authModal?.querySelector(".auth-modal-dialog");
const authScreens = new Set(["login", "register", "recover", "success", "error"]);
let authReturnFocus = null;
const syncAuthScreenState = (screen = "login") => {
if (!authModal || !authScreens.has(screen)) return;
authModal.dataset.authScreen = screen;
authModal.querySelectorAll(".auth-tab").forEach((tab) => {
const active = tab.dataset.authScreenTarget === screen;
tab.setAttribute("aria-selected", active ? "true" : "false");
tab.tabIndex = active ? 0 : -1;
});
authModal.querySelectorAll(".auth-view").forEach((view) => {
const active = view.classList.contains(`auth-view-${screen}`);
view.setAttribute("aria-hidden", active ? "false" : "true");
});
};
const setAuthScreen = (screen = "login") => {
if (!authModal || !authScreens.has(screen)) return;
syncAuthScreenState(screen);
};
const setAuthOpen = (open, screen = "login", trigger = null) => {
if (!authModal) return;
if (open) {
authReturnFocus = trigger || document.activeElement;
setProfileOpen(false);
setCatalogOpen(false);
setMobileSearchOpen(false);
setAuthScreen(screen);
}
authModal.classList.toggle("is-open", open);
authModal.setAttribute("aria-hidden", open ? "false" : "true");
authModal.toggleAttribute("inert", !open);
document.body.classList.toggle("auth-modal-open", open);
if (open) {
window.setTimeout(() => authModal.querySelector("[data-auth-close]")?.focus(), 80);
} else if (authReturnFocus && typeof authReturnFocus.focus === "function") {
authReturnFocus.focus();
authReturnFocus = null;
}
};
document.querySelectorAll("[data-auth-open]").forEach((trigger) => {
trigger.addEventListener("click", (event) => {
event.preventDefault();
setAuthOpen(true, trigger.dataset.authOpen || "login", trigger);
});
});
authModal?.querySelectorAll("[data-auth-screen-target]").forEach((control) => {
control.addEventListener("click", () => setAuthScreen(control.dataset.authScreenTarget || "login"));
});
authModal?.querySelectorAll("[data-auth-close]").forEach((control) => {
control.addEventListener("click", () => setAuthOpen(false));
});
authModal?.addEventListener("pointerdown", (event) => {
if (event.target !== authModal || authDialog?.contains(event.target)) return;
setAuthOpen(false);
});
authModal?.querySelectorAll(".auth-form").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
if (form.dataset.authForm === "recover") {
const email = form.querySelector('input[type="email"]')?.value.trim().toLowerCase() || "";
setAuthScreen(email.includes("error") || email.includes("fail") ? "error" : "success");
}
});
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || !authModal?.classList.contains("is-open")) return;
setAuthOpen(false);
});
const initialAuthScreen = (() => {
const param = new URLSearchParams(window.location.search).get("auth");
const hash = window.location.hash.startsWith("#auth-") ? window.location.hash.replace("#auth-", "") : "";
return authScreens.has(param) ? param : authScreens.has(hash) ? hash : "";
})();
setAuthScreen("login");
if (initialAuthScreen) setAuthOpen(true, initialAuthScreen);
if (window.matchMedia("(pointer: coarse)").matches) {
let lastTouchEnd = 0;
const stopZoom = (event) => {
event.preventDefault();
};
const stopMultiTouch = (event) => {
if (event.touches && event.touches.length > 1) event.preventDefault();
};
document.addEventListener("gesturestart", stopZoom, { passive: false });
document.addEventListener("gesturechange", stopZoom, { passive: false });
document.addEventListener("gestureend", stopZoom, { passive: false });
document.addEventListener("touchstart", stopMultiTouch, { passive: false });
document.addEventListener("touchmove", stopMultiTouch, { passive: false });
document.addEventListener(
"touchend",
(event) => {
const now = Date.now();
if (now - lastTouchEnd < 320) event.preventDefault();
lastTouchEnd = now;
},
{ passive: false }
);
}
const amountInput = document.querySelector('input[name="amount"]');
const steamLoginInput = document.querySelector('input[name="steam-login"]');
const amountField = document.querySelector(".amount-field");
const servicesCard = document.querySelector(".services-card");
const topupQuickPopover = document.getElementById("topupQuickPopover");
const payButton = document.querySelector(".pay-btn");
const payCurrencyIcon = payButton?.querySelector(".currency-icon");
const topupMarkCurrencyIcon = document.querySelector(".currency-mark .currency-icon");
const topupAmountCurrencyIcon = document.querySelector(".amount-value .currency-icon");
const promoToggleButton = document.querySelector(".promo-small");
const promoToggleLabel = promoToggleButton?.querySelector(".promo-small-label");
const topupDetails = document.getElementById("topupDetails");
const topupPromoSection = document.getElementById("topupPromoSection");
const topupPaymentSection = document.getElementById("topupPaymentSection");
const topupPromoInput = document.getElementById("topupPromoInput");
const topupPromoApply = document.querySelector(".topup-promo-apply");
const topupPromoBenefit = document.querySelector(".topup-promo-benefit");
const topupPromoSaving = document.querySelector("[data-promo-saving]");
const topupQuickAmounts = document.querySelectorAll("[data-quick-amount]");
const topupPaymentMethods = document.querySelectorAll(".topup-payment-method");
const topupCurrencyButtons = document.querySelectorAll("[data-topup-currency]");
const topupCredit = document.querySelector("[data-topup-credit]");
const topupFeeBadge = document.querySelector("[data-topup-fee-badge]");
const topupFinalPay = document.querySelector(".topup-final-pay");
const topupResult = document.querySelector("[data-topup-result]");
const topupEndpoint = "{{ path('payment_cryptobot_create') }}";
const topupRequiresAuth = {{ user ? 'false' : 'true' }};
const maxTopupAmount = 100000;
let promoSectionOpen = false;
let paymentSectionOpen = false;
let promoApplied = false;
let selectedPaymentFee = 0;
let selectedPaymentMethod = document.querySelector(".topup-payment-method.is-selected")?.dataset.paymentMethod || "cryptobot";
let selectedTopupCurrency = document.querySelector("[data-topup-currency].is-active")?.dataset.topupCurrency || "RUB";
let quickPopoverOpen = false;
const minSteamTopupRubAmount = 50;
const topupCurrencySettings = {
RUB: { symbol: "₽", iconClass: "currency-icon-ruble", quickAmounts: [500, 1000, 2500, 5000] },
USD: { symbol: "$", iconClass: "currency-icon-dollar", quickAmounts: [5, 10, 25, 50] },
KZT: { symbol: "₸", iconClass: "currency-icon-tenge", quickAmounts: [1000, 2500, 5000, 10000] },
};
const topupCurrencyIconClasses = Object.values(topupCurrencySettings).map((settings) => settings.iconClass);
const formatNumber = (value) => new Intl.NumberFormat("ru-RU").format(Math.max(0, Math.round(value)));
const formatTopupAmount = (value) => {
const settings = topupCurrencySettings[selectedTopupCurrency] || topupCurrencySettings.RUB;
return `${formatNumber(value)} ${settings.symbol}`;
};
const setTopupCurrencyIcon = (icon, currency) => {
if (!icon) return;
const settings = topupCurrencySettings[currency] || topupCurrencySettings.RUB;
icon.classList.remove(...topupCurrencyIconClasses);
icon.classList.add(settings.iconClass);
};
const topupEscape = (value) =>
String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const openTopupAuth = (trigger = topupFinalPay) => {
const returnPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (typeof setAuthOpen === "function") {
window.PlayPapaAuth?.setReturnPath?.(returnPath);
setAuthOpen(true, "login", trigger);
return;
}
if (window.PlayPapaAuth?.openModal?.("login", trigger, returnPath)) return;
const url = new URL("/", window.location.origin);
url.searchParams.set("auth", "login");
url.searchParams.set("redirect", returnPath);
url.searchParams.set("reason", "checkout");
window.location.assign(`${url.pathname}${url.search}`);
};
const renderTopupResult = (order, error = "") => {
if (!topupResult) return;
topupResult.hidden = false;
topupResult.classList.toggle("is-error", Boolean(error));
topupResult.classList.toggle("is-success", !error && order?.status === "completed");
if (error) {
topupResult.innerHTML = `<strong>Не получилось оформить пополнение</strong><p>${topupEscape(error)}</p>`;
return;
}
topupResult.innerHTML = `
<strong>${topupEscape(order?.status_label || "Счёт создан")}</strong>
<p>${topupEscape(order?.message || "Открываем страницу статуса и оплаты.")}</p>
${order?.partner_order_id ? `<dl><div><dt>Partner ID</dt><dd>${topupEscape(order.partner_order_id)}</dd></div></dl>` : ""}
`;
};
const syncQuickPopover = () => {
topupQuickPopover?.classList.toggle("is-open", quickPopoverOpen);
topupQuickPopover?.setAttribute("aria-hidden", quickPopoverOpen ? "false" : "true");
topupQuickPopover?.toggleAttribute("inert", !quickPopoverOpen);
amountInput?.setAttribute("aria-expanded", quickPopoverOpen ? "true" : "false");
servicesCard?.classList.toggle("has-quick-popover", quickPopoverOpen);
};
const syncTopupExpansion = () => {
const detailsOpen = promoSectionOpen || paymentSectionOpen;
topupDetails?.classList.toggle("is-open", detailsOpen);
topupDetails?.setAttribute("aria-hidden", detailsOpen ? "false" : "true");
topupDetails?.toggleAttribute("inert", !detailsOpen);
topupPromoSection?.classList.toggle("is-visible", promoSectionOpen);
topupPromoSection?.setAttribute("aria-hidden", promoSectionOpen ? "false" : "true");
topupPromoSection?.toggleAttribute("inert", !promoSectionOpen);
promoToggleButton?.setAttribute("aria-expanded", promoSectionOpen ? "true" : "false");
if (promoToggleLabel) promoToggleLabel.textContent = promoSectionOpen ? "Скрыть промокод" : "Ввести промокод";
topupPaymentSection?.classList.toggle("is-visible", paymentSectionOpen);
topupPaymentSection?.setAttribute("aria-hidden", paymentSectionOpen ? "false" : "true");
topupPaymentSection?.toggleAttribute("inert", !paymentSectionOpen);
payButton?.setAttribute("aria-expanded", paymentSectionOpen ? "true" : "false");
servicesCard?.classList.toggle("is-payment-open", paymentSectionOpen);
};
const syncAmount = () => {
if (!amountInput) return;
let value = amountInput.value.replace(/\D/g, "");
if (value) value = String(Math.min(Number(value), maxTopupAmount));
amountInput.value = value;
amountInput.style.setProperty("--amount-digits", String(Math.max(value.length, 3)));
const numericAmount = Number(value || 0);
setTopupCurrencyIcon(topupMarkCurrencyIcon, selectedTopupCurrency);
setTopupCurrencyIcon(topupAmountCurrencyIcon, selectedTopupCurrency);
setTopupCurrencyIcon(payCurrencyIcon, selectedTopupCurrency);
if (payButton && payCurrencyIcon) {
payButton.textContent = `Оплатить ${formatNumber(numericAmount)}`;
payButton.append(payCurrencyIcon);
}
topupQuickAmounts.forEach((button) => {
button.classList.toggle("is-active", Number(button.dataset.quickAmount) === numericAmount);
});
if (topupCredit) topupCredit.textContent = formatTopupAmount(numericAmount);
const amountToPay = numericAmount * (1 + selectedPaymentFee / 100);
if (topupFinalPay) topupFinalPay.textContent = `Оплатить ${formatTopupAmount(amountToPay)}`;
if (topupFeeBadge) {
topupFeeBadge.textContent = selectedPaymentFee ? `+${selectedPaymentFee}% комиссия` : "Без комиссии";
topupFeeBadge.classList.toggle("has-fee", selectedPaymentFee > 0);
}
if (topupPromoSaving) topupPromoSaving.textContent = formatTopupAmount(numericAmount * 0.05);
};
const syncTopupCurrency = () => {
const settings = topupCurrencySettings[selectedTopupCurrency] || topupCurrencySettings.RUB;
topupCurrencyButtons.forEach((button) => {
const selected = button.dataset.topupCurrency === selectedTopupCurrency;
button.classList.toggle("is-active", selected);
button.setAttribute("aria-pressed", selected ? "true" : "false");
});
topupQuickAmounts.forEach((button, index) => {
const amount = settings.quickAmounts[index] || settings.quickAmounts[0];
button.dataset.quickAmount = String(amount);
button.textContent = formatTopupAmount(amount);
});
syncAmount();
};
amountInput?.addEventListener("input", syncAmount);
amountInput?.addEventListener("focus", () => {
quickPopoverOpen = true;
syncQuickPopover();
});
amountInput?.addEventListener("click", () => {
quickPopoverOpen = true;
syncQuickPopover();
});
amountInput?.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
quickPopoverOpen = false;
syncQuickPopover();
amountInput.blur();
});
amountInput?.addEventListener("blur", () => {
if (!amountInput.value) amountInput.value = selectedTopupCurrency === "RUB" ? String(minSteamTopupRubAmount) : "500";
if (selectedTopupCurrency === "RUB" && Number(amountInput.value) < minSteamTopupRubAmount) {
amountInput.value = String(minSteamTopupRubAmount);
}
syncAmount();
});
promoToggleButton?.addEventListener("click", () => {
promoSectionOpen = !promoSectionOpen;
syncTopupExpansion();
});
payButton?.addEventListener("click", () => {
quickPopoverOpen = false;
syncQuickPopover();
paymentSectionOpen = true;
syncTopupExpansion();
});
topupCurrencyButtons.forEach((button) => {
button.addEventListener("click", () => {
selectedTopupCurrency = button.dataset.topupCurrency || "RUB";
const settings = topupCurrencySettings[selectedTopupCurrency] || topupCurrencySettings.RUB;
if (amountInput) amountInput.value = String(settings.quickAmounts[0]);
syncTopupCurrency();
});
});
topupQuickAmounts.forEach((button) => {
button.addEventListener("click", () => {
if (!amountInput) return;
amountInput.value = button.dataset.quickAmount || "500";
syncAmount();
if (button.closest(".topup-quick-popover") && (!paymentSectionOpen || window.matchMedia("(min-width: 768px)").matches)) {
quickPopoverOpen = false;
syncQuickPopover();
}
});
});
document.addEventListener("pointerdown", (event) => {
if (!quickPopoverOpen || amountField?.contains(event.target)) return;
if (paymentSectionOpen && window.matchMedia("(max-width: 767px)").matches) return;
quickPopoverOpen = false;
syncQuickPopover();
});
topupPaymentMethods.forEach((method) => {
method.addEventListener("click", () => {
if (method.disabled || method.getAttribute("aria-disabled") === "true") return;
selectedPaymentFee = Number(method.dataset.fee || 0);
selectedPaymentMethod = method.dataset.paymentMethod || "cryptobot";
topupPaymentMethods.forEach((item) => {
const selected = item === method;
item.classList.toggle("is-selected", selected);
item.setAttribute("aria-checked", selected ? "true" : "false");
});
syncAmount();
});
});
topupPromoApply?.addEventListener("click", () => {
if (!topupPromoInput?.value.trim()) {
topupPromoInput?.focus();
return;
}
promoApplied = true;
topupPromoBenefit?.classList.add("is-visible");
topupPromoBenefit?.setAttribute("aria-hidden", "false");
topupPromoApply.textContent = "Применено";
syncAmount();
});
topupPromoInput?.addEventListener("input", () => {
if (!promoApplied) return;
promoApplied = false;
topupPromoBenefit?.classList.remove("is-visible");
topupPromoBenefit?.setAttribute("aria-hidden", "true");
if (topupPromoApply) topupPromoApply.textContent = "Применить";
});
topupFinalPay?.addEventListener("click", async () => {
if (topupRequiresAuth) {
openTopupAuth(topupFinalPay);
return;
}
const steamLogin = steamLoginInput?.value.trim() || "";
const amount = amountInput?.value.trim() || "";
if (!steamLogin) {
renderTopupResult(null, "Введите логин Steam.");
steamLoginInput?.focus();
return;
}
if (!amount || Number(amount) <= 0) {
renderTopupResult(null, "Введите сумму пополнения.");
amountInput?.focus();
return;
}
if (selectedTopupCurrency === "RUB" && Number(amount) < minSteamTopupRubAmount) {
renderTopupResult(null, `Минимальная сумма Steam-пополнения: ${minSteamTopupRubAmount} ₽.`);
amountInput?.focus();
return;
}
if (selectedPaymentMethod !== "cryptobot") {
renderTopupResult(null, "Этот способ оплаты пока не подключен.");
return;
}
const originalText = topupFinalPay.textContent;
topupFinalPay.disabled = true;
topupFinalPay.textContent = "Создаем счёт...";
renderTopupResult({ status_label: "Проверяем Steam", message: "Проверяем логин и сумму в GalaxyLink." });
try {
const response = await fetch(topupEndpoint, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
service: "steam_topup",
steam_login: steamLogin,
amount,
currency: selectedTopupCurrency,
payment: selectedPaymentMethod,
promo_code: topupPromoInput?.value.trim() || "",
}),
});
const payload = await response.json().catch(() => ({}));
if (response.status === 401 && payload.auth_required) {
openTopupAuth(topupFinalPay);
return;
}
if (!response.ok || !payload.ok) throw new Error(payload.error || `HTTP ${response.status}`);
if (payload.status_url) {
renderTopupResult({ status_label: "Счёт создан", message: "Открываем страницу статуса. Там будет кнопка оплаты и автообновление GalaxyLink." });
window.location.href = payload.status_url;
return;
}
if (payload.redirect_url) {
renderTopupResult({ status_label: "Переходим к оплате", message: "Открываем CryptoBot." });
window.location.href = payload.redirect_url;
return;
}
renderTopupResult(payload.order);
} catch (error) {
renderTopupResult(null, error.message || "Неизвестная ошибка оформления.");
} finally {
topupFinalPay.disabled = false;
topupFinalPay.textContent = originalText;
}
});
syncTopupExpansion();
syncQuickPopover();
syncTopupCurrency();
const services = [
["service-steam.png", "Steam"],
["service-telegram.png", "Telegram"],
["service-roblox.png", "Roblox"],
["service-brawl.png", "Brawl Stars"],
["service-pubg.png", "PUBG Mob..."],
["service-appstore.png", "App Store"],
["service-chatgpt.png", "ChatGPT"],
["service-playstation.png", "PlayStation"],
["service-tiktok.png", "TikTok"],
["service-mobile.png", "Mobile Leg..."],
];
const servicesRow = document.getElementById("servicesRow");
if (servicesRow) {
services.forEach(([icon, label, active]) => {
servicesRow.insertAdjacentHTML(
"beforeend",
`<button class="service-item ${active ? "is-active" : ""}"><img src="./assets/${icon}" alt=""><span>${label}</span></button>`
);
});
servicesRow.insertAdjacentHTML(
"beforeend",
`<button class="service-item more-service"><span class="more-icon"><img class="more-icon-img" src="./assets/more-service.svg" alt=""></span><span>еще 841</span></button>`
);
}
const discountPattern = ["red", "orange", "red", "green", "red"];
const escapeHtml = (value) =>
String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const catalogProducts = Array.isArray(window.playpapaCatalog?.products) ? window.playpapaCatalog.products : [];
const fallbackProducts = [
{
name: "DOOM 2016 STEAM KEY РФ+СНГ",
image_url: "./assets/product-pubg.png",
price_label: "990 ₽",
url: "/product",
},
];
const renderProducts = catalogProducts.length ? catalogProducts : fallbackProducts;
const productMarkup = (product, color, index) => `
<article class="product-card">
<div class="product-cover">
<img src="${escapeHtml(product.image_url || "./assets/product-pubg.png")}" alt="">
${product.discount_label ? `<span class="discount ${color}">${escapeHtml(product.discount_label)}</span>` : ""}
</div>
<div class="product-body">
<h3>${escapeHtml(product.name || "Товар PlayPapa")}</h3>
<div class="price-line"><strong>${escapeHtml(product.price_label || "Цена уточняется")}</strong></div>
<a href="${escapeHtml(product.url || "/product")}">Купить</a>
</div>
</article>
`;
document.querySelectorAll("[data-products]").forEach((grid) => {
const items = Array.from({ length: 5 }, (_, i) =>
productMarkup(renderProducts[i % renderProducts.length], discountPattern[i % discountPattern.length], i)
).join("");
grid.innerHTML = items;
});
</script>
{% endblock %}