nodebb-plugin-niki-loyalty 1.0.22 → 1.0.24

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 CHANGED
@@ -14,10 +14,7 @@ const SETTINGS = {
14
14
  pointsPerHeartbeat: 5,
15
15
  dailyCap: 250,
16
16
 
17
- // Tek bar hedefi
18
17
  barMax: 250,
19
-
20
- // ✅ 4 kademe (Seçenek B: claim = puan düşer)
21
18
  rewards: [
22
19
  { id: 'cookie', at: 60, title: 'Ücretsiz Kurabiye', type: 'free_item', meta: { item: 'cookie' } },
23
20
  { id: 'c35', at: 120, title: '%35 İndirimli Kahve', type: 'discount', meta: { product: 'coffee', percent: 35 } },
@@ -25,13 +22,10 @@ const SETTINGS = {
25
22
  { id: 'coffee', at: 250, title: 'Ücretsiz Kahve', type: 'free_item', meta: { item: 'coffee' } },
26
23
  ],
27
24
 
28
- // Eski akış uyumluluğu (istersen sonra kaldırırız)
29
- coffeeCost: 250,
30
-
31
- // Kupon token süreleri (sn)
32
25
  couponTTLSeconds: 10 * 60, // 10 dk
33
26
  };
34
27
 
28
+
35
29
  // ✅ TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
36
30
  const TEST_MODE_UNLIMITED = false;
37
31
 
@@ -226,60 +220,65 @@ Plugin.init = async function (params) {
226
220
  // Client bu endpoint'i çağıracak.
227
221
  // -------------------------
228
222
  router.post('/api/niki-loyalty/claim', middleware.ensureLoggedIn, async (req, res) => {
229
- try {
230
- const uid = req.uid;
231
- const rewardId = (req.body && req.body.rewardId) ? String(req.body.rewardId) : '';
232
- const reward = findRewardById(rewardId);
233
-
234
- if (!reward) {
235
- return res.json({ success: false, message: 'Geçersiz ödül' });
236
- }
237
-
238
- const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
239
-
240
- if (!TEST_MODE_UNLIMITED && points < reward.at) {
241
- return res.json({ success: false, message: 'Yetersiz puan' });
242
- }
243
-
244
- // ✅ Puan düş
245
- if (!TEST_MODE_UNLIMITED) {
246
- await user.decrementUserFieldBy(uid, 'niki_points', reward.at);
247
- }
248
-
249
- const newBalance = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
250
-
251
- // ✅ Kupon oluştur (kasa tarayacak)
252
- const token = makeToken();
253
- const couponKey = `niki:coupon:${token}`;
254
-
255
- const couponPayload = safeStringify({
256
- token,
257
- ts: Date.now(),
258
- rewardId: reward.id,
259
- at: reward.at,
260
- title: reward.title,
261
- type: reward.type,
262
- meta: reward.meta,
263
- ownerUid: uid,
264
- });
265
-
266
- await db.set(couponKey, couponPayload);
267
- await db.expire(couponKey, SETTINGS.couponTTLSeconds);
268
-
269
- // user log
270
- await addUserLog(uid, 'spend', reward.at, `Ödül alındı: ${reward.title}`, { rewardId: reward.id });
223
+ try {
224
+ const uid = req.uid;
225
+ const rewardId = (req.body && req.body.rewardId) ? String(req.body.rewardId) : '';
226
+ const reward = findRewardById(rewardId);
227
+ if (!reward) return res.json({ success: false, message: 'Geçersiz ödül' });
228
+
229
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
230
+ if (!TEST_MODE_UNLIMITED && points < reward.at) {
231
+ return res.json({ success: false, message: 'Yetersiz puan' });
232
+ }
271
233
 
234
+ // ✅ Aynı ödülden aktif kupon varsa tekrar üretme (spam engeli)
235
+ const reserveKey = `niki:reserve:${uid}:${reward.id}`;
236
+ const existingToken = await db.get(reserveKey);
237
+ if (existingToken) {
272
238
  return res.json({
273
239
  success: true,
274
- token,
240
+ token: existingToken,
275
241
  reward: { id: reward.id, at: reward.at, title: reward.title, type: reward.type, meta: reward.meta },
276
- newBalance,
277
- message: 'Ödül hazır! Kasada okutarak kullan.',
242
+ message: 'Zaten aktif bir kuponun var. Kasada okutabilirsin.',
278
243
  });
279
- } catch (err) {
280
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
281
244
  }
282
- });
245
+
246
+ // ✅ token oluştur
247
+ const token = makeToken();
248
+ const couponKey = `niki:coupon:${token}`;
249
+
250
+ const couponPayload = safeStringify({
251
+ token,
252
+ ts: Date.now(),
253
+ rewardId: reward.id,
254
+ cost: reward.at, // ✅ puan düşülecek tutar
255
+ title: reward.title,
256
+ type: reward.type,
257
+ meta: reward.meta,
258
+ ownerUid: uid,
259
+ });
260
+
261
+ await db.set(couponKey, couponPayload);
262
+ await db.expire(couponKey, SETTINGS.couponTTLSeconds);
263
+
264
+ // ✅ rezervasyonu da tut
265
+ await db.set(reserveKey, token);
266
+ await db.expire(reserveKey, SETTINGS.couponTTLSeconds);
267
+
268
+ // user log (claim logu - puan düşmeden)
269
+ await addUserLog(uid, 'spend', 0, `Kupon oluşturuldu: ${reward.title}`, { rewardId: reward.id, token });
270
+
271
+ return res.json({
272
+ success: true,
273
+ token,
274
+ reward: { id: reward.id, at: reward.at, title: reward.title, type: reward.type, meta: reward.meta },
275
+ message: 'Kupon hazır! Kasada okutunca puanın düşer.',
276
+ });
277
+ } catch (err) {
278
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
279
+ }
280
+ });
281
+
283
282
 
284
283
  // -------------------------
285
284
  // 4) KASA HISTORY (admin/mod)
@@ -341,54 +340,65 @@ Plugin.init = async function (params) {
341
340
  // 5) KASA: COUPON SCAN (admin/mod) ✅ yeni akış
342
341
  // Claim ile üretilen token kasada okutulur.
343
342
  // -------------------------
344
- router.post('/api/niki-loyalty/scan-coupon', middleware.ensureLoggedIn, async (req, res) => {
345
- try {
346
- const token = (req.body && req.body.token) ? String(req.body.token) : '';
347
-
348
- const staffOk = await isStaff(req.uid);
349
- if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
343
+ router.post('/api/niki-loyalty/scan-coupon', middleware.ensureLoggedIn, async (req, res) => {
344
+ try {
345
+ const token = (req.body && req.body.token) ? String(req.body.token) : '';
350
346
 
351
- const raw = await db.get(`niki:coupon:${token}`);
352
- if (!raw) return res.json({ success: false, message: 'Geçersiz / Süresi dolmuş kupon' });
347
+ const staffOk = await isStaff(req.uid);
348
+ if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
353
349
 
354
- const coupon = safeParseMaybeJson(raw);
355
- if (!coupon || !coupon.ownerUid) {
356
- await db.delete(`niki:coupon:${token}`);
357
- return res.json({ success: false, message: 'Kupon bozuk' });
358
- }
350
+ const raw = await db.get(`niki:coupon:${token}`);
351
+ if (!raw) return res.json({ success: false, message: 'Geçersiz / Süresi dolmuş kupon' });
359
352
 
360
- // tek kullanımlık
353
+ const coupon = safeParseMaybeJson(raw);
354
+ if (!coupon || !coupon.ownerUid || !coupon.rewardId) {
361
355
  await db.delete(`niki:coupon:${token}`);
356
+ return res.json({ success: false, message: 'Kupon bozuk' });
357
+ }
362
358
 
363
- const customerUid = parseInt(coupon.ownerUid, 10);
364
- const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
359
+ const customerUid = parseInt(coupon.ownerUid, 10);
360
+ const cost = parseInt(coupon.cost || 0, 10);
365
361
 
366
- // kasa log (artık amount = coupon.at)
367
- await addKasaLog(
368
- req.uid,
369
- customerData?.username || 'Bilinmeyen',
370
- customerUid,
371
- parseInt(coupon.at || 0, 10),
372
- coupon.rewardId,
373
- coupon.title
374
- );
362
+ // puan yeter mi? (scan anında kontrol)
363
+ const pts = parseInt((await user.getUserField(customerUid, 'niki_points')) || 0, 10);
364
+ if (!TEST_MODE_UNLIMITED && pts < cost) {
365
+ return res.json({ success: false, message: 'Yetersiz bakiye (puan değişmiş olabilir)' });
366
+ }
375
367
 
376
- return res.json({
377
- success: true,
378
- customer: customerData,
379
- coupon: {
380
- rewardId: coupon.rewardId,
381
- title: coupon.title,
382
- type: coupon.type,
383
- meta: coupon.meta,
384
- at: coupon.at,
385
- },
386
- message: 'Kupon onaylandı!',
387
- });
388
- } catch (err) {
389
- return res.status(500).json({ success: false, message: 'Sunucu hatası' });
368
+ // ✅ puan düş (asıl düşüş burada)
369
+ if (!TEST_MODE_UNLIMITED && cost > 0) {
370
+ await user.decrementUserFieldBy(customerUid, 'niki_points', cost);
390
371
  }
391
- });
372
+
373
+ // ✅ tek kullanımlık: kuponu sil
374
+ await db.delete(`niki:coupon:${token}`);
375
+ // ✅ rezervasyonu da sil
376
+ await db.delete(`niki:reserve:${customerUid}:${coupon.rewardId}`);
377
+
378
+ const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
379
+
380
+ // user log (gerçek spend burada)
381
+ await addUserLog(customerUid, 'spend', cost, `Kullanıldı: ${coupon.title}`, { rewardId: coupon.rewardId });
382
+
383
+ // kasa log
384
+ await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, cost, coupon.rewardId, coupon.title);
385
+
386
+ return res.json({
387
+ success: true,
388
+ customer: customerData,
389
+ coupon: {
390
+ rewardId: coupon.rewardId,
391
+ title: coupon.title,
392
+ type: coupon.type,
393
+ meta: coupon.meta,
394
+ cost,
395
+ },
396
+ message: 'Kupon onaylandı!',
397
+ });
398
+ } catch (err) {
399
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
400
+ }
401
+ });
392
402
 
393
403
  // -------------------------
394
404
  // 6) (ESKİ) QR OLUŞTUR / OKUT — opsiyonel uyumluluk
@@ -413,6 +423,45 @@ Plugin.init = async function (params) {
413
423
  return res.status(500).json({ success: false, message: 'Sunucu hatası' });
414
424
  }
415
425
  });
