repo-wrapped 0.0.2

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.
Files changed (51) hide show
  1. package/README.md +94 -0
  2. package/dist/cli.js +24 -0
  3. package/dist/commands/generate.js +95 -0
  4. package/dist/commands/index.js +24 -0
  5. package/dist/constants/chronotypes.js +23 -0
  6. package/dist/constants/colors.js +18 -0
  7. package/dist/constants/index.js +18 -0
  8. package/dist/formatters/index.js +17 -0
  9. package/dist/formatters/timeFormatter.js +29 -0
  10. package/dist/generators/html/scripts/export.js +125 -0
  11. package/dist/generators/html/scripts/knowledge.js +120 -0
  12. package/dist/generators/html/scripts/modal.js +68 -0
  13. package/dist/generators/html/scripts/navigation.js +156 -0
  14. package/dist/generators/html/scripts/tabs.js +18 -0
  15. package/dist/generators/html/scripts/tooltip.js +21 -0
  16. package/dist/generators/html/styles/achievements.css +387 -0
  17. package/dist/generators/html/styles/base.css +818 -0
  18. package/dist/generators/html/styles/components.css +1391 -0
  19. package/dist/generators/html/styles/knowledge.css +221 -0
  20. package/dist/generators/html/templates/achievementsSection.js +156 -0
  21. package/dist/generators/html/templates/commitQualitySection.js +89 -0
  22. package/dist/generators/html/templates/contributionGraph.js +73 -0
  23. package/dist/generators/html/templates/impactSection.js +117 -0
  24. package/dist/generators/html/templates/knowledgeSection.js +226 -0
  25. package/dist/generators/html/templates/streakSection.js +42 -0
  26. package/dist/generators/html/templates/timePatternsSection.js +110 -0
  27. package/dist/generators/html/utils/colorUtils.js +21 -0
  28. package/dist/generators/html/utils/commitMapBuilder.js +24 -0
  29. package/dist/generators/html/utils/dateRangeCalculator.js +57 -0
  30. package/dist/generators/html/utils/developerStatsCalculator.js +29 -0
  31. package/dist/generators/html/utils/scriptLoader.js +16 -0
  32. package/dist/generators/html/utils/styleLoader.js +18 -0
  33. package/dist/generators/html/utils/weekGrouper.js +28 -0
  34. package/dist/index.js +77 -0
  35. package/dist/types/index.js +2 -0
  36. package/dist/utils/achievementDefinitions.js +433 -0
  37. package/dist/utils/achievementEngine.js +170 -0
  38. package/dist/utils/commitQualityAnalyzer.js +368 -0
  39. package/dist/utils/fileHotspotAnalyzer.js +270 -0
  40. package/dist/utils/gitParser.js +125 -0
  41. package/dist/utils/htmlGenerator.js +449 -0
  42. package/dist/utils/impactAnalyzer.js +248 -0
  43. package/dist/utils/knowledgeDistributionAnalyzer.js +374 -0
  44. package/dist/utils/matrixGenerator.js +350 -0
  45. package/dist/utils/slideGenerator.js +313 -0
  46. package/dist/utils/streakCalculator.js +135 -0
  47. package/dist/utils/timePatternAnalyzer.js +305 -0
  48. package/dist/utils/wrappedDisplay.js +115 -0
  49. package/dist/utils/wrappedGenerator.js +377 -0
  50. package/dist/utils/wrappedHtmlGenerator.js +552 -0
  51. package/package.json +55 -0
