@orjok/commons 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,20 @@
1
+ import {
2
+ MediaUrlCache,
3
+ _resetMediaUrlWarning,
4
+ aggregateChapterMetrics,
5
+ aggregateSubjectMetrics,
6
+ aggregateTopicMetrics,
7
+ calculateAccuracy,
8
+ isDifficultyMatch,
9
+ isLanguageMatch,
10
+ isStorageKey,
11
+ isValidMCQ,
12
+ resolveMediaUrl,
13
+ resolveMediaUrls,
14
+ shuffle,
15
+ shuffleInPlace
16
+ } from "./chunk-ZDGYAVEC.js";
17
+
1
18
  // src/services/user.service.ts
2
19
  var UserService = class {
3
20
  constructor(network) {
@@ -30,10 +47,10 @@ var UserService = class {
30
47
  );
31
48
  return result.data.updateUser;
32
49
  }
33
- async getQuestions(userId, nextToken) {
50
+ async getQuestions(userId, nextToken, limit = 10) {
34
51
  const result = await this.network.query(
35
- `query ListUserQuestions($owner: ID!, $nextToken: String, $sortDirection: ModelSortDirection) {
36
- listQuestionObjectByOwnerAndCreatedAt(owner: $owner, sortDirection: $sortDirection, nextToken: $nextToken) {
52
+ `query ListUserQuestions($owner: ID!, $limit: Int, $nextToken: String, $sortDirection: ModelSortDirection) {
53
+ listQuestionObjectByOwnerAndCreatedAt(owner: $owner, limit: $limit, sortDirection: $sortDirection, nextToken: $nextToken) {
37
54
  items {
38
55
  id question imageUrl language level difficulty type voteCount
39
56
  verificationStatus owner packId createdAt
@@ -43,14 +60,14 @@ var UserService = class {
43
60
  nextToken
44
61
  }
45
62
  }`,
46
- { owner: userId, nextToken, sortDirection: "DESC" }
63
+ { owner: userId, limit, nextToken, sortDirection: "DESC" }
47
64
  );
48
65
  return result.data.listQuestionObjectByOwnerAndCreatedAt;
49
66
  }
50
- async getPacks(userId, nextToken) {
67
+ async getPacks(userId, nextToken, limit = 10) {
51
68
  const result = await this.network.query(
52
- `query ListUserPacks($owner: ID!, $nextToken: String, $sortDirection: ModelSortDirection) {
53
- listPackByOwnerAndCreatedAt(owner: $owner, sortDirection: $sortDirection, nextToken: $nextToken) {
69
+ `query ListUserPacks($owner: ID!, $limit: Int, $nextToken: String, $sortDirection: ModelSortDirection) {
70
+ listPackByOwnerAndCreatedAt(owner: $owner, limit: $limit, sortDirection: $sortDirection, nextToken: $nextToken) {
54
71
  items {
55
72
  id name language level difficulty questionCount automaticNumbering
56
73
  owner subjectId chapterId topicId verificationStatus createdAt
@@ -59,7 +76,7 @@ var UserService = class {
59
76
  nextToken
60
77
  }
61
78
  }`,
62
- { owner: userId, nextToken, sortDirection: "DESC" }
79
+ { owner: userId, limit, nextToken, sortDirection: "DESC" }
63
80
  );
64
81
  return result.data.listPackByOwnerAndCreatedAt;
65
82
  }
@@ -159,6 +176,23 @@ var QuestionService = class {
159
176
  );
160
177
  return result.data.listQuestionObjectByPackIdAndOrder.items[0]?.id ?? null;
161
178
  }
179
+ async listByPack(packId, limit = 200, sortDirection = "ASC", nextToken) {
180
+ const result = await this.network.query(
181
+ `query ListQuestionsByPack($packId: ID!, $sortDirection: ModelSortDirection, $limit: Int, $nextToken: String) {
182
+ listQuestionObjectByPackIdAndOrder(packId: $packId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
183
+ items {
184
+ id question answer explanation extra imageUrl language level difficulty type
185
+ markingInstructions voteCount owner packId subjectId chapterId topicId order createdAt
186
+ options { items { id content } }
187
+ tags { items { id tagId } }
188
+ }
189
+ nextToken
190
+ }
191
+ }`,
192
+ { packId, sortDirection, limit, nextToken }
193
+ );
194
+ return result.data.listQuestionObjectByPackIdAndOrder;
195
+ }
162
196
  async update(id, fields) {
163
197
  const result = await this.network.mutate(
164
198
  `mutation UpdateQuestion($input: UpdateQuestionObjectInput!) {
@@ -172,6 +206,41 @@ var QuestionService = class {
172
206
  );
173
207
  return result.data.updateQuestionObject;
174
208
  }
209
+ async updateOption(id, content) {
210
+ const result = await this.network.mutate(
211
+ `mutation UpdateOption($input: UpdateOptionInput!) {
212
+ updateOption(input: $input) {
213
+ id
214
+ content
215
+ }
216
+ }`,
217
+ { input: { id, content } }
218
+ );
219
+ return result.data.updateOption;
220
+ }
221
+ async createOption(input) {
222
+ const result = await this.network.mutate(
223
+ `mutation CreateOption($input: CreateOptionInput!) {
224
+ createOption(input: $input) {
225
+ id
226
+ content
227
+ }
228
+ }`,
229
+ { input }
230
+ );
231
+ return result.data.createOption;
232
+ }
233
+ async deleteOption(id) {
234
+ const result = await this.network.mutate(
235
+ `mutation DeleteOption($input: DeleteOptionInput!) {
236
+ deleteOption(input: $input) {
237
+ id
238
+ }
239
+ }`,
240
+ { input: { id } }
241
+ );
242
+ return result.data.deleteOption;
243
+ }
175
244
  async delete(id) {
176
245
  const result = await this.network.mutate(
177
246
  `mutation DeleteQuestion($input: DeleteQuestionObjectInput!) {
@@ -239,7 +308,7 @@ var QuestionService = class {
239
308
  listQuestionObjectBySubjectIdAndCreatedAt(subjectId: $subjectId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
240
309
  items {
241
310
  id question imageUrl language level difficulty type voteCount
242
- verificationStatus owner packId createdAt
311
+ verificationStatus owner packId subjectId chapterId topicId createdAt
243
312
  options { items { id content } }
244
313
  }
245
314
  nextToken
@@ -261,7 +330,7 @@ var QuestionService = class {
261
330
  listQuestionObjectByChapterIdAndCreatedAt(chapterId: $chapterId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
262
331
  items {
263
332
  id question imageUrl language level difficulty type voteCount
264
- verificationStatus owner packId createdAt
333
+ verificationStatus owner packId subjectId chapterId topicId createdAt
265
334
  options { items { id content } }
266
335
  }
267
336
  nextToken
@@ -283,7 +352,7 @@ var QuestionService = class {
283
352
  listQuestionObjectByTopicIdAndCreatedAt(topicId: $topicId, sortDirection: $sortDirection, limit: $limit, nextToken: $nextToken) {
284
353
  items {
285
354
  id question imageUrl language level difficulty type voteCount
286
- verificationStatus owner packId createdAt
355
+ verificationStatus owner packId subjectId chapterId topicId createdAt
287
356
  options { items { id content } }
288
357
  }
289
358
  nextToken
@@ -368,7 +437,22 @@ var PackService = class {
368
437
  }`,
369
438
  { id }
370
439
  );
371
- return result.data.getPack;
440
+ const pack = result.data.getPack;
441
+ if (pack && pack.questions && Array.isArray(pack.questions.items)) {
442
+ pack.questions.items.sort((a, b) => {
443
+ const orderA = typeof a.order === "number" ? a.order : a.order ? Number(a.order) : 0;
444
+ const orderB = typeof b.order === "number" ? b.order : b.order ? Number(b.order) : 0;
445
+ if (orderA !== orderB) {
446
+ if (orderA === 0) return 1;
447
+ if (orderB === 0) return -1;
448
+ return orderA - orderB;
449
+ }
450
+ const timeA = a.createdAt ? new Date(a.createdAt).getTime() : 0;
451
+ const timeB = b.createdAt ? new Date(b.createdAt).getTime() : 0;
452
+ return timeA - timeB;
453
+ });
454
+ }
455
+ return pack;
372
456
  }
373
457
  async update(id, fields) {
374
458
  const result = await this.network.mutate(
@@ -954,13 +1038,34 @@ var MediaService = class {
954
1038
  this.storage = storage;
955
1039
  }
956
1040
  async uploadImage(questionId, image) {
957
- const result = await this.network.mutate(
958
- `mutation UploadImage($questionId: ID!, $image: String!) {
959
- uploadImage(questionId: $questionId, image: $image) { status imageUrl }
960
- }`,
961
- { questionId, image }
962
- );
963
- return result.data.uploadImage;
1041
+ const payloadLength = image ? image.length : 0;
1042
+ console.log(`[MediaService.uploadImage] Initiating upload mutation: questionId="${questionId}", payloadLength=${payloadLength} chars`);
1043
+ try {
1044
+ const result = await this.network.mutate(
1045
+ `mutation UploadImage($questionId: ID!, $image: String!) {
1046
+ uploadImage(questionId: $questionId, image: $image) { status imageUrl }
1047
+ }`,
1048
+ { questionId, image }
1049
+ );
1050
+ if (result.errors && result.errors.length > 0) {
1051
+ console.error(
1052
+ `[MediaService.uploadImage] GraphQL errors returned for questionId "${questionId}":`,
1053
+ JSON.stringify(result.errors, null, 2)
1054
+ );
1055
+ }
1056
+ const response = result.data?.uploadImage;
1057
+ if (!response) {
1058
+ console.error(`[MediaService.uploadImage] No uploadImage object returned in GraphQL response for questionId "${questionId}". Full result:`, result);
1059
+ } else if (response.status === "Error" || response.status === "error" || !response.imageUrl) {
1060
+ console.error(`[MediaService.uploadImage] Backend returned error status or missing imageUrl for questionId "${questionId}":`, response);
1061
+ } else {
1062
+ console.log(`[MediaService.uploadImage] Upload mutation success for questionId "${questionId}": imageUrl="${response.imageUrl}"`);
1063
+ }
1064
+ return response;
1065
+ } catch (err) {
1066
+ console.error(`[MediaService.uploadImage] Network/GraphQL mutation failed for questionId "${questionId}":`, err);
1067
+ throw err;
1068
+ }
964
1069
  }
965
1070
  async deleteImage(imageUrl) {
966
1071
  const result = await this.network.mutate(
@@ -1023,68 +1128,100 @@ var ProgressService = class {
1023
1128
  this.network = network;
1024
1129
  }
1025
1130
  async track(input) {
1131
+ const variables = {
1132
+ questionId: input.questionId,
1133
+ isCorrect: input.isCorrect,
1134
+ subjectId: input.subjectId ?? null,
1135
+ chapterId: input.chapterId ?? null,
1136
+ topicId: input.topicId ?? null
1137
+ };
1026
1138
  const result = await this.network.mutate(
1027
1139
  `mutation TrackProgress($questionId: ID!, $isCorrect: Boolean!, $subjectId: ID, $chapterId: ID, $topicId: ID) {
1028
1140
  trackPracticeProgress(questionId: $questionId, isCorrect: $isCorrect, subjectId: $subjectId, chapterId: $chapterId, topicId: $topicId) { status }
1029
1141
  }`,
1030
- input
1142
+ variables
1031
1143
  );
1032
- return result.data.trackPracticeProgress;
1144
+ return result.data?.trackPracticeProgress || { status: "success" };
1033
1145
  }
1034
1146
  async getSubjectProgress(userId) {
1035
- const result = await this.network.query(
1036
- `query ListSubjectProgress($userId: String!) {
1037
- listUserSubjectProgressByUserIdAndSubjectId(userId: $userId) {
1038
- items { id userId subjectId practicedCount mistakeCount correctedCount accuracyScore }
1039
- }
1040
- }`,
1041
- { userId }
1042
- );
1043
- return result.data.listUserSubjectProgressByUserIdAndSubjectId.items;
1147
+ try {
1148
+ const result = await this.network.query(
1149
+ `query ListSubjectProgress($userId: String!) {
1150
+ listUserSubjectProgressByUserIdAndSubjectId(userId: $userId) {
1151
+ items { id userId subjectId practicedCount mistakeCount correctedCount accuracyScore }
1152
+ }
1153
+ }`,
1154
+ { userId }
1155
+ );
1156
+ return result.data?.listUserSubjectProgressByUserIdAndSubjectId?.items || result.data?.listUserSubjectProgressesByUserIdAndSubjectId?.items || [];
1157
+ } catch (e) {
1158
+ console.error("Error fetching subject progress:", e);
1159
+ return [];
1160
+ }
1044
1161
  }
1045
1162
  async getChapterProgress(userId) {
1046
- const result = await this.network.query(
1047
- `query ListChapterProgress($userId: String!) {
1048
- listUserChapterProgressByUserIdAndChapterId(userId: $userId) {
1049
- items { id userId chapterId practicedCount mistakeCount correctedCount accuracyScore }
1050
- }
1051
- }`,
1052
- { userId }
1053
- );
1054
- return result.data.listUserChapterProgressByUserIdAndChapterId.items;
1163
+ try {
1164
+ const result = await this.network.query(
1165
+ `query ListChapterProgress($userId: String!) {
1166
+ listUserChapterProgressByUserIdAndChapterId(userId: $userId) {
1167
+ items { id userId chapterId practicedCount mistakeCount correctedCount accuracyScore }
1168
+ }
1169
+ }`,
1170
+ { userId }
1171
+ );
1172
+ return result.data?.listUserChapterProgressByUserIdAndChapterId?.items || result.data?.listUserChapterProgressesByUserIdAndChapterId?.items || [];
1173
+ } catch (e) {
1174
+ console.error("Error fetching chapter progress:", e);
1175
+ return [];
1176
+ }
1055
1177
  }
1056
1178
  async getTopicProgress(userId) {
1057
- const result = await this.network.query(
1058
- `query ListTopicProgress($userId: String!) {
1059
- listUserTopicProgressByUserIdAndTopicId(userId: $userId) {
1060
- items { id userId topicId practicedCount mistakeCount correctedCount accuracyScore }
1061
- }
1062
- }`,
1063
- { userId }
1064
- );
1065
- return result.data.listUserTopicProgressByUserIdAndTopicId.items;
1179
+ try {
1180
+ const result = await this.network.query(
1181
+ `query ListTopicProgress($userId: String!) {
1182
+ listUserTopicProgressByUserIdAndTopicId(userId: $userId) {
1183
+ items { id userId topicId practicedCount mistakeCount correctedCount accuracyScore }
1184
+ }
1185
+ }`,
1186
+ { userId }
1187
+ );
1188
+ return result.data?.listUserTopicProgressByUserIdAndTopicId?.items || result.data?.listUserTopicProgressesByUserIdAndTopicId?.items || [];
1189
+ } catch (e) {
1190
+ console.error("Error fetching topic progress:", e);
1191
+ return [];
1192
+ }
1066
1193
  }
1067
1194
  async getTopicMetrics(userId) {
1068
- const result = await this.network.query(
1069
- `query ListTopicMetrics($filter: ModelUserTopicMetricFilterInput) {
1070
- listUserTopicMetrics(filter: $filter) {
1071
- items { id userId topicId totalAttempted totalCorrect accuracy }
1072
- }
1073
- }`,
1074
- { filter: { userId: { eq: userId } } }
1075
- );
1076
- return result.data.listUserTopicMetrics.items;
1195
+ try {
1196
+ const result = await this.network.query(
1197
+ `query ListTopicMetrics($filter: ModelUserTopicMetricFilterInput) {
1198
+ listUserTopicMetrics(filter: $filter) {
1199
+ items { id userId topicId totalAttempted totalCorrect accuracy }
1200
+ }
1201
+ }`,
1202
+ { filter: { userId: { eq: userId } } }
1203
+ );
1204
+ return result.data?.listUserTopicMetrics?.items || [];
1205
+ } catch (e) {
1206
+ console.error("Error fetching topic metrics:", e);
1207
+ return [];
1208
+ }
1077
1209
  }
1078
1210
  async getMistakenQuestions(userId) {
1079
- const result = await this.network.query(
1080
- `query ListMistakes($userId: String!, $filter: ModelQuestionTrackingFilterInput) {
1081
- listQuestionTrackingByUserIdAndSubjectId(userId: $userId, filter: $filter) {
1082
- items { id userId questionId subjectId chapterId topicId status attempts }
1083
- }
1084
- }`,
1085
- { userId, filter: { status: { eq: "MISTAKE" } } }
1086
- );
1087
- return result.data.listQuestionTrackingByUserIdAndSubjectId.items;
1211
+ try {
1212
+ const result = await this.network.query(
1213
+ `query ListMistakes($userId: String!, $filter: ModelQuestionTrackingFilterInput) {
1214
+ listQuestionTrackingByUserIdAndQuestionId(userId: $userId, filter: $filter) {
1215
+ items { id userId questionId subjectId chapterId topicId status attempts }
1216
+ }
1217
+ }`,
1218
+ { userId, filter: { status: { eq: "MISTAKE" } } }
1219
+ );
1220
+ return result.data?.listQuestionTrackingByUserIdAndQuestionId?.items || result.data?.listQuestionTrackingsByUserIdAndQuestionId?.items || result.data?.listQuestionTrackings?.items || [];
1221
+ } catch (e) {
1222
+ console.error("Error fetching mistaken questions:", e);
1223
+ return [];
1224
+ }
1088
1225
  }
1089
1226
  };
1090
1227
 
@@ -1147,6 +1284,7 @@ var NewsService = class {
1147
1284
  var OrjokClient = class {
1148
1285
  constructor(config) {
1149
1286
  this.auth = config.authProvider;
1287
+ this.storage = config.storageProvider;
1150
1288
  this.users = new UserService(config.networkProvider);
1151
1289
  this.questions = new QuestionService(config.networkProvider);
1152
1290
  this.packs = new PackService(config.networkProvider);
@@ -1163,6 +1301,118 @@ function createOrjokClient(config) {
1163
1301
  return new OrjokClient(config);
1164
1302
  }
1165
1303
 
1304
+ // src/types/select-options.ts
1305
+ var levelOptions = [
1306
+ { value: "SSC", label: "SSC" },
1307
+ { value: "HSC+Admission", label: "HSC/Admission" },
1308
+ { value: "BCS", label: "BCS" }
1309
+ ];
1310
+ var languageOptions = [
1311
+ { value: "Bangla", label: "Bangla" },
1312
+ { value: "English", label: "English" },
1313
+ { value: "Any", label: "Any" }
1314
+ ];
1315
+ var difficultyOptions = [
1316
+ { value: "Easy", label: "Easy" },
1317
+ { value: "Medium", label: "Medium" },
1318
+ { value: "Hard", label: "Hard" }
1319
+ ];
1320
+
1321
+ // src/logic/avatar.ts
1322
+ var HAIR_COLORS = [
1323
+ { name: "Original", filter: "none", hex: "#e2e8f0", brightness: 1 },
1324
+ { name: "White", filter: "grayscale(100%) brightness(500%)", hex: "#ffffff", brightness: 2 },
1325
+ { name: "Black", filter: "grayscale(100%) brightness(40%)", hex: "#1a202c", brightness: 0.15 },
1326
+ { name: "Brown", filter: "sepia(100%) saturate(300%) hue-rotate(330deg) brightness(80%)", hex: "#7b3f00", brightness: 0.35 },
1327
+ { name: "Blonde", filter: "sepia(100%) saturate(400%) hue-rotate(20deg) brightness(130%)", hex: "#e6c27a", brightness: 0.8 },
1328
+ { name: "Red", filter: "sepia(100%) saturate(500%) hue-rotate(320deg) brightness(90%)", hex: "#9b2c2c", brightness: 0.4 },
1329
+ { name: "Blue", filter: "sepia(100%) saturate(500%) hue-rotate(180deg) brightness(90%)", hex: "#2b6cb0", brightness: 0.4 },
1330
+ { name: "Green", filter: "sepia(100%) saturate(400%) hue-rotate(80deg) brightness(90%)", hex: "#2f855a", brightness: 0.45 }
1331
+ ];
1332
+ var CLOTHES_COLORS = [
1333
+ { name: "Original", filter: "none", hex: "#e2e8f0", brightness: 1 },
1334
+ { name: "White", filter: "grayscale(100%) brightness(500%)", hex: "#ffffff", brightness: 2 },
1335
+ { name: "Black", filter: "grayscale(100%) brightness(30%)", hex: "#1a202c", brightness: 0.12 },
1336
+ { name: "Red", filter: "sepia(100%) saturate(500%) hue-rotate(320deg)", hex: "#c53030", brightness: 0.45 },
1337
+ { name: "Orange", filter: "sepia(100%) saturate(500%) hue-rotate(350deg)", hex: "#dd6b20", brightness: 0.55 },
1338
+ { name: "Yellow", filter: "sepia(100%) saturate(500%) hue-rotate(20deg) brightness(120%)", hex: "#d69e2e", brightness: 0.8 },
1339
+ { name: "Green", filter: "sepia(100%) saturate(500%) hue-rotate(80deg)", hex: "#38a169", brightness: 0.55 },
1340
+ { name: "Blue", filter: "sepia(100%) saturate(500%) hue-rotate(180deg)", hex: "#3182ce", brightness: 0.45 },
1341
+ { name: "Purple", filter: "sepia(100%) saturate(500%) hue-rotate(240deg)", hex: "#805ad5", brightness: 0.45 },
1342
+ { name: "Pink", filter: "sepia(100%) saturate(400%) hue-rotate(290deg)", hex: "#d53f8c", brightness: 0.55 }
1343
+ ];
1344
+ var COLOR_HUES = {
1345
+ Original: 0,
1346
+ Red: 0,
1347
+ Orange: 24,
1348
+ Yellow: 45,
1349
+ Green: 140,
1350
+ Blue: 210,
1351
+ Purple: 270,
1352
+ Pink: 320,
1353
+ Brown: 25,
1354
+ Blonde: 45
1355
+ };
1356
+ var RENDER_ORDER = [
1357
+ "Background",
1358
+ "Hair Back",
1359
+ "Clothes Back",
1360
+ "Skin Color Body",
1361
+ "Clothes Front",
1362
+ "Skin Color Head",
1363
+ "Accessories",
1364
+ "Facial Expression",
1365
+ "Beard",
1366
+ "Hair Front",
1367
+ "Eyewear",
1368
+ "Headwears"
1369
+ ];
1370
+ var INITIAL_AVATAR_DATA = {
1371
+ Background: [],
1372
+ Hair: [],
1373
+ "Skin Color": [],
1374
+ Clothes: [],
1375
+ Accessories: [],
1376
+ Headwears: [],
1377
+ Eyewear: [],
1378
+ "Facial Expression": [],
1379
+ Beard: []
1380
+ };
1381
+ function getActualOptionFileName(category, baseName, config, selections) {
1382
+ if (baseName === "None") return null;
1383
+ const items = config[category] || [];
1384
+ const matched = items.filter((item) => item.base_name === baseName);
1385
+ if (matched.length === 0) return null;
1386
+ for (const item of matched) {
1387
+ if (item.conditions && item.conditions.length > 0) {
1388
+ const allMet = item.conditions.every((cond) => {
1389
+ return Object.values(selections).some(
1390
+ (sel) => sel && (sel.includes(cond) || cond.includes(sel))
1391
+ );
1392
+ });
1393
+ if (allMet) return item;
1394
+ }
1395
+ }
1396
+ return matched.find((item) => !item.conditions || item.conditions.length === 0) || matched[0] || null;
1397
+ }
1398
+ function parseAvatarConfig(configStr) {
1399
+ if (!configStr) {
1400
+ return { selections: {}, colors: {} };
1401
+ }
1402
+ try {
1403
+ const parsed = JSON.parse(configStr);
1404
+ return {
1405
+ selections: parsed.selections || {},
1406
+ colors: parsed.colors || {}
1407
+ };
1408
+ } catch {
1409
+ return { selections: {}, colors: {} };
1410
+ }
1411
+ }
1412
+ function serializeAvatarConfig(state) {
1413
+ return JSON.stringify(state);
1414
+ }
1415
+
1166
1416
  // src/logic/elo.ts
1167
1417
  var TIERS = [
1168
1418
  { name: "Iron", minRating: 0, maxRating: 799, color: "#6B7280" },
@@ -1263,6 +1513,40 @@ function detectQuestionType(option2, option3, option4) {
1263
1513
  }
1264
1514
  return "MCQ";
1265
1515
  }
1516
+ function isCorrectAnswer(selected, correct) {
1517
+ if (!selected || !correct) return false;
1518
+ const selTrim = String(selected).trim();
1519
+ const corrTrim = String(correct).trim();
1520
+ if (selTrim === corrTrim) return true;
1521
+ const selNum = Number(selTrim);
1522
+ const corrNum = Number(corrTrim);
1523
+ if (!isNaN(selNum) && !isNaN(corrNum) && selNum === corrNum) {
1524
+ return true;
1525
+ }
1526
+ return false;
1527
+ }
1528
+ function calculateDefaultExamMinutes(mode, questions) {
1529
+ const mcqCount = questions.filter((q) => q.type !== "WRITTEN").length;
1530
+ const writtenCount = questions.filter((q) => q.type === "WRITTEN").length;
1531
+ if (mode === "MCQ") return Math.max(1, mcqCount * 1);
1532
+ if (mode === "WRITTEN") return Math.max(1, writtenCount * 10);
1533
+ return Math.max(1, mcqCount * 1 + writtenCount * 10);
1534
+ }
1535
+ function reorderObjectKeys(obj, oldIndex, newIndex, length) {
1536
+ const arr = [];
1537
+ for (let i = 0; i < length; i++) {
1538
+ arr.push(obj[i]);
1539
+ }
1540
+ const [movedItem] = arr.splice(oldIndex, 1);
1541
+ arr.splice(newIndex, 0, movedItem);
1542
+ const newObj = {};
1543
+ arr.forEach((item, idx) => {
1544
+ if (item !== void 0) {
1545
+ newObj[idx] = item;
1546
+ }
1547
+ });
1548
+ return newObj;
1549
+ }
1266
1550
 
1267
1551
  // src/logic/import-export.ts
1268
1552
  var MAX_QUESTIONS = 200;
@@ -1468,126 +1752,37 @@ function formatQuestionsToCsv(questions, packSubjectId, packChapterId, packTopic
1468
1752
  ${rows.join("\n")}`;
1469
1753
  }
1470
1754
 
1471
- // src/logic/media-url.ts
1472
- function isStorageKey(value) {
1473
- return !value.startsWith("http://") && !value.startsWith("https://");
1474
- }
1475
- var MediaUrlCache = class {
1476
- constructor(ttlMs = 50 * 60 * 1e3) {
1477
- this.cache = /* @__PURE__ */ new Map();
1478
- this.ttlMs = ttlMs;
1479
- }
1480
- get(key) {
1481
- const entry = this.cache.get(key);
1482
- if (!entry) return void 0;
1483
- if (Date.now() >= entry.expiresAt) {
1484
- this.cache.delete(key);
1485
- return void 0;
1486
- }
1487
- return entry.url;
1755
+ // src/navigation.ts
1756
+ function getActiveTab(pathname) {
1757
+ const normalized = pathname.toLowerCase();
1758
+ if (normalized.startsWith("/contests") || normalized.startsWith("/contest")) {
1759
+ return "contests";
1488
1760
  }
1489
- set(key, url) {
1490
- this.cache.set(key, { url, expiresAt: Date.now() + this.ttlMs });
1761
+ if (normalized.startsWith("/courses") || normalized.startsWith("/course") || normalized.startsWith("/exams")) {
1762
+ return "courses";
1491
1763
  }
1492
- clear() {
1493
- this.cache.clear();
1764
+ if (normalized.startsWith("/profile") || normalized.startsWith("/dashboard") || normalized.startsWith("/edit-profile")) {
1765
+ return "profile";
1494
1766
  }
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;
1767
+ 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")) {
1768
+ return "learn";
1510
1769
  }
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
- }
1527
- function _resetMediaUrlWarning() {
1528
- warnedNoStorage = false;
1770
+ return "home";
1529
1771
  }
1530
-
1531
- // src/logic/practice.ts
1532
- function aggregateSubjectMetrics(progress) {
1533
- let totalPracticed = 0;
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);
1571
- }
1572
-
1573
- // src/utils/shuffle.ts
1574
- function shuffle(array) {
1575
- const result = [...array];
1576
- let currentIndex = result.length;
1577
- while (currentIndex !== 0) {
1578
- const randomIndex = Math.floor(Math.random() * currentIndex);
1579
- currentIndex--;
1580
- [result[currentIndex], result[randomIndex]] = [result[randomIndex], result[currentIndex]];
1581
- }
1582
- return result;
1772
+ function getCanonicalPath(pathname) {
1773
+ const normalized = pathname.toLowerCase();
1774
+ if (normalized === "/explore/questions") return "/learn/questions";
1775
+ if (normalized === "/explore/packs") return "/learn/packs";
1776
+ if (normalized === "/question-bank") return "/learn/bank";
1777
+ if (normalized === "/contest/all") return "/contests";
1778
+ if (normalized === "/dashboard") return "/profile";
1779
+ if (normalized === "/edit-profile") return "/profile/edit";
1780
+ return pathname;
1583
1781
  }
1584
- function shuffleInPlace(array) {
1585
- let currentIndex = array.length;
1586
- while (currentIndex !== 0) {
1587
- const randomIndex = Math.floor(Math.random() * currentIndex);
1588
- currentIndex--;
1589
- [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
1590
- }
1782
+ function isAuthGatedRoute(pathname) {
1783
+ const normalized = pathname.toLowerCase();
1784
+ 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");
1785
+ return isProtectedPath;
1591
1786
  }
1592
1787
 
1593
1788
  // src/utils/bangla-numbers.ts
@@ -1600,15 +1795,51 @@ function engToBanglaNumber(num) {
1600
1795
  }
1601
1796
 
1602
1797
  // src/utils/format-curriculum.ts
1603
- function formatCurriculumName(idString, dbName) {
1604
- if (dbName) return dbName;
1798
+ function extractBilingualName(name, language) {
1799
+ if (!name) return "";
1800
+ const trimmed = name.trim();
1801
+ const match = trimmed.match(/^(.*?)\s*[\(\[(]\s*([^\)\])]+)\s*[\)\])]\s*$/);
1802
+ if (!match) {
1803
+ return trimmed;
1804
+ }
1805
+ const part1 = match[1]?.trim() || "";
1806
+ const part2 = match[2]?.trim() || "";
1807
+ if (!part1 && !part2) return trimmed;
1808
+ if (!part1) return part2;
1809
+ if (!part2) return part1;
1810
+ const hasBengali = (text) => /[\u0980-\u09FF]/.test(text);
1811
+ const isPart1Bengali = hasBengali(part1);
1812
+ const isPart2Bengali = hasBengali(part2);
1813
+ let banglaPart = part1;
1814
+ let englishPart = part2;
1815
+ if (!isPart1Bengali && isPart2Bengali) {
1816
+ banglaPart = part2;
1817
+ englishPart = part1;
1818
+ } else {
1819
+ banglaPart = part1;
1820
+ englishPart = part2;
1821
+ }
1822
+ const lang = language?.toLowerCase();
1823
+ if (lang === "en") {
1824
+ return englishPart || banglaPart || trimmed;
1825
+ }
1826
+ if (lang === "bn") {
1827
+ return banglaPart || englishPart || trimmed;
1828
+ }
1829
+ return trimmed;
1830
+ }
1831
+ function formatCurriculumName(idString, dbName, language) {
1832
+ if (dbName) {
1833
+ return extractBilingualName(dbName, language);
1834
+ }
1605
1835
  if (!idString) return "";
1606
1836
  const namePart = idString.split("::")[0] ?? "";
1607
1837
  const clean = namePart.replace(/[-_]/g, " ");
1608
- return clean.split(/\s+/).map((word) => {
1838
+ const formatted = clean.split(/\s+/).map((word) => {
1609
1839
  if (!word) return "";
1610
1840
  return word.charAt(0).toUpperCase() + word.slice(1);
1611
1841
  }).join(" ");
1842
+ return extractBilingualName(formatted, language);
1612
1843
  }
1613
1844
 
1614
1845
  // src/utils/relative-time.ts
@@ -1673,11 +1904,119 @@ function processLatexText(str) {
1673
1904
  result += remainingText.replace(/\\n/g, "\n");
1674
1905
  return result;
1675
1906
  }
1907
+
1908
+ // src/utils/time.ts
1909
+ function convertToBangladeshTime(isoString) {
1910
+ const options = {
1911
+ year: "numeric",
1912
+ month: "long",
1913
+ day: "numeric",
1914
+ hour: "numeric",
1915
+ minute: "numeric",
1916
+ hour12: true
1917
+ };
1918
+ const date = new Date(isoString);
1919
+ return date.toLocaleString("en-GB", {
1920
+ ...options,
1921
+ timeZone: "Asia/Dhaka"
1922
+ });
1923
+ }
1924
+ function getDhakaNow() {
1925
+ const now = /* @__PURE__ */ new Date();
1926
+ const parts = new Intl.DateTimeFormat("en-CA", {
1927
+ timeZone: "Asia/Dhaka",
1928
+ year: "numeric",
1929
+ month: "2-digit",
1930
+ day: "2-digit",
1931
+ hour: "2-digit",
1932
+ minute: "2-digit",
1933
+ second: "2-digit",
1934
+ hour12: false
1935
+ }).formatToParts(now);
1936
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
1937
+ return /* @__PURE__ */ new Date(`${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}`);
1938
+ }
1939
+ function calculateTimeLeft(endTime) {
1940
+ const target = new Date(endTime).getTime();
1941
+ const now = (/* @__PURE__ */ new Date()).getTime();
1942
+ const difference = target - now;
1943
+ if (difference <= 0 || isNaN(difference)) {
1944
+ return {
1945
+ days: 0,
1946
+ hours: 0,
1947
+ minutes: 0,
1948
+ seconds: 0,
1949
+ isEnded: true
1950
+ };
1951
+ }
1952
+ return {
1953
+ days: Math.floor(difference / (1e3 * 60 * 60 * 24)),
1954
+ hours: Math.floor(difference / (1e3 * 60 * 60) % 24),
1955
+ minutes: Math.floor(difference / 1e3 / 60 % 60),
1956
+ seconds: Math.floor(difference / 1e3 % 60),
1957
+ isEnded: false
1958
+ };
1959
+ }
1960
+ function formatDate(date, locale = "en-GB") {
1961
+ const d = typeof date === "string" ? new Date(date) : date;
1962
+ return d.toLocaleDateString(locale, {
1963
+ day: "2-digit",
1964
+ month: "short",
1965
+ year: "numeric"
1966
+ });
1967
+ }
1968
+
1969
+ // src/utils/currency.ts
1970
+ function formatBDT(amount) {
1971
+ return amount.toLocaleString("en-US");
1972
+ }
1973
+
1974
+ // src/utils/url.ts
1975
+ function getYouTubeId(url) {
1976
+ if (!url) return null;
1977
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|[?&]v=)([^#&?]*).*/;
1978
+ const match = url.match(regExp);
1979
+ return match && match[2]?.length === 11 ? match[2] : null;
1980
+ }
1981
+
1982
+ // src/utils/file.ts
1983
+ function sanitizeFileName(name) {
1984
+ return name.replace(/[\s,]+/g, "_");
1985
+ }
1986
+
1987
+ // src/utils/guards.ts
1988
+ function isQuestionLevel(value) {
1989
+ return [
1990
+ "One",
1991
+ "Two",
1992
+ "Three",
1993
+ "Four",
1994
+ "Five",
1995
+ "Six",
1996
+ "Seven",
1997
+ "Eight",
1998
+ "SSC",
1999
+ "HSC",
2000
+ "Admission",
2001
+ "HSC+Admission",
2002
+ "BCS"
2003
+ ].includes(value);
2004
+ }
2005
+ function isLanguage(value) {
2006
+ return ["Bangla", "English", "Any"].includes(value);
2007
+ }
2008
+ function isDifficulty(value) {
2009
+ return ["Easy", "Medium", "Hard"].includes(value);
2010
+ }
1676
2011
  export {
1677
2012
  AIService,
2013
+ CLOTHES_COLORS,
2014
+ COLOR_HUES,
1678
2015
  ContestService,
1679
2016
  CourseService,
1680
2017
  CurriculumService,
2018
+ HAIR_COLORS,
2019
+ INITIAL_AVATAR_DATA,
1681
2020
  MediaService,
1682
2021
  MediaUrlCache,
1683
2022
  NewsService,
@@ -1685,6 +2024,7 @@ export {
1685
2024
  PackService,
1686
2025
  ProgressService,
1687
2026
  QuestionService,
2027
+ RENDER_ORDER,
1688
2028
  UserService,
1689
2029
  _resetMediaUrlWarning,
1690
2030
  aggregateChapterMetrics,
@@ -1692,31 +2032,57 @@ export {
1692
2032
  aggregateTopicMetrics,
1693
2033
  buildImageAnswer,
1694
2034
  calculateAccuracy,
2035
+ calculateDefaultExamMinutes,
1695
2036
  calculateExamTime,
2037
+ calculateTimeLeft,
1696
2038
  computeRatingDelta,
2039
+ convertToBangladeshTime,
1697
2040
  createOrjokClient,
1698
2041
  detectQuestionType,
2042
+ difficultyOptions,
1699
2043
  engToBanglaNumber,
2044
+ extractBilingualName,
1700
2045
  filterQuestionsByMode,
2046
+ formatBDT,
1701
2047
  formatCurriculumName,
2048
+ formatDate,
1702
2049
  formatExamTime,
1703
2050
  formatQuestionsToCsv,
1704
2051
  formatQuestionsToText,
2052
+ getActiveTab,
2053
+ getActualOptionFileName,
1705
2054
  getAllTiers,
2055
+ getCanonicalPath,
2056
+ getDhakaNow,
1706
2057
  getEloProgress,
1707
2058
  getEloTier,
1708
2059
  getRatingForSubject,
1709
2060
  getRelativeTime,
2061
+ getYouTubeId,
2062
+ isAuthGatedRoute,
2063
+ isCorrectAnswer,
2064
+ isDifficulty,
2065
+ isDifficultyMatch,
1710
2066
  isImageAnswer,
2067
+ isLanguage,
2068
+ isLanguageMatch,
2069
+ isQuestionLevel,
1711
2070
  isStorageKey,
2071
+ isValidMCQ,
2072
+ languageOptions,
2073
+ levelOptions,
1712
2074
  normalizeDifficulty,
2075
+ parseAvatarConfig,
1713
2076
  parseCsvQuestions,
1714
2077
  parseImageKeys,
1715
2078
  parseTextQuestions,
1716
2079
  processLatexText,
2080
+ reorderObjectKeys,
1717
2081
  resolveMediaUrl,
1718
2082
  resolveMediaUrls,
2083
+ sanitizeFileName,
1719
2084
  scoreExam,
2085
+ serializeAvatarConfig,
1720
2086
  shuffle,
1721
2087
  shuffleInPlace
1722
2088
  };