@magmamath/students-features 0.6.6-rc.10 → 0.6.6-rc.13

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 (42) hide show
  1. package/dist/commonjs/features/gifCelebrations/constants.js +2 -1
  2. package/dist/commonjs/features/gifCelebrations/constants.js.map +1 -1
  3. package/dist/commonjs/features/gifCelebrations/helpers.js +36 -33
  4. package/dist/commonjs/features/gifCelebrations/helpers.js.map +1 -1
  5. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.cache.js +1 -0
  6. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.cache.js.map +1 -1
  7. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.model.js +70 -52
  8. package/dist/commonjs/features/gifCelebrations/model/GifCelebrations.model.js.map +1 -1
  9. package/dist/module/features/gifCelebrations/components/Progress.js.map +1 -1
  10. package/dist/module/features/gifCelebrations/constants.js +1 -0
  11. package/dist/module/features/gifCelebrations/constants.js.map +1 -1
  12. package/dist/module/features/gifCelebrations/helpers.js +33 -31
  13. package/dist/module/features/gifCelebrations/helpers.js.map +1 -1
  14. package/dist/module/features/gifCelebrations/model/GifCelebrations.cache.js +1 -0
  15. package/dist/module/features/gifCelebrations/model/GifCelebrations.cache.js.map +1 -1
  16. package/dist/module/features/gifCelebrations/model/GifCelebrations.model.js +72 -54
  17. package/dist/module/features/gifCelebrations/model/GifCelebrations.model.js.map +1 -1
  18. package/dist/typescript/commonjs/features/gifCelebrations/constants.d.ts +1 -0
  19. package/dist/typescript/commonjs/features/gifCelebrations/constants.d.ts.map +1 -1
  20. package/dist/typescript/commonjs/features/gifCelebrations/helpers.d.ts +3 -2
  21. package/dist/typescript/commonjs/features/gifCelebrations/helpers.d.ts.map +1 -1
  22. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrations.cache.d.ts.map +1 -1
  23. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrations.model.d.ts +6 -5
  24. package/dist/typescript/commonjs/features/gifCelebrations/model/GifCelebrations.model.d.ts.map +1 -1
  25. package/dist/typescript/commonjs/features/gifCelebrations/types/model.types.d.ts +11 -5
  26. package/dist/typescript/commonjs/features/gifCelebrations/types/model.types.d.ts.map +1 -1
  27. package/dist/typescript/module/features/gifCelebrations/constants.d.ts +1 -0
  28. package/dist/typescript/module/features/gifCelebrations/constants.d.ts.map +1 -1
  29. package/dist/typescript/module/features/gifCelebrations/helpers.d.ts +3 -2
  30. package/dist/typescript/module/features/gifCelebrations/helpers.d.ts.map +1 -1
  31. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrations.cache.d.ts.map +1 -1
  32. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrations.model.d.ts +6 -5
  33. package/dist/typescript/module/features/gifCelebrations/model/GifCelebrations.model.d.ts.map +1 -1
  34. package/dist/typescript/module/features/gifCelebrations/types/model.types.d.ts +11 -5
  35. package/dist/typescript/module/features/gifCelebrations/types/model.types.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/features/gifCelebrations/components/Progress.tsx +1 -1
  38. package/src/features/gifCelebrations/constants.ts +2 -0
  39. package/src/features/gifCelebrations/helpers.ts +51 -30
  40. package/src/features/gifCelebrations/model/GifCelebrations.cache.ts +2 -0
  41. package/src/features/gifCelebrations/model/GifCelebrations.model.ts +77 -62
  42. package/src/features/gifCelebrations/types/model.types.ts +11 -5
@@ -4,7 +4,8 @@ import {
4
4
  GifsGroups,
5
5
  GifsStructure,
6
6
  GroupNumber,
7
- ProblemsPercentageTable,
7
+ GroupNumberString,
8
+ ProblemsGifsTable,
8
9
  } from './types/model.types'
9
10
  import { MIN_WIDTH_FOR_ONE_DIGIT, MIN_WIDTH_FOR_TWO_DIGITS } from './constants'
10
11
  import { Image } from 'react-native'
