nodebb-plugin-niki-loyalty 1.0.20 → 1.0.22

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
@@ -11,9 +11,25 @@ const Plugin = {};
11
11
  // AYARLAR
12
12
  // =========================
13
13
  const SETTINGS = {
14
- pointsPerHeartbeat: 500,
15
- dailyCap: 2500000,
14
+ pointsPerHeartbeat: 5,
15
+ dailyCap: 250,
16
+
17
+ // Tek bar hedefi
18
+ barMax: 250,
19
+
20
+ // ✅ 4 kademe (Seçenek B: claim = puan düşer)
21
+ rewards: [
22
+ { id: 'cookie', at: 60, title: 'Ücretsiz Kurabiye', type: 'free_item', meta: { item: 'cookie' } },
23
+ { id: 'c35', at: 120, title: '%35 İndirimli Kahve', type: 'discount', meta: { product: 'coffee', percent: 35 } },
24
+ { id: 'c60', at: 180, title: '%60 İndirimli Kahve', type: 'discount', meta: { product: 'coffee', percent: 60 } },
25
+ { id: 'coffee', at: 250, title: 'Ücretsiz Kahve', type: 'free_item', meta: { item: 'coffee' } },
26
+ ],
27
+
28
+ // Eski akış uyumluluğu (istersen sonra kaldırırız)
16
29
  coffeeCost: 250,
30
+
31
+ // Kupon token süreleri (sn)
32
+ couponTTLSeconds: 10 * 60, // 10 dk
17
33
  };
18
34
 
19
35
  // ✅ TEST: sınırsız kullanım (puan kontrolünü kapatmak için true)
@@ -24,15 +40,12 @@ const TEST_MODE_UNLIMITED = false;
24
40
  // =========================
