nodebb-plugin-niki-loyalty 1.0.21 → 1.0.23

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.
@@ -1,248 +1,451 @@
1
1
  'use strict';
2
2
 
3
- /* globals document, $, window, ajaxify, app, localStorage */
4
-
5
3
  $(document).ready(function () {
6
-
7
- // --- GENEL AYARLAR ---
8
- const config = {
9
- // Widget Görünümü
10
- logoUrl: "https://i.ibb.co/nZvtpss/logo-placeholder.png",
11
- defaultLabel: "PUANIM",
12
-
13
- // Topic Notification Ayarları
14
- targetCategoryId: 1, // Takip edilecek Kategori ID
15
- checkInterval: 300000, // 5 Dakika (ms)
16
- showDuration: 15000, // Konu ekranda kaç sn kalsın?
17
- maxShowCount: 5, // Bir konu bir kişiye max kaç kez gösterilsin?
18
- topicValidity: 86400000, // Konu 24 saatten eskiyse gösterme (ms)
19
-
20
- // API Yolları
21
- walletRoute: 'niki-wallet',
22
- csrf_token: config.csrf_token
23
- };
24
-
25
- // Global Durum Değişkenleri
26
- let currentPoints = "..."; // Başlangıçta boş
27
- let isTopicAnimating = false; // Şu an konu mu gösteriliyor?
28
-
29
- // --- 1. WIDGET HTML ŞABLONU ---
30
- const widgetHtml = `
31
- <div id="niki-floating-widget" class="niki-hidden">
32
- <div id="niki-widget-content" class="niki-widget-content">
33
- <img src="${config.logoUrl}" class="niki-widget-logo" alt="Niki">
34
- <div class="niki-widget-text">
35
- <span id="niki-lbl" class="niki-lbl">${config.defaultLabel}</span>
36
- <span id="niki-val" class="niki-val">${currentPoints}</span>
37
- </div>
38
- </div>
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>
39
32
  </div>
40
- `;
41
-
42
- // --- 2. BAŞLATMA FONKSİYONU ---
43
- function initNikiWidget() {
44
- // Kullanıcı giriş yapmamışsa widget'ı gösterme
45
- if (!app.user.uid || app.user.uid <= 0) return;
46
-
47
- // HTML Enjeksiyonu
48
- if ($('#niki-floating-widget').length === 0) {
49
- $('body').append(widgetHtml);
50
- }
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
+ }
53
+
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));
59
+
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
+
65
+ function initNikiWidget() {
66
+ if (!app.user.uid || app.user.uid <= 0) return;
67
+
68
+ if ($('#niki-floating-widget').length === 0) {
69
+ $('body').append(widgetHtml);
70
+ }
51
71
 
52
- const $widget = $('#niki-floating-widget');
53
- const $val = $('#niki-val');
72
+ const cachedPoints = localStorage.getItem('niki_last_points');
73
+ if (cachedPoints !== null) {
74
+ $('#niki-live-points').text(cachedPoints);
75
+ $('#niki-floating-widget').removeClass('niki-hidden');
76
+ }
54
77
 
55
- // CACHE KONTROLÜ (Hızlı açılış için)
56
- const cachedPoints = localStorage.getItem('niki_last_points');
57
- if (cachedPoints !== null) {
58
- currentPoints = cachedPoints;
59
- $val.text(currentPoints);
60
- $widget.removeClass('niki-hidden');
61
- }
78
+ fixLogo();
79
+
80
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
81
+ const freshPoints = data?.points || 0;
82
+ setPointsUI(freshPoints);
83
+ fixLogo();
84
+
85
+ // Wallet sayfasındaysa rewards render et
86
+ if (isWalletPage()) {
87
+ renderWalletFromData(data);
88
+ }
89
+ }).fail(function () {
90
+ if (cachedPoints === null) {
91
+ setPointsUI(0);
92
+ }
93
+ });
94
+ }
62
95
 