@@ -27,40 +28,49 @@ import { Image } from 'react-native'
27
28
  * Group 2: >30% <60% - Mid celebrations
28
29
  * Group 3: >=60% - Party
29
30
  */
30
- export const calculatePercentageTable = (totalProblems: number): ProblemsPercentageTable => {
31
- if (totalProblems <= 12) return { 50: Math.ceil(totalProblems * 0.5) }
31
+ export const generateProblemsGifsTable = (
32
+ totalProblems: number,
33
+ resolvedProblems: number,
34
+ gifs: GifsStructure,
35
+ ): ProblemsGifsTable => {
36
+ const percentageMilestones = getPercentageMilestones(totalProblems)
32
37
 
33
- if (totalProblems <= 18) {
34
- return {
35
- 33: Math.ceil(totalProblems * 0.33),
36
- 66: Math.ceil(totalProblems * 0.66),
37
- }
38
- }
38
+ const result: ProblemsGifsTable = {}
39
39
 
40
- if (totalProblems <= 24) {
41
- return {
42
- 25: Math.ceil(totalProblems * 0.25),
43
- 50: Math.ceil(totalProblems * 0.5),
44
- 75: Math.ceil(totalProblems * 0.75),
45
- }
46
- }
40
+ for (const percentage of percentageMilestones) {
41
+ const problemNumber = Math.ceil(totalProblems * (percentage / 100))
42
+
43
+ if (problemNumber <= resolvedProblems) continue
47
44
 
48
- if (totalProblems <= 30) {
49
- return {
50
- 20: Math.ceil(totalProblems * 0.2),
51
- 40: Math.ceil(totalProblems * 0.4),
52
- 60: Math.ceil(totalProblems * 0.6),
53
- 80: Math.ceil(totalProblems * 0.8),
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
+ }
54
59
  }
55
60
  }
56
61
 
57
- return {
58
- 15: Math.ceil(totalProblems * 0.15),
59
- 30: Math.ceil(totalProblems * 0.3),
60
- 45: Math.ceil(totalProblems * 0.45),
61
- 60: Math.ceil(totalProblems * 0.6),
62
- 75: Math.ceil(totalProblems * 0.75),
63
- }
62
+ return result
63
+ }
64
+ const getPercentageMilestones = (totalProblems: number): number[] => {
65
+ if (totalProblems <= 12) return [50]
66
+
67
+ if (totalProblems <= 18) return [33, 66]
68
+
69
+ if (totalProblems <= 24) return [25, 50, 75]
70
+
71
+ if (totalProblems <= 30) return [20, 40, 60, 80]
72
+
73
+ return [15, 30, 45, 60, 75]
64
74
  }
65
75
 
66
76
  export const generateGifsStructure = (gifs: GifsGroups): GifsStructure =>
@@ -70,7 +80,7 @@ export const generateGifsStructure = (gifs: GifsGroups): GifsStructure =>
70
80
  value.map((url) => ({
71
81
  url,
72
82
  isShown: false,
73
- group: key as `group${GroupNumber}`,
83
+ group: key as GroupNumberString,
74
84
  })),
75
85
  ]),
76
86
  )
@@ -121,3 +131,14 @@ export const getImagePath = async (url: string, cacheMethod?: CustomGetCachePath
121
131
 
122
132
  export const constrainDimension = (value: number, min: number, max: number) =>
123
133
  Math.max(min, Math.min(max, value))
134
+
135
+ export const shouldRefreshGifs = (timestamp: string | null): 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
+ }
@@ -32,6 +32,8 @@ export class GifCelebrationsCache {
32
32
  })
33
33
 
