nodebb-plugin-niki-loyalty 1.0.18 → 1.0.21

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,8 +11,8 @@ const Plugin = {};
11
11
  // AYARLAR
12
12
  // =========================
13
13
  const SETTINGS = {
14
- pointsPerHeartbeat: 500,
15
- dailyCap: 2500000,
14
+ pointsPerHeartbeat: 5,
15
+ dailyCap: 250,
16
16
  coffeeCost: 250,
17
17
  };
18
18
 
@@ -160,45 +160,64 @@ Plugin.init = async function (params) {
160
160
  });
161
161
 
162
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([]);
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
168
 
169
- const raw = await db.getListRange('niki:kasa:history', 0, -1);
169
+ const raw = await db.getListRange('niki:kasa:history', 0, -1);
170
170
 
171
- const rows = (raw || [])
172
- .map(safeParseMaybeJson)
173
- .filter(Boolean)
174
- .reverse();
175
-
176
- const enriched = await Promise.all(rows.map(async (item) => {
177
- if (!item || !item.cuid) return item;
178
-
179
- try {
180
- // ✅ userslug + username + picture al
181
- const uData = await user.getUserFields(item.cuid, ['username', 'userslug', 'picture']);
182
- const userslug = uData?.userslug;
183
-
184
- return {
185
- ...item,
186
- cust: item.cust || uData?.username || 'Bilinmeyen',
187
- picture: item.picture || uData?.picture || '',
188
- userslug: userslug || item.userslug || '',
189
- profileUrl: makeProfileUrl(userslug || item.userslug),
190
- };
191
- } catch (e) {
192
- // tek item patlasa bile endpoint düşmesin
193
- return item;
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; }
194
178
  }
195
- }));
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
+ });
219
+
196
220
 
197
- return res.json(enriched);
198
- } catch (err) {
199
- return res.status(500).json([]);
200
- }
201
- });
202
221
 
203
222
  // 4) QR OLUŞTUR