63
- // GÜNCEL VERİ ÇEKME (Sunucudan)
64
- $.get('/api/niki-loyalty/wallet-data', function(data) {
65
- const freshPoints = data.points || 0;
66
- currentPoints = freshPoints;
67
-
68
- // Eğer şu an konu animasyonu oynamıyorsa puanı güncelle
69
- if (!isTopicAnimating) {
70
- $val.text(freshPoints);
71
- }
72
-
73
- $widget.removeClass('niki-hidden');
74
- localStorage.setItem('niki_last_points', freshPoints); // Cache güncelle
75
- fixLogo();
76
- }).fail(function() {
77
- // Hata olursa ve cache yoksa 0 göster
78
- if (cachedPoints === null) {
79
- currentPoints = "0";
80
- $val.text("0");
81
- $widget.removeClass('niki-hidden');
82
- }
83
- });
96
+ function isWalletPage() {
97
+ // nodebb template adı her projede değişebilir; pathname ile de kontrol edelim
98
+ const p = window.location.pathname || '';
99
+ return (ajaxify?.data?.template === 'niki-wallet') || p.indexOf('niki-wallet') > -1;
100
+ }
84
101
 
85
- // Tıklama Olayı (Event Delegation)
86
- $('body').off('click', '#niki-widget-content').on('click', '#niki-widget-content', function () {
87
- const link = $(this).data('link') || config.walletRoute;
88
- ajaxify.go(link);
89
- });
102
+ initNikiWidget();
90
103
 