25
41
  function safeParseMaybeJson(x) {
26
42
  if (x == null) return null;
27
-
28
- // bazı DB’lerde object dönebilir
29
43
  if (typeof x === 'object') return x;
30
44
 
31
45
  if (typeof x === 'string') {
32
46
  try {
33
47
  return JSON.parse(x);
34
48
  } catch (e) {
35
- // "[object Object]" gibi bozuk kayıtları atla
36
49
  return null;
37
50
  }
38
51
  }
@@ -53,15 +66,36 @@ function makeProfileUrl(userslug) {
53
66
  return `${rp}/user/${userslug}`;
54
67
  }
55
68
 
69
+ function makeToken() {
70
+ return Math.random().toString(36).substring(2) + Date.now().toString(36);
71
+ }
72
+
73
+ function getTodayKey() {
74
+ return new Date().toISOString().slice(0, 10).replace(/-/g, '');
75
+ }
76
+
77
+ function findRewardById(id) {
78
+ return SETTINGS.rewards.find(r => r.id === id) || null;
79
+ }
80
+
81
+ async function isStaff(uid) {
82
+ const [isAdmin, isMod] = await Promise.all([
83
+ user.isAdministrator(uid),
84
+ user.isGlobalModerator(uid),
85
+ ]);
86
+ return isAdmin || isMod;
87
+ }
88
+
56
89
  // =========================
57
90
  // LOG FONKSİYONLARI
58
91
  // =========================
59
- async function addUserLog(uid, type, amount, desc) {
92
+ async function addUserLog(uid, type, amount, desc, extra) {
60
93
  const logEntry = {
61
94
  ts: Date.now(),
62
95
  type, // 'earn' | 'spend'
63
96
  amt: amount,
64
97
  txt: desc,
98
+ ...(extra ? { extra } : {}),
65
99
  };
66
100
 
67
101
  const payload = safeStringify(logEntry);
@@ -71,13 +105,15 @@ async function addUserLog(uid, type, amount, desc) {
71
105
  await db.listTrim(`niki:activity:${uid}`, -50, -1);
72
106
  }
73
107
 
74
- async function addKasaLog(staffUid, customerName, customerUid) {
108
+ async function addKasaLog(staffUid, customerName, customerUid, amount, rewardId, rewardTitle) {
75
109
  const logEntry = {
76
110
  ts: Date.now(),
77
111
  staff: staffUid,
78
- cust: customerName, // bazen eski kayıtlarda boş olabilir, endpoint tamamlayacak
112
+ cust: customerName,
79
113
  cuid: customerUid,
80
- amt: SETTINGS.coffeeCost,
114
+ amt: amount,
115
+ rewardId: rewardId || '',
116
+ rewardTitle: rewardTitle || '',
81
117
  };
82
118
 
83
119
  const payload = safeStringify(logEntry);
@@ -94,34 +130,41 @@ Plugin.init = async function (params) {
94
130
  const router = params.router;
95
131
  const middleware = params.middleware;
96
132
 
133
+ // -------------------------
97
134
  // 1) HEARTBEAT (puan kazanma)
135
+ // -------------------------
98
136
  router.post('/api/niki-loyalty/heartbeat', middleware.ensureLoggedIn, async (req, res) => {
99
137
  try {
100
138
  const uid = req.uid;
101
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
139
+ const today = getTodayKey();
102
140
  const dailyKey = `niki:daily:${uid}:${today}`;
103
141
 
104
142
  const currentDailyScore = parseInt((await db.getObjectField(dailyKey, 'score')) || 0, 10);
105
143
 
106
- if (currentDailyScore >= SETTINGS.dailyCap) {
144
+ if (!TEST_MODE_UNLIMITED && currentDailyScore >= SETTINGS.dailyCap) {
107
145
  return res.json({ earned: false, reason: 'daily_cap' });
108
146
  }
109
147
 
110
148
  await user.incrementUserFieldBy(uid, 'niki_points', SETTINGS.pointsPerHeartbeat);
111
149
  await db.incrObjectFieldBy(dailyKey, 'score', SETTINGS.pointsPerHeartbeat);
112
150
 
113
- const newBalance = await user.getUserField(uid, 'niki_points');
151
+ const newBalance = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
152
+
153
+ await addUserLog(uid, 'earn', SETTINGS.pointsPerHeartbeat, 'Aktiflik Puanı ⚡');
154
+
114
155
  return res.json({ earned: true, points: SETTINGS.pointsPerHeartbeat, total: newBalance });
115
156
  } catch (err) {
116
157
  return res.status(500).json({ earned: false, reason: 'server_error' });
117
158
  }
118
159
  });
119
160
 
120
- // 2) WALLET DATA (cüzdan + geçmiş)
161
+ // -------------------------
162
+ // 2) WALLET DATA (cüzdan + geçmiş + rewards)
163
+ // -------------------------
121
164
  router.get('/api/niki-loyalty/wallet-data', middleware.ensureLoggedIn, async (req, res) => {
122
165
  try {
123
166
  const uid = req.uid;
124
- const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
167
+ const today = getTodayKey();
125
168
 
126
169
  const [userData, dailyData, historyRaw] = await Promise.all([
127
170
  user.getUserFields(uid, ['niki_points']),
@@ -135,17 +178,33 @@ Plugin.init = async function (params) {
135
178
  let dailyPercent = (dailyScore / SETTINGS.dailyCap) * 100;
136
179
  if (dailyPercent > 100) dailyPercent = 100;
137
180
 
138
- // history parse güvenli
181
+ const barPercent = Math.min(100, (currentPoints / SETTINGS.barMax) * 100);
182
+
139
183
  const history = (historyRaw || [])
140
184
  .map(safeParseMaybeJson)
141
185
  .filter(Boolean)
142
186
  .reverse();
143
187
 
188
+ const rewards = SETTINGS.rewards.map(r => ({
189
+ id: r.id,
190
+ at: r.at,
191
+ title: r.title,
192
+ type: r.type,
193
+ meta: r.meta,
194
+ unlocked: currentPoints >= r.at,
195
+ }));
196
+
144
197
  return res.json({
145
198
  points: currentPoints,
199
+
146
200
  dailyScore,
147
201
  dailyCap: SETTINGS.dailyCap,
148
202
  dailyPercent,
203
+
204
+ barMax: SETTINGS.barMax,
205
+ barPercent,
206
+
207
+ rewards,
149
208
  history,
150
209
  });
151
210
  } catch (err) {
@@ -154,72 +213,187 @@ Plugin.init = async function (params) {
154
213
  dailyScore: 0,
155
214
  dailyCap: SETTINGS.dailyCap,
156
215
  dailyPercent: 0,
216
+ barMax: SETTINGS.barMax,
217
+ barPercent: 0,
218
+ rewards: SETTINGS.rewards.map(r => ({ id: r.id, at: r.at, title: r.title, type: r.type, meta: r.meta, unlocked: false })),
157
219
  history: [],
158
220
  });
159
221
  }
160
222
  });
161
223
 
162
- // 3) KASA HISTORY (admin/mod)
163
- router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
164
- try {
165
- const isAdmin = await user.isAdministrator(req.uid);
166
- const isMod = await user.isGlobalModerator(req.uid);
167
- if (!isAdmin && !isMod) return res.status(403).json([]);
168
-
169
- const raw = await db.getListRange('niki:kasa:history', 0, -1);
170
-
171
- // Kayıtlar bazen JSON string, bazen bozuk olabilir → güvenli parse
172
- const rows = (raw || [])
173
- .map((x) => {
174
- if (!x) return null;
175
- if (typeof x === 'object') return x;
176
- if (typeof x === 'string') {
177
- try { return JSON.parse(x); } catch (e) { return null; }
178
- }
179
- return null;
180
- })
181
- .filter(Boolean)
182
- .reverse();
183
-
184
- // cuid’lerden uid listesi çıkar
185
- const uids = rows
186
- .map(r => parseInt(r.cuid, 10))
187
- .filter(n => Number.isFinite(n) && n > 0);
188
-
189
- // NodeBB core user datası (profile-looks mantığı)
190
- const users = await user.getUsersFields(uids, [
191
- 'uid', 'username', 'userslug', 'picture', 'icon:bgColor',
192
- ]);
193
-
194
- const userMap = {};
195
- (users || []).forEach(u => { userMap[u.uid] = u; });
196
-
197
- const rp = nconf.get('relative_path') || '';
198
-
199
- const enriched = rows.map(r => {
200
- const uid = parseInt(r.cuid, 10);
201
- const u = userMap[uid];
202
- if (!u) return r;
203
-
204
- return {
205
- ...r,
206
- cust: u.username || r.cust || 'Bilinmeyen',
207
- userslug: u.userslug || r.userslug || '',
208
- picture: u.picture || r.picture || '',
209
- iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
210
- profileUrl: (u.userslug ? `${rp}/user/${u.userslug}` : ''),
211
- };
212
- });
213
-
214
- return res.json(enriched);
215
- } catch (err) {
216
- return res.status(500).json([]);
217
- }
218
- });
224
+ // -------------------------
225
+ // 3) CLAIM REWARD (Seçenek B: claim = puan düşer + kupon token üretir)
226
+ // Client bu endpoint'i çağıracak.
227
+ // -------------------------
228
+ 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);
219
233
 
234
+ if (!reward) {
235
+ return res.json({ success: false, message: 'Geçersiz ödül' });
236
+ }
220
237
 
238
+ const points = parseInt((await user.getUserField(uid, 'niki_points')) || 0, 10);
221
239
 
222
- // 4) QR OLUŞTUR
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 });
271
+
272
+ return res.json({
273
+ success: true,
274
+ token,
275
+ 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.',
278
+ });
279
+ } catch (err) {
280
+ return res.status(500).json({ success: false, message: 'Sunucu hatası' });
281
+ }
282
+ });
283
+
284
+ // -------------------------
285
+ // 4) KASA HISTORY (admin/mod)
286
+ // -------------------------
287
+ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (req, res) => {
288
+ try {
289
+ const staffOk = await isStaff(req.uid);
290
+ if (!staffOk) return res.status(403).json([]);
291
+
292
+ const raw = await db.getListRange('niki:kasa:history', 0, -1);
293
+
294
+ const rows = (raw || [])
295
+ .map((x) => {
296
+ if (!x) return null;
297
+ if (typeof x === 'object') return x;
298
+ if (typeof x === 'string') {
299
+ try { return JSON.parse(x); } catch (e) { return null; }
300
+ }
301
+ return null;
302
+ })
303
+ .filter(Boolean)
304
+ .reverse();
305
+
306
+ const uids = rows
307
+ .map(r => parseInt(r.cuid, 10))
308
+ .filter(n => Number.isFinite(n) && n > 0);
309
+
310
+ const users = await user.getUsersFields(uids, [
311
+ 'uid', 'username', 'userslug', 'picture', 'icon:bgColor',
312
+ ]);
313
+
314
+ const userMap = {};
315
+ (users || []).forEach(u => { userMap[u.uid] = u; });
316
+
317
+ const rp = nconf.get('relative_path') || '';
318
+
319
+ const enriched = rows.map(r => {
320
+ const uid = parseInt(r.cuid, 10);
321
+ const u = userMap[uid];
322
+ if (!u) return r;
323
+
324
+ return {
325
+ ...r,
326
+ cust: u.username || r.cust || 'Bilinmeyen',
327
+ userslug: u.userslug || r.userslug || '',
328
+ picture: u.picture || r.picture || '',
329
+ iconBg: u['icon:bgColor'] || r.iconBg || '#4b5563',
330
+ profileUrl: (u.userslug ? `${rp}/user/${u.userslug}` : ''),
331
+ };
332
+ });
333
+
334
+ return res.json(enriched);
335
+ } catch (err) {
336
+ return res.status(500).json([]);
337
+ }
338
+ });
339
+
340
+ // -------------------------
341
+ // 5) KASA: COUPON SCAN (admin/mod) ✅ yeni akış
342
+ // Claim ile üretilen token kasada okutulur.
343
+ // -------------------------
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' });
350
+
351
+ const raw = await db.get(`niki:coupon:${token}`);
352
+ if (!raw) return res.json({ success: false, message: 'Geçersiz / Süresi dolmuş kupon' });
353
+
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
+ }
359
+
360
+ // ✅ tek kullanımlık
361
+ await db.delete(`niki:coupon:${token}`);
362
+
363
+ const customerUid = parseInt(coupon.ownerUid, 10);
364
+ const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
365
+
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
+ );
375
+
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ı' });
390
+ }
391
+ });
392
+
393
+ // -------------------------
394
+ // 6) (ESKİ) QR OLUŞTUR / OKUT — opsiyonel uyumluluk
395
+ // İstersen tamamen kaldırırız. Şimdilik 250 "Ücretsiz Kahve" gibi düşünebilirsin.
396
+ // -------------------------
223
397
  router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
