@speakableio/core 0.1.99 → 0.1.101

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/hooks.js ADDED
@@ -0,0 +1,1571 @@
1
+ // src/providers/SpeakableProvider.tsx
2
+ import { createContext, useContext, useEffect, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var FsCtx = createContext(null);
5
+ function useSpeakableApi() {
6
+ const ctx = useContext(FsCtx);
7
+ if (!ctx) throw new Error("useSpeakableApi must be used within a SpeakableProvider");
8
+ return ctx;
9
+ }
10
+
11
+ // src/domains/assignment/hooks/assignment.hooks.ts
12
+ import { useQuery } from "@tanstack/react-query";
13
+ var assignmentQueryKeys = {
14
+ all: ["assignments"],
15
+ byId: (id) => [...assignmentQueryKeys.all, id],
16
+ list: () => [...assignmentQueryKeys.all, "list"]
17
+ };
18
+ function useAssignment({
19
+ assignmentId,
20
+ enabled = true,
21
+ analyticType,
22
+ userId
23
+ }) {
24
+ const { speakableApi } = useSpeakableApi();
25
+ return useQuery({
26
+ queryKey: assignmentQueryKeys.byId(assignmentId),
27
+ queryFn: () => speakableApi.assignmentRepo.getAssignment({
28
+ assignmentId,
29
+ analyticType,
30
+ currentUserId: userId
31
+ }),
32
+ enabled
33
+ });
34
+ }
35
+
36
+ // src/domains/assignment/hooks/score-hooks.ts
37
+ import { useMutation, useQuery as useQuery2 } from "@tanstack/react-query";
38
+
39
+ // src/utils/debounce.utils.ts
40
+ function debounce(func, waitFor) {
41
+ let timeoutId;
42
+ return (...args) => new Promise((resolve, reject) => {
43
+ if (timeoutId) {
44
+ clearTimeout(timeoutId);
45
+ }
46
+ timeoutId = setTimeout(async () => {
47
+ try {
48
+ const result = await func(...args);
49
+ resolve(result);
50
+ } catch (error) {
51
+ reject(error);
52
+ }
53
+ }, waitFor);
54
+ });
55
+ }
56
+
57
+ // src/lib/tanstack/handle-optimistic-update-query.ts
58
+ var handleOptimisticUpdate = async ({
59
+ queryClient,
60
+ queryKey,
61
+ newData
62
+ }) => {
63
+ await queryClient.cancelQueries({
64
+ queryKey
65
+ });
66
+ const previousData = queryClient.getQueryData(queryKey);
67
+ if (previousData === void 0) {
68
+ queryClient.setQueryData(queryKey, newData);
69
+ } else {
70
+ queryClient.setQueryData(queryKey, { ...previousData, ...newData });
71
+ }
72
+ return { previousData };
73
+ };
74
+
75
+ // src/constants/speakable-plans.ts
76
+ var FEEDBACK_PLANS = {
77
+ FEEDBACK_TRANSCRIPT: "FEEDBACK_TRANSCRIPT",
78
+ // Transcript from the audio
79
+ FEEDBACK_SUMMARY: "FEEDBACK_SUMMARY",
80
+ // Chatty summary (Free plan)
81
+ FEEDBACK_GRAMMAR_INSIGHTS: "FEEDBACK_GRAMMAR_INSIGHTS",
82
+ // Grammar insights
83
+ FEEDBACK_SUGGESTED_RESPONSE: "FEEDBACK_SUGGESTED_RESPONSE",
84
+ // Suggested Response
85
+ FEEDBACK_RUBRIC: "FEEDBACK_RUBRIC",
86
+ // Suggested Response
87
+ FEEDBACK_GRADING_STANDARDS: "FEEDBACK_GRADING_STANDARDS",
88
+ // ACTFL / WIDA Estimate
89
+ FEEDBACK_TARGET_LANGUAGE: "FEEDBACK_TARGET_LANGUAGE",
90
+ // Ability to set the feedback language to the target language of the student
91
+ FEEDBACK_DISABLE_ALLOW_RETRIES: "FEEDBACK_DISABLE_ALLOW_RETRIES"
92
+ // Turn of allow retries
93
+ };
94
+ var AUTO_GRADING_PLANS = {
95
+ AUTO_GRADING_PASS_FAIL: "AUTO_GRADING_PASS_FAIL",
96
+ // Pass / fail grading
97
+ AUTO_GRADING_RUBRICS: "AUTO_GRADING_RUBRICS",
98
+ // Autograded rubrics
99
+ AUTO_GRADING_STANDARDS_BASED: "AUTO_GRADING_STANDARDS_BASED"
100
+ // Standards based grading
101
+ };
102
+ var AI_ASSISTANT_PLANS = {
103
+ AI_ASSISTANT_DOCUMENT_UPLOADS: "AI_ASSISTANT_DOCUMENT_UPLOADS",
104
+ // Allow document uploading
105
+ AI_ASSISTANT_UNLIMITED_USE: "AI_ASSISTANT_UNLIMITED_USE"
106
+ // Allow unlimited use of AI assistant. Otherwise, limits are used.
107
+ };
108
+ var ASSIGNMENT_SETTINGS_PLANS = {
109
+ ASSESSMENTS: "ASSESSMENTS",
110
+ // Ability to create assessment assignment types
111
+ GOOGLE_CLASSROOM_GRADE_PASSBACK: "GOOGLE_CLASSROOM_GRADE_PASSBACK"
112
+ // Assignment scores can sync with classroom
113
+ };
114
+ var ANALYTICS_PLANS = {
115
+ ANALYTICS_GRADEBOOK: "ANALYTICS_GRADEBOOK",
116
+ // Access to the gradebook page
117
+ ANALYTICS_CLASSROOM_ANALYTICS: "ANALYTICS_CLASSROOM_ANALYTICS",
118
+ // Access to the classroom analytics page
119
+ ANALYTICS_STUDENT_PROGRESS_REPORTS: "ANALYTICS_STUDENT_PROGRESS_REPORTS",
120
+ // Access to the panel that shows an individual student's progress and assignments
121
+ ANALYTICS_ASSIGNMENT_RESULTS: "ANALYTICS_ASSIGNMENT_RESULTS",
122
+ // Access to the assigment RESULTS page
123
+ ANALYTICS_ORGANIZATION: "ANALYTICS_ORGANIZATION"
124
+ // Access to the organization analytics panel (for permitted admins)
125
+ };
126
+ var SPACES_PLANS = {
127
+ SPACES_CREATE_SPACE: "SPACES_CREATE_SPACE",
128
+ // Ability to create spaces
129
+ SPACES_CHECK_POINTS: "SPACES_CHECK_POINTS"
130
+ // Feature not available yet. Ability to create checkpoints for spaces for data aggregation
131
+ };
132
+ var DISCOVER_PLANS = {
133
+ DISCOVER_ORGANIZATION_LIBRARY: "DISCOVER_ORGANIZATION_LIBRARY"
134
+ // Access to the organizations shared library
135
+ };
136
+ var MEDIA_AREA_PLANS = {
137
+ MEDIA_AREA_DOCUMENT_UPLOAD: "MEDIA_AREA_DOCUMENT_UPLOAD",
138
+ MEDIA_AREA_AUDIO_FILES: "MEDIA_AREA_AUDIO_FILES"
139
+ };
140
+ var FREE_PLAN = [];
141
+ var TEACHER_PRO_PLAN = [
142
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
143
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
144
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
145
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
146
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
147
+ SPACES_PLANS.SPACES_CREATE_SPACE
148
+ // AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
149
+ ];
150
+ var SCHOOL_STARTER = [
151
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
152
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
153
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
154
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
155
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
156
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
157
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
158
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
159
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
160
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
161
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
162
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
163
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
164
+ // ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
165
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
166
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
167
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
168
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
169
+ // ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
170
+ SPACES_PLANS.SPACES_CREATE_SPACE,
171
+ SPACES_PLANS.SPACES_CHECK_POINTS,
172
+ // DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
173
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
174
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
175
+ ];
176
+ var ORGANIZATION_PLAN = [
177
+ FEEDBACK_PLANS.FEEDBACK_TRANSCRIPT,
178
+ FEEDBACK_PLANS.FEEDBACK_SUMMARY,
179
+ FEEDBACK_PLANS.FEEDBACK_GRAMMAR_INSIGHTS,
180
+ FEEDBACK_PLANS.FEEDBACK_SUGGESTED_RESPONSE,
181
+ FEEDBACK_PLANS.FEEDBACK_RUBRIC,
182
+ FEEDBACK_PLANS.FEEDBACK_GRADING_STANDARDS,
183
+ FEEDBACK_PLANS.FEEDBACK_DISABLE_ALLOW_RETRIES,
184
+ FEEDBACK_PLANS.FEEDBACK_TARGET_LANGUAGE,
185
+ AUTO_GRADING_PLANS.AUTO_GRADING_PASS_FAIL,
186
+ AUTO_GRADING_PLANS.AUTO_GRADING_RUBRICS,
187
+ AUTO_GRADING_PLANS.AUTO_GRADING_STANDARDS_BASED,
188
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_DOCUMENT_UPLOADS,
189
+ AI_ASSISTANT_PLANS.AI_ASSISTANT_UNLIMITED_USE,
190
+ ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS,
191
+ ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK,
192
+ ANALYTICS_PLANS.ANALYTICS_GRADEBOOK,
193
+ ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS,
194
+ ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS,
195
+ ANALYTICS_PLANS.ANALYTICS_ORGANIZATION,
196
+ SPACES_PLANS.SPACES_CREATE_SPACE,
197
+ SPACES_PLANS.SPACES_CHECK_POINTS,
198
+ DISCOVER_PLANS.DISCOVER_ORGANIZATION_LIBRARY,
199
+ MEDIA_AREA_PLANS.MEDIA_AREA_DOCUMENT_UPLOAD,
200
+ MEDIA_AREA_PLANS.MEDIA_AREA_AUDIO_FILES
201
+ ];
202
+ var SpeakablePlanTypes = {
203
+ basic: "basic",
204
+ teacher_pro: "teacher_pro",
205
+ school_starter: "school_starter",
206
+ organization: "organization",
207
+ // OLD PLANS
208
+ starter: "starter",
209
+ growth: "growth",
210
+ professional: "professional"
211
+ };
212
+ var SpeakablePermissionsMap = {
213
+ [SpeakablePlanTypes.basic]: FREE_PLAN,
214
+ [SpeakablePlanTypes.starter]: TEACHER_PRO_PLAN,
215
+ [SpeakablePlanTypes.teacher_pro]: TEACHER_PRO_PLAN,
216
+ [SpeakablePlanTypes.growth]: ORGANIZATION_PLAN,
217
+ [SpeakablePlanTypes.professional]: ORGANIZATION_PLAN,
218
+ [SpeakablePlanTypes.organization]: ORGANIZATION_PLAN,
219
+ [SpeakablePlanTypes.school_starter]: SCHOOL_STARTER
220
+ };
221
+ var SpeakablePlanHierarchy = [
222
+ SpeakablePlanTypes.basic,
223
+ SpeakablePlanTypes.starter,
224
+ SpeakablePlanTypes.teacher_pro,
225
+ SpeakablePlanTypes.growth,
226
+ SpeakablePlanTypes.professional,
227
+ SpeakablePlanTypes.school_starter,
228
+ SpeakablePlanTypes.organization
229
+ ];
230
+
231
+ // src/hooks/usePermissions.ts
232
+ var usePermissions = () => {
233
+ const { permissions } = useSpeakableApi();
234
+ const has = (permission) => {
235
+ var _a;
236
+ return (_a = permissions.permissions) == null ? void 0 : _a.includes(permission);
237
+ };
238
+ return {
239
+ plan: permissions.plan,
240
+ permissionsLoaded: permissions.loaded,
241
+ isStripePlan: permissions.isStripePlan,
242
+ refreshDate: permissions.refreshDate,
243
+ isInstitutionPlan: permissions.isInstitutionPlan,
244
+ subscriptionId: permissions.subscriptionId,
245
+ contact: permissions.contact,
246
+ hasGradebook: has(ANALYTICS_PLANS.ANALYTICS_GRADEBOOK),
247
+ hasGoogleClassroomGradePassback: has(ASSIGNMENT_SETTINGS_PLANS.GOOGLE_CLASSROOM_GRADE_PASSBACK),
248
+ hasAssessments: has(ASSIGNMENT_SETTINGS_PLANS.ASSESSMENTS),
249
+ hasSectionAnalytics: has(ANALYTICS_PLANS.ANALYTICS_CLASSROOM_ANALYTICS),
250
+ hasStudentReports: has(ANALYTICS_PLANS.ANALYTICS_STUDENT_PROGRESS_REPORTS),
251
+ permissions: permissions || [],
252
+ hasStudentPortfolios: permissions.hasStudentPortfolios,
253
+ isFreeOrgTrial: permissions.type === "free_org_trial",
254
+ freeOrgTrialExpired: permissions.freeOrgTrialExpired
255
+ };
256
+ };
257
+ var usePermissions_default = usePermissions;
258
+
259
+ // src/lib/firebase/api.ts
260
+ var FirebaseAPI = class _FirebaseAPI {
261
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
262
+ constructor() {
263
+ this.config = null;
264
+ }
265
+ static getInstance() {
266
+ if (!_FirebaseAPI.instance) {
267
+ _FirebaseAPI.instance = new _FirebaseAPI();
268
+ }
269
+ return _FirebaseAPI.instance;
270
+ }
271
+ initialize(config) {
272
+ this.config = config;
273
+ }
274
+ get db() {
275
+ if (!this.config) throw new Error("Firebase API not initialized");
276
+ return this.config.db;
277
+ }
278
+ get helpers() {
279
+ if (!this.config) throw new Error("Firebase API not initialized");
280
+ return this.config.helpers;
281
+ }
282
+ get httpsCallable() {
283
+ var _a;
284
+ return (_a = this.config) == null ? void 0 : _a.httpsCallable;
285
+ }
286
+ logEvent(name, data) {
287
+ var _a;
288
+ (_a = this.config) == null ? void 0 : _a.logEvent(name, data);
289
+ }
290
+ accessQueryConstraints() {
291
+ const { query, orderBy, limit, startAt, startAfter, endAt, endBefore, where, increment } = this.helpers;
292
+ return {
293
+ query,
294
+ orderBy,
295
+ limit,
296
+ startAt,
297
+ startAfter,
298
+ endAt,
299
+ endBefore,
300
+ where,
301
+ increment
302
+ };
303
+ }
304
+ accessHelpers() {
305
+ const { doc, collection, writeBatch, serverTimestamp, setDoc } = this.helpers;
306
+ return {
307
+ doc: (path) => doc(this.db, path),
308
+ collection: (path) => collection(this.db, path),
309
+ writeBatch: () => writeBatch(this.db),
310
+ serverTimestamp,
311
+ setDoc
312
+ };
313
+ }
314
+ async getDoc(path) {
315
+ const { getDoc, doc } = this.helpers;
316
+ const docRef = doc(this.db, path);
317
+ const docSnap = await getDoc(docRef);
318
+ const data = docSnap.exists() ? {
319
+ ...docSnap.data(),
320
+ id: docSnap.id
321
+ } : null;
322
+ return {
323
+ id: docSnap.id,
324
+ data
325
+ };
326
+ }
327
+ async getDocs(path, ...queryConstraints) {
328
+ const { getDocs, query, collection } = this.helpers;
329
+ const collectionRef = collection(this.db, path);
330
+ const q = queryConstraints.length > 0 ? query(collectionRef, ...queryConstraints) : collectionRef;
331
+ const querySnapshot = await getDocs(q);
332
+ const data = querySnapshot.docs.map((doc) => ({
333
+ data: doc.data(),
334
+ id: doc.id
335
+ }));
336
+ return {
337
+ data,
338
+ querySnapshot,
339
+ empty: querySnapshot.empty
340
+ };
341
+ }
342
+ async addDoc(path, data) {
343
+ const { addDoc, collection } = this.helpers;
344
+ const collectionRef = collection(this.db, path);
345
+ const docRef = await addDoc(collectionRef, data);
346
+ return {
347
+ ...data,
348
+ id: docRef.id
349
+ };
350
+ }
351
+ async setDoc(path, data, options = {}) {
352
+ const { setDoc, doc } = this.helpers;
353
+ const docRef = doc(this.db, path);
354
+ await setDoc(docRef, data, options);
355
+ }
356
+ async updateDoc(path, data) {
357
+ const { updateDoc, doc } = this.helpers;
358
+ const docRef = doc(this.db, path);
359
+ await updateDoc(docRef, data);
360
+ }
361
+ async deleteDoc(path) {
362
+ const { deleteDoc, doc } = this.helpers;
363
+ const docRef = doc(this.db, path);
364
+ await deleteDoc(docRef);
365
+ }
366
+ async runTransaction(updateFunction) {
367
+ const { runTransaction } = this.helpers;
368
+ return runTransaction(this.db, updateFunction);
369
+ }
370
+ async runBatch(operations) {
371
+ const { writeBatch } = this.helpers;
372
+ const batch = writeBatch(this.db);
373
+ await Promise.all(operations.map((op) => op()));
374
+ await batch.commit();
375
+ }
376
+ writeBatch() {
377
+ const { writeBatch } = this.helpers;
378
+ const batch = writeBatch(this.db);
379
+ return batch;
380
+ }
381
+ };
382
+ var api = FirebaseAPI.getInstance();
383
+
384
+ // src/hooks/useGoogleClassroom.ts
385
+ var useGoogleClassroom = () => {
386
+ const submitAssignmentToGoogleClassroom = async ({
387
+ assignment,
388
+ scores,
389
+ googleUserId = null
390
+ // optional to override the user's googleUserId
391
+ }) => {
392
+ var _a, _b, _c;
393
+ try {
394
+ const { googleClassroomUserId = null } = scores;
395
+ const googleId = googleUserId || googleClassroomUserId;
396
+ if (!googleId)
397
+ return {
398
+ error: true,
399
+ message: "No Google Classroom ID found"
400
+ };
401
+ const { courseWorkId, maxPoints, owners, courseId } = assignment;
402
+ const draftGrade = (scores == null ? void 0 : scores.score) ? (scores == null ? void 0 : scores.score) / 100 * maxPoints : 0;
403
+ const result = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssignmentToGoogleClassroomV2")) == null ? void 0 : _c({
404
+ teacherId: owners[0],
405
+ courseId,
406
+ courseWorkId,
407
+ userId: googleId,
408
+ draftGrade
409
+ }));
410
+ return result;
411
+ } catch (error) {
412
+ return { error: true, message: error.message };
413
+ }
414
+ };
415
+ return {
416
+ submitAssignmentToGoogleClassroom
417
+ };
418
+ };
419
+
420
+ // src/constants/analytics.constants.ts
421
+ var ANALYTICS_EVENT_TYPES = {
422
+ VOICE_SUCCESS: "voice_success",
423
+ VOICE_FAIL: "voice_fail",
424
+ RESPOND_CARD_SUCCESS: "respond_card_success",
425
+ RESPOND_CARD_FAIL: "respond_card_fail",
426
+ RESPOND_WRITE_CARD_SUCCESS: "respond_write_card_success",
427
+ RESPOND_WRITE_CARD_FAIL: "respond_write_card_fail",
428
+ RESPOND_WRITE_CARD_SUBMITTED: "respond_write_card_submitted",
429
+ RESPOND_WRITE_CARD_ERROR: "respond_write_card_error",
430
+ RESPOND_CARD_ERROR: "respond_card_error",
431
+ RESPOND_CARD_SUBMITTED: "respond_card_submitted",
432
+ RESPOND_FREE_PLAN: "respond_free_plan",
433
+ RESPOND_WRITE_FREE_PLAN: "respond_write_free_plan",
434
+ SUBMISSION: "assignment_submitted",
435
+ ASSIGNMENT_STARTED: "assignment_started",
436
+ CREATE_ASSIGNMENT: "create_assignment",
437
+ MC_SUCCESS: "multiple_choice_success",
438
+ MC_FAIL: "multiple_choice_fail",
439
+ MC_ERROR: "multiple_choice_error",
440
+ ACTFL_LEVEL: "actfl_level",
441
+ WIDA_LEVEL: "wida_level"
442
+ };
443
+
444
+ // src/lib/firebase/firebase-analytics/assignment.ts
445
+ var logSubmitAssignment = (data = {}) => {
446
+ var _a, _b, _c;
447
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
448
+ eventType: ANALYTICS_EVENT_TYPES.SUBMISSION,
449
+ ...data
450
+ });
451
+ api.logEvent(ANALYTICS_EVENT_TYPES.SUBMISSION, data);
452
+ };
453
+ var logStartAssignment = (data = {}) => {
454
+ var _a, _b, _c;
455
+ if (data.courseId) {
456
+ (_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "handleCouresAnalyticsEvent")) == null ? void 0 : _c({
457
+ eventType: ANALYTICS_EVENT_TYPES.ASSIGNMENT_STARTED,
458
+ ...data
459
+ });
460
+ }
461
+ api.logEvent(ANALYTICS_EVENT_TYPES.ASSIGNMENT_STARTED, data);
462
+ };
463
+
464
+ // src/domains/notification/services/create-notification.service.ts
465
+ import dayjs from "dayjs";
466
+
467
+ // src/constants/web.constants.ts
468
+ var WEB_BASE_URL = "https://app.speakable.io";
469
+
470
+ // src/domains/notification/notification.constants.ts
471
+ var SPEAKABLE_NOTIFICATIONS = {
472
+ NEW_ASSIGNMENT: "new_assignment",
473
+ ASSESSMENT_SUBMITTED: "assessment_submitted",
474
+ ASSESSMENT_SCORED: "assessment_scored",
475
+ NEW_COMMENT: "NEW_COMMENT"
476
+ };
477
+ var SpeakableNotificationTypes = {
478
+ NEW_ASSIGNMENT: "NEW_ASSIGNMENT",
479
+ FEEDBACK_FROM_TEACHER: "FEEDBACK_FROM_TEACHER",
480
+ MESSAGE_FROM_STUDENT: "MESSAGE_FROM_STUDENT",
481
+ PHRASE_MARKED_CORRECT: "PHRASE_MARKED_CORRECT",
482
+ STUDENT_PROGRESS: "STUDENT_PROGRESS",
483
+ PLAYLIST_FOLLOWERS: "PLAYLIST_FOLLOWERS",
484
+ PLAYLIST_PLAYS: "PLAYLIST_PLAYS",
485
+ // New notifications
486
+ ASSESSMENT_SUBMITTED: "ASSESSMENT_SUBMITTED",
487
+ // Notification FOR TEACHER when student submits assessment
488
+ ASSESSMENT_SCORED: "ASSESSMENT_SCORED",
489
+ // Notification FOR STUDENT when teacher scores assessment
490
+ // Comment
491
+ NEW_COMMENT: "NEW_COMMENT"
492
+ };
493
+
494
+ // src/utils/error-handler.ts
495
+ var ServiceError = class extends Error {
496
+ constructor(message, originalError, code) {
497
+ super(message);
498
+ this.originalError = originalError;
499
+ this.code = code;
500
+ this.name = "ServiceError";
501
+ }
502
+ };
503
+ function withErrorHandler(fn, serviceName) {
504
+ return async (...args) => {
505
+ try {
506
+ return await fn(...args);
507
+ } catch (error) {
508
+ if (error instanceof Error && "code" in error) {
509
+ const firebaseError = error;
510
+ throw new ServiceError(
511
+ `Error in ${serviceName}: ${firebaseError.message}`,
512
+ error,
513
+ firebaseError.code
514
+ );
515
+ }
516
+ if (error instanceof Error) {
517
+ throw new ServiceError(`Error in ${serviceName}: ${error.message}`, error);
518
+ }
519
+ throw new ServiceError(`Unknown error in ${serviceName}`, error);
520
+ }
521
+ };
522
+ }
523
+
524
+ // src/domains/notification/services/send-notification.service.ts
525
+ var _sendNotification = async (sendTo, notification) => {
526
+ var _a, _b, _c;
527
+ const results = await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "createNotificationV2")) == null ? void 0 : _c({
528
+ sendTo,
529
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
530
+ notification
531
+ }));
532
+ return results;
533
+ };
534
+ var sendNotification = withErrorHandler(_sendNotification, "sendNotification");
535
+
536
+ // src/domains/notification/services/create-notification.service.ts
537
+ var createNotification = async ({
538
+ data,
539
+ type,
540
+ userId,
541
+ profile
542
+ }) => {
543
+ let result;
544
+ switch (type) {
545
+ case SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT:
546
+ result = await handleAssignNotifPromise({ data, profile });
547
+ break;
548
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED:
549
+ result = await createAssessmentSubmissionNotification({
550
+ data,
551
+ profile,
552
+ userId
553
+ });
554
+ break;
555
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED:
556
+ result = await createAssessmentScoredNotification({
557
+ data,
558
+ profile
559
+ });
560
+ break;
561
+ default:
562
+ result = null;
563
+ break;
564
+ }
565
+ return result;
566
+ };
567
+ var handleAssignNotifPromise = async ({
568
+ data: assignments,
569
+ profile
570
+ }) => {
571
+ if (!assignments.length) return;
572
+ try {
573
+ const notifsPromises = assignments.map(async (assignment) => {
574
+ const {
575
+ section,
576
+ section: { members },
577
+ ...rest
578
+ } = assignment;
579
+ if (!section || !members) throw new Error("Invalid assignment data");
580
+ const data = { section, sendTo: members, assignment: { ...rest } };
581
+ return createNewAssignmentNotification({ data, profile });
582
+ });
583
+ await Promise.all(notifsPromises);
584
+ return {
585
+ success: true,
586
+ message: "Assignment notifications sent successfully"
587
+ };
588
+ } catch (error) {
589
+ console.error("Error in handleAssignNotifPromise:", error);
590
+ throw error;
591
+ }
592
+ };
593
+ var createNewAssignmentNotification = async ({
594
+ data,
595
+ profile
596
+ }) => {
597
+ var _a;
598
+ const { assignment, sendTo } = data;
599
+ const teacherName = profile.displayName || "Your teacher";
600
+ const dueDate = assignment.dueDateTimestamp ? dayjs(assignment.dueDateTimestamp.toDate()).format("MMM Do") : null;
601
+ const results = await sendNotification(sendTo, {
602
+ courseId: assignment.courseId,
603
+ type: SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT,
604
+ senderName: teacherName,
605
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
606
+ messagePreview: `A new assignment "${assignment.name}" is now available. ${dueDate ? `Due ${dueDate}` : ""}`,
607
+ title: "New Assignment Available!",
608
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
609
+ });
610
+ return results;
611
+ };
612
+ var createAssessmentSubmissionNotification = async ({
613
+ data: assignment,
614
+ profile,
615
+ userId
616
+ }) => {
617
+ var _a;
618
+ const studentName = profile.displayName || "Your student";
619
+ const results = await sendNotification(assignment.owners, {
620
+ courseId: assignment.courseId,
621
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED,
622
+ link: `${WEB_BASE_URL}/a/${assignment.id}?studentId=${userId}`,
623
+ title: `Assessment Submitted!`,
624
+ senderName: studentName,
625
+ messagePreview: `${studentName} has submitted the assessment "${assignment.name}"`,
626
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url
627
+ });
628
+ return results;
629
+ };
630
+ var createAssessmentScoredNotification = async ({
631
+ data,
632
+ profile
633
+ }) => {
634
+ var _a, _b, _c, _d, _e;
635
+ const { assignment, sendTo } = data;
636
+ const teacherName = profile.displayName || "Your teacher";
637
+ const title = `${assignment.isAssessment ? "Assessment" : "Assignment"} Reviewed!`;
638
+ const messagePreview = `Your ${assignment.isAssessment ? "assessment" : "assignment"} has been reviewed by your teacher. Click to view the feedback.`;
639
+ const results = await sendNotification(sendTo, {
640
+ courseId: assignment.courseId,
641
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED,
642
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
643
+ title,
644
+ messagePreview,
645
+ imageUrl: (_a = profile.image) == null ? void 0 : _a.url,
646
+ senderName: teacherName
647
+ });
648
+ await ((_e = (_c = (_b = api).httpsCallable) == null ? void 0 : _c.call(_b, "sendAssessmentScoredEmail")) == null ? void 0 : _e({
649
+ assessmentTitle: assignment.name,
650
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
651
+ senderImage: ((_d = profile.image) == null ? void 0 : _d.url) || "",
652
+ studentId: sendTo[0],
653
+ teacherName: profile.displayName
654
+ }));
655
+ return results;
656
+ };
657
+
658
+ // src/domains/notification/hooks/notification.hooks.ts
659
+ var notificationQueryKeys = {
660
+ all: ["notifications"],
661
+ byId: (id) => [...notificationQueryKeys.all, id]
662
+ };
663
+ var useCreateNotification = () => {
664
+ const { user, queryClient } = useSpeakableApi();
665
+ const handleCreateNotifications = async (type, data) => {
666
+ var _a, _b;
667
+ const result = await createNotification({
668
+ type,
669
+ userId: user.auth.uid,
670
+ profile: (_a = user == null ? void 0 : user.profile) != null ? _a : {},
671
+ data
672
+ });
673
+ queryClient.invalidateQueries({
674
+ queryKey: notificationQueryKeys.byId((_b = user == null ? void 0 : user.auth.uid) != null ? _b : "")
675
+ });
676
+ return result;
677
+ };
678
+ return {
679
+ createNotification: handleCreateNotifications
680
+ };
681
+ };
682
+
683
+ // src/domains/assignment/assignment.constants.ts
684
+ var ASSIGNMENTS_COLLECTION = "assignments";
685
+ var ANALYTICS_SUBCOLLECTION = "analytics";
686
+ var SCORES_SUBCOLLECTION = "scores";
687
+ var refsAssignmentFiresotre = {
688
+ allAssignments: () => ASSIGNMENTS_COLLECTION,
689
+ assignment: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}`,
690
+ assignmentAllAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}`,
691
+ assignmentAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}/${params.type}`,
692
+ assignmentScores: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${SCORES_SUBCOLLECTION}/${params.userId}`
693
+ };
694
+
695
+ // src/domains/assignment/utils/create-default-score.ts
696
+ var defaultScore = (props) => {
697
+ const { serverTimestamp } = api.accessHelpers();
698
+ const score = {
699
+ progress: 0,
700
+ score: 0,
701
+ startDate: serverTimestamp(),
702
+ status: "IN_PROGRESS",
703
+ submitted: false,
704
+ cards: {},
705
+ lastPlayed: serverTimestamp(),
706
+ owners: props.owners,
707
+ userId: props.userId
708
+ };
709
+ if (props.googleClassroomUserId) {
710
+ score.googleClassroomUserId = props.googleClassroomUserId;
711
+ }
712
+ if (props.courseId) {
713
+ score.courseId = props.courseId;
714
+ }
715
+ return score;
716
+ };
717
+
718
+ // src/domains/assignment/score-practice.constants.ts
719
+ var SCORES_PRACTICE_COLLECTION = "users";
720
+ var SCORES_PRACTICE_SUBCOLLECTION = "practice";
721
+ var refsScoresPractice = {
722
+ practiceScores: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}`,
723
+ practiceScoreHistoryRefDoc: (params) => `${SCORES_PRACTICE_COLLECTION}/${params.userId}/${SCORES_PRACTICE_SUBCOLLECTION}/${params.setId}/attempts/${params.date}`
724
+ };
725
+
726
+ // src/domains/assignment/services/create-score.service.ts
727
+ async function _createScore(params) {
728
+ var _a, _b, _c;
729
+ if (params.isAssignment) {
730
+ const ref = refsAssignmentFiresotre.assignmentScores({
731
+ id: params.activityId,
732
+ userId: params.userId
733
+ });
734
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "updateAssignmentGradebookStatus")) == null ? void 0 : _c({
735
+ assignmentId: params.activityId,
736
+ userId: params.userId,
737
+ status: "IN_PROGRESS",
738
+ score: null
739
+ }));
740
+ await api.setDoc(ref, params.scoreData, { merge: true });
741
+ return {
742
+ id: params.userId
743
+ };
744
+ } else {
745
+ const ref = refsScoresPractice.practiceScores({
746
+ userId: params.userId,
747
+ setId: params.activityId
748
+ });
749
+ await api.setDoc(ref, params.scoreData);
750
+ return {
751
+ id: params.userId
752
+ };
753
+ }
754
+ }
755
+ var createScore = withErrorHandler(_createScore, "createScore");
756
+
757
+ // src/domains/assignment/services/get-score.service.ts
758
+ async function getAssignmentScore({
759
+ userId,
760
+ assignment,
761
+ googleClassroomUserId
762
+ }) {
763
+ const path = refsAssignmentFiresotre.assignmentScores({
764
+ id: assignment.id,
765
+ userId
766
+ });
767
+ const response = await api.getDoc(path);
768
+ if (response.data == null) {
769
+ const newScore = {
770
+ ...defaultScore({
771
+ owners: [userId],
772
+ userId,
773
+ courseId: assignment.courseId,
774
+ googleClassroomUserId
775
+ }),
776
+ assignmentId: assignment.id
777
+ };
778
+ logStartAssignment({
779
+ courseId: assignment.courseId
780
+ });
781
+ const result = await createScore({
782
+ activityId: assignment.id,
783
+ userId,
784
+ isAssignment: true,
785
+ scoreData: newScore
786
+ });
787
+ return {
788
+ ...newScore,
789
+ id: result.id
790
+ };
791
+ }
792
+ return response.data;
793
+ }
794
+ async function getPracticeScore({ userId, setId }) {
795
+ const path = refsScoresPractice.practiceScores({ userId, setId });
796
+ const response = await api.getDoc(path);
797
+ if (response.data == null) {
798
+ const newScore = {
799
+ ...defaultScore({
800
+ owners: [userId],
801
+ userId
802
+ }),
803
+ setId
804
+ };
805
+ const result = await createScore({
806
+ activityId: setId,
807
+ userId,
808
+ isAssignment: false,
809
+ scoreData: newScore
810
+ });
811
+ return {
812
+ ...newScore,
813
+ id: result.id
814
+ };
815
+ }
816
+ return response.data;
817
+ }
818
+ async function _getScore(params) {
819
+ if (params.isAssignment) {
820
+ return await getAssignmentScore({
821
+ userId: params.userId,
822
+ assignment: {
823
+ id: params.activityId,
824
+ courseId: params.courseId
825
+ },
826
+ googleClassroomUserId: params.googleClassroomUserId
827
+ });
828
+ } else {
829
+ return await getPracticeScore({
830
+ userId: params.userId,
831
+ setId: params.activityId
832
+ });
833
+ }
834
+ }
835
+ var getScore = withErrorHandler(_getScore, "getScore");
836
+
837
+ // src/domains/assignment/utils/calculateScoreAndProgress.ts
838
+ var calculateScoreAndProgress = (scores, cardsList, weights) => {
839
+ const totalSetPoints = cardsList.reduce((acc, cardId) => {
840
+ acc += (weights == null ? void 0 : weights[cardId]) || 1;
841
+ return acc;
842
+ }, 0);
843
+ const totalPointsAwarded = Object.keys((scores == null ? void 0 : scores.cards) || {}).reduce((acc, cardId) => {
844
+ var _a, _b;
845
+ const cardScores = (_a = scores == null ? void 0 : scores.cards) == null ? void 0 : _a[cardId];
846
+ if ((cardScores == null ? void 0 : cardScores.completed) || (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0) {
847
+ const score2 = (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0 ? Number((_b = cardScores == null ? void 0 : cardScores.score) != null ? _b : 0) : null;
848
+ const weight = (weights == null ? void 0 : weights[cardId]) || 1;
849
+ const fraction = (score2 != null ? score2 : 0) / 100;
850
+ if (score2 || score2 === 0) {
851
+ acc += weight * fraction;
852
+ } else {
853
+ acc += weight;
854
+ }
855
+ }
856
+ return acc;
857
+ }, 0);
858
+ const totalCompletedCards = Object.keys((scores == null ? void 0 : scores.cards) || {}).reduce((acc, cardId) => {
859
+ var _a;
860
+ const cardScores = (_a = scores == null ? void 0 : scores.cards) == null ? void 0 : _a[cardId];
861
+ if ((cardScores == null ? void 0 : cardScores.completed) || (cardScores == null ? void 0 : cardScores.score) || (cardScores == null ? void 0 : cardScores.score) === 0) {
862
+ acc += 1;
863
+ }
864
+ return acc;
865
+ }, 0);
866
+ const percent = totalPointsAwarded / totalSetPoints;
867
+ const score = Math.round(percent * 100);
868
+ const progress = Math.round(totalCompletedCards / (cardsList.length || 1) * 100);
869
+ return { score, progress };
870
+ };
871
+ var calculateScoreAndProgress_default = calculateScoreAndProgress;
872
+
873
+ // src/domains/assignment/services/update-score.service.ts
874
+ async function _updateScore(params) {
875
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
876
+ id: params.activityId,
877
+ userId: params.userId
878
+ }) : refsScoresPractice.practiceScores({
879
+ setId: params.activityId,
880
+ userId: params.userId
881
+ });
882
+ await api.updateDoc(path, {
883
+ ...params.data
884
+ });
885
+ }
886
+ var updateScore = withErrorHandler(_updateScore, "updateScore");
887
+ async function _updateCardScore(params) {
888
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
889
+ id: params.activityId,
890
+ userId: params.userId
891
+ }) : refsScoresPractice.practiceScores({
892
+ setId: params.activityId,
893
+ userId: params.userId
894
+ });
895
+ const updates = Object.keys(params.updates.cardScore).reduce(
896
+ (acc, key) => {
897
+ acc[`cards.${params.cardId}.${key}`] = params.updates.cardScore[key];
898
+ return acc;
899
+ },
900
+ {}
901
+ );
902
+ if (params.updates.progress) {
903
+ updates.progress = params.updates.progress;
904
+ }
905
+ if (params.updates.score) {
906
+ updates.score = params.updates.score;
907
+ }
908
+ await api.updateDoc(path, {
909
+ ...updates
910
+ });
911
+ }
912
+ var updateCardScore = withErrorHandler(_updateCardScore, "updateCardScore");
913
+
914
+ // src/domains/assignment/services/clear-score.service.ts
915
+ import dayjs2 from "dayjs";
916
+ async function clearScore(params) {
917
+ var _a, _b, _c, _d, _e;
918
+ const update = {
919
+ [`cards.${params.cardId}`]: {
920
+ attempts: ((_a = params.cardScores.attempts) != null ? _a : 1) + 1,
921
+ correct: (_b = params.cardScores.correct) != null ? _b : 0,
922
+ // save old score history
923
+ history: [
924
+ {
925
+ ...params.cardScores,
926
+ attempts: (_c = params.cardScores.attempts) != null ? _c : 1,
927
+ correct: (_d = params.cardScores.correct) != null ? _d : 0,
928
+ retryTime: dayjs2().format("YYYY-MM-DD HH:mm:ss"),
929
+ history: null
930
+ },
931
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
932
+ ...(_e = params.cardScores.history) != null ? _e : []
933
+ ]
934
+ }
935
+ };
936
+ const path = params.isAssignment ? refsAssignmentFiresotre.assignmentScores({
937
+ id: params.activityId,
938
+ userId: params.userId
939
+ }) : refsScoresPractice.practiceScores({
940
+ setId: params.activityId,
941
+ userId: params.userId
942
+ });
943
+ await api.updateDoc(path, update);
944
+ return {
945
+ update,
946
+ activityId: params.activityId
947
+ };
948
+ }
949
+
950
+ // src/domains/assignment/services/submit-assignment-score.service.ts
951
+ import dayjs3 from "dayjs";
952
+ async function _submitAssignmentScore({
953
+ cardIds,
954
+ assignment,
955
+ weights,
956
+ userId,
957
+ status,
958
+ studentName
959
+ }) {
960
+ const { serverTimestamp } = api.accessHelpers();
961
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
962
+ const fieldsUpdated = {
963
+ submitted: true,
964
+ progress: 100,
965
+ submissionDate: serverTimestamp(),
966
+ status
967
+ };
968
+ if (assignment.isAssessment) {
969
+ const result = await handleAssessment(
970
+ assignment,
971
+ userId,
972
+ cardIds,
973
+ weights,
974
+ fieldsUpdated,
975
+ studentName
976
+ );
977
+ return result;
978
+ } else if (assignment.courseId) {
979
+ await handleCourseAssignment(assignment, userId);
980
+ }
981
+ await api.updateDoc(path, { ...fieldsUpdated });
982
+ return { success: true, fieldsUpdated };
983
+ }
984
+ var submitAssignmentScore = withErrorHandler(
985
+ _submitAssignmentScore,
986
+ "submitAssignmentScore"
987
+ );
988
+ async function handleAssessment(assignment, userId, cardIds, weights, fieldsUpdated, studentName) {
989
+ var _a, _b, _c;
990
+ const path = refsAssignmentFiresotre.assignmentScores({ id: assignment.id, userId });
991
+ const response = await api.getDoc(path);
992
+ if (!response.data) {
993
+ throw new Error("Score not found");
994
+ }
995
+ const { score: scoreCalculated } = calculateScoreAndProgress_default(response.data, cardIds, weights);
996
+ await api.updateDoc(path, { score: scoreCalculated, status: "PENDING_REVIEW" });
997
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssessment")) == null ? void 0 : _c({
998
+ assignmentId: assignment.id,
999
+ assignmentTitle: assignment.name,
1000
+ userId,
1001
+ teacherId: assignment.owners[0],
1002
+ studentName
1003
+ }));
1004
+ fieldsUpdated.status = "PENDING_REVIEW";
1005
+ return { success: true, fieldsUpdated };
1006
+ }
1007
+ async function handleCourseAssignment(assignment, userId) {
1008
+ var _a, _b, _c;
1009
+ await ((_c = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "submitAssignmentV2")) == null ? void 0 : _c({
1010
+ assignmentId: assignment.id,
1011
+ userId
1012
+ }));
1013
+ }
1014
+ async function submitPracticeScore({
1015
+ setId,
1016
+ userId,
1017
+ scores
1018
+ }) {
1019
+ const { serverTimestamp } = api.accessHelpers();
1020
+ const date = dayjs3().format("YYYY-MM-DD-HH-mm");
1021
+ const ref = refsScoresPractice.practiceScoreHistoryRefDoc({ setId, userId, date });
1022
+ const fieldsUpdated = {
1023
+ ...scores,
1024
+ submitted: true,
1025
+ progress: 100,
1026
+ submissionDate: serverTimestamp(),
1027
+ status: "SUBMITTED"
1028
+ };
1029
+ await api.setDoc(ref, { ...fieldsUpdated });
1030
+ const refScores = refsScoresPractice.practiceScores({ userId, setId });
1031
+ await api.deleteDoc(refScores);
1032
+ return { success: true, fieldsUpdated };
1033
+ }
1034
+
1035
+ // src/domains/assignment/hooks/score-hooks.ts
1036
+ var scoreQueryKeys = {
1037
+ all: ["scores"],
1038
+ byId: (id) => [...scoreQueryKeys.all, id],
1039
+ list: () => [...scoreQueryKeys.all, "list"]
1040
+ };
1041
+ function useScore({
1042
+ isAssignment,
1043
+ activityId,
1044
+ userId = "",
1045
+ courseId,
1046
+ enabled = true,
1047
+ googleClassroomUserId
1048
+ }) {
1049
+ return useQuery2({
1050
+ queryFn: () => getScore({
1051
+ userId,
1052
+ courseId,
1053
+ activityId,
1054
+ googleClassroomUserId,
1055
+ isAssignment
1056
+ }),
1057
+ queryKey: scoreQueryKeys.byId(activityId),
1058
+ enabled
1059
+ });
1060
+ }
1061
+ var debounceUpdateScore = debounce(updateScore, 1e3);
1062
+ function useUpdateScore() {
1063
+ const { queryClient } = useSpeakableApi();
1064
+ const mutation = useMutation({
1065
+ mutationFn: debounceUpdateScore,
1066
+ onMutate: (variables) => {
1067
+ return handleOptimisticUpdate({
1068
+ queryClient,
1069
+ queryKey: scoreQueryKeys.byId(variables.activityId),
1070
+ newData: variables.data
1071
+ });
1072
+ },
1073
+ onError: (_, variables, context) => {
1074
+ if (context == null ? void 0 : context.previousData)
1075
+ queryClient.setQueryData(scoreQueryKeys.byId(variables.activityId), context.previousData);
1076
+ },
1077
+ onSettled: (_, err, variables) => {
1078
+ queryClient.invalidateQueries({
1079
+ queryKey: scoreQueryKeys.byId(variables.activityId)
1080
+ });
1081
+ }
1082
+ });
1083
+ return {
1084
+ mutationUpdateScore: mutation
1085
+ };
1086
+ }
1087
+ function useUpdateCardScore({
1088
+ isAssignment,
1089
+ activityId,
1090
+ userId,
1091
+ cardIds,
1092
+ weights
1093
+ }) {
1094
+ const { queryClient } = useSpeakableApi();
1095
+ const queryKey = scoreQueryKeys.byId(activityId);
1096
+ const mutation = useMutation({
1097
+ mutationFn: async ({ cardId, cardScore }) => {
1098
+ const previousScores = queryClient.getQueryData(queryKey);
1099
+ const { progress, score, newScoreUpdated, updatedCardScore } = getScoreUpdated({
1100
+ previousScores: previousScores != null ? previousScores : {},
1101
+ cardId,
1102
+ cardScore,
1103
+ cardIds,
1104
+ weights
1105
+ });
1106
+ await updateCardScore({
1107
+ userId,
1108
+ cardId,
1109
+ isAssignment,
1110
+ activityId,
1111
+ updates: {
1112
+ cardScore: updatedCardScore,
1113
+ progress,
1114
+ score
1115
+ }
1116
+ });
1117
+ return { cardId, scoresUpdated: newScoreUpdated };
1118
+ },
1119
+ onMutate: ({ cardId, cardScore }) => {
1120
+ queryClient.setQueryData(queryKey, (previousScore) => {
1121
+ const updates = handleOptimisticScore({
1122
+ score: previousScore,
1123
+ cardId,
1124
+ cardScore,
1125
+ cardIds,
1126
+ weights
1127
+ });
1128
+ return {
1129
+ ...previousScore,
1130
+ ...updates
1131
+ };
1132
+ });
1133
+ },
1134
+ // eslint-disable-next-line no-unused-vars
1135
+ onError: (error) => {
1136
+ console.log(error.message);
1137
+ const previousData = queryClient.getQueryData(queryKey);
1138
+ if (previousData) {
1139
+ queryClient.setQueryData(queryKey, previousData);
1140
+ }
1141
+ }
1142
+ });
1143
+ return {
1144
+ mutationUpdateCardScore: mutation
1145
+ };
1146
+ }
1147
+ var getScoreUpdated = ({
1148
+ cardId,
1149
+ cardScore,
1150
+ previousScores,
1151
+ cardIds,
1152
+ weights
1153
+ }) => {
1154
+ var _a, _b;
1155
+ const previousCard = (_a = previousScores.cards) == null ? void 0 : _a[cardId];
1156
+ const newCardScore = {
1157
+ ...previousCard != null ? previousCard : {},
1158
+ ...cardScore
1159
+ };
1160
+ const newScores = {
1161
+ ...previousScores,
1162
+ cards: {
1163
+ ...(_b = previousScores.cards) != null ? _b : {},
1164
+ [cardId]: newCardScore
1165
+ }
1166
+ };
1167
+ const { score, progress } = calculateScoreAndProgress_default(newScores, cardIds, weights);
1168
+ return {
1169
+ newScoreUpdated: newScores,
1170
+ updatedCardScore: cardScore,
1171
+ score,
1172
+ progress
1173
+ };
1174
+ };
1175
+ var handleOptimisticScore = ({
1176
+ score,
1177
+ cardId,
1178
+ cardScore,
1179
+ cardIds,
1180
+ weights
1181
+ }) => {
1182
+ var _a;
1183
+ let cards = { ...(_a = score == null ? void 0 : score.cards) != null ? _a : {} };
1184
+ cards = {
1185
+ ...cards,
1186
+ [cardId]: {
1187
+ ...cards[cardId],
1188
+ ...cardScore
1189
+ }
1190
+ };
1191
+ const { score: scoreValue, progress } = calculateScoreAndProgress_default(
1192
+ // @ts-ignore
1193
+ {
1194
+ ...score != null ? score : {},
1195
+ cards
1196
+ },
1197
+ cardIds,
1198
+ weights
1199
+ );
1200
+ return { cards, score: scoreValue, progress };
1201
+ };
1202
+ function useClearScore() {
1203
+ const { queryClient } = useSpeakableApi();
1204
+ const mutation = useMutation({
1205
+ mutationFn: clearScore,
1206
+ onSettled: (result) => {
1207
+ var _a;
1208
+ queryClient.invalidateQueries({
1209
+ queryKey: scoreQueryKeys.byId((_a = result == null ? void 0 : result.activityId) != null ? _a : "")
1210
+ });
1211
+ }
1212
+ });
1213
+ return {
1214
+ mutationClearScore: mutation
1215
+ };
1216
+ }
1217
+ function useSubmitAssignmentScore({
1218
+ onAssignmentSubmitted,
1219
+ studentName
1220
+ }) {
1221
+ const { queryClient } = useSpeakableApi();
1222
+ const { hasGoogleClassroomGradePassback } = usePermissions_default();
1223
+ const { submitAssignmentToGoogleClassroom } = useGoogleClassroom();
1224
+ const { createNotification: createNotification2 } = useCreateNotification();
1225
+ const mutation = useMutation({
1226
+ mutationFn: async ({
1227
+ assignment,
1228
+ userId,
1229
+ cardIds,
1230
+ weights,
1231
+ scores,
1232
+ status
1233
+ }) => {
1234
+ try {
1235
+ const scoreUpdated = await submitAssignmentScore({
1236
+ assignment,
1237
+ userId,
1238
+ cardIds,
1239
+ weights,
1240
+ status,
1241
+ studentName
1242
+ });
1243
+ if (assignment.courseWorkId != null && !assignment.isAssessment && hasGoogleClassroomGradePassback) {
1244
+ await submitAssignmentToGoogleClassroom({
1245
+ assignment,
1246
+ scores
1247
+ });
1248
+ }
1249
+ if (assignment.isAssessment) {
1250
+ createNotification2(SpeakableNotificationTypes.ASSESSMENT_SUBMITTED, assignment);
1251
+ }
1252
+ if (assignment == null ? void 0 : assignment.id) {
1253
+ logSubmitAssignment({
1254
+ courseId: assignment == null ? void 0 : assignment.courseId
1255
+ });
1256
+ }
1257
+ onAssignmentSubmitted(assignment.id);
1258
+ queryClient.setQueryData(scoreQueryKeys.byId(assignment.id), {
1259
+ ...scores,
1260
+ ...scoreUpdated.fieldsUpdated
1261
+ });
1262
+ return {
1263
+ success: true,
1264
+ message: "Score submitted successfully"
1265
+ };
1266
+ } catch (error) {
1267
+ return {
1268
+ success: false,
1269
+ error
1270
+ };
1271
+ }
1272
+ }
1273
+ });
1274
+ return {
1275
+ submitAssignmentScore: mutation.mutateAsync,
1276
+ isLoading: mutation.isPending
1277
+ };
1278
+ }
1279
+ function useSubmitPracticeScore() {
1280
+ const { queryClient } = useSpeakableApi();
1281
+ const mutation = useMutation({
1282
+ mutationFn: async ({
1283
+ setId,
1284
+ userId,
1285
+ scores
1286
+ }) => {
1287
+ try {
1288
+ await submitPracticeScore({
1289
+ setId,
1290
+ userId,
1291
+ scores
1292
+ });
1293
+ queryClient.invalidateQueries({
1294
+ queryKey: scoreQueryKeys.byId(setId)
1295
+ });
1296
+ return {
1297
+ success: true,
1298
+ message: "Score submitted successfully"
1299
+ };
1300
+ } catch (error) {
1301
+ return {
1302
+ success: false,
1303
+ error
1304
+ };
1305
+ }
1306
+ }
1307
+ });
1308
+ return {
1309
+ submitPracticeScore: mutation.mutateAsync,
1310
+ isLoading: mutation.isPending
1311
+ };
1312
+ }
1313
+
1314
+ // src/domains/cards/card.hooks.ts
1315
+ import { useMutation as useMutation2, useQueries, useQuery as useQuery3 } from "@tanstack/react-query";
1316
+ import { useMemo } from "react";
1317
+
1318
+ // src/domains/cards/card.constants.ts
1319
+ var CARDS_COLLECTION = "flashcards";
1320
+ var refsCardsFiresotre = {
1321
+ allCards: CARDS_COLLECTION,
1322
+ card: (id) => `${CARDS_COLLECTION}/${id}`
1323
+ };
1324
+
1325
+ // src/domains/cards/services/get-card.service.ts
1326
+ async function _getCard(params) {
1327
+ const ref = refsCardsFiresotre.card(params.cardId);
1328
+ const response = await api.getDoc(ref);
1329
+ if (!response.data) return null;
1330
+ return response.data;
1331
+ }
1332
+ var getCard = withErrorHandler(_getCard, "getCard");
1333
+
1334
+ // src/domains/cards/services/create-card.service.ts
1335
+ import { v4 } from "uuid";
1336
+
1337
+ // src/utils/text-utils.ts
1338
+ import sha1 from "js-sha1";
1339
+ var purify = (word) => {
1340
+ return word.normalize("NFD").replace(/\/([^" "]*)/g, "").replace(/\([^()]*\)/g, "").replace(/([^()]*)/g, "").replace(/[\u0300-\u036f]/g, "").replace(/[-]/g, " ").replace(/[.,/#!¡¿?؟。,.?$%^&*;:{}=\-_`~()’'…\s]/g, "").replace(/\s\s+/g, " ").toLowerCase().trim();
1341
+ };
1342
+ var cleanString = (words) => {
1343
+ const splitWords = words == null ? void 0 : words.split("+");
1344
+ if (splitWords && splitWords.length === 1) {
1345
+ const newWord = purify(words);
1346
+ return newWord;
1347
+ } else if (splitWords && splitWords.length > 1) {
1348
+ const split = splitWords.map((w) => purify(w));
1349
+ return split;
1350
+ } else {
1351
+ return "";
1352
+ }
1353
+ };
1354
+ var getWordHash = (word, language) => {
1355
+ const cleanedWord = cleanString(word);
1356
+ const wordHash = sha1(`${language}-${cleanedWord}`);
1357
+ console.log("wordHash core library", wordHash);
1358
+ return wordHash;
1359
+ };
1360
+
1361
+ // src/domains/cards/services/get-card-verification-status.service.ts
1362
+ var charactarLanguages = ["zh", "ja", "ko"];
1363
+ var getVerificationStatus = async (target_text, language) => {
1364
+ if ((target_text == null ? void 0 : target_text.length) < 3 && !charactarLanguages.includes(language)) {
1365
+ return "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
1366
+ }
1367
+ const hash = getWordHash(target_text, language);
1368
+ const response = await api.getDoc(`checked-pronunciations/${hash}`);
1369
+ try {
1370
+ if (response.data) {
1371
+ return processRecord(response.data);
1372
+ } else {
1373
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1374
+ }
1375
+ } catch (e) {
1376
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1377
+ }
1378
+ };
1379
+ var processRecord = (data) => {
1380
+ const { pronunciations = 0, fails = 0 } = data;
1381
+ const attempts = pronunciations + fails;
1382
+ const successRate = attempts > 0 ? pronunciations / attempts * 100 : 0;
1383
+ let newStatus = null;
1384
+ if (attempts < 6) {
1385
+ return "NOT_CHECKED" /* NOT_CHECKED */;
1386
+ }
1387
+ if (successRate > 25) {
1388
+ newStatus = "VERIFIED" /* VERIFIED */;
1389
+ } else if (successRate > 10) {
1390
+ newStatus = "WARNING" /* WARNING */;
1391
+ } else if (fails > 20 && successRate < 10 && pronunciations > 1) {
1392
+ newStatus = "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
1393
+ } else if (pronunciations === 0 && fails > 20) {
1394
+ newStatus = "NOT_WORKING" /* NOT_WORKING */;
1395
+ } else {
1396
+ newStatus = "NOT_CHECKED" /* NOT_CHECKED */;
1397
+ }
1398
+ return newStatus;
1399
+ };
1400
+
1401
+ // src/domains/cards/services/create-card.service.ts
1402
+ async function _createCard({ data }) {
1403
+ const response = await api.addDoc(refsCardsFiresotre.allCards, data);
1404
+ return response;
1405
+ }
1406
+ var createCard = withErrorHandler(_createCard, "createCard");
1407
+ async function _createCards({ cards }) {
1408
+ const { writeBatch, doc } = api.accessHelpers();
1409
+ const batch = writeBatch();
1410
+ const cardsWithId = [];
1411
+ for (const card of cards) {
1412
+ const cardId = v4();
1413
+ const ref = doc(refsCardsFiresotre.card(cardId));
1414
+ const newCardObject = {
1415
+ ...card,
1416
+ id: cardId
1417
+ };
1418
+ if (card.type === "READ_REPEAT" /* READ_REPEAT */ && card.target_text && card.language) {
1419
+ const verificationStatus = await getVerificationStatus(card.target_text, card.language);
1420
+ newCardObject.verificationStatus = verificationStatus || null;
1421
+ }
1422
+ cardsWithId.push(newCardObject);
1423
+ batch.set(ref, newCardObject);
1424
+ }
1425
+ await batch.commit();
1426
+ return cardsWithId;
1427
+ }
1428
+ var createCards = withErrorHandler(_createCards, "createCards");
1429
+
1430
+ // src/domains/cards/card.hooks.ts
1431
+ var cardsQueryKeys = {
1432
+ all: ["cards"],
1433
+ one: (params) => [...cardsQueryKeys.all, params.cardId]
1434
+ };
1435
+ function useCards({
1436
+ cardIds,
1437
+ enabled = true,
1438
+ asObject
1439
+ }) {
1440
+ const queries = useQueries({
1441
+ queries: cardIds.map((cardId) => ({
1442
+ enabled: enabled && cardIds.length > 0,
1443
+ queryKey: cardsQueryKeys.one({
1444
+ cardId
1445
+ }),
1446
+ queryFn: () => getCard({ cardId })
1447
+ }))
1448
+ });
1449
+ const cards = queries.map((query) => query.data).filter(Boolean);
1450
+ const cardsObject = useMemo(() => {
1451
+ if (!asObject) return null;
1452
+ return cards.reduce((acc, card) => {
1453
+ acc[card.id] = card;
1454
+ return acc;
1455
+ }, {});
1456
+ }, [asObject, cards]);
1457
+ return {
1458
+ cards,
1459
+ cardsObject,
1460
+ cardsQueries: queries
1461
+ };
1462
+ }
1463
+ function useCreateCard() {
1464
+ const { queryClient } = useSpeakableApi();
1465
+ const mutationCreateCard = useMutation2({
1466
+ mutationFn: createCard,
1467
+ onSuccess: (cardCreated) => {
1468
+ queryClient.invalidateQueries({ queryKey: cardsQueryKeys.one({ cardId: cardCreated.id }) });
1469
+ }
1470
+ });
1471
+ return {
1472
+ mutationCreateCard
1473
+ };
1474
+ }
1475
+ function useCreateCards() {
1476
+ const mutationCreateCards = useMutation2({
1477
+ mutationFn: createCards
1478
+ });
1479
+ return {
1480
+ mutationCreateCards
1481
+ };
1482
+ }
1483
+ function getCardFromCache({
1484
+ cardId,
1485
+ queryClient
1486
+ }) {
1487
+ return queryClient.getQueryData(cardsQueryKeys.one({ cardId }));
1488
+ }
1489
+ function updateCardInCache({
1490
+ cardId,
1491
+ card,
1492
+ queryClient
1493
+ }) {
1494
+ queryClient.setQueryData(cardsQueryKeys.one({ cardId }), card);
1495
+ }
1496
+ function useGetCard({ cardId, enabled = true }) {
1497
+ const query = useQuery3({
1498
+ queryKey: cardsQueryKeys.one({ cardId }),
1499
+ queryFn: () => getCard({ cardId }),
1500
+ enabled: enabled && !!cardId
1501
+ });
1502
+ return query;
1503
+ }
1504
+
1505
+ // src/domains/sets/set.hooks.ts
1506
+ import { useQuery as useQuery4 } from "@tanstack/react-query";
1507
+
1508
+ // src/domains/sets/set.constants.ts
1509
+ var SETS_COLLECTION = "sets";
1510
+ var refsSetsFirestore = {
1511
+ allSets: SETS_COLLECTION,
1512
+ set: (id) => `${SETS_COLLECTION}/${id}`
1513
+ };
1514
+
1515
+ // src/domains/sets/services/get-set.service.ts
1516
+ async function _getSet({ setId }) {
1517
+ const response = await api.getDoc(refsSetsFirestore.set(setId));
1518
+ return response.data;
1519
+ }
1520
+ var getSet = withErrorHandler(_getSet, "getSet");
1521
+
1522
+ // src/domains/sets/set.hooks.ts
1523
+ var setsQueryKeys = {
1524
+ all: ["sets"],
1525
+ one: (params) => [...setsQueryKeys.all, params.setId]
1526
+ };
1527
+ var useSet = ({ setId, enabled }) => {
1528
+ return useQuery4({
1529
+ queryKey: setsQueryKeys.one({ setId }),
1530
+ queryFn: () => getSet({ setId }),
1531
+ enabled: setId !== void 0 && setId !== "" && enabled
1532
+ });
1533
+ };
1534
+ function getSetFromCache({
1535
+ setId,
1536
+ queryClient
1537
+ }) {
1538
+ if (!setId) return null;
1539
+ return queryClient.getQueryData(setsQueryKeys.one({ setId }));
1540
+ }
1541
+ function updateSetInCache({
1542
+ set,
1543
+ queryClient
1544
+ }) {
1545
+ const { id, ...setData } = set;
1546
+ queryClient.setQueryData(setsQueryKeys.one({ setId: id }), setData);
1547
+ }
1548
+ export {
1549
+ assignmentQueryKeys,
1550
+ cardsQueryKeys,
1551
+ getCardFromCache,
1552
+ getSetFromCache,
1553
+ scoreQueryKeys,
1554
+ setsQueryKeys,
1555
+ updateCardInCache,
1556
+ updateSetInCache,
1557
+ useAssignment,
1558
+ useCards,
1559
+ useClearScore,
1560
+ useCreateCard,
1561
+ useCreateCards,
1562
+ useCreateNotification,
1563
+ useGetCard,
1564
+ useScore,
1565
+ useSet,
1566
+ useSubmitAssignmentScore,
1567
+ useSubmitPracticeScore,
1568
+ useUpdateCardScore,
1569
+ useUpdateScore
1570
+ };
1571
+ //# sourceMappingURL=hooks.js.map