feat: Hugo engine for iGaming affiliate template (layouts, assets, i18n, verified data, CI build script)

This commit is contained in:
pipeline
2026-07-16 06:07:22 +03:00
parent b9385a1f72
commit 1816ff2093
58 changed files with 5631 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
/* =========================================================================
age-gate.js — non-dismissible age verification interstitial.
The <html> class "age-gate-active" is set as early as possible by an inline
script in baseof.html to avoid a flash for returning (confirmed) visitors.
This module wires the confirm/exit buttons, syncs the displayed age with the
active locale (set by the locale switcher) and — because the dialog is
role="dialog" aria-modal="true" — manages focus: it moves focus into the
dialog, traps Tab/Shift+Tab, marks the rest of the page inert while active,
and restores focus on confirm.
Deliberately dependency-free (no Alpine, no eval/new Function): the gate is
driven purely by the CSS "age-gate-active" class, so it still fails safe
(gate shows, confirm works) even if Alpine never loads.
========================================================================= */
(function () {
"use strict";
var STORAGE_KEY = "ageConfirmed";
var INERT_SELECTORS = ["header", "main", "footer"];
var FOCUSABLE =
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
var gate = null;
var lastFocused = null;
var trapActive = false;
function hasConfirmed() {
try {
return localStorage.getItem(STORAGE_KEY) === "1";
} catch (e) {
return false;
}
}
function focusableIn(el) {
return Array.prototype.filter.call(
el.querySelectorAll(FOCUSABLE),
function (node) {
return (
node.offsetWidth > 0 ||
node.offsetHeight > 0 ||
node === document.activeElement
);
}
);
}
function onKeydown(e) {
if (!trapActive || e.key !== "Tab" || !gate) return;
var f = focusableIn(gate);
if (!f.length) {
e.preventDefault();
return;
}
var first = f[0];
var last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
function onFocusIn(e) {
if (trapActive && gate && !gate.contains(e.target)) {
var f = focusableIn(gate);
if (f.length) f[0].focus();
}
}
function setBackgroundInert(on) {
INERT_SELECTORS.forEach(function (sel) {
var el = document.querySelector(sel);
if (!el) return;
if (on) {
el.setAttribute("inert", "");
el.setAttribute("aria-hidden", "true");
} else {
el.removeAttribute("inert");
el.removeAttribute("aria-hidden");
}
});
}
function activateTrap() {
if (trapActive || !gate) return;
trapActive = true;
lastFocused = document.activeElement;
setBackgroundInert(true);
document.addEventListener("keydown", onKeydown, true);
document.addEventListener("focusin", onFocusIn);
var confirmBtn = gate.querySelector("[data-age-confirm]");
if (confirmBtn) {
// Defer to ensure the element is focusable after the first paint.
window.setTimeout(function () {
confirmBtn.focus();
}, 0);
}
}
function releaseTrap() {
trapActive = false;
setBackgroundInert(false);
document.removeEventListener("keydown", onKeydown, true);
document.removeEventListener("focusin", onFocusIn);
}
function restoreFocus() {
var target =
(lastFocused &&
document.contains(lastFocused) &&
lastFocused !== document.body &&
lastFocused) ||
document.querySelector("header a, header button") ||
document.getElementById("main");
if (!target) return;
if (target.id === "main" && !target.hasAttribute("tabindex")) {
target.setAttribute("tabindex", "-1");
}
try {
target.focus();
} catch (e) {
/* focus can fail on detached nodes — ignore */
}
}
function confirmAge() {
try {
localStorage.setItem(STORAGE_KEY, "1");
} catch (e) {
/* storage unavailable — still allow through for this session */
}
document.documentElement.classList.remove("age-gate-active");
releaseTrap();
restoreFocus();
}
function exitSite() {
window.location.href = "https://www.google.com/";
}
function init() {
gate = document.getElementById("age-gate");
if (!gate) {
document.documentElement.classList.remove("age-gate-active");
return;
}
var confirmBtn = gate.querySelector("[data-age-confirm]");
if (confirmBtn) {
confirmBtn.addEventListener("click", confirmAge);
}
var exitBtn = gate.querySelector("[data-age-exit]");
if (exitBtn) {
exitBtn.addEventListener("click", exitSite);
}
if (hasConfirmed()) {
document.documentElement.classList.remove("age-gate-active");
setBackgroundInert(false);
} else {
document.documentElement.classList.add("age-gate-active");
activateTrap();
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
+154
View File
@@ -0,0 +1,154 @@
/* =========================================================================
cookie-consent.js — GDPR-style consent manager.
Non-essential scripts must be added to the page as:
<script type="text/plain" data-cookiecategory="analytics"> ... </script>
They stay inert until the visitor grants that category, at which point this
module reactivates them. Consent is stored in localStorage.
========================================================================= */
(function () {
"use strict";
var STORAGE_KEY = "cookieConsent";
var CATEGORIES = ["necessary", "analytics", "marketing"];
function readConsent() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "null");
} catch (e) {
return null;
}
}
function writeConsent(consent) {
consent.necessary = true; // always on
consent.ts = Date.now();
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(consent));
} catch (e) {
/* ignore */
}
activateScripts(consent);
}
function activateScripts(consent) {
var nodes = document.querySelectorAll(
'script[type="text/plain"][data-cookiecategory]'
);
Array.prototype.forEach.call(nodes, function (node) {
if (node.getAttribute("data-cookie-activated") === "1") return;
var cat = node.getAttribute("data-cookiecategory");
if (!consent[cat]) return;
var s = document.createElement("script");
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
if (attr.name === "type" || attr.name === "data-cookiecategory") continue;
s.setAttribute(attr.name, attr.value);
}
s.type = "text/javascript";
if (!node.src) {
s.text = node.textContent;
}
node.parentNode.insertBefore(s, node.nextSibling);
node.setAttribute("data-cookie-activated", "1");
});
}
function banner() {
return document.getElementById("cookie-consent");
}
function showBanner() {
var b = banner();
if (b) b.hidden = false;
}
function hideBanner() {
var b = banner();
if (b) b.hidden = true;
}
function acceptAll() {
var consent = { necessary: true };
CATEGORIES.forEach(function (c) {
consent[c] = true;
});
writeConsent(consent);
hideBanner();
}
function rejectNonEssential() {
var consent = { necessary: true, analytics: false, marketing: false };
writeConsent(consent);
hideBanner();
}
function savePreferences() {
var b = banner();
var consent = { necessary: true };
CATEGORIES.forEach(function (c) {
if (c === "necessary") return;
var box = b
? b.querySelector('[data-cc-category="' + c + '"]')
: null;
consent[c] = !!(box && box.checked);
});
writeConsent(consent);
hideBanner();
}
function toggleManage() {
var b = banner();
if (!b) return;
var panel = b.querySelector("[data-cc-panel]");
if (panel) panel.hidden = !panel.hidden;
}
function init() {
var b = banner();
var existing = readConsent();
if (existing) {
activateScripts(existing);
if (b) b.hidden = true;
} else if (b) {
b.hidden = false;
}
if (!b) return;
var accept = b.querySelector("[data-cc-accept-all]");
if (accept) accept.addEventListener("click", acceptAll);
var reject = b.querySelector("[data-cc-reject]");
if (reject) reject.addEventListener("click", rejectNonEssential);
var save = b.querySelector("[data-cc-save]");
if (save) save.addEventListener("click", savePreferences);
var manage = b.querySelector("[data-cc-manage]");
if (manage) manage.addEventListener("click", toggleManage);
}
// Public: reopen the consent banner (used by footer / cookie-policy links).
window.openCookieSettings = function () {
var existing = readConsent();
var b = banner();
if (b && existing) {
CATEGORIES.forEach(function (c) {
if (c === "necessary") return;
var box = b.querySelector('[data-cc-category="' + c + '"]');
if (box) box.checked = !!existing[c];
});
var panel = b.querySelector("[data-cc-panel]");
if (panel) panel.hidden = false;
}
showBanner();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
+47
View File
@@ -0,0 +1,47 @@
/* =========================================================================
games-filter.js — Alpine.js component powering the slots library filter.
Cards are rendered server-side by Hugo (so thumbnails go through the image
pipeline); this component toggles their visibility via data-attributes.
Registered on `alpine:init`, so this file must load before the Alpine CDN.
========================================================================= */
document.addEventListener("alpine:init", function () {
window.Alpine.data("gamesFilter", function () {
return {
provider: "all",
category: "all",
search: "",
visibleCount: 0,
get normalizedSearch() {
return this.search.trim().toLowerCase();
},
cardVisible: function (el) {
var okProvider =
this.provider === "all" || el.dataset.provider === this.provider;
var okCategory =
this.category === "all" || el.dataset.category === this.category;
var okSearch =
this.normalizedSearch === "" ||
(el.dataset.title || "").indexOf(this.normalizedSearch) !== -1;
return okProvider && okCategory && okSearch;
},
recount: function () {
var self = this;
var cards = this.$root.querySelectorAll("[data-game]");
var n = 0;
Array.prototype.forEach.call(cards, function (el) {
if (self.cardVisible(el)) n++;
});
this.visibleCount = n;
},
reset: function () {
this.provider = "all";
this.category = "all";
this.search = "";
},
};
});
});
+5
View File
File diff suppressed because one or more lines are too long