204
223
  router.post('/api/niki-loyalty/generate-qr', middleware.ensureLoggedIn, async (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-niki-loyalty",
3
- "version": "1.0.18",
3
+ "version": "1.0.21",
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,84 +1,181 @@
1
1
  'use strict';
2
2
 
3
+ /* globals document, $, window, ajaxify, app, localStorage */
4
+
3
5
  $(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";
6
+
7
+ // --- GENEL AYARLAR ---
8
+ const config = {
9
+ // Widget Görünümü
10
+ logoUrl: "https://i.ibb.co/nZvtpss/logo-placeholder.png",
11
+ defaultLabel: "PUANIM",
12
+
13
+ // Topic Notification Ayarları
14
+ targetCategoryId: 1, // Takip edilecek Kategori ID
15
+ checkInterval: 300000, // 5 Dakika (ms)
16
+ showDuration: 15000, // Konu ekranda kaç sn kalsın?
17
+ maxShowCount: 5, // Bir konu bir kişiye max kaç kez gösterilsin?
18
+ topicValidity: 86400000, // Konu 24 saatten eskiyse gösterme (ms)
19
+
20
+ // API Yolları
21
+ walletRoute: 'niki-wallet',
22
+ csrf_token: config.csrf_token
23
+ };
24
+
25
+ // Global Durum Değişkenleri
26
+ let currentPoints = "..."; // Başlangıçta boş
27
+ let isTopicAnimating = false; // Şu an konu mu gösteriliyor?
7
28
 
8
- // Widget HTML Şablonu
29
+ // --- 1. WIDGET HTML ŞABLONU ---
9
30
  const widgetHtml = `
10
31
  <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">
32
+ <div id="niki-widget-content" class="niki-widget-content">
33
+ <img src="${config.logoUrl}" class="niki-widget-logo" alt="Niki">
13
34
  <div class="niki-widget-text">
14
- <span class="niki-lbl">PUANIM</span>
15
- <span class="niki-val" id="niki-live-points">...</span>
35
+ <span id="niki-lbl" class="niki-lbl">${config.defaultLabel}</span>
36
+ <span id="niki-val" class="niki-val">${currentPoints}</span>
16
37
  </div>
17
38
  </div>
18
39
  </div>
19
40
  `;
20
41
 
21
- // 1. Widget Başlatma ve Veri Yönetimi
42
+ // --- 2. BAŞLATMA FONKSİYONU ---
22
43
  function initNikiWidget() {
44
+ // Kullanıcı giriş yapmamışsa widget'ı gösterme
23
45
  if (!app.user.uid || app.user.uid <= 0) return;
24
46
 
25
- // Widget yoksa ekle
47
+ // HTML Enjeksiyonu
26
48
  if ($('#niki-floating-widget').length === 0) {
27
49
  $('body').append(widgetHtml);
28
50
  }
29
51
 
30
- // --- HIZLI YÜKLEME (CACHE) ---
31
- // Önce hafızadaki son puanı hemen göster (Bekletme yapmaz)
52
+ const $widget = $('#niki-floating-widget');
53
+ const $val = $('#niki-val');
54
+
55
+ // CACHE KONTROLÜ (Hızlı açılış için)
32
56
  const cachedPoints = localStorage.getItem('niki_last_points');
33
57
  if (cachedPoints !== null) {
34
- $('#niki-live-points').text(cachedPoints);
35
- $('#niki-floating-widget').removeClass('niki-hidden');
58
+ currentPoints = cachedPoints;
59
+ $val.text(currentPoints);
60
+ $widget.removeClass('niki-hidden');
36
61
  }
37
62
 
38
- // Logo Kontrolü (Garanti olsun)
39
- fixLogo();
40
-
41
- // --- GÜNCEL VERİ ÇEKME ---
42
- // Arka planda sunucuya sor: "Puan değişti mi?"
63
+ // GÜNCEL VERİ ÇEKME (Sunucudan)
43
64
  $.get('/api/niki-loyalty/wallet-data', function(data) {
44
65
  const freshPoints = data.points || 0;
66
+ currentPoints = freshPoints;
45
67
 
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)
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
54
75
  fixLogo();
55
76
  }).fail(function() {
56
- // Hata olursa ve cache yoksa 0 yaz
77
+ // Hata olursa ve cache yoksa 0 göster
57
78
  if (cachedPoints === null) {
58
- $('#niki-live-points').text('0');
59
- $('#niki-floating-widget').removeClass('niki-hidden');
79
+ currentPoints = "0";
80
+ $val.text("0");
81
+ $widget.removeClass('niki-hidden');
60
82
  }
61
83
  });
84
+
85
+ // Tıklama Olayı (Event Delegation)
86
+ $('body').off('click', '#niki-widget-content').on('click', '#niki-widget-content', function () {
87
+ const link = $(this).data('link') || config.walletRoute;
88
+ ajaxify.go(link);
89
+ });
90
+
91
+ // Topic Notification Kontrolünü Başlat
92
+ startTopicCheck();
93
+ }
94
+
95
+ // --- 3. TOPIC NOTIFICATION SİSTEMİ ---
96
+ function startTopicCheck() {
97
+ // Eski interval varsa temizle
98
+ if (window.nikiTopicInterval) clearInterval(window.nikiTopicInterval);
99
+
100
+ // Fonksiyon: Kontrol Et ve Göster
101
+ const checkAndShow = function() {
102
+ $.get('/api/category/' + config.targetCategoryId, function (data) {
103
+ if (data && data.topics && data.topics.length > 0) {
104
+ const topic = data.topics[0];
105
+ const now = Date.now();
106
+
107
+ // KURAL 1: Süre Kontrolü (24 Saat)
108
+ if (now - topic.timestamp > config.topicValidity) return;
109
+
110
+ // KURAL 2: Gösterim Sayısı Kontrolü (LocalStorage)
111
+ let storageData = JSON.parse(localStorage.getItem('niki_topic_tracking') || '{}');
112
+
113
+ // Konu değişmişse sayacı sıfırla
114
+ if (storageData.tid !== topic.tid) {
115
+ storageData = { tid: topic.tid, count: 0 };
116
+ }
117
+
118
+ // KURAL 3: Limit Dolmadıysa Göster
119
+ if (storageData.count < config.maxShowCount) {
120
+ animateTopicNotification(topic);
121
+
122
+ // Sayacı artır ve kaydet
123
+ storageData.count++;
124
+ localStorage.setItem('niki_topic_tracking', JSON.stringify(storageData));
125
+ }
126
+ }
127
+ });
128
+ };
129
+
130
+ // Sayfa açılır açılmaz bir kez kontrol et
131
+ checkAndShow();
132
+
133
+ // Sonra 5 dakikada bir devam et
134
+ window.nikiTopicInterval = setInterval(checkAndShow, config.checkInterval);
135
+ }
136
+
137
+ function animateTopicNotification(topic) {
138
+ const $val = $('#niki-val');
139
+ const $lbl = $('#niki-lbl');
140
+ const $container = $('#niki-widget-content');
141
+
142
+ if ($val.length > 0) {
143
+ isTopicAnimating = true; // Flag'i kaldır (Puan güncellemesi araya girmesin)
144
+
145
+ $val.fadeOut(200, function () {
146
+ $lbl.text('YENİ KONU:');
147
+ $(this).text(topic.title).fadeIn(200);
148
+
149
+ // Linki ve Stili Değiştir
150
+ $container.data('link', 'topic/' + topic.slug);
151
+ $container.addClass('highlight-topic');
152
+ });
153
+
154
+ // 15 Saniye sonra eski haline dön
155
+ setTimeout(function () {
156
+ $val.fadeOut(200, function () {
157
+ $lbl.text(config.defaultLabel);
158
+ $(this).text(currentPoints).fadeIn(200); // Güncel puanı yaz
159
+
160
+ // Linki ve Stili Sıfırla
161
+ $container.data('link', config.walletRoute);
162
+ $container.removeClass('highlight-topic');
163
+
164
+ isTopicAnimating = false; // Flag'i indir
165
+ });
166
+ }, config.showDuration);
167
+ }
62
168
  }
63
169
 
64
- // Logo Düzeltici (Senin çalışan kodun entegresi)
170
+ // --- 4. YARDIMCI FONKSİYONLAR ---
65
171
  function fixLogo() {
66
172
  const img = document.querySelector("img.niki-widget-logo");
67
- if (img && img.src !== NIKI_LOGO_URL) {
68
- img.src = NIKI_LOGO_URL;
173
+ if (img && img.src !== config.logoUrl) {
174
+ img.src = config.logoUrl;
69
175
  }
70
176
  }
71
177
 
72
- // Başlat
73
- initNikiWidget();
74
-
75
- // Sayfa Geçişlerinde Tekrar Çalıştır
76
- $(window).on('action:ajaxify.end', function () {
77
- initNikiWidget();
78
- setTimeout(fixLogo, 500); // 0.5sn sonra son bir kontrol
79
- });
80
-
81
- // --- AKTİFLİK SİSTEMİ (Heartbeat) ---
178
+ // --- 5. AKTİFLİK SİSTEMİ (HEARTBEAT) ---
82
179
  let activeSeconds = 0;
83
180
  let isUserActive = false;
84
181
  let idleTimer;
@@ -88,29 +185,42 @@ $(document).ready(function () {
88
185
  clearTimeout(idleTimer);
89
186
  idleTimer = setTimeout(() => { isUserActive = false; }, 30000);
90
187
  }
188
+
189
+ // Kullanıcı hareketlerini dinle
91
190
  $(window).on('mousemove scroll keydown click touchstart', resetIdleTimer);
92
191
 
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);
192
+ // Her saniye kontrol et
193
+ if (!window.nikiHeartbeatInterval) {
194
+ window.nikiHeartbeatInterval = setInterval(() => {
195
+ // Sadece bir konunun içindeysek (ajaxify.data.template.topic) ve sekme açıksa say
196
+ if (ajaxify.data.template.topic && document.visibilityState === 'visible' && isUserActive) {
197
+ activeSeconds++;
198
+ }
199
+ // 60 saniye dolduysa sunucuya bildir
200
+ if (activeSeconds >= 60) {
201
+ sendHeartbeat();
202
+ activeSeconds = 0;
203
+ }
204
+ }, 1000);
205
+ }
102
206
 
103
207
  function sendHeartbeat() {
104
208
  $.post('/api/niki-loyalty/heartbeat', { _csrf: config.csrf_token }, function(res) {
105
209
  if (res.earned) {
106
- // Puanı güncelle
107
- $('#niki-live-points').text(res.total);
108
- // Hafızayı da güncelle
210
+ // Global puan değişkenini güncelle
211
+ currentPoints = res.total;
109
212
  localStorage.setItem('niki_last_points', res.total);
110
213
 
214
+ // Eğer o sırada Konu Animasyonu oynamıyorsa, ekrandaki puanı da güncelle
215
+ if (!isTopicAnimating) {
216
+ $('#niki-val').text(res.total);
217
+
218
+ // Ufak bir zıplama efekti
219
+ $('#niki-floating-widget').addClass('niki-bounce');
220
+ setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
221
+ }
222
+
111
223
  showNikiToast(`+${res.points} Puan Kazandın! ☕`);
112
- $('#niki-floating-widget').addClass('niki-bounce');
113
- setTimeout(() => $('#niki-floating-widget').removeClass('niki-bounce'), 500);
114
224
  }
115
225
  });
116
226
  }
@@ -125,4 +235,14 @@ $(document).ready(function () {
125
235
  setTimeout(() => toast.remove(), 3000);
126
236
  }, 3000);
127
237
  }
238
+
239
+ // --- BAŞLATMA ---
240
+ initNikiWidget();
241
+
242
+ // Sayfa geçişlerinde widget'ı koru
243
+ $(window).on('action:ajaxify.end', function () {
244
+ initNikiWidget();
245
+ setTimeout(fixLogo, 500);
246
+ });
247
+
128
248
  });