@magmamath/students-features 0.6.7-rc.2 → 0.6.7-rc.4

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.
Files changed (173) hide show
  1. package/dist/commonjs/features/endOfAssignment/components/Modal/Modal.js +0 -1
  2. package/dist/commonjs/features/endOfAssignment/components/Modal/Modal.js.map +1 -1
  3. package/dist/commonjs/features/gifCelebrations/GifCelebrations.js +62 -0
  4. package/dist/commonjs/features/gifCelebrations/GifCelebrations.js.map +1 -0
  5. package/dist/commonjs/features/gifCelebrations/assets/confetti.webp +0 -0
  6. package/dist/commonjs/features/gifCelebrations/assets/gifFallback.gif +0 -0
  7. package/dist/commonjs/features/gifCelebrations/components/Backdrop.js +44 -0
  8. package/dist/commonjs/features/gifCelebrations/components/Backdrop.js.map +1 -0
  9. package/dist/commonjs/features/gifCelebrations/components/ContentContainer.js +46 -0
  10. package/dist/commonjs/features/gifCelebrations/components/ContentContainer.js.map +1 -0
  11. package/dist/commonjs/features/gifCelebrations/components/Gif.js +90 -0
  12. package/dist/commonjs/features/gifCelebrations/components/Gif.js.map +1 -0
  13. package/dist/commonjs/features/gifCelebrations/components/Progress.js +96 -0
  14. package/dist/commonjs/features/gifCelebrations/components/Progress.js.map +1 -0
  15. package/dist/commonjs/features/gifCelebrations/constants.js +34 -0
  16. package/dist/commonjs/features/gifCelebrations/constants.js.map +1 -0
  17. package/dist/commonjs/features/gifCelebrations/helpers.js +114 -0
  18. package/dist/commonjs/features/gifCelebrations/helpers.js.map +1 -0
  19. package/dist/commonjs/features/gifCelebrations/index.js +20 -0
  20. package/dist/commonjs/features/gifCelebrations/index.js.map +1 -0
  21. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.cache.js +30 -0
  22. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.cache.js.map +1 -0
  23. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.model.js +138 -0
  24. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.model.js.map +1 -0
  25. package/dist/commonjs/features/gifCelebrations/model/GifCelebrationsApi.js +27 -0
  26. package/dist/commonjs/features/gifCelebrations/model/GifCelebrationsApi.js.map +1 -0
  27. package/dist/commonjs/features/gifCelebrations/types/model.types.js +6 -0
  28. package/dist/commonjs/features/gifCelebrations/types/model.types.js.map +1 -0
  29. package/dist/commonjs/index.js +12 -0
  30. package/dist/commonjs/index.js.map +1 -1
  31. package/dist/commonjs/lib/effector/createControllerEffect.js.map +1 -1
  32. package/dist/commonjs/lib/helpers/helpers.js.map +1 -1
  33. package/dist/commonjs/react-app-env.d.js +2 -0
  34. package/dist/commonjs/react-app-env.d.js.map +1 -0
  35. package/dist/commonjs/shared/translation/localization/en.json +5 -0
  36. package/dist/commonjs/shared/translation/localization/gb.json +5 -0
  37. package/dist/commonjs/shared/translation/localization/sw.json +5 -0
  38. package/dist/commonjs/types/common.types.js +2 -0
  39. package/dist/commonjs/types/common.types.js.map +1 -0
  40. package/dist/module/features/endOfAssignment/components/Modal/Modal.js +1 -2
  41. package/dist/module/features/endOfAssignment/components/Modal/Modal.js.map +1 -1
  42. package/dist/module/features/gifCelebrations/GifCelebrations.js +55 -0
  43. package/dist/module/features/gifCelebrations/GifCelebrations.js.map +1 -0
  44. package/dist/module/features/gifCelebrations/assets/confetti.webp +0 -0
  45. package/dist/module/features/gifCelebrations/assets/gifFallback.gif +0 -0
  46. package/dist/module/features/gifCelebrations/components/Backdrop.js +36 -0
  47. package/dist/module/features/gifCelebrations/components/Backdrop.js.map +1 -0
  48. package/dist/module/features/gifCelebrations/components/ContentContainer.js +38 -0
  49. package/dist/module/features/gifCelebrations/components/ContentContainer.js.map +1 -0
  50. package/dist/module/features/gifCelebrations/components/Gif.js +82 -0
  51. package/dist/module/features/gifCelebrations/components/Gif.js.map +1 -0
  52. package/dist/module/features/gifCelebrations/components/Progress.js +88 -0
  53. package/dist/module/features/gifCelebrations/components/Progress.js.map +1 -0
  54. package/dist/module/features/gifCelebrations/constants.js +30 -0
  55. package/dist/module/features/gifCelebrations/constants.js.map +1 -0
  56. package/dist/module/features/gifCelebrations/helpers.js +103 -0
  57. package/dist/module/features/gifCelebrations/helpers.js.map +1 -0
  58. package/dist/module/features/gifCelebrations/index.js +5 -0
  59. package/dist/module/features/gifCelebrations/index.js.map +1 -0
  60. package/dist/module/features/gifCelebrations/model/GifCelebrations.cache.js +25 -0
  61. package/dist/module/features/gifCelebrations/model/GifCelebrations.cache.js.map +1 -0
  62. package/dist/module/features/gifCelebrations/model/GifCelebrations.model.js +133 -0
  63. package/dist/module/features/gifCelebrations/model/GifCelebrations.model.js.map +1 -0
  64. package/dist/module/features/gifCelebrations/model/GifCelebrationsApi.js +22 -0
  65. package/dist/module/features/gifCelebrations/model/GifCelebrationsApi.js.map +1 -0
  66. package/dist/module/features/gifCelebrations/types/model.types.js +4 -0
  67. package/dist/module/features/gifCelebrations/types/model.types.js.map +1 -0
  68. package/dist/module/index.js +1 -0
  69. package/dist/module/index.js.map +1 -1
  70. package/dist/module/lib/effector/createControllerEffect.js.map +1 -1
  71. package/dist/module/react-app-env.d.js +2 -0
  72. package/dist/module/react-app-env.d.js.map +1 -0
  73. package/dist/module/shared/translation/localization/en.json +5 -0
  74. package/dist/module/shared/translation/localization/gb.json +5 -0
  75. package/dist/module/shared/translation/localization/sw.json +5 -0
  76. package/dist/module/types/common.types.js +2 -0
  77. package/dist/module/types/common.types.js.map +1 -0
  78. package/dist/typescript/commonjs/features/endOfAssignment/components/Modal/Modal.d.ts.map +1 -1
  79. package/dist/typescript/commonjs/features/gifCelebrations/GifCelebrations.d.ts +10 -0
  80. package/dist/typescript/commonjs/features/gifCelebrations/GifCelebrations.d.ts.map +1 -0
  81. package/dist/typescript/commonjs/features/gifCelebrations/components/Backdrop.d.ts +10 -0
  82. package/dist/typescript/commonjs/features/gifCelebrations/components/Backdrop.d.ts.map +1 -0
  83. package/dist/typescript/commonjs/features/gifCelebrations/components/ContentContainer.d.ts +10 -0
  84. package/dist/typescript/commonjs/features/gifCelebrations/components/ContentContainer.d.ts.map +1 -0
  85. package/dist/typescript/commonjs/features/gifCelebrations/components/Gif.d.ts +10 -0
  86. package/dist/typescript/commonjs/features/gifCelebrations/components/Gif.d.ts.map +1 -0
  87. package/dist/typescript/commonjs/features/gifCelebrations/components/Progress.d.ts +10 -0
  88. package/dist/typescript/commonjs/features/gifCelebrations/components/Progress.d.ts.map +1 -0
  89. package/dist/typescript/commonjs/features/gifCelebrations/constants.d.ts +26 -0
  90. package/dist/typescript/commonjs/features/gifCelebrations/constants.d.ts.map +1 -0
  91. package/dist/typescript/commonjs/features/gifCelebrations/helpers.d.ts +29 -0
  92. package/dist/typescript/commonjs/features/gifCelebrations/helpers.d.ts.map +1 -0
  93. package/dist/typescript/commonjs/features/gifCelebrations/index.d.ts +3 -0
  94. package/dist/typescript/commonjs/features/gifCelebrations/index.d.ts.map +1 -0
  95. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrations.cache.d.ts +18 -0
  96. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrations.cache.d.ts.map +1 -0
  97. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrations.model.d.ts +25 -0
  98. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrations.model.d.ts.map +1 -0
  99. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrationsApi.d.ts +7 -0
  100. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrationsApi.d.ts.map +1 -0
  101. package/dist/typescript/commonjs/features/gifCelebrations/types/model.types.d.ts +45 -0
  102. package/dist/typescript/commonjs/features/gifCelebrations/types/model.types.d.ts.map +1 -0
  103. package/dist/typescript/commonjs/index.d.ts +1 -0
  104. package/dist/typescript/commonjs/index.d.ts.map +1 -1
  105. package/dist/typescript/commonjs/lib/effector/createControllerEffect.d.ts +1 -1
  106. package/dist/typescript/commonjs/lib/effector/createControllerEffect.d.ts.map +1 -1
  107. package/dist/typescript/commonjs/lib/helpers/helpers.d.ts.map +1 -1
  108. package/dist/typescript/commonjs/shared/translation/constants.d.ts +15 -0
  109. package/dist/typescript/commonjs/shared/translation/constants.d.ts.map +1 -1
  110. package/dist/typescript/commonjs/shared/translation/model.d.ts +10 -0
  111. package/dist/typescript/commonjs/shared/translation/model.d.ts.map +1 -1
  112. package/dist/typescript/commonjs/types/common.types.d.ts +5 -0
  113. package/dist/typescript/commonjs/types/common.types.d.ts.map +1 -0
  114. package/dist/typescript/module/features/endOfAssignment/components/Modal/Modal.d.ts.map +1 -1
  115. package/dist/typescript/module/features/gifCelebrations/GifCelebrations.d.ts +10 -0
  116. package/dist/typescript/module/features/gifCelebrations/GifCelebrations.d.ts.map +1 -0
  117. package/dist/typescript/module/features/gifCelebrations/components/Backdrop.d.ts +10 -0
  118. package/dist/typescript/module/features/gifCelebrations/components/Backdrop.d.ts.map +1 -0
  119. package/dist/typescript/module/features/gifCelebrations/components/ContentContainer.d.ts +10 -0
  120. package/dist/typescript/module/features/gifCelebrations/components/ContentContainer.d.ts.map +1 -0
  121. package/dist/typescript/module/features/gifCelebrations/components/Gif.d.ts +10 -0
  122. package/dist/typescript/module/features/gifCelebrations/components/Gif.d.ts.map +1 -0
  123. package/dist/typescript/module/features/gifCelebrations/components/Progress.d.ts +10 -0
  124. package/dist/typescript/module/features/gifCelebrations/components/Progress.d.ts.map +1 -0
  125. package/dist/typescript/module/features/gifCelebrations/constants.d.ts +26 -0
  126. package/dist/typescript/module/features/gifCelebrations/constants.d.ts.map +1 -0
  127. package/dist/typescript/module/features/gifCelebrations/helpers.d.ts +29 -0
  128. package/dist/typescript/module/features/gifCelebrations/helpers.d.ts.map +1 -0
  129. package/dist/typescript/module/features/gifCelebrations/index.d.ts +3 -0
  130. package/dist/typescript/module/features/gifCelebrations/index.d.ts.map +1 -0
  131. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrations.cache.d.ts +18 -0
  132. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrations.cache.d.ts.map +1 -0
  133. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrations.model.d.ts +25 -0
  134. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrations.model.d.ts.map +1 -0
  135. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrationsApi.d.ts +7 -0
  136. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrationsApi.d.ts.map +1 -0
  137. package/dist/typescript/module/features/gifCelebrations/types/model.types.d.ts +45 -0
  138. package/dist/typescript/module/features/gifCelebrations/types/model.types.d.ts.map +1 -0
  139. package/dist/typescript/module/index.d.ts +1 -0
  140. package/dist/typescript/module/index.d.ts.map +1 -1
  141. package/dist/typescript/module/lib/effector/createControllerEffect.d.ts +1 -1
  142. package/dist/typescript/module/lib/effector/createControllerEffect.d.ts.map +1 -1
  143. package/dist/typescript/module/lib/helpers/helpers.d.ts.map +1 -1
  144. package/dist/typescript/module/shared/translation/constants.d.ts +15 -0
  145. package/dist/typescript/module/shared/translation/constants.d.ts.map +1 -1
  146. package/dist/typescript/module/shared/translation/model.d.ts +10 -0
  147. package/dist/typescript/module/shared/translation/model.d.ts.map +1 -1
  148. package/dist/typescript/module/types/common.types.d.ts +5 -0
  149. package/dist/typescript/module/types/common.types.d.ts.map +1 -0
  150. package/package.json +3 -5
  151. package/src/features/endOfAssignment/components/Modal/Modal.tsx +1 -2
  152. package/src/features/gifCelebrations/GifCelebrations.tsx +75 -0
  153. package/src/features/gifCelebrations/assets/confetti.webp +0 -0
  154. package/src/features/gifCelebrations/assets/gifFallback.gif +0 -0
  155. package/src/features/gifCelebrations/components/Backdrop.tsx +41 -0
  156. package/src/features/gifCelebrations/components/ContentContainer.tsx +44 -0
  157. package/src/features/gifCelebrations/components/Gif.tsx +88 -0
  158. package/src/features/gifCelebrations/components/Progress.tsx +106 -0
  159. package/src/features/gifCelebrations/constants.ts +32 -0
  160. package/src/features/gifCelebrations/helpers.ts +144 -0
  161. package/src/features/gifCelebrations/index.ts +2 -0
  162. package/src/features/gifCelebrations/model/GifCelebrations.cache.ts +41 -0
  163. package/src/features/gifCelebrations/model/GifCelebrations.model.ts +177 -0
  164. package/src/features/gifCelebrations/model/GifCelebrationsApi.ts +26 -0
  165. package/src/features/gifCelebrations/types/model.types.ts +53 -0
  166. package/src/index.ts +1 -0
  167. package/src/lib/effector/createControllerEffect.ts +41 -41
  168. package/src/lib/helpers/helpers.ts +1 -1
  169. package/src/react-app-env.d.ts +6 -0
  170. package/src/shared/translation/localization/en.json +5 -0
  171. package/src/shared/translation/localization/gb.json +5 -0
  172. package/src/shared/translation/localization/sw.json +5 -0
  173. package/src/types/common.types.ts +4 -0