224
398
  try {
225
399
  const uid = req.uid;
@@ -229,7 +403,7 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
229
403
  return res.json({ success: false, message: 'Yetersiz Puan' });
230
404
  }
231
405
 
232
- const token = Math.random().toString(36).substring(2) + Date.now().toString(36);
406
+ const token = makeToken();
233
407
 
234
408
  await db.set(`niki:qr:${token}`, uid);
235
409
  await db.expire(`niki:qr:${token}`, 120);
@@ -240,14 +414,12 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
240
414
  }
241
415
  });
242
416
 
243
- // 5) QR OKUT (admin/mod)
244
417
  router.post('/api/niki-loyalty/scan-qr', middleware.ensureLoggedIn, async (req, res) => {
245
418
  try {
246
419
  const token = (req.body && req.body.token) ? String(req.body.token) : '';
247
420
 
248
- const isAdmin = await user.isAdministrator(req.uid);
249
- const isMod = await user.isGlobalModerator(req.uid);
250
- if (!isAdmin && !isMod) return res.status(403).json({ success: false, message: 'Yetkisiz' });
421
+ const staffOk = await isStaff(req.uid);
422
+ if (!staffOk) return res.status(403).json({ success: false, message: 'Yetkisiz' });
251
423
 
252
424
  const customerUid = await db.get(`niki:qr:${token}`);
253
425
  if (!customerUid) return res.json({ success: false, message: 'Geçersiz Kod' });
@@ -257,22 +429,16 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
257
429
  return res.json({ success: false, message: 'Yetersiz Bakiye' });
258
430
  }
259
431
 
260
- // ✅ puan düşür
261
432
  if (!TEST_MODE_UNLIMITED) {
262
433
  await user.decrementUserFieldBy(customerUid, 'niki_points', SETTINGS.coffeeCost);
263
434
  }
264
435
 
265
- // token tek kullanımlık
266
436
  await db.delete(`niki:qr:${token}`);
267
437
 
268
- // müşteri bilgisi
269
438
  const customerData = await user.getUserFields(customerUid, ['username', 'picture', 'userslug']);
270
439
 
271
- // user log
272
440
  await addUserLog(customerUid, 'spend', SETTINGS.coffeeCost, 'Kahve Keyfi ☕');
273
-
274
- // kasa log
275
- await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid);
441
+ await addKasaLog(req.uid, customerData?.username || 'Bilinmeyen', customerUid, SETTINGS.coffeeCost, 'coffee', 'Ücretsiz Kahve (QR)');
276
442
 
277
443
  return res.json({
278
444
  success: true,
@@ -284,15 +450,25 @@ router.get('/api/niki-loyalty/kasa-history', middleware.ensureLoggedIn, async (r
284
450
  }
285
451
  });
286
452
 
287
- // 6) SAYFA ROTASI (kasa sayfası)
453
+ // -------------------------
454
+ // 7) SAYFA ROTASI (kasa sayfası)
455
+ // -------------------------
288
456
  routeHelpers.setupPageRoute(router, '/niki-kasa', middleware, [], async (req, res) => {
289
- const isAdmin = await user.isAdministrator(req.uid);
290
- const isMod = await user.isGlobalModerator(req.uid);
291
- if (!isAdmin && !isMod) return res.render('403', {});
457
+ const staffOk = await isStaff(req.uid);
458
+ if (!staffOk) return res.render('403', {});
292
459
  return res.render('niki-kasa', { title: 'Niki Kasa' });
293
460
  });
294
461
  };
295
462
 
463
+ // -------------------------
464
+ // ✅ plugin.json'da action:topic.get -> checkTopicVisit vardı.
465
+ // Method yoksa NodeBB şikayet eder. Şimdilik NO-OP koydum.
466
+ // Sonra istersen gerçekten "topic view" ile puan ekleriz.
467
+ // -------------------------
468
+ Plugin.checkTopicVisit = async function () {
469
+ return;
470
+ };
471
+
296
472
  // client.js inject
297
473
  Plugin.addScripts = async function (scripts) {
298
474
  scripts.push('plugins/nodebb-plugin-niki-loyalty/static/lib/client.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
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,128 +1,292 @@
1
1
  'use strict';
2
2
 
3
3
  $(document).ready(function () {
4
- // --- AYARLAR ---
5
- // 1. Logo Ayarı (Senin çalışan linkin)
6
- const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
7
-
8
- // Widget HTML Şablonu
9
- const widgetHtml = `
10
- <div id="niki-floating-widget" class="niki-hidden">
11
- <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
12
- <img src="${NIKI_LOGO_URL}" class="niki-widget-logo" alt="Niki">
13
- <div class="niki-widget-text">
14
- <span class="niki-lbl">PUANIM</span>
15
- <span class="niki-val" id="niki-live-points">...</span>
16
- </div>
17
- </div>
4
+ // --- AYARLAR ---
5
+ const NIKI_LOGO_URL = "https://i.ibb.co/nZvtpss/logo-placeholder.png";
6
+
7
+ // Widget HTML Şablonu
8
+ const widgetHtml = `
9
+ <div id="niki-floating-widget" class="niki-hidden">
10
+ <div class="niki-widget-content" onclick="ajaxify.go('niki-wallet')">
11
+ <img src="${NIKI_LOGO_URL}" class="niki-widget-logo" alt="Niki">
12
+ <div class="niki-widget-text">
13
+ <span class="niki-lbl">PUANIM</span>
14
+ <span class="niki-val" id="niki-live-points">...</span>
18
15
  </div>
19
- `;
16
+ </div>
17
+ </div>
18
+ `;
20
19
 
21
- // 1. Widget Başlatma ve Veri Yönetimi
22
- function initNikiWidget() {
23
- if (!app.user.uid || app.user.uid <= 0) return;
20
+ // -------------------------
21
+ // UI Helpers
22
+ // -------------------------
23
+ function fixLogo() {
24
+ const img = document.querySelector("img.niki-widget-logo");
25
+ if (img && img.src !== NIKI_LOGO_URL) img.src = NIKI_LOGO_URL;
26
+ }
24
27
 
25
- // Widget yoksa ekle
26
- if ($('#niki-floating-widget').length === 0) {
27
- $('body').append(widgetHtml);
28
- }
28
+ function showNikiToast(msg, kind) {
29
+ // kind: 'ok' | 'warn' | 'err' (şimdilik css yoksa da sorun değil)
30
+ $('.niki-toast').remove();
31
+ const icon = (kind === 'err') ? 'fa-triangle-exclamation' : (kind === 'warn' ? 'fa-circle-info' : 'fa-paw');
32
+ const toast = $(`<div class="niki-toast niki-${kind || 'ok'}"><i class="fa ${icon}"></i> ${msg}</div>`);
33
+ $('body').append(toast);
34
+ setTimeout(() => toast.addClass('show'), 50);
35
+ setTimeout(() => {
36
+ toast.removeClass('show');
37
+ setTimeout(() => toast.remove(), 350);
38
+ }, 3000);
39
+ }
40
+
41
+ function setPointsUI(points) {
42
+ const p = Number.isFinite(+points) ? +points : 0;
43
+ $('#niki-live-points').text(p);
44
+ $('#niki-floating-widget').removeClass('niki-hidden');
45
+ localStorage.setItem('niki_last_points', String(p));
46
+
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);
50
+ }
51
+
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;
29
64
 
30
- // --- HIZLI YÜKLEME (CACHE) ---
31
- // Önce hafızadaki son puanı hemen göster (Bekletme yapmaz)
32
- const cachedPoints = localStorage.getItem('niki_last_points');
33
- if (cachedPoints !== null) {
34
- $('#niki-live-points').text(cachedPoints);
35
- $('#niki-floating-widget').removeClass('niki-hidden');
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);
36
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 : [];
37
92
 
38
- // Logo Kontrolü (Garanti olsun)
39
- fixLogo();
40
-
41
- // --- GÜNCEL VERİ ÇEKME ---
42
- // Arka planda sunucuya sor: "Puan değişti mi?"
43
- $.get('/api/niki-loyalty/wallet-data', function(data) {
44
- const freshPoints = data.points || 0;
45
-
46
- // Puanı güncelle
47
- $('#niki-live-points').text(freshPoints);
48
- $('#niki-floating-widget').removeClass('niki-hidden'); // İlk kez açılıyorsa göster
49
-
50
- // Yeni puanı hafızaya at (Bir sonraki giriş için)
51
- localStorage.setItem('niki_last_points', freshPoints);
52
-
53
- // Logoyu tekrar kontrol et (Resim geç yüklendiyse)
54
- fixLogo();
55
- }).fail(function() {
56
- // Hata olursa ve cache yoksa 0 yaz
57
- if (cachedPoints === null) {
58
- $('#niki-live-points').text('0');
59
- $('#niki-floating-widget').removeClass('niki-hidden');
60
- }
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
61
156
  });
62
- }
157
+ });
158
+ }
63
159
 
64
- // Logo Düzeltici (Senin çalışan kodun entegresi)
65
- function fixLogo() {
66
- const img = document.querySelector("img.niki-widget-logo");
67
- if (img && img.src !== NIKI_LOGO_URL) {
68
- img.src = NIKI_LOGO_URL;
69
- }
70
- }
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
+ }
71
175
 