@@ -0,0 +1,433 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCategoryLabel = exports.getTierColor = exports.ACHIEVEMENTS = void 0;
4
+ exports.ACHIEVEMENTS = [
5
+ // ========================================
6
+ // 🎯 MILESTONE BADGES
7
+ // ========================================
8
+ {
9
+ id: 'first-steps',
10
+ name: 'First Steps',
11
+ emoji: '🌱',
12
+ description: 'Made your first commit',
13
+ category: 'milestone',
14
+ tier: 'bronze',
15
+ criteria: { type: 'commit-count', threshold: 1, comparator: '>=' },
16
+ progress: 0,
17
+ isUnlocked: false,
18
+ isSecret: false
19
+ },
20
+ {
21
+ id: 'getting-started',
22
+ name: 'Getting Started',
23
+ emoji: '⭐',
24
+ description: 'Reached 10 commits',
25
+ category: 'milestone',
26
+ tier: 'bronze',
27
+ criteria: { type: 'commit-count', threshold: 10, comparator: '>=' },
28
+ progress: 0,
29
+ isUnlocked: false,
30
+ isSecret: false
31
+ },
32
+ {
33
+ id: 'on-fire',
34
+ name: 'On Fire',
35
+ emoji: '🔥',
36
+ description: 'Reached 50 commits',
37
+ category: 'milestone',
38
+ tier: 'silver',
39
+ criteria: { type: 'commit-count', threshold: 50, comparator: '>=' },
40
+ progress: 0,
41
+ isUnlocked: false,
42
+ isSecret: false
43
+ },
44
+ {
45
+ id: 'century-club',
46
+ name: 'Century Club',
47
+ emoji: '👑',
48
+ description: 'Reached 100 commits',
49
+ category: 'milestone',
50
+ tier: 'gold',
51
+ criteria: { type: 'commit-count', threshold: 100, comparator: '>=' },
52
+ progress: 0,
53
+ isUnlocked: false,
54
+ isSecret: false
55
+ },
56
+ {
57
+ id: 'champion',
58
+ name: 'Champion',
59
+ emoji: '🏆',
60
+ description: 'Reached 500 commits',
61
+ category: 'milestone',
62
+ tier: 'platinum',
63
+ criteria: { type: 'commit-count', threshold: 500, comparator: '>=' },
64
+ progress: 0,
65
+ isUnlocked: false,
66
+ isSecret: false
67
+ },
68
+ {
69
+ id: 'master',
70
+ name: 'Master',
71
+ emoji: '💫',
72
+ description: 'Reached 1000 commits',
73
+ category: 'milestone',
74
+ tier: 'platinum',
75
+ criteria: { type: 'commit-count', threshold: 1000, comparator: '>=' },
76
+ progress: 0,
77
+ isUnlocked: false,
78
+ isSecret: false
79
+ },
80
+ {
81
+ id: 'legend',
82
+ name: 'Legend',
83
+ emoji: '🌟',
84
+ description: 'Reached 5000 commits',
85
+ category: 'milestone',
86
+ tier: 'legendary',
87
+ criteria: { type: 'commit-count', threshold: 5000, comparator: '>=' },
88
+ progress: 0,
89
+ isUnlocked: false,
90
+ isSecret: false
91
+ },
92
+ // ========================================
93
+ // 📅 STREAK ACHIEVEMENTS
94
+ // ========================================
95
+ {
96
+ id: 'consistency',
97
+ name: 'Consistency',
98
+ emoji: '📅',
99
+ description: 'Maintained a 3-day commit streak',
100
+ category: 'time',
101
+ tier: 'bronze',
102
+ criteria: { type: 'streak', threshold: 3, comparator: '>=' },
103
+ progress: 0,
104
+ isUnlocked: false,
105
+ isSecret: false
106
+ },
107
+ {
108
+ id: 'week-warrior',
109
+ name: 'Week Warrior',
110
+ emoji: '⚡',
111
+ description: 'Maintained a 7-day commit streak',
112
+ category: 'time',
113
+ tier: 'silver',
114
+ criteria: { type: 'streak', threshold: 7, comparator: '>=' },
115
+ progress: 0,
116
+ isUnlocked: false,
117
+ isSecret: false
118
+ },
119
+ {
120
+ id: 'streak-on-fire',
121
+ name: 'Streak On Fire',
122
+ emoji: '🔥',
123
+ description: 'Maintained a 14-day commit streak',
124
+ category: 'time',
125
+ tier: 'gold',
126
+ criteria: { type: 'streak', threshold: 14, comparator: '>=' },
127
+ progress: 0,
128
+ isUnlocked: false,
129
+ isSecret: false
130
+ },
131
+ {
132
+ id: 'unstoppable',
133
+ name: 'Unstoppable',
134
+ emoji: '💪',
135
+ description: 'Maintained a 30-day commit streak',
136
+ category: 'time',
137
+ tier: 'platinum',
138
+ criteria: { type: 'streak', threshold: 30, comparator: '>=' },
139
+ progress: 0,
140
+ isUnlocked: false,
141
+ isSecret: false
142
+ },
143
+ {
144
+ id: 'iron-will',
145
+ name: 'Iron Will',
146
+ emoji: '🦾',
147
+ description: 'Maintained a 60-day commit streak',
148
+ category: 'time',
149
+ tier: 'platinum',
150
+ criteria: { type: 'streak', threshold: 60, comparator: '>=' },
151
+ progress: 0,
152
+ isUnlocked: false,
153
+ isSecret: false
154
+ },
155
+ {
156
+ id: 'code-machine',
157
+ name: 'Code Machine',
158
+ emoji: '👾',
159
+ description: 'Maintained a 100-day commit streak',
160
+ category: 'time',
161
+ tier: 'legendary',
162
+ criteria: { type: 'streak', threshold: 100, comparator: '>=' },
163
+ progress: 0,
164
+ isUnlocked: false,
165
+ isSecret: false
166
+ },
167
+ // ========================================
168
+ // 🎨 QUALITY BADGES
169
+ // ========================================
170
+ {
171
+ id: 'clean-coder',
172
+ name: 'Clean Coder',
173
+ emoji: '✨',
174
+ description: '90%+ commits follow conventions',
175
+ category: 'quality',
176
+ tier: 'gold',
177
+ criteria: {
178
+ type: 'custom',
179
+ customCheck: (data) => data.commitQuality.conventionalCommits.adherence >= 90
180
+ },
181
+ progress: 0,
182
+ isUnlocked: false,
183
+ isSecret: false
184
+ },
185
+ {
186
+ id: 'documentarian',
187
+ name: 'Documentarian',
188
+ emoji: '📝',
189
+ description: '50%+ commits include detailed bodies',
190
+ category: 'quality',
191
+ tier: 'silver',
192
+ criteria: {
193
+ type: 'custom',
194
+ customCheck: (data) => (data.commitQuality.bodyQuality.withBody / data.totalCommits) >= 0.5
195
+ },
196
+ progress: 0,
197
+ isUnlocked: false,
198
+ isSecret: false
199
+ },
200
+ {
201
+ id: 'quality-champion',
202
+ name: 'Quality Champion',
203
+ emoji: '🏅',
204
+ description: 'Achieved 9.0+ overall quality score',
205
+ category: 'quality',
206
+ tier: 'platinum',
207
+ criteria: { type: 'quality-score', threshold: 9.0, comparator: '>=' },
208
+ progress: 0,
209
+ isUnlocked: false,
210
+ isSecret: false
211
+ },
212
+ {
213
+ id: 'rising-star',
214
+ name: 'Rising Star',
215
+ emoji: '📈',
216
+ description: 'Quality score above 7.0',
217
+ category: 'quality',
218
+ tier: 'gold',
219
+ criteria: { type: 'quality-score', threshold: 7.0, comparator: '>=' },
220
+ progress: 0,
221
+ isUnlocked: false,
222
+ isSecret: false
223
+ },
224
+ // ========================================
225
+ // ⏰ TIME PATTERN BADGES
226
+ // ========================================
227
+ {
228
+ id: 'early-bird',
229
+ name: 'Early Bird',
230
+ emoji: '🌅',
231
+ description: '50+ commits before 9am',
232
+ category: 'time',
233
+ tier: 'silver',
234
+ criteria: {
235
+ type: 'time-pattern',
236
+ customCheck: (data) => {
237
+ const earlyCommits = data.commits.filter(c => {
238
+ const hour = new Date(c.date).getHours();
239
+ return hour >= 5 && hour < 9;
240
+ }).length;
241
+ return earlyCommits >= 50;
242
+ }
243
+ },
244
+ progress: 0,
245
+ isUnlocked: false,
246
+ isSecret: false
247
+ },
248
+ {
249
+ id: 'night-owl',
250
+ name: 'Night Owl',
251
+ emoji: '🦉',
252
+ description: '50+ commits after 9pm',
253
+ category: 'time',
254
+ tier: 'silver',
255
+ criteria: {
256
+ type: 'time-pattern',
257
+ customCheck: (data) => {
258
+ const lateCommits = data.commits.filter(c => {
259
+ const hour = new Date(c.date).getHours();
260
+ return hour >= 21 || hour < 5;
261
+ }).length;
262
+ return lateCommits >= 50;
263
+ }
264
+ },
265
+ progress: 0,
266
+ isUnlocked: false,
267
+ isSecret: false
268
+ },
269
+ {
270
+ id: 'balanced-soul',
271
+ name: 'Balanced Soul',
272
+ emoji: '⚖️',
273
+ description: 'Even distribution across all time periods',
274
+ category: 'time',
275
+ tier: 'gold',
276
+ criteria: {
277
+ type: 'custom',
278
+ customCheck: (data) => data.timePattern.chronotype === 'balanced'
279
+ },
280
+ progress: 0,
281
+ isUnlocked: false,
282
+ isSecret: false
283
+ },
284
+ {
285
+ id: 'work-life-balance',
286
+ name: 'Balanced Life',
287
+ emoji: '🌴',
288
+ description: 'Excellent work-life balance score',
289
+ category: 'time',
290
+ tier: 'gold',
291
+ criteria: {
292
+ type: 'custom',
293
+ customCheck: (data) => data.timePattern.workLifeBalance.score >= 4
294
+ },
295
+ progress: 0,
296
+ isUnlocked: false,
297
+ isSecret: false
298
+ },
299
+ // ========================================
300
+ // 🎮 SPECIAL ACHIEVEMENTS
301
+ // ========================================
302
+ {
303
+ id: 'midnight-coder',
304
+ name: 'Midnight Coder',
305
+ emoji: '🎆',
306
+ description: 'Committed at exactly midnight',
307
+ category: 'special',
308
+ tier: 'silver',
309
+ criteria: {
310
+ type: 'custom',
311
+ customCheck: (data) => data.commits.some(c => {
312
+ const date = new Date(c.date);
313
+ return date.getHours() === 0 && date.getMinutes() === 0;
314
+ })
315
+ },
316
+ progress: 0,
317
+ isUnlocked: false,
318
+ isSecret: true
319
+ },
320
+ {
321
+ id: 'lucky-seven',
322
+ name: 'Lucky Seven',
323
+ emoji: '🎰',
324
+ description: 'Reached exactly 777 commits',
325
+ category: 'special',
326
+ tier: 'gold',
327
+ criteria: { type: 'commit-count', threshold: 777, comparator: '==' },
328
+ progress: 0,
329
+ isUnlocked: false,
330
+ isSecret: true
331
+ },
332
+ {
333
+ id: 'binary-master',
334
+ name: 'Binary Master',
335
+ emoji: '🔢',
336
+ description: 'Reached exactly 1024 commits',
337
+ category: 'special',
338
+ tier: 'platinum',
339
+ criteria: { type: 'commit-count', threshold: 1024, comparator: '==' },
340
+ progress: 0,
341
+ isUnlocked: false,
342
+ isSecret: true
343
+ },
344
+ {
345
+ id: 'weekend-warrior',
346
+ name: 'Weekend Warrior',
347
+ emoji: '🎪',
348
+ description: '20%+ commits on weekends',
349
+ category: 'special',
350
+ tier: 'silver',
351
+ criteria: {
352
+ type: 'custom',
353
+ customCheck: (data) => {
354
+ const weekendRatio = data.timePattern.workLifeBalance.weekendCommits / data.totalCommits;
355
+ return weekendRatio >= 0.2;
356
+ }
357
+ },
358
+ progress: 0,
359
+ isUnlocked: false,
360
+ isSecret: false
361
+ },
362
+ // ========================================
363
+ // 🏆 META ACHIEVEMENTS
364
+ // ========================================
365
+ {
366
+ id: 'collector',
367
+ name: 'Collector',
368
+ emoji: '🎖️',
369
+ description: 'Earned 10 badges',
370
+ category: 'meta',
371
+ tier: 'silver',
372
+ criteria: {
373
+ type: 'custom',
374
+ customCheck: (data) => false // Will be checked separately
375
+ },
376
+ progress: 0,
377
+ isUnlocked: false,
378
+ isSecret: false
379
+ },
380
+ {
381
+ id: 'completionist',
382
+ name: 'Completionist',
383
+ emoji: '🏅',
384
+ description: 'Earned 25 badges',
385
+ category: 'meta',
386
+ tier: 'gold',
387
+ criteria: {
388
+ type: 'custom',
389
+ customCheck: (data) => false // Will be checked separately
390
+ },
391
+ progress: 0,
392
+ isUnlocked: false,
393
+ isSecret: false
394
+ },
395
+ {
396
+ id: 'badge-king',
397
+ name: 'Badge King',
398
+ emoji: '👑',
399
+ description: 'Earned 50 badges',
400
+ category: 'meta',
401
+ tier: 'legendary',
402
+ criteria: {
403
+ type: 'custom',
404
+ customCheck: (data) => false // Will be checked separately
405
+ },
406
+ progress: 0,
407
+ isUnlocked: false,
408
+ isSecret: false
409
+ }
410
+ ];
411
+ function getTierColor(tier) {
412
+ switch (tier) {
413
+ case 'bronze': return '#cd7f32';
414
+ case 'silver': return '#c0c0c0';
415
+ case 'gold': return '#ffd700';
416
+ case 'platinum': return '#e5e4e2';
417
+ case 'legendary': return '#ff6b6b';
418
+ default: return '#8b949e';
419
+ }
420
+ }
421
+ exports.getTierColor = getTierColor;
422
+ function getCategoryLabel(category) {
423
+ switch (category) {
424
+ case 'milestone': return '🎯 Milestone';
425
+ case 'quality': return '🎨 Quality';
426
+ case 'collaboration': return '🤝 Collaboration';
427
+ case 'time': return '⏰ Time & Consistency';
428
+ case 'special': return '🎮 Special';
429
+ case 'meta': return '🏆 Meta';
430
+ default: return category;
431
+ }
432
+ }
433
+ exports.getCategoryLabel = getCategoryLabel;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNextMilestone = exports.getAchievementsByCategory = exports.getRecentAchievements = exports.checkAchievements = void 0;
4
+ const achievementDefinitions_1 = require("./achievementDefinitions");
5
+ function checkAchievements(data) {
6
+ // Create a deep copy of achievements to avoid mutation
7
+ const achievements = achievementDefinitions_1.ACHIEVEMENTS.map(a => ({ ...a }));
8
+ const newlyEarned = [];
9
+ // Check each achievement
10
+ for (const achievement of achievements) {
11
+ if (!achievement.isUnlocked) {
12
+ // Calculate progress
13
+ achievement.progress = calculateProgress(achievement, data);
14
+ // Check if criteria is met
15
+ if (meetsCriteria(achievement, data)) {
16
+ achievement.isUnlocked = true;
17
+ achievement.earnedDate = new Date();
18
+ newlyEarned.push(achievement);
19
+ }
20
+ }
21
+ else {
22
+ // Already unlocked, progress is 100
23
+ achievement.progress = 100;
24
+ }
25
+ }
26
+ // Check meta achievements (based on unlocked count)
27
+ const unlockedCount = achievements.filter(a => a.isUnlocked && a.category !== 'meta').length;
28
+ const metaAchievements = achievements.filter(a => a.category === 'meta');
29
+ for (const metaAch of metaAchievements) {
30
+ if (!metaAch.isUnlocked) {
31
+ if (metaAch.id === 'collector' && unlockedCount >= 10) {
32
+ metaAch.isUnlocked = true;
33
+ metaAch.earnedDate = new Date();
34
+ metaAch.progress = 100;
35
+ newlyEarned.push(metaAch);
36
+ }
37
+ else if (metaAch.id === 'completionist' && unlockedCount >= 25) {
38
+ metaAch.isUnlocked = true;
39
+ metaAch.earnedDate = new Date();
40
+ metaAch.progress = 100;
41
+ newlyEarned.push(metaAch);
42
+ }
43
+ else if (metaAch.id === 'badge-king' && unlockedCount >= 50) {
44
+ metaAch.isUnlocked = true;
45
+ metaAch.earnedDate = new Date();
46
+ metaAch.progress = 100;
47
+ newlyEarned.push(metaAch);
48
+ }
49
+ else {
50
+ // Calculate meta progress
51
+ if (metaAch.id === 'collector') {
52
+ metaAch.progress = Math.min(100, (unlockedCount / 10) * 100);
53
+ }
54
+ else if (metaAch.id === 'completionist') {
55
+ metaAch.progress = Math.min(100, (unlockedCount / 25) * 100);
56
+ }
57
+ else if (metaAch.id === 'badge-king') {
58
+ metaAch.progress = Math.min(100, (unlockedCount / 50) * 100);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ // Find next milestones (top 3 closest to unlocking)
64
+ const lockedAchievements = achievements.filter(a => !a.isUnlocked);
65
+ const nextMilestones = lockedAchievements
66
+ .sort((a, b) => b.progress - a.progress)
67
+ .slice(0, 3);
68
+ const totalUnlocked = achievements.filter(a => a.isUnlocked).length;
69
+ const completionPercentage = (totalUnlocked / achievements.length) * 100;
70
+ return {
71
+ achievements,
72
+ unlockedCount: totalUnlocked,
73
+ totalCount: achievements.length,
74
+ recentlyEarned: newlyEarned,
75
+ nextMilestones,
76
+ completionPercentage
77
+ };
78
+ }
79
+ exports.checkAchievements = checkAchievements;
80
+ function meetsCriteria(achievement, data) {
81
+ const { type, threshold, comparator, customCheck } = achievement.criteria;
82
+ // Handle custom criteria
83
+ if (customCheck) {
84
+ return customCheck(data);
85
+ }
86
+ // Handle standard criteria
87
+ if (threshold === undefined || comparator === undefined) {
88
+ return false;
89
+ }
90
+ const value = extractValue(data, type);
91
+ return compare(value, threshold, comparator);
92
+ }
93
+ function calculateProgress(achievement, data) {
94
+ const { type, threshold, customCheck } = achievement.criteria;
95
+ // Custom checks don't have numeric progress
96
+ if (customCheck) {
97
+ // Try to calculate progress for known patterns
98
+ if (achievement.id === 'early-bird' || achievement.id === 'night-owl') {
99
+ const isEarlyBird = achievement.id === 'early-bird';
100
+ const targetCommits = data.commits.filter(c => {
101
+ const hour = new Date(c.date).getHours();
102
+ if (isEarlyBird) {
103
+ return hour >= 5 && hour < 9;
104
+ }
105
+ else {
106
+ return hour >= 21 || hour < 5;
107
+ }
108
+ }).length;
109
+ return Math.min(100, (targetCommits / 50) * 100);
110
+ }
111
+ // For other custom checks, return 0 or 100
112
+ return meetsCriteria(achievement, data) ? 100 : 0;
113
+ }
114
+ if (threshold === undefined) {
115
+ return 0;
116
+ }
117
+ const current = extractValue(data, type);
118
+ return Math.min(100, (current / threshold) * 100);
119
+ }
120
+ function extractValue(data, type) {
121
+ switch (type) {
122
+ case 'commit-count':
123
+ return data.totalCommits;
124
+ case 'streak':
125
+ return data.streakData.longestStreak.days;
126
+ case 'quality-score':
127
+ return data.commitQuality.overallScore;
128
+ default:
129
+ return 0;
130
+ }
131
+ }
132
+ function compare(value, threshold, comparator) {
133
+ switch (comparator) {
134
+ case '>=':
135
+ return value >= threshold;
136
+ case '>':
137
+ return value > threshold;
138
+ case '==':
139
+ return value === threshold;
140
+ case '<':
141
+ return value < threshold;
142
+ case '<=':
143
+ return value <= threshold;
144
+ default:
145
+ return false;
146
+ }
147
+ }
148
+ function getRecentAchievements(progress, days = 7) {
149
+ const cutoffDate = new Date();
150
+ cutoffDate.setDate(cutoffDate.getDate() - days);
151
+ return progress.achievements
152
+ .filter(a => a.isUnlocked && a.earnedDate && a.earnedDate >= cutoffDate)
153
+ .sort((a, b) => {
154
+ if (!a.earnedDate || !b.earnedDate)
155
+ return 0;
156
+ return b.earnedDate.getTime() - a.earnedDate.getTime();
157
+ });
158
+ }
159
+ exports.getRecentAchievements = getRecentAchievements;
160
+ function getAchievementsByCategory(progress, category) {
161
+ return progress.achievements.filter(a => a.category === category);
162
+ }
163
+ exports.getAchievementsByCategory = getAchievementsByCategory;
164
+ function getNextMilestone(progress) {
165
+ const locked = progress.achievements.filter(a => !a.isUnlocked);
166
+ if (locked.length === 0)
167
+ return null;
168
+ return locked.reduce((closest, current) => current.progress > closest.progress ? current : closest);
169
+ }
170
+ exports.getNextMilestone = getNextMilestone;