nodebb-plugin-niki-loyalty 1.0.24 → 1.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Niki The Cat Coffee Loyalty System - Earn points while studying on IEU Forum.",
5
5
  "main": "library.js",
6
6
  "nbbpm": {
@@ -1,472 +1,128 @@
1
1
  'use strict';
2
2
 
3
3
  $(document).ready(function () {
4
- // =========================================
5
- // AYARLAR
6
- // =========================================
7
- const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
8
-
9
- // 60/120/180/250 QR üret (kasa okutup puan düşsün) ✅ senin istediğin
10
- // Not: Backend tarafında /api/niki-loyalty/claim rewardId ile token döndürmeli,
11
- // puan düşümü scan-coupon tarafında olmalı.
12
- const REWARDS_FALLBACK = [
13
- { id: 'cookie', at: 60, title: 'Ücretsiz Kurabiye' },
14
- { id: 'c35', at: 120, title: '%35 İndirimli Kahve' },
15
- { id: 'c60', at: 180, title: '%60 İndirimli Kahve' },
16
- { id: 'coffee', at: 250, title: 'Ücretsiz Kahve' },
17
- ];
18
-
19
- // QR ekranda kalma süresi (eski gibi) ✅ 2 dakika
20
- const QR_TTL_SECONDS = 120;
21
-
22
- // =========================================
23
- // FLOATING WIDGET
24
- // =========================================
25
- const widgetHtml = `
26
- <div id="niki-floating-widget" class="niki-hidden">
27
- <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
28
- <img src="${NIKI_LOGO_URL}" class="niki-widget-logo" alt="Niki">
29
- <div class="niki-widget-text">
30
- <span class="niki-lbl">PUANIM</span>
31
- <span class="niki-val" id="niki-live-points">...</span>
4
+ // --- AYARLAR ---
5
+ // 1. Logo Ayarı (Senin çalışan linkin)
6
+ const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
7
+
8
+ // Widget HTML Şablonu
9
+ const widgetHtml = `
10
+ <div id="niki-floating-widget" class="niki-hidden">
11
+ <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
12
+ <img src="${NIKI_LOGO_URL}" class="niki-widget-logo" alt="Niki">
13
+ <div class="niki-widget-text">
14
+ <span class="niki-lbl">PUANIM</span>
15
+ <span class="niki-val" id="niki-live-points">...</span>
16
+ </div>
17
+ </div>
32
18
  </div>
33
- </div>
34
- </div>
35
- `;
36
-
37
- function fixLogo() {
38
- const img = document.querySelector("img.niki-widget-logo");
39
- if (img && img.src !== NIKI_LOGO_URL) img.src = NIKI_LOGO_URL;
40
- }
41
-
42
- function showNikiToast(msg, kind) {
43
- $('.niki-toast').remove();
44
- const icon = (kind === 'err') ? 'fa-triangle-exclamation' : (kind === 'warn' ? 'fa-circle-info' : 'fa-paw');
45
- const toast = $(`<div class="niki-toast niki-${kind || 'ok'}"><i class="fa ${icon}"></i> ${msg}</div>`);
46
- $('body').append(toast);
47
- setTimeout(() => toast.addClass('show'), 50);
48
- setTimeout(() => {
49
- toast.removeClass('show');
50
- setTimeout(() => toast.remove(), 350);
51
- }, 3000);
52
- }
19
+ `;
53
20
 
54
- function setPointsUI(points) {
55
- const p = Number.isFinite(+points) ? +points : 0;
56
- $('#niki-live-points').text(p);
57
- $('#niki-floating-widget').removeClass('niki-hidden');
58
- localStorage.setItem('niki_last_points', String(p));
21
+ // 1. Widget Başlatma ve Veri Yönetimi
22
+ function initNikiWidget() {
23
+ if (!app.user.uid || app.user.uid <= 0) return;
59
24
 
60
- // Wallet tpl içinde varsa güncelle
61
- $('#my-points, #niki-wallet-points, .niki-wallet-points').text(p);
62
- $('#target-txt').text(`${p} / 250`);
63
- }
64
- $(document).on('click', '#btn-qr-auto', function () {
65
- $.post('/api/niki-loyalty/claim-auto', { _csrf: config.csrf_token }, function (res) {
66
- if (!res || !res.success) {
67
- app.alertError(res?.message || 'QR oluşturulamadı');
68
- return;
69
- }
70
-
71
- // modal aç (tpl’deki openCouponModal varsa onu çağır)
72
- const title = `ÖDÜL • ${res.cost} PUAN`;
73
- if (typeof window.openCouponModal === 'function') {
74
- window.openCouponModal(title, res.token);
75
- } else {
76
- // client.js içindeki openQRModal fallback’ı varsa kullan
77
- openQRModal({ token: res.token, title, ttlSeconds: 120 });
78
- }
79
-
80
- // ✅ polling: puan düşünce success’e geç
81
- initialPointsForQR = parseInt(localStorage.getItem('niki_last_points') || '0', 10) || null;
82
- startPollingForSuccess();
83
- });
84
- });
25
+ // Widget yoksa ekle
26
+ if ($('#niki-floating-widget').length === 0) {
27
+ $('body').append(widgetHtml);
28
+ }
85
29
 
86
- function initNikiWidget() {
87
- if (!app.user.uid || app.user.uid <= 0) return;
30
+ // --- HIZLI YÜKLEME (CACHE) ---
31
+ // Önce hafızadaki son puanı hemen göster (Bekletme yapmaz)
32
+ const cachedPoints = localStorage.getItem('niki_last_points');
33
+ if (cachedPoints !== null) {
34
+ $('#niki-live-points').text(cachedPoints);
35
+ $('#niki-floating-widget').removeClass('niki-hidden');
36
+ }
88
37
 
89
- if ($('#niki-floating-widget').length === 0) {
90
- $('body').append(widgetHtml);
38
+ // Logo Kontrolü (Garanti olsun)
39
+ fixLogo();
40
+
41
+ // --- GÜNCEL VERİ ÇEKME ---
42
+ // Arka planda sunucuya sor: "Puan değişti mi?"
43
+ $.get('/api/niki-loyalty/wallet-data', function(data) {
44
+ const freshPoints = data.points || 0;
45
+
46
+ // Puanı güncelle
47
+ $('#niki-live-points').text(freshPoints);
48
+ $('#niki-floating-widget').removeClass('niki-hidden'); // İlk kez açılıyorsa göster
49
+
50
+ // Yeni puanı hafızaya at (Bir sonraki giriş için)
51
+ localStorage.setItem('niki_last_points', freshPoints);
52
+
53
+ // Logoyu tekrar kontrol et (Resim geç yüklendiyse)
54
+ fixLogo();
55
+ }).fail(function() {
56
+ // Hata olursa ve cache yoksa 0 yaz
57
+ if (cachedPoints === null) {
58
+ $('#niki-live-points').text('0');
59
+ $('#niki-floating-widget').removeClass('niki-hidden');
60
+ }
61
+ });
91
62
  }
92
63
 
93
- const cachedPoints = localStorage.getItem('niki_last_points');
94
- if (cachedPoints !== null) {
95
- $('#niki-live-points').text(cachedPoints);
96
- $('#niki-floating-widget').removeClass('niki-hidden');
64
+ // Logo Düzeltici (Senin çalışan kodun entegresi)
65
+ function fixLogo() {
66
+ const img = document.querySelector("img.niki-widget-logo");
67
+ if (img && img.src !== NIKI_LOGO_URL) {
68
+ img.src = NIKI_LOGO_URL;
69
+ }
97
70
  }
98
71
 
99
- fixLogo();
100
-
101
- $.get('/api/niki-loyalty/wallet-data', function (data) {
102
- const freshPoints = data?.points || 0;
103
- setPointsUI(freshPoints);
104
- fixLogo();
105
-
106
- // Wallet sayfasındaysa rewards render et
107
- if (isWalletPage()) {
108
- renderWalletFromData(data);
109
- }
110
- }).fail(function () {
111
- if (cachedPoints === null) {
112
- setPointsUI(0);
113
- }
114
- });
115
- }
116
-
117
- function isWalletPage() {
118
- // nodebb template adı her projede değişebilir; pathname ile de kontrol edelim
119
- const p = window.location.pathname || '';
120
- return (ajaxify?.data?.template === 'niki-wallet') || p.indexOf('niki-wallet') > -1;
121
- }
122
-
123
- initNikiWidget();
124
-
125
- $(window).on('action:ajaxify.end', function () {
72
+ // Başlat
126
73
  initNikiWidget();
127
- setTimeout(fixLogo, 400);
128
- });
129
-
130
- // =========================================
131
- // HEARTBEAT
132
- // =========================================
133
- let activeSeconds = 0;
134
- let isUserActive = false;
135
- let idleTimer;
136
74
 
137
- function resetIdleTimer() {
138
- isUserActive = true;
139
- clearTimeout(idleTimer);
140
- idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
141
- }
142
- $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
75
+ // Sayfa Geçişlerinde Tekrar Çalıştır
76
+ $(window).on('action:ajaxify.end', function () {
77
+ initNikiWidget();
78
+ setTimeout(fixLogo, 500); // 0.5sn sonra son bir kontrol
79
+ });
143
80
 
144
- setInterval(() => {
145
- if (ajaxify?.data?.template?.topic && document.visibilityState === 'visible' && isUserActive) {
146
- activeSeconds++;
147
- }
148
- if (activeSeconds >= 60) {
149
- sendHeartbeat();
150
- activeSeconds = 0;
81
+ // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
82
+ let activeSeconds = 0;
83
+ let isUserActive = false;
84
+ let idleTimer;
85
+
86
+ function resetIdleTimer() {
87
+ isUserActive = true;
88
+ clearTimeout(idleTimer);
89
+ idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
151
90
  }
152
- }, 1000);
153
-
154
- function sendHeartbeat() {
155
- $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function (res) {
156
- if (res && res.earned) {
157
- setPointsUI(res.total);
158
- showNikiToast(`+${res.points} Puan Kazandın! ⚡`, 'ok');
91
+ $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
159
92
 
160
- $('#niki-floating-widget').addClass('niki-bounce');
161
- setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
162
-
163
- // wallet açıksa güncelle
164
- if (isWalletPage()) {
165
- refreshWallet();
93
+ setInterval(() => {
94
+ if (ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
95
+ activeSeconds++;
166
96
  }
167
- }
168
- });
169
- }
170
-
171
- // =========================================
172
- // WALLET: Rewards render + QR flow (2 dk)
173
- // =========================================
174
- let pollInterval = null;
175
- let initialPointsForQR = null;
176
- let timerInterval = null;
177
-
178
- function refreshWallet() {
179
- $.get('/api/niki-loyalty/wallet-data', function (data) {
180
- setPointsUI(data?.points || 0);
181
- renderWalletFromData(data);
182
- });
183
- }
184
-
185
- function renderWalletFromData(data) {
186
- // 1) Bar (varsa)
187
- try {
188
- const p = parseInt(data?.points || 0, 10) || 0;
189
- const goal = parseInt(data?.barMax || 250, 10) || 250;
190
- const pct = Math.min(100, (p / goal) * 100);
191
- $('#prog-bar').css('width', pct + '%');
192
- $('#target-txt').text(`${p} / ${goal}`);
193
- } catch (e) {}
194
-
195
- // 2) Rewards container varsa bas
196
- const $wrap = $('#niki-rewards');
197
- if ($wrap.length === 0) return;
198
-
199
- const points = parseInt(data?.points || 0, 10) || 0;
200
- const rewards = Array.isArray(data?.rewards) && data.rewards.length ? data.rewards : REWARDS_FALLBACK.map(r => ({
201
- id: r.id, at: r.at, title: r.title, unlocked: points >= r.at
202
- }));
203
-
204
- const cards = rewards.map(r => {
205
- const at = parseInt(r.at || 0, 10) || 0;
206
- const unlocked = points >= at;
207
-
208
- const btnText = unlocked ? 'QR OLUŞTUR' : `${at - points} PUAN EKSİK`;
209
- const btnDisabled = unlocked ? '' : 'disabled';
210
-
211
- return `
212
- <div class="niki-r-card ${unlocked ? 'on' : ''}" style="display:flex;align-items:center;justify-content:space-between;padding:12px 12px;border-radius:16px;background:#FAFAFA;border:1px solid #EEE;margin:10px 0;">
213
- <div style="text-align:left;">
214
- <div style="font-weight:800;color:#1a1a1a;font-size:13px;">${escapeHtml(r.title)}</div>
215
- <div style="color:#888;font-size:11px;font-weight:700;margin-top:2px;">${at} puan</div>
216
- </div>
217
- <button class="niki-genqr-btn" data-reward="${escapeAttr(r.id)}" data-title="${escapeAttr(r.title)}" data-cost="${at}"
218
- ${btnDisabled}
219
- style="border:none;border-radius:14px;padding:12px 12px;font-weight:900;font-size:12px;cursor:${unlocked ? 'pointer' : 'not-allowed'};background:${unlocked ? '#1a1a1a' : '#E0E0E0'};color:${unlocked ? '#fff' : '#999'};min-width:120px;">
220
- ${btnText}
221
- </button>
222
- </div>
223
- `;
224
- }).join('');
225
-
226
- $wrap.html(`
227
- <div style="font-size:11px;color:#888;text-align:left;margin:6px 0 10px;font-weight:700;">
228
- QR oluşturunca puanın <b>kasada okutulunca</b> düşer. (2 dakika geçerli)
229
- </div>
230
- ${cards}
231
- `);
232
-
233
- // click bind
234
- $wrap.off('click.nikiGen').on('click.nikiGen', '.niki-genqr-btn:not([disabled])', function () {
235
- const rewardId = $(this).data('reward');
236
- const title = $(this).data('title') || 'Ödül';
237
- const cost = parseInt($(this).data('cost') || 0, 10) || 0;
238
-
239
- generateRewardQR({ rewardId, title, cost });
240
- });
241
- }
242
-
243
- function generateRewardQR({ rewardId, title, cost }) {
244
- // qr modal varsa onu kullan, yoksa basit popup yap
245
- if (!rewardId) return;
246
-
247
- // güvenli: o anda puan snapshot
248
- $.get('/api/niki-loyalty/wallet-data', function (data) {
249
- const points = parseInt(data?.points || 0, 10) || 0;
250
- if (points < cost) {
251
- showNikiToast('Yetersiz puan', 'warn');
252
- refreshWallet();
253
- return;
254
- }
255
-
256
- if (!confirm(`${title} için QR oluşturulsun mu? (2 dakika geçerli)`)) return;
257
-
258
- // ✅ Claim = sadece token üret (puan düşmez), okutunca düşer
259
- $.post('/api/niki-loyalty/claim', { rewardId, _csrf: config.csrf_token }, function (res) {
260
- if (!res || !res.success || !res.token) {
261
- showNikiToast(res?.message || 'QR oluşturulamadı', 'err');
262
- return;
97
+ if (activeSeconds >= 60) {
98
+ sendHeartbeat();
99
+ activeSeconds = 0;
263
100
  }
101
+ }, 1000);
264
102
 
265
- initialPointsForQR = points;
266
- openQRModal({
267
- token: res.token,
268
- title: title,
269
- ttlSeconds: QR_TTL_SECONDS,
103
+ function sendHeartbeat() {
104
+ $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function(res) {
105
+ if (res.earned) {
106
+ // Puanı güncelle
107
+ $('#niki-live-points').text(res.total);
108
+ // Hafızayı da güncelle
109
+ localStorage.setItem('niki_last_points', res.total);
110
+
111
+ showNikiToast(`+${res.points} Puan Kazandın! ☕`);
112
+ $('#niki-floating-widget').addClass('niki-bounce');
113
+ setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
114
+ }
270
115
  });
271
-
272
- startPollingForSuccess();
273
- }).fail(function () {
274
- showNikiToast('Sunucu hatası (claim)', 'err');
275
- });
276
- });
277
- }
278
-
279
- function openQRModal({ token, title, ttlSeconds }) {
280
- // Wallet tpl'nin modalını destekle
281
- const $modal = $('#modal-qr');
282
- const hasTplModal = $modal.length > 0;
283
-
284
- if (!hasTplModal) {
285
- // fallback: hızlı modal enjekte
286
- injectFallbackQRModal();
287
116
  }
288
117
 
289
- // tpl modal elemanları varsa set
290
- $('#view-code').show();
291
- $('#view-success').hide();
292
- $('#ticket-title').text((title || 'ÖDÜL') + ' • KASAYA GÖSTER');
293
- $('#ticket-sub').text('QR’ı kasada okut.');
294
-
295
- // QR bas (qrcodejs yoksa token yaz)
296
- const qrEl = document.getElementById('qrcode');
297
- if (qrEl) {
298
- $(qrEl).empty();
299
-
300
- if (typeof QRCode !== 'undefined') {
301
- // Büyük, okunaklı QR
302
- new QRCode(qrEl, {
303
- text: String(token),
304
- width: 260,
305
- height: 260,
306
- colorDark: "#000000",
307
- colorLight: "#ffffff",
308
- correctLevel: QRCode.CorrectLevel.L
309
- });
310
- } else {
311
- $(qrEl).html(`<div style="font-weight:900;word-break:break-all;">${escapeHtml(token)}</div>`);
312
- }
118
+ function showNikiToast(msg) {
119
+ $('.niki-toast').remove();
120
+ const toast = $(`<div class="niki-toast"><i class="fa fa-paw"></i> ${msg}</div>`);
121
+ $('body').append(toast);
122
+ setTimeout(() => { toast.addClass('show'); }, 100);
123
+ setTimeout(() => {
124
+ toast.removeClass('show');
125
+ setTimeout(() => toast.remove(), 3000);
126
+ }, 3000);
313
127
  }
314
-
315
- // modal aç
316
- $('#modal-qr').fadeIn(200).css('display', 'flex');
317
-
318
- // timer
319
- startTimer(ttlSeconds);
320
- }
321
-
322
- function startTimer(durationSec) {
323
- clearInterval(timerInterval);
324
- let timer = durationSec;
325
- const total = durationSec;
326
-
327
- $('#time-bar').css('width', '100%');
328
- $('#timer-txt').text(formatTime(timer));
329
-
330
- timerInterval = setInterval(function () {
331
- // modal kapanırsa dur
332
- if (!$('#modal-qr').is(':visible') || $('#view-success').is(':visible')) {
333
- clearInterval(timerInterval);
334
- timerInterval = null;
335
- return;
336
- }
337
-
338
- const pct = Math.max(0, (timer / total) * 100);
339
- $('#time-bar').css('width', pct + '%');
340
- $('#timer-txt').text(formatTime(timer));
341
-
342
- timer -= 1;
343
- if (timer < 0) {
344
- clearInterval(timerInterval);
345
- timerInterval = null;
346
- closeQRModal();
347
- showNikiToast('Süre doldu, kod geçersiz olabilir.', 'warn');
348
- }
349
- }, 1000);
350
- }
351
-
352
- function startPollingForSuccess() {
353
- clearInterval(pollInterval);
354
-
355
- pollInterval = setInterval(() => {
356
- // modal açık değilse polling durdur
357
- if (!$('#modal-qr').is(':visible')) {
358
- clearInterval(pollInterval);
359
- pollInterval = null;
360
- return;
361
- }
362
-
363
- // ✅ Eski gibi: puan düşerse -> kasa okutmuş demektir
364
- $.get('/api/niki-loyalty/wallet-data', function (data) {
365
- const currentP = parseInt(data?.points || 0, 10) || 0;
366
-
367
- // Puan düşmüşse başarı
368
- if (initialPointsForQR != null && currentP < initialPointsForQR) {
369
- clearInterval(pollInterval);
370
- pollInterval = null;
371
-
372
- showSuccessView();
373
- setPointsUI(currentP);
374
- // reward list refresh
375
- renderWalletFromData(data);
376
- }
377
- });
378
- }, 2000);
379
- }
380
-
381
- function showSuccessView() {
382
- $('#view-code').hide();
383
- $('#view-success').fadeIn(200);
384
-
385
- // confetti varsa patlat
386
- try {
387
- if (typeof confetti !== 'undefined') {
388
- confetti({ particleCount: 140, spread: 70, origin: { y: 0.6 } });
389
- }
390
- } catch (e) {}
391
- }
392
-
393
- // tpl'nin closeQR fonksiyonu varsa onu çağır, yoksa kendin kapat
394
- window.closeQR = window.closeQR || function () { closeQRModal(); };
395
-
396
- function closeQRModal() {
397
- $('#modal-qr').fadeOut(200);
398
- clearInterval(pollInterval);
399
- pollInterval = null;
400
- clearInterval(timerInterval);
401
- timerInterval = null;
402
- initialPointsForQR = null;
403
-
404
- // wallet refresh
405
- if (isWalletPage()) {
406
- setTimeout(refreshWallet, 400);
407
- }
408
- }
409
-
410
- function injectFallbackQRModal() {
411
- if ($('#modal-qr').length) return;
412
-
413
- $('body').append(`
414
- <div id="modal-qr" class="qr-overlay" style="position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(10,10,10,.92);z-index:10000;">
415
- <div class="ticket-card" style="background:#fff;width:320px;border-radius:26px;overflow:hidden;position:relative;">
416
- <div class="close-circle" onclick="closeQR()" style="position:absolute;top:12px;right:12px;width:30px;height:30px;border-radius:50%;background:rgba(0,0,0,.08);display:flex;align-items:center;justify-content:center;cursor:pointer;">
417
- <i class="fa fa-times"></i>
418
- </div>
419
-
420
- <div id="view-code">
421
- <div class="ticket-top" id="ticket-title" style="background:#111;padding:22px;color:#C5A065;font-weight:900;letter-spacing:2px;font-size:12px;text-align:center;">
422
- KASAYA GÖSTERİNİZ
423
- </div>
424
- <div class="ticket-body" style="padding:30px 22px;text-align:center;">
425
- <div id="qrcode" style="display:flex;justify-content:center;margin-bottom:16px;"></div>
426
- <div id="ticket-sub" style="font-size:13px;font-weight:900;color:#111;margin-top:4px;">QR hazır</div>
427
- <div class="timer-wrapper" style="width:100%;height:6px;background:#eee;border-radius:10px;overflow:hidden;margin-top:12px;">
428
- <div class="timer-bar" id="time-bar" style="height:100%;background:#C5A065;width:100%;"></div>
429
- </div>
430
- <div class="timer-text" id="timer-txt" style="font-size:12px;color:#777;margin-top:8px;font-weight:800;">2:00</div>
431
- </div>
432
- </div>
433
-
434
- <div id="view-success" class="success-view" style="display:none;padding:34px 18px;text-align:center;">
435
- <div class="success-icon" style="width:92px;height:92px;margin:0 auto 16px;border-radius:50%;background:#2E7D32;color:#fff;display:flex;align-items:center;justify-content:center;font-size:44px;">
436
- <i class="fa fa-check"></i>
437
- </div>
438
- <div style="font-size:20px;font-weight:900;color:#111;">AFİYET OLSUN!</div>
439
- <div style="font-size:13px;color:#777;margin-top:6px;">Kasa onayı alındı.</div>
440
- <button class="niki-btn" onclick="closeQR()" style="margin-top:16px;background:#111;color:#fff;width:100%;padding:14px;border-radius:16px;border:none;font-weight:900;">
441
- TAMAM
442
- </button>
443
- </div>
444
- </div>
445
- </div>
446
- `);
447
- }
448
-
449
- function formatTime(sec) {
450
- sec = Math.max(0, parseInt(sec, 10) || 0);
451
- const m = Math.floor(sec / 60);
452
- const s = sec % 60;
453
- return `${m}:${s < 10 ? '0' + s : s}`;
454
- }
455
-
456
- function escapeHtml(str) {
457
- return String(str || '')
458
- .replaceAll('&', '&amp;')
459
- .replaceAll('<', '&lt;')
460
- .replaceAll('>', '&gt;')
461
- .replaceAll('"', '&quot;')
462
- .replaceAll("'", '&#39;');
463
- }
464
- function escapeAttr(str) {
465
- return escapeHtml(str).replaceAll(' ', '');
466
- }
467
-
468
- // Wallet sayfasına ilk girişte render
469
- if (isWalletPage()) {
470
- refreshWallet();
471
- }
472
- });
128
+ });