@orjok/commons 1.1.0 → 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.
package/dist/index.js CHANGED
@@ -1,19 +1,36 @@
1
1
  import {
2
2
  MediaUrlCache,
3
+ XP_REWARDS,
4
+ XP_TIERS,
3
5
  _resetMediaUrlWarning,
4
6
  aggregateChapterMetrics,
5
7
  aggregateSubjectMetrics,
6
8
  aggregateTopicMetrics,
7
9
  calculateAccuracy,
10
+ calculateUnreadCount,
11
+ calculateXpReward,
12
+ difficultyOptions,
13
+ formatLevelUpNotification,
14
+ getAllXpTiers,
15
+ getLevelFromXp,
16
+ getPublicExamsByLevel,
17
+ getXpForLevel,
18
+ getXpProgress,
19
+ getXpRequiredForNextLevel,
20
+ getXpTier,
8
21
  isDifficultyMatch,
9
22
  isLanguageMatch,
10
23
  isStorageKey,
11
24
  isValidMCQ,
25
+ languageOptions,
26
+ levelOptions,
27
+ mergeNotificationTimeline,
28
+ publicExamOptions,
12
29
  resolveMediaUrl,
13
30
  resolveMediaUrls,
14
31
  shuffle,
15
32
  shuffleInPlace
16
- } from "./chunk-ZDGYAVEC.js";
33
+ } from "./chunk-B4UJE3F4.js";
17
34
 
18
35
  // src/services/user.service.ts
19
36
  var UserService = class {
@@ -25,11 +42,12 @@ var UserService = class {
25
42
  `query GetUser($id: String!) {
26
43
  getUser(id: $id) {
27
44
  id fullName avatarUrl avatarConfig tier subscriptionExpiresAt
28
- aiEvaluationRemaining questionCount packCount contestCount
45
+ aiEvaluationRemaining aiQuestionGenRemaining questionCount packCount contestCount
29
46
  EngineeringRating MedicalRating VarsityRating BCSRating
30
47
  sscOverallRating sscPhysicsRating sscChemistryRating sscMathRating sscBiologyRating
31
48
  hscOverallRating hscPhysicsRating hscChemistryRating hscMathRating hscBiologyRating
32
49
  bcsOverallRating
50
+ xp userLevel
33
51
  }
34
52
  }`,
35
53
  { id }
@@ -37,13 +55,16 @@ var UserService = class {
37
55
  return result.data.getUser;
38
56
  }
39
57
  async updateAvatar(input) {
58
+ const updateInput = { id: input.userId };
59
+ if (input.avatarUrl !== void 0) updateInput.avatarUrl = input.avatarUrl;
60
+ if (input.avatarConfig !== void 0) updateInput.avatarConfig = input.avatarConfig;
40
61
  const result = await this.network.mutate(
41
62
  `mutation UpdateUser($input: UpdateUserInput!) {
42
63
  updateUser(input: $input) {
43
64
  id fullName avatarUrl avatarConfig
44
65
  }
45
66
  }`,
46
- { input: { id: input.userId, avatarUrl: input.avatarUrl, avatarConfig: input.avatarConfig } }
67
+ { input: updateInput }
47
68
  );
48
69
  return result.data.updateUser;
49
70
  }
@@ -107,8 +128,59 @@ var UserService = class {
107
128
  );
108
129
  return result.data.listCourseEnrolledByUserIdAndCreatedAt.items;
109
130
  }
131
+ async updateProfile(input) {
132
+ const result = await this.network.mutate(
133
+ `mutation UpdateUserProfile($input: UpdateUserInput!) {
134
+ updateUser(input: $input) {
135
+ id fullName avatarUrl avatarConfig tier subscriptionExpiresAt
136
+ aiEvaluationRemaining aiQuestionGenRemaining questionCount packCount contestCount
137
+ xp userLevel
138
+ }
139
+ }`,
140
+ { input }
141
+ );
142
+ return result.data.updateUser;
143
+ }
144
+ async list(input = {}) {
145
+ const result = await this.network.query(
146
+ `query ListUsers($limit: Int, $nextToken: String, $filter: ModelUserFilterInput) {
147
+ listUsers(limit: $limit, nextToken: $nextToken, filter: $filter) {
148
+ items {
149
+ id fullName avatarUrl avatarConfig tier subscriptionExpiresAt
150
+ aiEvaluationRemaining aiQuestionGenRemaining questionCount packCount contestCount
151
+ xp userLevel
152
+ }
153
+ nextToken
154
+ }
155
+ }`,
156
+ { limit: input.limit ?? 20, nextToken: input.nextToken, filter: input.filter }
157
+ );
158
+ return result.data.listUsers;
159
+ }
110
160
  };
111
161
 
162
+ // src/utils/pagination.ts
163
+ async function paginateWithAccumulator(fetchPage, targetLimit = 12, initialNextToken, maxIterations = 10) {
164
+ const accumulated = [];
165
+ let currentNextToken = initialNextToken;
166
+ let iterations = 0;
167
+ while (accumulated.length < targetLimit && iterations < maxIterations) {
168
+ iterations++;
169
+ const remaining = targetLimit - accumulated.length;
170
+ const response = await fetchPage(remaining, currentNextToken);
171
+ const items = response?.items || [];
172
+ accumulated.push(...items);
173
+ currentNextToken = response?.nextToken;
174
+ if (!currentNextToken || accumulated.length >= targetLimit) {
175
+ break;
176
+ }
177
+ }
178
+ return {
179
+ items: accumulated.slice(0, targetLimit),
180
+ nextToken: currentNextToken ?? null
181
+ };
182
+ }
183
+
112
184
  // src/services/question.service.ts