426
+ const TIERS = [60, 120, 180, 250];
427
+
428
+ function pickTier(points) {
429
+ // en büyük uygun tier
430
+ let chosen = 0;
431
+ for (const t of TIERS) if (points >= t) chosen = t;
432
+ return chosen; // 0 ise yetersiz
433
+ }
434
+
435
+ router.post('/api/niki-loyalty/claim-auto', middleware.ensureLoggedIn, async (req, res) => {
436
+ try {
437
+ const uid = req.uid;
438
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
439
+
440
+ if (TEST_MODE_UNLIMITED) {
441
+ // testte 250 gibi davran
442
+ const token = makeToken();
443
+ await db.set(`niki:coupon:${token}`, safeStringify({ token, ts: Date.now(), ownerUid: uid, cost: 250 }));
444
+ await db.expire(`niki:coupon:${token}`, 120);
445
+ return res.json({ success: true, token, cost: 250 });
446
+ }
447
+
448
+ const cost = pickTier(points);
449
+ if (!cost) return res.json({ success: false, message: 'En az 60 puan gerekli.' });
450
+
451
+ const token = makeToken();
452
+ await db.set(`niki:coupon:${token}`, safeStringify({
453
+ token,
454
+ ts: Date.now(),
455
+ ownerUid: uid,
456
+ cost, // ✅ kasada düşülecek miktar
457
+ }));
458
+ await db.expire(`niki:coupon:${token}`, 120); // ✅ 2 dk
459
+
460
+ return res.json({ success: true, token, cost });
461
+ } catch (e) {
462
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
463
+ }
464
+ });
416
465
 