72
- // Başlat
73
- initNikiWidget();
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');
74
188
 
75
- // Sayfa Geçişlerinde Tekrar Çalıştır
76
- $(window).on('action:ajaxify.end', function () {
77
- initNikiWidget();
78
- setTimeout(fixLogo, 500); // 0.5sn sonra son bir kontrol
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 });
79
197
  });
198
+ }
199
+
200
+ // Basit escape helper’lar (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
+ }
212
+
213
+ // -------------------------
214
+ // 1) Widget Başlatma ve Veri Yönetimi
215
+ // -------------------------
216
+ function initNikiWidget() {
217
+ if (!app.user.uid || app.user.uid <= 0) return;
80
218
 
81
- // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
82
- let activeSeconds = 0;
83
- let isUserActive = false;
84
- let idleTimer;
85
-
86
- function resetIdleTimer() {
87
- isUserActive = true;
88
- clearTimeout(idleTimer);
89
- idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
219
+ // Widget yoksa ekle
220
+ if ($('#niki-floating-widget').length === 0) {
221
+ $('body').append(widgetHtml);
90
222
  }
91
- $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
92
223
 
93
- setInterval(() => {
94
- if (ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
95
- activeSeconds++;
96
- }
97
- if (activeSeconds >= 60) {
98
- sendHeartbeat();
99
- activeSeconds = 0;
100
- }
101
- }, 1000);
102
-
103
- function sendHeartbeat() {
104
- $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function(res) {
105
- if (res.earned) {
106
- // Puanı güncelle
107
- $('#niki-live-points').text(res.total);
108
- // Hafızayı da güncelle
109
- localStorage.setItem('niki_last_points', res.total);
110
-
111
- showNikiToast(`+${res.points} Puan Kazandın! ☕`);
112
- $('#niki-floating-widget').addClass('niki-bounce');
113
- setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
114
- }
115
- });
224
+ // Cache: anında göster
225
+ const cachedPoints = localStorage.getItem('niki_last_points');
226
+ if (cachedPoints !== null) {
227
+ $('#niki-live-points').text(cachedPoints);
228
+ $('#niki-floating-widget').removeClass('niki-hidden');
116
229
  }
117
230
 
118
- function showNikiToast(msg) {
119
- $('.niki-toast').remove();
120
- const toast = $(`<div class="niki-toast"><i class="fa fa-paw"></i> ${msg}</div>`);
121
- $('body').append(toast);
122
- setTimeout(() => { toast.addClass('show'); }, 100);
123
- setTimeout(() => {
124
- toast.removeClass('show');
125
- setTimeout(() => toast.remove(), 3000);
126
- }, 3000);
231
+ fixLogo();
232
+
233
+ // Sunucudan taze veri çek
234
+ const onWalletPage = (ajaxify.data && ajaxify.data.template === 'niki-wallet');
235
+ fetchWalletData({ renderRewards: onWalletPage }).always(function () {
236
+ fixLogo();
237
+ });
238
+ }
239
+
240
+ // Başlat
241
+ initNikiWidget();
242
+
243
+ // Sayfa geçişlerinde tekrar çalıştır
244
+ $(window).on('action:ajaxify.end', function () {
245
+ initNikiWidget();
246
+ setTimeout(fixLogo, 400);
247
+ });
248
+
249
+ // -------------------------
250
+ // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
251
+ // -------------------------
252
+ let activeSeconds = 0;
253
+ let isUserActive = false;
254
+ let idleTimer;
255
+
256
+ function resetIdleTimer() {
257
+ isUserActive = true;
258
+ clearTimeout(idleTimer);
259
+ idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
260
+ }
261
+ $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
262
+
263
+ setInterval(() => {
264
+ if (ajaxify.data && ajaxify.data.template && ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
265
+ activeSeconds++;
266
+ }
267
+ if (activeSeconds >= 60) {
268
+ sendHeartbeat();
269
+ activeSeconds = 0;
127
270
  }
128
- });
271
+ }, 1000);
272
+
273
+ function sendHeartbeat() {
274
+ $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function (res) {
275
+ if (res && res.earned) {
276
+ setPointsUI(res.total);
277
+
278
+ showNikiToast(`+${res.points} Puan Kazandın! ⚡`, 'ok');
279
+
280
+ $('#niki-floating-widget').addClass('niki-bounce');
281
+ setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
282
+
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 });
287
+ }
288
+ }
289
+ });
290
+ }
291
+ });
292
+
@@ -1,35 +1,238 @@
1
1
  <div class="niki-wallet-wrapper">
