@speakableio/core 0.1.2 → 0.1.3

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.mjs CHANGED
@@ -47,13 +47,21 @@ var FirebaseAPI = class _FirebaseAPI {
47
47
  this.config = config;
48
48
  }
49
49
  get db() {
50
- if (!this.config) throw new Error("FirebaseAPI not initialized");
50
+ if (!this.config) throw new Error("Firebase API not initialized");
51
51
  return this.config.db;
52
52
  }
53
53
  get helpers() {
54
- if (!this.config) throw new Error("FirebaseAPI not initialized");
54
+ if (!this.config) throw new Error("Firebase API not initialized");
55
55
  return this.config.helpers;
56
56
  }
57
+ get functions() {
58
+ if (!this.config) throw new Error("Firebase API not initialized");
59
+ return this.config.functions;
60
+ }
61
+ httpsCallable(functionName) {
62
+ if (!this.config) throw new Error("Firebase API httpsCallable not initialized");
63
+ return this.config.httpsCallable(this.functions, functionName);
64
+ }
57
65
  accessQueryConstraints() {
58
66
  const { query, orderBy, limit, startAt, startAfter, endAt, endBefore } = this.helpers;
59
67
  return {
@@ -66,13 +74,27 @@ var FirebaseAPI = class _FirebaseAPI {
66
74
  endBefore
67
75
  };
68
76
  }
77
+ accessHelpers() {
78
+ const { doc, collection, writeBatch, serverTimestamp, setDoc } = this.helpers;
79
+ return {
80
+ doc: (path) => doc(this.db, path),
81
+ collection: (path) => collection(this.db, path),
82
+ writeBatch: () => writeBatch(this.db),
83
+ serverTimestamp,
84
+ setDoc
85
+ };
86
+ }
69
87
  async getDoc(path) {
70
88
  const { getDoc, doc } = this.helpers;
71
89
  const docRef = doc(this.db, path);
72
90
  const docSnap = await getDoc(docRef);
91
+ const data = docSnap.exists() ? {
92
+ id: docSnap.id,
93
+ ...docSnap.data()
94
+ } : null;
73
95
  return {
74
96
  id: docSnap.id,
75
- data: docSnap.exists() ? docSnap.data() : null
97
+ data
76
98
  };
77
99
  }
78
100
  async getDocs(path, ...queryConstraints) {
@@ -93,12 +115,15 @@ var FirebaseAPI = class _FirebaseAPI {
93
115
  const { addDoc, collection } = this.helpers;
94
116
  const collectionRef = collection(this.db, path);
95
117
  const docRef = await addDoc(collectionRef, data);
96
- return docRef.id;
118
+ return {
119
+ id: docRef.id,
120
+ ...data
121
+ };
97
122
  }
98
- async setDoc(path, data) {
123
+ async setDoc(path, data, options = {}) {
99
124
  const { setDoc, doc } = this.helpers;
100
125
  const docRef = doc(this.db, path);
101
- await setDoc(docRef, data);
126
+ await setDoc(docRef, data, options);
102
127
  }
103
128
  async updateDoc(path, data) {
104
129
  const { updateDoc, doc } = this.helpers;
@@ -120,6 +145,11 @@ var FirebaseAPI = class _FirebaseAPI {
120
145
  await Promise.all(operations.map((op) => op()));
121
146
  await batch.commit();
122
147
  }
148
+ writeBatch() {
149
+ const { writeBatch } = this.helpers;
150
+ const batch = writeBatch(this.db);
151
+ return batch;
152
+ }
123
153
  };
124
154
  var api = FirebaseAPI.getInstance();
125
155
 
@@ -136,10 +166,10 @@ var ANALYTICS_SUBCOLLECTION = "analytics";
136
166
  var SCORES_SUBCOLLECTION = "scores";
137
167
  var refsAssignmentFiresotre = {
138
168
  allAssignments: () => ASSIGNMENTS_COLLECTION,
139
- assignment: (id) => `${ASSIGNMENTS_COLLECTION}/${id}`,
140
- assignmentAllAnalytics: (id) => `${ASSIGNMENTS_COLLECTION}/${id}/${ANALYTICS_SUBCOLLECTION}`,
141
- assignmentAnalytics: (id, type) => `${ASSIGNMENTS_COLLECTION}/${id}/${ANALYTICS_SUBCOLLECTION}/${type}`,
142
- assignmentScores: (id, userId) => `${ASSIGNMENTS_COLLECTION}/${id}/${SCORES_SUBCOLLECTION}/${userId}`
169
+ assignment: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}`,
170
+ assignmentAllAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}`,
171
+ assignmentAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}/${params.type}`,
172
+ assignmentScores: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${SCORES_SUBCOLLECTION}/${params.userId}`
143
173
  };
144
174
 
145
175
  // src/domains/assignment/services/get-assignments-score.service.ts
@@ -150,21 +180,30 @@ var _getAssignmentScores = async ({
150
180
  currentUserId
151
181
  }) => {
152
182
  if (analyticType === "student" /* Student */) {
153
- const path = refsAssignmentFiresotre.assignmentScores(assignmentId, currentUserId);
183
+ const path = refsAssignmentFiresotre.assignmentScores({
184
+ id: assignmentId,
185
+ userId: currentUserId
186
+ });
154
187
  const response = await api.getDoc(path);
155
188
  return { scores: response.data, id: assignmentId };
156
189
  }
157
190
  if (analyticType === "student_summary" /* StudentSummary */ && studentId) {
158
- const path = refsAssignmentFiresotre.assignmentScores(assignmentId, studentId);
191
+ const path = refsAssignmentFiresotre.assignmentScores({
192
+ id: assignmentId,
193
+ userId: studentId
194
+ });
159
195
  const response = await api.getDoc(path);
160
196
  return { scores: response.data, id: assignmentId };
161
197
  }
162
198
  if (analyticType !== "all" /* All */ && ASSIGNMENT_ANALYTICS_TYPES.includes(analyticType)) {
163
- const ref = refsAssignmentFiresotre.assignmentAnalytics(assignmentId, analyticType);
199
+ const ref = refsAssignmentFiresotre.assignmentAnalytics({
200
+ id: assignmentId,
201
+ type: analyticType
202
+ });
164
203
  const docData = await api.getDoc(ref);
165
204
  return { scores: docData.data, id: assignmentId };
166
205
  } else if (analyticType === "all" /* All */) {
167
- const ref = refsAssignmentFiresotre.assignmentAllAnalytics(assignmentId);
206
+ const ref = refsAssignmentFiresotre.assignmentAllAnalytics({ id: assignmentId });
168
207
  const response = await api.getDocs(ref);
169
208
  const data = response.data.reduce((acc, curr) => {
170
209
  acc[curr.id] = curr;
@@ -230,7 +269,7 @@ var checkAssignmentAvailability = (scheduledTime) => {
230
269
 
231
270
  // src/domains/assignment/services/get-assignment.service.ts
232
271
  async function _getAssignment(params) {
233
- const path = refsAssignmentFiresotre.assignment(params.assignmentId);
272
+ const path = refsAssignmentFiresotre.assignment({ id: params.assignmentId });
234
273
  const response = await api.getDoc(path);
235
274
  if (!response.data) return null;
236
275
  const assignment = response.data;
@@ -263,7 +302,7 @@ var createAssignmentRepo = () => {
263
302
  };
264
303
  };
265
304
 
266
- // src/domains/assignment/assignment.hooks.ts
305
+ // src/domains/assignment/hooks/assignment.hooks.ts
267
306
  import { useQuery } from "@tanstack/react-query";
268
307
  var assignmentQueryKeys = {
269
308
  all: ["assignments"],
@@ -288,16 +327,609 @@ function useAssignment({
288
327
  });
289
328
  }
290
329
 
330
+ // src/domains/cards/card.hooks.ts
331
+ import { useMutation, useQueries } from "@tanstack/react-query";
332
+ import { useMemo } from "react";
333
+
334
+ // src/domains/cards/card.constants.ts
335
+ var FeedbackTypesCard = /* @__PURE__ */ ((FeedbackTypesCard2) => {
336
+ FeedbackTypesCard2["SuggestedResponse"] = "suggested_response";
337
+ FeedbackTypesCard2["Wida"] = "wida";
338
+ FeedbackTypesCard2["GrammarInsights"] = "grammar_insights";
339
+ FeedbackTypesCard2["Actfl"] = "actfl";
340
+ FeedbackTypesCard2["ProficiencyLevel"] = "proficiency_level";
341
+ return FeedbackTypesCard2;
342
+ })(FeedbackTypesCard || {});
343
+ var LeniencyCard = /* @__PURE__ */ ((LeniencyCard2) => {
344
+ LeniencyCard2["CONFIDENCE"] = "confidence";
345
+ LeniencyCard2["EASY"] = "easy";
346
+ LeniencyCard2["NORMAL"] = "normal";
347
+ LeniencyCard2["HARD"] = "hard";
348
+ return LeniencyCard2;
349
+ })(LeniencyCard || {});
350
+ var LENIENCY_OPTIONS = [
351
+ {
352
+ label: "Build Confidence - most lenient",
353
+ value: "confidence" /* CONFIDENCE */
354
+ },
355
+ {
356
+ label: "Very Lenient",
357
+ value: "easy" /* EASY */
358
+ },
359
+ {
360
+ label: "Normal",
361
+ value: "normal" /* NORMAL */
362
+ },
363
+ {
364
+ label: "No leniency - most strict",
365
+ value: "hard" /* HARD */
366
+ }
367
+ ];
368
+ var STUDENT_LEVELS_OPTIONS = [
369
+ {
370
+ label: "Beginner",
371
+ description: "Beginner Level: Just starting out. Can say a few basic words and phrases.",
372
+ value: "beginner"
373
+ },
374
+ {
375
+ label: "Elementary",
376
+ description: "Elementary Level: Can understand simple sentences and have very basic conversations.",
377
+ value: "elementary"
378
+ },
379
+ {
380
+ label: "Intermediate",
381
+ description: "Intermediate Level: Can talk about everyday topics and handle common situations.",
382
+ value: "intermediate"
383
+ },
384
+ {
385
+ label: "Advanced",
386
+ description: "Advanced Level: Can speak and understand with ease, and explain ideas clearly.",
387
+ value: "advanced"
388
+ },
389
+ {
390
+ label: "Fluent",
391
+ description: "Fluent Level: Speaks naturally and easily. Can use the language in work or school settings.",
392
+ value: "fluent"
393
+ },
394
+ {
395
+ label: "Native-like",
396
+ description: "Native-like Level: Understands and speaks like a native. Can discuss complex ideas accurately.",
397
+ value: "nativeLike"
398
+ }
399
+ ];
400
+ var BASE_RESPOND_FIELD_VALUES = {
401
+ title: "",
402
+ allowRetries: true,
403
+ respondTime: 180,
404
+ maxCharacters: 1e3
405
+ };
406
+ var BASE_REPEAT_FIELD_VALUES = {
407
+ repeat: 1
408
+ };
409
+ var BASE_MULTIPLE_CHOICE_FIELD_VALUES = {
410
+ MCQType: "single",
411
+ answer: ["A"],
412
+ choices: [
413
+ { option: "A", value: "Option A" },
414
+ { option: "B", value: "Option B" },
415
+ { option: "C", value: "Option C" }
416
+ ]
417
+ };
418
+ var VerificationCardStatus = /* @__PURE__ */ ((VerificationCardStatus2) => {
419
+ VerificationCardStatus2["VERIFIED"] = "VERIFIED";
420
+ VerificationCardStatus2["WARNING"] = "WARNING";
421
+ VerificationCardStatus2["NOT_RECOMMENDED"] = "NOT_RECOMMENDED";
422
+ VerificationCardStatus2["NOT_WORKING"] = "NOT_WORKING";
423
+ VerificationCardStatus2["NOT_CHECKED"] = "NOT_CHECKED";
424
+ return VerificationCardStatus2;
425
+ })(VerificationCardStatus || {});
426
+ var CARDS_COLLECTION = "flashcards";
427
+ var refsCardsFiresotre = {
428
+ allCards: CARDS_COLLECTION,
429
+ card: (id) => `${CARDS_COLLECTION}/${id}`
430
+ };
431
+
432
+ // src/domains/cards/services/get-card.service.ts
433
+ async function _getCard(params) {
434
+ const ref = refsCardsFiresotre.card(params.cardId);
435
+ const response = await api.getDoc(ref);
436
+ if (!response.data) return null;
437
+ return response.data;
438
+ }
439
+ var getCard = withErrorHandler(_getCard, "getCard");
440
+
441
+ // src/domains/cards/services/create-card.service.ts
442
+ import { v4 } from "uuid";
443
+
444
+ // src/domains/cards/card.model.ts
445
+ var CardActivityType = /* @__PURE__ */ ((CardActivityType2) => {
446
+ CardActivityType2["READ_REPEAT"] = "READ_REPEAT";
447
+ CardActivityType2["VIDEO"] = "VIDEO";
448
+ CardActivityType2["TEXT"] = "TEXT";
449
+ CardActivityType2["READ_RESPOND"] = "READ_RESPOND";
450
+ CardActivityType2["FREE_RESPONSE"] = "FREE_RESPONSE";
451
+ CardActivityType2["REPEAT"] = "REPEAT";
452
+ CardActivityType2["RESPOND"] = "RESPOND";
453
+ CardActivityType2["RESPOND_WRITE"] = "RESPOND_WRITE";
454
+ CardActivityType2["TEXT_TO_SPEECH"] = "TEXT_TO_SPEECH";
455
+ CardActivityType2["MULTIPLE_CHOICE"] = "MULTIPLE_CHOICE";
456
+ CardActivityType2["PODCAST"] = "PODCAST";
457
+ CardActivityType2["MEDIA_PAGE"] = "MEDIA_PAGE";
458
+ CardActivityType2["WRITE"] = "WRITE";
459
+ CardActivityType2["SHORT_ANSWER"] = "SHORT_ANSWER";
460
+ CardActivityType2["SHORT_STORY"] = "SHORT_STORY";
461
+ CardActivityType2["SPEAK"] = "SPEAK";
462
+ CardActivityType2["CONVERSATION"] = "CONVERSATION";
463
+ CardActivityType2["CONVERSATION_WRITE"] = "CONVERSATION_WRITE";
464
+ CardActivityType2["DIALOGUE"] = "DIALOGUE";
465
+ CardActivityType2["INSTRUCTION"] = "INSTRUCTION";
466
+ CardActivityType2["LISTEN"] = "LISTEN";
467
+ CardActivityType2["READ"] = "READ";
468
+ CardActivityType2["ANSWER"] = "ANSWER";
469
+ return CardActivityType2;
470
+ })(CardActivityType || {});
471
+ var RESPOND_CARD_ACTIVITY_TYPES = [
472
+ "READ_RESPOND" /* READ_RESPOND */,
473
+ "RESPOND" /* RESPOND */,
474
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
475
+ "FREE_RESPONSE" /* FREE_RESPONSE */
476
+ ];
477
+ var MULTIPLE_CHOICE_CARD_ACTIVITY_TYPES = ["MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */];
478
+ var REPEAT_CARD_ACTIVITY_TYPES = ["READ_REPEAT" /* READ_REPEAT */, "REPEAT" /* REPEAT */];
479
+ var RESPOND_WRITE_CARD_ACTIVITY_TYPES = [
480
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
481
+ "FREE_RESPONSE" /* FREE_RESPONSE */
482
+ ];
483
+ var RESPOND_AUDIO_CARD_ACTIVITY_TYPES = [
484
+ "RESPOND" /* RESPOND */,
485
+ "READ_RESPOND" /* READ_RESPOND */
486
+ ];
487
+ var ALLOWED_CARD_ACTIVITY_TYPES_FOR_SUMMARY = [
488
+ "REPEAT" /* REPEAT */,
489
+ "RESPOND" /* RESPOND */,
490
+ "READ_REPEAT" /* READ_REPEAT */,
491
+ "READ_RESPOND" /* READ_RESPOND */,
492
+ "FREE_RESPONSE" /* FREE_RESPONSE */,
493
+ "RESPOND_WRITE" /* RESPOND_WRITE */,
494
+ "MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */
495
+ ];
496
+
497
+ // src/utils/text-utils.ts
498
+ import sha1 from "js-sha1";
499
+ var purify = (word) => {
500
+ 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();
501
+ };
502
+ var cleanString = (words) => {
503
+ const splitWords = words?.split("+");
504
+ if (splitWords && splitWords.length === 1) {
505
+ const newWord = purify(words);
506
+ return newWord;
507
+ } else if (splitWords && splitWords.length > 1) {
508
+ const split = splitWords.map((w) => purify(w));
509
+ return split;
510
+ } else {
511
+ return "";
512
+ }
513
+ };
514
+ var getWordHash = (word, language) => {
515
+ const cleanedWord = cleanString(word);
516
+ const wordHash = sha1(`${language}-${cleanedWord}`);
517
+ return wordHash;
518
+ };
519
+
520
+ // src/domains/cards/services/get-card-verification-status.service.ts
521
+ var charactarLanguages = ["zh", "ja", "ko"];
522
+ var getVerificationStatus = async (target_text, language) => {
523
+ if (target_text?.length < 3 && !charactarLanguages.includes(language)) {
524
+ return "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
525
+ }
526
+ const hash = getWordHash(target_text, language);
527
+ const response = await api.getDoc(`checked-pronunciations/${hash}`);
528
+ try {
529
+ if (response.data) {
530
+ return processRecord(response.data);
531
+ } else {
532
+ return "NOT_CHECKED" /* NOT_CHECKED */;
533
+ }
534
+ } catch (e) {
535
+ return "NOT_CHECKED" /* NOT_CHECKED */;
536
+ }
537
+ };
538
+ var processRecord = (data) => {
539
+ const { pronunciations = 0, fails = 0 } = data;
540
+ const attempts = pronunciations + fails;
541
+ const successRate = attempts > 0 ? pronunciations / attempts * 100 : 0;
542
+ let newStatus = null;
543
+ if (attempts < 6) {
544
+ return "NOT_CHECKED" /* NOT_CHECKED */;
545
+ }
546
+ if (successRate > 25) {
547
+ newStatus = "VERIFIED" /* VERIFIED */;
548
+ } else if (successRate > 10) {
549
+ newStatus = "WARNING" /* WARNING */;
550
+ } else if (fails > 20 && successRate < 10 && pronunciations > 1) {
551
+ newStatus = "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
552
+ } else if (pronunciations === 0 && fails > 20) {
553
+ newStatus = "NOT_WORKING" /* NOT_WORKING */;
554
+ } else {
555
+ newStatus = "NOT_CHECKED" /* NOT_CHECKED */;
556
+ }
557
+ return newStatus;
558
+ };
559
+
560
+ // src/domains/cards/services/create-card.service.ts
561
+ async function _createCard({ data }) {
562
+ const response = await api.addDoc(refsCardsFiresotre.allCards, data);
563
+ return response;
564
+ }
565
+ var createCard = withErrorHandler(_createCard, "createCard");
566
+ async function _createCards({ cards }) {
567
+ const { writeBatch, doc } = api.accessHelpers();
568
+ const batch = writeBatch();
569
+ const cardsWithId = [];
570
+ for (const card of cards) {
571
+ const cardId = v4();
572
+ const ref = doc(refsCardsFiresotre.card(cardId));
573
+ const newCardObject = {
574
+ ...card,
575
+ id: cardId
576
+ };
577
+ if (card.type === "READ_REPEAT" /* READ_REPEAT */ && card.target_text && card.language) {
578
+ const verificationStatus = await getVerificationStatus(card.target_text, card.language);
579
+ newCardObject.verificationStatus = verificationStatus || null;
580
+ }
581
+ cardsWithId.push(newCardObject);
582
+ batch.set(ref, newCardObject);
583
+ }
584
+ await batch.commit();
585
+ return cardsWithId;
586
+ }
587
+ var createCards = withErrorHandler(_createCards, "createCards");
588
+
589
+ // src/domains/cards/card.hooks.ts
590
+ var cardsQueryKeys = {
591
+ all: ["cards"],
592
+ one: (params) => [...cardsQueryKeys.all, params.cardId]
593
+ };
594
+ function useCards({
595
+ cardIds,
596
+ enabled = true,
597
+ asObject
598
+ }) {
599
+ const queries = useQueries({
600
+ queries: cardIds.map((cardId) => ({
601
+ enabled: enabled && cardIds.length > 0,
602
+ queryKey: cardsQueryKeys.one({
603
+ cardId
604
+ }),
605
+ queryFn: () => getCard({ cardId })
606
+ }))
607
+ });
608
+ const cards = queries.map((query) => query.data).filter(Boolean);
609
+ const cardsObject = useMemo(() => {
610
+ if (!asObject) return null;
611
+ return cards.reduce((acc, card) => {
612
+ acc[card.id] = card;
613
+ return acc;
614
+ }, {});
615
+ }, [asObject, cards]);
616
+ return {
617
+ cards,
618
+ cardsObject,
619
+ cardsQueries: queries
620
+ };
621
+ }
622
+ function useCreateCard() {
623
+ const { queryClient } = useSpeakableApi();
624
+ const mutationCreateCard = useMutation({
625
+ mutationFn: createCard,
626
+ onSuccess: (cardCreated) => {
627
+ queryClient.invalidateQueries({ queryKey: cardsQueryKeys.one({ cardId: cardCreated.id }) });
628
+ }
629
+ });
630
+ return {
631
+ mutationCreateCard
632
+ };
633
+ }
634
+ function useCreateCards() {
635
+ const mutationCreateCards = useMutation({
636
+ mutationFn: createCards
637
+ });
638
+ return {
639
+ mutationCreateCards
640
+ };
641
+ }
642
+ function getCardFromCache({
643
+ cardId,
644
+ queryClient
645
+ }) {
646
+ return queryClient.getQueryData(cardsQueryKeys.one({ cardId }));
647
+ }
648
+ function updateCardInCache({
649
+ cardId,
650
+ card,
651
+ queryClient
652
+ }) {
653
+ queryClient.setQueryData(cardsQueryKeys.one({ cardId }), card);
654
+ }
655
+
656
+ // src/domains/cards/card.repo.ts
657
+ var createCardRepo = () => {
658
+ return {
659
+ createCard,
660
+ createCards,
661
+ getCard
662
+ };
663
+ };
664
+
665
+ // src/domains/sets/set.hooks.ts
666
+ import { useQuery as useQuery2 } from "@tanstack/react-query";
667
+
668
+ // src/domains/sets/set.constants.ts
669
+ var SETS_COLLECTION = "sets";
670
+ var refsSetsFirestore = {
671
+ allSets: SETS_COLLECTION,
672
+ set: (id) => `${SETS_COLLECTION}/${id}`
673
+ };
674
+
675
+ // src/domains/sets/services/get-set.service.ts
676
+ async function _getSet({ setId }) {
677
+ const response = await api.getDoc(refsSetsFirestore.set(setId));
678
+ return response.data;
679
+ }
680
+ var getSet = withErrorHandler(_getSet, "getSet");
681
+
682
+ // src/domains/sets/set.hooks.ts
683
+ var setsQueryKeys = {
684
+ all: ["sets"],
685
+ one: (params) => [...setsQueryKeys.all, params.setId]
686
+ };
687
+ var useSet = ({ setId, enabled }) => {
688
+ return useQuery2({
689
+ queryKey: setsQueryKeys.one({ setId }),
690
+ queryFn: () => getSet({ setId }),
691
+ enabled: setId !== void 0 && setId !== "" && enabled
692
+ });
693
+ };
694
+ function getSetFromCache({
695
+ setId,
696
+ queryClient
697
+ }) {
698
+ if (!setId) return null;
699
+ return queryClient.getQueryData(setsQueryKeys.one({ setId }));
700
+ }
701
+ function updateSetInCache({
702
+ set,
703
+ queryClient
704
+ }) {
705
+ const { id, ...setData } = set;
706
+ queryClient.setQueryData(setsQueryKeys.one({ setId: id }), setData);
707
+ }
708
+
709
+ // src/domains/sets/set.repo.ts
710
+ var createSetRepo = () => {
711
+ return {
712
+ getSet
713
+ };
714
+ };
715
+
716
+ // src/domains/notification/notification.constants.ts
717
+ var SPEAKABLE_NOTIFICATIONS = {
718
+ NEW_ASSIGNMENT: "new_assignment",
719
+ ASSESSMENT_SUBMITTED: "assessment_submitted",
720
+ ASSESSMENT_SCORED: "assessment_scored",
721
+ NEW_COMMENT: "NEW_COMMENT"
722
+ };
723
+ var SpeakableNotificationTypes = {
724
+ NEW_ASSIGNMENT: "NEW_ASSIGNMENT",
725
+ FEEDBACK_FROM_TEACHER: "FEEDBACK_FROM_TEACHER",
726
+ MESSAGE_FROM_STUDENT: "MESSAGE_FROM_STUDENT",
727
+ PHRASE_MARKED_CORRECT: "PHRASE_MARKED_CORRECT",
728
+ STUDENT_PROGRESS: "STUDENT_PROGRESS",
729
+ PLAYLIST_FOLLOWERS: "PLAYLIST_FOLLOWERS",
730
+ PLAYLIST_PLAYS: "PLAYLIST_PLAYS",
731
+ // New notifications
732
+ ASSESSMENT_SUBMITTED: "ASSESSMENT_SUBMITTED",
733
+ // Notification FOR TEACHER when student submits assessment
734
+ ASSESSMENT_SCORED: "ASSESSMENT_SCORED",
735
+ // Notification FOR STUDENT when teacher scores assessment
736
+ // Comment
737
+ NEW_COMMENT: "NEW_COMMENT"
738
+ };
739
+
740
+ // src/domains/notification/services/create-notification.service.ts
741
+ import dayjs2 from "dayjs";
742
+
743
+ // src/constants/web.constants.ts
744
+ var WEB_BASE_URL = "https://app.speakable.io";
745
+
746
+ // src/lib/firebase/firebase-functions.ts
747
+ var SpeakableFirebaseFunctions = {
748
+ updateAssignmentGradebookStatus: api.httpsCallable("updateAssignmentGradebookStatus"),
749
+ onSetOpened: api.httpsCallable("onSetOpened"),
750
+ updateAlgoliaIndex: api.httpsCallable("updateAlgoliaIndex"),
751
+ submitAssignmentToGoogleClassroomV2: api.httpsCallable("submitAssignmentToGoogleClassroomV2"),
752
+ submitLTIAssignmentScore: api.httpsCallable("submitLTIAssignmentScore"),
753
+ submitAssignmentV2: api.httpsCallable("submitLTIAssignmentScoreV2"),
754
+ submitAssessment: api.httpsCallable("submitAssessment"),
755
+ sendAssessmentScoredEmail: api.httpsCallable("sendAssessmentScoredEmail"),
756
+ createNotification: api.httpsCallable("createNotificationV2")
757
+ };
758
+
759
+ // src/domains/notification/services/send-notification.service.ts
760
+ var _sendNotification = async (sendTo, notification) => {
761
+ const results = await SpeakableFirebaseFunctions.createNotification({
762
+ sendTo,
763
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
764
+ notification
765
+ });
766
+ return results;
767
+ };
768
+ var sendNotification = withErrorHandler(_sendNotification, "sendNotification");
769
+
770
+ // src/domains/notification/services/create-notification.service.ts
771
+ var createNotification = async ({
772
+ data,
773
+ type,
774
+ userId,
775
+ profile
776
+ }) => {
777
+ let result;
778
+ switch (type) {
779
+ case SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT:
780
+ result = await handleAssignNotifPromise({ data, profile });
781
+ break;
782
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED:
783
+ result = await createAssessmentSubmissionNotification({
784
+ data,
785
+ profile,
786
+ userId
787
+ });
788
+ break;
789
+ case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED:
790
+ result = await createAssessmentScoredNotification({
791
+ data,
792
+ profile
793
+ });
794
+ break;
795
+ default:
796
+ result = null;
797
+ break;
798
+ }
799
+ return result;
800
+ };
801
+ var handleAssignNotifPromise = async ({
802
+ data: assignments,
803
+ profile
804
+ }) => {
805
+ if (!assignments.length) return;
806
+ try {
807
+ const notifsPromises = assignments.map(async (assignment) => {
808
+ const {
809
+ section,
810
+ section: { members },
811
+ ...rest
812
+ } = assignment;
813
+ if (!section || !members) throw new Error("Invalid assignment data");
814
+ const data = { section, sendTo: members, assignment: { ...rest } };
815
+ return createNewAssignmentNotification({ data, profile });
816
+ });
817
+ await Promise.all(notifsPromises);
818
+ return {
819
+ success: true,
820
+ message: "Assignment notifications sent successfully"
821
+ };
822
+ } catch (error) {
823
+ console.error("Error in handleAssignNotifPromise:", error);
824
+ throw error;
825
+ }
826
+ };
827
+ var createNewAssignmentNotification = async ({
828
+ data,
829
+ profile
830
+ }) => {
831
+ const { assignment, sendTo } = data;
832
+ const teacherName = profile.displayName || "Your teacher";
833
+ const dueDate = assignment.dueDateTimestamp ? dayjs2(assignment.dueDateTimestamp.toDate()).format("MMM Do") : null;
834
+ const results = await sendNotification(sendTo, {
835
+ courseId: assignment.courseId,
836
+ type: SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT,
837
+ senderName: teacherName,
838
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
839
+ messagePreview: `A new assignment "${assignment.name}" is now available. ${dueDate ? `Due ${dueDate}` : ""}`,
840
+ title: "New Assignment Available!",
841
+ imageUrl: profile.image?.url
842
+ });
843
+ return results;
844
+ };
845
+ var createAssessmentSubmissionNotification = async ({
846
+ data: assignment,
847
+ profile,
848
+ userId
849
+ }) => {
850
+ const studentName = profile.displayName || "Your student";
851
+ const results = await sendNotification(assignment.owners, {
852
+ courseId: assignment.courseId,
853
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED,
854
+ link: `${WEB_BASE_URL}/a/${assignment.id}?studentId=${userId}`,
855
+ title: `Assessment Submitted!`,
856
+ senderName: studentName,
857
+ messagePreview: `${studentName} has submitted the assessment "${assignment.name}"`,
858
+ imageUrl: profile.image?.url
859
+ });
860
+ return results;
861
+ };
862
+ var createAssessmentScoredNotification = async ({
863
+ data,
864
+ profile
865
+ }) => {
866
+ const { assignment, sendTo } = data;
867
+ const teacherName = profile.displayName || "Your teacher";
868
+ const title = `${assignment.isAssessment ? "Assessment" : "Assignment"} Reviewed!`;
869
+ const messagePreview = `Your ${assignment.isAssessment ? "assessment" : "assignment"} has been reviewed by your teacher. Click to view the feedback.`;
870
+ const results = await sendNotification(sendTo, {
871
+ courseId: assignment.courseId,
872
+ type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED,
873
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
874
+ title,
875
+ messagePreview,
876
+ imageUrl: profile.image?.url,
877
+ senderName: teacherName
878
+ });
879
+ await SpeakableFirebaseFunctions.sendAssessmentScoredEmail({
880
+ assessmentTitle: assignment.name,
881
+ link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
882
+ senderImage: profile.image?.url || "",
883
+ studentId: sendTo[0],
884
+ teacherName: profile.displayName
885
+ });
886
+ return results;
887
+ };
888
+
889
+ // src/domains/notification/hooks/notification.hooks.ts
890
+ var notificationQueryKeys = {
891
+ all: ["notifications"],
892
+ byId: (id) => [...notificationQueryKeys.all, id]
893
+ };
894
+ var useCreateNotification = () => {
895
+ const { user, queryClient } = useSpeakableApi();
896
+ const handleCreateNotifications = async (type, data) => {
897
+ const result = await createNotification({
898
+ type,
899
+ userId: user?.auth.uid ?? "",
900
+ profile: user?.profile ?? {},
901
+ data
902
+ });
903
+ queryClient.invalidateQueries({
904
+ queryKey: notificationQueryKeys.byId(user?.auth.uid ?? "")
905
+ });
906
+ return result;
907
+ };
908
+ return {
909
+ createNotification: handleCreateNotifications
910
+ };
911
+ };
912
+
291
913
  // src/lib/create-firebase-client.ts
292
- async function createFsClient(db, platform) {
914
+ import { httpsCallable as webHttpsCallable } from "firebase/functions";
915
+ import { httpsCallable as nativeHttpsCallable } from "@react-native-firebase/functions";
916
+ async function createFsClient({
917
+ db,
918
+ platform,
919
+ functions
920
+ }) {
293
921
  const dbAsFirestore = db;
294
922
  const helpers = platform === "web" ? await import("firebase/firestore") : await import("@react-native-firebase/firestore");
923
+ const httpsCallable = platform === "web" ? webHttpsCallable : nativeHttpsCallable;
295
924
  api.initialize({
296
925
  db: dbAsFirestore,
297
- helpers
926
+ helpers,
927
+ functions,
928
+ httpsCallable
298
929
  });
299
930
  return {
300
- assignmentRepo: createAssignmentRepo()
931
+ assignmentRepo: createAssignmentRepo(),
932
+ cardRepo: createCardRepo()
301
933
  };
302
934
  }
303
935
 
@@ -308,21 +940,33 @@ function SpeakableProvider({
308
940
  db,
309
941
  platform,
310
942
  children,
311
- queryClient
943
+ queryClient,
944
+ user,
945
+ permissions,
946
+ firebaseFunctions,
947
+ localStorage
312
948
  }) {
313
949
  const [speakableApi, setSpeakableApi] = useState(null);
314
950
  useEffect(() => {
315
- createFsClient(db, platform).then((repos) => {
951
+ createFsClient({
952
+ db,
953
+ platform,
954
+ functions: firebaseFunctions
955
+ }).then((repos) => {
316
956
  setSpeakableApi(repos);
317
957
  });
318
- }, [db, platform]);
958
+ }, [db, firebaseFunctions, platform]);
319
959
  if (!speakableApi) return null;
320
960
  return /* @__PURE__ */ jsx(
321
961
  FsCtx.Provider,
322
962
  {
323
963
  value: {
324
964
  speakableApi,
325
- queryClient
965
+ queryClient,
966
+ user,
967
+ permissions,
968
+ firebaseFunctions,
969
+ localStorage
326
970
  },
327
971
  children
328
972
  }
@@ -334,11 +978,43 @@ function useSpeakableApi() {
334
978
  return ctx;
335
979
  }
336
980
  export {
981
+ ALLOWED_CARD_ACTIVITY_TYPES_FOR_SUMMARY,
982
+ BASE_MULTIPLE_CHOICE_FIELD_VALUES,
983
+ BASE_REPEAT_FIELD_VALUES,
984
+ BASE_RESPOND_FIELD_VALUES,
985
+ CardActivityType,
986
+ FeedbackTypesCard,
337
987
  FsCtx,
988
+ LENIENCY_OPTIONS,
989
+ LeniencyCard,
990
+ MULTIPLE_CHOICE_CARD_ACTIVITY_TYPES,
991
+ REPEAT_CARD_ACTIVITY_TYPES,
992
+ RESPOND_AUDIO_CARD_ACTIVITY_TYPES,
993
+ RESPOND_CARD_ACTIVITY_TYPES,
994
+ RESPOND_WRITE_CARD_ACTIVITY_TYPES,
995
+ SPEAKABLE_NOTIFICATIONS,
996
+ STUDENT_LEVELS_OPTIONS,
997
+ SpeakableNotificationTypes,
338
998
  SpeakableProvider,
999
+ VerificationCardStatus,
339
1000
  assignmentQueryKeys,
1001
+ cardsQueryKeys,
340
1002
  createAssignmentRepo,
1003
+ createCardRepo,
341
1004
  createFsClient,
1005
+ createSetRepo,
1006
+ getCardFromCache,
1007
+ getSetFromCache,
1008
+ refsCardsFiresotre,
1009
+ refsSetsFirestore,
1010
+ setsQueryKeys,
1011
+ updateCardInCache,
1012
+ updateSetInCache,
342
1013
  useAssignment,
1014
+ useCards,
1015
+ useCreateCard,
1016
+ useCreateCards,
1017
+ useCreateNotification,
1018
+ useSet,
343
1019
  useSpeakableApi
344
1020
  };