PageSourceSearch

https://pizzeria-planeta.pl/js/account-promo-popup.js

js pizzeria-planeta.pl collected 2026-10-08 07:35:25 UTC 7,625 bytes, 225 lines download raw bytes

1import { getConfig } from './config.js';
2import { isCustomerLoggedIn } from './customer-auth.js';
3import { t } from './i18n.js';
4import { resolvePublicPath, isEnglish } from './locale.js';
5import { trackAbImpression, trackAbRegisterClick } from './ab-tests.js';
6
7const STORAGE_KEY = 'planeta_account_promo_day';
8const DEFAULT_DELAY_SECONDS = 5;
9const TRIGGERS = new Set(['disabled', 'delay', 'open_cart', 'after_order']);
10const BLOCKING_BODY_CLASSES = [
11  'author-contact-open',
12  'account-modal-open',
13  'gallery-panel-open'
14];
15
16let delayTimer = null;
17let retryTimer = null;
18
19export function normalizeAccountPromoPopup(raw = null) {
20  const src = raw && typeof raw === 'object' ? raw : {};
21  let trigger = String(src.trigger || 'delay').trim();
22  if (!TRIGGERS.has(trigger)) trigger = 'delay';
23  const delaySeconds = Math.min(
24    120,
25    Math.max(1, Number(src.delaySeconds) || DEFAULT_DELAY_SECONDS)
26  );
27  return { trigger, delaySeconds };
28}
29
30export function getAccountPromoPopupSettings(config = getConfig()) {
31  return normalizeAccountPromoPopup(config?.accountPromoPopup);
32}
33
34function todayKey() {
35  const d = new Date();
36  const y = d.getFullYear();
37  const m = String(d.getMonth() + 1).padStart(2, '0');
38  const day = String(d.getDate()).padStart(2, '0');
39  return `${y}-${m}-${day}`;
40}
41
42function wasShownToday() {
43  try {
44    return localStorage.getItem(STORAGE_KEY) === todayKey();
45  } catch {
46    return true;
47  }
48}
49
50function markShownToday() {
51  try {
52    localStorage.setItem(STORAGE_KEY, todayKey());
53  } catch {
54    // ignore
55  }
56}
57
58function hasBlockingOverlay() {
59  if (BLOCKING_BODY_CLASSES.some((cls) => document.body.classList.contains(cls))) {
60    return true;
61  }
62  const productModal = document.getElementById('product-modal');
63  if (productModal && !productModal.hidden) return true;
64  return false;
65}
66
67function registerHref() {
68  return `${resolvePublicPath('konto.html')}?tab=register`;
69}
70
71function escapeHtml(value) {
72  return String(value ?? '')
73    .replaceAll('&', '&')
74    .replaceAll('<', '&lt;')
75    .replaceAll('>', '&gt;')
76    .replaceAll('"', '&quot;');
77}
78
79function escapeAttr(value) {
80  return escapeHtml(value).replaceAll("'", '&#39;');
81}
82
83function buildMarkup(config = getConfig()) {
84  const ab = config?._abAccountPromo;
85  const english = isEnglish();
86  const abTitle = ab?.titlePl
87    ? (english ? (ab.titleEn || ab.titlePl) : ab.titlePl)
88    : t('accountPromo.title');
89  const abDesc = ab?.descPl
90    ? (english ? (ab.descEn || ab.descPl) : ab.descPl)
91    : t('accountPromo.desc');
92  return `
93    <div class="account-promo-backdrop" data-account-promo-dismiss tabindex="-1"></div>
94    <div
95      class="account-promo"
96      role="dialog"
97      aria-modal="true"
98      aria-labelledby="account-promo-title"
99      aria-describedby="account-promo-desc"
100    >
101      <button type="button" class="account-promo__close" data-account-promo-dismiss aria-label="${escapeAttr(t('accountPromo.close'))}">✕</button>
102      <div class="account-promo__sparkles" aria-hidden="true">✨</div>
103      <div class="account-promo__emoji" aria-hidden="true">
104        <span class="account-promo__emoji-wheel">🎡</span>
105        <span class="account-promo__emoji-pizza">🍕</span>
106      </div>
107      <h2 id="account-promo-title" class="account-promo__title">${escapeHtml(abTitle)}</h2>
108      <p id="account-promo-desc" class="account-promo__desc">${escapeHtml(abDesc)}</p>
109      <ul class="account-promo__perks" aria-label="${escapeAttr(t('accountPromo.perksLabel'))}">
110        <li><span aria-hidden="true">🎯</span> ${escapeHtml(t('accountPromo.perk1'))}</li>
111        <li><span aria-hidden="true">💸</span> ${escapeHtml(t('accountPromo.perk2'))}</li>
112        <li><span aria-hidden="true">🎁</span> ${escapeHtml(t('accountPromo.perk3'))}</li>
113      </ul>
114      <div class="account-promo__actions">
115        <a class="btn btn-primary account-promo__cta" href="${escapeAttr(registerHref())}">
116          ${escapeHtml(t('accountPromo.cta'))}
117        </a>
118        <button type="button" class="account-promo__later" data-account-promo-dismiss>
119          ${escapeHtml(t('accountPromo.later'))}
120        </button>
121      </div>
122    </div>
123  `;
124}
125
126function closePromo(root) {
127  if (!root || root.dataset.closing === '1') return;
128  root.dataset.closing = '1';
129  markShownToday();
130  root.classList.remove('is-visible');
131  document.body.classList.remove('account-promo-open');
132  const onEnd = () => {
133    root.removeEventListener('transitionend', onEnd);
134    root.remove();
135  };
136  root.addEventListener('transitionend', onEnd);
137  window.setTimeout(() => {
138    if (root.isConnected) root.remove();
139  }, 400);
140}
141
142function openPromo() {
143  if (document.getElementById('account-promo-root')) return;
144  if (isCustomerLoggedIn() || wasShownToday()) return;
145
146  const root = document.createElement('div');
147  root.id = 'account-promo-root';
148  root.className = 'account-promo-root';
149  root.innerHTML = buildMarkup(getConfig());
150  document.body.appendChild(root);
151  document.body.classList.add('account-promo-open');
152
153  const onDismiss = (e) => {
154    if (e.target.closest('[data-account-promo-dismiss]')) {
155      e.preventDefault();
156      closePromo(root);
157    }
158  };
159  root.addEventListener('click', onDismiss);
160
161  const onKey = (e) => {
162    if (e.key === 'Escape') {
163      closePromo(root);
164      document.removeEventListener('keydown', onKey);
165    }
166  };
167  document.addEventListener('keydown', onKey);
168
169  root.querySelector('.account-promo__cta')?.addEventListener('click', () => {
170    markShownToday();
171    trackAbRegisterClick();
172  });
173
174  trackAbImpression('account_popup');
175
176  requestAnimationFrame(() => {
177    requestAnimationFrame(() => root.classList.add('is-visible'));
178  });
179
180  root.querySelector('.account-promo__cta')?.focus();
181}
182
183function tryShowPromo() {
184  if (isCustomerLoggedIn() || wasShownToday()) return;
185  if (hasBlockingOverlay()) {
186    if (retryTimer) window.clearTimeout(retryTimer);
187    retryTimer = window.setTimeout(tryShowPromo, 1500);
188    return;
189  }
190  openPromo();
191}
192
193function canShowForTrigger(expectedTrigger, config = getConfig()) {
194  const settings = getAccountPromoPopupSettings(config);
195  if (settings.trigger !== expectedTrigger) return false;
196  if (isCustomerLoggedIn() || wasShownToday()) return false;
197  return true;
198}
199
200/** Po załadowaniu strony — tylko gdy trigger = delay. */
201export function initAccountPromoPopup(config = getConfig()) {
202  const settings = getAccountPromoPopupSettings(config);
203  if (settings.trigger === 'disabled') return;
204  if (settings.trigger === 'open_cart') return;
205  if (settings.trigger === 'after_order') return;
206  if (!canShowForTrigger('delay', config)) return;
207
208  if (delayTimer) window.clearTimeout(delayTimer);
209  delayTimer = window.setTimeout(() => {
210    tryShowPromo();
211  }, settings.delaySeconds * 1000);
212}
213
214/** Wywołaj przy otwarciu koszyka — tylko gdy trigger = open_cart. */
215export function triggerAccountPromoOnCartOpen(config = getConfig()) {
216  if (!canShowForTrigger('open_cart', config)) return;
217  // Krótka pauza, żeby upsell produktowy zdążył się otworzyć (wtedy poczekamy).
218  window.setTimeout(() => tryShowPromo(), 600);
219}
220
221/** Strona „dziękujemy” / po udanym zamówieniu — tylko gdy trigger = after_order. */
222export function triggerAccountPromoAfterOrder(config = getConfig()) {
223  if (!canShowForTrigger('after_order', config)) return;
224  window.setTimeout(() => tryShowPromo(), 1600);
225}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.