417
466
  router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
418
467
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
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,10 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  $(document).ready(function () {
4
- // --- AYARLAR ---
4
+ // =========================================
5
+ // AYARLAR
6
+ // =========================================
5
7
  const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
6
8
 
7
- // Widget HTML Şablonu
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
+ // =========================================
8
25
  const widgetHtml = `
9
26
  <div id="niki-floating-widget" class="niki-hidden">
10
27
  <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
@@ -17,16 +34,12 @@ $(document).ready(function () {
17
34
  </div>
18
35
  `;
19
36
 
20
- // -------------------------
21
- // UI Helpers
22
- // -------------------------
23
37
  function fixLogo() {
24
38
  const img = document.querySelector("img.niki-widget-logo");
25
39
  if (img && img.src !== NIKI_LOGO_URL) img.src = NIKI_LOGO_URL;
26
40
  }
27
41
 
28
42
  function showNikiToast(msg, kind) {
29
- // kind: 'ok' | 'warn' | 'err' (şimdilik css yoksa da sorun değil)
30
43
  $('.niki-toast').remove();
31
44
  const icon = (kind === 'err') ? 'fa-triangle-exclamation' : (kind === 'warn' ? 'fa-circle-info' : 'fa-paw');
32
45
  const toast = $(`<div class="niki-toast niki-${kind || 'ok'}"><i class="fa ${icon}"></i> ${msg}</div>`);
@@ -44,184 +57,39 @@ $(document).ready(function () {
44
57
  $('#niki-floating-widget').removeClass('niki-hidden');
45
58
  localStorage.setItem('niki_last_points', String(p));
46
59
 
47
- // Wallet sayfasındaysan, oradaki puan UI'ını da güncellemeyi dene (id/class varsa)
48
- // (Şablon değişken olabilir, yoksa dokunmaz)
49
- $('#niki-wallet-points, .niki-wallet-points').text(p);
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`);
50
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
+ }
51
70
 
52
- // -------------------------
53
- // Data Fetch (wallet-data)
54
- // -------------------------
55
- function fetchWalletData(opts) {
56
- opts = opts || {};
57
- return $.get('/api/niki-loyalty/wallet-data')
58
- .done(function (data) {
59
- if (!data) return;
60
- setPointsUI(data.points || 0);
61
-
62
- // İstersen wallet sayfasında bar/milestone UI render etmek için data’yı sakla
63
- window.__NIKI_WALLET_DATA__ = data;
64
-
65
- // Wallet template'in içine "rewards" render eden bir alan koyduysan burada çağır
66
- // (Alan yoksa hiçbir şey olmaz, güvenli)
67
- if (opts.renderRewards) {
68
- renderWalletRewards(data);
69
- }
70
- })
71
- .fail(function () {
72
- // cache yoksa 0 göster
73
- const cached = localStorage.getItem('niki_last_points');
74
- if (cached === null) {
75
- setPointsUI(0);
76
- }
77
- });
78
- }
79
-
80
- // -------------------------
81
- // Wallet Rewards Render + Claim (Seçenek B)
82
- // Bu render ancak sayfanda #niki-rewards gibi bir container varsa çalışır.
83
- // Yoksa dokunmaz.
84
- // -------------------------
85
- function renderWalletRewards(data) {
86
- const $wrap = $('#niki-rewards');
87
- if ($wrap.length === 0) return; // container yoksa geç
88
-
89
- const points = Number(data.points || 0);
90
- const barMax = Number(data.barMax || 250);
91
- const rewards = Array.isArray(data.rewards) ? data.rewards : [];
92
-
93
- // Basit milestone bar (container varsa)
94
- const barPercent = Math.min(100, (points / barMax) * 100);
95
-
96
- let barHtml = `
97
- <div class="niki-mbar">
98
- <div class="niki-mbar-top">
99
- <div class="niki-mbar-title">Ödül Barı</div>
100
- <div class="niki-mbar-val">${points} / ${barMax}</div>
101
- </div>
102
- <div class="niki-mbar-track">
103
- <div class="niki-mbar-fill" style="width:${barPercent}%"></div>
104
- ${rewards.map(r => {
105
- const left = Math.min(100, (Number(r.at) / barMax) * 100);
106
- const unlocked = points >= Number(r.at);
107
- return `<span class="niki-mbar-dot ${unlocked ? 'on' : ''}" style="left:${left}%"
108
- title="${escapeHtml(r.title)} • ${r.at} puan"></span>`;
109
- }).join('')}
110
- </div>
111
- <div class="niki-mbar-hint">Ödülü aldığında puanın düşer.</div>
112
- </div>
113
- `;
114
-
115
- let cardsHtml = rewards.map(r => {
116
- const at = Number(r.at || 0);
117
- const unlocked = points >= at;
118
-
119
- const badge = unlocked ? `<span class="niki-r-badge on">Alınabilir</span>` : `<span class="niki-r-badge">Kilitli</span>`;
120
- const btn = unlocked
121
- ? `<button class="btn btn-sm btn-primary niki-claim-btn" data-reward="${escapeAttr(r.id)}">
122
- Ödülü Al
123
- </button>`
124
- : `<button class="btn btn-sm btn-default niki-claim-btn" disabled>
125
- ${at} puan gerekli
126
- </button>`;
127
-
128
- return `
129
- <div class="niki-r-card ${unlocked ? 'on' : ''}">
130
- <div class="niki-r-left">
131
- <div class="niki-r-title">${escapeHtml(r.title)}</div>
132
- <div class="niki-r-sub">${at} puan</div>
133
- </div>
134
- <div class="niki-r-right">
135
- ${badge}
136
- ${btn}
137
- </div>
138
- </div>
139
- `;
140
- }).join('');
141
-
142
- $wrap.html(barHtml + `<div class="niki-r-list">${cardsHtml}</div>`);
143
-
144
- // Claim handler
145
- $wrap.off('click.nikiClaim').on('click.nikiClaim', '.niki-claim-btn:not([disabled])', function () {
146
- const rewardId = $(this).data('reward');
147
- if (!rewardId) return;
148
-
149
- // Butonu anlık kilitle (double click engeli)
150
- const $btn = $(this);
151
- $btn.prop('disabled', true).text('Hazırlanıyor...');
152
-
153
- claimReward(rewardId)
154
- .always(() => {
155
- // UI refresh, button state render sonrası zaten güncellenecek
156
- });
157
- });
158
- }
159
-
160
- function claimReward(rewardId) {
161
- return $.ajax({
162
- url: '/api/niki-loyalty/claim',
163
- method: 'POST',
164
- data: {
165
- rewardId: rewardId,
166
- _csrf: config.csrf_token,
167
- },
168
- }).done(function (res) {
169
- if (!res || !res.success) {
170
- showNikiToast(res?.message || 'Ödül alınamadı', 'err');
171
- // Wallet UI’ı tazele
172
- fetchWalletData({ renderRewards: true });
173
- return;
174
- }
175
-
176
- // ✅ Puan düşmüş yeni bakiye
177
- if (typeof res.newBalance !== 'undefined') {
178
- setPointsUI(res.newBalance);
179
- }
180
-
181
- // ✅ Token geldi (kasada okutulacak)
182
- // Token'ı localStorage'a kaydedelim (wallet ekranı QR üretirken kullanabilsin)
183
- if (res.token) {
184
- localStorage.setItem('niki_last_coupon_token', res.token);
185
- }
186
-
187
- showNikiToast(res.message || 'Ödül hazır! Kasada okut.', 'ok');
188
-
189
- // Wallet sayfasını tazele ve rewards’ları yeniden render et
190
- fetchWalletData({ renderRewards: true });
191
-
192
- // Eğer wallet template’in içinde "QR göster" alanın varsa, event ile haber ver
193
- $(window).trigger('niki:coupon.ready', [res]);
194
- }).fail(function () {
195
- showNikiToast('Sunucu hatası: ödül alınamadı', 'err');
196
- fetchWalletData({ renderRewards: true });
197
- });
198
- }
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
+ }
199
79
 
200
- // Basit escape helperlar (XSS koruması)
201
- function escapeHtml(str) {
202
- return String(str || '')
203
- .replaceAll('&', '&amp;')
204
- .replaceAll('<', '&lt;')
205
- .replaceAll('>', '&gt;')
206
- .replaceAll('"', '&quot;')
207
- .replaceAll("'", '&#39;');
208
- }
209
- function escapeAttr(str) {
210
- return escapeHtml(str).replaceAll(' ', '');
211
- }
80
+ // polling: puan düşünce successe geç
81
+ initialPointsForQR = parseInt(localStorage.getItem('niki_last_points') || '0', 10) || null;
82
+ startPollingForSuccess();
83
+ });
84
+ });
212
85
 
213
- // -------------------------
214
- // 1) Widget Başlatma ve Veri Yönetimi
215
- // -------------------------
216
86
  function initNikiWidget() {
217
87
  if (!app.user.uid || app.user.uid <= 0) return;
218
88
 
219
- // Widget yoksa ekle
220
89
  if ($('#niki-floating-widget').length === 0) {
221
90
  $('body').append(widgetHtml);
222
91
  }
223
92
 
224
- // Cache: anında göster
225
93
  const cachedPoints = localStorage.getItem('niki_last_points');
226
94
  if (cachedPoints !== null) {
227
95
  $('#niki-live-points').text(cachedPoints);
@@ -230,25 +98,38 @@ $(document).ready(function () {
230
98
 
231
99
  fixLogo();
232
100
 
233
- // Sunucudan taze veri çek
234
- const onWalletPage = (ajaxify.data && ajaxify.data.template === 'niki-wallet');
235
- fetchWalletData({ renderRewards: onWalletPage }).always(function () {
101
+ $.get('/api/niki-loyalty/wallet-data', function (data) {
102
+ const freshPoints = data?.points || 0;
103
+ setPointsUI(freshPoints);
236
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
+ }
237
114
  });
238
115
  }
239
116
 
240
- // Başlat
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
+
241
123
  initNikiWidget();
242
124
 
243
- // Sayfa geçişlerinde tekrar çalıştır
244
125
  $(window).on('action:ajaxify.end', function () {
245
126
  initNikiWidget();
246
127
  setTimeout(fixLogo, 400);
247
128
  });
248
129
 
249
- // -------------------------
250
- // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
251
- // -------------------------
130
+ // =========================================
131
+ // HEARTBEAT
132
+ // =========================================
252
133
  let activeSeconds = 0;
253
134
  let isUserActive = false;
254
135
  let idleTimer;
@@ -261,7 +142,7 @@ $(document).ready(function () {
261
142
  $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
262
143
 
263
144
  setInterval(() => {
264
- if (ajaxify.data && ajaxify.data.template && ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
145
+ if (ajaxify?.data?.template?.topic && document.visibilityState === 'visible' && isUserActive) {
265
146
  activeSeconds++;
266
147
  }
267
148
  if (activeSeconds >= 60) {
@@ -274,19 +155,318 @@ $(document).ready(function () {
274
155
  $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function (res) {
275
156
  if (res && res.earned) {
276
157
  setPointsUI(res.total);
277
-
278
158
  showNikiToast(`+${res.points} Puan Kazandın! ⚡`, 'ok');
279
159
 
280
160
  $('#niki-floating-widget').addClass('niki-bounce');
281
161
  setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
282
162
 
283
- // Wallet sayfasındaysan rewards'ları da tazele
284
- const onWalletPage = (ajaxify.data && ajaxify.data.template === 'niki-wallet');
285
- if (onWalletPage) {
286
- fetchWalletData({ renderRewards: true });
163
+ // wallet açıksa güncelle
164
+ if (isWalletPage()) {
165
+ refreshWallet();
287
166
  }
288
167
  }
289
168
  });
290
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;
263
+ }
264
+
265
+ initialPointsForQR = points;
266
+ openQRModal({
267
+ token: res.token,
268
+ title: title,
269
+ ttlSeconds: QR_TTL_SECONDS,
270
+ });
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
+ }
288
+
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
+ }
313
+ }
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
+ }
291
472
  });
292
-
@@ -1,3 +1,6 @@
1
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
2
+ <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
3
+
1
4
  <div class="niki-wallet-wrapper">
2
5
  <div class="niki-header-bg"></div>
3
6
 
@@ -7,10 +10,9 @@
7
10
  </div>
8
11
 
9
12
  <div class="niki-balance-label">Toplam Bakiye</div>
10
- <!-- client.js anlık güncelleyebilsin diye id ekledim -->
11
13
  <div class="niki-balance-big" id="niki-wallet-points">{points}</div>
12
14
 
13
- <!-- ✅ ÖDÜL BARI + KADEME DOTLARI (60/120/180/250) -->
15
+ <!-- ✅ ÖDÜL BARI -->
14
16
  <div class="niki-reward-stats">
15
17
  <div style="display:flex; justify-content:space-between; font-size:12px; color:#bdbdbd; font-weight:700;">
16
18
  <span>Ödül Barı</span>
@@ -20,7 +22,6 @@
20
22
  <div class="niki-progress-track niki-reward-track" style="position:relative;">
21
23
  <div class="niki-progress-fill" id="niki-reward-fill" style="width: 0%;"></div>
22
24
 
23
- <!-- Milestone dotlar -->
24
25
  <span class="niki-ms-dot" data-at="60" style="left:24%;" title="60 • Ücretsiz Kurabiye"></span>
25
26
  <span class="niki-ms-dot" data-at="120" style="left:48%;" title="120 • %35 İndirimli Kahve"></span>
26
27
  <span class="niki-ms-dot" data-at="180" style="left:72%;" title="180 • %60 İndirimli Kahve"></span>
@@ -28,37 +29,19 @@
28
29
  </div>
29
30
 
30
31
  <div style="font-size:11px; color:#a7a7a7; margin-top:6px;">
31
- Ödülü aldığında <b style="color:#fff;">puan düşer</b>. (Kasa için QR/kupon oluşur.)
32
+ QR oluşturduktan sonra <b style="color:#fff;">kasada okutulunca</b> puanın düşer. (QR 2 dakika geçerli)
32
33
  </div>
33
34
  </div>
34
35
 
35
- <!-- ✅ REWARD KARTLARI (client.js #niki-rewards içine basacak) -->
36
- <div id="niki-rewards" style="margin-top:16px;"></div>
37
-
38
- <!-- ✅ Kupon/QR alanı (claim sonrası token burada gösterilecek) -->
39
- <div id="niki-coupon-area" style="display:none; margin-top:16px;">
40
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
41
- <div style="font-weight:800; color:#fff;">Kasada Okut</div>
42
- <button class="niki-mini-btn" id="niki-coupon-close" type="button">Kapat</button>
43
- </div>
44
-
45
- <div class="niki-coupon-card">
46
- <div style="font-size:12px; color:#cfcfcf; margin-bottom:8px;">
47
- <span id="niki-coupon-title">Ödül Kuponu</span>
48
- </div>
36
+ <!-- ✅ REWARD KARTLARI (client.js buraya basacak: QR OLUŞTUR butonları) -->
37
+ <button id="btn-qr-auto" class="niki-btn">
38
+ <i class="fa fa-qrcode"></i> QR OLUŞTUR
39
+ </button>
49
40
 
50
- <div id="niki-coupon-qr" class="niki-qr-box">
51
- <!-- QR burada üretilecek (client.js event ile doldurabilir) -->
52
- <div style="color:#aaa; font-size:12px;">QR hazırlanıyor...</div>
53
- </div>
54
-
55
- <div style="margin-top:10px; font-size:11px; color:#a7a7a7;">
56
- Kuponun süresi sınırlı olabilir. Kasada okutunca tek kullanımlık olur.
57
- </div>
58
- </div>
59
- </div>
41
+ <div style="font-size:11px; color:#888; margin-top:10px;">
42
+ QR oluşturduktan sonra <b>kasada okutulunca</b> puanın düşer. (QR 2 dakika geçerli)
43
+ </div>
60
44
 
61
- <!-- ✅ Günlük kazanım aynı kaldı -->
62
45
  <div class="niki-daily-stats" style="margin-top:18px;">
63
46
  <div style="display:flex; justify-content:space-between; font-size:12px; color:#888; font-weight:700;">
64
47
  <span>Günlük Kazanım</span>
@@ -74,21 +57,50 @@
74
57
  </div>
75
58
  </div>
76
59
 
77
- <!-- ❌ Eski kahve QR butonunu kaldırmadım ama UX için “Eski Sistem” diye ayırdım -->
78
- <button class="niki-btn-action" id="niki-old-qr-btn" style="margin-top:14px;">
79
- <i class="fa fa-qrcode"></i> (Eski) 250 Puan → QR Oluştur
80
- </button>
81
-
82
60
  <p style="font-size:12px; color:#ccc; margin-top:15px;">
83
61
  Niki The Cat Coffee &copy; Loyalty Program
84
62
  </p>
85
63
  </div>
86
64
  </div>
87
65
 
66
+ <!-- ✅ QR MODAL (2 dk) -->
67
+ <div id="modal-qr" class="qr-overlay">
68
+ <div class="ticket-card">
69
+ <div class="close-circle" onclick="closeQR()"><i class="fa fa-times"></i></div>
70
+
71
+ <div id="view-code">
72
+ <div class="ticket-top" id="ticket-title">KASAYA GÖSTERİNİZ</div>
73
+ <div class="ticket-body">
74
+ <div id="qrcode"></div>
75
+
76
+ <div style="font-size:14px; font-weight:800; color:#1a1a1a; margin-top:6px;" id="ticket-sub">
77
+ QR hazır — kasada okut.
78
+ </div>
79
+
80
+ <div class="timer-wrapper"><div class="timer-bar" id="time-bar"></div></div>
81
+ <div class="timer-text" id="timer-txt">2:00</div>
82
+
83
+ <div style="margin-top:10px; font-size:11px; color:#888;">
84
+ Okutma işlemi tamamlanınca otomatik onay ekranına geçer.
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <div id="view-success" class="success-view">
90
+ <div class="success-icon"><i class="fa fa-check"></i></div>
91
+ <h2 style="font-size:24px; color:#1a1a1a; margin-bottom:10px;">AFİYET OLSUN!</h2>
92
+ <p style="color:#888; font-size:14px; margin-bottom:20px;">Kasa onayı alındı, puanın güncellendi.</p>
93
+ <button class="niki-btn" onclick="closeQR()" style="padding:15px; font-size:14px;">
94
+ TAMAM
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
88
100
  <style>
89
- /* Bu küçük CSS'ler sadece yeni bölümleri düzgün gösterir */
90
- .niki-reward-stats{ margin-top: 14px; }
101
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap');
91
102
 
103
+ .niki-reward-stats{ margin-top: 14px; }
92
104
  .niki-reward-track{ height: 12px; border-radius: 999px; overflow: visible; }
93
105
  #niki-reward-fill{ height: 100%; border-radius: 999px; }
94
106
 
@@ -104,83 +116,10 @@
104
116
  background: rgba(255,255,255,.95);
105
117
  border-color: rgba(255,255,255,.95);
106
118
  }
107
-
108
- /* rewards container'ın içini client.js dolduruyor */
109
- .niki-mbar{ margin-bottom: 12px; }
110
- .niki-mbar-top{ display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
111
- .niki-mbar-title{ font-size:12px; font-weight:800; color:#fff; }
112
- .niki-mbar-val{ font-size:12px; font-weight:800; color:#d7d7d7; }
113
- .niki-mbar-track{ position:relative; height:10px; border-radius:999px; background: rgba(255,255,255,.10); overflow: hidden; }
114
- .niki-mbar-fill{ height:100%; border-radius:999px; background: rgba(255,255,255,.85); width:0%; }
115
- .niki-mbar-dot{
116
- position:absolute; top:50%; transform: translate(-50%,-50%);
117
- width:10px; height:10px; border-radius:999px;
118
- background: rgba(255,255,255,.18);
119
- border: 1px solid rgba(255,255,255,.28);
120
- }
121
- .niki-mbar-dot.on{ background: rgba(255,255,255,.95); border-color: rgba(255,255,255,.95); }
122
- .niki-mbar-hint{ margin-top:6px; font-size:11px; color:#a7a7a7; }
123
-
124
- .niki-r-list{ display:flex; flex-direction:column; gap:10px; }
125
- .niki-r-card{
126
- display:flex; justify-content:space-between; align-items:center;
127
- padding: 12px 12px;
128
- border-radius: 14px;
129
- background: rgba(255,255,255,.06);
130
- border: 1px solid rgba(255,255,255,.10);
131
- }
132
- .niki-r-card.on{
133
- background: rgba(255,255,255,.10);
134
- border-color: rgba(255,255,255,.18);
135
- }
136
- .niki-r-title{ font-weight:800; color:#fff; font-size:13px; }
137
- .niki-r-sub{ color:#bdbdbd; font-size:11px; margin-top:2px; }
138
- .niki-r-right{ display:flex; gap:10px; align-items:center; }
139
- .niki-r-badge{
140
- font-size:11px; font-weight:800;
141
- padding: 6px 10px;
142
- border-radius: 999px;
143
- background: rgba(255,255,255,.08);
144
- color: #cfcfcf;
145
- border: 1px solid rgba(255,255,255,.10);
146
- white-space: nowrap;
147
- }
148
- .niki-r-badge.on{
149
- background: rgba(255,255,255,.18);
150
- color:#fff;
151
- border-color: rgba(255,255,255,.22);
152
- }
153
-
154
- .niki-mini-btn{
155
- background: rgba(255,255,255,.10);
156
- border: 1px solid rgba(255,255,255,.18);
157
- color:#fff;
158
- padding: 6px 10px;
159
- border-radius: 10px;
160
- font-weight:800;
161
- font-size:12px;
162
- cursor:pointer;
163
- }
164
-
165
- .niki-coupon-card{
166
- padding: 14px;
167
- border-radius: 16px;
168
- background: rgba(255,255,255,.06);
169
- border: 1px solid rgba(255,255,255,.10);
170
- }
171
- .niki-qr-box{
172
- width: 100%;
173
- min-height: 220px;
174
- border-radius: 14px;
175
- display:flex; align-items:center; justify-content:center;
176
- background: rgba(0,0,0,.20);
177
- border: 1px dashed rgba(255,255,255,.18);
178
- }
179
119
  </style>
180
120
 
181
121
  <script>
182
- // Bu script sadece wallet sayfasında barı güncellemek için minik bir helper.
183
- // Asıl render/claim zaten güncel client.js içinde.
122
+ // sadece bar/dot görsel güncelleme (client.js wallet-data çekince zaten id'leri update ediyor)
184
123
  (function(){
185
124
  function applyRewardBar(data){
186
125
  try{
@@ -197,7 +136,6 @@
197
136
  const bm = document.getElementById('niki-bar-max');
198
137
  if (bm) bm.textContent = barMax;
199
138
 
200
- // dot highlight (60/120/180/250)
201
139
  document.querySelectorAll('.niki-ms-dot').forEach(dot=>{
202
140
  const at = Number(dot.getAttribute('data-at') || 0);
203
141
  if (points >= at) dot.classList.add('on');
@@ -206,33 +144,27 @@
206
144
  }catch(e){}
207
145
  }
208
146
 
209
- // client.js zaten wallet-data'yı window.__NIKI_WALLET_DATA__ içine koyuyor
147
+ // client.js her refreshte window.__NIKI_WALLET_DATA__ set ediyorsa kullan
210
148
  if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
211
149
 
212
- // ajaxify sonrası tekrar dene
213
150
  $(window).on('action:ajaxify.end', function(){
214
151
  if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
215
152
  });
216
153
 
217
- // claim sonrası event
218
- $(window).on('niki:coupon.ready', function(_, res){
219
- // İstersen burada QR üretip #niki-coupon-qr içine basarsın.
220
- // (QR üretim kütüphanen wallet sayfasında varsa entegre edebiliriz.)
221
- const area = document.getElementById('niki-coupon-area');
222
- if (area) area.style.display = 'block';
223
- const t = document.getElementById('niki-coupon-title');
224
- if (t && res && res.reward && res.reward.title) t.textContent = res.reward.title + ' • Kupon';
225
-
226
- // Şimdilik tokenı text olarak da gösterelim (QR yoksa bile)
227
- const qrBox = document.getElementById('niki-coupon-qr');
228
- if (qrBox && res && res.token){
229
- qrBox.innerHTML = '<div style="text-align:center;color:#fff;font-weight:800;">TOKEN</div>'
230
- + '<div style="margin-top:6px;color:#cfcfcf;font-size:12px;word-break:break-all;">' + String(res.token) + '</div>';
154
+ // client.js QR oluşturunca şu event’i basıyor:
155
+ // $(window).trigger('niki:qr.open', [{ token, title }])
156
+ // Ben client.js’te bunu basmıyorum çünkü modalı direkt client.js açıyor.
157
+ // Ama olur da event’le açmak istersen aşağıyı kullanabilirsin:
158
+ $(window).on('niki:qr.open', function(_, payload){
159
+ if(!payload || !payload.token) return;
160
+ if(typeof window.openQRWithToken === 'function'){
161
+ window.openQRWithToken(payload.token, payload.title || 'Ödül');
231
162
  }
232
163
  });
233
-
234
- $('#niki-coupon-close').on('click', function(){
235
- $('#niki-coupon-area').hide();
236
- });
237
164
  })();
165
+
166
+ // ✅ client.js modalı açıyor ama tpl tarafında closeQR global kalsın
167
+ window.closeQR = window.closeQR || function(){
168
+ $('#modal-qr').fadeOut(200);
169
+ };
238
170
  </script>