@orjok/commons 1.0.2 → 1.2.0

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.
@@ -0,0 +1,529 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/types/select-options.ts
2
+ var levelOptions = [
3
+ { value: "SSC", label: "SSC" },
4
+ { value: "HSC+Admission", label: "HSC/Admission" },
5
+ { value: "BCS", label: "BCS" }
6
+ ];
7
+ var languageOptions = [
8
+ { value: "Bangla", label: "Bangla" },
9
+ { value: "English", label: "English" },
10
+ { value: "Any", label: "Any" }
11
+ ];
12
+ var difficultyOptions = [
13
+ { value: "Easy", label: "Easy" },
14
+ { value: "Medium", label: "Medium" },
15
+ { value: "Hard", label: "Hard" }
16
+ ];
17
+ var publicExamOptions = [
18
+ // SSC
19
+ { value: "SSC", label: "SSC", level: "SSC" },
20
+ { value: "College Admission", label: "College Admission", level: "SSC" },
21
+ { value: "School Exam", label: "School Exam (Test/Pre-Test)", level: "SSC" },
22
+ // HSC & Admission
23
+ { value: "HSC", label: "HSC", level: "HSC+Admission" },
24
+ { value: "College Exam", label: "College Exam (Test/Pre-Test)", level: "HSC+Admission" },
25
+ { value: "BUET", label: "BUET", level: "HSC+Admission" },
26
+ { value: "CKRUET", label: "CKRUET (CUET, KUET, RUET)", level: "HSC+Admission" },
27
+ { value: "MIST", label: "MIST", level: "HSC+Admission" },
28
+ { value: "BUTEX", label: "BUTEX", level: "HSC+Admission" },
29
+ { value: "IUT", label: "IUT", level: "HSC+Admission" },
30
+ { value: "MAT", label: "Medical (MAT)", level: "HSC+Admission" },
31
+ { value: "DAT", label: "Dental (DAT)", level: "HSC+Admission" },
32
+ { value: "AFMC", label: "AFMC", level: "HSC+Admission" },
33
+ { value: "DU", label: "Dhaka University (DU)", level: "HSC+Admission" },
34
+ { value: "JU", label: "Jahangirnagar University (JU)", level: "HSC+Admission" },
35
+ { value: "RU", label: "Rajshahi University (RU)", level: "HSC+Admission" },
36
+ { value: "CU", label: "Chittagong University (CU)", level: "HSC+Admission" },
37
+ { value: "GST", label: "GST Cluster (\u0997\u09C1\u099A\u09CD\u099B)", level: "HSC+Admission" },
38
+ { value: "Agri-Cluster", label: "Agri Cluster (\u0995\u09C3\u09B7\u09BF \u0997\u09C1\u099A\u09CD\u099B)", level: "HSC+Admission" },
39
+ { value: "BUP", label: "BUP", level: "HSC+Admission" },
40
+ { value: "IBA", label: "IBA", level: "HSC+Admission" },
41
+ // BCS & Job
42
+ { value: "BCS", label: "BCS", level: "BCS" },
43
+ { value: "Bank Job", label: "Bank Job", level: "BCS" },
44
+ { value: "NTRCA", label: "NTRCA / Primary", level: "BCS" },
45
+ { value: "Govt Job", label: "Govt Job / Non-Cadre", level: "BCS" },
46
+ // Universal
47
+ { value: "Other", label: "Other", level: ["SSC", "HSC+Admission", "BCS"] }
48
+ ];
49
+ function getPublicExamsByLevel(level) {
50
+ if (!level) return publicExamOptions;
51
+ const normalizedLevel = level === "Admission" || level === "HSC" ? "HSC+Admission" : level;
52
+ return publicExamOptions.filter((opt) => {
53
+ if (Array.isArray(opt.level)) {
54
+ return opt.level.includes(normalizedLevel) || opt.level.includes(level);
55
+ }
56
+ return opt.level === normalizedLevel || opt.level === level;
57
+ });
58
+ }
59
+
60
+ // src/logic/media-url.ts
61
+ function isStorageKey(value) {
62
+ return !value.startsWith("http://") && !value.startsWith("https://") && !value.startsWith("/") && !value.startsWith("data:") && !value.startsWith("blob:");
63
+ }
64
+ var MediaUrlCache = class {
65
+ constructor(ttlMs = 50 * 60 * 1e3) {
66
+ this.cache = /* @__PURE__ */ new Map();
67
+ this.ttlMs = ttlMs;
68
+ }
69
+ get(key) {
70
+ const entry = this.cache.get(key);
71
+ if (!entry) return void 0;
72
+ if (Date.now() >= entry.expiresAt) {
73
+ this.cache.delete(key);
74
+ return void 0;
75
+ }
76
+ return entry.url;
77
+ }
78
+ set(key, url) {
79
+ this.cache.set(key, { url, expiresAt: Date.now() + this.ttlMs });
80
+ }
81
+ clear() {
82
+ this.cache.clear();
83
+ }
84
+ };
85
+ var warnedNoStorage = false;
86
+ async function resolveMediaUrl(value, storage, cache) {
87
+ if (!value || value.length === 0 || value === "unchanged") return null;
88
+ if (!isStorageKey(value)) return value;
89
+ if (cache) {
90
+ const cached = cache.get(value);
91
+ if (cached) return cached;
92
+ }
93
+ if (!storage) {
94
+ if (!warnedNoStorage) {
95
+ warnedNoStorage = true;
96
+ console.warn("@orjok/commons: resolveMediaUrl called without a StorageProvider \u2014 returning raw key");
97
+ }
98
+ return value;
99
+ }
100
+ const { url } = await storage.getFileUrl(value);
101
+ _optionalChain([cache, 'optionalAccess', _ => _.set, 'call', _2 => _2(value, url)]);
102
+ return url;
103
+ }
104
+ async function resolveMediaUrls(obj, fields, storage, cache) {
105
+ const copy = { ...obj };
106
+ await Promise.all(
107
+ fields.map(async (field) => {
108
+ const val = copy[field];
109
+ if (typeof val === "string") {
110
+ copy[field] = await resolveMediaUrl(val, storage, cache);
111
+ }
112
+ })
113
+ );
114
+ return copy;
115
+ }
116
+ function _resetMediaUrlWarning() {
117
+ warnedNoStorage = false;
118
+ }
119
+
120
+ // src/logic/practice.ts
121
+ function aggregateSubjectMetrics(progress) {
122
+ let totalPracticed = 0;
123
+ let totalMistakes = 0;
124
+ let totalCorrected = 0;
125
+ for (const p of progress) {
126
+ totalPracticed += _nullishCoalesce(p.practicedCount, () => ( 0));
127
+ totalMistakes += _nullishCoalesce(p.mistakeCount, () => ( 0));
128
+ totalCorrected += _nullishCoalesce(p.correctedCount, () => ( 0));
129
+ }
130
+ const accuracy = totalPracticed > 0 ? Math.round((totalPracticed - totalMistakes) / totalPracticed * 100) : 0;
131
+ return { totalPracticed, totalMistakes, totalCorrected, accuracy };
132
+ }
133
+ function aggregateChapterMetrics(progress) {
134
+ let totalPracticed = 0;
135
+ let totalMistakes = 0;
136
+ let totalCorrected = 0;
137
+ for (const p of progress) {
138
+ totalPracticed += _nullishCoalesce(p.practicedCount, () => ( 0));
139
+ totalMistakes += _nullishCoalesce(p.mistakeCount, () => ( 0));
140
+ totalCorrected += _nullishCoalesce(p.correctedCount, () => ( 0));
141
+ }
142
+ const accuracy = totalPracticed > 0 ? Math.round((totalPracticed - totalMistakes) / totalPracticed * 100) : 0;
143
+ return { totalPracticed, totalMistakes, totalCorrected, accuracy };
144
+ }
145
+ function aggregateTopicMetrics(progress) {
146
+ let totalPracticed = 0;
147
+ let totalMistakes = 0;
148
+ let totalCorrected = 0;
149
+ for (const p of progress) {
150
+ totalPracticed += _nullishCoalesce(p.practicedCount, () => ( 0));
151
+ totalMistakes += _nullishCoalesce(p.mistakeCount, () => ( 0));
152
+ totalCorrected += _nullishCoalesce(p.correctedCount, () => ( 0));
153
+ }
154
+ const accuracy = totalPracticed > 0 ? Math.round((totalPracticed - totalMistakes) / totalPracticed * 100) : 0;
155
+ return { totalPracticed, totalMistakes, totalCorrected, accuracy };
156
+ }
157
+ function calculateAccuracy(practiced, mistakes) {
158
+ if (practiced <= 0) return 0;
159
+ return Math.round((practiced - mistakes) / practiced * 100);
160
+ }
161
+ function isLanguageMatch(qLang, targetLang) {
162
+ if (!targetLang || targetLang === "All") return true;
163
+ if (!qLang) return true;
164
+ const q = qLang.trim().toLowerCase();
165
+ const t = targetLang.trim().toLowerCase();
166
+ if (t === "bangla" || t === "bn" || t === "bengali") {
167
+ return q === "bangla" || q === "bn" || q === "bengali" || q === "all";
168
+ }
169
+ if (t === "english" || t === "en") {
170
+ return q === "english" || q === "en" || q === "all";
171
+ }
172
+ return q === t || q === "all";
173
+ }
174
+ function isDifficultyMatch(qDiff, targetDiff) {
175
+ if (!targetDiff || targetDiff === "All") return true;
176
+ if (!qDiff) return true;
177
+ return qDiff.trim().toLowerCase() === targetDiff.trim().toLowerCase();
178
+ }
179
+ function isValidMCQ(q) {
180
+ if (!q || typeof q !== "object") return false;
181
+ const item = q;
182
+ if (!item.id || item.type === "WRITTEN") return false;
183
+ const rawOpts = item.options;
184
+ const opts = Array.isArray(rawOpts) ? rawOpts : rawOpts && typeof rawOpts === "object" && Array.isArray(rawOpts.items) ? rawOpts.items : [];
185
+ return opts.length >= 2;
186
+ }
187
+
188
+ // src/logic/xp.ts
189
+ var XP_TIERS = [
190
+ {
191
+ id: "novice",
192
+ minLevel: 1,
193
+ maxLevel: 4,
194
+ nameEn: "Novice Learner",
195
+ nameBn: "\u09B6\u09BF\u0995\u09CD\u09B7\u09BE\u09A8\u09AC\u09BF\u09B8",
196
+ color: "#64748B",
197
+ // slate-500
198
+ badgeBg: "bg-slate-100 dark:bg-slate-800/60",
199
+ badgeBorder: "border-slate-300 dark:border-slate-700",
200
+ badgeText: "text-slate-700 dark:text-slate-200"
201
+ },
202
+ {
203
+ id: "dedicated",
204
+ minLevel: 5,
205
+ maxLevel: 9,
206
+ nameEn: "Dedicated Student",
207
+ nameBn: "\u0985\u09A8\u09C1\u09B0\u09BE\u0997\u09C0 \u09B6\u09BF\u0995\u09CD\u09B7\u09BE\u09B0\u09CD\u09A5\u09C0",
208
+ color: "#10B981",
209
+ // emerald-500
210
+ badgeBg: "bg-emerald-50 dark:bg-emerald-950/40",
211
+ badgeBorder: "border-emerald-300 dark:border-emerald-700",
212
+ badgeText: "text-emerald-700 dark:text-emerald-300"
213
+ },
214
+ {
215
+ id: "skilled",
216
+ minLevel: 10,
217
+ maxLevel: 19,
218
+ nameEn: "Skilled Scholar",
219
+ nameBn: "\u09A6\u0995\u09CD\u09B7 \u09B6\u09BF\u0995\u09CD\u09B7\u09BE\u09B0\u09CD\u09A5\u09C0",
220
+ color: "#0284C7",
221
+ // sky-600
222
+ badgeBg: "bg-sky-50 dark:bg-sky-950/40",
223
+ badgeBorder: "border-sky-300 dark:border-sky-700",
224
+ badgeText: "text-sky-700 dark:text-sky-300"
225
+ },
226
+ {
227
+ id: "master",
228
+ minLevel: 20,
229
+ maxLevel: 34,
230
+ nameEn: "Master Practitioner",
231
+ nameBn: "\u099C\u09CD\u099E\u09BE\u09A8\u09C0 \u09B8\u09BE\u09A7\u0995",
232
+ color: "#8B5CF6",
233
+ // purple-500
234
+ badgeBg: "bg-purple-50 dark:bg-purple-950/40",
235
+ badgeBorder: "border-purple-300 dark:border-purple-700",
236
+ badgeText: "text-purple-700 dark:text-purple-300"
237
+ },
238
+ {
239
+ id: "grandmaster",
240
+ minLevel: 35,
241
+ maxLevel: 49,
242
+ nameEn: "Grandmaster",
243
+ nameBn: "\u09AE\u09B9\u09BE\u099C\u09CD\u099E\u09BE\u09A8\u09C0",
244
+ color: "#F59E0B",
245
+ // amber-500
246
+ badgeBg: "bg-amber-50 dark:bg-amber-950/40",
247
+ badgeBorder: "border-amber-300 dark:border-amber-700",
248
+ badgeText: "text-amber-700 dark:text-amber-300"
249
+ },
250
+ {
251
+ id: "legend",
252
+ minLevel: 50,
253
+ maxLevel: Infinity,
254
+ nameEn: "Legendary Genius",
255
+ nameBn: "\u0995\u09BF\u0982\u09AC\u09A6\u09A8\u09CD\u09A4\u09BF",
256
+ color: "#F43F5E",
257
+ // rose-500
258
+ badgeBg: "bg-rose-50 dark:bg-rose-950/40",
259
+ badgeBorder: "border-rose-300 dark:border-rose-700",
260
+ badgeText: "text-rose-700 dark:text-rose-300"
261
+ }
262
+ ];
263
+ var XP_REWARDS = {
264
+ PRACTICE_CORRECT: 10,
265
+ PRACTICE_ATTEMPT: 2,
266
+ PACK_EXAM_BASE: 20,
267
+ PACK_EXAM_CORRECT: 5,
268
+ PACK_EXAM_HIGH_SCORE_BONUS: 25,
269
+ // For scoring >= 80%
270
+ CONTEST_PARTICIPATION: 50,
271
+ CONTEST_TOP_10_BONUS: 100,
272
+ CONTEST_TOP_25_BONUS: 50,
273
+ CREATE_QUESTION: 25,
274
+ CREATE_PACK: 50,
275
+ VOTE: 2,
276
+ AI_EVALUATION: 15
277
+ };
278
+ function getXpForLevel(level) {
279
+ if (!Number.isFinite(level) || level <= 1) return 0;
280
+ const l = Math.floor(level);
281
+ return 25 * (l * l + l - 2);
282
+ }
283
+ function getLevelFromXp(xp) {
284
+ if (!Number.isFinite(xp) || xp <= 0) return 1;
285
+ const clampedXp = Math.max(0, xp);
286
+ const level = Math.floor((Math.sqrt(9 + 0.16 * clampedXp) - 1) / 2);
287
+ return Math.max(1, level);
288
+ }
289
+ function getXpRequiredForNextLevel(level) {
290
+ const currentLevel = Math.max(1, Math.floor(Number.isFinite(level) ? level : 1));
291
+ const currentLevelStartXp = getXpForLevel(currentLevel);
292
+ const nextLevelStartXp = getXpForLevel(currentLevel + 1);
293
+ return nextLevelStartXp - currentLevelStartXp;
294
+ }
295
+ function getXpProgress(xp) {
296
+ const validXp = Math.max(0, Number.isFinite(xp) ? Math.floor(xp) : 0);
297
+ const level = getLevelFromXp(validXp);
298
+ const currentLevelXp = getXpForLevel(level);
299
+ const nextLevelXp = getXpForLevel(level + 1);
300
+ const xpRequiredForLevel = nextLevelXp - currentLevelXp;
301
+ const xpInLevel = validXp - currentLevelXp;
302
+ const xpToNextLevel = Math.max(0, nextLevelXp - validXp);
303
+ const progressPercent = xpRequiredForLevel > 0 ? Math.min(100, Math.max(0, Math.round(xpInLevel / xpRequiredForLevel * 100))) : 100;
304
+ const tier = getXpTier(level);
305
+ return {
306
+ level,
307
+ xp: validXp,
308
+ currentLevelXp,
309
+ nextLevelXp,
310
+ xpInLevel,
311
+ xpRequiredForLevel,
312
+ xpToNextLevel,
313
+ progressPercent,
314
+ tier
315
+ };
316
+ }
317
+ function getXpTier(level) {
318
+ const lvl = Math.max(1, Math.floor(Number.isFinite(level) ? level : 1));
319
+ const found = XP_TIERS.find((t) => lvl >= t.minLevel && lvl <= t.maxLevel);
320
+ return _nullishCoalesce(found, () => ( XP_TIERS[XP_TIERS.length - 1]));
321
+ }
322
+ function getAllXpTiers() {
323
+ return [...XP_TIERS];
324
+ }
325
+ function calculateXpReward(action, metadata) {
326
+ switch (action) {
327
+ case "practice":
328
+ return _optionalChain([metadata, 'optionalAccess', _3 => _3.isCorrect]) ? XP_REWARDS.PRACTICE_CORRECT : XP_REWARDS.PRACTICE_ATTEMPT;
329
+ case "packExam": {
330
+ const correct = _nullishCoalesce(_optionalChain([metadata, 'optionalAccess', _4 => _4.correctCount]), () => ( 0));
331
+ const total = _nullishCoalesce(_optionalChain([metadata, 'optionalAccess', _5 => _5.totalQuestions]), () => ( 0));
332
+ let totalXp = XP_REWARDS.PACK_EXAM_BASE + correct * XP_REWARDS.PACK_EXAM_CORRECT;
333
+ if (total > 0 && correct / total >= 0.8) {
334
+ totalXp += XP_REWARDS.PACK_EXAM_HIGH_SCORE_BONUS;
335
+ }
336
+ return totalXp;
337
+ }
338
+ case "contest": {
339
+ let contestXp = XP_REWARDS.CONTEST_PARTICIPATION;
340
+ const rank = _optionalChain([metadata, 'optionalAccess', _6 => _6.rank]);
341
+ const total = _optionalChain([metadata, 'optionalAccess', _7 => _7.totalParticipants]);
342
+ if (rank && total && total > 0) {
343
+ const topRatio = rank / total;
344
+ if (topRatio <= 0.1) {
345
+ contestXp += XP_REWARDS.CONTEST_TOP_10_BONUS;
346
+ } else if (topRatio <= 0.25) {
347
+ contestXp += XP_REWARDS.CONTEST_TOP_25_BONUS;
348
+ }
349
+ }
350
+ return contestXp;
351
+ }
352
+ case "createQuestion":
353
+ return XP_REWARDS.CREATE_QUESTION;
354
+ case "createPack":
355
+ return XP_REWARDS.CREATE_PACK;
356
+ case "vote":
357
+ return XP_REWARDS.VOTE;
358
+ case "aiEvaluation":
359
+ return XP_REWARDS.AI_EVALUATION;
360
+ default:
361
+ return 0;
362
+ }
363
+ }
364
+
365
+ // src/logic/notification.ts
366
+ function toBanglaNumeral(num) {
367
+ const banglaDigits = ["\u09E6", "\u09E7", "\u09E8", "\u09E9", "\u09EA", "\u09EB", "\u09EC", "\u09ED", "\u09EE", "\u09EF"];
368
+ return String(num).replace(/\d/g, (digit) => banglaDigits[parseInt(digit, 10)] || digit);
369
+ }
370
+ function formatLevelUpNotification({
371
+ newLevel,
372
+ oldLevel = Math.max(1, newLevel - 1),
373
+ userName
374
+ }) {
375
+ const currentTier = getXpTier(newLevel);
376
+ const previousTier = getXpTier(oldLevel);
377
+ const isTierPromotion = currentTier.id !== previousTier.id && newLevel > oldLevel;
378
+ const displayName = userName ? `${userName}, ` : "";
379
+ const bnLevel = toBanglaNumeral(newLevel);
380
+ let titleEn = `Level ${newLevel} Reached`;
381
+ let titleBn = `\u09B2\u09C7\u09AD\u09C7\u09B2 ${bnLevel} \u0986\u09A8\u09B2\u0995 \u09B9\u09AF\u09BC\u09C7\u099B\u09C7`;
382
+ let messageEn = `Congratulations! ${displayName}you reached Level ${newLevel}. Keep practicing to conquer higher milestones!`;
383
+ let messageBn = `\u0985\u09AD\u09BF\u09A8\u09A8\u09CD\u09A6\u09A8! ${displayName}\u0986\u09AA\u09A8\u09BF \u09B2\u09C7\u09AD\u09C7\u09B2 ${bnLevel}-\u098F \u0989\u09A8\u09CD\u09A8\u09C0\u09A4 \u09B9\u09AF\u09BC\u09C7\u099B\u09C7\u09A8\u0964 \u09A8\u09A4\u09C1\u09A8 \u0989\u099A\u09CD\u099A\u09A4\u09BE\u09DF \u09AA\u09CC\u0981\u099B\u09BE\u09A4\u09C7 \u0985\u09A8\u09C1\u09B6\u09C0\u09B2\u09A8 \u099A\u09BE\u09B2\u09BF\u09DF\u09C7 \u09AF\u09BE\u09A8!`;
384
+ if (isTierPromotion) {
385
+ titleEn = `Promoted to ${currentTier.nameEn}`;
386
+ titleBn = `${currentTier.nameBn} \u09AA\u09A6\u09C7 \u0989\u09A8\u09CD\u09A8\u09C0\u09A4`;
387
+ messageEn = `Incredible achievement! ${displayName}you advanced to Level ${newLevel} and earned the title of ${currentTier.nameEn}!`;
388
+ messageBn = `\u0985\u09B8\u09BE\u09A7\u09BE\u09B0\u09A3 \u09B8\u09BE\u09AB\u09B2\u09CD\u09AF! ${displayName}\u0986\u09AA\u09A8\u09BF \u09B2\u09C7\u09AD\u09C7\u09B2 ${bnLevel}-\u098F \u09AA\u09CC\u0981\u099B\u09C7 ${currentTier.nameBn} \u0996\u09C7\u09A4\u09BE\u09AC \u0985\u09B0\u09CD\u099C\u09A8 \u0995\u09B0\u09C7\u099B\u09C7\u09A8!`;
389
+ }
390
+ return {
391
+ type: "LEVEL_UP",
392
+ newLevel,
393
+ oldLevel,
394
+ tierId: currentTier.id,
395
+ tierNameEn: currentTier.nameEn,
396
+ tierNameBn: currentTier.nameBn,
397
+ isTierPromotion,
398
+ titleEn,
399
+ titleBn,
400
+ messageEn,
401
+ messageBn,
402
+ metadata: {
403
+ newLevel,
404
+ oldLevel,
405
+ tierId: currentTier.id,
406
+ isTierPromotion
407
+ }
408
+ };
409
+ }
410
+ function mergeNotificationTimeline({
411
+ announcements = [],
412
+ userNotifications = [],
413
+ lastReadAnnouncementAt
414
+ }) {
415
+ const items = [];
416
+ const safeAnnouncements = announcements || [];
417
+ const safeUserNotifs = userNotifications || [];
418
+ for (const ann of safeAnnouncements) {
419
+ const isUnread = !lastReadAnnouncementAt || (ann.createdAt ? new Date(ann.createdAt) > new Date(lastReadAnnouncementAt) : false);
420
+ items.push({
421
+ id: ann.id,
422
+ itemType: "ANNOUNCEMENT",
423
+ title: ann.title,
424
+ message: ann.message,
425
+ senderType: ann.senderType,
426
+ senderName: ann.senderName,
427
+ senderAvatar: ann.senderAvatar,
428
+ category: ann.category || "ANNOUNCEMENT",
429
+ actionUrl: ann.actionUrl,
430
+ priority: ann.priority,
431
+ isUnread,
432
+ createdAt: ann.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
433
+ rawAnnouncement: ann
434
+ });
435
+ }
436
+ for (const notif of safeUserNotifs) {
437
+ items.push({
438
+ id: notif.id,
439
+ itemType: "USER_NOTIFICATION",
440
+ title: notif.title,
441
+ message: notif.message,
442
+ senderType: notif.senderType || "SYSTEM",
443
+ senderName: notif.senderName,
444
+ senderAvatar: notif.senderAvatar,
445
+ category: notif.type || "SYSTEM",
446
+ actionUrl: notif.actionUrl,
447
+ isUnread: !notif.isRead,
448
+ createdAt: notif.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
449
+ metadata: notif.metadata,
450
+ rawUserNotification: notif
451
+ });
452
+ }
453
+ return items.sort((a, b) => {
454
+ const timeA = new Date(a.createdAt).getTime();
455
+ const timeB = new Date(b.createdAt).getTime();
456
+ return timeB - timeA;
457
+ });
458
+ }
459
+ function calculateUnreadCount({
460
+ announcements = [],
461
+ userNotifications = [],
462
+ lastReadAnnouncementAt
463
+ }) {
464
+ let count = 0;
465
+ for (const notif of userNotifications || []) {
466
+ if (!notif.isRead) count++;
467
+ }
468
+ for (const ann of announcements || []) {
469
+ if (!lastReadAnnouncementAt || ann.createdAt && new Date(ann.createdAt) > new Date(lastReadAnnouncementAt)) {
470
+ count++;
471
+ }
472
+ }
473
+ return count;
474
+ }
475
+
476
+ // src/utils/shuffle.ts
477
+ function shuffle(array) {
478
+ const result = [...array];
479
+ let currentIndex = result.length;
480
+ while (currentIndex !== 0) {
481
+ const randomIndex = Math.floor(Math.random() * currentIndex);
482
+ currentIndex--;
483
+ [result[currentIndex], result[randomIndex]] = [result[randomIndex], result[currentIndex]];
484
+ }
485
+ return result;
486
+ }
487
+ function shuffleInPlace(array) {
488
+ let currentIndex = array.length;
489
+ while (currentIndex !== 0) {
490
+ const randomIndex = Math.floor(Math.random() * currentIndex);
491
+ currentIndex--;
492
+ [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
493
+ }
494
+ }
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+
518
+
519
+
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+
528
+ exports.levelOptions = levelOptions; exports.languageOptions = languageOptions; exports.difficultyOptions = difficultyOptions; exports.publicExamOptions = publicExamOptions; exports.getPublicExamsByLevel = getPublicExamsByLevel; exports.isStorageKey = isStorageKey; exports.MediaUrlCache = MediaUrlCache; exports.resolveMediaUrl = resolveMediaUrl; exports.resolveMediaUrls = resolveMediaUrls; exports._resetMediaUrlWarning = _resetMediaUrlWarning; exports.aggregateSubjectMetrics = aggregateSubjectMetrics; exports.aggregateChapterMetrics = aggregateChapterMetrics; exports.aggregateTopicMetrics = aggregateTopicMetrics; exports.calculateAccuracy = calculateAccuracy; exports.isLanguageMatch = isLanguageMatch; exports.isDifficultyMatch = isDifficultyMatch; exports.isValidMCQ = isValidMCQ; exports.XP_TIERS = XP_TIERS; exports.XP_REWARDS = XP_REWARDS; exports.getXpForLevel = getXpForLevel; exports.getLevelFromXp = getLevelFromXp; exports.getXpRequiredForNextLevel = getXpRequiredForNextLevel; exports.getXpProgress = getXpProgress; exports.getXpTier = getXpTier; exports.getAllXpTiers = getAllXpTiers; exports.calculateXpReward = calculateXpReward; exports.formatLevelUpNotification = formatLevelUpNotification; exports.mergeNotificationTimeline = mergeNotificationTimeline; exports.calculateUnreadCount = calculateUnreadCount; exports.shuffle = shuffle; exports.shuffleInPlace = shuffleInPlace;
529
+ //# sourceMappingURL=chunk-ODXD2WJN.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/orjok/orjok/packages/commons/dist/chunk-ODXD2WJN.cjs","../src/types/select-options.ts","../src/logic/media-url.ts","../src/logic/practice.ts","../src/logic/xp.ts","../src/logic/notification.ts","../src/utils/shuffle.ts"],"names":[],"mappings":"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","file":"/home/runner/work/orjok/orjok/packages/commons/dist/chunk-ODXD2WJN.cjs","sourcesContent":[null,"import type { Difficulty, Language, Level } from \"./enums\";\n\nexport interface SelectOption<T = string> {\n value: T;\n label: string;\n}\n\nexport const levelOptions: SelectOption<Level>[] = [\n { value: \"SSC\", label: \"SSC\" },\n { value: \"HSC+Admission\", label: \"HSC/Admission\" },\n { value: \"BCS\", label: \"BCS\" },\n];\n\nexport const languageOptions: SelectOption<Language>[] = [\n { value: \"Bangla\", label: \"Bangla\" },\n { value: \"English\", label: \"English\" },\n { value: \"Any\", label: \"Any\" },\n];\n\nexport const difficultyOptions: SelectOption<Difficulty>[] = [\n { value: \"Easy\", label: \"Easy\" },\n { value: \"Medium\", label: \"Medium\" },\n { value: \"Hard\", label: \"Hard\" },\n];\n\nexport interface PublicExamOption extends SelectOption<string> {\n level: Level | Level[] | string | string[];\n}\n\nexport const publicExamOptions: PublicExamOption[] = [\n // SSC\n { value: \"SSC\", label: \"SSC\", level: \"SSC\" },\n { value: \"College Admission\", label: \"College Admission\", level: \"SSC\" },\n { value: \"School Exam\", label: \"School Exam (Test/Pre-Test)\", level: \"SSC\" },\n\n // HSC & Admission\n { value: \"HSC\", label: \"HSC\", level: \"HSC+Admission\" },\n { value: \"College Exam\", label: \"College Exam (Test/Pre-Test)\", level: \"HSC+Admission\" },\n { value: \"BUET\", label: \"BUET\", level: \"HSC+Admission\" },\n { value: \"CKRUET\", label: \"CKRUET (CUET, KUET, RUET)\", level: \"HSC+Admission\" },\n { value: \"MIST\", label: \"MIST\", level: \"HSC+Admission\" },\n { value: \"BUTEX\", label: \"BUTEX\", level: \"HSC+Admission\" },\n { value: \"IUT\", label: \"IUT\", level: \"HSC+Admission\" },\n { value: \"MAT\", label: \"Medical (MAT)\", level: \"HSC+Admission\" },\n { value: \"DAT\", label: \"Dental (DAT)\", level: \"HSC+Admission\" },\n { value: \"AFMC\", label: \"AFMC\", level: \"HSC+Admission\" },\n { value: \"DU\", label: \"Dhaka University (DU)\", level: \"HSC+Admission\" },\n { value: \"JU\", label: \"Jahangirnagar University (JU)\", level: \"HSC+Admission\" },\n { value: \"RU\", label: \"Rajshahi University (RU)\", level: \"HSC+Admission\" },\n { value: \"CU\", label: \"Chittagong University (CU)\", level: \"HSC+Admission\" },\n { value: \"GST\", label: \"GST Cluster (গুচ্ছ)\", level: \"HSC+Admission\" },\n { value: \"Agri-Cluster\", label: \"Agri Cluster (কৃষি গুচ্ছ)\", level: \"HSC+Admission\" },\n { value: \"BUP\", label: \"BUP\", level: \"HSC+Admission\" },\n { value: \"IBA\", label: \"IBA\", level: \"HSC+Admission\" },\n\n // BCS & Job\n { value: \"BCS\", label: \"BCS\", level: \"BCS\" },\n { value: \"Bank Job\", label: \"Bank Job\", level: \"BCS\" },\n { value: \"NTRCA\", label: \"NTRCA / Primary\", level: \"BCS\" },\n { value: \"Govt Job\", label: \"Govt Job / Non-Cadre\", level: \"BCS\" },\n\n // Universal\n { value: \"Other\", label: \"Other\", level: [\"SSC\", \"HSC+Admission\", \"BCS\"] },\n];\n\n/**\n * Returns public exams relevant to the given level.\n * Handles normalized level matching (e.g. \"Admission\" maps to \"HSC+Admission\").\n */\nexport function getPublicExamsByLevel(level?: string | null): PublicExamOption[] {\n if (!level) return publicExamOptions;\n const normalizedLevel = level === \"Admission\" || level === \"HSC\" ? \"HSC+Admission\" : level;\n return publicExamOptions.filter((opt) => {\n if (Array.isArray(opt.level)) {\n return opt.level.includes(normalizedLevel as Level) || opt.level.includes(level as Level);\n }\n return opt.level === normalizedLevel || opt.level === level;\n });\n}\n","import type { StorageProvider } from \"../providers/storage\";\n\nexport function isStorageKey(value: string): boolean {\n return (\n !value.startsWith(\"http://\") &&\n !value.startsWith(\"https://\") &&\n !value.startsWith(\"/\") &&\n !value.startsWith(\"data:\") &&\n !value.startsWith(\"blob:\")\n );\n}\n\ninterface CacheEntry {\n url: string;\n expiresAt: number;\n}\n\nexport class MediaUrlCache {\n private cache = new Map<string, CacheEntry>();\n private readonly ttlMs: number;\n\n constructor(ttlMs: number = 50 * 60 * 1000) {\n this.ttlMs = ttlMs;\n }\n\n get(key: string): string | undefined {\n const entry = this.cache.get(key);\n if (!entry) return undefined;\n if (Date.now() >= entry.expiresAt) {\n this.cache.delete(key);\n return undefined;\n }\n return entry.url;\n }\n\n set(key: string, url: string): void {\n this.cache.set(key, { url, expiresAt: Date.now() + this.ttlMs });\n }\n\n clear(): void {\n this.cache.clear();\n }\n}\n\nlet warnedNoStorage = false;\n\nexport async function resolveMediaUrl(\n value: string | null | undefined,\n storage: StorageProvider | undefined,\n cache?: MediaUrlCache,\n): Promise<string | null> {\n if (!value || value.length === 0 || value === \"unchanged\") return null;\n if (!isStorageKey(value)) return value;\n\n if (cache) {\n const cached = cache.get(value);\n if (cached) return cached;\n }\n\n if (!storage) {\n if (!warnedNoStorage) {\n warnedNoStorage = true;\n console.warn(\"@orjok/commons: resolveMediaUrl called without a StorageProvider — returning raw key\");\n }\n return value;\n }\n\n const { url } = await storage.getFileUrl(value);\n cache?.set(value, url);\n return url;\n}\n\nexport async function resolveMediaUrls<T extends Record<string, unknown>>(\n obj: T,\n fields: (keyof T)[],\n storage: StorageProvider | undefined,\n cache?: MediaUrlCache,\n): Promise<T> {\n const copy = { ...obj };\n await Promise.all(\n fields.map(async (field) => {\n const val = copy[field];\n if (typeof val === \"string\") {\n (copy as Record<string, unknown>)[field as string] = await resolveMediaUrl(val, storage, cache);\n }\n }),\n );\n return copy;\n}\n\n// Reset the one-time warning flag (for testing only)\nexport function _resetMediaUrlWarning(): void {\n warnedNoStorage = false;\n}\n","import type { UserSubjectProgress, UserChapterProgress, UserTopicProgress } from \"../types/models\";\n\nexport interface PracticeMetrics {\n totalPracticed: number;\n totalMistakes: number;\n totalCorrected: number;\n accuracy: number;\n}\n\nexport function aggregateSubjectMetrics(progress: UserSubjectProgress[]): PracticeMetrics {\n let totalPracticed = 0;\n let totalMistakes = 0;\n let totalCorrected = 0;\n\n for (const p of progress) {\n totalPracticed += p.practicedCount ?? 0;\n totalMistakes += p.mistakeCount ?? 0;\n totalCorrected += p.correctedCount ?? 0;\n }\n\n const accuracy = totalPracticed > 0\n ? Math.round(((totalPracticed - totalMistakes) / totalPracticed) * 100)\n : 0;\n\n return { totalPracticed, totalMistakes, totalCorrected, accuracy };\n}\n\nexport function aggregateChapterMetrics(progress: UserChapterProgress[]): PracticeMetrics {\n let totalPracticed = 0;\n let totalMistakes = 0;\n let totalCorrected = 0;\n\n for (const p of progress) {\n totalPracticed += p.practicedCount ?? 0;\n totalMistakes += p.mistakeCount ?? 0;\n totalCorrected += p.correctedCount ?? 0;\n }\n\n const accuracy = totalPracticed > 0\n ? Math.round(((totalPracticed - totalMistakes) / totalPracticed) * 100)\n : 0;\n\n return { totalPracticed, totalMistakes, totalCorrected, accuracy };\n}\n\nexport function aggregateTopicMetrics(progress: UserTopicProgress[]): PracticeMetrics {\n let totalPracticed = 0;\n let totalMistakes = 0;\n let totalCorrected = 0;\n\n for (const p of progress) {\n totalPracticed += p.practicedCount ?? 0;\n totalMistakes += p.mistakeCount ?? 0;\n totalCorrected += p.correctedCount ?? 0;\n }\n\n const accuracy = totalPracticed > 0\n ? Math.round(((totalPracticed - totalMistakes) / totalPracticed) * 100)\n : 0;\n\n return { totalPracticed, totalMistakes, totalCorrected, accuracy };\n}\n\nexport function calculateAccuracy(practiced: number, mistakes: number): number {\n if (practiced <= 0) return 0;\n return Math.round(((practiced - mistakes) / practiced) * 100);\n}\n\nexport function isLanguageMatch(qLang?: string | null, targetLang?: string): boolean {\n if (!targetLang || targetLang === \"All\") return true;\n if (!qLang) return true;\n const q = qLang.trim().toLowerCase();\n const t = targetLang.trim().toLowerCase();\n if (t === \"bangla\" || t === \"bn\" || t === \"bengali\") {\n return q === \"bangla\" || q === \"bn\" || q === \"bengali\" || q === \"all\";\n }\n if (t === \"english\" || t === \"en\") {\n return q === \"english\" || q === \"en\" || q === \"all\";\n }\n return q === t || q === \"all\";\n}\n\nexport function isDifficultyMatch(qDiff?: string | null, targetDiff?: string): boolean {\n if (!targetDiff || targetDiff === \"All\") return true;\n if (!qDiff) return true;\n return qDiff.trim().toLowerCase() === targetDiff.trim().toLowerCase();\n}\n\nexport function isValidMCQ(q: unknown): boolean {\n if (!q || typeof q !== \"object\") return false;\n const item = q as Record<string, unknown>;\n if (!item.id || item.type === \"WRITTEN\") return false;\n\n const rawOpts = item.options as unknown;\n const opts = Array.isArray(rawOpts)\n ? rawOpts\n : (rawOpts && typeof rawOpts === \"object\" && Array.isArray((rawOpts as { items?: unknown[] }).items))\n ? (rawOpts as { items: unknown[] }).items\n : [];\n\n return opts.length >= 2;\n}\n\n","/**\n * @file xp.ts\n * @description Experience Points (XP) and Level progression logic for the Orjok platform.\n */\n\nexport interface XpTier {\n id: \"novice\" | \"dedicated\" | \"skilled\" | \"master\" | \"grandmaster\" | \"legend\";\n minLevel: number;\n maxLevel: number;\n nameEn: string;\n nameBn: string;\n color: string;\n badgeBg: string;\n badgeBorder: string;\n badgeText: string;\n}\n\nexport const XP_TIERS: XpTier[] = [\n {\n id: \"novice\",\n minLevel: 1,\n maxLevel: 4,\n nameEn: \"Novice Learner\",\n nameBn: \"শিক্ষানবিস\",\n color: \"#64748B\", // slate-500\n badgeBg: \"bg-slate-100 dark:bg-slate-800/60\",\n badgeBorder: \"border-slate-300 dark:border-slate-700\",\n badgeText: \"text-slate-700 dark:text-slate-200\",\n },\n {\n id: \"dedicated\",\n minLevel: 5,\n maxLevel: 9,\n nameEn: \"Dedicated Student\",\n nameBn: \"অনুরাগী শিক্ষার্থী\",\n color: \"#10B981\", // emerald-500\n badgeBg: \"bg-emerald-50 dark:bg-emerald-950/40\",\n badgeBorder: \"border-emerald-300 dark:border-emerald-700\",\n badgeText: \"text-emerald-700 dark:text-emerald-300\",\n },\n {\n id: \"skilled\",\n minLevel: 10,\n maxLevel: 19,\n nameEn: \"Skilled Scholar\",\n nameBn: \"দক্ষ শিক্ষার্থী\",\n color: \"#0284C7\", // sky-600\n badgeBg: \"bg-sky-50 dark:bg-sky-950/40\",\n badgeBorder: \"border-sky-300 dark:border-sky-700\",\n badgeText: \"text-sky-700 dark:text-sky-300\",\n },\n {\n id: \"master\",\n minLevel: 20,\n maxLevel: 34,\n nameEn: \"Master Practitioner\",\n nameBn: \"জ্ঞানী সাধক\",\n color: \"#8B5CF6\", // purple-500\n badgeBg: \"bg-purple-50 dark:bg-purple-950/40\",\n badgeBorder: \"border-purple-300 dark:border-purple-700\",\n badgeText: \"text-purple-700 dark:text-purple-300\",\n },\n {\n id: \"grandmaster\",\n minLevel: 35,\n maxLevel: 49,\n nameEn: \"Grandmaster\",\n nameBn: \"মহাজ্ঞানী\",\n color: \"#F59E0B\", // amber-500\n badgeBg: \"bg-amber-50 dark:bg-amber-950/40\",\n badgeBorder: \"border-amber-300 dark:border-amber-700\",\n badgeText: \"text-amber-700 dark:text-amber-300\",\n },\n {\n id: \"legend\",\n minLevel: 50,\n maxLevel: Infinity,\n nameEn: \"Legendary Genius\",\n nameBn: \"কিংবদন্তি\",\n color: \"#F43F5E\", // rose-500\n badgeBg: \"bg-rose-50 dark:bg-rose-950/40\",\n badgeBorder: \"border-rose-300 dark:border-rose-700\",\n badgeText: \"text-rose-700 dark:text-rose-300\",\n },\n];\n\nexport const XP_REWARDS = {\n PRACTICE_CORRECT: 10,\n PRACTICE_ATTEMPT: 2,\n PACK_EXAM_BASE: 20,\n PACK_EXAM_CORRECT: 5,\n PACK_EXAM_HIGH_SCORE_BONUS: 25, // For scoring >= 80%\n CONTEST_PARTICIPATION: 50,\n CONTEST_TOP_10_BONUS: 100,\n CONTEST_TOP_25_BONUS: 50,\n CREATE_QUESTION: 25,\n CREATE_PACK: 50,\n VOTE: 2,\n AI_EVALUATION: 15,\n} as const;\n\nexport type XpRewardAction =\n | \"practice\"\n | \"packExam\"\n | \"contest\"\n | \"createQuestion\"\n | \"createPack\"\n | \"vote\"\n | \"aiEvaluation\";\n\nexport interface XpRewardMetadata {\n isCorrect?: boolean;\n correctCount?: number;\n totalQuestions?: number;\n rank?: number;\n totalParticipants?: number;\n}\n\nexport interface XpProgressInfo {\n level: number;\n xp: number;\n currentLevelXp: number;\n nextLevelXp: number;\n xpInLevel: number;\n xpRequiredForLevel: number;\n xpToNextLevel: number;\n progressPercent: number;\n tier: XpTier;\n}\n\n/**\n * Calculates the total cumulative XP required to reach a specific level.\n * Formula: 25 * (L^2 + L - 2) = 25 * (L - 1) * (L + 2)\n * Level 1 = 0 XP\n * Level 2 = 100 XP\n * Level 3 = 250 XP\n * Level 4 = 450 XP\n * Level 5 = 700 XP\n * Level 6 = 1000 XP\n * Level 10 = 2700 XP\n * Level 20 = 10450 XP\n * Level 50 = 63700 XP\n */\nexport function getXpForLevel(level: number): number {\n if (!Number.isFinite(level) || level <= 1) return 0;\n const l = Math.floor(level);\n return 25 * (l * l + l - 2);\n}\n\n/**\n * Calculates the user's level based on cumulative XP.\n * Derived by inverting: 25 * L^2 + 25 * L - (50 + XP) = 0\n * L = floor((sqrt(9 + 0.16 * XP) - 1) / 2)\n */\nexport function getLevelFromXp(xp: number): number {\n if (!Number.isFinite(xp) || xp <= 0) return 1;\n const clampedXp = Math.max(0, xp);\n const level = Math.floor((Math.sqrt(9 + 0.16 * clampedXp) - 1) / 2);\n return Math.max(1, level);\n}\n\n/**\n * Returns the XP required to advance from `level` to `level + 1`.\n */\nexport function getXpRequiredForNextLevel(level: number): number {\n const currentLevel = Math.max(1, Math.floor(Number.isFinite(level) ? level : 1));\n const currentLevelStartXp = getXpForLevel(currentLevel);\n const nextLevelStartXp = getXpForLevel(currentLevel + 1);\n return nextLevelStartXp - currentLevelStartXp;\n}\n\n/**\n * Returns complete progress details for a given cumulative XP value.\n */\nexport function getXpProgress(xp: number): XpProgressInfo {\n const validXp = Math.max(0, Number.isFinite(xp) ? Math.floor(xp) : 0);\n const level = getLevelFromXp(validXp);\n const currentLevelXp = getXpForLevel(level);\n const nextLevelXp = getXpForLevel(level + 1);\n const xpRequiredForLevel = nextLevelXp - currentLevelXp;\n const xpInLevel = validXp - currentLevelXp;\n const xpToNextLevel = Math.max(0, nextLevelXp - validXp);\n\n const progressPercent = xpRequiredForLevel > 0\n ? Math.min(100, Math.max(0, Math.round((xpInLevel / xpRequiredForLevel) * 100)))\n : 100;\n\n const tier = getXpTier(level);\n\n return {\n level,\n xp: validXp,\n currentLevelXp,\n nextLevelXp,\n xpInLevel,\n xpRequiredForLevel,\n xpToNextLevel,\n progressPercent,\n tier,\n };\n}\n\n/**\n * Returns the tier metadata for a given level.\n */\nexport function getXpTier(level: number): XpTier {\n const lvl = Math.max(1, Math.floor(Number.isFinite(level) ? level : 1));\n const found = XP_TIERS.find((t) => lvl >= t.minLevel && lvl <= t.maxLevel);\n return found ?? XP_TIERS[XP_TIERS.length - 1]!;\n}\n\n/**\n * Returns all configured XP tiers.\n */\nexport function getAllXpTiers(): XpTier[] {\n return [...XP_TIERS];\n}\n\n/**\n * Calculates the XP reward for a given user activity.\n */\nexport function calculateXpReward(action: XpRewardAction, metadata?: XpRewardMetadata): number {\n switch (action) {\n case \"practice\":\n return metadata?.isCorrect ? XP_REWARDS.PRACTICE_CORRECT : XP_REWARDS.PRACTICE_ATTEMPT;\n\n case \"packExam\": {\n const correct = metadata?.correctCount ?? 0;\n const total = metadata?.totalQuestions ?? 0;\n let totalXp = XP_REWARDS.PACK_EXAM_BASE + correct * XP_REWARDS.PACK_EXAM_CORRECT;\n if (total > 0 && correct / total >= 0.8) {\n totalXp += XP_REWARDS.PACK_EXAM_HIGH_SCORE_BONUS;\n }\n return totalXp;\n }\n\n case \"contest\": {\n let contestXp: number = XP_REWARDS.CONTEST_PARTICIPATION;\n const rank = metadata?.rank;\n const total = metadata?.totalParticipants;\n if (rank && total && total > 0) {\n const topRatio = rank / total;\n if (topRatio <= 0.1) {\n contestXp += XP_REWARDS.CONTEST_TOP_10_BONUS;\n } else if (topRatio <= 0.25) {\n contestXp += XP_REWARDS.CONTEST_TOP_25_BONUS;\n }\n }\n return contestXp;\n }\n\n case \"createQuestion\":\n return XP_REWARDS.CREATE_QUESTION;\n\n case \"createPack\":\n return XP_REWARDS.CREATE_PACK;\n\n case \"vote\":\n return XP_REWARDS.VOTE;\n\n case \"aiEvaluation\":\n return XP_REWARDS.AI_EVALUATION;\n\n default:\n return 0;\n }\n}\n","/**\n * @file notification.ts\n * @description Logic for notification formatting, level up messaging, and feed management.\n */\n\nimport { getXpTier } from \"./xp\";\nimport type { Announcement, UserNotification } from \"../types/models\";\n\nexport interface LevelUpNotificationResult {\n type: \"LEVEL_UP\";\n newLevel: number;\n oldLevel: number;\n tierId: string;\n tierNameEn: string;\n tierNameBn: string;\n isTierPromotion: boolean;\n titleEn: string;\n titleBn: string;\n messageEn: string;\n messageBn: string;\n metadata: {\n newLevel: number;\n oldLevel: number;\n tierId: string;\n isTierPromotion: boolean;\n };\n}\n\nexport interface FormatLevelUpParams {\n newLevel: number;\n oldLevel?: number;\n userName?: string;\n}\n\nfunction toBanglaNumeral(num: number | string): string {\n const banglaDigits = [\"০\", \"১\", \"২\", \"৩\", \"৪\", \"৫\", \"৬\", \"৭\", \"৮\", \"৯\"];\n return String(num).replace(/\\d/g, (digit) => banglaDigits[parseInt(digit, 10)] || digit);\n}\n\n/**\n * Format level-up notifications with bilingual support and tier promotion detection.\n */\nexport function formatLevelUpNotification({\n newLevel,\n oldLevel = Math.max(1, newLevel - 1),\n userName,\n}: FormatLevelUpParams): LevelUpNotificationResult {\n const currentTier = getXpTier(newLevel);\n const previousTier = getXpTier(oldLevel);\n const isTierPromotion = currentTier.id !== previousTier.id && newLevel > oldLevel;\n\n const displayName = userName ? `${userName}, ` : \"\";\n const bnLevel = toBanglaNumeral(newLevel);\n\n let titleEn = `Level ${newLevel} Reached`;\n let titleBn = `লেভেল ${bnLevel} আনলক হয়েছে`;\n let messageEn = `Congratulations! ${displayName}you reached Level ${newLevel}. Keep practicing to conquer higher milestones!`;\n let messageBn = `অভিনন্দন! ${displayName}আপনি লেভেল ${bnLevel}-এ উন্নীত হয়েছেন। নতুন উচ্চতায় পৌঁছাতে অনুশীলন চালিয়ে যান!`;\n\n if (isTierPromotion) {\n titleEn = `Promoted to ${currentTier.nameEn}`;\n titleBn = `${currentTier.nameBn} পদে উন্নীত`;\n messageEn = `Incredible achievement! ${displayName}you advanced to Level ${newLevel} and earned the title of ${currentTier.nameEn}!`;\n messageBn = `অসাধারণ সাফল্য! ${displayName}আপনি লেভেল ${bnLevel}-এ পৌঁছে ${currentTier.nameBn} খেতাব অর্জন করেছেন!`;\n }\n\n return {\n type: \"LEVEL_UP\",\n newLevel,\n oldLevel,\n tierId: currentTier.id,\n tierNameEn: currentTier.nameEn,\n tierNameBn: currentTier.nameBn,\n isTierPromotion,\n titleEn,\n titleBn,\n messageEn,\n messageBn,\n metadata: {\n newLevel,\n oldLevel,\n tierId: currentTier.id,\n isTierPromotion,\n },\n };\n}\n\nexport interface NotificationTimelineItem {\n id: string;\n itemType: \"ANNOUNCEMENT\" | \"USER_NOTIFICATION\";\n title: string;\n message: string;\n senderType: string;\n senderName?: string | null;\n senderAvatar?: string | null;\n category?: string | null;\n actionUrl?: string | null;\n priority?: string | null;\n isUnread: boolean;\n createdAt: string;\n metadata?: string | null;\n rawAnnouncement?: Announcement;\n rawUserNotification?: UserNotification;\n}\n\nexport interface MergeTimelineParams {\n announcements?: Announcement[] | null;\n userNotifications?: UserNotification[] | null;\n lastReadAnnouncementAt?: string | null;\n}\n\n/**\n * Merges announcements and user-specific notifications into a single chronological stream.\n */\nexport function mergeNotificationTimeline({\n announcements = [],\n userNotifications = [],\n lastReadAnnouncementAt,\n}: MergeTimelineParams): NotificationTimelineItem[] {\n const items: NotificationTimelineItem[] = [];\n\n const safeAnnouncements = announcements || [];\n const safeUserNotifs = userNotifications || [];\n\n for (const ann of safeAnnouncements) {\n const isUnread =\n !lastReadAnnouncementAt ||\n (ann.createdAt ? new Date(ann.createdAt) > new Date(lastReadAnnouncementAt) : false);\n\n items.push({\n id: ann.id,\n itemType: \"ANNOUNCEMENT\",\n title: ann.title,\n message: ann.message,\n senderType: ann.senderType,\n senderName: ann.senderName,\n senderAvatar: ann.senderAvatar,\n category: ann.category || \"ANNOUNCEMENT\",\n actionUrl: ann.actionUrl,\n priority: ann.priority,\n isUnread,\n createdAt: ann.createdAt || new Date().toISOString(),\n rawAnnouncement: ann,\n });\n }\n\n for (const notif of safeUserNotifs) {\n items.push({\n id: notif.id,\n itemType: \"USER_NOTIFICATION\",\n title: notif.title,\n message: notif.message,\n senderType: notif.senderType || \"SYSTEM\",\n senderName: notif.senderName,\n senderAvatar: notif.senderAvatar,\n category: notif.type || \"SYSTEM\",\n actionUrl: notif.actionUrl,\n isUnread: !notif.isRead,\n createdAt: notif.createdAt || new Date().toISOString(),\n metadata: notif.metadata,\n rawUserNotification: notif,\n });\n }\n\n // Sort descending by createdAt\n return items.sort((a, b) => {\n const timeA = new Date(a.createdAt).getTime();\n const timeB = new Date(b.createdAt).getTime();\n return timeB - timeA;\n });\n}\n\n/**\n * Calculate total unread count for badge display.\n */\nexport function calculateUnreadCount({\n announcements = [],\n userNotifications = [],\n lastReadAnnouncementAt,\n}: MergeTimelineParams): number {\n let count = 0;\n\n for (const notif of userNotifications || []) {\n if (!notif.isRead) count++;\n }\n\n for (const ann of announcements || []) {\n if (\n !lastReadAnnouncementAt ||\n (ann.createdAt && new Date(ann.createdAt) > new Date(lastReadAnnouncementAt))\n ) {\n count++;\n }\n }\n\n return count;\n}\n","export function shuffle<T>(array: T[]): T[] {\n const result = [...array];\n let currentIndex = result.length;\n while (currentIndex !== 0) {\n const randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n [result[currentIndex], result[randomIndex]] = [result[randomIndex]!, result[currentIndex]!];\n }\n return result;\n}\n\nexport function shuffleInPlace<T>(array: T[]): void {\n let currentIndex = array.length;\n while (currentIndex !== 0) {\n const randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n [array[currentIndex], array[randomIndex]] = [array[randomIndex]!, array[currentIndex]!];\n }\n}\n"]}