91
- // Topic Notification Kontrolünü Başlat
92
- startTopicCheck();
104
+ $(window).on('action:ajaxify.end', function () {
105
+ initNikiWidget();
106
+ setTimeout(fixLogo, 400);
107
+ });
108
+
109
+ // =========================================
110
+ // HEARTBEAT
111
+ // =========================================
112
+ let activeSeconds = 0;
113
+ let isUserActive = false;
114
+ let idleTimer;
115
+
116
+ function resetIdleTimer() {
117
+ isUserActive = true;
118
+ clearTimeout(idleTimer);
119
+ idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
120
+ }
121
+ $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
122
+
123
+ setInterval(() => {
124
+ if (ajaxify?.data?.template?.topic && document.visibilityState === 'visible' && isUserActive) {
125
+ activeSeconds++;
93
126
  }
94
-
95
- // --- 3. TOPIC NOTIFICATION SİSTEMİ ---
96
- function startTopicCheck() {
97
- // Eski interval varsa temizle
98
- if (window.nikiTopicInterval) clearInterval(window.nikiTopicInterval);
99
-
100
- // Fonksiyon: Kontrol Et ve Göster
101
- const checkAndShow = function() {
102
- $.get('/api/category/' + config.targetCategoryId, function (data) {
103
- if (data && data.topics && data.topics.length > 0) {
104
- const topic = data.topics[0];
105
- const now = Date.now();
106
-
107
- // KURAL 1: Süre Kontrolü (24 Saat)
108
- if (now - topic.timestamp > config.topicValidity) return;
109
-
110
- // KURAL 2: Gösterim Sayısı Kontrolü (LocalStorage)
111
- let storageData = JSON.parse(localStorage.getItem('niki_topic_tracking') || '{}');
112
-
113
- // Konu değişmişse sayacı sıfırla
114
- if (storageData.tid !== topic.tid) {
115
- storageData = { tid: topic.tid, count: 0 };
116
- }
117
-
118
- // KURAL 3: Limit Dolmadıysa Göster
119
- if (storageData.count < config.maxShowCount) {
120
- animateTopicNotification(topic);
121
-
122
- // Sayacı artır ve kaydet
123
- storageData.count++;
124
- localStorage.setItem('niki_topic_tracking', JSON.stringify(storageData));
125
- }
126
- }
127
- });
128
- };
129
-
130
- // Sayfa açılır açılmaz bir kez kontrol et
131
- checkAndShow();
132
-
133
- // Sonra 5 dakikada bir devam et
134
- window.nikiTopicInterval = setInterval(checkAndShow, config.checkInterval);
127
+ if (activeSeconds >= 60) {
128
+ sendHeartbeat();
129
+ activeSeconds = 0;
135
130
  }
131
+ }, 1000);
136
132
 
137
- function animateTopicNotification(topic) {
138
- const $val = $('#niki-val');
139
- const $lbl = $('#niki-lbl');
140
- const $container = $('#niki-widget-content');
141
-
142
- if ($val.length > 0) {
143
- isTopicAnimating = true; // Flag'i kaldır (Puan güncellemesi araya girmesin)
144
-
145
- $val.fadeOut(200, function () {
146
- $lbl.text('YENİ KONU:');
147
- $(this).text(topic.title).fadeIn(200);
148
-
149
- // Linki ve Stili Değiştir
150
- $container.data('link', 'topic/' + topic.slug);
151
- $container.addClass('highlight-topic');
152
- });
153
-
154
- // 15 Saniye sonra eski haline dön
155
- setTimeout(function () {
156
- $val.fadeOut(200, function () {
157
- $lbl.text(config.defaultLabel);
158
- $(this).text(currentPoints).fadeIn(200); // Güncel puanı yaz
159
-
160
- // Linki ve Stili Sıfırla
161
- $container.data('link', config.walletRoute);
162
- $container.removeClass('highlight-topic');
163
-
164
- isTopicAnimating = false; // Flag'i indir
165
- });
166
- }, config.showDuration);
167
- }
168
- }
133
+ function sendHeartbeat() {
134
+ $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function (res) {
135
+ if (res && res.earned) {
136
+ setPointsUI(res.total);
137
+ showNikiToast(`+${res.points} Puan Kazandın! ⚡`, 'ok');
138
+
139
+ $('#niki-floating-widget').addClass('niki-bounce');
140
+ setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
169
141
 
170
- // --- 4. YARDIMCI FONKSİYONLAR ---
171
- function fixLogo() {
172
- const img = document.querySelector("img.niki-widget-logo");
173
- if (img && img.src !== config.logoUrl) {
174
- img.src = config.logoUrl;
142
+ // wallet açıksa güncelle
143
+ if (isWalletPage()) {
144
+ refreshWallet();
145
+ }
146
+ }
147
+ });
148
+ }
149
+
150
+ // =========================================
151
+ // WALLET: Rewards render + QR flow (2 dk)
152
+ // =========================================
153
+ let pollInterval = null;
154
+ let initialPointsForQR = null;
155
+ let timerInterval = null;
156
+
157
+ function refreshWallet() {
158
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
159
+ setPointsUI(data?.points || 0);
160
+ renderWalletFromData(data);
161
+ });
162
+ }
163
+
164
+ function renderWalletFromData(data) {
165
+ // 1) Bar (varsa)
166
+ try {
167
+ const p = parseInt(data?.points || 0, 10) || 0;
168
+ const goal = parseInt(data?.barMax || 250, 10) || 250;
169
+ const pct = Math.min(100, (p / goal) * 100);
170
+ $('#prog-bar').css('width', pct + '%');
171
+ $('#target-txt').text(`${p} / ${goal}`);
172
+ } catch (e) {}
173
+
174
+ // 2) Rewards container varsa bas
175
+ const $wrap = $('#niki-rewards');
176
+ if ($wrap.length === 0) return;
177
+
178
+ const points = parseInt(data?.points || 0, 10) || 0;
179
+ const rewards = Array.isArray(data?.rewards) && data.rewards.length ? data.rewards : REWARDS_FALLBACK.map(r => ({
180
+ id: r.id, at: r.at, title: r.title, unlocked: points >= r.at
181
+ }));
182
+
183
+ const cards = rewards.map(r => {
184
+ const at = parseInt(r.at || 0, 10) || 0;
185
+ const unlocked = points >= at;
186
+
187
+ const btnText = unlocked ? 'QR OLUŞTUR' : `${at - points} PUAN EKSİK`;
188
+ const btnDisabled = unlocked ? '' : 'disabled';
189
+
190
+ return `
191
+ <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;">
192
+ <div style="text-align:left;">
193
+ <div style="font-weight:800;color:#1a1a1a;font-size:13px;">${escapeHtml(r.title)}</div>
194
+ <div style="color:#888;font-size:11px;font-weight:700;margin-top:2px;">${at} puan</div>
195
+ </div>
196
+ <button class="niki-genqr-btn" data-reward="${escapeAttr(r.id)}" data-title="${escapeAttr(r.title)}" data-cost="${at}"
197
+ ${btnDisabled}
198
+ 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;">
199
+ ${btnText}
200
+ </button>
201
+ </div>
202
+ `;
203
+ }).join('');
204
+
205
+ $wrap.html(`
206
+ <div style="font-size:11px;color:#888;text-align:left;margin:6px 0 10px;font-weight:700;">
207
+ QR oluşturunca puanın <b>kasada okutulunca</b> düşer. (2 dakika geçerli)
208
+ </div>
209
+ ${cards}
210
+ `);
211
+
212
+ // click bind
213
+ $wrap.off('click.nikiGen').on('click.nikiGen', '.niki-genqr-btn:not([disabled])', function () {
214
+ const rewardId = $(this).data('reward');
215
+ const title = $(this).data('title') || 'Ödül';
216
+ const cost = parseInt($(this).data('cost') || 0, 10) || 0;
217
+
218
+ generateRewardQR({ rewardId, title, cost });
219
+ });
220
+ }
221
+
222
+ function generateRewardQR({ rewardId, title, cost }) {
223
+ // qr modal varsa onu kullan, yoksa basit popup yap
224
+ if (!rewardId) return;
225
+
226
+ // güvenli: o anda puan snapshot
227
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
228
+ const points = parseInt(data?.points || 0, 10) || 0;
229
+ if (points < cost) {
230
+ showNikiToast('Yetersiz puan', 'warn');
231
+ refreshWallet();
232
+ return;
233
+ }
234
+
235
+ if (!confirm(`${title} için QR oluşturulsun mu? (2 dakika geçerli)`)) return;
236
+
237
+ // ✅ Claim = sadece token üret (puan düşmez), okutunca düşer
238
+ $.post('/api/niki-loyalty/claim', { rewardId, _csrf: config.csrf_token }, function (res) {
239
+ if (!res || !res.success || !res.token) {
240
+ showNikiToast(res?.message || 'QR oluşturulamadı', 'err');
241
+ return;
175
242
  }
176
- }
177
243
 
178
- // --- 5. AKTİFLİK SİSTEMİ (HEARTBEAT) ---
179
- let activeSeconds = 0;
180
- let isUserActive = false;
181
- let idleTimer;
182
-
183
- function resetIdleTimer() {
184
- isUserActive = true;
185
- clearTimeout(idleTimer);
186
- idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
187
- }
188
-
189
- // Kullanıcı hareketlerini dinle
190
- $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
191
-
192
- // Her saniye kontrol et
193
- if (!window.nikiHeartbeatInterval) {
194
- window.nikiHeartbeatInterval = setInterval(() => {
195
- // Sadece bir konunun içindeysek (ajaxify.data.template.topic) ve sekme açıksa say
196
- if (ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
197
- activeSeconds++;
198
- }
199
- // 60 saniye dolduysa sunucuya bildir
200
- if (activeSeconds >= 60) {
201
- sendHeartbeat();
202
- activeSeconds = 0;
203
- }
204
- }, 1000);
244
+ initialPointsForQR = points;
245
+ openQRModal({
246
+ token: res.token,
247
+ title: title,
248
+ ttlSeconds: QR_TTL_SECONDS,
249
+ });
250
+
251
+ startPollingForSuccess();
252
+ }).fail(function () {
253
+ showNikiToast('Sunucu hatası (claim)', 'err');
254
+ });
255
+ });
256
+ }
257
+
258
+ function openQRModal({ token, title, ttlSeconds }) {
259
+ // Wallet tpl'nin modalını destekle
260
+ const $modal = $('#modal-qr');
261
+ const hasTplModal = $modal.length > 0;
262
+
263
+ if (!hasTplModal) {
264
+ // fallback: hızlı modal enjekte
265
+ injectFallbackQRModal();
205
266
  }
206
267
 
207
- function sendHeartbeat() {
208
- $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function(res) {
209
- if (res.earned) {
210
- // Global puan değişkenini güncelle
211
- currentPoints = res.total;
212
- localStorage.setItem('niki_last_points', res.total);
213
-
214
- // Eğer o sırada Konu Animasyonu oynamıyorsa, ekrandaki puanı da güncelle
215
- if (!isTopicAnimating) {
216
- $('#niki-val').text(res.total);
217
-
218
- // Ufak bir zıplama efekti
219
- $('#niki-floating-widget').addClass('niki-bounce');
220
- setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
221
- }
222
-
223
- showNikiToast(`+${res.points} Puan Kazandın! ☕`);
224
- }
268
+ // tpl modal elemanları varsa set
269
+ $('#view-code').show();
270
+ $('#view-success').hide();
271
+ $('#ticket-title').text((title || 'ÖDÜL') + ' • KASAYA GÖSTER');
272
+ $('#ticket-sub').text('QR’ı kasada okut.');
273
+
274
+ // QR bas (qrcodejs yoksa token yaz)
275
+ const qrEl = document.getElementById('qrcode');
276
+ if (qrEl) {
277
+ $(qrEl).empty();
278
+
279
+ if (typeof QRCode !== 'undefined') {
280
+ // Büyük, okunaklı QR
281
+ new QRCode(qrEl, {
282
+ text: String(token),
283
+ width: 260,
284
+ height: 260,
285
+ colorDark: "#000000",
286
+ colorLight: "#ffffff",
287
+ correctLevel: QRCode.CorrectLevel.L
225
288
  });
289
+ } else {
290
+ $(qrEl).html(`<div style="font-weight:900;word-break:break-all;">${escapeHtml(token)}</div>`);
291
+ }
226
292
  }
227
293
 
228
- function showNikiToast(msg) {
229
- $('.niki-toast').remove();
230
- const toast = $(`<div class="niki-toast"><i class="fa fa-paw"></i> ${msg}</div>`);
231
- $('body').append(toast);
232
- setTimeout(() => { toast.addClass('show'); }, 100);
233
- setTimeout(() => {
234
- toast.removeClass('show');
235
- setTimeout(() => toast.remove(), 3000);
236
- }, 3000);
294
+ // modal
295
+ $('#modal-qr').fadeIn(200).css('display', 'flex');
296
+
297
+ // timer
298
+ startTimer(ttlSeconds);
299
+ }
300
+
301
+ function startTimer(durationSec) {
302
+ clearInterval(timerInterval);
303
+ let timer = durationSec;
304
+ const total = durationSec;
305
+
306
+ $('#time-bar').css('width', '100%');
307
+ $('#timer-txt').text(formatTime(timer));
308
+
309
+ timerInterval = setInterval(function () {
310
+ // modal kapanırsa dur
311
+ if (!$('#modal-qr').is(':visible') || $('#view-success').is(':visible')) {
312
+ clearInterval(timerInterval);
313
+ timerInterval = null;
314
+ return;
315
+ }
316
+
317
+ const pct = Math.max(0, (timer / total) * 100);
318
+ $('#time-bar').css('width', pct + '%');
319
+ $('#timer-txt').text(formatTime(timer));
320
+
321
+ timer -= 1;
322
+ if (timer < 0) {
323
+ clearInterval(timerInterval);
324
+ timerInterval = null;
325
+ closeQRModal();
326
+ showNikiToast('Süre doldu, kod geçersiz olabilir.', 'warn');
327
+ }
328
+ }, 1000);
329
+ }
330
+
331
+ function startPollingForSuccess() {
332
+ clearInterval(pollInterval);
333
+
334
+ pollInterval = setInterval(() => {
335
+ // modal açık değilse polling durdur
336
+ if (!$('#modal-qr').is(':visible')) {
337
+ clearInterval(pollInterval);
338
+ pollInterval = null;
339
+ return;
340
+ }
341
+
342
+ // ✅ Eski gibi: puan düşerse -> kasa okutmuş demektir
343
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
344
+ const currentP = parseInt(data?.points || 0, 10) || 0;
345
+
346
+ // Puan düşmüşse başarı
347
+ if (initialPointsForQR != null && currentP < initialPointsForQR) {
348
+ clearInterval(pollInterval);
349
+ pollInterval = null;
350
+
351
+ showSuccessView();
352
+ setPointsUI(currentP);
353
+ // reward list refresh
354
+ renderWalletFromData(data);
355
+ }
356
+ });
357
+ }, 2000);
358
+ }
359
+
360
+ function showSuccessView() {
361
+ $('#view-code').hide();
362
+ $('#view-success').fadeIn(200);
363
+
364
+ // confetti varsa patlat
365
+ try {
366
+ if (typeof confetti !== 'undefined') {
367
+ confetti({ particleCount: 140, spread: 70, origin: { y: 0.6 } });
368
+ }
369
+ } catch (e) {}
370
+ }
371
+
372
+ // tpl'nin closeQR fonksiyonu varsa onu çağır, yoksa kendin kapat
373
+ window.closeQR = window.closeQR || function () { closeQRModal(); };
374
+
375
+ function closeQRModal() {
376
+ $('#modal-qr').fadeOut(200);
377
+ clearInterval(pollInterval);
378
+ pollInterval = null;
379
+ clearInterval(timerInterval);
380
+ timerInterval = null;
381
+ initialPointsForQR = null;
382
+
383
+ // wallet refresh
384
+ if (isWalletPage()) {
385
+ setTimeout(refreshWallet, 400);
237
386
  }
387
+ }
238
388
 
239
- // --- BAŞLATMA ---
240
- initNikiWidget();
389
+ function injectFallbackQRModal() {
390
+ if ($('#modal-qr').length) return;
241
391
 
242
- // Sayfa geçişlerinde widget'ı koru
243
- $(window).on('action:ajaxify.end', function () {
244
- initNikiWidget();
245
- setTimeout(fixLogo, 500);
246
- });
392
+ $('body').append(`
393
+ <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;">
394
+ <div class="ticket-card" style="background:#fff;width:320px;border-radius:26px;overflow:hidden;position:relative;">
395
+ <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;">
396
+ <i class="fa fa-times"></i>
397
+ </div>
247
398
 
248
- });
399
+ <div id="view-code">
400
+ <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;">
401
+ KASAYA GÖSTERİNİZ
402
+ </div>
403
+ <div class="ticket-body" style="padding:30px 22px;text-align:center;">
404
+ <div id="qrcode" style="display:flex;justify-content:center;margin-bottom:16px;"></div>
405
+ <div id="ticket-sub" style="font-size:13px;font-weight:900;color:#111;margin-top:4px;">QR hazır</div>
406
+ <div class="timer-wrapper" style="width:100%;height:6px;background:#eee;border-radius:10px;overflow:hidden;margin-top:12px;">
407
+ <div class="timer-bar" id="time-bar" style="height:100%;background:#C5A065;width:100%;"></div>
408
+ </div>
409
+ <div class="timer-text" id="timer-txt" style="font-size:12px;color:#777;margin-top:8px;font-weight:800;">2:00</div>
410
+ </div>
411
+ </div>
412
+
413
+ <div id="view-success" class="success-view" style="display:none;padding:34px 18px;text-align:center;">
414
+ <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;">
415
+ <i class="fa fa-check"></i>
416
+ </div>
417
+ <div style="font-size:20px;font-weight:900;color:#111;">AFİYET OLSUN!</div>
418
+ <div style="font-size:13px;color:#777;margin-top:6px;">Kasa onayı alındı.</div>
419
+ <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;">
420
+ TAMAM
421
+ </button>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ `);
426
+ }
427
+
428
+ function formatTime(sec) {
429
+ sec = Math.max(0, parseInt(sec, 10) || 0);
430
+ const m = Math.floor(sec / 60);
431
+ const s = sec % 60;
432
+ return `${m}:${s < 10 ? '0' + s : s}`;
433
+ }
434
+
435
+ function escapeHtml(str) {
436
+ return String(str || '')
437
+ .replaceAll('&', '&amp;')
438
+ .replaceAll('<', '&lt;')
439
+ .replaceAll('>', '&gt;')
440
+ .replaceAll('"', '&quot;')
441
+ .replaceAll("'", '&#39;');
442
+ }
443
+ function escapeAttr(str) {
444
+ return escapeHtml(str).replaceAll(' ', '');
445
+ }
446
+
447
+ // Wallet sayfasına ilk girişte render
448
+ if (isWalletPage()) {
449
+ refreshWallet();
450
+ }
451
+ });