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, "<") 34 .replace(/>/g, ">") 35 .replace(/"/g, """); 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.