113
185
  var QuestionService = class {
114
186
  constructor(network) {
@@ -116,8 +188,8 @@ var QuestionService = class {
116
188
  }
117
189
  async create(input) {
118
190
  const result = await this.network.mutate(
119
- `mutation CreateQuestion($question: String!, $answer: String!, $options: String!, $language: String!, $level: String!, $difficulty: String!, $type: String, $explanation: String, $tags: String, $imagePayload: String, $optionsImagesPayload: String, $packId: ID, $order: Int, $contestId: ID, $subjectId: ID, $chapterId: ID, $topicId: ID, $extra: String, $markingInstructions: String) {
120
- createQuestion(question: $question, answer: $answer, options: $options, language: $language, level: $level, difficulty: $difficulty, type: $type, explanation: $explanation, tags: $tags, imagePayload: $imagePayload, optionsImagesPayload: $optionsImagesPayload, packId: $packId, order: $order, contestId: $contestId, subjectId: $subjectId, chapterId: $chapterId, topicId: $topicId, extra: $extra, markingInstructions: $markingInstructions) {
191
+ `mutation CreateQuestion($question: String!, $answer: String!, $options: String!, $language: String!, $level: String!, $publicExam: String, $difficulty: String!, $type: String, $explanation: String, $tags: String, $imagePayload: String, $optionsImagesPayload: String, $packId: ID, $order: Int, $contestId: ID, $subjectId: ID, $chapterId: ID, $topicId: ID, $extra: String, $markingInstructions: String) {
192
+ createQuestion(question: $question, answer: $answer, options: $options, language: $language, level: $level, publicExam: $publicExam, difficulty: $difficulty, type: $type, explanation: $explanation, tags: $tags, imagePayload: $imagePayload, optionsImagesPayload: $optionsImagesPayload, packId: $packId, order: $order, contestId: $contestId, subjectId: $subjectId, chapterId: $chapterId, topicId: $topicId, extra: $extra, markingInstructions: $markingInstructions) {
121
193
  status questionId
122
194
  }
123
195
  }`,
@@ -129,7 +201,7 @@ var QuestionService = class {
129
201
  const result = await this.network.query(
130
202
  `query GetQuestion($id: String!) {
131
203
  getQuestionObject(id: $id) {
132
- id question imageUrl language level difficulty type voteCount
204
+ id question imageUrl language level publicExam difficulty type voteCount
133
205
  verificationStatus owner packId subjectId chapterId topicId order createdAt
134
206
  options { items { id content } }
135
207
  tags { items { id tagId } }
@@ -143,7 +215,7 @@ var QuestionService = class {
143
215
  const result = await this.network.query(
144
216
  `query GetFullQuestion($id: String!) {
145
217
  getQuestionObject(id: $id) {
146
- id question answer explanation extra imageUrl language level difficulty type
218
+ id question answer explanation extra imageUrl language level publicExam difficulty type
147
219
  markingInstructions voteCount verificationStatus owner packId
148
220
  subjectId chapterId topicId order createdAt
149
221
  options { items { id content } }
@@ -181,7 +253,7 @@ var QuestionService = class {
181
253
  `query ListQuestionsByPack($packId: ID!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
182
254
  listQuestionObjectByPackIdAndOrder(packId: $packId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
183
255
  items {
184
- id question answer explanation extra imageUrl language level difficulty type
256
+ id question answer explanation extra imageUrl language level publicExam difficulty type
185
257
  markingInstructions voteCount owner packId subjectId chapterId topicId order createdAt
186
258
  options { items { id content } }
187
259
  tags { items { id tagId } }
@@ -197,7 +269,7 @@ var QuestionService = class {
197
269
  const result = await this.network.mutate(
198
270
  `mutation UpdateQuestion($input: UpdateQuestionObjectInput!) {
199
271
  updateQuestionObject(input: $input) {
200
- id question answer explanation extra imageUrl language level difficulty type
272
+ id question answer explanation extra imageUrl language level publicExam difficulty type
201
273
  markingInstructions voteCount owner packId subjectId chapterId topicId order createdAt
202
274
  options { items { id content } }
203
275
  }
@@ -297,70 +369,288 @@ var QuestionService = class {
297
369
  return result.data.addQuestionTag;
298
370
  }
299
371
  async listBySubject(input) {
300
- const variables = {
301
- subjectId: input.id,
302
- sortDirection: input.sortDirection ?? "DESC",
303
- limit: input.limit ?? 20,
304
- nextToken: input.nextToken
305
- };
306
- const result = await this.network.query(
307
- `query ListQuestionsBySubject($subjectId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
308
- listQuestionObjectBySubjectIdAndCreatedAt(subjectId: $subjectId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
309
- items {
310
- id question imageUrl language level difficulty type voteCount
311
- verificationStatus owner packId subjectId chapterId topicId createdAt
312
- options { items { id content } }
313
- }
314
- nextToken
372
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
373
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
374
+ const filter = {};
375
+ if (input.publicExam) {
376
+ filter.publicExam = { eq: input.publicExam };
377
+ }
378
+ if (input.questionType && input.questionType !== "Both") {
379
+ filter.type = { eq: input.questionType };
380
+ }
381
+ const targetLimit = input.limit ?? 12;
382
+ const executeQuery = async (pageLimit, nextToken) => {
383
+ if (hasLanguage) {
384
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
385
+ const variables = {
386
+ subjectId: input.id,
387
+ language_difficulty_createdAt: { beginsWith: rangeKey },
388
+ sortDirection: input.sortDirection ?? "DESC",
389
+ limit: pageLimit,
390
+ nextToken,
391
+ ...Object.keys(filter).length > 0 ? { filter } : {}
392
+ };
393
+ const result = await this.network.query(
394
+ `query ListQuestionsBySubjectAndLangDiff($subjectId: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
395
+ listQuestionObjectBySubjectIdAndLanguage_difficulty_createdAt(subjectId: $subjectId, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
396
+ items {
397
+ id question imageUrl language level publicExam difficulty type voteCount
398
+ verificationStatus owner packId subjectId chapterId topicId createdAt
399
+ options { items { id content } }
400
+ }
401
+ nextToken
402
+ }
403
+ }`,
404
+ variables
405
+ );
406
+ return result.data.listQuestionObjectBySubjectIdAndLanguage_difficulty_createdAt;
407
+ } else {
408
+ const queryFilter = { ...filter };
409
+ if (hasDifficulty) {
410
+ queryFilter.difficulty = { eq: input.difficulty };
315
411
  }
316
- }`,
317
- variables
318
- );
319
- return result.data.listQuestionObjectBySubjectIdAndCreatedAt;
412
+ const variables = {
413
+ subjectId: input.id,
414
+ sortDirection: input.sortDirection ?? "DESC",
415
+ limit: pageLimit,
416
+ nextToken,
417
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
418
+ };
419
+ const result = await this.network.query(
420
+ `query ListQuestionsBySubject($subjectId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
421
+ listQuestionObjectBySubjectIdAndCreatedAt(subjectId: $subjectId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
422
+ items {
423
+ id question imageUrl language level publicExam difficulty type voteCount
424
+ verificationStatus owner packId subjectId chapterId topicId createdAt
425
+ options { items { id content } }
426
+ }
427
+ nextToken
428
+ }
429
+ }`,
430
+ variables
431
+ );
432
+ return result.data.listQuestionObjectBySubjectIdAndCreatedAt;
433
+ }
434
+ };
435
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
436
+ if (!hasPostFilter) {
437
+ return executeQuery(targetLimit, input.nextToken);
438
+ }
439
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
320
440
  }
321
441
  async listByChapter(input) {
322
- const variables = {
323
- chapterId: input.id,
324
- sortDirection: input.sortDirection ?? "DESC",
325
- limit: input.limit ?? 20,
326
- nextToken: input.nextToken
327
- };
328
- const result = await this.network.query(
329
- `query ListQuestionsByChapter($chapterId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
330
- listQuestionObjectByChapterIdAndCreatedAt(chapterId: $chapterId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
331
- items {
332
- id question imageUrl language level difficulty type voteCount
333
- verificationStatus owner packId subjectId chapterId topicId createdAt
334
- options { items { id content } }
335
- }
336
- nextToken
442
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
443
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
444
+ const filter = {};
445
+ if (input.publicExam) {
446
+ filter.publicExam = { eq: input.publicExam };
447
+ }
448
+ if (input.questionType && input.questionType !== "Both") {
449
+ filter.type = { eq: input.questionType };
450
+ }
451
+ const targetLimit = input.limit ?? 12;
452
+ const executeQuery = async (pageLimit, nextToken) => {
453
+ if (hasLanguage) {
454
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
455
+ const variables = {
456
+ chapterId: input.id,
457
+ language_difficulty_createdAt: { beginsWith: rangeKey },
458
+ sortDirection: input.sortDirection ?? "DESC",
459
+ limit: pageLimit,
460
+ nextToken,
461
+ ...Object.keys(filter).length > 0 ? { filter } : {}
462
+ };
463
+ const result = await this.network.query(
464
+ `query ListQuestionsByChapterAndLangDiff($chapterId: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
465
+ listQuestionObjectByChapterIdAndLanguage_difficulty_createdAt(chapterId: $chapterId, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
466
+ items {
467
+ id question imageUrl language level publicExam difficulty type voteCount
468
+ verificationStatus owner packId subjectId chapterId topicId createdAt
469
+ options { items { id content } }
470
+ }
471
+ nextToken
472
+ }
473
+ }`,
474
+ variables
475
+ );
476
+ return result.data.listQuestionObjectByChapterIdAndLanguage_difficulty_createdAt;
477
+ } else {
478
+ const queryFilter = { ...filter };
479
+ if (hasDifficulty) {
480
+ queryFilter.difficulty = { eq: input.difficulty };
337
481
  }
338
- }`,
339
- variables
340
- );
341
- return result.data.listQuestionObjectByChapterIdAndCreatedAt;
482
+ const variables = {
483
+ chapterId: input.id,
484
+ sortDirection: input.sortDirection ?? "DESC",
485
+ limit: pageLimit,
486
+ nextToken,
487
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
488
+ };
489
+ const result = await this.network.query(
490
+ `query ListQuestionsByChapter($chapterId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
491
+ listQuestionObjectByChapterIdAndCreatedAt(chapterId: $chapterId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
492
+ items {
493
+ id question imageUrl language level publicExam difficulty type voteCount
494
+ verificationStatus owner packId subjectId chapterId topicId createdAt
495
+ options { items { id content } }
496
+ }
497
+ nextToken
498
+ }
499
+ }`,
500
+ variables
501
+ );
502
+ return result.data.listQuestionObjectByChapterIdAndCreatedAt;
503
+ }
504
+ };
505
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
506
+ if (!hasPostFilter) {
507
+ return executeQuery(targetLimit, input.nextToken);
508
+ }
509
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
342
510
  }
343
511
  async listByTopic(input) {
344
- const variables = {
345
- topicId: input.id,
346
- sortDirection: input.sortDirection ?? "DESC",
347
- limit: input.limit ?? 20,
348
- nextToken: input.nextToken
512
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
513
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
514
+ const filter = {};
515
+ if (input.publicExam) {
516
+ filter.publicExam = { eq: input.publicExam };
517
+ }
518
+ if (input.questionType && input.questionType !== "Both") {
519
+ filter.type = { eq: input.questionType };
520
+ }
521
+ const targetLimit = input.limit ?? 12;
522
+ const executeQuery = async (pageLimit, nextToken) => {
523
+ if (hasLanguage) {
524
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
525
+ const variables = {
526
+ topicId: input.id,
527
+ language_difficulty_createdAt: { beginsWith: rangeKey },
528
+ sortDirection: input.sortDirection ?? "DESC",
529
+ limit: pageLimit,
530
+ nextToken,
531
+ ...Object.keys(filter).length > 0 ? { filter } : {}
532
+ };
533
+ const result = await this.network.query(
534
+ `query ListQuestionsByTopicAndLangDiff($topicId: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
535
+ listQuestionObjectByTopicIdAndLanguage_difficulty_createdAt(topicId: $topicId, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
536
+ items {
537
+ id question imageUrl language level publicExam difficulty type voteCount
538
+ verificationStatus owner packId subjectId chapterId topicId createdAt
539
+ options { items { id content } }
540
+ }
541
+ nextToken
542
+ }
543
+ }`,
544
+ variables
545
+ );
546
+ return result.data.listQuestionObjectByTopicIdAndLanguage_difficulty_createdAt;
547
+ } else {
548
+ const queryFilter = { ...filter };
549
+ if (hasDifficulty) {
550
+ queryFilter.difficulty = { eq: input.difficulty };
551
+ }
552
+ const variables = {
553
+ topicId: input.id,
554
+ sortDirection: input.sortDirection ?? "DESC",
555
+ limit: pageLimit,
556
+ nextToken,
557
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
558
+ };
559
+ const result = await this.network.query(
560
+ `query ListQuestionsByTopic($topicId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
561
+ listQuestionObjectByTopicIdAndCreatedAt(topicId: $topicId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
562
+ items {
563
+ id question imageUrl language level publicExam difficulty type voteCount
564
+ verificationStatus owner packId subjectId chapterId topicId createdAt
565
+ options { items { id content } }
566
+ }
567
+ nextToken
568
+ }
569
+ }`,
570
+ variables
571
+ );
572
+ return result.data.listQuestionObjectByTopicIdAndCreatedAt;
573
+ }
349
574
  };
350
- const result = await this.network.query(
351
- `query ListQuestionsByTopic($topicId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
352
- listQuestionObjectByTopicIdAndCreatedAt(topicId: $topicId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
353
- items {
354
- id question imageUrl language level difficulty type voteCount
355
- verificationStatus owner packId subjectId chapterId topicId createdAt
356
- options { items { id content } }
575
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
576
+ if (!hasPostFilter) {
577
+ return executeQuery(targetLimit, input.nextToken);
578
+ }
579
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
580
+ }
581
+ async listByPublicExam(input) {
582
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
583
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
584
+ const filter = {};
585
+ if (input.questionType && input.questionType !== "Both") {
586
+ filter.type = { eq: input.questionType };
587
+ }
588
+ const targetLimit = input.limit ?? 12;
589
+ const executeQuery = async (pageLimit, nextToken) => {
590
+ if (hasLanguage) {
591
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
592
+ const variables2 = {
593
+ publicExam: input.publicExam,
594
+ language_difficulty_createdAt: { beginsWith: rangeKey },
595
+ sortDirection: input.sortDirection ?? "DESC",
596
+ limit: pageLimit,
597
+ nextToken,
598
+ ...Object.keys(filter).length > 0 ? { filter } : {}
599
+ };
600
+ try {
601
+ const result2 = await this.network.query(
602
+ `query ListQuestionsByPublicExamAndLangDiff($publicExam: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
603
+ listQuestionObjectByPublicExamAndLanguage_difficulty_createdAt(publicExam: $publicExam, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
604
+ items {
605
+ id question imageUrl language level publicExam difficulty type voteCount
606
+ verificationStatus owner packId subjectId chapterId topicId createdAt
607
+ options { items { id content } }
608
+ }
609
+ nextToken
610
+ }
611
+ }`,
612
+ variables2
613
+ );
614
+ if (result2.data?.listQuestionObjectByPublicExamAndLanguage_difficulty_createdAt) {
615
+ return result2.data.listQuestionObjectByPublicExamAndLanguage_difficulty_createdAt;
357
616
  }
358
- nextToken
617
+ } catch {
359
618
  }
360
- }`,
361
- variables
362
- );
363
- return result.data.listQuestionObjectByTopicIdAndCreatedAt;
619
+ }
620
+ const fallbackFilter = { ...filter };
621
+ if (hasLanguage) {
622
+ fallbackFilter.language = { eq: input.language };
623
+ }
624
+ if (hasDifficulty) {
625
+ fallbackFilter.difficulty = { eq: input.difficulty };
626
+ }
627
+ const variables = {
628
+ publicExam: input.publicExam,
629
+ sortDirection: input.sortDirection ?? "DESC",
630
+ limit: pageLimit,
631
+ nextToken,
632
+ ...Object.keys(fallbackFilter).length > 0 ? { filter: fallbackFilter } : {}
633
+ };
634
+ const result = await this.network.query(
635
+ `query ListQuestionsByPublicExam($publicExam: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
636
+ listQuestionObjectByPublicExamAndCreatedAt(publicExam: $publicExam, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
637
+ items {
638
+ id question imageUrl language level publicExam difficulty type voteCount
639
+ verificationStatus owner packId subjectId chapterId topicId createdAt
640
+ options { items { id content } }
641
+ }
642
+ nextToken
643
+ }
644
+ }`,
645
+ variables
646
+ );
647
+ return result.data.listQuestionObjectByPublicExamAndCreatedAt;
648
+ };
649
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasLanguage || hasDifficulty;
650
+ if (!hasPostFilter) {
651
+ return executeQuery(targetLimit, input.nextToken);
652
+ }
653
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
364
654
  }
365
655
  async listByRandomHash(input) {
366
656
  const compositeKey = [input.language, input.difficulty].filter(Boolean).join("_");
@@ -374,7 +664,7 @@ var QuestionService = class {
374
664
  `query ListByRandomHash($subjectId: String!, $language_difficulty_randomHash: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int) {
375
665
  listQuestionObjectBySubjectIdAndLanguage_difficulty_randomHash(subjectId: $subjectId, language_difficulty_randomHash: $language_difficulty_randomHash, sortDirection: $sortDirection, limit: $limit) {
376
666
  items {
377
- id question imageUrl language level difficulty type voteCount
667
+ id question imageUrl language level publicExam difficulty type voteCount
378
668
  verificationStatus owner packId subjectId chapterId topicId createdAt
379
669
  options { items { id content } }
380
670
  }
@@ -385,6 +675,76 @@ var QuestionService = class {
385
675
  );
386
676
  return result.data.listQuestionObjectBySubjectIdAndLanguage_difficulty_randomHash;
387
677
  }
678
+ async listByLevel(input) {
679
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
680
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
681
+ const filter = {};
682
+ if (input.publicExam) {
683
+ filter.publicExam = { eq: input.publicExam };
684
+ }
685
+ if (input.questionType && input.questionType !== "Both") {
686
+ filter.type = { eq: input.questionType };
687
+ }
688
+ const targetLimit = input.limit ?? 12;
689
+ const executeQuery = async (pageLimit, nextToken) => {
690
+ if (hasLanguage) {
691
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
692
+ const variables = {
693
+ level: input.level,
694
+ language_difficulty_createdAt: { beginsWith: rangeKey },
695
+ sortDirection: input.sortDirection ?? "DESC",
696
+ limit: pageLimit,
697
+ nextToken,
698
+ ...Object.keys(filter).length > 0 ? { filter } : {}
699
+ };
700
+ const result = await this.network.query(
701
+ `query ListQuestionsByLevelAndLangDiff($level: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
702
+ listQuestionObjectByLevelAndLanguage_difficulty_createdAt(level: $level, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
703
+ items {
704
+ id question imageUrl language level publicExam difficulty type voteCount
705
+ verificationStatus owner packId subjectId chapterId topicId createdAt
706
+ options { items { id content } }
707
+ }
708
+ nextToken
709
+ }
710
+ }`,
711
+ variables
712
+ );
713
+ return result.data.listQuestionObjectByLevelAndLanguage_difficulty_createdAt;
714
+ } else {
715
+ const queryFilter = { ...filter };
716
+ if (hasDifficulty) {
717
+ queryFilter.difficulty = { eq: input.difficulty };
718
+ }
719
+ const variables = {
720
+ level: input.level,
721
+ sortDirection: input.sortDirection ?? "DESC",
722
+ limit: pageLimit,
723
+ nextToken,
724
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
725
+ };
726
+ const result = await this.network.query(
727
+ `query ListQuestionsByLevel($level: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelQuestionObjectFilterInput) {
728
+ listQuestionObjectByLevelAndCreatedAt(level: $level, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
729
+ items {
730
+ id question imageUrl language level publicExam difficulty type voteCount
731
+ verificationStatus owner packId subjectId chapterId topicId createdAt
732
+ options { items { id content } }
733
+ }
734
+ nextToken
735
+ }
736
+ }`,
737
+ variables
738
+ );
739
+ return result.data.listQuestionObjectByLevelAndCreatedAt;
740
+ }
741
+ };
742
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
743
+ if (!hasPostFilter) {
744
+ return executeQuery(targetLimit, input.nextToken);
745
+ }
746
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
747
+ }
388
748
  };
389
749
 
390
750
  // src/services/pack.service.ts
@@ -394,8 +754,8 @@ var PackService = class {
394
754
  }
395
755
  async create(input) {
396
756
  const result = await this.network.mutate(
397
- `mutation CreatePack($name: String!, $level: String!, $language: String!, $difficulty: String!, $automaticNumbering: Boolean, $subjectId: ID, $chapterId: ID, $topicId: ID) {
398
- createPackObject(name: $name, level: $level, language: $language, difficulty: $difficulty, automaticNumbering: $automaticNumbering, subjectId: $subjectId, chapterId: $chapterId, topicId: $topicId) {
757
+ `mutation CreatePack($name: String!, $level: String!, $publicExam: String, $language: String!, $difficulty: String!, $automaticNumbering: Boolean, $subjectId: ID, $chapterId: ID, $topicId: ID) {
758
+ createPackObject(name: $name, level: $level, publicExam: $publicExam, language: $language, difficulty: $difficulty, automaticNumbering: $automaticNumbering, subjectId: $subjectId, chapterId: $chapterId, topicId: $topicId) {
399
759
  status id
400
760
  }
401
761
  }`,
@@ -407,7 +767,7 @@ var PackService = class {
407
767
  const result = await this.network.query(
408
768
  `query GetPack($id: ID!) {
409
769
  getPack(id: $id) {
410
- id name language level difficulty questionCount automaticNumbering
770
+ id name language level publicExam difficulty questionCount automaticNumbering
411
771
  owner subjectId chapterId topicId packGroupId verificationStatus createdAt
412
772
  tags { items { id tagId } }
413
773
  user { id fullName avatarUrl }
@@ -421,13 +781,13 @@ var PackService = class {
421
781
  const result = await this.network.query(
422
782
  `query GetFullPack($id: ID!) {
423
783
  getPack(id: $id) {
424
- id name language level difficulty questionCount automaticNumbering
784
+ id name language level publicExam difficulty questionCount automaticNumbering
425
785
  owner subjectId chapterId topicId packGroupId verificationStatus createdAt
426
786
  tags { items { id tagId } }
427
787
  user { id fullName avatarUrl }
428
788
  questions(sortDirection: ASC, limit: 200) {
429
789
  items {
430
- id question answer explanation extra imageUrl language level difficulty type
790
+ id question answer explanation extra imageUrl language level publicExam difficulty type
431
791
  markingInstructions voteCount owner order createdAt
432
792
  options { items { id content } }
433
793
  tags { items { id tagId } }
@@ -440,6 +800,11 @@ var PackService = class {
440
800
  const pack = result.data.getPack;
441
801
  if (pack && pack.questions && Array.isArray(pack.questions.items)) {
442
802
  pack.questions.items.sort((a, b) => {
803
+ const isWrittenA = a.type === "WRITTEN" ? 1 : 0;
804
+ const isWrittenB = b.type === "WRITTEN" ? 1 : 0;
805
+ if (isWrittenA !== isWrittenB) {
806
+ return isWrittenA - isWrittenB;
807
+ }
443
808
  const orderA = typeof a.order === "number" ? a.order : a.order ? Number(a.order) : 0;
444
809
  const orderB = typeof b.order === "number" ? b.order : b.order ? Number(b.order) : 0;
445
810
  if (orderA !== orderB) {
@@ -458,7 +823,7 @@ var PackService = class {
458
823
  const result = await this.network.mutate(
459
824
  `mutation UpdatePack($input: UpdatePackInput!) {
460
825
  updatePack(input: $input) {
461
- id name language level difficulty questionCount automaticNumbering
826
+ id name language level publicExam difficulty questionCount automaticNumbering
462
827
  owner subjectId chapterId topicId verificationStatus createdAt
463
828
  }
464
829
  }`,
@@ -468,12 +833,15 @@ var PackService = class {
468
833
  }
469
834
  async delete(id) {
470
835
  const result = await this.network.mutate(
471
- `mutation DeletePack($input: DeletePackInput!) {
472
- deletePack(input: $input) { id }
836
+ `mutation DeletePackCascade($packId: ID!) {
837
+ deletePackCascade(packId: $packId) { status }
473
838
  }`,
474
- { input: { id } }
839
+ { packId: id }
475
840
  );
476
- return result.data.deletePack;
841
+ if (result.data.deletePackCascade?.status === "success") {
842
+ return { id };
843
+ }
844
+ return null;
477
845
  }
478
846
  async verify(id, userId) {
479
847
  const result = await this.network.mutate(
@@ -493,7 +861,27 @@ var PackService = class {
493
861
  );
494
862
  return result.data.createPackReport;
495
863
  }
496
- async submitExam(packId, selectedOptions) {
864
+ async submitExam(packId, selectedOptions, options) {
865
+ if (options?.functionUrl) {
866
+ try {
867
+ const headers = { "Content-Type": "application/json" };
868
+ if (options.authToken) {
869
+ headers["Authorization"] = `Bearer ${options.authToken}`;
870
+ }
871
+ const response = await fetch(options.functionUrl, {
872
+ method: "POST",
873
+ headers,
874
+ body: JSON.stringify({ packId, selectedOptions: JSON.stringify(selectedOptions) })
875
+ });
876
+ if (response.ok) {
877
+ const data = await response.json();
878
+ return data;
879
+ }
880
+ console.warn(`[PackService] Function URL failed with status ${response.status}, falling back to GraphQL...`);
881
+ } catch (err) {
882
+ console.warn("[PackService] Function URL fetch error, falling back to GraphQL...", err);
883
+ }
884
+ }
497
885
  const result = await this.network.mutate(
498
886
  `mutation SubmitPackExam($packId: ID!, $selectedOptions: String!) {
499
887
  submitPackExam(packId: $packId, selectedOptions: $selectedOptions) {
@@ -510,7 +898,7 @@ var PackService = class {
510
898
  listPackExamResultsByOwnerAndCreatedAt(owner: $owner, sortDirection: $sortDirection, nextToken: $nextToken) {
511
899
  items {
512
900
  id packId selectedOptions aiEvaluations owner createdAt
513
- pack { id name language level difficulty questionCount }
901
+ pack { id name language level publicExam difficulty questionCount }
514
902
  }
515
903
  nextToken
516
904
  }
@@ -524,7 +912,7 @@ var PackService = class {
524
912
  `query GetPackExamResult($id: ID!) {
525
913
  getPackExamResults(id: $id) {
526
914
  id packId selectedOptions aiEvaluations owner createdAt
527
- pack { id name language level difficulty questionCount }
915
+ pack { id name language level publicExam difficulty questionCount }
528
916
  }
529
917
  }`,
530
918
  { id: resultId }
@@ -532,79 +920,462 @@ var PackService = class {
532
920
  return result.data.getPackExamResults;
533
921
  }
534
922
  async listBySubject(input) {
535
- const result = await this.network.query(
536
- `query ListPacksBySubject($subjectId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
537
- listPackBySubjectIdAndCreatedAt(subjectId: $subjectId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
538
- items {
539
- id name language level difficulty questionCount owner createdAt verificationStatus
540
- tags { items { id tagId } }
541
- }
542
- nextToken
923
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
924
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
925
+ const filter = {};
926
+ if (input.publicExam) {
927
+ filter.publicExam = { eq: input.publicExam };
928
+ }
929
+ const targetLimit = input.limit ?? 12;
930
+ const executeQuery = async (pageLimit, nextToken) => {
931
+ if (hasLanguage) {
932
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
933
+ const variables = {
934
+ subjectId: input.id,
935
+ language_difficulty_createdAt: { beginsWith: rangeKey },
936
+ sortDirection: input.sortDirection ?? "DESC",
937
+ limit: pageLimit,
938
+ nextToken,
939
+ ...Object.keys(filter).length > 0 ? { filter } : {}
940
+ };
941
+ const result = await this.network.query(
942
+ `query ListPacksBySubjectAndLangDiff($subjectId: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
943
+ listPackBySubjectIdAndLanguage_difficulty_createdAt(subjectId: $subjectId, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
944
+ items {
945
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
946
+ tags { items { id tagId } }
947
+ }
948
+ nextToken
949
+ }
950
+ }`,
951
+ variables
952
+ );
953
+ return result.data.listPackBySubjectIdAndLanguage_difficulty_createdAt;
954
+ } else {
955
+ const queryFilter = { ...filter };
956
+ if (hasDifficulty) {
957
+ queryFilter.difficulty = { eq: input.difficulty };
543
958
  }
544
- }`,
545
- { subjectId: input.id, sortDirection: "DESC", limit: input.limit ?? 20, nextToken: input.nextToken }
546
- );
547
- return result.data.listPackBySubjectIdAndCreatedAt;
959
+ const variables = {
960
+ subjectId: input.id,
961
+ sortDirection: input.sortDirection ?? "DESC",
962
+ limit: pageLimit,
963
+ nextToken,
964
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
965
+ };
966
+ const result = await this.network.query(
967
+ `query ListPacksBySubject($subjectId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
968
+ listPackBySubjectIdAndCreatedAt(subjectId: $subjectId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
969
+ items {
970
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
971
+ tags { items { id tagId } }
972
+ }
973
+ nextToken
974
+ }
975
+ }`,
976
+ variables
977
+ );
978
+ return result.data.listPackBySubjectIdAndCreatedAt;
979
+ }
980
+ };
981
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
982
+ if (!hasPostFilter) {
983
+ return executeQuery(targetLimit, input.nextToken);
984
+ }
985
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
548
986
  }
549
987
  async listByChapter(input) {
550
- const result = await this.network.query(
551
- `query ListPacksByChapter($chapterId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
552
- listPackByChapterIdAndCreatedAt(chapterId: $chapterId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
553
- items {
554
- id name language level difficulty questionCount owner createdAt verificationStatus
555
- tags { items { id tagId } }
556
- }
557
- nextToken
988
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
989
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
990
+ const filter = {};
991
+ if (input.publicExam) {
992
+ filter.publicExam = { eq: input.publicExam };
993
+ }
994
+ const targetLimit = input.limit ?? 12;
995
+ const executeQuery = async (pageLimit, nextToken) => {
996
+ if (hasLanguage) {
997
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
998
+ const variables = {
999
+ chapterId: input.id,
1000
+ language_difficulty_createdAt: { beginsWith: rangeKey },
1001
+ sortDirection: input.sortDirection ?? "DESC",
1002
+ limit: pageLimit,
1003
+ nextToken,
1004
+ ...Object.keys(filter).length > 0 ? { filter } : {}
1005
+ };
1006
+ const result = await this.network.query(
1007
+ `query ListPacksByChapterAndLangDiff($chapterId: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1008
+ listPackByChapterIdAndLanguage_difficulty_createdAt(chapterId: $chapterId, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1009
+ items {
1010
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1011
+ tags { items { id tagId } }
1012
+ }
1013
+ nextToken
1014
+ }
1015
+ }`,
1016
+ variables
1017
+ );
1018
+ return result.data.listPackByChapterIdAndLanguage_difficulty_createdAt;
1019
+ } else {
1020
+ const queryFilter = { ...filter };
1021
+ if (hasDifficulty) {
1022
+ queryFilter.difficulty = { eq: input.difficulty };
558
1023
  }
559
- }`,
560
- { chapterId: input.id, sortDirection: "DESC", limit: input.limit ?? 20, nextToken: input.nextToken }
561
- );
562
- return result.data.listPackByChapterIdAndCreatedAt;
1024
+ const variables = {
1025
+ chapterId: input.id,
1026
+ sortDirection: input.sortDirection ?? "DESC",
1027
+ limit: pageLimit,
1028
+ nextToken,
1029
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
1030
+ };
1031
+ const result = await this.network.query(
1032
+ `query ListPacksByChapter($chapterId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1033
+ listPackByChapterIdAndCreatedAt(chapterId: $chapterId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1034
+ items {
1035
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1036
+ tags { items { id tagId } }
1037
+ }
1038
+ nextToken
1039
+ }
1040
+ }`,
1041
+ variables
1042
+ );
1043
+ return result.data.listPackByChapterIdAndCreatedAt;
1044
+ }
1045
+ };
1046
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
1047
+ if (!hasPostFilter) {
1048
+ return executeQuery(targetLimit, input.nextToken);
1049
+ }
1050
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
563
1051
  }
564
1052
  async listByTopic(input) {
565
- const result = await this.network.query(
566
- `query ListPacksByTopic($topicId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
567
- listPackByTopicIdAndCreatedAt(topicId: $topicId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
568
- items {
569
- id name language level difficulty questionCount owner createdAt verificationStatus
570
- tags { items { id tagId } }
1053
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
1054
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
1055
+ const filter = {};
1056
+ if (input.publicExam) {
1057
+ filter.publicExam = { eq: input.publicExam };
1058
+ }
1059
+ const targetLimit = input.limit ?? 12;
1060
+ const executeQuery = async (pageLimit, nextToken) => {
1061
+ if (hasLanguage) {
1062
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
1063
+ const variables = {
1064
+ topicId: input.id,
1065
+ language_difficulty_createdAt: { beginsWith: rangeKey },
1066
+ sortDirection: input.sortDirection ?? "DESC",
1067
+ limit: pageLimit,
1068
+ nextToken,
1069
+ ...Object.keys(filter).length > 0 ? { filter } : {}
1070
+ };
1071
+ const result = await this.network.query(
1072
+ `query ListPacksByTopicAndLangDiff($topicId: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1073
+ listPackByTopicIdAndLanguage_difficulty_createdAt(topicId: $topicId, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1074
+ items {
1075
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1076
+ tags { items { id tagId } }
1077
+ }
1078
+ nextToken
1079
+ }
1080
+ }`,
1081
+ variables
1082
+ );
1083
+ return result.data.listPackByTopicIdAndLanguage_difficulty_createdAt;
1084
+ } else {
1085
+ const queryFilter = { ...filter };
1086
+ if (hasDifficulty) {
1087
+ queryFilter.difficulty = { eq: input.difficulty };
1088
+ }
1089
+ const variables = {
1090
+ topicId: input.id,
1091
+ sortDirection: input.sortDirection ?? "DESC",
1092
+ limit: pageLimit,
1093
+ nextToken,
1094
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
1095
+ };
1096
+ const result = await this.network.query(
1097
+ `query ListPacksByTopic($topicId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1098
+ listPackByTopicIdAndCreatedAt(topicId: $topicId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1099
+ items {
1100
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1101
+ tags { items { id tagId } }
1102
+ }
1103
+ nextToken
1104
+ }
1105
+ }`,
1106
+ variables
1107
+ );
1108
+ return result.data.listPackByTopicIdAndCreatedAt;
1109
+ }
1110
+ };
1111
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
1112
+ if (!hasPostFilter) {
1113
+ return executeQuery(targetLimit, input.nextToken);
1114
+ }
1115
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
1116
+ }
1117
+ async listByPublicExam(input) {
1118
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
1119
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
1120
+ const filter = {};
1121
+ const targetLimit = input.limit ?? 12;
1122
+ const executeQuery = async (pageLimit, nextToken) => {
1123
+ if (hasLanguage) {
1124
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
1125
+ const variables = {
1126
+ publicExam: input.publicExam,
1127
+ language_difficulty_createdAt: { beginsWith: rangeKey },
1128
+ sortDirection: input.sortDirection ?? "DESC",
1129
+ limit: pageLimit,
1130
+ nextToken,
1131
+ ...Object.keys(filter).length > 0 ? { filter } : {}
1132
+ };
1133
+ try {
1134
+ const result2 = await this.network.query(
1135
+ `query ListPacksByPublicExamAndLangDiff($publicExam: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1136
+ listPackByPublicExamAndLanguage_difficulty_createdAt(publicExam: $publicExam, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1137
+ items {
1138
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1139
+ tags { items { id tagId } }
1140
+ }
1141
+ nextToken
1142
+ }
1143
+ }`,
1144
+ variables
1145
+ );
1146
+ if (result2.data?.listPackByPublicExamAndLanguage_difficulty_createdAt) {
1147
+ return result2.data.listPackByPublicExamAndLanguage_difficulty_createdAt;
571
1148
  }
572
- nextToken
1149
+ } catch {
573
1150
  }
574
- }`,
575
- { topicId: input.id, sortDirection: "DESC", limit: input.limit ?? 20, nextToken: input.nextToken }
576
- );
577
- return result.data.listPackByTopicIdAndCreatedAt;
578
- }
579
- async listGroups(type, level) {
580
- const queryField = level ? "listPackGroupByTypeLevelAndOrder" : "listPackGroupByTypeAndOrder";
581
- const variables = level ? { typeLevel: `${type}_${level}`, sortDirection: "ASC" } : { type, sortDirection: "ASC" };
582
- const result = await this.network.query(
583
- level ? `query ListPackGroups($typeLevel: String!, $sortDirection: ModelSortDirection) {
584
- listPackGroupByTypeLevelAndOrder(typeLevel: $typeLevel, sortDirection: $sortDirection) {
585
- items { id name description imageUrl type level typeLevel packCount order }
1151
+ }
1152
+ const fallbackFilter = { ...filter };
1153
+ if (hasLanguage) {
1154
+ fallbackFilter.language = { eq: input.language };
1155
+ }
1156
+ if (hasDifficulty) {
1157
+ fallbackFilter.difficulty = { eq: input.difficulty };
1158
+ }
1159
+ try {
1160
+ const variables = {
1161
+ publicExam: input.publicExam,
1162
+ sortDirection: input.sortDirection ?? "DESC",
1163
+ limit: pageLimit,
1164
+ nextToken,
1165
+ ...Object.keys(fallbackFilter).length > 0 ? { filter: fallbackFilter } : {}
1166
+ };
1167
+ const result2 = await this.network.query(
1168
+ `query ListPacksByPublicExam($publicExam: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1169
+ listPackByPublicExamAndCreatedAt(publicExam: $publicExam, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1170
+ items {
1171
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1172
+ tags { items { id tagId } }
1173
+ }
1174
+ nextToken
586
1175
  }
587
- }` : `query ListPackGroups($type: String!, $sortDirection: ModelSortDirection) {
588
- listPackGroupByTypeAndOrder(type: $type, sortDirection: $sortDirection) {
589
- items { id name description imageUrl type level typeLevel packCount order }
1176
+ }`,
1177
+ variables
1178
+ );
1179
+ if (result2.data?.listPackByPublicExamAndCreatedAt) {
1180
+ return result2.data.listPackByPublicExamAndCreatedAt;
1181
+ }
1182
+ } catch {
1183
+ }
1184
+ const scanFilter = {
1185
+ publicExam: { eq: input.publicExam },
1186
+ ...fallbackFilter
1187
+ };
1188
+ const result = await this.network.query(
1189
+ `query ListPacksFallback($filter: ModelPackFilterInput, $limit: Int, $nextToken: String) {
1190
+ listPacks(filter: $filter, limit: $limit, nextToken: $nextToken) {
1191
+ items {
1192
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1193
+ tags { items { id tagId } }
1194
+ }
1195
+ nextToken
1196
+ }
1197
+ }`,
1198
+ { filter: scanFilter, limit: pageLimit, nextToken }
1199
+ );
1200
+ return result.data.listPacks;
1201
+ };
1202
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasLanguage || hasDifficulty;
1203
+ if (!hasPostFilter) {
1204
+ return executeQuery(targetLimit, input.nextToken);
1205
+ }
1206
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
1207
+ }
1208
+ async listByLevel(input) {
1209
+ const hasLanguage = Boolean(input.language && input.language !== "All" && input.language !== "Any");
1210
+ const hasDifficulty = Boolean(input.difficulty && input.difficulty !== "All");
1211
+ const filter = {};
1212
+ if (input.publicExam) {
1213
+ filter.publicExam = { eq: input.publicExam };
1214
+ }
1215
+ const targetLimit = input.limit ?? 12;
1216
+ const executeQuery = async (pageLimit, nextToken) => {
1217
+ if (hasLanguage) {
1218
+ const rangeKey = hasDifficulty ? `${input.language}#${input.difficulty}#` : `${input.language}#`;
1219
+ const variables = {
1220
+ level: input.level,
1221
+ language_difficulty_createdAt: { beginsWith: rangeKey },
1222
+ sortDirection: input.sortDirection ?? "DESC",
1223
+ limit: pageLimit,
1224
+ nextToken,
1225
+ ...Object.keys(filter).length > 0 ? { filter } : {}
1226
+ };
1227
+ const result = await this.network.query(
1228
+ `query ListPacksByLevelAndLangDiff($level: String!, $language_difficulty_createdAt: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1229
+ listPackByLevelAndLanguage_difficulty_createdAt(level: $level, language_difficulty_createdAt: $language_difficulty_createdAt, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1230
+ items {
1231
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1232
+ tags { items { id tagId } }
1233
+ }
1234
+ nextToken
590
1235
  }
591
1236
  }`,
592
- variables
1237
+ variables
1238
+ );
1239
+ return result.data.listPackByLevelAndLanguage_difficulty_createdAt;
1240
+ } else {
1241
+ const queryFilter = { ...filter };
1242
+ if (hasDifficulty) {
1243
+ queryFilter.difficulty = { eq: input.difficulty };
1244
+ }
1245
+ const variables = {
1246
+ level: input.level,
1247
+ sortDirection: input.sortDirection ?? "DESC",
1248
+ limit: pageLimit,
1249
+ nextToken,
1250
+ ...Object.keys(queryFilter).length > 0 ? { filter: queryFilter } : {}
1251
+ };
1252
+ const result = await this.network.query(
1253
+ `query ListPacksByLevel($level: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String, $filter: ModelPackFilterInput) {
1254
+ listPackByLevelAndCreatedAt(level: $level, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken, filter: $filter) {
1255
+ items {
1256
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus subjectId chapterId topicId
1257
+ tags { items { id tagId } }
1258
+ }
1259
+ nextToken
1260
+ }
1261
+ }`,
1262
+ variables
1263
+ );
1264
+ return result.data.listPackByLevelAndCreatedAt;
1265
+ }
1266
+ };
1267
+ const hasPostFilter = hasLanguage ? Object.keys(filter).length > 0 : Object.keys(filter).length > 0 || hasDifficulty;
1268
+ if (!hasPostFilter) {
1269
+ return executeQuery(targetLimit, input.nextToken);
1270
+ }
1271
+ return paginateWithAccumulator(executeQuery, targetLimit, input.nextToken);
1272
+ }
1273
+ async createGroup(input) {
1274
+ const typeLevel = input.type && input.level ? `${input.type}_${input.level}` : void 0;
1275
+ const mutationInput = {
1276
+ name: input.name,
1277
+ description: input.description,
1278
+ imageUrl: input.imageUrl,
1279
+ type: input.type,
1280
+ level: input.level,
1281
+ typeLevel,
1282
+ order: input.order
1283
+ };
1284
+ const result = await this.network.mutate(
1285
+ `mutation CreatePackGroup($input: CreatePackGroupInput!) {
1286
+ createPackGroup(input: $input) {
1287
+ id name description imageUrl type level typeLevel packCount order
1288
+ }
1289
+ }`,
1290
+ { input: mutationInput }
593
1291
  );
594
- return result.data[queryField].items;
1292
+ const created = result.data.createPackGroup;
1293
+ if (created && input.packIds && input.packIds.length > 0) {
1294
+ await Promise.all(
1295
+ input.packIds.map((pId) => this.update(pId, { packGroupId: created.id }))
1296
+ );
1297
+ }
1298
+ return created;
1299
+ }
1300
+ async deleteGroup(id) {
1301
+ const result = await this.network.mutate(
1302
+ `mutation DeletePackGroup($input: DeletePackGroupInput!) {
1303
+ deletePackGroup(input: $input) {
1304
+ id
1305
+ }
1306
+ }`,
1307
+ { input: { id } }
1308
+ );
1309
+ return result.data.deletePackGroup;
1310
+ }
1311
+ async listGroups(typeOrInput, level) {
1312
+ let type;
1313
+ let targetLevel;
1314
+ let limit;
1315
+ let nextToken;
1316
+ let sortDirection = "ASC";
1317
+ const isLegacyCall = typeof typeOrInput === "string";
1318
+ if (isLegacyCall) {
1319
+ type = typeOrInput;
1320
+ targetLevel = level;
1321
+ } else {
1322
+ type = typeOrInput.type ?? "QB";
1323
+ targetLevel = typeOrInput.level;
1324
+ limit = typeOrInput.limit;
1325
+ nextToken = typeOrInput.nextToken;
1326
+ sortDirection = typeOrInput.sortDirection ?? "ASC";
1327
+ }
1328
+ const queryField = targetLevel ? "listPackGroupByTypeLevelAndOrder" : "listPackGroupByTypeAndOrder";
1329
+ const variables = isLegacyCall ? targetLevel ? { typeLevel: `${type}_${targetLevel}`, sortDirection } : { type, sortDirection } : targetLevel ? { typeLevel: `${type}_${targetLevel}`, sortDirection, limit: limit ?? 20, nextToken } : { type, sortDirection, limit: limit ?? 20, nextToken };
1330
+ const queryString = isLegacyCall ? targetLevel ? `query ListPackGroups($typeLevel: String!, $sortDirection: ModelSortDirection) {
1331
+ listPackGroupByTypeLevelAndOrder(typeLevel: $typeLevel, sortDirection: $sortDirection) {
1332
+ items { id name description imageUrl type level typeLevel packCount order }
1333
+ }
1334
+ }` : `query ListPackGroups($type: String!, $sortDirection: ModelSortDirection) {
1335
+ listPackGroupByTypeAndOrder(type: $type, sortDirection: $sortDirection) {
1336
+ items { id name description imageUrl type level typeLevel packCount order }
1337
+ }
1338
+ }` : targetLevel ? `query ListPackGroupsByLevel($typeLevel: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
1339
+ listPackGroupByTypeLevelAndOrder(typeLevel: $typeLevel, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
1340
+ items { id name description imageUrl type level typeLevel packCount order }
1341
+ nextToken
1342
+ }
1343
+ }` : `query ListPackGroupsByType($type: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
1344
+ listPackGroupByTypeAndOrder(type: $type, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
1345
+ items { id name description imageUrl type level typeLevel packCount order }
1346
+ nextToken
1347
+ }
1348
+ }`;
1349
+ const result = await this.network.query(queryString, variables);
1350
+ const paginated = result.data[queryField];
1351
+ return isLegacyCall ? paginated.items : paginated;
595
1352
  }
596
1353
  async getGroup(id) {
597
1354
  const result = await this.network.query(
598
1355
  `query GetPackGroup($id: ID!) {
599
1356
  getPackGroup(id: $id) {
600
1357
  id name description imageUrl type level typeLevel packCount order
601
- packs { items { id name language level difficulty questionCount owner createdAt verificationStatus } }
1358
+ packs { items { id name language level publicExam difficulty questionCount owner createdAt verificationStatus } }
602
1359
  }
603
1360
  }`,
604
1361
  { id }
605
1362
  );
606
1363
  return result.data.getPackGroup;
607
1364
  }
1365
+ async list(input) {
1366
+ const result = await this.network.query(
1367
+ `query ListPacks($limit: Int, $nextToken: String) {
1368
+ listPacks(limit: $limit, nextToken: $nextToken) {
1369
+ items {
1370
+ id name language level publicExam difficulty questionCount owner createdAt verificationStatus packGroupId
1371
+ }
1372
+ nextToken
1373
+ }
1374
+ }`,
1375
+ { limit: input?.limit ?? 20, nextToken: input?.nextToken }
1376
+ );
1377
+ return result.data.listPacks;
1378
+ }
608
1379
  };
609
1380
 
610
1381
  // src/services/contest.service.ts
@@ -962,6 +1733,28 @@ var CourseService = class {
962
1733
  );
963
1734
  return result.data.publishCourseLiveExamResults;
964
1735
  }
1736
+ async uploadCourseItem(input) {
1737
+ const signed = await this.getSignedURL({
1738
+ courseId: input.courseId,
1739
+ fileName: input.fileName,
1740
+ fileType: input.fileType,
1741
+ parentId: input.parentId ?? void 0
1742
+ });
1743
+ if (signed && signed.signedURL && input.fileData) {
1744
+ await fetch(signed.signedURL, {
1745
+ method: "PUT",
1746
+ body: input.fileData,
1747
+ headers: { "Content-Type": input.fileType }
1748
+ });
1749
+ }
1750
+ return this.createItem({
1751
+ courseId: input.courseId,
1752
+ name: input.itemName || input.fileName,
1753
+ order: input.itemOrder,
1754
+ type: "file",
1755
+ parentId: input.parentId ?? void 0
1756
+ });
1757
+ }
965
1758
  };
966
1759
 
967
1760
  // src/services/curriculum.service.ts
@@ -1029,6 +1822,67 @@ var CurriculumService = class {
1029
1822
  );
1030
1823
  return result.data.listTopicByChapterId.items;
1031
1824
  }
1825
+ async recalculateQuestionCounts() {
1826
+ let nextToken = null;
1827
+ const counts = {};
1828
+ do {
1829
+ const res = await this.network.query(
1830
+ `query ListQuestionsForRecalc($nextToken: String) {
1831
+ listQuestionObjects(limit: 1000, nextToken: $nextToken) {
1832
+ items { id subjectId chapterId topicId }
1833
+ nextToken
1834
+ }
1835
+ }`,
1836
+ { nextToken }
1837
+ );
1838
+ const items = res.data?.listQuestionObjects?.items || [];
1839
+ for (const q of items) {
1840
+ if (q.subjectId) counts[q.subjectId] = (counts[q.subjectId] || 0) + 1;
1841
+ }
1842
+ nextToken = res.data?.listQuestionObjects?.nextToken ?? null;
1843
+ } while (nextToken);
1844
+ const [hscSubjects, sscSubjects] = await Promise.all([
1845
+ this.getSubjects("HSC").catch(() => []),
1846
+ this.getSubjects("SSC").catch(() => [])
1847
+ ]);
1848
+ const allSubjects = [...hscSubjects, ...sscSubjects];
1849
+ for (const s of allSubjects) {
1850
+ await this.network.mutate(
1851
+ `mutation UpdateSubjectCount($input: UpdateSubjectInput!) {
1852
+ updateSubject(input: $input) { id }
1853
+ }`,
1854
+ { input: { id: s.id, questionCount: counts[s.id] || 0 } }
1855
+ );
1856
+ }
1857
+ }
1858
+ async recalculateCreatorStats() {
1859
+ let nextToken = null;
1860
+ const userQuestionCounts = {};
1861
+ do {
1862
+ const res = await this.network.query(
1863
+ `query ListQuestionsForCreatorStats($nextToken: String) {
1864
+ listQuestionObjects(limit: 1000, nextToken: $nextToken) {
1865
+ items { id owner }
1866
+ nextToken
1867
+ }
1868
+ }`,
1869
+ { nextToken }
1870
+ );
1871
+ const items = res.data?.listQuestionObjects?.items || [];
1872
+ for (const q of items) {
1873
+ if (q.owner) userQuestionCounts[q.owner] = (userQuestionCounts[q.owner] || 0) + 1;
1874
+ }
1875
+ nextToken = res.data?.listQuestionObjects?.nextToken ?? null;
1876
+ } while (nextToken);
1877
+ for (const [userId, count] of Object.entries(userQuestionCounts)) {
1878
+ await this.network.mutate(
1879
+ `mutation UpdateCreatorStats($input: UpdateUserInput!) {
1880
+ updateUser(input: $input) { id }
1881
+ }`,
1882
+ { input: { id: userId, questionCount: count } }
1883
+ );
1884
+ }
1885
+ }
1032
1886
  };
1033
1887
 
1034
1888
  // src/services/media.service.ts
@@ -1207,7 +2061,7 @@ var ProgressService = class {
1207
2061
  return [];
1208
2062
  }
1209
2063
  }
1210
- async getMistakenQuestions(userId) {
2064
+ async getMistakenQuestions(userId, status = "MISTAKE") {
1211
2065
  try {
1212
2066
  const result = await this.network.query(
1213
2067
  `query ListMistakes($userId: String!, $filter: ModelQuestionTrackingFilterInput) {
@@ -1215,7 +2069,7 @@ var ProgressService = class {
1215
2069
  items { id userId questionId subjectId chapterId topicId status attempts }
1216
2070
  }
1217
2071
  }`,
1218
- { userId, filter: { status: { eq: "MISTAKE" } } }
2072
+ { userId, filter: { status: { eq: status } } }
1219
2073
  );
1220
2074
  return result.data?.listQuestionTrackingByUserIdAndQuestionId?.items || result.data?.listQuestionTrackingsByUserIdAndQuestionId?.items || result.data?.listQuestionTrackings?.items || [];
1221
2075
  } catch (e) {
@@ -1223,6 +2077,9 @@ var ProgressService = class {
1223
2077
  return [];
1224
2078
  }
1225
2079
  }
2080
+ async getCorrectedQuestions(userId) {
2081
+ return this.getMistakenQuestions(userId, "CORRECTED");
2082
+ }
1226
2083
  };
1227
2084
 
1228
2085
  // src/services/ai.service.ts
@@ -1230,7 +2087,27 @@ var AIService = class {
1230
2087
  constructor(network) {
1231
2088
  this.network = network;
1232
2089
  }
1233
- async evaluateWrittenAnswer(input) {
2090
+ async evaluateWrittenAnswer(input, options) {
2091
+ if (options?.functionUrl) {
2092
+ try {
2093
+ const headers = { "Content-Type": "application/json" };
2094
+ if (options.authToken) {
2095
+ headers["Authorization"] = `Bearer ${options.authToken}`;
2096
+ }
2097
+ const response = await fetch(options.functionUrl, {
2098
+ method: "POST",
2099
+ headers,
2100
+ body: JSON.stringify(input)
2101
+ });
2102
+ if (response.ok) {
2103
+ const data = await response.json();
2104
+ return data;
2105
+ }
2106
+ console.warn(`[AIService] Function URL failed with status ${response.status}, falling back to GraphQL...`);
2107
+ } catch (err) {
2108
+ console.warn("[AIService] Function URL fetch error, falling back to GraphQL...", err);
2109
+ }
2110
+ }
1234
2111
  const result = await this.network.query(
1235
2112
  `query EvaluateWrittenAnswer($question: String!, $userAnswer: String!, $correctAnswer: String!, $markingInstructions: String, $topicId: String, $topicName: String, $questionImageUrl: String) {
1236
2113
  evaluateWrittenAnswer(question: $question, userAnswer: $userAnswer, correctAnswer: $correctAnswer, markingInstructions: $markingInstructions, topicId: $topicId, topicName: $topicName, questionImageUrl: $questionImageUrl) {
@@ -1280,10 +2157,256 @@ var NewsService = class {
1280
2157
  }
1281
2158
  };
1282
2159
 
2160
+ // src/services/notification.service.ts
2161
+ var NotificationService = class {
2162
+ constructor(network) {
2163
+ this.network = network;
2164
+ }
2165
+ /**
2166
+ * List broadcast announcements (ordered by category and createdAt).
2167
+ */
2168
+ async listAnnouncements(limit = 20, nextToken) {
2169
+ const result = await this.network.query(
2170
+ `query ListAnnouncements($limit: Int, $nextToken: String) {
2171
+ listAnnouncements(limit: $limit, nextToken: $nextToken) {
2172
+ items {
2173
+ id title message senderType senderName senderAvatar category actionUrl priority createdAt updatedAt
2174
+ }
2175
+ nextToken
2176
+ }
2177
+ }`,
2178
+ { limit, nextToken }
2179
+ );
2180
+ return result.data.listAnnouncements ?? { items: [], nextToken: null };
2181
+ }
2182
+ /**
2183
+ * List announcements by category.
2184
+ */
2185
+ async listAnnouncementsByCategory(category = "ANNOUNCEMENT", limit = 20, nextToken) {
2186
+ const result = await this.network.query(
2187
+ `query ListAnnouncementsByCategory($category: String!, $limit: Int, $nextToken: String, $sortDirection: ModelSortDirection) {
2188
+ listAnnouncementByCategoryAndCreatedAt(category: $category, limit: $limit, nextToken: $nextToken, sortDirection: $sortDirection) {
2189
+ items {
2190
+ id title message senderType senderName senderAvatar category actionUrl priority createdAt updatedAt
2191
+ }
2192
+ nextToken
2193
+ }
2194
+ }`,
2195
+ { category, limit, nextToken, sortDirection: "DESC" }
2196
+ );
2197
+ return result.data.listAnnouncementByCategoryAndCreatedAt ?? {
2198
+ items: [],
2199
+ nextToken: null
2200
+ };
2201
+ }
2202
+ /**
2203
+ * Create an announcement (Admins only).
2204
+ */
2205
+ async createAnnouncement(input) {
2206
+ const result = await this.network.mutate(
2207
+ `mutation CreateAnnouncement($input: CreateAnnouncementInput!) {
2208
+ createAnnouncement(input: $input) {
2209
+ id title message senderType senderName senderAvatar category actionUrl priority createdAt updatedAt
2210
+ }
2211
+ }`,
2212
+ {
2213
+ input: {
2214
+ ...input,
2215
+ senderType: input.senderType || "ORJOK",
2216
+ category: input.category || "ANNOUNCEMENT",
2217
+ priority: input.priority || "NORMAL"
2218
+ }
2219
+ }
2220
+ );
2221
+ return result.data.createAnnouncement;
2222
+ }
2223
+ /**
2224
+ * Delete an announcement (Admins only).
2225
+ */
2226
+ async deleteAnnouncement(id) {
2227
+ const result = await this.network.mutate(
2228
+ `mutation DeleteAnnouncement($input: DeleteAnnouncementInput!) {
2229
+ deleteAnnouncement(input: $input) { id }
2230
+ }`,
2231
+ { input: { id } }
2232
+ );
2233
+ return Boolean(result.data.deleteAnnouncement?.id);
2234
+ }
2235
+ /**
2236
+ * List user notifications for a specific user ID.
2237
+ */
2238
+ async listUserNotifications(userId, limit = 20, nextToken) {
2239
+ const result = await this.network.query(
2240
+ `query ListUserNotifications($userId: ID!, $limit: Int, $nextToken: String, $sortDirection: ModelSortDirection) {
2241
+ listUserNotificationByUserIdAndCreatedAt(userId: $userId, limit: $limit, nextToken: $nextToken, sortDirection: $sortDirection) {
2242
+ items {
2243
+ id userId title message type senderType senderName senderAvatar actionUrl isRead readAt metadata createdAt updatedAt
2244
+ }
2245
+ nextToken
2246
+ }
2247
+ }`,
2248
+ { userId, limit, nextToken, sortDirection: "DESC" }
2249
+ );
2250
+ return result.data.listUserNotificationByUserIdAndCreatedAt ?? {
2251
+ items: [],
2252
+ nextToken: null
2253
+ };
2254
+ }
2255
+ /**
2256
+ * Create a user notification (Level up, direct message, system alert).
2257
+ */
2258
+ async createUserNotification(input) {
2259
+ const result = await this.network.mutate(
2260
+ `mutation CreateUserNotification($input: CreateUserNotificationInput!) {
2261
+ createUserNotification(input: $input) {
2262
+ id userId title message type senderType senderName senderAvatar actionUrl isRead readAt metadata createdAt updatedAt
2263
+ }
2264
+ }`,
2265
+ {
2266
+ input: {
2267
+ ...input,
2268
+ isRead: false
2269
+ }
2270
+ }
2271
+ );
2272
+ return result.data.createUserNotification;
2273
+ }
2274
+ /**
2275
+ * Mark a personal notification as read.
2276
+ */
2277
+ async markNotificationAsRead(id) {
2278
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2279
+ const result = await this.network.mutate(
2280
+ `mutation UpdateUserNotification($input: UpdateUserNotificationInput!) {
2281
+ updateUserNotification(input: $input) {
2282
+ id isRead readAt
2283
+ }
2284
+ }`,
2285
+ {
2286
+ input: {
2287
+ id,
2288
+ isRead: true,
2289
+ readAt: now
2290
+ }
2291
+ }
2292
+ );
2293
+ return result.data.updateUserNotification;
2294
+ }
2295
+ /**
2296
+ * Delete a personal user notification.
2297
+ */
2298
+ async deleteUserNotification(id) {
2299
+ const result = await this.network.mutate(
2300
+ `mutation DeleteUserNotification($input: DeleteUserNotificationInput!) {
2301
+ deleteUserNotification(input: $input) { id }
2302
+ }`,
2303
+ { input: { id } }
2304
+ );
2305
+ return Boolean(result.data.deleteUserNotification?.id);
2306
+ }
2307
+ /**
2308
+ * Update the user's lastReadAnnouncementAt timestamp in DynamoDB.
2309
+ */
2310
+ async updateLastReadAnnouncement(userId, timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
2311
+ const result = await this.network.mutate(
2312
+ `mutation UpdateUserLastRead($input: UpdateUserInput!) {
2313
+ updateUser(input: $input) { id lastReadAnnouncementAt }
2314
+ }`,
2315
+ { input: { id: userId, lastReadAnnouncementAt: timestamp } }
2316
+ );
2317
+ return Boolean(result.data.updateUser?.id);
2318
+ }
2319
+ };
2320
+
2321
+ // src/services/subscription.service.ts
2322
+ var SubscriptionService = class {
2323
+ constructor(network) {
2324
+ this.network = network;
2325
+ }
2326
+ async createRequest(input) {
2327
+ const result = await this.network.mutate(
2328
+ `mutation CreateSubscriptionRequest($input: CreateSubscriptionRequestInput!) {
2329
+ createSubscriptionRequest(input: $input) {
2330
+ id userId userFullName userEmail itemType itemId itemTitle amount billingCycle paymentMethod senderPhone contactPhone trxId status aiEvals aiQuestions adminNotes processedAt processedBy createdAt updatedAt
2331
+ }
2332
+ }`,
2333
+ { input }
2334
+ );
2335
+ return result.data.createSubscriptionRequest;
2336
+ }
2337
+ async getRequest(id) {
2338
+ const result = await this.network.query(
2339
+ `query GetSubscriptionRequest($id: ID!) {
2340
+ getSubscriptionRequest(id: $id) {
2341
+ id userId userFullName userEmail itemType itemId itemTitle amount billingCycle paymentMethod senderPhone contactPhone trxId status aiEvals aiQuestions adminNotes processedAt processedBy createdAt updatedAt
2342
+ }
2343
+ }`,
2344
+ { id }
2345
+ );
2346
+ return result.data.getSubscriptionRequest;
2347
+ }
2348
+ async listRequests(input = {}) {
2349
+ const limit = input.limit ?? 20;
2350
+ const sortDirection = input.sortDirection ?? "DESC";
2351
+ if (input.status) {
2352
+ const result2 = await this.network.query(
2353
+ `query ListSubscriptionRequestsByStatus($status: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
2354
+ listSubscriptionRequestByStatusAndCreatedAt(status: $status, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
2355
+ items {
2356
+ id userId userFullName userEmail itemType itemId itemTitle amount billingCycle paymentMethod senderPhone contactPhone trxId status aiEvals aiQuestions adminNotes processedAt processedBy createdAt updatedAt
2357
+ }
2358
+ nextToken
2359
+ }
2360
+ }`,
2361
+ { status: input.status, sortDirection, limit, nextToken: input.nextToken }
2362
+ );
2363
+ return result2.data.listSubscriptionRequestByStatusAndCreatedAt;
2364
+ }
2365
+ if (input.userId) {
2366
+ const result2 = await this.network.query(
2367
+ `query ListSubscriptionRequestsByUser($userId: String!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
2368
+ listSubscriptionRequestByUserIdAndCreatedAt(userId: $userId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
2369
+ items {
2370
+ id userId userFullName userEmail itemType itemId itemTitle amount billingCycle paymentMethod senderPhone contactPhone trxId status aiEvals aiQuestions adminNotes processedAt processedBy createdAt updatedAt
2371
+ }
2372
+ nextToken
2373
+ }
2374
+ }`,
2375
+ { userId: input.userId, sortDirection, limit, nextToken: input.nextToken }
2376
+ );
2377
+ return result2.data.listSubscriptionRequestByUserIdAndCreatedAt;
2378
+ }
2379
+ const result = await this.network.query(
2380
+ `query ListAllSubscriptionRequests($limit: Int, $nextToken: String, $filter: ModelSubscriptionRequestFilterInput) {
2381
+ listSubscriptionRequests(limit: $limit, nextToken: $nextToken, filter: $filter) {
2382
+ items {
2383
+ id userId userFullName userEmail itemType itemId itemTitle amount billingCycle paymentMethod senderPhone contactPhone trxId status aiEvals aiQuestions adminNotes processedAt processedBy createdAt updatedAt
2384
+ }
2385
+ nextToken
2386
+ }
2387
+ }`,
2388
+ { limit, nextToken: input.nextToken, filter: input.filter }
2389
+ );
2390
+ return result.data.listSubscriptionRequests;
2391
+ }
2392
+ async updateRequest(input) {
2393
+ const result = await this.network.mutate(
2394
+ `mutation UpdateSubscriptionRequest($input: UpdateSubscriptionRequestInput!) {
2395
+ updateSubscriptionRequest(input: $input) {
2396
+ id userId userFullName userEmail itemType itemId itemTitle amount billingCycle paymentMethod senderPhone contactPhone trxId status aiEvals aiQuestions adminNotes processedAt processedBy createdAt updatedAt
2397
+ }
2398
+ }`,
2399
+ { input }
2400
+ );
2401
+ return result.data.updateSubscriptionRequest;
2402
+ }
2403
+ };
2404
+
1283
2405
  // src/client.ts
1284
2406
  var OrjokClient = class {
1285
2407
  constructor(config) {
1286
2408
  this.auth = config.authProvider;
2409
+ this.network = config.networkProvider;
1287
2410
  this.storage = config.storageProvider;
1288
2411
  this.users = new UserService(config.networkProvider);
1289
2412
  this.questions = new QuestionService(config.networkProvider);
@@ -1295,29 +2418,14 @@ var OrjokClient = class {
1295
2418
  this.progress = new ProgressService(config.networkProvider);
1296
2419
  this.ai = new AIService(config.networkProvider);
1297
2420
  this.news = new NewsService(config.networkProvider);
2421
+ this.notifications = new NotificationService(config.networkProvider);
2422
+ this.subscriptions = new SubscriptionService(config.networkProvider);
1298
2423
  }
1299
2424
  };
1300
2425
  function createOrjokClient(config) {
1301
2426
  return new OrjokClient(config);
1302
2427
  }
1303
2428
 
1304
- // src/types/select-options.ts
1305
- var levelOptions = [
1306
- { value: "SSC", label: "SSC" },
1307
- { value: "HSC+Admission", label: "HSC/Admission" },
1308
- { value: "BCS", label: "BCS" }
1309
- ];
1310
- var languageOptions = [
1311
- { value: "Bangla", label: "Bangla" },
1312
- { value: "English", label: "English" },
1313
- { value: "Any", label: "Any" }
1314
- ];
1315
- var difficultyOptions = [
1316
- { value: "Easy", label: "Easy" },
1317
- { value: "Medium", label: "Medium" },
1318
- { value: "Hard", label: "Hard" }
1319
- ];
1320
-
1321
2429
  // src/logic/avatar.ts
1322
2430
  var HAIR_COLORS = [
1323
2431
  { name: "Original", filter: "none", hex: "#e2e8f0", brightness: 1 },
@@ -1403,15 +2511,124 @@ function parseAvatarConfig(configStr) {
1403
2511
  const parsed = JSON.parse(configStr);
1404
2512
  return {
1405
2513
  selections: parsed.selections || {},
1406
- colors: parsed.colors || {}
2514
+ colors: parsed.colors || {},
2515
+ gender: parsed.gender
1407
2516
  };
1408
2517
  } catch {
1409
2518
  return { selections: {}, colors: {} };
1410
2519
  }
1411
2520
  }
2521
+ function parseAvatarConfigWithDefaults(configStr, seed) {
2522
+ const parsed = parseAvatarConfig(configStr);
2523
+ const hasSelections = Object.keys(parsed.selections).length > 0;
2524
+ if (!hasSelections) {
2525
+ return generateRandomAvatarState({ seed });
2526
+ }
2527
+ return {
2528
+ selections: { ...parsed.selections },
2529
+ colors: { ...parsed.colors },
2530
+ gender: parsed.gender
2531
+ };
2532
+ }
1412
2533
  function serializeAvatarConfig(state) {
1413
2534
  return JSON.stringify(state);
1414
2535
  }
2536
+ function createSeededRandom(seedStr) {
2537
+ let h = 1779033703 ^ seedStr.length;
2538
+ for (let i = 0; i < seedStr.length; i++) {
2539
+ h = Math.imul(h ^ seedStr.charCodeAt(i), 3432918353);
2540
+ h = h << 13 | h >>> 19;
2541
+ }
2542
+ return () => {
2543
+ h = Math.imul(h ^ h >>> 16, 2246822507);
2544
+ h = Math.imul(h ^ h >>> 13, 3266489909);
2545
+ return ((h ^= h >>> 16) >>> 0) / 4294967296;
2546
+ };
2547
+ }
2548
+ function pickRandom(arr, fallback, rng = Math.random) {
2549
+ if (arr.length === 0) return fallback;
2550
+ const item = arr[Math.floor(rng() * arr.length)];
2551
+ return item !== void 0 ? item : fallback;
2552
+ }
2553
+ function generateRandomAvatarState(options) {
2554
+ const rng = options?.seed ? createSeededRandom(options.seed) : Math.random;
2555
+ const gender = options?.gender || (rng() > 0.5 ? "Male" : "Female");
2556
+ const backgrounds = ["Solid", "3 Stripes"];
2557
+ const skinColors = [
2558
+ "Light Peach",
2559
+ "Warm Peach",
2560
+ "Golden Tan",
2561
+ "Sand Beige",
2562
+ "Warm Brown",
2563
+ "Deep Brown"
2564
+ ];
2565
+ const maleHairs = ["Clean", "Mid", "Curled", "Bald"];
2566
+ const femaleHairs = ["Bun", "Long", "Mid", "Short"];
2567
+ const clothesList = ["Tshirt", "Hoodie", "Turtleneck"];
2568
+ const expressions = ["Normal", "Happy", "Joyful", "Playful", "Surprised"];
2569
+ const eywears = ["None", "None", "None", "None", "Normal Glasses"];
2570
+ const headwears = ["None", "None", "None", "None", "Beanie", "HeadPhones"];
2571
+ const hairList = gender === "Male" ? maleHairs : femaleHairs;
2572
+ const bg = pickRandom(backgrounds, "Solid", rng);
2573
+ const skin = pickRandom(skinColors, "Light Peach", rng);
2574
+ const hair = pickRandom(hairList, gender === "Male" ? "Clean" : "Short", rng);
2575
+ const clothes = pickRandom(clothesList, "Tshirt", rng);
2576
+ const expression = pickRandom(expressions, "Normal", rng);
2577
+ const eyewear = pickRandom(eywears, "None", rng);
2578
+ const headwear = pickRandom(headwears, "None", rng);
2579
+ const hairColorPool = [
2580
+ "Black",
2581
+ "Black",
2582
+ "Brown",
2583
+ "Brown",
2584
+ "Blonde",
2585
+ "White",
2586
+ "Red",
2587
+ "Blue"
2588
+ ];
2589
+ const hairColor = pickRandom(hairColorPool, "Black", rng);
2590
+ const clothesColorPool = [
2591
+ "Blue",
2592
+ "Red",
2593
+ "Green",
2594
+ "Orange",
2595
+ "Purple",
2596
+ "Black",
2597
+ "Yellow",
2598
+ "Pink"
2599
+ ];
2600
+ const clothesColor = pickRandom(clothesColorPool, "Blue", rng);
2601
+ const bgColorPool = ["Blue", "Green", "Purple", "Orange", "Red", "Pink", "Original"];
2602
+ const bgColor = pickRandom(bgColorPool, "Blue", rng);
2603
+ return {
2604
+ selections: {
2605
+ Background: bg,
2606
+ "Hair Back": hair,
2607
+ "Skin Color": skin,
2608
+ "Clothes Back": clothes,
2609
+ "Clothes Front": clothes,
2610
+ Accessories: "None",
2611
+ Headwears: headwear,
2612
+ Eyewear: eyewear,
2613
+ "Facial Expression": expression,
2614
+ Beard: "None",
2615
+ "Hair Front": hair
2616
+ },
2617
+ colors: {
2618
+ "Background Color": bgColor,
2619
+ "Hair Color": hairColor,
2620
+ "Clothes Color": clothesColor,
2621
+ "Accessory Color": "Original",
2622
+ "Headwears Color": "Original",
2623
+ "Eyewear Color": "Original",
2624
+ "Facial Expression Color": "Original"
2625
+ },
2626
+ gender
2627
+ };
2628
+ }
2629
+ function generateRandomAvatarConfig(options) {
2630
+ return JSON.stringify(generateRandomAvatarState(options));
2631
+ }
1415
2632
 
1416
2633
  // src/logic/elo.ts
1417
2634
  var TIERS = [
@@ -1548,6 +2765,212 @@ function reorderObjectKeys(obj, oldIndex, newIndex, length) {
1548
2765
  return newObj;
1549
2766
  }
1550
2767
 
2768
+ // src/logic/home.ts
2769
+ function mapCognitoLevelToStandardLevel(cognitoLevel) {
2770
+ if (!cognitoLevel) return "HSC+Admission";
2771
+ const normalized = cognitoLevel.trim().toLowerCase();
2772
+ if (normalized === "class_9" || normalized === "class_10" || normalized === "ssc") {
2773
+ return "SSC";
2774
+ }
2775
+ if (normalized === "class_11" || normalized === "class_12" || normalized === "admission" || normalized === "hsc" || normalized === "hsc+admission") {
2776
+ return "HSC+Admission";
2777
+ }
2778
+ if (normalized === "bcs" || normalized === "job") {
2779
+ return "BCS";
2780
+ }
2781
+ return "HSC+Admission";
2782
+ }
2783
+ function getTimeOfDayGreeting(customHour, lang = "en") {
2784
+ const hour = customHour !== void 0 ? customHour : (/* @__PURE__ */ new Date()).getHours();
2785
+ if (hour >= 5 && hour < 12) {
2786
+ return {
2787
+ greeting: lang === "bn" ? "\u09B6\u09C1\u09AD \u09B8\u0995\u09BE\u09B2" : "Good morning",
2788
+ period: "morning"
2789
+ };
2790
+ }
2791
+ if (hour >= 12 && hour < 17) {
2792
+ return {
2793
+ greeting: lang === "bn" ? "\u09B6\u09C1\u09AD \u09A6\u09C1\u09AA\u09C1\u09B0" : "Good afternoon",
2794
+ period: "afternoon"
2795
+ };
2796
+ }
2797
+ if (hour >= 17 && hour < 21) {
2798
+ return {
2799
+ greeting: lang === "bn" ? "\u09B6\u09C1\u09AD \u09B8\u09A8\u09CD\u09A7\u09CD\u09AF\u09BE" : "Good evening",
2800
+ period: "evening"
2801
+ };
2802
+ }
2803
+ return {
2804
+ greeting: lang === "bn" ? "\u09B6\u09C1\u09AD \u09B0\u09BE\u09A4\u09CD\u09B0\u09BF" : "Good evening",
2805
+ period: "night"
2806
+ };
2807
+ }
2808
+ function getRecommendedSubjectsForLevel(level, lang = "en") {
2809
+ const isBn = lang === "bn";
2810
+ const normalizedLevel = mapCognitoLevelToStandardLevel(level);
2811
+ if (normalizedLevel === "SSC") {
2812
+ return [
2813
+ {
2814
+ id: "ssc-physics",
2815
+ name: isBn ? "\u09AA\u09A6\u09BE\u09B0\u09CD\u09A5\u09AC\u09BF\u099C\u09CD\u099E\u09BE\u09A8" : "Physics",
2816
+ iconKey: "atom",
2817
+ colorClass: "from-blue-500 to-indigo-600"
2818
+ },
2819
+ {
2820
+ id: "ssc-chemistry",
2821
+ name: isBn ? "\u09B0\u09B8\u09BE\u09DF\u09A8" : "Chemistry",
2822
+ iconKey: "flask",
2823
+ colorClass: "from-emerald-500 to-teal-600"
2824
+ },
2825
+ {
2826
+ id: "ssc-higher-math",
2827
+ name: isBn ? "\u0989\u099A\u09CD\u099A\u09A4\u09B0 \u0997\u09A3\u09BF\u09A4" : "Higher Math",
2828
+ iconKey: "math",
2829
+ colorClass: "from-amber-500 to-orange-600"
2830
+ },
2831
+ {
2832
+ id: "ssc-general-math",
2833
+ name: isBn ? "\u09B8\u09BE\u09A7\u09BE\u09B0\u09A3 \u0997\u09A3\u09BF\u09A4" : "General Math",
2834
+ iconKey: "calculator",
2835
+ colorClass: "from-violet-500 to-purple-600"
2836
+ },
2837
+ {
2838
+ id: "ssc-biology",
2839
+ name: isBn ? "\u099C\u09C0\u09AC\u09AC\u09BF\u099C\u09CD\u099E\u09BE\u09A8" : "Biology",
2840
+ iconKey: "dna",
2841
+ colorClass: "from-rose-500 to-pink-600"
2842
+ },
2843
+ {
2844
+ id: "ssc-bangla",
2845
+ name: isBn ? "\u09AC\u09BE\u0982\u09B2\u09BE" : "Bangla",
2846
+ iconKey: "book",
2847
+ colorClass: "from-cyan-500 to-blue-600"
2848
+ }
2849
+ ];
2850
+ }
2851
+ if (normalizedLevel === "BCS") {
2852
+ return [
2853
+ {
2854
+ id: "bcs-bangladesh-affairs",
2855
+ name: isBn ? "\u09AC\u09BE\u0982\u09B2\u09BE\u09A6\u09C7\u09B6 \u09AC\u09BF\u09B7\u09DF\u09BE\u09AC\u09B2\u09C0" : "Bangladesh Affairs",
2856
+ iconKey: "monument",
2857
+ colorClass: "from-emerald-500 to-green-600"
2858
+ },
2859
+ {
2860
+ id: "bcs-international-affairs",
2861
+ name: isBn ? "\u0986\u09A8\u09CD\u09A4\u09B0\u09CD\u099C\u09BE\u09A4\u09BF\u0995 \u09AC\u09BF\u09B7\u09DF\u09BE\u09AC\u09B2\u09C0" : "International Affairs",
2862
+ iconKey: "world",
2863
+ colorClass: "from-blue-500 to-cyan-600"
2864
+ },
2865
+ {
2866
+ id: "bcs-bangla",
2867
+ name: isBn ? "\u09AC\u09BE\u0982\u09B2\u09BE \u09AD\u09BE\u09B7\u09BE \u0993 \u09B8\u09BE\u09B9\u09BF\u09A4\u09CD\u09AF" : "Bangla Literature",
2868
+ iconKey: "book",
2869
+ colorClass: "from-amber-500 to-orange-600"
2870
+ },
2871
+ {
2872
+ id: "bcs-english",
2873
+ name: isBn ? "\u0987\u0982\u09B0\u09C7\u099C\u09BF \u09AD\u09BE\u09B7\u09BE \u0993 \u09B8\u09BE\u09B9\u09BF\u09A4\u09CD\u09AF" : "English Language",
2874
+ iconKey: "language",
2875
+ colorClass: "from-violet-500 to-purple-600"
2876
+ },
2877
+ {
2878
+ id: "bcs-math-mental-ability",
2879
+ name: isBn ? "\u0997\u09BE\u09A3\u09BF\u09A4\u09BF\u0995 \u0993 \u09AE\u09BE\u09A8\u09B8\u09BF\u0995 \u09A6\u0995\u09CD\u09B7\u09A4\u09BE" : "Math & Mental Ability",
2880
+ iconKey: "brain",
2881
+ colorClass: "from-pink-500 to-rose-600"
2882
+ },
2883
+ {
2884
+ id: "bcs-general-science",
2885
+ name: isBn ? "\u09B8\u09BE\u09A7\u09BE\u09B0\u09A3 \u09AC\u09BF\u099C\u09CD\u099E\u09BE\u09A8 \u0993 \u09AA\u09CD\u09B0\u09AF\u09C1\u0995\u09CD\u09A4\u09BF" : "General Science",
2886
+ iconKey: "atom",
2887
+ colorClass: "from-teal-500 to-emerald-600"
2888
+ }
2889
+ ];
2890
+ }
2891
+ return [
2892
+ {
2893
+ id: "hsc-physics",
2894
+ name: isBn ? "\u09AA\u09A6\u09BE\u09B0\u09CD\u09A5\u09AC\u09BF\u099C\u09CD\u099E\u09BE\u09A8" : "Physics",
2895
+ iconKey: "atom",
2896
+ colorClass: "from-blue-500 to-indigo-600"
2897
+ },
2898
+ {
2899
+ id: "hsc-chemistry",
2900
+ name: isBn ? "\u09B0\u09B8\u09BE\u09DF\u09A8" : "Chemistry",
2901
+ iconKey: "flask",
2902
+ colorClass: "from-emerald-500 to-teal-600"
2903
+ },
2904
+ {
2905
+ id: "hsc-higher-math",
2906
+ name: isBn ? "\u0989\u099A\u09CD\u099A\u09A4\u09B0 \u0997\u09A3\u09BF\u09A4" : "Higher Math",
2907
+ iconKey: "math",
2908
+ colorClass: "from-amber-500 to-orange-600"
2909
+ },
2910
+ {
2911
+ id: "hsc-biology",
2912
+ name: isBn ? "\u099C\u09C0\u09AC\u09AC\u09BF\u099C\u09CD\u099E\u09BE\u09A8" : "Biology",
2913
+ iconKey: "dna",
2914
+ colorClass: "from-rose-500 to-pink-600"
2915
+ },
2916
+ {
2917
+ id: "hsc-ict",
2918
+ name: isBn ? "\u09A4\u09A5\u09CD\u09AF \u0993 \u09AF\u09CB\u0997\u09BE\u09AF\u09CB\u0997 \u09AA\u09CD\u09B0\u09AF\u09C1\u0995\u09CD\u09A4\u09BF" : "ICT",
2919
+ iconKey: "device-laptop",
2920
+ colorClass: "from-cyan-500 to-sky-600"
2921
+ },
2922
+ {
2923
+ id: "hsc-bangla",
2924
+ name: isBn ? "\u09AC\u09BE\u0982\u09B2\u09BE" : "Bangla",
2925
+ iconKey: "book",
2926
+ colorClass: "from-violet-500 to-purple-600"
2927
+ }
2928
+ ];
2929
+ }
2930
+ function calculateStreakFromDates(activityDates) {
2931
+ if (!activityDates || activityDates.length === 0) {
2932
+ return { streak: 0, isActiveToday: false };
2933
+ }
2934
+ const uniqueDateStrings = Array.from(
2935
+ new Set(
2936
+ activityDates.map((d) => {
2937
+ try {
2938
+ return new Date(d).toISOString().split("T")[0];
2939
+ } catch {
2940
+ return null;
2941
+ }
2942
+ }).filter((d) => Boolean(d))
2943
+ )
2944
+ ).sort((a, b) => a > b ? -1 : 1);
2945
+ if (uniqueDateStrings.length === 0) {
2946
+ return { streak: 0, isActiveToday: false };
2947
+ }
2948
+ const todayStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2949
+ const yesterdayDate = /* @__PURE__ */ new Date();
2950
+ yesterdayDate.setDate(yesterdayDate.getDate() - 1);
2951
+ const yesterdayStr = yesterdayDate.toISOString().split("T")[0];
2952
+ const latestDate = uniqueDateStrings[0];
2953
+ if (!latestDate) {
2954
+ return { streak: 0, isActiveToday: false };
2955
+ }
2956
+ const isActiveToday = latestDate === todayStr;
2957
+ if (latestDate !== todayStr && latestDate !== yesterdayStr) {
2958
+ return { streak: 0, isActiveToday: false };
2959
+ }
2960
+ let streak = 0;
2961
+ const checkDate = new Date(latestDate);
2962
+ for (const dateStr of uniqueDateStrings) {
2963
+ const expectedStr = checkDate.toISOString().split("T")[0];
2964
+ if (dateStr === expectedStr) {
2965
+ streak += 1;
2966
+ checkDate.setDate(checkDate.getDate() - 1);
2967
+ } else {
2968
+ break;
2969
+ }
2970
+ }
2971
+ return { streak, isActiveToday };
2972
+ }
2973
+
1551
2974
  // src/logic/import-export.ts
1552
2975
  var MAX_QUESTIONS = 200;
1553
2976
  function parseFields(parts) {
@@ -1752,6 +3175,250 @@ function formatQuestionsToCsv(questions, packSubjectId, packChapterId, packTopic
1752
3175
  ${rows.join("\n")}`;
1753
3176
  }
1754
3177
 
3178
+ // src/logic/subscription.ts
3179
+ var MANUAL_PAYMENT_CONFIG = {
3180
+ phoneNumber: "01323567688",
3181
+ accountType: "Personal",
3182
+ methods: ["BKASH", "ROCKET"]
3183
+ };
3184
+ var SUBSCRIPTION_PLANS = {
3185
+ FREE: {
3186
+ id: "FREE",
3187
+ tier: "FREE",
3188
+ titleKey: "upgrade.freeTitle",
3189
+ descKey: "upgrade.freeDesc",
3190
+ pricing: {
3191
+ monthly: 0,
3192
+ quarterly: 0,
3193
+ biannually: 0,
3194
+ annually: 0
3195
+ },
3196
+ aiQuotas: {
3197
+ monthly: { aiEvals: 0, aiQuestions: 0 },
3198
+ quarterly: { aiEvals: 0, aiQuestions: 0 },
3199
+ biannually: { aiEvals: 0, aiQuestions: 0 },
3200
+ annually: { aiEvals: 0, aiQuestions: 0 }
3201
+ }
3202
+ },
3203
+ PRO: {
3204
+ id: "PRO",
3205
+ tier: "PRO",
3206
+ titleKey: "upgrade.proTitle",
3207
+ descKey: "upgrade.proDesc",
3208
+ pricing: {
3209
+ monthly: 149,
3210
+ quarterly: 399,
3211
+ biannually: 699,
3212
+ annually: 999
3213
+ },
3214
+ aiQuotas: {
3215
+ monthly: { aiEvals: 0, aiQuestions: 0 },
3216
+ quarterly: { aiEvals: 0, aiQuestions: 0 },
3217
+ biannually: { aiEvals: 0, aiQuestions: 0 },
3218
+ annually: { aiEvals: 0, aiQuestions: 0 }
3219
+ }
3220
+ },
3221
+ PRO_AI: {
3222
+ id: "PRO_AI",
3223
+ tier: "PRO_AI",
3224
+ titleKey: "upgrade.aiTitle",
3225
+ descKey: "upgrade.aiDesc",
3226
+ pricing: {
3227
+ monthly: 349,
3228
+ quarterly: 899,
3229
+ biannually: 1599,
3230
+ annually: 2499
3231
+ },
3232
+ aiQuotas: {
3233
+ monthly: { aiEvals: 100, aiQuestions: 450 },
3234
+ quarterly: { aiEvals: 280, aiQuestions: 1100 },
3235
+ biannually: { aiEvals: 500, aiQuestions: 2e3 },
3236
+ annually: { aiEvals: 800, aiQuestions: 3e3 }
3237
+ }
3238
+ }
3239
+ };
3240
+ var AI_EVAL_BOOSTER_PACKS = [
3241
+ {
3242
+ id: "eval_booster_starter",
3243
+ type: "AI_EVAL",
3244
+ amount: 40,
3245
+ price: 69,
3246
+ titleKey: "upgrade.boosters.evalStarterTitle",
3247
+ descKey: "upgrade.boosters.evalStarterDesc"
3248
+ },
3249
+ {
3250
+ id: "eval_booster_standard",
3251
+ type: "AI_EVAL",
3252
+ amount: 100,
3253
+ price: 159,
3254
+ titleKey: "upgrade.boosters.evalStandardTitle",
3255
+ descKey: "upgrade.boosters.evalStandardDesc",
3256
+ badgeKey: "upgrade.popular"
3257
+ },
3258
+ {
3259
+ id: "eval_booster_mega",
3260
+ type: "AI_EVAL",
3261
+ amount: 220,
3262
+ price: 349,
3263
+ titleKey: "upgrade.boosters.evalMegaTitle",
3264
+ descKey: "upgrade.boosters.evalMegaDesc",
3265
+ badgeKey: "upgrade.bestValue"
3266
+ }
3267
+ ];
3268
+ var AI_QUESTION_BOOSTER_PACKS = [
3269
+ {
3270
+ id: "question_booster_drill",
3271
+ type: "AI_QUESTION",
3272
+ amount: 100,
3273
+ price: 39,
3274
+ titleKey: "upgrade.boosters.questionDrillTitle",
3275
+ descKey: "upgrade.boosters.questionDrillDesc"
3276
+ },
3277
+ {
3278
+ id: "question_booster_chapter",
3279
+ type: "AI_QUESTION",
3280
+ amount: 250,
3281
+ price: 99,
3282
+ titleKey: "upgrade.boosters.questionChapterTitle",
3283
+ descKey: "upgrade.boosters.questionChapterDesc",
3284
+ badgeKey: "upgrade.popular"
3285
+ },
3286
+ {
3287
+ id: "question_booster_exam",
3288
+ type: "AI_QUESTION",
3289
+ amount: 700,
3290
+ price: 249,
3291
+ titleKey: "upgrade.boosters.questionExamTitle",
3292
+ descKey: "upgrade.boosters.questionExamDesc",
3293
+ badgeKey: "upgrade.bestValue"
3294
+ }
3295
+ ];
3296
+ function getCycleMultiplier(cycle) {
3297
+ switch (cycle) {
3298
+ case "quarterly":
3299
+ return 3;
3300
+ case "biannually":
3301
+ return 6;
3302
+ case "annually":
3303
+ return 12;
3304
+ case "monthly":
3305
+ default:
3306
+ return 1;
3307
+ }
3308
+ }
3309
+ function getPlanOriginalPrice(tier, cycle) {
3310
+ const normalizedTier = tier === "AI_ANNUAL" || tier === "AI_MONTHLY" ? "PRO_AI" : tier;
3311
+ const plan = SUBSCRIPTION_PLANS[normalizedTier] || SUBSCRIPTION_PLANS.FREE;
3312
+ const monthlyPrice = plan.pricing.monthly ?? 0;
3313
+ return monthlyPrice * getCycleMultiplier(cycle);
3314
+ }
3315
+ function getPlanPrice(tier, cycle) {
3316
+ const normalizedTier = tier === "AI_ANNUAL" || tier === "AI_MONTHLY" ? "PRO_AI" : tier;
3317
+ const plan = SUBSCRIPTION_PLANS[normalizedTier] || SUBSCRIPTION_PLANS.FREE;
3318
+ return plan.pricing[cycle] ?? 0;
3319
+ }
3320
+ function getPlanMonthlyEquivalentPrice(tier, cycle) {
3321
+ const normalizedTier = tier === "AI_ANNUAL" || tier === "AI_MONTHLY" ? "PRO_AI" : tier;
3322
+ const plan = SUBSCRIPTION_PLANS[normalizedTier] || SUBSCRIPTION_PLANS.FREE;
3323
+ const cyclePrice = plan.pricing[cycle] ?? 0;
3324
+ const multiplier = getCycleMultiplier(cycle);
3325
+ return Math.round(cyclePrice / multiplier);
3326
+ }
3327
+ function getPlanDiscountPercent(cycle) {
3328
+ switch (cycle) {
3329
+ case "quarterly":
3330
+ return 10;
3331
+ case "biannually":
3332
+ return 22;
3333
+ case "annually":
3334
+ return 44;
3335
+ case "monthly":
3336
+ default:
3337
+ return 0;
3338
+ }
3339
+ }
3340
+ function getPlanAiQuotas(tier, cycle) {
3341
+ const normalizedTier = tier === "AI_ANNUAL" || tier === "AI_MONTHLY" ? "PRO_AI" : tier;
3342
+ const plan = SUBSCRIPTION_PLANS[normalizedTier] || SUBSCRIPTION_PLANS.FREE;
3343
+ return plan.aiQuotas[cycle] ?? { aiEvals: 0, aiQuestions: 0 };
3344
+ }
3345
+ function isValidBangladeshiPhone(phone) {
3346
+ if (!phone) return false;
3347
+ const cleaned = phone.replace(/[\s\-()]/g, "");
3348
+ const regex = /^(?:\+?88)?01[3-9]\d{8}$/;
3349
+ return regex.test(cleaned);
3350
+ }
3351
+ function isValidTrxId(trxId) {
3352
+ if (!trxId) return false;
3353
+ const cleaned = trxId.trim();
3354
+ return cleaned.length >= 4 && cleaned.length <= 40;
3355
+ }
3356
+ function hasProAccess(tier, expiresAt) {
3357
+ if (!tier) return false;
3358
+ const upper = tier.toUpperCase();
3359
+ const isTierPro = upper === "PRO" || upper === "PRO_AI";
3360
+ if (!isTierPro) return false;
3361
+ if (!expiresAt) return true;
3362
+ const expTime = new Date(expiresAt).getTime();
3363
+ if (isNaN(expTime)) return true;
3364
+ return expTime > Date.now();
3365
+ }
3366
+ function hasAiTierAccess(tier, expiresAt) {
3367
+ if (!tier) return false;
3368
+ const upper = tier.toUpperCase();
3369
+ const isTierAi = upper === "PRO_AI";
3370
+ if (!isTierAi) return false;
3371
+ if (!expiresAt) return true;
3372
+ const expTime = new Date(expiresAt).getTime();
3373
+ if (isNaN(expTime)) return true;
3374
+ return expTime > Date.now();
3375
+ }
3376
+ var FREE_DAILY_PRACTICE_LIMIT = 30;
3377
+ function canExportPack(tier, expiresAt, isCreator) {
3378
+ return Boolean(isCreator || hasProAccess(tier, expiresAt));
3379
+ }
3380
+ function canAccessMistakeVault(tier, expiresAt, isCreator) {
3381
+ return Boolean(isCreator || hasProAccess(tier, expiresAt));
3382
+ }
3383
+ function canWatchContestSolution(tier, expiresAt, isCreator) {
3384
+ return Boolean(isCreator || hasProAccess(tier, expiresAt));
3385
+ }
3386
+ function shouldTrackPracticeProgress(tier, expiresAt, isCreator) {
3387
+ return Boolean(isCreator || hasProAccess(tier, expiresAt));
3388
+ }
3389
+ function getDailyPracticeLimit(tier, expiresAt) {
3390
+ if (hasProAccess(tier, expiresAt)) return null;
3391
+ return FREE_DAILY_PRACTICE_LIMIT;
3392
+ }
3393
+ function checkDailyPracticeLimit(dailyCount, lastPracticeDate, tier, expiresAt, todayStr) {
3394
+ if (hasProAccess(tier, expiresAt)) {
3395
+ return {
3396
+ allowed: true,
3397
+ remaining: Infinity,
3398
+ currentCount: dailyCount,
3399
+ isUnlimited: true
3400
+ };
3401
+ }
3402
+ const today = todayStr || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3403
+ const isSameDay = lastPracticeDate ? lastPracticeDate.startsWith(today) : false;
3404
+ const currentCount = isSameDay ? Number.isFinite(dailyCount) ? dailyCount : 0 : 0;
3405
+ const remaining = Math.max(0, FREE_DAILY_PRACTICE_LIMIT - currentCount);
3406
+ const allowed = currentCount < FREE_DAILY_PRACTICE_LIMIT;
3407
+ return {
3408
+ allowed,
3409
+ remaining,
3410
+ currentCount,
3411
+ isUnlimited: false
3412
+ };
3413
+ }
3414
+ function hasSufficientAiQuestionQuota(quota, requested) {
3415
+ if (requested <= 0) return false;
3416
+ return (quota ?? 0) >= requested;
3417
+ }
3418
+ function hasSufficientAiEvalQuota(quota) {
3419
+ return (quota ?? 0) > 0;
3420
+ }
3421
+
1755
3422
  // src/navigation.ts
1756
3423
  function getActiveTab(pathname) {
1757
3424
  const normalized = pathname.toLowerCase();
@@ -1906,7 +3573,7 @@ function processLatexText(str) {
1906
3573
  }
1907
3574
 
1908
3575
  // src/utils/time.ts
1909
- function convertToBangladeshTime(isoString) {
3576
+ function convertToBangladeshTime(isoString, locale = "en-GB") {
1910
3577
  const options = {
1911
3578
  year: "numeric",
1912
3579
  month: "long",
@@ -1916,7 +3583,8 @@ function convertToBangladeshTime(isoString) {
1916
3583
  hour12: true
1917
3584
  };
1918
3585
  const date = new Date(isoString);
1919
- return date.toLocaleString("en-GB", {
3586
+ const resolvedLocale = locale === "bn" ? "bn-BD" : locale;
3587
+ return date.toLocaleString(resolvedLocale, {
1920
3588
  ...options,
1921
3589
  timeZone: "Asia/Dhaka"
1922
3590
  });
@@ -2008,24 +3676,42 @@ function isLanguage(value) {
2008
3676
  function isDifficulty(value) {
2009
3677
  return ["Easy", "Medium", "Hard"].includes(value);
2010
3678
  }
3679
+ function normalizeLevel(value) {
3680
+ if (!value) return "";
3681
+ const clean = value.trim().replace(/\s+/g, "+");
3682
+ const upper = clean.toUpperCase();
3683
+ if (upper === "SSC" || upper.includes("CLASS_9") || upper.includes("CLASS_10")) return "SSC";
3684
+ if (upper === "HSC" || upper === "ADMISSION" || upper === "HSC+ADMISSION" || upper.includes("CLASS_11") || upper.includes("CLASS_12")) return "HSC+Admission";
3685
+ if (upper === "BCS") return "BCS";
3686
+ return clean;
3687
+ }
2011
3688
  export {
2012
3689
  AIService,
3690
+ AI_EVAL_BOOSTER_PACKS,
3691
+ AI_QUESTION_BOOSTER_PACKS,
2013
3692
  CLOTHES_COLORS,
2014
3693
  COLOR_HUES,
2015
3694
  ContestService,
2016
3695
  CourseService,
2017
3696
  CurriculumService,
3697
+ FREE_DAILY_PRACTICE_LIMIT,
2018
3698
  HAIR_COLORS,
2019
3699
  INITIAL_AVATAR_DATA,
3700
+ MANUAL_PAYMENT_CONFIG,
2020
3701
  MediaService,
2021
3702
  MediaUrlCache,
2022
3703
  NewsService,
3704
+ NotificationService,
2023
3705
  OrjokClient,
2024
3706
  PackService,
2025
3707
  ProgressService,
2026
3708
  QuestionService,
2027
3709
  RENDER_ORDER,
3710
+ SUBSCRIPTION_PLANS,
3711
+ SubscriptionService,
2028
3712
  UserService,
3713
+ XP_REWARDS,
3714
+ XP_TIERS,
2029
3715
  _resetMediaUrlWarning,
2030
3716
  aggregateChapterMetrics,
2031
3717
  aggregateSubjectMetrics,
@@ -2034,7 +3720,14 @@ export {
2034
3720
  calculateAccuracy,
2035
3721
  calculateDefaultExamMinutes,
2036
3722
  calculateExamTime,
3723
+ calculateStreakFromDates,
2037
3724
  calculateTimeLeft,
3725
+ calculateUnreadCount,
3726
+ calculateXpReward,
3727
+ canAccessMistakeVault,
3728
+ canExportPack,
3729
+ canWatchContestSolution,
3730
+ checkDailyPracticeLimit,
2038
3731
  computeRatingDelta,
2039
3732
  convertToBangladeshTime,
2040
3733
  createOrjokClient,
@@ -2047,18 +3740,41 @@ export {
2047
3740
  formatCurriculumName,
2048
3741
  formatDate,
2049
3742
  formatExamTime,
3743
+ formatLevelUpNotification,
2050
3744
  formatQuestionsToCsv,
2051
3745
  formatQuestionsToText,
3746
+ generateRandomAvatarConfig,
3747
+ generateRandomAvatarState,
2052
3748
  getActiveTab,
2053
3749
  getActualOptionFileName,
2054
3750
  getAllTiers,
3751
+ getAllXpTiers,
2055
3752
  getCanonicalPath,
3753
+ getCycleMultiplier,
3754
+ getDailyPracticeLimit,
2056
3755
  getDhakaNow,
2057
3756
  getEloProgress,
2058
3757
  getEloTier,
3758
+ getLevelFromXp,
3759
+ getPlanAiQuotas,
3760
+ getPlanDiscountPercent,
3761
+ getPlanMonthlyEquivalentPrice,
3762
+ getPlanOriginalPrice,
3763
+ getPlanPrice,
3764
+ getPublicExamsByLevel,
2059
3765
  getRatingForSubject,
3766
+ getRecommendedSubjectsForLevel,
2060
3767
  getRelativeTime,
3768
+ getTimeOfDayGreeting,
3769
+ getXpForLevel,
3770
+ getXpProgress,
3771
+ getXpRequiredForNextLevel,
3772
+ getXpTier,
2061
3773
  getYouTubeId,
3774
+ hasAiTierAccess,
3775
+ hasProAccess,
3776
+ hasSufficientAiEvalQuota,
3777
+ hasSufficientAiQuestionQuota,
2062
3778
  isAuthGatedRoute,
2063
3779
  isCorrectAnswer,
2064
3780
  isDifficulty,
@@ -2068,21 +3784,30 @@ export {
2068
3784
  isLanguageMatch,
2069
3785
  isQuestionLevel,
2070
3786
  isStorageKey,
3787
+ isValidBangladeshiPhone,
2071
3788
  isValidMCQ,
3789
+ isValidTrxId,
2072
3790
  languageOptions,
2073
3791
  levelOptions,
3792
+ mapCognitoLevelToStandardLevel,
3793
+ mergeNotificationTimeline,
2074
3794
  normalizeDifficulty,
3795
+ normalizeLevel,
3796
+ paginateWithAccumulator,
2075
3797
  parseAvatarConfig,
3798
+ parseAvatarConfigWithDefaults,
2076
3799
  parseCsvQuestions,
2077
3800
  parseImageKeys,
2078
3801
  parseTextQuestions,
2079
3802
  processLatexText,
3803
+ publicExamOptions,
2080
3804
  reorderObjectKeys,
2081
3805
  resolveMediaUrl,
2082
3806
  resolveMediaUrls,
2083
3807
  sanitizeFileName,
2084
3808
  scoreExam,
2085
3809
  serializeAvatarConfig,
3810
+ shouldTrackPracticeProgress,
2086
3811
  shuffle,
2087
3812
  shuffleInPlace
2088
3813
  };