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.
- package/library.js +268 -82
- package/package.json +1 -1
- package/static/lib/client.js +427 -224
- package/templates/niki-wallet.tpl +158 -29
package/static/lib/client.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<div
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
294
|
+
// modal aç
|
|
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
|
-
|
|
240
|
-
|
|
389
|
+
function injectFallbackQRModal() {
|
|
390
|
+
if ($('#modal-qr').length) return;
|
|
241
391
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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('&', '&')
|
|
438
|
+
.replaceAll('<', '<')
|
|
439
|
+
.replaceAll('>', '>')
|
|
440
|
+
.replaceAll('"', '"')
|
|
441
|
+
.replaceAll("'", ''');
|
|
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
|
+
});
|