-
Notifications
You must be signed in to change notification settings - Fork 1
/
SprachyUserSPA.ts
168 lines (137 loc) · 5.03 KB
/
SprachyUserSPA.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import _ from 'lodash'
import type { SprachyAPIClient } from "./SprachyAPIClient"
import type { ProgressItem, User, ProgressSummary } from "$lib/api"
import type { Exercise, Pattern } from "$lib/Pattern"
import { sprachdex } from "$lib/sprachdex"
import { time } from "$lib/time"
import { CanvasEffects } from "$lib/client/CanvasEffects"
import { SpeechSystem } from '$lib/SpeechSystem'
import { derived, writable, type Writable } from 'svelte/store'
export type Review = Exercise & {
pattern: PatternAndProgress
}
declare const window: any
/**
* Single page application state for when the user is signed in
*/
export class SprachyUserSPA {
user: Writable<User> = writable({}) as any
progressItems: Writable<ProgressItem[]> = writable([])
speech = new SpeechSystem()
/**
* For drawing success confetti animation
*/
effects = new CanvasEffects()
constructor(readonly api: SprachyAPIClient, readonly backgroundApi: SprachyAPIClient, summary: ProgressSummary) {
this.receiveProgress(summary)
// Expose some stuff for debugging
this.user.subscribe($user => window.user = $user)
}
admin = derived(this.user, user => user.isAdmin)
async refreshProgress() {
const summary = await this.api.getProgress()
this.receiveProgress(summary)
}
receiveProgress(summary: ProgressSummary) {
this.user.set(summary.user)
this.progressItems.set(summary.progressItems)
}
// Update local progress with a single new item
receiveProgressItem(item: ProgressItem) {
this.progressItems.update(items => {
for (let i = 0; i < items.length; i++) {
if (items[i]!.patternId === item.patternId) {
items[i] = item
return items
}
}
items.push(item)
return items
})
}
allViewablePatterns = derived(this.admin,
$admin => $admin ? sprachdex.patternsIncludingDrafts : sprachdex.publishedPatterns)
progressItemByPatternId = derived(this.progressItems,
$progressItems => _.keyBy($progressItems, (p) => p.patternId))
patternsAndProgress = derived(
[this.allViewablePatterns, this.progressItemByPatternId],
([$allViewablePatterns, $progressItemByPatternId]) => {
return $allViewablePatterns.map(pattern => {
return Object.assign({}, pattern, {
progress: new PatternProgress(pattern, $progressItemByPatternId[pattern.id])
})
})
}
)
patternAndProgressById = derived(this.patternsAndProgress,
$patternsAndProgress => _.keyBy($patternsAndProgress, (p) => p.id))
nextPatternToLearn = derived(this.patternsAndProgress, $patternsAndProgress => {
return $patternsAndProgress.find((p) => p.progress.srsLevel === 0) as PatternAndProgress | undefined
})
/** All patterns for which the user has completed at least level 1 */
learnedPatterns = derived(this.patternsAndProgress, $patternsAndProgress => {
return $patternsAndProgress.filter(p => p.progress.srsLevel > 0) as PatternAndProgress[]
})
/**
* All learned patterns which are ready for levelup.
* Ordered by previous review time, so the patterns you haven't
* reviewed for the longest come first.
*/
patternsReadyToLevel = derived(this.learnedPatterns, $learnedPatterns => {
const patterns = $learnedPatterns.filter(p => p.progress.levelableAt && p.progress.levelableAt <= Date.now())
return _.sortBy(patterns, p => p.progress.levelableAt)
})
nextLevelablePattern = derived(this.learnedPatterns, $learnedPatterns => {
const patterns = _.sortBy($learnedPatterns, p => p.progress.levelableAt)
return patterns[0]
})
/** Get reviews for all learned patterns, regardless of levelup availability */
allReviews = derived(this.learnedPatterns, $learnedPatterns => {
let reviews: Review[] = []
for (const pattern of $learnedPatterns) {
for (const exercise of pattern.exercises) {
reviews.push(Object.assign({}, exercise, { pattern }))
}
}
return reviews
})
/** Get reviews from patterns ready to level */
reviewsForLeveling = derived(this.patternsReadyToLevel, $patternsReadyToLevel => {
let reviews: Review[] = []
for (const pattern of $patternsReadyToLevel) {
for (const exercise of pattern.exercises) {
reviews.push(Object.assign({}, exercise, { pattern }))
}
}
return reviews
})
}
class PatternProgress {
constructor(readonly pattern: Pattern, readonly item?: ProgressItem) { }
get srsLevel() {
return this.item?.srsLevel || 0
}
get mastered() {
return this.srsLevel >= this.pattern.maxLevel
}
get levelableAt(): number | null {
if (this.mastered) {
return null
} else if (this.item) {
return this.item.lastLeveledAt + time.toNextSRSLevel(this.item.srsLevel)
} else {
return Date.now()
}
}
get readyToLevel(): boolean {
return !!(this.levelableAt && this.levelableAt <= Date.now())
}
get completedLevels() {
const levels: number[] = []
for (let i = 0; i < this.srsLevel; i++) {
levels.push(i)
}
return levels
}
}
export type PatternAndProgress = Pattern & { progress: PatternProgress }