@orjok/commons 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/amplify.cjs +30 -5
- package/dist/amplify.cjs.map +1 -1
- package/dist/amplify.d.cts +5 -2
- package/dist/amplify.d.ts +5 -2
- package/dist/amplify.js +30 -5
- package/dist/amplify.js.map +1 -1
- package/dist/chunk-B4UJE3F4.js +529 -0
- package/dist/chunk-B4UJE3F4.js.map +1 -0
- package/dist/chunk-ODXD2WJN.cjs +529 -0
- package/dist/chunk-ODXD2WJN.cjs.map +1 -0
- package/dist/hooks.cjs +639 -2
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +162 -3
- package/dist/hooks.d.ts +162 -3
- package/dist/hooks.js +638 -1
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +2454 -363
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +303 -15
- package/dist/index.d.ts +303 -15
- package/dist/index.js +2430 -339
- package/dist/index.js.map +1 -1
- package/dist/{client-BaOC79Nc.d.cts → notification-BoqtkNrc.d.ts} +392 -12
- package/dist/{client-Bm9aiyTx.d.ts → notification-Ctfy0m05.d.cts} +392 -12
- package/dist/{storage-DLqdJgQt.d.cts → storage-9pK8KwIM.d.cts} +4 -2
- package/dist/{storage-DLqdJgQt.d.ts → storage-9pK8KwIM.d.ts} +4 -2
- package/dist/testing.cjs +34 -3
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +9 -2
- package/dist/testing.d.ts +9 -2
- package/dist/testing.js +34 -3
- package/dist/testing.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MediaUrlCache,
|
|
3
|
+
XP_REWARDS,
|
|
4
|
+
XP_TIERS,
|
|
5
|
+
_resetMediaUrlWarning,
|
|
6
|
+
aggregateChapterMetrics,
|
|
7
|
+
aggregateSubjectMetrics,
|
|
8
|
+
aggregateTopicMetrics,
|
|
9
|
+
calculateAccuracy,
|
|
10
|
+
calculateUnreadCount,
|
|
11
|
+
calculateXpReward,
|
|
12
|
+
difficultyOptions,
|
|
13
|
+
formatLevelUpNotification,
|
|
14
|
+
getAllXpTiers,
|
|
15
|
+
getLevelFromXp,
|
|
16
|
+
getPublicExamsByLevel,
|
|
17
|
+
getXpForLevel,
|
|
18
|
+
getXpProgress,
|
|
19
|
+
getXpRequiredForNextLevel,
|
|
20
|
+
getXpTier,
|
|
21
|
+
isDifficultyMatch,
|
|
22
|
+
isLanguageMatch,
|
|
23
|
+
isStorageKey,
|
|
24
|
+
isValidMCQ,
|
|
25
|
+
languageOptions,
|
|
26
|
+
levelOptions,
|
|
27
|
+
mergeNotificationTimeline,
|
|
28
|
+
publicExamOptions,
|
|
29
|
+
resolveMediaUrl,
|
|
30
|
+
resolveMediaUrls,
|
|
31
|
+
shuffle,
|
|
32
|
+
shuffleInPlace
|
|
33
|
+
} from "./chunk-B4UJE3F4.js";
|
|
34
|
+
|
|
1
35
|
// src/services/user.service.ts
|
|
2
36
|
var UserService = class {
|
|
3
37
|
constructor(network) {
|
|
@@ -8,11 +42,12 @@ var UserService = class {
|
|
|
8
42
|
`query GetUser($id: String!) {
|
|
9
43
|
getUser(id: $id) {
|
|
10
44
|
id fullName avatarUrl avatarConfig tier subscriptionExpiresAt
|
|
11
|
-
aiEvaluationRemaining questionCount packCount contestCount
|
|
45
|
+
aiEvaluationRemaining aiQuestionGenRemaining questionCount packCount contestCount
|
|
12
46
|
EngineeringRating MedicalRating VarsityRating BCSRating
|
|
13
47
|
sscOverallRating sscPhysicsRating sscChemistryRating sscMathRating sscBiologyRating
|
|
14
48
|
hscOverallRating hscPhysicsRating hscChemistryRating hscMathRating hscBiologyRating
|
|
15
49
|
bcsOverallRating
|
|
50
|
+
xp userLevel
|
|
16
51
|
}
|
|
17
52
|
}`,
|
|
18
53
|
{ id }
|
|
@@ -20,20 +55,23 @@ var UserService = class {
|
|
|
20
55
|
return result.data.getUser;
|
|
21
56
|
}
|
|
22
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;
|
|
23
61
|
const result = await this.network.mutate(
|
|
24
62
|
`mutation UpdateUser($input: UpdateUserInput!) {
|
|
25
63
|
updateUser(input: $input) {
|
|
26
64
|
id fullName avatarUrl avatarConfig
|
|
27
65
|
}
|
|
28
66
|
}`,
|
|
29
|
-
{ input:
|
|
67
|
+
{ input: updateInput }
|
|
30
68
|
);
|
|
31
69
|
return result.data.updateUser;
|
|
32
70
|
}
|
|
33
|
-
async getQuestions(userId, nextToken) {
|
|
71
|
+
async getQuestions(userId, nextToken, limit = 10) {
|
|
34
72
|
const result = await this.network.query(
|
|
35
|
-
`query ListUserQuestions($owner: ID!, $nextToken: String, $sortDirection: ModelSortDirection) {
|
|
36
|
-
listQuestionObjectByOwnerAndCreatedAt(owner: $owner, sortDirection: $sortDirection, nextToken: $nextToken) {
|
|
73
|
+
`query ListUserQuestions($owner: ID!, $limit: Int, $nextToken: String, $sortDirection: ModelSortDirection) {
|
|
74
|
+
listQuestionObjectByOwnerAndCreatedAt(owner: $owner, limit: $limit, sortDirection: $sortDirection, nextToken: $nextToken) {
|
|
37
75
|
items {
|
|
38
76
|
id question imageUrl language level difficulty type voteCount
|
|
39
77
|
verificationStatus owner packId createdAt
|
|
@@ -43,14 +81,14 @@ var UserService = class {
|
|
|
43
81
|
nextToken
|
|
44
82
|
}
|
|
45
83
|
}`,
|
|
46
|
-
{ owner: userId, nextToken, sortDirection: "DESC" }
|
|
84
|
+
{ owner: userId, limit, nextToken, sortDirection: "DESC" }
|
|
47
85
|
);
|
|
48
86
|
return result.data.listQuestionObjectByOwnerAndCreatedAt;
|
|
49
87
|
}
|
|
50
|
-
async getPacks(userId, nextToken) {
|
|
88
|
+
async getPacks(userId, nextToken, limit = 10) {
|
|
51
89
|
const result = await this.network.query(
|
|
52
|
-
`query ListUserPacks($owner: ID!, $nextToken: String, $sortDirection: ModelSortDirection) {
|
|
53
|
-
listPackByOwnerAndCreatedAt(owner: $owner, sortDirection: $sortDirection, nextToken: $nextToken) {
|
|
90
|
+
`query ListUserPacks($owner: ID!, $limit: Int, $nextToken: String, $sortDirection: ModelSortDirection) {
|
|
91
|
+
listPackByOwnerAndCreatedAt(owner: $owner, limit: $limit, sortDirection: $sortDirection, nextToken: $nextToken) {
|
|
54
92
|
items {
|
|
55
93
|
id name language level difficulty questionCount automaticNumbering
|
|
56
94
|
owner subjectId chapterId topicId verificationStatus createdAt
|
|
@@ -59,7 +97,7 @@ var UserService = class {
|
|
|
59
97
|
nextToken
|
|
60
98
|
}
|
|
61
99
|
}`,
|
|
62
|
-
{ owner: userId, nextToken, sortDirection: "DESC" }
|
|
100
|
+
{ owner: userId, limit, nextToken, sortDirection: "DESC" }
|
|
63
101
|
);
|
|
64
102
|
return result.data.listPackByOwnerAndCreatedAt;
|
|
65
103
|
}
|
|
@@ -90,8 +128,59 @@ var UserService = class {
|
|
|
90
128
|
);
|
|
91
129
|
return result.data.listCourseEnrolledByUserIdAndCreatedAt.items;
|
|
92
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
|
+
}
|
|
93
160
|
};
|
|
94
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
|
+
|
|
95
184
|
// src/services/question.service.ts
|
|
96
185
|
var QuestionService = class {
|
|
97
186
|
constructor(network) {
|
|
@@ -99,8 +188,8 @@ var QuestionService = class {
|
|
|
99
188
|
}
|
|
100
189
|
async create(input) {
|
|
101
190
|
const result = await this.network.mutate(
|
|
102
|
-
`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) {
|
|
103
|
-
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) {
|
|
104
193
|
status questionId
|
|
105
194
|
}
|
|
106
195
|
}`,
|
|
@@ -112,7 +201,7 @@ var QuestionService = class {
|
|
|
112
201
|
const result = await this.network.query(
|
|
113
202
|
`query GetQuestion($id: String!) {
|
|
114
203
|
getQuestionObject(id: $id) {
|
|
115
|
-
id question imageUrl language level difficulty type voteCount
|
|
204
|
+
id question imageUrl language level publicExam difficulty type voteCount
|
|
116
205
|
verificationStatus owner packId subjectId chapterId topicId order createdAt
|
|
117
206
|
options { items { id content } }
|
|
118
207
|
tags { items { id tagId } }
|
|
@@ -126,7 +215,7 @@ var QuestionService = class {
|
|
|
126
215
|
const result = await this.network.query(
|
|
127
216
|
`query GetFullQuestion($id: String!) {
|
|
128
217
|
getQuestionObject(id: $id) {
|
|
129
|
-
id question answer explanation extra imageUrl language level difficulty type
|
|
218
|
+
id question answer explanation extra imageUrl language level publicExam difficulty type
|
|
130
219
|
markingInstructions voteCount verificationStatus owner packId
|
|
131
220
|
subjectId chapterId topicId order createdAt
|
|
132
221
|
options { items { id content } }
|
|
@@ -159,11 +248,28 @@ var QuestionService = class {
|
|
|
159
248
|
);
|
|
160
249
|
return result.data.listQuestionObjectByPackIdAndOrder.items[0]?.id ?? null;
|
|
161
250
|
}
|
|
251
|
+
async listByPack(packId, limit = 200, sortDirection = "ASC", nextToken) {
|
|
252
|
+
const result = await this.network.query(
|
|
253
|
+
`query ListQuestionsByPack($packId: ID!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
|
|
254
|
+
listQuestionObjectByPackIdAndOrder(packId: $packId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
|
|
255
|
+
items {
|
|
256
|
+
id question answer explanation extra imageUrl language level publicExam difficulty type
|
|
257
|
+
markingInstructions voteCount owner packId subjectId chapterId topicId order createdAt
|
|
258
|
+
options { items { id content } }
|
|
259
|
+
tags { items { id tagId } }
|
|
260
|
+
}
|
|
261
|
+
nextToken
|
|
262
|
+
}
|
|
263
|
+
}`,
|
|
264
|
+
{ packId, sortDirection, limit, nextToken }
|
|
265
|
+
);
|
|
266
|
+
return result.data.listQuestionObjectByPackIdAndOrder;
|
|
267
|
+
}
|
|
162
268
|
async update(id, fields) {
|
|
163
269
|
const result = await this.network.mutate(
|
|
164
270
|
`mutation UpdateQuestion($input: UpdateQuestionObjectInput!) {
|
|
165
271
|
updateQuestionObject(input: $input) {
|
|
166
|
-
id question answer explanation extra imageUrl language level difficulty type
|
|
272
|
+
id question answer explanation extra imageUrl language level publicExam difficulty type
|
|
167
273
|
markingInstructions voteCount owner packId subjectId chapterId topicId order createdAt
|
|
168
274
|
options { items { id content } }
|
|
169
275
|
}
|
|
@@ -172,6 +278,41 @@ var QuestionService = class {
|
|
|
172
278
|
);
|
|
173
279
|
return result.data.updateQuestionObject;
|
|
174
280
|
}
|
|
281
|
+
async updateOption(id, content) {
|
|
282
|
+
const result = await this.network.mutate(
|
|
283
|
+
`mutation UpdateOption($input: UpdateOptionInput!) {
|
|
284
|
+
updateOption(input: $input) {
|
|
285
|
+
id
|
|
286
|
+
content
|
|
287
|
+
}
|
|
288
|
+
}`,
|
|
289
|
+
{ input: { id, content } }
|
|
290
|
+
);
|
|
291
|
+
return result.data.updateOption;
|
|
292
|
+
}
|
|
293
|
+
async createOption(input) {
|
|
294
|
+
const result = await this.network.mutate(
|
|
295
|
+
`mutation CreateOption($input: CreateOptionInput!) {
|
|
296
|
+
createOption(input: $input) {
|
|
297
|
+
id
|
|
298
|
+
content
|
|
299
|
+
}
|
|
300
|
+
}`,
|
|
301
|
+
{ input }
|
|
302
|
+
);
|
|
303
|
+
return result.data.createOption;
|
|
304
|
+
}
|
|
305
|
+
async deleteOption(id) {
|
|
306
|
+
const result = await this.network.mutate(
|
|
307
|
+
`mutation DeleteOption($input: DeleteOptionInput!) {
|
|
308
|
+
deleteOption(input: $input) {
|
|
309
|
+
id
|
|
310
|
+
}
|
|
311
|
+
}`,
|
|
312
|
+
{ input: { id } }
|
|
313
|
+
);
|
|
314
|
+
return result.data.deleteOption;
|
|
315
|
+
}
|
|
175
316
|
async delete(id) {
|
|
176
317
|
const result = await this.network.mutate(
|
|
177
318
|
`mutation DeleteQuestion($input: DeleteQuestionObjectInput!) {
|
|
@@ -228,70 +369,288 @@ var QuestionService = class {
|
|
|
228
369
|
return result.data.addQuestionTag;
|
|
229
370
|
}
|
|
230
371
|
async listBySubject(input) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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 };
|
|
246
411
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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);
|
|
251
440
|
}
|
|
252
441
|
async listByChapter(input) {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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 };
|
|
268
481
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
|
273
510
|
}
|
|
274
511
|
async listByTopic(input) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
}
|
|
280
574
|
};
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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;
|
|
288
616
|
}
|
|
289
|
-
|
|
617
|
+
} catch {
|
|
290
618
|
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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);
|
|
295
654
|
}
|
|
296
655
|
async listByRandomHash(input) {
|
|
297
656
|
const compositeKey = [input.language, input.difficulty].filter(Boolean).join("_");
|
|
@@ -305,7 +664,7 @@ var QuestionService = class {
|
|
|
305
664
|
`query ListByRandomHash($subjectId: String!, $language_difficulty_randomHash: ModelStringKeyConditionInput, $sortDirection: ModelSortDirection, $limit: Int) {
|
|
306
665
|
listQuestionObjectBySubjectIdAndLanguage_difficulty_randomHash(subjectId: $subjectId, language_difficulty_randomHash: $language_difficulty_randomHash, sortDirection: $sortDirection, limit: $limit) {
|
|
307
666
|
items {
|
|
308
|
-
id question imageUrl language level difficulty type voteCount
|
|
667
|
+
id question imageUrl language level publicExam difficulty type voteCount
|
|
309
668
|
verificationStatus owner packId subjectId chapterId topicId createdAt
|
|
310
669
|
options { items { id content } }
|
|
311
670
|
}
|
|
@@ -316,6 +675,76 @@ var QuestionService = class {
|
|
|
316
675
|
);
|
|
317
676
|
return result.data.listQuestionObjectBySubjectIdAndLanguage_difficulty_randomHash;
|
|
318
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
|
+
}
|
|
319
748
|
};
|
|
320
749
|
|
|
321
750
|
// src/services/pack.service.ts
|
|
@@ -325,8 +754,8 @@ var PackService = class {
|
|
|
325
754
|
}
|
|
326
755
|
async create(input) {
|
|
327
756
|
const result = await this.network.mutate(
|
|
328
|
-
`mutation CreatePack($name: String!, $level: String!, $language: String!, $difficulty: String!, $automaticNumbering: Boolean, $subjectId: ID, $chapterId: ID, $topicId: ID) {
|
|
329
|
-
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) {
|
|
330
759
|
status id
|
|
331
760
|
}
|
|
332
761
|
}`,
|
|
@@ -338,7 +767,7 @@ var PackService = class {
|
|
|
338
767
|
const result = await this.network.query(
|
|
339
768
|
`query GetPack($id: ID!) {
|
|
340
769
|
getPack(id: $id) {
|
|
341
|
-
id name language level difficulty questionCount automaticNumbering
|
|
770
|
+
id name language level publicExam difficulty questionCount automaticNumbering
|
|
342
771
|
owner subjectId chapterId topicId packGroupId verificationStatus createdAt
|
|
343
772
|
tags { items { id tagId } }
|
|
344
773
|
user { id fullName avatarUrl }
|
|
@@ -352,13 +781,13 @@ var PackService = class {
|
|
|
352
781
|
const result = await this.network.query(
|
|
353
782
|
`query GetFullPack($id: ID!) {
|
|
354
783
|
getPack(id: $id) {
|
|
355
|
-
id name language level difficulty questionCount automaticNumbering
|
|
784
|
+
id name language level publicExam difficulty questionCount automaticNumbering
|
|
356
785
|
owner subjectId chapterId topicId packGroupId verificationStatus createdAt
|
|
357
786
|
tags { items { id tagId } }
|
|
358
787
|
user { id fullName avatarUrl }
|
|
359
788
|
questions(sortDirection: ASC, limit: 200) {
|
|
360
789
|
items {
|
|
361
|
-
id question answer explanation extra imageUrl language level difficulty type
|
|
790
|
+
id question answer explanation extra imageUrl language level publicExam difficulty type
|
|
362
791
|
markingInstructions voteCount owner order createdAt
|
|
363
792
|
options { items { id content } }
|
|
364
793
|
tags { items { id tagId } }
|
|
@@ -368,13 +797,33 @@ var PackService = class {
|
|
|
368
797
|
}`,
|
|
369
798
|
{ id }
|
|
370
799
|
);
|
|
371
|
-
|
|
800
|
+
const pack = result.data.getPack;
|
|
801
|
+
if (pack && pack.questions && Array.isArray(pack.questions.items)) {
|
|
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
|
+
}
|
|
808
|
+
const orderA = typeof a.order === "number" ? a.order : a.order ? Number(a.order) : 0;
|
|
809
|
+
const orderB = typeof b.order === "number" ? b.order : b.order ? Number(b.order) : 0;
|
|
810
|
+
if (orderA !== orderB) {
|
|
811
|
+
if (orderA === 0) return 1;
|
|
812
|
+
if (orderB === 0) return -1;
|
|
813
|
+
return orderA - orderB;
|
|
814
|
+
}
|
|
815
|
+
const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
|
816
|
+
const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
|
817
|
+
return timeA - timeB;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
return pack;
|
|
372
821
|
}
|
|
373
822
|
async update(id, fields) {
|
|
374
823
|
const result = await this.network.mutate(
|
|
375
824
|
`mutation UpdatePack($input: UpdatePackInput!) {
|
|
376
825
|
updatePack(input: $input) {
|
|
377
|
-
id name language level difficulty questionCount automaticNumbering
|
|
826
|
+
id name language level publicExam difficulty questionCount automaticNumbering
|
|
378
827
|
owner subjectId chapterId topicId verificationStatus createdAt
|
|
379
828
|
}
|
|
380
829
|
}`,
|
|
@@ -384,12 +833,15 @@ var PackService = class {
|
|
|
384
833
|
}
|
|
385
834
|
async delete(id) {
|
|
386
835
|
const result = await this.network.mutate(
|
|
387
|
-
`mutation
|
|
388
|
-
|
|
836
|
+
`mutation DeletePackCascade($packId: ID!) {
|
|
837
|
+
deletePackCascade(packId: $packId) { status }
|
|
389
838
|
}`,
|
|
390
|
-
{
|
|
839
|
+
{ packId: id }
|
|
391
840
|
);
|
|
392
|
-
|
|
841
|
+
if (result.data.deletePackCascade?.status === "success") {
|
|
842
|
+
return { id };
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
393
845
|
}
|
|
394
846
|
async verify(id, userId) {
|
|
395
847
|
const result = await this.network.mutate(
|
|
@@ -409,7 +861,27 @@ var PackService = class {
|
|
|
409
861
|
);
|
|
410
862
|
return result.data.createPackReport;
|
|
411
863
|
}
|
|
412
|
-
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
|
+
}
|
|
413
885
|
const result = await this.network.mutate(
|
|
414
886
|
`mutation SubmitPackExam($packId: ID!, $selectedOptions: String!) {
|
|
415
887
|
submitPackExam(packId: $packId, selectedOptions: $selectedOptions) {
|
|
@@ -426,7 +898,7 @@ var PackService = class {
|
|
|
426
898
|
listPackExamResultsByOwnerAndCreatedAt(owner: $owner, sortDirection: $sortDirection, nextToken: $nextToken) {
|
|
427
899
|
items {
|
|
428
900
|
id packId selectedOptions aiEvaluations owner createdAt
|
|
429
|
-
pack { id name language level difficulty questionCount }
|
|
901
|
+
pack { id name language level publicExam difficulty questionCount }
|
|
430
902
|
}
|
|
431
903
|
nextToken
|
|
432
904
|
}
|
|
@@ -440,7 +912,7 @@ var PackService = class {
|
|
|
440
912
|
`query GetPackExamResult($id: ID!) {
|
|
441
913
|
getPackExamResults(id: $id) {
|
|
442
914
|
id packId selectedOptions aiEvaluations owner createdAt
|
|
443
|
-
pack { id name language level difficulty questionCount }
|
|
915
|
+
pack { id name language level publicExam difficulty questionCount }
|
|
444
916
|
}
|
|
445
917
|
}`,
|
|
446
918
|
{ id: resultId }
|
|
@@ -448,79 +920,462 @@ var PackService = class {
|
|
|
448
920
|
return result.data.getPackExamResults;
|
|
449
921
|
}
|
|
450
922
|
async listBySubject(input) {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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 };
|
|
459
958
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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);
|
|
464
986
|
}
|
|
465
987
|
async listByChapter(input) {
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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 };
|
|
474
1023
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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);
|
|
479
1051
|
}
|
|
480
1052
|
async listByTopic(input) {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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;
|
|
487
1148
|
}
|
|
488
|
-
|
|
1149
|
+
} catch {
|
|
489
1150
|
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
502
1175
|
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
506
1235
|
}
|
|
507
1236
|
}`,
|
|
508
|
-
|
|
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 }
|
|
509
1291
|
);
|
|
510
|
-
|
|
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;
|
|
511
1352
|
}
|
|
512
1353
|
async getGroup(id) {
|
|
513
1354
|
const result = await this.network.query(
|
|
514
1355
|
`query GetPackGroup($id: ID!) {
|
|
515
1356
|
getPackGroup(id: $id) {
|
|
516
1357
|
id name description imageUrl type level typeLevel packCount order
|
|
517
|
-
packs { items { id name language level difficulty questionCount owner createdAt verificationStatus } }
|
|
1358
|
+
packs { items { id name language level publicExam difficulty questionCount owner createdAt verificationStatus } }
|
|
518
1359
|
}
|
|
519
1360
|
}`,
|
|
520
1361
|
{ id }
|
|
521
1362
|
);
|
|
522
1363
|
return result.data.getPackGroup;
|
|
523
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
|
+
}
|
|
524
1379
|
};
|
|
525
1380
|
|
|
526
1381
|
// src/services/contest.service.ts
|
|
@@ -878,6 +1733,28 @@ var CourseService = class {
|
|
|
878
1733
|
);
|
|
879
1734
|
return result.data.publishCourseLiveExamResults;
|
|
880
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
|
+
}
|
|
881
1758
|
};
|
|
882
1759
|
|
|
883
1760
|
// src/services/curriculum.service.ts
|
|
@@ -945,6 +1822,67 @@ var CurriculumService = class {
|
|
|
945
1822
|
);
|
|
946
1823
|
return result.data.listTopicByChapterId.items;
|
|
947
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
|
+
}
|
|
948
1886
|
};
|
|
949
1887
|
|
|
950
1888
|
// src/services/media.service.ts
|
|
@@ -954,13 +1892,34 @@ var MediaService = class {
|
|
|
954
1892
|
this.storage = storage;
|
|
955
1893
|
}
|
|
956
1894
|
async uploadImage(questionId, image) {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1895
|
+
const payloadLength = image ? image.length : 0;
|
|
1896
|
+
console.log(`[MediaService.uploadImage] Initiating upload mutation: questionId="${questionId}", payloadLength=${payloadLength} chars`);
|
|
1897
|
+
try {
|
|
1898
|
+
const result = await this.network.mutate(
|
|
1899
|
+
`mutation UploadImage($questionId: ID!, $image: String!) {
|
|
1900
|
+
uploadImage(questionId: $questionId, image: $image) { status imageUrl }
|
|
1901
|
+
}`,
|
|
1902
|
+
{ questionId, image }
|
|
1903
|
+
);
|
|
1904
|
+
if (result.errors && result.errors.length > 0) {
|
|
1905
|
+
console.error(
|
|
1906
|
+
`[MediaService.uploadImage] GraphQL errors returned for questionId "${questionId}":`,
|
|
1907
|
+
JSON.stringify(result.errors, null, 2)
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
const response = result.data?.uploadImage;
|
|
1911
|
+
if (!response) {
|
|
1912
|
+
console.error(`[MediaService.uploadImage] No uploadImage object returned in GraphQL response for questionId "${questionId}". Full result:`, result);
|
|
1913
|
+
} else if (response.status === "Error" || response.status === "error" || !response.imageUrl) {
|
|
1914
|
+
console.error(`[MediaService.uploadImage] Backend returned error status or missing imageUrl for questionId "${questionId}":`, response);
|
|
1915
|
+
} else {
|
|
1916
|
+
console.log(`[MediaService.uploadImage] Upload mutation success for questionId "${questionId}": imageUrl="${response.imageUrl}"`);
|
|
1917
|
+
}
|
|
1918
|
+
return response;
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
console.error(`[MediaService.uploadImage] Network/GraphQL mutation failed for questionId "${questionId}":`, err);
|
|
1921
|
+
throw err;
|
|
1922
|
+
}
|
|
964
1923
|
}
|
|
965
1924
|
async deleteImage(imageUrl) {
|
|
966
1925
|
const result = await this.network.mutate(
|
|
@@ -997,149 +1956,449 @@ var MediaService = class {
|
|
|
997
1956
|
if (!this.storage) throw new Error("StorageProvider not configured");
|
|
998
1957
|
return this.storage.uploadFile(path, data, options);
|
|
999
1958
|
}
|
|
1000
|
-
async createVideo(input) {
|
|
1959
|
+
async createVideo(input) {
|
|
1960
|
+
const result = await this.network.query(
|
|
1961
|
+
`query CreateCourseVideo($courseId: ID!, $fileName: String!, $fileType: String!) {
|
|
1962
|
+
createCourseVideo(courseId: $courseId, fileName: $fileName, fileType: $fileType) { status videoId }
|
|
1963
|
+
}`,
|
|
1964
|
+
input
|
|
1965
|
+
);
|
|
1966
|
+
return result.data.createCourseVideo;
|
|
1967
|
+
}
|
|
1968
|
+
async uploadVideoToBunny(input) {
|
|
1969
|
+
const result = await this.network.mutate(
|
|
1970
|
+
`mutation UploadR2VideoToBunny($key: String!, $videoId: String!, $fileType: String!) {
|
|
1971
|
+
uploadR2VideoToBunny(key: $key, videoId: $videoId, fileType: $fileType) { status }
|
|
1972
|
+
}`,
|
|
1973
|
+
input
|
|
1974
|
+
);
|
|
1975
|
+
return result.data.uploadR2VideoToBunny;
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
|
|
1979
|
+
// src/services/progress.service.ts
|
|
1980
|
+
var ProgressService = class {
|
|
1981
|
+
constructor(network) {
|
|
1982
|
+
this.network = network;
|
|
1983
|
+
}
|
|
1984
|
+
async track(input) {
|
|
1985
|
+
const variables = {
|
|
1986
|
+
questionId: input.questionId,
|
|
1987
|
+
isCorrect: input.isCorrect,
|
|
1988
|
+
subjectId: input.subjectId ?? null,
|
|
1989
|
+
chapterId: input.chapterId ?? null,
|
|
1990
|
+
topicId: input.topicId ?? null
|
|
1991
|
+
};
|
|
1992
|
+
const result = await this.network.mutate(
|
|
1993
|
+
`mutation TrackProgress($questionId: ID!, $isCorrect: Boolean!, $subjectId: ID, $chapterId: ID, $topicId: ID) {
|
|
1994
|
+
trackPracticeProgress(questionId: $questionId, isCorrect: $isCorrect, subjectId: $subjectId, chapterId: $chapterId, topicId: $topicId) { status }
|
|
1995
|
+
}`,
|
|
1996
|
+
variables
|
|
1997
|
+
);
|
|
1998
|
+
return result.data?.trackPracticeProgress || { status: "success" };
|
|
1999
|
+
}
|
|
2000
|
+
async getSubjectProgress(userId) {
|
|
2001
|
+
try {
|
|
2002
|
+
const result = await this.network.query(
|
|
2003
|
+
`query ListSubjectProgress($userId: String!) {
|
|
2004
|
+
listUserSubjectProgressByUserIdAndSubjectId(userId: $userId) {
|
|
2005
|
+
items { id userId subjectId practicedCount mistakeCount correctedCount accuracyScore }
|
|
2006
|
+
}
|
|
2007
|
+
}`,
|
|
2008
|
+
{ userId }
|
|
2009
|
+
);
|
|
2010
|
+
return result.data?.listUserSubjectProgressByUserIdAndSubjectId?.items || result.data?.listUserSubjectProgressesByUserIdAndSubjectId?.items || [];
|
|
2011
|
+
} catch (e) {
|
|
2012
|
+
console.error("Error fetching subject progress:", e);
|
|
2013
|
+
return [];
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
async getChapterProgress(userId) {
|
|
2017
|
+
try {
|
|
2018
|
+
const result = await this.network.query(
|
|
2019
|
+
`query ListChapterProgress($userId: String!) {
|
|
2020
|
+
listUserChapterProgressByUserIdAndChapterId(userId: $userId) {
|
|
2021
|
+
items { id userId chapterId practicedCount mistakeCount correctedCount accuracyScore }
|
|
2022
|
+
}
|
|
2023
|
+
}`,
|
|
2024
|
+
{ userId }
|
|
2025
|
+
);
|
|
2026
|
+
return result.data?.listUserChapterProgressByUserIdAndChapterId?.items || result.data?.listUserChapterProgressesByUserIdAndChapterId?.items || [];
|
|
2027
|
+
} catch (e) {
|
|
2028
|
+
console.error("Error fetching chapter progress:", e);
|
|
2029
|
+
return [];
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
async getTopicProgress(userId) {
|
|
2033
|
+
try {
|
|
2034
|
+
const result = await this.network.query(
|
|
2035
|
+
`query ListTopicProgress($userId: String!) {
|
|
2036
|
+
listUserTopicProgressByUserIdAndTopicId(userId: $userId) {
|
|
2037
|
+
items { id userId topicId practicedCount mistakeCount correctedCount accuracyScore }
|
|
2038
|
+
}
|
|
2039
|
+
}`,
|
|
2040
|
+
{ userId }
|
|
2041
|
+
);
|
|
2042
|
+
return result.data?.listUserTopicProgressByUserIdAndTopicId?.items || result.data?.listUserTopicProgressesByUserIdAndTopicId?.items || [];
|
|
2043
|
+
} catch (e) {
|
|
2044
|
+
console.error("Error fetching topic progress:", e);
|
|
2045
|
+
return [];
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
async getTopicMetrics(userId) {
|
|
2049
|
+
try {
|
|
2050
|
+
const result = await this.network.query(
|
|
2051
|
+
`query ListTopicMetrics($filter: ModelUserTopicMetricFilterInput) {
|
|
2052
|
+
listUserTopicMetrics(filter: $filter) {
|
|
2053
|
+
items { id userId topicId totalAttempted totalCorrect accuracy }
|
|
2054
|
+
}
|
|
2055
|
+
}`,
|
|
2056
|
+
{ filter: { userId: { eq: userId } } }
|
|
2057
|
+
);
|
|
2058
|
+
return result.data?.listUserTopicMetrics?.items || [];
|
|
2059
|
+
} catch (e) {
|
|
2060
|
+
console.error("Error fetching topic metrics:", e);
|
|
2061
|
+
return [];
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
async getMistakenQuestions(userId, status = "MISTAKE") {
|
|
2065
|
+
try {
|
|
2066
|
+
const result = await this.network.query(
|
|
2067
|
+
`query ListMistakes($userId: String!, $filter: ModelQuestionTrackingFilterInput) {
|
|
2068
|
+
listQuestionTrackingByUserIdAndQuestionId(userId: $userId, filter: $filter) {
|
|
2069
|
+
items { id userId questionId subjectId chapterId topicId status attempts }
|
|
2070
|
+
}
|
|
2071
|
+
}`,
|
|
2072
|
+
{ userId, filter: { status: { eq: status } } }
|
|
2073
|
+
);
|
|
2074
|
+
return result.data?.listQuestionTrackingByUserIdAndQuestionId?.items || result.data?.listQuestionTrackingsByUserIdAndQuestionId?.items || result.data?.listQuestionTrackings?.items || [];
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
console.error("Error fetching mistaken questions:", e);
|
|
2077
|
+
return [];
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
async getCorrectedQuestions(userId) {
|
|
2081
|
+
return this.getMistakenQuestions(userId, "CORRECTED");
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
// src/services/ai.service.ts
|
|
2086
|
+
var AIService = class {
|
|
2087
|
+
constructor(network) {
|
|
2088
|
+
this.network = network;
|
|
2089
|
+
}
|
|
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
|
+
}
|
|
2111
|
+
const result = await this.network.query(
|
|
2112
|
+
`query EvaluateWrittenAnswer($question: String!, $userAnswer: String!, $correctAnswer: String!, $markingInstructions: String, $topicId: String, $topicName: String, $questionImageUrl: String) {
|
|
2113
|
+
evaluateWrittenAnswer(question: $question, userAnswer: $userAnswer, correctAnswer: $correctAnswer, markingInstructions: $markingInstructions, topicId: $topicId, topicName: $topicName, questionImageUrl: $questionImageUrl) {
|
|
2114
|
+
score maxScore feedback isCorrect markingExplanation improvementFeedback subquestions markLocation
|
|
2115
|
+
}
|
|
2116
|
+
}`,
|
|
2117
|
+
input
|
|
2118
|
+
);
|
|
2119
|
+
return result.data.evaluateWrittenAnswer;
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
// src/services/news.service.ts
|
|
2124
|
+
var NewsService = class {
|
|
2125
|
+
constructor(network) {
|
|
2126
|
+
this.network = network;
|
|
2127
|
+
}
|
|
2128
|
+
async create(title, content, image, category, excerpt) {
|
|
2129
|
+
const result = await this.network.mutate(
|
|
2130
|
+
`mutation CreateNews($input: CreateNewsArticleInput!) {
|
|
2131
|
+
createNewsArticle(input: $input) { id title content image category excerpt createdAt updatedAt }
|
|
2132
|
+
}`,
|
|
2133
|
+
{ input: { title, content, image, category, excerpt } }
|
|
2134
|
+
);
|
|
2135
|
+
return result.data.createNewsArticle;
|
|
2136
|
+
}
|
|
2137
|
+
async list(nextToken) {
|
|
1001
2138
|
const result = await this.network.query(
|
|
1002
|
-
`query
|
|
1003
|
-
|
|
2139
|
+
`query ListNews($nextToken: String) {
|
|
2140
|
+
listNewsArticles(nextToken: $nextToken) {
|
|
2141
|
+
items { id title content image category excerpt createdAt updatedAt }
|
|
2142
|
+
nextToken
|
|
2143
|
+
}
|
|
1004
2144
|
}`,
|
|
1005
|
-
|
|
2145
|
+
{ nextToken }
|
|
1006
2146
|
);
|
|
1007
|
-
return result.data.
|
|
2147
|
+
return result.data.listNewsArticles;
|
|
1008
2148
|
}
|
|
1009
|
-
async
|
|
1010
|
-
const result = await this.network.
|
|
1011
|
-
`
|
|
1012
|
-
|
|
2149
|
+
async get(id) {
|
|
2150
|
+
const result = await this.network.query(
|
|
2151
|
+
`query GetNews($id: ID!) {
|
|
2152
|
+
getNewsArticle(id: $id) { id title content image category excerpt createdAt updatedAt }
|
|
1013
2153
|
}`,
|
|
1014
|
-
|
|
2154
|
+
{ id }
|
|
1015
2155
|
);
|
|
1016
|
-
return result.data.
|
|
2156
|
+
return result.data.getNewsArticle;
|
|
1017
2157
|
}
|
|
1018
2158
|
};
|
|
1019
2159
|
|
|
1020
|
-
// src/services/
|
|
1021
|
-
var
|
|
2160
|
+
// src/services/notification.service.ts
|
|
2161
|
+
var NotificationService = class {
|
|
1022
2162
|
constructor(network) {
|
|
1023
2163
|
this.network = network;
|
|
1024
2164
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
+
}
|
|
1029
2177
|
}`,
|
|
1030
|
-
|
|
2178
|
+
{ limit, nextToken }
|
|
1031
2179
|
);
|
|
1032
|
-
return result.data.
|
|
2180
|
+
return result.data.listAnnouncements ?? { items: [], nextToken: null };
|
|
1033
2181
|
}
|
|
1034
|
-
|
|
2182
|
+
/**
|
|
2183
|
+
* List announcements by category.
|
|
2184
|
+
*/
|
|
2185
|
+
async listAnnouncementsByCategory(category = "ANNOUNCEMENT", limit = 20, nextToken) {
|
|
1035
2186
|
const result = await this.network.query(
|
|
1036
|
-
`query
|
|
1037
|
-
|
|
1038
|
-
items {
|
|
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
|
|
1039
2193
|
}
|
|
1040
2194
|
}`,
|
|
1041
|
-
{
|
|
2195
|
+
{ category, limit, nextToken, sortDirection: "DESC" }
|
|
1042
2196
|
);
|
|
1043
|
-
return result.data.
|
|
2197
|
+
return result.data.listAnnouncementByCategoryAndCreatedAt ?? {
|
|
2198
|
+
items: [],
|
|
2199
|
+
nextToken: null
|
|
2200
|
+
};
|
|
1044
2201
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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"
|
|
1050
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 }
|
|
1051
2230
|
}`,
|
|
1052
|
-
{
|
|
2231
|
+
{ input: { id } }
|
|
1053
2232
|
);
|
|
1054
|
-
return result.data.
|
|
2233
|
+
return Boolean(result.data.deleteAnnouncement?.id);
|
|
1055
2234
|
}
|
|
1056
|
-
|
|
2235
|
+
/**
|
|
2236
|
+
* List user notifications for a specific user ID.
|
|
2237
|
+
*/
|
|
2238
|
+
async listUserNotifications(userId, limit = 20, nextToken) {
|
|
1057
2239
|
const result = await this.network.query(
|
|
1058
|
-
`query
|
|
1059
|
-
|
|
1060
|
-
items {
|
|
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
|
|
1061
2246
|
}
|
|
1062
2247
|
}`,
|
|
1063
|
-
{ userId }
|
|
2248
|
+
{ userId, limit, nextToken, sortDirection: "DESC" }
|
|
1064
2249
|
);
|
|
1065
|
-
return result.data.
|
|
2250
|
+
return result.data.listUserNotificationByUserIdAndCreatedAt ?? {
|
|
2251
|
+
items: [],
|
|
2252
|
+
nextToken: null
|
|
2253
|
+
};
|
|
1066
2254
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
|
1072
2263
|
}
|
|
1073
2264
|
}`,
|
|
1074
|
-
{
|
|
2265
|
+
{
|
|
2266
|
+
input: {
|
|
2267
|
+
...input,
|
|
2268
|
+
isRead: false
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
1075
2271
|
);
|
|
1076
|
-
return result.data.
|
|
2272
|
+
return result.data.createUserNotification;
|
|
1077
2273
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
|
1083
2283
|
}
|
|
1084
2284
|
}`,
|
|
1085
|
-
{
|
|
2285
|
+
{
|
|
2286
|
+
input: {
|
|
2287
|
+
id,
|
|
2288
|
+
isRead: true,
|
|
2289
|
+
readAt: now
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
1086
2292
|
);
|
|
1087
|
-
return result.data.
|
|
2293
|
+
return result.data.updateUserNotification;
|
|
1088
2294
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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);
|
|
1095
2306
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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 }
|
|
1102
2314
|
}`,
|
|
1103
|
-
input
|
|
2315
|
+
{ input: { id: userId, lastReadAnnouncementAt: timestamp } }
|
|
1104
2316
|
);
|
|
1105
|
-
return result.data.
|
|
2317
|
+
return Boolean(result.data.updateUser?.id);
|
|
1106
2318
|
}
|
|
1107
2319
|
};
|
|
1108
2320
|
|
|
1109
|
-
// src/services/
|
|
1110
|
-
var
|
|
2321
|
+
// src/services/subscription.service.ts
|
|
2322
|
+
var SubscriptionService = class {
|
|
1111
2323
|
constructor(network) {
|
|
1112
2324
|
this.network = network;
|
|
1113
2325
|
}
|
|
1114
|
-
async
|
|
2326
|
+
async createRequest(input) {
|
|
1115
2327
|
const result = await this.network.mutate(
|
|
1116
|
-
`mutation
|
|
1117
|
-
|
|
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
|
+
}
|
|
1118
2332
|
}`,
|
|
1119
|
-
{ input
|
|
2333
|
+
{ input }
|
|
1120
2334
|
);
|
|
1121
|
-
return result.data.
|
|
2335
|
+
return result.data.createSubscriptionRequest;
|
|
1122
2336
|
}
|
|
1123
|
-
async
|
|
2337
|
+
async getRequest(id) {
|
|
1124
2338
|
const result = await this.network.query(
|
|
1125
|
-
`query
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
nextToken
|
|
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
|
|
1129
2342
|
}
|
|
1130
2343
|
}`,
|
|
1131
|
-
{
|
|
2344
|
+
{ id }
|
|
1132
2345
|
);
|
|
1133
|
-
return result.data.
|
|
2346
|
+
return result.data.getSubscriptionRequest;
|
|
1134
2347
|
}
|
|
1135
|
-
async
|
|
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
|
+
}
|
|
1136
2379
|
const result = await this.network.query(
|
|
1137
|
-
`query
|
|
1138
|
-
|
|
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
|
+
}
|
|
1139
2387
|
}`,
|
|
1140
|
-
{
|
|
2388
|
+
{ limit, nextToken: input.nextToken, filter: input.filter }
|
|
1141
2389
|
);
|
|
1142
|
-
return result.data.
|
|
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;
|
|
1143
2402
|
}
|
|
1144
2403
|
};
|
|
1145
2404
|
|
|
@@ -1147,6 +2406,8 @@ var NewsService = class {
|
|
|
1147
2406
|
var OrjokClient = class {
|
|
1148
2407
|
constructor(config) {
|
|
1149
2408
|
this.auth = config.authProvider;
|
|
2409
|
+
this.network = config.networkProvider;
|
|
2410
|
+
this.storage = config.storageProvider;
|
|
1150
2411
|
this.users = new UserService(config.networkProvider);
|
|
1151
2412
|
this.questions = new QuestionService(config.networkProvider);
|
|
1152
2413
|
this.packs = new PackService(config.networkProvider);
|
|
@@ -1157,12 +2418,218 @@ var OrjokClient = class {
|
|
|
1157
2418
|
this.progress = new ProgressService(config.networkProvider);
|
|
1158
2419
|
this.ai = new AIService(config.networkProvider);
|
|
1159
2420
|
this.news = new NewsService(config.networkProvider);
|
|
2421
|
+
this.notifications = new NotificationService(config.networkProvider);
|
|
2422
|
+
this.subscriptions = new SubscriptionService(config.networkProvider);
|
|
1160
2423
|
}
|
|
1161
2424
|
};
|
|
1162
2425
|
function createOrjokClient(config) {
|
|
1163
2426
|
return new OrjokClient(config);
|
|
1164
2427
|
}
|
|
1165
2428
|
|
|
2429
|
+
// src/logic/avatar.ts
|
|
2430
|
+
var HAIR_COLORS = [
|
|
2431
|
+
{ name: "Original", filter: "none", hex: "#e2e8f0", brightness: 1 },
|
|
2432
|
+
{ name: "White", filter: "grayscale(100%) brightness(500%)", hex: "#ffffff", brightness: 2 },
|
|
2433
|
+
{ name: "Black", filter: "grayscale(100%) brightness(40%)", hex: "#1a202c", brightness: 0.15 },
|
|
2434
|
+
{ name: "Brown", filter: "sepia(100%) saturate(300%) hue-rotate(330deg) brightness(80%)", hex: "#7b3f00", brightness: 0.35 },
|
|
2435
|
+
{ name: "Blonde", filter: "sepia(100%) saturate(400%) hue-rotate(20deg) brightness(130%)", hex: "#e6c27a", brightness: 0.8 },
|
|
2436
|
+
{ name: "Red", filter: "sepia(100%) saturate(500%) hue-rotate(320deg) brightness(90%)", hex: "#9b2c2c", brightness: 0.4 },
|
|
2437
|
+
{ name: "Blue", filter: "sepia(100%) saturate(500%) hue-rotate(180deg) brightness(90%)", hex: "#2b6cb0", brightness: 0.4 },
|
|
2438
|
+
{ name: "Green", filter: "sepia(100%) saturate(400%) hue-rotate(80deg) brightness(90%)", hex: "#2f855a", brightness: 0.45 }
|
|
2439
|
+
];
|
|
2440
|
+
var CLOTHES_COLORS = [
|
|
2441
|
+
{ name: "Original", filter: "none", hex: "#e2e8f0", brightness: 1 },
|
|
2442
|
+
{ name: "White", filter: "grayscale(100%) brightness(500%)", hex: "#ffffff", brightness: 2 },
|
|
2443
|
+
{ name: "Black", filter: "grayscale(100%) brightness(30%)", hex: "#1a202c", brightness: 0.12 },
|
|
2444
|
+
{ name: "Red", filter: "sepia(100%) saturate(500%) hue-rotate(320deg)", hex: "#c53030", brightness: 0.45 },
|
|
2445
|
+
{ name: "Orange", filter: "sepia(100%) saturate(500%) hue-rotate(350deg)", hex: "#dd6b20", brightness: 0.55 },
|
|
2446
|
+
{ name: "Yellow", filter: "sepia(100%) saturate(500%) hue-rotate(20deg) brightness(120%)", hex: "#d69e2e", brightness: 0.8 },
|
|
2447
|
+
{ name: "Green", filter: "sepia(100%) saturate(500%) hue-rotate(80deg)", hex: "#38a169", brightness: 0.55 },
|
|
2448
|
+
{ name: "Blue", filter: "sepia(100%) saturate(500%) hue-rotate(180deg)", hex: "#3182ce", brightness: 0.45 },
|
|
2449
|
+
{ name: "Purple", filter: "sepia(100%) saturate(500%) hue-rotate(240deg)", hex: "#805ad5", brightness: 0.45 },
|
|
2450
|
+
{ name: "Pink", filter: "sepia(100%) saturate(400%) hue-rotate(290deg)", hex: "#d53f8c", brightness: 0.55 }
|
|
2451
|
+
];
|
|
2452
|
+
var COLOR_HUES = {
|
|
2453
|
+
Original: 0,
|
|
2454
|
+
Red: 0,
|
|
2455
|
+
Orange: 24,
|
|
2456
|
+
Yellow: 45,
|
|
2457
|
+
Green: 140,
|
|
2458
|
+
Blue: 210,
|
|
2459
|
+
Purple: 270,
|
|
2460
|
+
Pink: 320,
|
|
2461
|
+
Brown: 25,
|
|
2462
|
+
Blonde: 45
|
|
2463
|
+
};
|
|
2464
|
+
var RENDER_ORDER = [
|
|
2465
|
+
"Background",
|
|
2466
|
+
"Hair Back",
|
|
2467
|
+
"Clothes Back",
|
|
2468
|
+
"Skin Color Body",
|
|
2469
|
+
"Clothes Front",
|
|
2470
|
+
"Skin Color Head",
|
|
2471
|
+
"Accessories",
|
|
2472
|
+
"Facial Expression",
|
|
2473
|
+
"Beard",
|
|
2474
|
+
"Hair Front",
|
|
2475
|
+
"Eyewear",
|
|
2476
|
+
"Headwears"
|
|
2477
|
+
];
|
|
2478
|
+
var INITIAL_AVATAR_DATA = {
|
|
2479
|
+
Background: [],
|
|
2480
|
+
Hair: [],
|
|
2481
|
+
"Skin Color": [],
|
|
2482
|
+
Clothes: [],
|
|
2483
|
+
Accessories: [],
|
|
2484
|
+
Headwears: [],
|
|
2485
|
+
Eyewear: [],
|
|
2486
|
+
"Facial Expression": [],
|
|
2487
|
+
Beard: []
|
|
2488
|
+
};
|
|
2489
|
+
function getActualOptionFileName(category, baseName, config, selections) {
|
|
2490
|
+
if (baseName === "None") return null;
|
|
2491
|
+
const items = config[category] || [];
|
|
2492
|
+
const matched = items.filter((item) => item.base_name === baseName);
|
|
2493
|
+
if (matched.length === 0) return null;
|
|
2494
|
+
for (const item of matched) {
|
|
2495
|
+
if (item.conditions && item.conditions.length > 0) {
|
|
2496
|
+
const allMet = item.conditions.every((cond) => {
|
|
2497
|
+
return Object.values(selections).some(
|
|
2498
|
+
(sel) => sel && (sel.includes(cond) || cond.includes(sel))
|
|
2499
|
+
);
|
|
2500
|
+
});
|
|
2501
|
+
if (allMet) return item;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
return matched.find((item) => !item.conditions || item.conditions.length === 0) || matched[0] || null;
|
|
2505
|
+
}
|
|
2506
|
+
function parseAvatarConfig(configStr) {
|
|
2507
|
+
if (!configStr) {
|
|
2508
|
+
return { selections: {}, colors: {} };
|
|
2509
|
+
}
|
|
2510
|
+
try {
|
|
2511
|
+
const parsed = JSON.parse(configStr);
|
|
2512
|
+
return {
|
|
2513
|
+
selections: parsed.selections || {},
|
|
2514
|
+
colors: parsed.colors || {},
|
|
2515
|
+
gender: parsed.gender
|
|
2516
|
+
};
|
|
2517
|
+
} catch {
|
|
2518
|
+
return { selections: {}, colors: {} };
|
|
2519
|
+
}
|
|
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
|
+
}
|
|
2533
|
+
function serializeAvatarConfig(state) {
|
|
2534
|
+
return JSON.stringify(state);
|
|
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
|
+
}
|
|
2632
|
+
|
|
1166
2633
|
// src/logic/elo.ts
|
|
1167
2634
|
var TIERS = [
|
|
1168
2635
|
{ name: "Iron", minRating: 0, maxRating: 799, color: "#6B7280" },
|
|
@@ -1263,6 +2730,246 @@ function detectQuestionType(option2, option3, option4) {
|
|
|
1263
2730
|
}
|
|
1264
2731
|
return "MCQ";
|
|
1265
2732
|
}
|
|
2733
|
+
function isCorrectAnswer(selected, correct) {
|
|
2734
|
+
if (!selected || !correct) return false;
|
|
2735
|
+
const selTrim = String(selected).trim();
|
|
2736
|
+
const corrTrim = String(correct).trim();
|
|
2737
|
+
if (selTrim === corrTrim) return true;
|
|
2738
|
+
const selNum = Number(selTrim);
|
|
2739
|
+
const corrNum = Number(corrTrim);
|
|
2740
|
+
if (!isNaN(selNum) && !isNaN(corrNum) && selNum === corrNum) {
|
|
2741
|
+
return true;
|
|
2742
|
+
}
|
|
2743
|
+
return false;
|
|
2744
|
+
}
|
|
2745
|
+
function calculateDefaultExamMinutes(mode, questions) {
|
|
2746
|
+
const mcqCount = questions.filter((q) => q.type !== "WRITTEN").length;
|
|
2747
|
+
const writtenCount = questions.filter((q) => q.type === "WRITTEN").length;
|
|
2748
|
+
if (mode === "MCQ") return Math.max(1, mcqCount * 1);
|
|
2749
|
+
if (mode === "WRITTEN") return Math.max(1, writtenCount * 10);
|
|
2750
|
+
return Math.max(1, mcqCount * 1 + writtenCount * 10);
|
|
2751
|
+
}
|
|
2752
|
+
function reorderObjectKeys(obj, oldIndex, newIndex, length) {
|
|
2753
|
+
const arr = [];
|
|
2754
|
+
for (let i = 0; i < length; i++) {
|
|
2755
|
+
arr.push(obj[i]);
|
|
2756
|
+
}
|
|
2757
|
+
const [movedItem] = arr.splice(oldIndex, 1);
|
|
2758
|
+
arr.splice(newIndex, 0, movedItem);
|
|
2759
|
+
const newObj = {};
|
|
2760
|
+
arr.forEach((item, idx) => {
|
|
2761
|
+
if (item !== void 0) {
|
|
2762
|
+
newObj[idx] = item;
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
return newObj;
|
|
2766
|
+
}
|
|
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
|
+
}
|
|
1266
2973
|
|
|
1267
2974
|
// src/logic/import-export.ts
|
|
1268
2975
|
var MAX_QUESTIONS = 200;
|
|
@@ -1468,126 +3175,281 @@ function formatQuestionsToCsv(questions, packSubjectId, packChapterId, packTopic
|
|
|
1468
3175
|
${rows.join("\n")}`;
|
|
1469
3176
|
}
|
|
1470
3177
|
|
|
1471
|
-
// src/logic/
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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 }
|
|
1486
3201
|
}
|
|
1487
|
-
|
|
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"
|
|
1488
3266
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
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"
|
|
1491
3294
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
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;
|
|
1494
3307
|
}
|
|
1495
|
-
};
|
|
1496
|
-
var warnedNoStorage = false;
|
|
1497
|
-
async function resolveMediaUrl(value, storage, cache) {
|
|
1498
|
-
if (!value || value.length === 0) return null;
|
|
1499
|
-
if (!isStorageKey(value)) return value;
|
|
1500
|
-
if (cache) {
|
|
1501
|
-
const cached = cache.get(value);
|
|
1502
|
-
if (cached) return cached;
|
|
1503
|
-
}
|
|
1504
|
-
if (!storage) {
|
|
1505
|
-
if (!warnedNoStorage) {
|
|
1506
|
-
warnedNoStorage = true;
|
|
1507
|
-
console.warn("@orjok/commons: resolveMediaUrl called without a StorageProvider \u2014 returning raw key");
|
|
1508
|
-
}
|
|
1509
|
-
return value;
|
|
1510
|
-
}
|
|
1511
|
-
const { url } = await storage.getFileUrl(value);
|
|
1512
|
-
cache?.set(value, url);
|
|
1513
|
-
return url;
|
|
1514
|
-
}
|
|
1515
|
-
async function resolveMediaUrls(obj, fields, storage, cache) {
|
|
1516
|
-
const copy = { ...obj };
|
|
1517
|
-
await Promise.all(
|
|
1518
|
-
fields.map(async (field) => {
|
|
1519
|
-
const val = copy[field];
|
|
1520
|
-
if (typeof val === "string") {
|
|
1521
|
-
copy[field] = await resolveMediaUrl(val, storage, cache);
|
|
1522
|
-
}
|
|
1523
|
-
})
|
|
1524
|
-
);
|
|
1525
|
-
return copy;
|
|
1526
3308
|
}
|
|
1527
|
-
function
|
|
1528
|
-
|
|
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);
|
|
1529
3314
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
let totalMistakes = 0;
|
|
1535
|
-
let totalCorrected = 0;
|
|
1536
|
-
for (const p of progress) {
|
|
1537
|
-
totalPracticed += p.practicedCount ?? 0;
|
|
1538
|
-
totalMistakes += p.mistakeCount ?? 0;
|
|
1539
|
-
totalCorrected += p.correctedCount ?? 0;
|
|
1540
|
-
}
|
|
1541
|
-
const accuracy = totalPracticed > 0 ? Math.round((totalPracticed - totalMistakes) / totalPracticed * 100) : 0;
|
|
1542
|
-
return { totalPracticed, totalMistakes, totalCorrected, accuracy };
|
|
1543
|
-
}
|
|
1544
|
-
function aggregateChapterMetrics(progress) {
|
|
1545
|
-
let totalPracticed = 0;
|
|
1546
|
-
let totalMistakes = 0;
|
|
1547
|
-
let totalCorrected = 0;
|
|
1548
|
-
for (const p of progress) {
|
|
1549
|
-
totalPracticed += p.practicedCount ?? 0;
|
|
1550
|
-
totalMistakes += p.mistakeCount ?? 0;
|
|
1551
|
-
totalCorrected += p.correctedCount ?? 0;
|
|
1552
|
-
}
|
|
1553
|
-
const accuracy = totalPracticed > 0 ? Math.round((totalPracticed - totalMistakes) / totalPracticed * 100) : 0;
|
|
1554
|
-
return { totalPracticed, totalMistakes, totalCorrected, accuracy };
|
|
1555
|
-
}
|
|
1556
|
-
function aggregateTopicMetrics(progress) {
|
|
1557
|
-
let totalPracticed = 0;
|
|
1558
|
-
let totalMistakes = 0;
|
|
1559
|
-
let totalCorrected = 0;
|
|
1560
|
-
for (const p of progress) {
|
|
1561
|
-
totalPracticed += p.practicedCount ?? 0;
|
|
1562
|
-
totalMistakes += p.mistakeCount ?? 0;
|
|
1563
|
-
totalCorrected += p.correctedCount ?? 0;
|
|
1564
|
-
}
|
|
1565
|
-
const accuracy = totalPracticed > 0 ? Math.round((totalPracticed - totalMistakes) / totalPracticed * 100) : 0;
|
|
1566
|
-
return { totalPracticed, totalMistakes, totalCorrected, accuracy };
|
|
1567
|
-
}
|
|
1568
|
-
function calculateAccuracy(practiced, mistakes) {
|
|
1569
|
-
if (practiced <= 0) return 0;
|
|
1570
|
-
return Math.round((practiced - mistakes) / practiced * 100);
|
|
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;
|
|
1571
3319
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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;
|
|
1581
3338
|
}
|
|
1582
|
-
return result;
|
|
1583
3339
|
}
|
|
1584
|
-
function
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
+
|
|
3422
|
+
// src/navigation.ts
|
|
3423
|
+
function getActiveTab(pathname) {
|
|
3424
|
+
const normalized = pathname.toLowerCase();
|
|
3425
|
+
if (normalized.startsWith("/contests") || normalized.startsWith("/contest")) {
|
|
3426
|
+
return "contests";
|
|
1590
3427
|
}
|
|
3428
|
+
if (normalized.startsWith("/courses") || normalized.startsWith("/course") || normalized.startsWith("/exams")) {
|
|
3429
|
+
return "courses";
|
|
3430
|
+
}
|
|
3431
|
+
if (normalized.startsWith("/profile") || normalized.startsWith("/dashboard") || normalized.startsWith("/edit-profile")) {
|
|
3432
|
+
return "profile";
|
|
3433
|
+
}
|
|
3434
|
+
if (normalized.startsWith("/learn") || normalized.startsWith("/practice") || normalized.startsWith("/questions") || normalized.startsWith("/question") || normalized.startsWith("/bank") || normalized.startsWith("/question-bank") || normalized.startsWith("/packs") || normalized.startsWith("/pack") || normalized.startsWith("/taxonomy") || normalized.startsWith("/explore")) {
|
|
3435
|
+
return "learn";
|
|
3436
|
+
}
|
|
3437
|
+
return "home";
|
|
3438
|
+
}
|
|
3439
|
+
function getCanonicalPath(pathname) {
|
|
3440
|
+
const normalized = pathname.toLowerCase();
|
|
3441
|
+
if (normalized === "/explore/questions") return "/learn/questions";
|
|
3442
|
+
if (normalized === "/explore/packs") return "/learn/packs";
|
|
3443
|
+
if (normalized === "/question-bank") return "/learn/bank";
|
|
3444
|
+
if (normalized === "/contest/all") return "/contests";
|
|
3445
|
+
if (normalized === "/dashboard") return "/profile";
|
|
3446
|
+
if (normalized === "/edit-profile") return "/profile/edit";
|
|
3447
|
+
return pathname;
|
|
3448
|
+
}
|
|
3449
|
+
function isAuthGatedRoute(pathname) {
|
|
3450
|
+
const normalized = pathname.toLowerCase();
|
|
3451
|
+
const isProtectedPath = normalized.includes("/exam") || normalized.includes("/enroll") || normalized.startsWith("/profile") || normalized.startsWith("/dashboard") || normalized.startsWith("/edit-profile") || normalized.startsWith("/avatar-creator") || normalized.startsWith("/practice");
|
|
3452
|
+
return isProtectedPath;
|
|
1591
3453
|
}
|
|
1592
3454
|
|
|
1593
3455
|
// src/utils/bangla-numbers.ts
|
|
@@ -1600,15 +3462,51 @@ function engToBanglaNumber(num) {
|
|
|
1600
3462
|
}
|
|
1601
3463
|
|
|
1602
3464
|
// src/utils/format-curriculum.ts
|
|
1603
|
-
function
|
|
1604
|
-
if (
|
|
3465
|
+
function extractBilingualName(name, language) {
|
|
3466
|
+
if (!name) return "";
|
|
3467
|
+
const trimmed = name.trim();
|
|
3468
|
+
const match = trimmed.match(/^(.*?)\s*[\(\[(]\s*([^\)\])]+)\s*[\)\])]\s*$/);
|
|
3469
|
+
if (!match) {
|
|
3470
|
+
return trimmed;
|
|
3471
|
+
}
|
|
3472
|
+
const part1 = match[1]?.trim() || "";
|
|
3473
|
+
const part2 = match[2]?.trim() || "";
|
|
3474
|
+
if (!part1 && !part2) return trimmed;
|
|
3475
|
+
if (!part1) return part2;
|
|
3476
|
+
if (!part2) return part1;
|
|
3477
|
+
const hasBengali = (text) => /[\u0980-\u09FF]/.test(text);
|
|
3478
|
+
const isPart1Bengali = hasBengali(part1);
|
|
3479
|
+
const isPart2Bengali = hasBengali(part2);
|
|
3480
|
+
let banglaPart = part1;
|
|
3481
|
+
let englishPart = part2;
|
|
3482
|
+
if (!isPart1Bengali && isPart2Bengali) {
|
|
3483
|
+
banglaPart = part2;
|
|
3484
|
+
englishPart = part1;
|
|
3485
|
+
} else {
|
|
3486
|
+
banglaPart = part1;
|
|
3487
|
+
englishPart = part2;
|
|
3488
|
+
}
|
|
3489
|
+
const lang = language?.toLowerCase();
|
|
3490
|
+
if (lang === "en") {
|
|
3491
|
+
return englishPart || banglaPart || trimmed;
|
|
3492
|
+
}
|
|
3493
|
+
if (lang === "bn") {
|
|
3494
|
+
return banglaPart || englishPart || trimmed;
|
|
3495
|
+
}
|
|
3496
|
+
return trimmed;
|
|
3497
|
+
}
|
|
3498
|
+
function formatCurriculumName(idString, dbName, language) {
|
|
3499
|
+
if (dbName) {
|
|
3500
|
+
return extractBilingualName(dbName, language);
|
|
3501
|
+
}
|
|
1605
3502
|
if (!idString) return "";
|
|
1606
3503
|
const namePart = idString.split("::")[0] ?? "";
|
|
1607
3504
|
const clean = namePart.replace(/[-_]/g, " ");
|
|
1608
|
-
|
|
3505
|
+
const formatted = clean.split(/\s+/).map((word) => {
|
|
1609
3506
|
if (!word) return "";
|
|
1610
3507
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
1611
3508
|
}).join(" ");
|
|
3509
|
+
return extractBilingualName(formatted, language);
|
|
1612
3510
|
}
|
|
1613
3511
|
|
|
1614
3512
|
// src/utils/relative-time.ts
|
|
@@ -1673,50 +3571,243 @@ function processLatexText(str) {
|
|
|
1673
3571
|
result += remainingText.replace(/\\n/g, "\n");
|
|
1674
3572
|
return result;
|
|
1675
3573
|
}
|
|
3574
|
+
|
|
3575
|
+
// src/utils/time.ts
|
|
3576
|
+
function convertToBangladeshTime(isoString, locale = "en-GB") {
|
|
3577
|
+
const options = {
|
|
3578
|
+
year: "numeric",
|
|
3579
|
+
month: "long",
|
|
3580
|
+
day: "numeric",
|
|
3581
|
+
hour: "numeric",
|
|
3582
|
+
minute: "numeric",
|
|
3583
|
+
hour12: true
|
|
3584
|
+
};
|
|
3585
|
+
const date = new Date(isoString);
|
|
3586
|
+
const resolvedLocale = locale === "bn" ? "bn-BD" : locale;
|
|
3587
|
+
return date.toLocaleString(resolvedLocale, {
|
|
3588
|
+
...options,
|
|
3589
|
+
timeZone: "Asia/Dhaka"
|
|
3590
|
+
});
|
|
3591
|
+
}
|
|
3592
|
+
function getDhakaNow() {
|
|
3593
|
+
const now = /* @__PURE__ */ new Date();
|
|
3594
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
3595
|
+
timeZone: "Asia/Dhaka",
|
|
3596
|
+
year: "numeric",
|
|
3597
|
+
month: "2-digit",
|
|
3598
|
+
day: "2-digit",
|
|
3599
|
+
hour: "2-digit",
|
|
3600
|
+
minute: "2-digit",
|
|
3601
|
+
second: "2-digit",
|
|
3602
|
+
hour12: false
|
|
3603
|
+
}).formatToParts(now);
|
|
3604
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
|
|
3605
|
+
return /* @__PURE__ */ new Date(`${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}`);
|
|
3606
|
+
}
|
|
3607
|
+
function calculateTimeLeft(endTime) {
|
|
3608
|
+
const target = new Date(endTime).getTime();
|
|
3609
|
+
const now = (/* @__PURE__ */ new Date()).getTime();
|
|
3610
|
+
const difference = target - now;
|
|
3611
|
+
if (difference <= 0 || isNaN(difference)) {
|
|
3612
|
+
return {
|
|
3613
|
+
days: 0,
|
|
3614
|
+
hours: 0,
|
|
3615
|
+
minutes: 0,
|
|
3616
|
+
seconds: 0,
|
|
3617
|
+
isEnded: true
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
3620
|
+
return {
|
|
3621
|
+
days: Math.floor(difference / (1e3 * 60 * 60 * 24)),
|
|
3622
|
+
hours: Math.floor(difference / (1e3 * 60 * 60) % 24),
|
|
3623
|
+
minutes: Math.floor(difference / 1e3 / 60 % 60),
|
|
3624
|
+
seconds: Math.floor(difference / 1e3 % 60),
|
|
3625
|
+
isEnded: false
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
function formatDate(date, locale = "en-GB") {
|
|
3629
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
3630
|
+
return d.toLocaleDateString(locale, {
|
|
3631
|
+
day: "2-digit",
|
|
3632
|
+
month: "short",
|
|
3633
|
+
year: "numeric"
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/utils/currency.ts
|
|
3638
|
+
function formatBDT(amount) {
|
|
3639
|
+
return amount.toLocaleString("en-US");
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
// src/utils/url.ts
|
|
3643
|
+
function getYouTubeId(url) {
|
|
3644
|
+
if (!url) return null;
|
|
3645
|
+
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|[?&]v=)([^#&?]*).*/;
|
|
3646
|
+
const match = url.match(regExp);
|
|
3647
|
+
return match && match[2]?.length === 11 ? match[2] : null;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// src/utils/file.ts
|
|
3651
|
+
function sanitizeFileName(name) {
|
|
3652
|
+
return name.replace(/[\s,]+/g, "_");
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
// src/utils/guards.ts
|
|
3656
|
+
function isQuestionLevel(value) {
|
|
3657
|
+
return [
|
|
3658
|
+
"One",
|
|
3659
|
+
"Two",
|
|
3660
|
+
"Three",
|
|
3661
|
+
"Four",
|
|
3662
|
+
"Five",
|
|
3663
|
+
"Six",
|
|
3664
|
+
"Seven",
|
|
3665
|
+
"Eight",
|
|
3666
|
+
"SSC",
|
|
3667
|
+
"HSC",
|
|
3668
|
+
"Admission",
|
|
3669
|
+
"HSC+Admission",
|
|
3670
|
+
"BCS"
|
|
3671
|
+
].includes(value);
|
|
3672
|
+
}
|
|
3673
|
+
function isLanguage(value) {
|
|
3674
|
+
return ["Bangla", "English", "Any"].includes(value);
|
|
3675
|
+
}
|
|
3676
|
+
function isDifficulty(value) {
|
|
3677
|
+
return ["Easy", "Medium", "Hard"].includes(value);
|
|
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
|
+
}
|
|
1676
3688
|
export {
|
|
1677
3689
|
AIService,
|
|
3690
|
+
AI_EVAL_BOOSTER_PACKS,
|
|
3691
|
+
AI_QUESTION_BOOSTER_PACKS,
|
|
3692
|
+
CLOTHES_COLORS,
|
|
3693
|
+
COLOR_HUES,
|
|
1678
3694
|
ContestService,
|
|
1679
3695
|
CourseService,
|
|
1680
3696
|
CurriculumService,
|
|
3697
|
+
FREE_DAILY_PRACTICE_LIMIT,
|
|
3698
|
+
HAIR_COLORS,
|
|
3699
|
+
INITIAL_AVATAR_DATA,
|
|
3700
|
+
MANUAL_PAYMENT_CONFIG,
|
|
1681
3701
|
MediaService,
|
|
1682
3702
|
MediaUrlCache,
|
|
1683
3703
|
NewsService,
|
|
3704
|
+
NotificationService,
|
|
1684
3705
|
OrjokClient,
|
|
1685
3706
|
PackService,
|
|
1686
3707
|
ProgressService,
|
|
1687
3708
|
QuestionService,
|
|
3709
|
+
RENDER_ORDER,
|
|
3710
|
+
SUBSCRIPTION_PLANS,
|
|
3711
|
+
SubscriptionService,
|
|
1688
3712
|
UserService,
|
|
3713
|
+
XP_REWARDS,
|
|
3714
|
+
XP_TIERS,
|
|
1689
3715
|
_resetMediaUrlWarning,
|
|
1690
3716
|
aggregateChapterMetrics,
|
|
1691
3717
|
aggregateSubjectMetrics,
|
|
1692
3718
|
aggregateTopicMetrics,
|
|
1693
3719
|
buildImageAnswer,
|
|
1694
3720
|
calculateAccuracy,
|
|
3721
|
+
calculateDefaultExamMinutes,
|
|
1695
3722
|
calculateExamTime,
|
|
3723
|
+
calculateStreakFromDates,
|
|
3724
|
+
calculateTimeLeft,
|
|
3725
|
+
calculateUnreadCount,
|
|
3726
|
+
calculateXpReward,
|
|
3727
|
+
canAccessMistakeVault,
|
|
3728
|
+
canExportPack,
|
|
3729
|
+
canWatchContestSolution,
|
|
3730
|
+
checkDailyPracticeLimit,
|
|
1696
3731
|
computeRatingDelta,
|
|
3732
|
+
convertToBangladeshTime,
|
|
1697
3733
|
createOrjokClient,
|
|
1698
3734
|
detectQuestionType,
|
|
3735
|
+
difficultyOptions,
|
|
1699
3736
|
engToBanglaNumber,
|
|
3737
|
+
extractBilingualName,
|
|
1700
3738
|
filterQuestionsByMode,
|
|
3739
|
+
formatBDT,
|
|
1701
3740
|
formatCurriculumName,
|
|
3741
|
+
formatDate,
|
|
1702
3742
|
formatExamTime,
|
|
3743
|
+
formatLevelUpNotification,
|
|
1703
3744
|
formatQuestionsToCsv,
|
|
1704
3745
|
formatQuestionsToText,
|
|
3746
|
+
generateRandomAvatarConfig,
|
|
3747
|
+
generateRandomAvatarState,
|
|
3748
|
+
getActiveTab,
|
|
3749
|
+
getActualOptionFileName,
|
|
1705
3750
|
getAllTiers,
|
|
3751
|
+
getAllXpTiers,
|
|
3752
|
+
getCanonicalPath,
|
|
3753
|
+
getCycleMultiplier,
|
|
3754
|
+
getDailyPracticeLimit,
|
|
3755
|
+
getDhakaNow,
|
|
1706
3756
|
getEloProgress,
|
|
1707
3757
|
getEloTier,
|
|
3758
|
+
getLevelFromXp,
|
|
3759
|
+
getPlanAiQuotas,
|
|
3760
|
+
getPlanDiscountPercent,
|
|
3761
|
+
getPlanMonthlyEquivalentPrice,
|
|
3762
|
+
getPlanOriginalPrice,
|
|
3763
|
+
getPlanPrice,
|
|
3764
|
+
getPublicExamsByLevel,
|
|
1708
3765
|
getRatingForSubject,
|
|
3766
|
+
getRecommendedSubjectsForLevel,
|
|
1709
3767
|
getRelativeTime,
|
|
3768
|
+
getTimeOfDayGreeting,
|
|
3769
|
+
getXpForLevel,
|
|
3770
|
+
getXpProgress,
|
|
3771
|
+
getXpRequiredForNextLevel,
|
|
3772
|
+
getXpTier,
|
|
3773
|
+
getYouTubeId,
|
|
3774
|
+
hasAiTierAccess,
|
|
3775
|
+
hasProAccess,
|
|
3776
|
+
hasSufficientAiEvalQuota,
|
|
3777
|
+
hasSufficientAiQuestionQuota,
|
|
3778
|
+
isAuthGatedRoute,
|
|
3779
|
+
isCorrectAnswer,
|
|
3780
|
+
isDifficulty,
|
|
3781
|
+
isDifficultyMatch,
|
|
1710
3782
|
isImageAnswer,
|
|
3783
|
+
isLanguage,
|
|
3784
|
+
isLanguageMatch,
|
|
3785
|
+
isQuestionLevel,
|
|
1711
3786
|
isStorageKey,
|
|
3787
|
+
isValidBangladeshiPhone,
|
|
3788
|
+
isValidMCQ,
|
|
3789
|
+
isValidTrxId,
|
|
3790
|
+
languageOptions,
|
|
3791
|
+
levelOptions,
|
|
3792
|
+
mapCognitoLevelToStandardLevel,
|
|
3793
|
+
mergeNotificationTimeline,
|
|
1712
3794
|
normalizeDifficulty,
|
|
3795
|
+
normalizeLevel,
|
|
3796
|
+
paginateWithAccumulator,
|
|
3797
|
+
parseAvatarConfig,
|
|
3798
|
+
parseAvatarConfigWithDefaults,
|
|
1713
3799
|
parseCsvQuestions,
|
|
1714
3800
|
parseImageKeys,
|
|
1715
3801
|
parseTextQuestions,
|
|
1716
3802
|
processLatexText,
|
|
3803
|
+
publicExamOptions,
|
|
3804
|
+
reorderObjectKeys,
|
|
1717
3805
|
resolveMediaUrl,
|
|
1718
3806
|
resolveMediaUrls,
|
|
3807
|
+
sanitizeFileName,
|
|
1719
3808
|
scoreExam,
|
|
3809
|
+
serializeAvatarConfig,
|
|
3810
|
+
shouldTrackPracticeProgress,
|
|
1720
3811
|
shuffle,
|
|
1721
3812
|
shuffleInPlace
|
|
1722
3813
|
};
|