@@ -0,0 +1,144 @@
1
+ import { TranslationKeys } from 'src/shared/translation/types'
2
+ import {
3
+ CustomGetCachePathMethod,
4
+ GifsGroups,
5
+ GifsStructure,
6
+ GroupNumber,
7
+ GroupNumberString,
8
+ ProblemsGifsTable,
9
+ } from './types/model.types'
10
+ import { MIN_WIDTH_FOR_ONE_DIGIT, MIN_WIDTH_FOR_TWO_DIGITS } from './constants'
11
+ import { Image } from 'react-native'
12
+
13
+ /**
14
+ * Number of problems When we show gif
15
+ * <6 Never
16
+ * 7-12 50% completion
17
+ * 13-18 33%, 66% completion
18
+ * 19-24 25%, 50%, 75 completion
19
+ * 25-30 20%, 40%, 60%, 80% completion
20
+ * .>30 15%, 30%, 45%, 60%, 75% completion
21
+ *
22
+ * We are also going to have different buckets with different gifs. The logic is as follows:
23
+ * Group 1: 15%, 20%, 25%, 30% - Early celebrations
24
+ * Group 2: 33%, 40%, 45%, 50% - Mid celebrations
25
+ * Group 3: 60%, 66%, 75%, 80% - Party
26
+ * or
27
+ * Group 1: <=30% - Early celebrations
28
+ * Group 2: >30% <60% - Mid celebrations
29
+ * Group 3: >=60% - Party
30
+ */
31
+ export const generateProblemsGifsTable = (
32
+ problemsAmount: number,
33
+ resolvedProblems: number,
34
+ gifs: GifsStructure,
35
+ ): ProblemsGifsTable => {
36
+ const percentageMilestones = getPercentageMilestones(problemsAmount)
37
+
38
+ const result: ProblemsGifsTable = {}
39
+
40
+ for (const percentage of percentageMilestones) {
41
+ const problemNumber = Math.ceil(problemsAmount * (percentage / 100))
42
+
43
+ if (problemNumber <= resolvedProblems) continue
44
+
45
+ const groupKey = findGroupForPercentage(percentage)
46
+ const gifsInGroup = gifs[groupKey]
47
+
48
+ if (!gifsInGroup || gifsInGroup.length === 0) continue
49
+
50
+ const unshownGif = gifsInGroup.find((gif) => !gif.isShown)
51
+
52
+ const selectedGif = unshownGif || gifsInGroup[0]
53
+
54
+ if (selectedGif) {
55
+ result[problemNumber] = {
56
+ url: selectedGif.url,
57
+ group: groupKey,
58
+ }
59
+ }
60
+ }
61
+
62
+ return result
63
+ }
64
+ const getPercentageMilestones = (problemsAmount: number): number[] => {
65
+ if (problemsAmount <= 12) return [50]
66
+
67
+ if (problemsAmount <= 18) return [33, 66]
68
+
69
+ if (problemsAmount <= 24) return [25, 50, 75]
70
+
71
+ if (problemsAmount <= 30) return [20, 40, 60, 80]
72
+
73
+ return [15, 30, 45, 60, 75]
74
+ }
75
+
76
+ export const generateGifsStructure = (gifs: GifsGroups): GifsStructure =>
77
+ Object.fromEntries(
78
+ Object.entries(gifs).map(([key, value]) => [
79
+ key,
80
+ value.map((url) => ({
81
+ url,
82
+ isShown: false,
83
+ group: key as GroupNumberString,
84
+ })),
85
+ ]),
86
+ )
87
+
88
+ export const getGifLabel = (relatedGroup: GroupNumber): TranslationKeys => {
89
+ if (relatedGroup === 2) {
90
+ return 'gifCelebrations.onTheWay'
91
+ }
92
+ if (relatedGroup === 3) {
93
+ return 'gifCelebrations.almostThere'
94
+ }
95
+
96
+ return 'gifCelebrations.greatStart'
97
+ }
98
+
99
+ export const getProgressLineWidth = (completedPercent: number) => {
100
+ if (completedPercent <= 9) {
101
+ return MIN_WIDTH_FOR_ONE_DIGIT
102
+ }
103
+ if (completedPercent <= 15) {
104
+ return MIN_WIDTH_FOR_TWO_DIGITS
105
+ }
106
+ return completedPercent
107
+ }
108
+
109
+ export const findGroupForPercentage = (percentage: number) => {
110
+ if (percentage <= 30) {
111
+ return 'group1'
112
+ }
113
+ if (percentage < 60) {
114
+ return 'group2'
115
+ }
116
+ return 'group3'
117
+ }
118
+
119
+ export const getImagePath = async (url: string, cacheMethod?: CustomGetCachePathMethod) => {
120
+ if (cacheMethod) {
121
+ const path = await cacheMethod(url)
122
+ if (path) return path
123
+ } else {
124
+ // @ts-ignore
125
+ const isImagesStored = await Image.queryCache([url]).then((images) => !!images[url])
126
+ if (isImagesStored) return url
127
+ }
128
+
129
+ throw new Error('Image not found in cache')
130
+ }
131
+
132
+ export const constrainDimension = (value: number, min: number, max: number) =>
133
+ Math.max(min, Math.min(max, value))
134
+
135
+ export const shouldRefreshGifs = (timestamp: string | null | undefined): boolean => {
136
+ if (!timestamp) return true
137
+
138
+ const REFRESH_INTERVAL_HOURS = 24
139
+ const lastFetchedDate = new Date(timestamp)
140
+ const currentDate = new Date()
141
+ const diffInHours = (currentDate.getTime() - lastFetchedDate.getTime()) / (1000 * 60 * 60)
142
+
143
+ return diffInHours > REFRESH_INTERVAL_HOURS
144
+ }
@@ -0,0 +1,2 @@
1
+ export { GifCelebrations } from './GifCelebrations'
2
+ export { GifCelebrationsModel } from './model/GifCelebrations.model'
@@ -0,0 +1,41 @@
1
+ import { createEffect, createEvent, createStore } from 'effector'
2
+ import {
3
+ GifsGroups,
4
+ GifsStructure,
5
+ CustomGetCachePathMethod,
6
+ CustomPrefetchMethod,
7
+ } from '../types/model.types'
8
+ import { generateGifsStructure } from '../helpers'
9
+ import { Image } from 'react-native'
10
+
11
+ type GifCelebrationsCacheProps = {
12
+ customPrefetchMethod?: CustomPrefetchMethod
13
+ customGetCachePathMethod?: CustomGetCachePathMethod
14
+ }
15
+
16
+ export class GifCelebrationsCache {
17
+ public readonly initialSet = createEvent<GifsGroups>()
18
+ public readonly set = createEvent<GifsStructure>()
19
+ public readonly reset = createEvent()
20
+
21
+ public readonly getCachePathMethod?: CustomGetCachePathMethod
22
+ public readonly prefetchMethod: CustomPrefetchMethod | typeof Image.prefetch
23
+
24
+ public readonly prefetchGifs = createEffect<string[], void>((gifs) => {
25
+ if (!gifs || gifs.length === 0) return
26
+
27
+ gifs.forEach((gifUrl) => {
28
+ this.prefetchMethod(gifUrl)
29
+ })
30
+ })
31
+
32
+ public readonly $gifs = createStore<GifsStructure | null>(null)
33
+ .reset(this.reset)
34
+ .on(this.initialSet, (_, gifs) => generateGifsStructure(gifs))
35
+ .on(this.set, (_, gifs) => gifs)
36
+
37
+ constructor({ customGetCachePathMethod, customPrefetchMethod }: GifCelebrationsCacheProps) {
38
+ this.prefetchMethod = customPrefetchMethod || Image.prefetch
39
+ this.getCachePathMethod = customGetCachePathMethod
40
+ }
41
+ }
@@ -0,0 +1,177 @@
1
+ import { createEffect, createEvent, createStore, sample, Store } from 'effector'
2
+ import { GifCelebrationsApi } from './GifCelebrationsApi'
3
+ import {
4
+ CustomLocalStorage,
5
+ GifCelebrationsModelProps,
6
+ GifObject,
7
+ GifsStructure,
8
+ ProblemsGifsTable,
9
+ } from '../types/model.types'
10
+ import { GifCelebrationsCache } from './GifCelebrations.cache'
11
+ import { generateProblemsGifsTable, shouldRefreshGifs } from '../helpers'
12
+ import { MIN_PROBLEMS_TO_SHOW_GIF, GIFS_FETCHED_KEY } from '../constants'
13
+
14
+ type HandleGifSelectionProps = {
15
+ gifsGroups: GifsStructure
16
+ resolvedProblemsAmount: number
17
+ problemsGifsTable: ProblemsGifsTable
18
+ }
19
+
20
+ export class GifCelebrationsModel {
21
+ public readonly api: GifCelebrationsApi
22
+ public readonly cache: GifCelebrationsCache
23
+
24
+ public readonly $problemsAmount: Store<number>
25
+ public readonly $resolvedProblemsAmount: Store<number>
26
+
27
+ public readonly play = createEvent()
28
+ public readonly stop = createEvent()
29
+
30
+ public readonly resetGifTriggerable = createEvent()
31
+ public readonly setGifTriggerable = createEvent<boolean>()
32
+ public readonly $isGifTriggerable = createStore<boolean>(false)
33
+ .reset(this.resetGifTriggerable)
34
+ .reset(this.stop)
35
+ .on(
36
+ this.setGifTriggerable,
37
+ (_, isGifCelebrationMayBeTriggered) => isGifCelebrationMayBeTriggered,
38
+ )
39
+
40
+ public readonly resetProblemsGifsTable = createEvent()
41
+ public readonly $problemsGifsTable = createStore<ProblemsGifsTable | null>(null).reset(
42
+ this.resetProblemsGifsTable,
43
+ )
44
+
45
+ public readonly setActiveGif = createEvent<GifObject>()
46
+ public readonly $activeGif = createStore<GifObject | null>(null)
47
+ .reset(this.stop)
48
+ .reset(this.resetGifTriggerable)
49
+ .on(this.setActiveGif, (_, gif) => gif)
50
+
51
+ private readonly localStorage: typeof localStorage | CustomLocalStorage
52
+
53
+ public readonly initialize = createEvent()
54
+
55
+ constructor({
56
+ api,
57
+ problemsAmount,
58
+ resolvedProblemsAmount,
59
+ customLocalStorage,
60
+ customGetCachePathMethod,
61
+ customPrefetchMethod,
62
+ }: GifCelebrationsModelProps) {
63
+ this.api = new GifCelebrationsApi(api)
64
+ this.cache = new GifCelebrationsCache({ customGetCachePathMethod, customPrefetchMethod })
65
+
66
+ this.$problemsAmount = problemsAmount
67
+ this.$resolvedProblemsAmount = resolvedProblemsAmount
68
+
69
+ this.localStorage = customLocalStorage || localStorage
70
+ this.setupEventHandlers()
71
+ }
72
+
73
+ private setupEventHandlers() {
74
+ sample({
75
+ clock: this.initialize,
76
+ source: this.cache.$gifs,
77
+ filter: (gifs) => {
78
+ if (!gifs) return true
79
+
80
+ const gifsFetchedTimestamp = this.localStorage.getItem(GIFS_FETCHED_KEY)
81
+ return shouldRefreshGifs(gifsFetchedTimestamp)
82
+ },
83
+ target: this.api.getGifsFx,
84
+ })
85
+
86
+ sample({
87
+ source: this.api.getGifsFx.doneData,
88
+ filter: (response) => response.ok,
89
+ fn: (response) => response.data,
90
+ target: this.cache.initialSet,
91
+ })
92
+
93
+ sample({
94
+ source: this.api.getGifsFx.doneData,
95
+ filter: (response) => response.ok,
96
+ target: createEffect(() => {
97
+ this.localStorage.setItem(GIFS_FETCHED_KEY, new Date().toISOString())
98
+ }),
99
+ })
100
+
101
+ sample({
102
+ clock: [this.initialize, this.api.getGifsFx.doneData],
103
+ source: {
104
+ gifs: this.cache.$gifs,
105
+ problemsAmount: this.$problemsAmount,
106
+ resolvedProblemsAmount: this.$resolvedProblemsAmount,
107
+ },
108
+ filter: ({ problemsAmount, gifs }) => !!(problemsAmount >= MIN_PROBLEMS_TO_SHOW_GIF && gifs),
109
+ fn: ({ problemsAmount, resolvedProblemsAmount, gifs }) => {
110
+ if (!gifs) return null
111
+ return generateProblemsGifsTable(problemsAmount, resolvedProblemsAmount, gifs)
112
+ },
113
+ target: this.$problemsGifsTable,
114
+ })
115
+
116
+ sample({
117
+ source: this.$problemsGifsTable,
118
+ filter: (problemsGifsTable) => !!problemsGifsTable,
119
+ fn: (problemsGifsTable) => {
120
+ if (!problemsGifsTable) return []
121
+ return Object.values(problemsGifsTable).map((gif) => gif.url)
122
+ },
123
+ target: this.cache.prefetchGifs,
124
+ })
125
+
126
+ sample({
127
+ clock: this.play,
128
+ source: {
129
+ isGifCelebrationMayBeTriggered: this.$isGifTriggerable,
130
+ gifsGroups: this.cache.$gifs,
131
+ resolvedProblemsAmount: this.$resolvedProblemsAmount,
132
+ problemsGifsTable: this.$problemsGifsTable,
133
+ },
134
+ filter: ({
135
+ isGifCelebrationMayBeTriggered,
136
+ gifsGroups,
137
+ resolvedProblemsAmount,
138
+ problemsGifsTable,
139
+ }) =>
140
+ !!(
141
+ isGifCelebrationMayBeTriggered &&
142
+ gifsGroups &&
143
+ resolvedProblemsAmount !== undefined &&
144
+ problemsGifsTable
145
+ ),
146
+ fn: (sources) => sources as HandleGifSelectionProps,
147
+ target: this.updateGifShownStatus,
148
+ })
149
+ }
150
+
151
+ private readonly updateGifShownStatus = createEffect(
152
+ ({ gifsGroups, resolvedProblemsAmount, problemsGifsTable }: HandleGifSelectionProps) => {
153
+ const gifToShow = problemsGifsTable[resolvedProblemsAmount]
154
+ const groupName = gifToShow.group
155
+
156
+ const gifsInGroup = gifsGroups[groupName]
157
+ const selectedGifIndex = gifsInGroup?.findIndex((gif) => gif.url === gifToShow.url)
158
+
159
+ if (!gifsInGroup || selectedGifIndex === -1) {
160
+ console.warn(`Gif with URL ${gifToShow.url} not found in group ${groupName}`)
161
+ return
162
+ }
163
+
164
+ const updatedGifsInGroup = gifsInGroup.map((gif, index) => ({
165
+ ...gif,
166
+ isShown: index === selectedGifIndex ? true : gif.isShown,
167
+ }))
168
+
169
+ this.cache.set({
170
+ ...gifsGroups,
171
+ [groupName]: updatedGifsInGroup,
172
+ })
173
+
174
+ this.setActiveGif(gifToShow)
175
+ },
176
+ )
177
+ }
@@ -0,0 +1,26 @@
1
+ import { createControllerEffect } from './../../../lib/effector/createControllerEffect'
2
+ import { GifCelebrationApiRequests } from '../types/model.types'
3
+ import { GET_GIFS_TIMEOUT } from '../constants'
4
+
5
+ export class GifCelebrationsApi {
6
+ public readonly getGifsFx
7
+
8
+ private failedGetGifxTimeout: NodeJS.Timeout | null = null
9
+
10
+ constructor(api: GifCelebrationApiRequests) {
11
+ this.getGifsFx = createControllerEffect(() => api.getGifs())
12
+
13
+ this.getGifsFx.fail.watch(() => {
14
+ this.failedGetGifxTimeout = setTimeout(() => {
15
+ this.getGifsFx()
16
+ }, GET_GIFS_TIMEOUT)
17
+ })
18
+
19
+ this.getGifsFx.done.watch(() => {
20
+ if (this.failedGetGifxTimeout) {
21
+ clearTimeout(this.failedGetGifxTimeout)
22
+ this.failedGetGifxTimeout = null
23
+ }
24
+ })
25
+ }
26
+ }
@@ -0,0 +1,53 @@
1
+ import { Store } from 'effector'
2
+
3
+ export type CustomGetCachePathMethod = (uri: string) => Promise<string | null>
4
+ export type CustomPrefetchMethod = (gifs: string[] | string) => Promise<unknown>
5
+ export type CustomLocalStorage = {
6
+ setItem: (key: string, value: string) => void
7
+ getItem: (key: string) => string | null | undefined
8
+ }
9
+
10
+ export type GroupNumber = number
11
+
12
+ export type GifCelebrationApiRequests = {
13
+ getGifs: () => Promise<GifResponse>
14
+ }
15
+
16
+ export type GifCelebrationsModelProps = {
17
+ api: GifCelebrationApiRequests
18
+ problemsAmount: Store<number>
19
+ resolvedProblemsAmount: Store<number>
20
+ customLocalStorage?: CustomLocalStorage
21
+ customPrefetchMethod?: CustomPrefetchMethod
22
+ customGetCachePathMethod?: CustomGetCachePathMethod
23
+ }
24
+
25
+ export type GifsGroup = string[]
26
+ export type GroupNumberString = `group${GroupNumber}`
27
+ export type GifsGroups = Record<GroupNumberString, GifsGroup>
28
+
29
+ export type GifObject = {
30
+ url: string
31
+ group: GroupNumberString
32
+ isShown?: boolean
33
+ }
34
+
35
+ export type GifsStructure = Record<GroupNumberString, GifObject[]>
36
+ /**
37
+ * Usually, the response contains 3 groups of gifs:
38
+ * - group1: early celebrations
39
+ * - group2: mid celebrations
40
+ * - group3: party
41
+ */
42
+ export type GifResponse = {
43
+ data: GifsGroups
44
+ ok: boolean
45
+ }
46
+ export type GifsPayload = {
47
+ group: number
48
+ }
49
+
50
+ export type ProblemsGifsTable = Record<number, GifObject>
51
+ export type ProgressValue = 0 | 1 | 2
52
+
53
+ export type TextComponentType = (text: string) => React.ReactNode
package/src/index.ts CHANGED
@@ -6,3 +6,4 @@ export * from './features/celebrations'
6
6
  export * from './features/endOfAssignment'
