PageSourceSearch

https://figureskatingdreams.com/assets/js/notifications.js?v=20260923acct1

js figureskatingdreams.com collected 2026-10-06 23:51:23 UTC 18,115 bytes, 506 lines download raw bytes

1/**
2 * User notification center — bell + dropdown + badge.
3 * Reuses /api/auth/me/notifications/inbox (session-bound; no client userId).
4 */
5(function (global) {
6  "use strict";
7
8  var panelOpen = false;
9  var lastItems = [];
10  var lastUnread = 0;
11
12  var TYPE_META = {
13    payment_pending: { icon: "creditCard", label: "Pembayaran", labelKey: "notif.typePayment", tone: "warn" },
14    payment_awaiting: { icon: "clock", label: "Verifikasi", labelKey: "notif.typeVerification", tone: "warn" },
15    payment_paid: { icon: "userCheck", label: "Lunas", labelKey: "notif.typePaid", tone: "ok" },
16    payment_rejected: { icon: "flag", label: "Ditolak", labelKey: "notif.typeRejected", tone: "bad" },
17    subscription_active: { icon: "crown", label: "Langganan", labelKey: "notif.typeSubscription", tone: "plus" },
18    subscription_expiring: { icon: "clock", label: "Langganan", labelKey: "notif.typeSubscription", tone: "warn" },
19    subscription_cancelled: { icon: "settings", label: "Langganan", labelKey: "notif.typeSubscription", tone: "muted" },
20    complimentary_granted: { icon: "gift", label: "Complimentary", labelKey: "notif.typeComplimentary", tone: "plus" },
21    complimentary_revoked: { icon: "lock", label: "Akses", labelKey: "notif.typeAccess", tone: "bad" },
22    wallet_credit: { icon: "coins", label: "Wallet", labelKey: "notif.typeWallet", tone: "ok" },
23    wallet_debit: { icon: "wallet", label: "Wallet", labelKey: "notif.typeWallet", tone: "warn" },
24    checkin_reward: { icon: "sparkles", label: "Check-in", labelKey: "notif.typeCheckin", tone: "ok" },
25    title_request: { icon: "megaphone", label: "Request", labelKey: "notif.typeRequest", tone: "muted" },
26    message_new: { icon: "mail", label: "Pesan", labelKey: "notif.typeMessage", tone: "plus" },
27    system: { icon: "bell", label: "Sistem", labelKey: "notif.typeSystem", tone: "muted" }
28  };
29
30  function esc(s) {
31    return String(s == null ? "" : s)
32      .replace(/&/g, "&")
33      .replace(/</g, "&lt;")
34      .replace(/>/g, "&gt;")
35      .replace(/"/g, "&quot;");
36  }
37
38  function localeText(key, fallback) {
39    if (global.MeteorSiteLocale && typeof global.MeteorSiteLocale.t === "function") {
40      return global.MeteorSiteLocale.t(key, null, fallback);
41    }
42    return fallback;
43  }
44
45  function localeCode() {
46    return global.MeteorSiteLocale && global.MeteorSiteLocale.getLocale && global.MeteorSiteLocale.getLocale() === "en" ? "en-US" : "id-ID";
47  }
48
49  function ic(name, size) {
50    if (global.MeteorAccountIcons && typeof global.MeteorAccountIcons.icon === "function") {
51      return global.MeteorAccountIcons.icon(name, size);
52    }
53    return "";
54  }
55
56  function typeMeta(type) {
57    return TYPE_META[type] || TYPE_META.system;
58  }
59
60  function relativeTime(iso) {
61    if (!iso) return "";
62    var t = Date.parse(iso);
63    if (isNaN(t)) return "";
64    var diff = Math.round((Date.now() - t) / 1000);
65    if (diff < 45) return localeText("notif.justNow", "Baru saja");
66    if (diff < 3600) return Math.floor(diff / 60) + " mnt";
67    if (diff < 86400) return Math.floor(diff / 3600) + " jam";
68    if (diff < 604800) return Math.floor(diff / 86400) + " hr";
69    try {
70      return new Date(t).toLocaleDateString(localeCode(), { day: "numeric", month: "short" });
71    } catch (e) {
72      return "";
73    }
74  }
75
76  function fetchInbox() {
77    var auth = global.MeteorUserAuth;
78    if (!auth || typeof auth.api !== "function") {
79      return Promise.resolve({ items: [], source: "none", unreadCount: 0 });
80    }
81    var user = typeof auth.getUser === "function" ? auth.getUser() : null;
82    if (!user) {
83      return Promise.resolve({ items: [], source: "guest", unreadCount: 0 });
84    }
85    return auth
86      .api("/me/notifications/inbox", { method: "GET" })
87      .then(function (data) {
88        var items = (data && data.items) || [];
89        if (!Array.isArray(items)) items = [];
90        return {
91          items: items,
92          source: "api",
93          unreadCount: data && typeof data.unreadCount === "number" ? data.unreadCount : items.filter(function (it) { return it && !it.read; }).length
94        };
95      })
96      .catch(function () {
97        return { items: [], source: "error", unreadCount: 0 };
98      });
99  }
100
101  function renderItem(item) {
102    var meta = typeMeta(item.type || item.category || "system");
103    var href = esc(item.href || "/notifications");
104    var title = esc(item.title || "");
105    var body = esc(item.body || "");
106    var time = esc(item.relativeTime || relativeTime(item.createdAt) || "");
107    var unread = item && item.read === false;
108    var cta =
109      href && href !== "/notifications"
110        ? '<span class="mf-notif-item-cta">' + esc(localeText("notif.open", "Buka")) + "</span>"
111        : "";
112    return (
113      '<a class="mf-notif-item is-tone-' +
114      esc(meta.tone) +
115      (unread ? " is-unread" : "") +
116      '" href="' +
117      href +
118      '" data-notif-id="' +
119      esc(item.id || "") +
120      '" role="listitem">' +
121      '<span class="mf-notif-item-icon" aria-hidden="true">' +
122      ic(meta.icon, 16) +
123      "</span>" +
124      '<span class="mf-notif-item-copy">' +
125      '<span class="mf-notif-item-meta">' +
126      '<span class="mf-notif-item-cat">' +
127      esc(localeText(meta.labelKey || "", meta.label)) +
128      "</span>" +
129      (time ? '<span class="mf-notif-item-time">' + time + "</span>" : "") +
130      "</span>" +
131      '<span class="mf-notif-item-title">' +
132      title +
133      "</span>" +
134      (body ? '<span class="mf-notif-item-body">' + body + "</span>" : "") +
135      cta +
136      "</span>" +
137      (unread ? '<span class="mf-notif-item-dot" aria-hidden="true"></span>' : "") +
138      "</a>"
139    );
140  }
141
142  function updateBadge(unreadCount) {
143    var btn = global.document.getElementById("mfNotifTrigger");
144    if (!btn) return;
145    var n = Number(unreadCount) || 0;
146    lastUnread = n;
147    btn.classList.toggle("has-unread", n > 0);
148    btn.setAttribute("aria-label", n > 0
149      ? localeText("notif.bellUnread", "Notifikasi, " + n + " belum dibaca")
150      : localeText("notif.bell", "Notifikasi"));
151    var badge = btn.querySelector(".mf-notif-badge");
152    if (n <= 0) {
153      if (badge) badge.remove();
154      return;
155    }
156    if (!badge) {
157      badge = global.document.createElement("span");
158      badge.className = "mf-notif-badge";
159      badge.setAttribute("aria-hidden", "true");
160      btn.appendChild(badge);
161    }
162    badge.textContent = n > 99 ? "99+" : String(n);
163  }
164
165  function markRead(id) {
166    var auth = global.MeteorUserAuth;
167    if (!auth || typeof auth.api !== "function" || !id) {
168      return Promise.resolve(null);
169    }
170    return auth
171      .api("/me/notifications/inbox", { method: "PATCH", body: { id: id } })
172      .then(function (data) {
173        if (data && typeof data.unreadCount === "number") {
174          updateBadge(data.unreadCount);
175        }
176        if (data && Array.isArray(data.items)) {
177          lastItems = data.items;
178        }
179        return data;
180      })
181      .catch(function () {
182        return null;
183      });
184  }
185
186  function markAllRead() {
187    var auth = global.MeteorUserAuth;
188    if (!auth || typeof auth.api !== "function") {
189      return Promise.resolve(null);
190    }
191    return auth
192      .api("/me/notifications/inbox", { method: "PATCH", body: { markAllRead: true } })
193      .then(function (data) {
194        if (data && Array.isArray(data.items)) lastItems = data.items;
195        updateBadge((data && data.unreadCount) || 0);
196        return data;
197      })
198      .catch(function () {
199        return null;
200      });
201  }
202
203  function refreshBadge() {
204    var auth = global.MeteorUserAuth;
205    if (!auth || typeof auth.api !== "function") return;
206    var user = typeof auth.getUser === "function" ? auth.getUser() : null;
207    if (!user) {
208      updateBadge(0);
209      return;
210    }
211    if (typeof user.unreadNotifications === "number") {
212      updateBadge(user.unreadNotifications);
213    }
214    auth
215      .api("/me/notifications/inbox", { method: "GET" })
216      .then(function (data) {
217        updateBadge((data && data.unreadCount) || 0);
218      })
219      .catch(function () {});
220  }
221
222  function renderPanelBody(items, empty, opts) {
223    opts = opts || {};
224    var unread = (items || []).some(function (it) { return it && !it.read; });
225    var listHtml;
226    if (empty || !items || !items.length) {
227      listHtml =
228        '<div class="mf-notif-empty">' +
229        '<div class="mf-notif-empty-icon" aria-hidden="true">' + ic("bell", 22) + "</div>" +
230        '<p class="mf-notif-empty-title">' +
231        esc(localeText("notif.emptyTitle", "Belum ada notifikasi")) +
232        "</p>" +
233        '<p class="mf-notif-empty-sub">' +
234        esc(localeText("notif.emptySub", "Pembayaran, langganan, koin, dan pengingat akun akan muncul di sini.")) +
235        "</p>" +
236        "</div>";
237    } else {
238      listHtml =
239        '<div class="mf-notif-list" role="list">' +
240        items.map(renderItem).join("") +
241        "</div>";
242    }
243    return (
244      '<div class="mf-notif-head">' +
245      '<div class="mf-notif-head-row">' +
246      '<p class="mf-notif-title" id="mfNotifPanelTitle">' +
247      esc(localeText("notif.title", "Notifikasi")) +
248      "</p>" +
249      (unread
250        ? '<button type="button" class="mf-notif-mark-all" id="mfNotifMarkAll">' +
251          esc(localeText("notif.markAll", "Tandai dibaca")) +
252          "</button>"
253        : "") +
254      "</div>" +
255      "</div>" +
256      listHtml +
257      '<div class="mf-notif-foot">' +
258      '<a class="mf-notif-see-all" href="/notifications">' +
259      esc(localeText("notif.seeAll", "Lihat semua notifikasi")) +
260      "</a>" +
261      "</div>"
262    );
263  }
264
265  function bindPanelActions(inner) {
266    if (!inner) return;
267    var markBtn = inner.querySelector("#mfNotifMarkAll");
268    if (markBtn) {
269      markBtn.addEventListener("click", function (e) {
270        e.preventDefault();
271        e.stopPropagation();
272        markBtn.disabled = true;
273        markAllRead().then(function (data) {
274          if (!panelOpen) return;
275          var items = (data && data.items) || lastItems.map(function (it) {
276            return Object.assign({}, it, { read: true });
277          });
278          inner.innerHTML = renderPanelBody(items, !items.length);
279          bindPanelActions(inner);
280          positionPanel();
281        });
282      });
283    }
284    var list = inner.querySelector(".mf-notif-list");
285    if (list) {
286      list.addEventListener("click", function (e) {
287        var link = e.target && e.target.closest ? e.target.closest("[data-notif-id]") : null;
288        if (!link) return;
289        var id = link.getAttribute("data-notif-id");
290        if (id) markRead(id);
291      });
292    }
293  }
294
295  function ensurePanel() {
296    var panel = global.document.getElementById("mfNotifPanel");
297    if (panel) return panel;
298    panel = global.document.createElement("div");
299    panel.id = "mfNotifPanel";
300    panel.className = "mf-notif-panel";
301    panel.hidden = true;
302    panel.setAttribute("role", "dialog");
303    panel.setAttribute("aria-modal", "false");
304    panel.setAttribute("aria-labelledby", "mfNotifPanelTitle");
305    panel.innerHTML = '<div class="mf-notif-panel-inner" id="mfNotifPanelInner"></div>';
306    global.document.body.appendChild(panel);
307    return panel;
308  }
309
310  function positionPanel() {
311    var panel = global.document.getElementById("mfNotifPanel");
312    var btn = global.document.getElementById("mfNotifTrigger");
313    if (!panel || !btn || panel.hidden) return;
314    var rect = btn.getBoundingClientRect();
315    var margin = 12;
316    var gap = 10;
317    var isMobile = global.innerWidth < 640;
318    panel.style.position = "fixed";
319    panel.style.zIndex = "262";
320    if (isMobile) {
321      panel.style.width = "calc(100vw - 24px)";
322      panel.style.left = "12px";
323      panel.style.right = "12px";
324      panel.style.top = "auto";
325      panel.style.bottom = "max(12px, env(safe-area-inset-bottom, 0px))";
326      panel.style.maxHeight = Math.min(420, global.innerHeight * 0.62) + "px";
327      return;
328    }
329    var width = Math.min(380, global.innerWidth - margin * 2);
330    panel.style.width = width + "px";
331    panel.style.left = "auto";
332    panel.style.bottom = "auto";
333    var top = Math.round(rect.bottom + gap);
334    panel.style.top = top + "px";
335    var right = Math.round(global.innerWidth - rect.right);
336    right = Math.max(margin, Math.min(right, global.innerWidth - width - margin));
337    panel.style.right = right + "px";
338    panel.style.maxHeight = Math.max(220, global.innerHeight - top - margin) + "px";
339  }
340
341  function closePanel() {
342    panelOpen = false;
343    var panel = global.document.getElementById("mfNotifPanel");
344    var btn = global.document.getElementById("mfNotifTrigger");
345    if (panel) {
346      panel.classList.remove("is-open");
347      panel.hidden = true;
348    }
349    if (btn) {
350      btn.setAttribute("aria-expanded", "false");
351      btn.classList.remove("mf-notif-trigger--active");
352    }
353  }
354
355  function openPanel() {
356    var panel = ensurePanel();
357    var btn = global.document.getElementById("mfNotifTrigger");
358    var inner = global.document.getElementById("mfNotifPanelInner");
359    if (!btn || !inner) return;
360
361    if (global.MeteorUserAccount && typeof global.MeteorUserAccount.closeMenu === "function") {
362      global.MeteorUserAccount.closeMenu();
363    }
364    if (global.MeteorSiteHeader && typeof global.MeteorSiteHeader.closeNav === "function") {
365      global.MeteorSiteHeader.closeNav();
366    }
367    if (global.MeteorSiteHeader && typeof global.MeteorSiteHeader.closeSearch === "function") {
368      global.MeteorSiteHeader.closeSearch();
369    }
370
371    var auth = global.MeteorUserAuth;
372    var user = auth && typeof auth.getUser === "function" ? auth.getUser() : null;
373    if (!user) {
374      panelOpen = true;
375      panel.hidden = false;
376      panel.classList.add("is-open");
377      btn.setAttribute("aria-expanded", "true");
378      btn.classList.add("mf-notif-trigger--active");
379      inner.innerHTML =
380        '<div class="mf-notif-empty">' +
381        '<div class="mf-notif-empty-icon" aria-hidden="true">' + ic("signIn", 22) + "</div>" +
382        '<p class="mf-notif-empty-title">' + esc(localeText("notif.loginTitle", "Masuk untuk lihat notifikasi")) + "</p>" +
383        '<p class="mf-notif-empty-sub">' + esc(localeText("notif.loginSub", "Status pembayaran, langganan, dan koin muncul setelah kamu login.")) + "</p>" +
384        '<div class="mf-notif-empty-actions">' +
385        '<a class="mf-notif-empty-btn" href="/auth/login?next=%2Fnotifications">' + esc(localeText("account.signIn", "Masuk")) + "</a>" +
386        "</div></div>";
387      positionPanel();
388      return;
389    }
390
391    panelOpen = true;
392    panel.hidden = false;
393    panel.classList.add("is-open");
394    btn.setAttribute("aria-expanded", "true");
395    btn.classList.add("mf-notif-trigger--active");
396    inner.innerHTML =
397      '<div class="mf-notif-loading" role="status">' +
398      esc(localeText("common.loading", "Memuat…")) +
399      "</div>";
400    positionPanel();
401
402    fetchInbox()
403      .then(function (result) {
404        var items = result.items || [];
405        lastItems = items;
406        updateBadge(typeof result.unreadCount === "number" ? result.unreadCount : 0);
407        if (!panelOpen) return;
408        if (result.source === "error") {
409          inner.innerHTML =
410            '<div class="mf-notif-empty">' +
411            '<p class="mf-notif-empty-title">' + esc(localeText("notif.errorTitle", "Gagal memuat")) + "</p>" +
412            '<p class="mf-notif-empty-sub">' + esc(localeText("notif.errorSub", "Coba buka lagi sebentar.")) + "</p>" +
413            "</div>";
414          positionPanel();
415          return;
416        }
417        inner.innerHTML = renderPanelBody(items, !items.length);
418        bindPanelActions(inner);
419        positionPanel();
420      })
421      .catch(function () {
422        if (!panelOpen) return;
423        lastItems = [];
424        inner.innerHTML = renderPanelBody([], true);
425        positionPanel();
426      });
427  }
428
429  function togglePanel() {
430    if (panelOpen) closePanel();
431    else openPanel();
432  }
433
434  function bindTrigger() {
435    var btn = global.document.getElementById("mfNotifTrigger");
436    if (!btn || btn.dataset.mfNotifBound === "1") return;
437    btn.dataset.mfNotifBound = "1";
438    btn.addEventListener("click", function (e) {
439      e.preventDefault();
440      e.stopPropagation();
441      togglePanel();
442    });
443  }
444
445  function ensureDismiss() {
446    if (global.document.body && global.document.body.dataset.mfNotifDismiss === "1") return;
447    if (global.document.body) global.document.body.dataset.mfNotifDismiss = "1";
448    global.document.addEventListener(
449      "click",
450      function (e) {
451        if (!panelOpen) return;
452        if (e.target && e.target.closest) {
453          if (e.target.closest("#mfNotifTrigger")) return;
454          if (e.target.closest("#mfNotifPanel")) return;
455        }
456        closePanel();
457      },
458      true
459    );
460    global.document.addEventListener("keydown", function (e) {
461      if (e.key === "Escape" && panelOpen) closePanel();
462    });
463    global.addEventListener(
464      "resize",
465      function () {
466        if (panelOpen) positionPanel();
467      },
468      { passive: true }
469    );
470    global.addEventListener(
471      "scroll",
472      function () {
473        if (panelOpen && global.innerWidth >= 640) positionPanel();
474      },
475      { passive: true }
476    );
477  }
478
479  function init() {
480    bindTrigger();
481    ensureDismiss();
482    ensurePanel();
483    refreshBadge();
484    global.document.addEventListener("meteor:user-updated", refreshBadge);
485    global.document.addEventListener("meteor:auth", refreshBadge);
486    global.document.addEventListener("meteor:entitlement", refreshBadge);
487    global.document.addEventListener("meteor:wallet", refreshBadge);
488    global.document.addEventListener("meteor:wallet-updated", refreshBadge);
489  }
490
491  global.MeteorNotifications = {
492    init: init,
493    open: openPanel,
494    close: closePanel,
495    toggle: togglePanel,
496    bindTrigger: bindTrigger,
497    refreshBadge: refreshBadge,
498    markRead: markRead,
499    markAllRead: markAllRead
500  };
501
502  global.document.addEventListener("meteor:header-ready", init);
503  if (global.document.readyState !== "loading") {
504    init();
505  }
506})(typeof window !== "undefined" ? window : this);

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.