34
34
  public readonly prefetchGifs = createEffect<string[], void>((gifs) => {
35
+ if (!gifs || gifs.length === 0) return
36
+
35
37
  gifs.forEach((gifUrl) => {
36
38
  this.prefetchMethod(gifUrl)
37
39
  })
@@ -1,19 +1,20 @@
1
- import { createEffect, createEvent, createStore, sample, Store } from 'effector'
1
+ import { createEffect, createEvent, createStore, merge, sample, Store } from 'effector'
2
2
  import { GifCelebrationsApi } from './GifCelebrationsApi'
3
3
  import {
4
+ CustomLocalStorage,
4
5
  GifCelebrationsModelProps,
5
6
  GifObject,
6
7
  GifsStructure,
7
- ProblemsPercentageTable,
8
+ ProblemsGifsTable,
8
9
  } from '../types/model.types'
9
10
  import { GifCelebrationsCache } from './GifCelebrations.cache'
10
- import { calculatePercentageTable, findGroupForPercentage } from '../helpers'
11
- import { GET_GIFS_TIMEOUT, MIN_PROBLEMS_TO_SHOW_GIF } from '../constants'
11
+ import { generateProblemsGifsTable, shouldRefreshGifs } from '../helpers'
12
+ import { GET_GIFS_TIMEOUT, MIN_PROBLEMS_TO_SHOW_GIF, GIFS_FETCHED_KEY } from '../constants'
12
13
 
13
14
  type HandleGifSelectionProps = {
14
15
  gifsGroups: GifsStructure
15
16
  resolvedProblem: number
16
- problemsPercentageTable: ProblemsPercentageTable
17
+ problemsGifsTable: ProblemsGifsTable
17
18
  }
18
19
 
19
20
  export class GifCelebrationsModel {
@@ -36,10 +37,10 @@ export class GifCelebrationsModel {
36
37
  (_, isGifCelebrationMayBeTriggered) => isGifCelebrationMayBeTriggered,
37
38
  )
38
39
 
39
- public readonly resetPercentageTable = createEvent()
40
- public readonly $problemsPercentageTable = createStore<ProblemsPercentageTable | null>(
41
- null,
42
- ).reset(this.resetPercentageTable)
40
+ public readonly resetProblemsGifsTable = createEvent()
41
+ public readonly $problemsGifsTable = createStore<ProblemsGifsTable | null>(null).reset(
42
+ this.resetProblemsGifsTable,
43
+ )
43
44
 
44
45
  public readonly setActiveGif = createEvent<GifObject>()
45
46
  public readonly $activeGif = createStore<GifObject | null>(null)
@@ -47,12 +48,15 @@ export class GifCelebrationsModel {
47
48
  .reset(this.resetGifTriggerable)
48
49
  .on(this.setActiveGif, (_, gif) => gif)
49
50
 
51
+ private readonly localStorage: typeof localStorage | CustomLocalStorage
52
+
50
53
  public readonly initialize = createEvent()
51
54
 
52
55
  constructor({
53
56
  api,
54
57
  totalProblems,
55
58
  resolvedProblems,
59
+ customLocalStorage,
56
60
  customGetCachePathMethod,
57
61
  customPrefetchMethod,
58
62
  }: GifCelebrationsModelProps) {
@@ -62,16 +66,22 @@ export class GifCelebrationsModel {
62
66
  this.$totalProblems = totalProblems
63
67
  this.$resolvedProblems = resolvedProblems
64
68
 
69
+ this.localStorage = customLocalStorage || localStorage
65
70
  this.setupEventHandlers()
66
71
  }
67
72
 
68
- private resendGefFxTimeout: NodeJS.Timeout | null = null
73
+ private failedGetGifxTimeout: NodeJS.Timeout | null = null
69
74
 
70
75
  private setupEventHandlers() {
71
76
  sample({
72
77
  clock: this.initialize,
73
78
  source: this.cache.$gifs,
74
- filter: (gifs) => !gifs,
79
+ filter: (gifs) => {
80
+ if (!gifs) return true
81
+
82
+ const gifsFetchedTimestamp = this.localStorage.getItem(GIFS_FETCHED_KEY) as string | null
83
+ return shouldRefreshGifs(gifsFetchedTimestamp)
84
+ },
75
85
  target: this.api.getGifsFx,
76
86
  })
77
87
 
@@ -79,19 +89,40 @@ export class GifCelebrationsModel {
79
89
  source: this.api.getGifsFx.doneData,
80
90
  filter: (response) => response.ok,
81
91
  fn: (response) => response.data,
82
- target: this.cache.prefetchAllGifs,
92
+ target: this.cache.initialSet,
93
+ })
94
+
95
+ sample({
96
+ source: this.api.getGifsFx.doneData,
97
+ filter: (response) => response.ok,
98
+ target: createEffect(() => {
99
+ this.localStorage.setItem(GIFS_FETCHED_KEY, new Date().toISOString())
100
+ }),
83
101
  })
84
102
 
85
103
  sample({
86
- clock: [this.initialize, this.$totalProblems.updates],
87
- source: this.$totalProblems,
88
- fn: (totalProblems) => {
89
- if (totalProblems && totalProblems >= MIN_PROBLEMS_TO_SHOW_GIF) {
90
- return calculatePercentageTable(totalProblems)
91
- }
92
- return null
104
+ clock: [this.cache.$gifs, this.$totalProblems],
105
+ source: {
106
+ gifs: this.cache.$gifs,
107
+ totalProblems: this.$totalProblems,
108
+ resolvedProblems: this.$resolvedProblems,
109
+ },
110
+ filter: ({ totalProblems, gifs }) => !!(totalProblems >= MIN_PROBLEMS_TO_SHOW_GIF && gifs),
111
+ fn: ({ totalProblems, resolvedProblems, gifs }) => {
112
+ if (!gifs) return null
113
+ return generateProblemsGifsTable(totalProblems, resolvedProblems, gifs)
114
+ },
115
+ target: this.$problemsGifsTable,
116
+ })
117
+
118
+ sample({
119
+ source: this.$problemsGifsTable,
120
+ filter: (problemsGifsTable) => !!problemsGifsTable,
121
+ fn: (problemsGifsTable) => {
122
+ if (!problemsGifsTable) return []
123
+ return Object.values(problemsGifsTable).map((gif) => gif.url)
93
124
  },
94
- target: this.$problemsPercentageTable,
125
+ target: this.cache.prefetchGifs,
95
126
  })
96
127
 
97
128
  sample({
@@ -100,79 +131,63 @@ export class GifCelebrationsModel {
100
131
  isGifCelebrationMayBeTriggered: this.$isGifTriggerable,
101
132
  gifsGroups: this.cache.$gifs,
102
133
  resolvedProblem: this.$resolvedProblems,
103
- problemsPercentageTable: this.$problemsPercentageTable,
134
+ problemsGifsTable: this.$problemsGifsTable,
104
135
  },
105
136
  filter: ({
106
137
  isGifCelebrationMayBeTriggered,
107
138
  gifsGroups,
108
139
  resolvedProblem,
109
- problemsPercentageTable,
140
+ problemsGifsTable,
110
141
  }) =>
111
142
  !!(
112
143
  isGifCelebrationMayBeTriggered &&
113
144
  gifsGroups &&
114
145
  resolvedProblem !== undefined &&
115
- problemsPercentageTable
146
+ problemsGifsTable
116
147
  ),
117
148
  fn: (sources) => sources as HandleGifSelectionProps,
118
149
  target: this.handleGifSelection,
119
150
  })
120
151
 
121
152
  this.api.getGifsFx.fail.watch(() => {
122
- this.resendGefFxTimeout = setTimeout(() => {
153
+ this.failedGetGifxTimeout = setTimeout(() => {
123
154
  this.api.getGifsFx()
124
155
  }, GET_GIFS_TIMEOUT)
125
156
  })
126
157
 
127
158
  this.api.getGifsFx.done.watch(() => {
128
- if (this.resendGefFxTimeout) {
129
- clearTimeout(this.resendGefFxTimeout)
130
- this.resendGefFxTimeout = null
159
+ if (this.failedGetGifxTimeout) {
160
+ clearTimeout(this.failedGetGifxTimeout)
161
+ this.failedGetGifxTimeout = null
131
162
  }
132
163
  })
133
164
  }
134
165
 
135
166
  private readonly handleGifSelection = createEffect(
136
- ({ gifsGroups, resolvedProblem, problemsPercentageTable }: HandleGifSelectionProps) => {
137
- const percentageMilestone = Object.keys(problemsPercentageTable).find(
138
- (key) => problemsPercentageTable[Number(key)] === resolvedProblem,
139
- )
140
-
141
- if (!percentageMilestone) return null
142
-
143
- const groupName = findGroupForPercentage(Number(percentageMilestone))
144
-
145
- const gifsForGroup = gifsGroups[groupName]
146
- const unusedGifIndex = gifsForGroup.findIndex((gif) => !gif.isShown)
147
-
148
- if (unusedGifIndex === -1) {
149
- const updatedGifsGroups = { ...gifsGroups }
150
- updatedGifsGroups[groupName] = gifsGroups[groupName].map((gif) => ({
151
- ...gif,
152
- isShown: false,
153
- }))
154
- const gifObject: GifObject = {
155
- ...updatedGifsGroups[groupName][0],
156
- isShown: true,
157
- }
158
- updatedGifsGroups[groupName][0].isShown = true
159
-
160
- this.setActiveGif(gifObject)
161
- this.cache.set(updatedGifsGroups)
162
- }
167
+ ({ gifsGroups, resolvedProblem, problemsGifsTable }: HandleGifSelectionProps) => {
168
+ const gifToShow = problemsGifsTable[resolvedProblem]
169
+ const groupName = gifToShow.group
163
170
 
164
- const selectedGif = gifsForGroup[unusedGifIndex]
171
+ const gifsInGroup = gifsGroups[groupName]
172
+ const selectedGifIndex = gifsInGroup?.findIndex((gif) => gif.url === gifToShow.url)
165
173
 
166
- const gifObject: GifObject = {
167
- ...selectedGif,
168
- isShown: true,
174
+ if (!gifsInGroup || selectedGifIndex === -1) {
175
+ console.warn(`Gif with URL ${gifToShow.url} not found in group ${groupName}`)
176
+ return
169
177
  }
170
178
 
171
- const updatedGifsGroups = { ...gifsGroups }
172
- updatedGifsGroups[groupName][unusedGifIndex].isShown = true
173
- this.cache.set(updatedGifsGroups)
179
+ const updatedGifsInGroup = gifsInGroup.map((gif, index) => ({
180
+ ...gif,
181
+ isShown: index === selectedGifIndex ? true : gif.isShown,
182
+ }))
183
+ console.log('updatedGifsInGroup: ', updatedGifsInGroup)
184
+
185
+ this.cache.set({
186
+ ...gifsGroups,
187
+ [groupName]: updatedGifsInGroup,
188
+ })
174
189
 
175
- this.setActiveGif(gifObject)
190
+ this.setActiveGif(gifToShow)
176
191
  },
177
192
  )
178
193
  }
@@ -2,6 +2,10 @@ import { Store } from 'effector'
2
2
 
3
3
  export type CustomGetCachePathMethod = (uri: string) => Promise<string | null>
4
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
+ }
5
9
 
6
10
  export type GroupNumber = number
7
11
 
@@ -13,20 +17,22 @@ export type GifCelebrationsModelProps = {
13
17
  api: GifCelebrationApiRequests
14
18
  totalProblems: Store<number>
15
19
  resolvedProblems: Store<number>
20
+ customLocalStorage?: CustomLocalStorage
16
21
  customPrefetchMethod?: CustomPrefetchMethod
17
22
  customGetCachePathMethod?: CustomGetCachePathMethod
18
23
  }
19
24
 
20
25
  export type GifsGroup = string[]
21
- export type GifsGroups = Record<`group${GroupNumber}`, GifsGroup>
26
+ export type GroupNumberString = `group${GroupNumber}`
27
+ export type GifsGroups = Record<GroupNumberString, GifsGroup>
22
28
 
23
29
  export type GifObject = {
24
30
  url: string
25
- isShown: boolean
26
- group: `group${GroupNumber}`
31
+ group: GroupNumberString
32
+ isShown?: boolean
27
33
  }
28
34
 
29
- export type GifsStructure = Record<`group${GroupNumber}`, GifObject[]>
35
+ export type GifsStructure = Record<GroupNumberString, GifObject[]>
30
36
  /**
31
37
  * Usually, the response contains 3 groups of gifs:
32
38
  * - group1: early celebrations
@@ -41,7 +47,7 @@ export type GifsPayload = {
41
47
  group: number
42
48
  }
43
49
 
44
- export type ProblemsPercentageTable = Record<number, number>
50
+ export type ProblemsGifsTable = Record<number, GifObject>
45
51
  export type ProgressValue = 0 | 1 | 2
46
52
 
47
53
  export type TextComponentType = (text: string) => React.ReactNode