7
7
  export * from './features/formulaSheet'
8
8
  export * from './features/problemSelector'
9
+ export * from './features/gifCelebrations'
@@ -1,66 +1,66 @@
1
1
  import { createEffect, type Effect } from 'effector'
2
2
 
3
3
  type LikeCanceledError = {
4
- code?: string
4
+ code?: string
5
5
  }
6
6
 
7
7
  export type ControlledEffect<P, T> = Effect<P, T> & {
8
- controller: AbortController
8
+ controller: AbortController
9
9
  }
10
10
 
11
11
  type ControlledRequestProps<P> = {
12
- data: P
13
- controller?: AbortController
14
- abortAfter?: number
12
+ data: P
13
+ controller?: AbortController
14
+ abortAfter?: number
15
15
  }
16
16
 
17
17
  type RequestOptionalConfig = { signal?: AbortSignal }
18
18
 
19
19
  export function controlledRequest<R = void, P = void>(
20
- request: (props: P, config?: RequestOptionalConfig) => Promise<R>,
20
+ request: (props: P, config?: RequestOptionalConfig) => Promise<R>,
21
21
  ) {
22
- return async ({
23
- data,
24
- controller = new AbortController(),
25
- abortAfter = 20000,
26
- }: ControlledRequestProps<P>) => {
27
- let timeout: ReturnType<typeof setTimeout> | null = null
22
+ return async ({
23
+ data,
24
+ controller = new AbortController(),
25
+ abortAfter = 20000,
26
+ }: ControlledRequestProps<P>) => {
27
+ let timeout: ReturnType<typeof setTimeout> | null = null
28
28
 
29
- if (abortAfter) {
30
- timeout = setTimeout(() => {
31
- controller.abort('timeout')
32
- }, abortAfter)
33
- }
34
-
35
- try {
36
- const response = await request(data, { signal: controller?.signal })
37
- if (timeout !== null) clearTimeout(timeout)
38
- return response
39
- } catch (error: unknown) {
40
- if ((error as LikeCanceledError).code === 'ERR_CANCELED') {
41
- if (controller.signal.reason === 'timeout') {
42
- throw new Error('Request timed out')
43
- }
44
- throw new Error('Request aborted manually')
45
- }
29
+ if (abortAfter) {
30
+ timeout = setTimeout(() => {
31
+ controller.abort('timeout')
32
+ }, abortAfter)
33
+ }
46
34
 
47
- throw error
35
+ try {
36
+ const response = await request(data, { signal: controller?.signal })
37
+ if (timeout !== null) clearTimeout(timeout)
38
+ return response
39
+ } catch (error: unknown) {
40
+ if ((error as LikeCanceledError).code === 'ERR_CANCELED') {
41
+ if (controller.signal.reason === 'timeout') {
42
+ throw new Error('Request timed out')
48
43
  }
44
+ throw new Error('Request aborted manually')
45
+ }
46
+
47
+ throw error
49
48
  }
49
+ }
50
50
  }
51
51
 
52
- export const createControllerEffect = <T, P>(
53
- fn: (props: P, config?: RequestOptionalConfig) => Promise<T>,
52
+ export const createControllerEffect = <T, P = void>(
53
+ fn: (props: P, config?: RequestOptionalConfig) => Promise<T>,
54
54
  ) => {
55
- const request = controlledRequest<T, P>(fn)
56
- const effect = createEffect((data: P) =>
57
- request({ data, controller: effect.controller }),
58
- ) as ControlledEffect<P, T>
59
- effect.controller = new AbortController()
55
+ const request = controlledRequest<T, P>(fn)
56
+ const effect = createEffect((data: P) =>
57
+ request({ data, controller: effect.controller }),
58
+ ) as ControlledEffect<P, T>
59
+ effect.controller = new AbortController()
60
60
 
61
- effect.fail.watch(() => {
62
- effect.controller = new AbortController()
63
- })
61
+ effect.fail.watch(() => {
62
+ effect.controller = new AbortController()
63
+ })
64
64
 
65
- return effect
65
+ return effect
66
66
  }
@@ -1,6 +1,6 @@
1
1
  import { getText } from '../../shared/translation/helpers'
2
2
 
3
- export const noop = () => {};
3
+ export const noop = () => {}
4
4
 
5
5
  export const generateRandomId = () =>
6
6
  Date.now().toString(36) + Math.random().toString(36).substring(2)
@@ -0,0 +1,6 @@
1
+ declare module '*.png'
2
+ declare module '*.svg'
3
+ declare module '*.jpeg'
4
+ declare module '*.jpg'
5
+ declare module '*.webp'
6
+ declare module '*.gif'
@@ -18,5 +18,10 @@
18
18
  "errorMessage": "Something went wrong! Try again",
19
19
  "howCanIHelp": "Hi! Do you want help?",
20
20
  "showError": "Show error"
21
+ },
22
+ "gifCelebrations": {
23
+ "greatStart": "Great start!",
24
+ "onTheWay": "On the way!",
25
+ "almostThere": "Almost there"
21
26
  }
22
27
  }
@@ -18,5 +18,10 @@
18
18
  "errorMessage": "Something went wrong! Try again",
19
19
  "howCanIHelp": "Hi! Do you want help?",
20
20
  "showError": "Show error"
21
+ },
22
+ "gifCelebrations": {
23
+ "greatStart": "Great start!",
24
+ "onTheWay": "On the way!",
25
+ "almostThere": "Almost there"
21
26
  }
22
27
  }
@@ -18,5 +18,10 @@
18
18
  "errorMessage": "Något gick snett! Försök igen",
19
19
  "howCanIHelp": "Hej! Hur kan jag hjälpa dig?",
20
20
  "showError": "Visa fel"
21
+ },
22
+ "gifCelebrations": {
23
+ "greatStart": "Bra start!",
24
+ "onTheWay": "På god väg!",
25
+ "almostThere": "Nästan där"
21
26
  }
22
27
  }
@@ -0,0 +1,4 @@
1
+ export type Size = {
2
+ width: number
3
+ height: number
4
+ }