2
- <div class="niki-header-bg"></div>
3
-
4
- <div class="niki-wallet-content">
5
- <div class="niki-wallet-avatar">
6
- <img src="https://i.imgur.com/kXUe4M6.png" alt="Niki">
2
+ <div class="niki-header-bg"></div>
3
+
4
+ <div class="niki-wallet-content">
5
+ <div class="niki-wallet-avatar">
6
+ <img src="https://i.imgur.com/kXUe4M6.png" alt="Niki">
7
+ </div>
8
+
9
+ <div class="niki-balance-label">Toplam Bakiye</div>
10
+ <!-- client.js anlık güncelleyebilsin diye id ekledim -->
11
+ <div class="niki-balance-big" id="niki-wallet-points">{points}</div>
12
+
13
+ <!-- ✅ ÖDÜL BARI + KADEME DOTLARI (60/120/180/250) -->
14
+ <div class="niki-reward-stats">
15
+ <div style="display:flex; justify-content:space-between; font-size:12px; color:#bdbdbd; font-weight:700;">
16
+ <span>Ödül Barı</span>
17
+ <span><span id="niki-bar-points">{points}</span> / <span id="niki-bar-max">250</span></span>
18
+ </div>
19
+
20
+ <div class="niki-progress-track niki-reward-track" style="position:relative;">
21
+ <div class="niki-progress-fill" id="niki-reward-fill" style="width: 0%;"></div>
22
+
23
+ <!-- Milestone dotlar -->
24
+ <span class="niki-ms-dot" data-at="60" style="left:24%;" title="60 • Ücretsiz Kurabiye"></span>
25
+ <span class="niki-ms-dot" data-at="120" style="left:48%;" title="120 • %35 İndirimli Kahve"></span>
26
+ <span class="niki-ms-dot" data-at="180" style="left:72%;" title="180 • %60 İndirimli Kahve"></span>
27
+ <span class="niki-ms-dot" data-at="250" style="left:100%;" title="250 • Ücretsiz Kahve"></span>
28
+ </div>
29
+
30
+ <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
+ </div>
33
+ </div>
34
+
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>
7
48
  </div>
8
49
 
9
- <div class="niki-balance-label">Toplam Bakiye</div>
10
- <div class="niki-balance-big">{points}</div>
11
-
12
- <div class="niki-daily-stats">
13
- <div style="display:flex; justify-content:space-between; font-size:12px; color:#888; font-weight:600;">
14
- <span>Günlük Kazanım</span>
15
- <span>{dailyScore} / {dailyCap}</span>
16
- </div>
17
-
18
- <div class="niki-progress-track">
19
- <div class="niki-progress-fill" style="width: {dailyPercent}%;"></div>
20
- </div>
21
-
22
- <div style="font-size:11px; color:#aaa;">
23
- Bugün daha fazla çalışarak limitini doldurabilirsin!
24
- </div>
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>
25
53
  </div>
26
54
 
27
- <button class="niki-btn-action">
28
- <i class="fa fa-qrcode"></i> KAHVE AL (QR OLUŞTUR)
29
- </button>
30
-
31
- <p style="font-size:12px; color:#ccc; margin-top:15px;">
32
- Niki The Cat Coffee &copy; Loyalty Program
33
- </p>
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>
34
59
  </div>
35
- </div>
60
+
61
+ <!-- ✅ Günlük kazanım aynı kaldı -->
62
+ <div class="niki-daily-stats" style="margin-top:18px;">
63
+ <div style="display:flex; justify-content:space-between; font-size:12px; color:#888; font-weight:700;">
64
+ <span>Günlük Kazanım</span>
65
+ <span>{dailyScore} / {dailyCap}</span>
66
+ </div>
67
+
68
+ <div class="niki-progress-track">
69
+ <div class="niki-progress-fill" style="width: {dailyPercent}%;"></div>
70
+ </div>
71
+
72
+ <div style="font-size:11px; color:#aaa;">
73
+ Bugün daha fazla çalışarak limitini doldurabilirsin!
74
+ </div>
75
+ </div>
76
+
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
+ <p style="font-size:12px; color:#ccc; margin-top:15px;">
83
+ Niki The Cat Coffee &copy; Loyalty Program
84
+ </p>
85
+ </div>
86
+ </div>
87
+
88
+ <style>
89
+ /* Bu küçük CSS'ler sadece yeni bölümleri düzgün gösterir */
90
+ .niki-reward-stats{ margin-top: 14px; }
91
+
92
+ .niki-reward-track{ height: 12px; border-radius: 999px; overflow: visible; }
93
+ #niki-reward-fill{ height: 100%; border-radius: 999px; }
94
+
95
+ .niki-ms-dot{
96
+ position:absolute; top:50%;
97
+ transform: translate(-50%, -50%);
98
+ width: 12px; height: 12px; border-radius: 999px;
99
+ background: rgba(255,255,255,.18);
100
+ border: 1px solid rgba(255,255,255,.28);
101
+ box-shadow: 0 6px 14px rgba(0,0,0,.25);
102
+ }
103
+ .niki-ms-dot.on{
104
+ background: rgba(255,255,255,.95);
105
+ border-color: rgba(255,255,255,.95);
106
+ }
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
+ </style>
180
+
181
+ <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.
184
+ (function(){
185
+ function applyRewardBar(data){
186
+ try{
187
+ const points = Number(data.points || 0);
188
+ const barMax = Number(data.barMax || 250);
189
+ const pct = Math.min(100, (points / barMax) * 100);
190
+
191
+ const fill = document.getElementById('niki-reward-fill');
192
+ if (fill) fill.style.width = pct + '%';
193
+
194
+ const bp = document.getElementById('niki-bar-points');
195
+ if (bp) bp.textContent = points;
196
+
197
+ const bm = document.getElementById('niki-bar-max');
198
+ if (bm) bm.textContent = barMax;
199
+
200
+ // dot highlight (60/120/180/250)
201
+ document.querySelectorAll('.niki-ms-dot').forEach(dot=>{
202
+ const at = Number(dot.getAttribute('data-at') || 0);
203
+ if (points >= at) dot.classList.add('on');
204
+ else dot.classList.remove('on');
205
+ });
206
+ }catch(e){}
207
+ }
208
+
209
+ // client.js zaten wallet-data'yı window.__NIKI_WALLET_DATA__ içine koyuyor
210
+ if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
211
+
212
+ // ajaxify sonrası tekrar dene
213
+ $(window).on('action:ajaxify.end', function(){
214
+ if (window.__NIKI_WALLET_DATA__) applyRewardBar(window.__NIKI_WALLET_DATA__);
215
+ });
216
+
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>';
231
+ }
232
+ });
233
+
234
+ $('#niki-coupon-close').on('click', function(){
235
+ $('#niki-coupon-area').hide();
236
+ });
237
+ })();
238
+ </script>