@01-edu/shared 1.0.0

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/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # 01-Edu shared library
2
+
3
+ ## `check-definitions` command
4
+
5
+
package/event-utils.js ADDED
@@ -0,0 +1,58 @@
1
+ export const SEC = 1000
2
+ export const MIN = 60 * SEC
3
+ export const HOUR = 60 * MIN
4
+ export const DAY = 24 * HOUR
5
+ export const WEEK = 7 * DAY
6
+
7
+ export const numTime = date => new Date(date).getTime()
8
+ export const numToISOString = number =>
9
+ number ? new Date(number).toISOString() : Number.NaN
10
+ export const getChildRegistrationStartAt = (parentEventStartAt, child) => {
11
+ const {
12
+ startAfter = 0,
13
+ eventStartDelay = 0,
14
+ registrationDuration,
15
+ } = child.attrs
16
+ return (
17
+ numTime(parentEventStartAt) +
18
+ (startAfter - (eventStartDelay + registrationDuration)) * MIN
19
+ )
20
+ }
21
+ export const getChildRegistrationEndAt = (parentEventStartAt, child) => {
22
+ const { startAfter = 0, eventStartDelay = 0 } = child.attrs
23
+ return numTime(parentEventStartAt) + (startAfter - eventStartDelay) * MIN
24
+ }
25
+ export const getChildEventStartAt = (parentEventStartAt, { attrs }) =>
26
+ numTime(parentEventStartAt) + (attrs.startAfter || 0) * MIN
27
+
28
+ export const getChildEventEndAt = (parentEventStartAt, child) => {
29
+ const { startAfter = 0, eventDuration } = child.attrs
30
+ return numTime(parentEventStartAt) + (startAfter + eventDuration) * MIN
31
+ }
32
+
33
+ export const getChildEventInfo = (parentEventStartAt, child) => ({
34
+ startAt: numToISOString(
35
+ getChildRegistrationStartAt(parentEventStartAt, child),
36
+ ),
37
+ endAt: numToISOString(getChildRegistrationEndAt(parentEventStartAt, child)),
38
+ eventJoinedAt: numToISOString(
39
+ getChildEventStartAt(parentEventStartAt, child),
40
+ ),
41
+ eventEndAt: numToISOString(getChildEventEndAt(parentEventStartAt, child)),
42
+ })
43
+
44
+ // NB: useful when creating event for first time (add event)
45
+ export const getEndOfEvent = ({ eventJoinedAt }, object) =>
46
+ numTime(eventJoinedAt) + object.attrs.eventDuration * MIN
47
+
48
+ // TODO: rename this fn - also used for project
49
+ // the best would be to find a word to label the child items that are not events
50
+ export const getQuestStartAt = (parentEventStartAt, quest) =>
51
+ numTime(parentEventStartAt) + quest.attrs.delay
52
+ export const getQuestExtraEndAt = (questStartAt, quest) => {
53
+ const { scopeExtraDuration, duration } = quest.attrs
54
+ return questStartAt + ((scopeExtraDuration || 0) + (duration || 1)) * DAY
55
+ }
56
+
57
+ // TODO: rename isPassed
58
+ export const isFinished = date => new Date(date).getTime() < Date.now()
@@ -0,0 +1,255 @@
1
+ /**
2
+ * lodash (Custom Build) <https://lodash.com/>
3
+ * Build: `lodash modularize exports="npm" -o ./`
4
+ * Copyright jQuery Foundation and other contributors <https://jquery.org/>
5
+ * Released under MIT license <https://lodash.com/license>
6
+ * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
7
+ * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
8
+ */
9
+
10
+ /** Used as references for various `Number` constants. */
11
+ var INFINITY = 1 / 0;
12
+
13
+ /** `Object#toString` result references. */
14
+ var symbolTag = '[object Symbol]';
15
+
16
+ /** Used to match Latin Unicode letters (excluding mathematical operators). */
17
+ var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g;
18
+
19
+ /** Used to compose unicode character classes. */
20
+ var rsComboMarksRange = '\\u0300-\\u036f\\ufe20-\\ufe23',
21
+ rsComboSymbolsRange = '\\u20d0-\\u20f0';
22
+
23
+ /** Used to compose unicode capture groups. */
24
+ var rsCombo = '[' + rsComboMarksRange + rsComboSymbolsRange + ']';
25
+
26
+ /**
27
+ * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and
28
+ * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols).
29
+ */
30
+ var reComboMark = RegExp(rsCombo, 'g');
31
+
32
+ /** Used to map Latin Unicode letters to basic Latin letters. */
33
+ var deburredLetters = {
34
+ // Latin-1 Supplement block.
35
+ '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A',
36
+ '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a',
37
+ '\xc7': 'C', '\xe7': 'c',
38
+ '\xd0': 'D', '\xf0': 'd',
39
+ '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E',
40
+ '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e',
41
+ '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I',
42
+ '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i',
43
+ '\xd1': 'N', '\xf1': 'n',
44
+ '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O',
45
+ '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o',
46
+ '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U',
47
+ '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u',
48
+ '\xdd': 'Y', '\xfd': 'y', '\xff': 'y',
49
+ '\xc6': 'Ae', '\xe6': 'ae',
50
+ '\xde': 'Th', '\xfe': 'th',
51
+ '\xdf': 'ss',
52
+ // Latin Extended-A block.
53
+ '\u0100': 'A', '\u0102': 'A', '\u0104': 'A',
54
+ '\u0101': 'a', '\u0103': 'a', '\u0105': 'a',
55
+ '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C',
56
+ '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c',
57
+ '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd',
58
+ '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E',
59
+ '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e',
60
+ '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G',
61
+ '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g',
62
+ '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h',
63
+ '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I',
64
+ '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i',
65
+ '\u0134': 'J', '\u0135': 'j',
66
+ '\u0136': 'K', '\u0137': 'k', '\u0138': 'k',
67
+ '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L',
68
+ '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l',
69
+ '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N',
70
+ '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n',
71
+ '\u014c': 'O', '\u014e': 'O', '\u0150': 'O',
72
+ '\u014d': 'o', '\u014f': 'o', '\u0151': 'o',
73
+ '\u0154': 'R', '\u0156': 'R', '\u0158': 'R',
74
+ '\u0155': 'r', '\u0157': 'r', '\u0159': 'r',
75
+ '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S',
76
+ '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's',
77
+ '\u0162': 'T', '\u0164': 'T', '\u0166': 'T',
78
+ '\u0163': 't', '\u0165': 't', '\u0167': 't',
79
+ '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U',
80
+ '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u',
81
+ '\u0174': 'W', '\u0175': 'w',
82
+ '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y',
83
+ '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z',
84
+ '\u017a': 'z', '\u017c': 'z', '\u017e': 'z',
85
+ '\u0132': 'IJ', '\u0133': 'ij',
86
+ '\u0152': 'Oe', '\u0153': 'oe',
87
+ '\u0149': "'n", '\u017f': 'ss'
88
+ };
89
+
90
+ /** Detect free variable `global` from Node.js. */
91
+ var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
92
+
93
+ /** Detect free variable `self`. */
94
+ var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
95
+
96
+ /** Used as a reference to the global object. */
97
+ var root = freeGlobal || freeSelf || Function('return this')();
98
+
99
+ /**
100
+ * The base implementation of `_.propertyOf` without support for deep paths.
101
+ *
102
+ * @private
103
+ * @param {Object} object The object to query.
104
+ * @returns {Function} Returns the new accessor function.
105
+ */
106
+ function basePropertyOf(object) {
107
+ return function(key) {
108
+ return object == null ? undefined : object[key];
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A
114
+ * letters to basic Latin letters.
115
+ *
116
+ * @private
117
+ * @param {string} letter The matched letter to deburr.
118
+ * @returns {string} Returns the deburred letter.
119
+ */
120
+ var deburrLetter = basePropertyOf(deburredLetters);
121
+
122
+ /** Used for built-in method references. */
123
+ var objectProto = Object.prototype;
124
+
125
+ /**
126
+ * Used to resolve the
127
+ * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
128
+ * of values.
129
+ */
130
+ var objectToString = objectProto.toString;
131
+
132
+ /** Built-in value references. */
133
+ var Symbol = root.Symbol;
134
+
135
+ /** Used to convert symbols to primitives and strings. */
136
+ var symbolProto = Symbol ? Symbol.prototype : undefined,
137
+ symbolToString = symbolProto ? symbolProto.toString : undefined;
138
+
139
+ /**
140
+ * The base implementation of `_.toString` which doesn't convert nullish
141
+ * values to empty strings.
142
+ *
143
+ * @private
144
+ * @param {*} value The value to process.
145
+ * @returns {string} Returns the string.
146
+ */
147
+ function baseToString(value) {
148
+ // Exit early for strings to avoid a performance hit in some environments.
149
+ if (typeof value == 'string') {
150
+ return value;
151
+ }
152
+ if (isSymbol(value)) {
153
+ return symbolToString ? symbolToString.call(value) : '';
154
+ }
155
+ var result = (value + '');
156
+ return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
157
+ }
158
+
159
+ /**
160
+ * Checks if `value` is object-like. A value is object-like if it's not `null`
161
+ * and has a `typeof` result of "object".
162
+ *
163
+ * @static
164
+ * @memberOf _
165
+ * @since 4.0.0
166
+ * @category Lang
167
+ * @param {*} value The value to check.
168
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
169
+ * @example
170
+ *
171
+ * _.isObjectLike({});
172
+ * // => true
173
+ *
174
+ * _.isObjectLike([1, 2, 3]);
175
+ * // => true
176
+ *
177
+ * _.isObjectLike(_.noop);
178
+ * // => false
179
+ *
180
+ * _.isObjectLike(null);
181
+ * // => false
182
+ */
183
+ function isObjectLike(value) {
184
+ return !!value && typeof value == 'object';
185
+ }
186
+
187
+ /**
188
+ * Checks if `value` is classified as a `Symbol` primitive or object.
189
+ *
190
+ * @static
191
+ * @memberOf _
192
+ * @since 4.0.0
193
+ * @category Lang
194
+ * @param {*} value The value to check.
195
+ * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
196
+ * @example
197
+ *
198
+ * _.isSymbol(Symbol.iterator);
199
+ * // => true
200
+ *
201
+ * _.isSymbol('abc');
202
+ * // => false
203
+ */
204
+ function isSymbol(value) {
205
+ return typeof value == 'symbol' ||
206
+ (isObjectLike(value) && objectToString.call(value) == symbolTag);
207
+ }
208
+
209
+ /**
210
+ * Converts `value` to a string. An empty string is returned for `null`
211
+ * and `undefined` values. The sign of `-0` is preserved.
212
+ *
213
+ * @static
214
+ * @memberOf _
215
+ * @since 4.0.0
216
+ * @category Lang
217
+ * @param {*} value The value to process.
218
+ * @returns {string} Returns the string.
219
+ * @example
220
+ *
221
+ * _.toString(null);
222
+ * // => ''
223
+ *
224
+ * _.toString(-0);
225
+ * // => '-0'
226
+ *
227
+ * _.toString([1, 2, 3]);
228
+ * // => '1,2,3'
229
+ */
230
+ function toString(value) {
231
+ return value == null ? '' : baseToString(value);
232
+ }
233
+
234
+ /**
235
+ * Deburrs `string` by converting
236
+ * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table)
237
+ * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A)
238
+ * letters to basic Latin letters and removing
239
+ * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks).
240
+ *
241
+ * @static
242
+ * @memberOf _
243
+ * @since 3.0.0
244
+ * @category String
245
+ * @param {string} [string=''] The string to deburr.
246
+ * @returns {string} Returns the deburred string.
247
+ * @example
248
+ *
249
+ * _.deburr('déjà vu');
250
+ * // => 'deja vu'
251
+ */
252
+ export function deburr(string) {
253
+ string = toString(string);
254
+ return string && string.replace(reLatin, deburrLetter).replace(reComboMark, '');
255
+ }
package/onboarding.js ADDED
@@ -0,0 +1,38 @@
1
+ export const onboardingTypes = new Set([
2
+ 'onboarding',
3
+ 'piscine-registration',
4
+ 'interview',
5
+ 'games',
6
+ 'administration',
7
+ 'module-registration',
8
+ 'form-step',
9
+ 'sign-step',
10
+ 'upload-step',
11
+ 'contact-validation-step',
12
+ ])
13
+
14
+ export const prevValidated = (key, object) => {
15
+ const { prev } = (
16
+ object.parent.type !== 'onboarding' ? object : object.parent
17
+ ).children[key]
18
+ if (!prev) return true
19
+ return prev.attrs.status === 'succeeded'
20
+ }
21
+
22
+ export const nextStep = (key, object) => {
23
+ const { next } = (object.type === 'onboarding' ? object : object.parent)
24
+ .children[key]
25
+
26
+ return next ? next.path : undefined
27
+ }
28
+
29
+ export const getOnboardingStep = onboarding => {
30
+ if (!onboarding) return undefined
31
+ const children = Object.values(onboarding.children)
32
+
33
+ return (
34
+ children.find(
35
+ ({ attrs }) => attrs.status === 'available' || attrs.status === 'failed',
36
+ ) || children[children.length - 1]
37
+ )
38
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@01-edu/shared",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "files": [
10
+ "./event-utils.js",
11
+ "./onboarding.js",
12
+ "./path.js",
13
+ "./programming-languages.js",
14
+ "./score.js",
15
+ "./skill-definitions.js",
16
+ "./toolbox.js",
17
+ "./event-utils.js",
18
+ "./lodash.deburr.js",
19
+ "./onboarding.js"
20
+ ],
21
+ "license": "MIT",
22
+ "bin": {
23
+ "check-definitions": "shared/check.js"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/01-edu/all.git"
28
+ }
29
+ }
package/path.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @returns {string} the relative path converted to absolute
3
+ * @argument relativePath {string} a relative path
4
+ * @argument currentPath {string} the current absolute path
5
+ */
6
+ export const getAbsolutePath = (relativePath, currentPath) => {
7
+ if (isAbsolutePath(relativePath)) {
8
+ throw new Error("'relativePath' must be a relative path")
9
+ }
10
+ // Ignore trailing `/` in the relative path
11
+ if (relativePath[relativePath.length - 1] === '/') {
12
+ relativePath = relativePath.slice(0, -1)
13
+ }
14
+ if (!isAbsolutePath(currentPath)) {
15
+ throw Error('the currentPath has to be an absolute path')
16
+ }
17
+ const absSegments = currentPath.split('/')
18
+ const pathSegments = relativePath.split('/')
19
+ for (const segment of pathSegments) {
20
+ if (segment === '..') {
21
+ if (absSegments.length > 0) {
22
+ absSegments.pop()
23
+ } else {
24
+ throw Error(
25
+ `Incorrect relative path = ${relativePath}, current path = ${currentPath}`,
26
+ )
27
+ }
28
+ } else if (segment !== '.') {
29
+ absSegments.push(segment)
30
+ }
31
+ }
32
+ return absSegments.join('/')
33
+ }
34
+
35
+ const walk = (object, segment) => {
36
+ if (!object) return
37
+ if (segment === '.') return object
38
+ return segment === '..' ? object.parent : object.children[segment]
39
+ }
40
+
41
+ /**
42
+ * @returns {Object} the object in the relative path
43
+ * @argument relativePath {string} a relative path
44
+ * @argument currentObj {Object} the object from where the relative path starts
45
+ */
46
+ export const getObjectFromRelativePath = (relativePath, currentObj) => {
47
+ const currentPath = currentObj.path
48
+ if (isAbsolutePath(relativePath)) {
49
+ throw Error("'relativePath' must be a relative path")
50
+ }
51
+ // Ignore trailing `/` in the relative path
52
+ if (relativePath[relativePath.length - 1] === '/') {
53
+ relativePath = relativePath.slice(0, -1)
54
+ }
55
+
56
+ const matchingObject = relativePath.split('/').reduce(walk, currentObj)
57
+ if (matchingObject) return matchingObject
58
+ throw Error(
59
+ `Incorrect relative path '${relativePath}': no object found — current path = '${currentPath}'`,
60
+ )
61
+ }
62
+
63
+ const isAbsolutePath = path => path.startsWith('/')
@@ -0,0 +1,27 @@
1
+ // because of the nature of esbuild we have to put the import string hard coded
2
+ export const langTypeInfo = {
3
+ go: {
4
+ legacy: true,
5
+ lang: 'go',
6
+ load: () => import('@codemirror/legacy-modes/mode/go'),
7
+ },
8
+ sh: {
9
+ legacy: true,
10
+ lang: 'shell',
11
+ load: () => import('@codemirror/legacy-modes/mode/shell'),
12
+ },
13
+ dart: {
14
+ lang: 'dart',
15
+ legacy: true,
16
+ load: () => import('@codemirror/legacy-modes/mode/clike'),
17
+ },
18
+ css: { lang: 'css', load: () => import('@codemirror/lang-css') },
19
+ html: { lang: 'html', load: () => import('@codemirror/lang-html') },
20
+ js: { lang: 'javascript', load: () => import('@codemirror/lang-javascript') },
21
+ md: { lang: 'markdown', load: () => import('@codemirror/lang-markdown') },
22
+ rs: { lang: 'rust', load: () => import('@codemirror/lang-rust') },
23
+ py: { lang: 'python', load: () => import('@codemirror/lang-python') },
24
+ java: { lang: 'java', load: () => import('@codemirror/lang-java') },
25
+ json: { lang: 'json', load: () => import('@codemirror/lang-json') },
26
+ }
27
+ export const supportedLang = new Set(Object.keys(langTypeInfo))
package/score.js ADDED
@@ -0,0 +1,80 @@
1
+ const levelPoints = 5
2
+ export const gamesScoring = {
3
+ memory: {
4
+ maxLevel: 22,
5
+ minRatio: 1,
6
+ freeAttempts: 1000,
7
+ maxAttempts: 1000,
8
+ shouldFailUnderScore: 30,
9
+ shouldFailUnderLevel: 7,
10
+ shouldSucceedFromScore: 60,
11
+ shouldSucceedFromLevel: 15,
12
+ },
13
+ zzle: {
14
+ maxLevel: 18,
15
+ minRatio: 1,
16
+ freeAttempts: 1000,
17
+ maxAttempts: 1000,
18
+ shouldFailUnderScore: 20,
19
+ shouldFailUnderLevel: 3,
20
+ shouldSucceedFromScore: 50,
21
+ shouldSucceedFromLevel: 10,
22
+ },
23
+ }
24
+
25
+ for (const game of Object.values(gamesScoring)) {
26
+ game.levels = [...Array(game.maxLevel + 50).keys()].map(index => ({
27
+ maxScore: ((index + 1) / 15 + 1) * levelPoints,
28
+ }))
29
+ game.maxScore = game.levels
30
+ .slice(0, game.maxLevel)
31
+ .reduce((acc, level) => acc + level.maxScore, 0)
32
+ }
33
+
34
+ const projectValue = (count, max, minRatio, free) => {
35
+ const dist = 1 - minRatio
36
+ const diff = max - (count - free)
37
+
38
+ if (count <= free || minRatio === 1) return 1
39
+ if (diff < 1) return minRatio
40
+ return (diff / max) * dist + minRatio
41
+ }
42
+
43
+ const getLevelRatio = (gameName, levelAttempts) => {
44
+ const { minRatio, freeAttempts, maxAttempts } = gamesScoring[gameName]
45
+
46
+ return projectValue(levelAttempts, maxAttempts, minRatio, freeAttempts)
47
+ }
48
+
49
+ export const getLevelScore = (gameName, levelAttempts, levelNumber) =>
50
+ // TODO: reduce level score differences
51
+ (levelNumber / 15 + 1) * levelPoints * getLevelRatio(gameName, levelAttempts)
52
+
53
+ export const getLevelMaxPoints = (levelIndex, gameName) =>
54
+ gamesScoring[gameName].levels[levelIndex].maxScore
55
+
56
+ export const getLevelsScore = (gameName, results) =>
57
+ results
58
+ .map((level, levelIndex) =>
59
+ getLevelScore(gameName, level.attempts, levelIndex + 1),
60
+ )
61
+ .reduce((a, b) => a + b, 0)
62
+
63
+ export const getGameScores = (gameName, gamePoints, results) => {
64
+ const totalLevelsScore = getLevelsScore(gameName, results)
65
+ const ratio = totalLevelsScore / gamesScoring[gameName].maxScore
66
+
67
+ return {
68
+ ratio,
69
+ percent: ratio * 100, // TODO: represent the percentage of level done
70
+ points: gamePoints * ratio,
71
+ }
72
+ }
73
+
74
+ export const getGlobalScore = games => {
75
+ const totalScoreInPercent = games
76
+ .map(game => getGameScores(game.name, game.points, game.results).percent)
77
+ .reduce((a, b) => a + b, 0)
78
+
79
+ return totalScoreInPercent / games.length
80
+ }
@@ -0,0 +1,272 @@
1
+ export const skillsSet = {
2
+ // technical skills
3
+ prog: {
4
+ name: 'Elementary programming',
5
+ description: 'Basics of computer programming',
6
+ type: 'technical',
7
+ },
8
+ algo: {
9
+ name: 'Elementary algorithms',
10
+ description: 'Problem-solving, algorithm design',
11
+ type: 'technical',
12
+ },
13
+ 'sys-admin': {
14
+ name: 'System administration',
15
+ description: 'System administration, dev ops',
16
+ type: 'technical',
17
+ },
18
+ 'front-end': {
19
+ name: 'Front-end',
20
+ description: 'Front-end technologies',
21
+ type: 'technical',
22
+ },
23
+ 'back-end': {
24
+ name: 'Back-end',
25
+ description: 'Back-end technologies',
26
+ type: 'technical',
27
+ },
28
+ stats: {
29
+ name: 'Statistics',
30
+ description: 'Data analysis, interpretation',
31
+ type: 'technical',
32
+ },
33
+ ai: {
34
+ name: 'AI',
35
+ description: 'Artificial intelligence, machine learning',
36
+ type: 'technical',
37
+ },
38
+ game: {
39
+ name: 'Game programming',
40
+ description: 'Game programming',
41
+ type: 'technical',
42
+ },
43
+ blockchain: {
44
+ name: 'Blockchain',
45
+ description: 'Blockchain',
46
+ type: 'technical',
47
+ },
48
+ mobile: {
49
+ name: 'Mobile development',
50
+ description: 'Mobile development',
51
+ type: 'technical',
52
+ },
53
+ tcp: {
54
+ name: 'TCP/IP',
55
+ description: 'TCP/IP',
56
+ type: 'technical',
57
+ },
58
+ cybersecurity: {
59
+ name: 'Cybersecurity',
60
+ description: 'Cybersecurity',
61
+ type: 'technical',
62
+ },
63
+ ux: {
64
+ name: 'UX/UI',
65
+ description: 'User experience design, user interface design',
66
+ type: 'technical',
67
+ },
68
+ cloud: {
69
+ name: 'Cloud',
70
+ description: 'Cloud infra, deployment and scaling',
71
+ type: 'technical',
72
+ },
73
+ automation: {
74
+ name: 'Automation',
75
+ description:
76
+ 'Automate tasks to reduce repetitive tasks and minimize potential errors',
77
+ type: 'technical',
78
+ },
79
+ ci: {
80
+ name: 'CI/CD',
81
+ description:
82
+ 'Set up CI/CD pipelines to maintain a consistent software release process',
83
+ type: 'technical',
84
+ },
85
+ testing: {
86
+ name: 'Testing',
87
+ description: 'TDD and other software testing strategies',
88
+ type: 'technical',
89
+ },
90
+ 'curriculum-objectives-completed': {
91
+ name: 'Curriculum objectives completed',
92
+ description:
93
+ 'Curriculum objectives completed to unlock the specialization branches',
94
+ type: 'technical',
95
+ },
96
+
97
+ // personal skills
98
+ // comm: {
99
+ // name: 'Communication',
100
+ // description: 'Communication, presentation, oral skills',
101
+ // type: 'personal',
102
+ // },
103
+ // team: {
104
+ // name: 'Team spirit',
105
+ // description: 'Ability to work with people',
106
+ // type: 'personal',
107
+ // },
108
+ // lead: {
109
+ // name: 'Leadership',
110
+ // description: 'Lead-taking, decision making',
111
+ // type: 'personal',
112
+ // },
113
+ // pro: {
114
+ // name: 'Professionalism',
115
+ // description: 'Responsible, reliable, on time',
116
+ // type: 'personal',
117
+ // },
118
+ // orga: {
119
+ // name: 'Organisation',
120
+ // description: 'Planning, time management',
121
+ // type: 'personal',
122
+ // },
123
+ // implication: {
124
+ // name: 'Implication',
125
+ // description: 'Motivated, involved in projects',
126
+ // type: 'personal',
127
+ // },
128
+ // initiative: {
129
+ // name: 'Self initiative',
130
+ // description: 'Autonomous, proactive',
131
+ // type: 'personal',
132
+ // },
133
+ // crea: {
134
+ // name: 'Creativity',
135
+ // description: 'Curiousity, inquisitiveness',
136
+ // type: 'personal',
137
+ // },
138
+ // precision: {
139
+ // name: 'Precision',
140
+ // description: 'Detail-oriented, clear code-writing',
141
+ // type: 'personal',
142
+ // },
143
+
144
+ // technologies
145
+ go: {
146
+ name: 'Go',
147
+ type: 'technology',
148
+ },
149
+ js: {
150
+ name: 'JS',
151
+ type: 'technology',
152
+ },
153
+ rust: {
154
+ name: 'Rust',
155
+ type: 'technology',
156
+ },
157
+ java: {
158
+ name: 'Java',
159
+ type: 'technology',
160
+ },
161
+ html: {
162
+ name: 'HTML',
163
+ type: 'technology',
164
+ },
165
+ css: {
166
+ name: 'CSS',
167
+ type: 'technology',
168
+ },
169
+ unix: {
170
+ name: 'Unix',
171
+ type: 'technology',
172
+ },
173
+ docker: {
174
+ name: 'Docker',
175
+ type: 'technology',
176
+ },
177
+ sql: {
178
+ name: 'SQL',
179
+ type: 'technology',
180
+ },
181
+ 'no-sql': {
182
+ name: 'Non-relational Databases',
183
+ type: 'technology',
184
+ },
185
+ c: {
186
+ name: 'C',
187
+ type: 'technology',
188
+ },
189
+ sh: {
190
+ name: 'Shell',
191
+ type: 'technology',
192
+ },
193
+ php: {
194
+ name: 'PHP',
195
+ type: 'technology',
196
+ },
197
+ python: {
198
+ name: 'Python',
199
+ type: 'technology',
200
+ },
201
+ ruby: {
202
+ name: 'Ruby',
203
+ type: 'technology',
204
+ },
205
+ 'c-sharp': {
206
+ name: 'C#',
207
+ type: 'technology',
208
+ },
209
+ 'c-pp': {
210
+ name: 'C++',
211
+ type: 'technology',
212
+ },
213
+ graphql: {
214
+ name: 'GraphQL',
215
+ type: 'technology',
216
+ },
217
+ rails: {
218
+ name: 'Ruby on Rails',
219
+ type: 'technology',
220
+ },
221
+ laravel: {
222
+ name: 'Laravel',
223
+ type: 'technology',
224
+ },
225
+ django: {
226
+ name: 'Django',
227
+ type: 'technology',
228
+ },
229
+ react: {
230
+ name: 'React',
231
+ type: 'technology',
232
+ },
233
+ dotnet: {
234
+ name: '.NET',
235
+ type: 'technology',
236
+ },
237
+ electron: {
238
+ name: 'Electron',
239
+ type: 'technology',
240
+ },
241
+ git: {
242
+ name: 'Git',
243
+ type: 'technology',
244
+ },
245
+ }
246
+
247
+ export const skillTypes = [
248
+ {
249
+ type: 'technical',
250
+ name: 'Technical skills',
251
+ description:
252
+ 'Technical skills are based on notions that are taught during the curriculum ; they represent what you learned from a project. They can be acquired when completing piscines, projects & exams.',
253
+ },
254
+ // {
255
+ // type: 'personal',
256
+ // name: 'Personal skills',
257
+ // description:
258
+ // 'Personal skills are based on feedback given by classmates that worked with you about your personal qualities & abilities ; they represent what people observed about you.',
259
+ // },
260
+ {
261
+ type: 'technology',
262
+ name: 'Technologies',
263
+ description:
264
+ 'Technologies gather the different programming languages, frameworks and platforms acquired from projects & piscines.',
265
+ },
266
+ ]
267
+
268
+ export const hasRequiredSkills = (requiredSkills, userSkills) =>
269
+ Object.entries(requiredSkills || {}).every(
270
+ ([skillName, requiredAmount]) =>
271
+ (userSkills?.[skillName] || 0) >= requiredAmount,
272
+ )
package/toolbox.js ADDED
@@ -0,0 +1,483 @@
1
+ import { isFinished } from './event-utils.js'
2
+ import { deburr } from './lodash.deburr.js'
3
+ import { onboardingTypes } from './onboarding.js'
4
+
5
+ const keyCodes = {
6
+ 13: ({ save, value, event, allowLineBreak }) => {
7
+ if (event.shiftKey || allowLineBreak) return false
8
+ save(value)
9
+ }, // enter
10
+ 27: ({ set, originalValue }) => set(originalValue), // esc
11
+ 83: ({ save, value, event }) =>
12
+ event.metaKey || event.ctrlKey ? save(value) : false, // cmd+s
13
+ }
14
+
15
+ export const contentObjects = new Set([
16
+ 'module',
17
+ 'piscine',
18
+ 'exam',
19
+ 'raid',
20
+ 'quest',
21
+ 'exercise',
22
+ 'project',
23
+ 'signup',
24
+ ])
25
+
26
+ export const objectTypes = new Set([...contentObjects, ...onboardingTypes])
27
+
28
+ export const childTypes = {
29
+ campus: ['signup', 'onboarding', 'piscine', 'module'],
30
+ signup: ['form-step', 'sign-step', 'upload-step', 'contact-validation-step'],
31
+ onboarding: [
32
+ 'games',
33
+ 'administration',
34
+ 'interview',
35
+ 'piscine-registration',
36
+ 'module-registration',
37
+ ],
38
+ administration: [
39
+ 'form-step',
40
+ 'sign-step',
41
+ 'upload-step',
42
+ 'contact-validation-step',
43
+ ],
44
+ piscine: ['quest', 'exam', 'raid', 'project'],
45
+ exam: ['exercise'],
46
+ quest: ['exercise'],
47
+ module: ['project', 'piscine', 'exam'],
48
+ }
49
+
50
+ export const handleKeyDownEvent = args => {
51
+ const handler = keyCodes[args.event.keyCode]
52
+
53
+ if (handler) {
54
+ if (handler(args) !== false) {
55
+ args.event.preventDefault()
56
+ }
57
+ }
58
+ }
59
+
60
+ export const last = array => array[array.length - 1]
61
+
62
+ export const upperFirst = str =>
63
+ str ? str[0].toUpperCase() + str.slice(1) : ''
64
+
65
+ export const replaceMap = (arr, elem, mapper) =>
66
+ arr.map(e => (e === elem ? mapper(e) : e))
67
+
68
+ // DATE & TIME
69
+
70
+ export const SEC = 1000
71
+ export const MIN = 60 * SEC
72
+ export const HOUR = 60 * MIN
73
+ export const DAY = 24 * HOUR
74
+ export const WEEK = 7 * DAY
75
+ export const YEAR = 365 * DAY
76
+
77
+ const unitsOfTime = Object.entries({
78
+ year: YEAR,
79
+ month: 2628000000,
80
+ day: DAY,
81
+ hour: HOUR,
82
+ minute: MIN,
83
+ second: SEC,
84
+ })
85
+
86
+ export const units = {
87
+ years: YEAR,
88
+ year: YEAR,
89
+ yrs: YEAR,
90
+ yr: YEAR,
91
+ y: YEAR,
92
+
93
+ weeks: WEEK,
94
+ week: WEEK,
95
+ w: WEEK,
96
+
97
+ days: DAY,
98
+ day: DAY,
99
+ d: DAY,
100
+
101
+ hours: HOUR,
102
+ hour: HOUR,
103
+ hrs: HOUR,
104
+ hr: HOUR,
105
+ h: HOUR,
106
+
107
+ minutes: MIN,
108
+ minute: MIN,
109
+ mins: MIN,
110
+ min: MIN,
111
+ m: MIN,
112
+
113
+ seconds: SEC,
114
+ second: SEC,
115
+ secs: SEC,
116
+ sec: SEC,
117
+ s: SEC,
118
+
119
+ milliseconds: 1,
120
+ millisecond: 1,
121
+ ms: 1,
122
+ }
123
+
124
+ const rtf = new Intl.RelativeTimeFormat('en', { style: 'narrow' })
125
+ export const relativeTime = elapsed => {
126
+ for (const [unit, amount] of unitsOfTime) {
127
+ if (Math.abs(elapsed) > amount || unit === 'second') {
128
+ return rtf.format(Math.round(elapsed / amount), unit)
129
+ }
130
+ }
131
+ }
132
+
133
+ export const padStart0 = s => s.toString().padStart(2, '0')
134
+
135
+ export const timeToMS = str => {
136
+ if (typeof str !== 'string') return 0
137
+ str = str.toLowerCase()
138
+ const matcher = /(?<value>[0-9.-]+) *(?<type>[a-z]+)/g
139
+ let match
140
+ let result = 0
141
+ while ((match = matcher.exec(str))) {
142
+ const { type, value } = match.groups
143
+ const unit = units[type]
144
+ const n = Number(value)
145
+ n && unit && (result += value * unit)
146
+ }
147
+ return Math.round(result)
148
+ }
149
+
150
+ // TODO: formated should be written formatted
151
+ export const formatedDurationColons = (seconds, { hoursOnly } = {}) => {
152
+ const d = !hoursOnly && Math.floor(seconds / 86400)
153
+ const h = Math.floor((!hoursOnly ? seconds % 86400 : seconds) / 3600)
154
+ const min = Math.floor((seconds % 3600) / 60)
155
+ const sec = Math.floor(seconds % 60)
156
+ const time = `${padStart0(h)}:${padStart0(min)}:${padStart0(sec)}`
157
+ return d ? `${d}d ${time}` : time
158
+ }
159
+
160
+ export const formatedMS = ms => formatedDuration(Math.round(ms / SEC))
161
+ export const timeUnits = [
162
+ { label: 'y', size: 31536000 },
163
+ { label: 'w', size: 604800 },
164
+ { label: 'd', size: 86400 },
165
+ { label: 'h', size: 3600 },
166
+ { label: 'm', size: 60 },
167
+ { label: 's', size: 1 },
168
+ ]
169
+ export const formatedDuration = (seconds, { noSeconds } = {}) => {
170
+ const parts = []
171
+ for (const { label, size } of timeUnits) {
172
+ const value = Math.floor(seconds / size)
173
+ if (value !== 0) {
174
+ parts.push(`${value}${label}`)
175
+ seconds -= value * size
176
+ }
177
+ }
178
+ if (!noSeconds && seconds !== 0) {
179
+ parts.push(`${seconds}s`)
180
+ }
181
+ return parts.join(' ') || (noSeconds ? '0m' : '0s')
182
+ }
183
+
184
+ const toDate = date => (date instanceof Date ? date : new Date(date))
185
+ export const toDateFormat = d => {
186
+ const date = toDate(d)
187
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
188
+ const day = date.getDate().toString().padStart(2, '0')
189
+
190
+ return `${date.getFullYear()}-${month}-${day}`
191
+ }
192
+
193
+ export const toISOStringWithTimeZone = d => toDate(d).toISOString().slice(0, -1)
194
+
195
+ export const toDateFormatWithTime = (d, separator = 'T') => {
196
+ const date = toDate(d)
197
+ const hh = String(date.getHours()).padStart(2, '0')
198
+ const mm = String(date.getMinutes()).padStart(2, '0')
199
+
200
+ return `${toDateFormat(date)}${separator}${hh}:${mm}`
201
+ }
202
+ export const displayDateTime = dateTime => toDateFormatWithTime(dateTime, ' | ')
203
+
204
+ export const getMonthName = month => monthNames[month]
205
+
206
+ export const getDayName = day => weekDays[day]
207
+
208
+ export const monthNames = [
209
+ 'Jan',
210
+ 'Feb',
211
+ 'Mar',
212
+ 'Apr',
213
+ 'May',
214
+ 'Jun',
215
+ 'Jul',
216
+ 'Aug',
217
+ 'Sep',
218
+ 'Oct',
219
+ 'Nov',
220
+ 'Dec',
221
+ ]
222
+
223
+ const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
224
+
225
+ const hour = hour => {
226
+ if (hour === 0 || hour === 12) return 12
227
+ return hour % 12
228
+ }
229
+
230
+ const minPad = min => String(min).padStart(2, '0')
231
+
232
+ const suffix = hour => {
233
+ if (hour === 0) return 'am'
234
+ if (hour === 12) return 'pm'
235
+ return hour < 12 ? 'am' : 'pm'
236
+ }
237
+
238
+ export const dateIsPermanent = endAt => {
239
+ const date =
240
+ endAt instanceof Date && !Number.isNaN(endAt.valueOf())
241
+ ? endAt
242
+ : new Date(endAt)
243
+
244
+ // 32503683600000 = year 3000 timestamp
245
+ return date.valueOf() > 32503683600000
246
+ }
247
+
248
+ const getDateElems = date => {
249
+ const d = new Date(date)
250
+ const day = d.getDay()
251
+ const jj = d.getDate()
252
+ const mm = d.getMonth()
253
+ const yyyy = d.getFullYear()
254
+ const hh = d.getHours()
255
+ const min = d.getMinutes()
256
+ return { day, jj, mm, yyyy, hh, min }
257
+ }
258
+
259
+ export const formatedDateTime = date => {
260
+ if (!date) return '-'
261
+ if (dateIsPermanent(date)) return '∞'
262
+ const { jj, mm, yyyy, hh, min } = getDateElems(date)
263
+
264
+ return `${monthNames[mm]} ${jj}, ${yyyy} at ${hour(hh)}:${minPad(
265
+ min,
266
+ )} ${suffix(hh)}`
267
+ }
268
+
269
+ export const formatedDateTimeWithDay = date => {
270
+ if (!date) return '-'
271
+ if (dateIsPermanent(date)) return '∞'
272
+ const { day, jj, mm, yyyy, hh, min } = getDateElems(date)
273
+
274
+ return `${getDayName(day)}, ${monthNames[mm]} ${jj}, ${yyyy} at ${hour(
275
+ hh,
276
+ )}:${minPad(min)} ${suffix(hh)}`
277
+ }
278
+
279
+ export const formatedDate = date => {
280
+ if (!date) return '-'
281
+ if (dateIsPermanent(date)) return '∞'
282
+ return toDateFormat(date)
283
+ }
284
+
285
+ export const defer = () => {
286
+ const result = {}
287
+ result.done = new Promise((resolve, reject) => {
288
+ result.resolve = resolve
289
+ result.reject = reject
290
+ })
291
+ return result
292
+ }
293
+
294
+ // slugify('A Test') => a-test
295
+ export const slugify = str =>
296
+ deburr(str)
297
+ .trim()
298
+ .toLowerCase()
299
+ .replace(/ /g, '-')
300
+ .replace(/([^a-zA-Z0-9._-]+)/, '')
301
+
302
+ export const fullName = ({ firstName, lastName }) =>
303
+ `${firstName || ''} ${lastName || ''}`
304
+
305
+ export const formatTime = t =>
306
+ new Date(t)
307
+ .toISOString()
308
+ .substr(11, 8)
309
+ .replace(/^[0:]+/, '')
310
+
311
+ export const wait = delay => new Promise(resolve => setTimeout(resolve, delay))
312
+
313
+ const lowertalize = str => str[0].toLowerCase() + str.slice(1)
314
+ const upperFirstMatch = (_, firstMatch) => firstMatch.toUpperCase()
315
+ export const toCamelCase = str =>
316
+ lowertalize(
317
+ str
318
+ .trim()
319
+ .toLowerCase()
320
+ .replace(/[ _-]+(.?)/g, upperFirstMatch),
321
+ )
322
+ export const isNotCamel = str => /[ _-]+(.?)/g.test(str)
323
+
324
+ const underscoreJoin = (_, a, b) => `${a}_${b}`
325
+ const toSnakeCase = str =>
326
+ str
327
+ .trim()
328
+ .replace(/(.)([A-Z])/g, underscoreJoin)
329
+ .toLowerCase()
330
+
331
+ // seems to match all cases:
332
+ // 'FOO-BAR_01Chart ki' > f-o-o-b-a-r-01-chart-ki
333
+ // phant0m-writ3r > pass
334
+ // phoneValidation > phone-validation
335
+ // CHart01 > c-hart01
336
+ // addtionnal_info > addtionnal-info
337
+ // id card > id-card
338
+ // should be enough for unique usage we have of it in check-refs
339
+ // checked the perfs and it is faster + more secure than using replace(s) + toLowerCase
340
+ const toLowerCase = x => x.toLowerCase()
341
+ export const toKebabCase = str =>
342
+ str
343
+ .match(
344
+ /[A-Z](?=[A-Z][a-z]+[0-9]*[a-z]|\b)|[A-Z]?[a-z]+[0-9]+[a-z]?[A-Z]*|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g,
345
+ )
346
+ .map(toLowerCase)
347
+ .join('-')
348
+
349
+ const spaceJoin = (_, a, b) => `${a} ${b}`
350
+ export const breath = str =>
351
+ str
352
+ .trim()
353
+ .replace(/(.)([A-Z])/g, spaceJoin)
354
+ .toLowerCase()
355
+ export const prettyKey = key => upperFirst(breath(key))
356
+
357
+ const switchKeys = (object, switchCase, switchAll) =>
358
+ mapEntries(object, ([key, value]) => [switchCase(key), switchAll(value)])
359
+
360
+ export const toCamelKeys = object => switchKeys(object, toCamelCase, toCamelAll)
361
+ export const toSnakeKeys = object => switchKeys(object, toSnakeCase, toSnakeAll)
362
+
363
+ const switchAll = (elem, switchCase, switchKeys, toSwitchAll) => {
364
+ if (!elem || typeof elem !== 'object') return elem
365
+ return Array.isArray(elem)
366
+ ? elem.map(toSwitchAll)
367
+ : switchKeys(elem, switchCase, toSwitchAll)
368
+ }
369
+
370
+ export const toCamelAll = elem =>
371
+ switchAll(elem, toCamelCase, toCamelKeys, toCamelAll)
372
+
373
+ export const toSnakeAll = elem =>
374
+ switchAll(elem, toSnakeCase, toSnakeKeys, toSnakeAll)
375
+
376
+ export const arrayOf = (size, start = 0) =>
377
+ [...Array(size + start).keys()].slice(start)
378
+
379
+ export const trimSlashEnd = path =>
380
+ path[path.length - 1] === '/' ? trimSlashEnd(path.slice(0, -1)) : path
381
+
382
+ export const mapEntries = (obj, fn) =>
383
+ Object.fromEntries(Object.entries(obj).map(fn))
384
+
385
+ export const mapValues = (obj, fn) =>
386
+ mapEntries(obj, ([k, v]) => [k, fn(v, k, obj)])
387
+
388
+ export const filterEntries = (obj, fn) =>
389
+ Object.fromEntries(Object.entries(obj).filter(fn))
390
+
391
+ export const filterValues = (obj, fn) =>
392
+ filterEntries(obj, ([k, v]) => [k, fn(v, k, obj)])
393
+
394
+ export const getChanges = (newObject, refObject) =>
395
+ filterEntries(newObject, ([key, value]) => !eq(value, refObject[key]))
396
+
397
+ export const hasChanged = (newObject, refObject) =>
398
+ Object.keys(getChanges(newObject, refObject)).length !== 0
399
+
400
+ export const shuffle = arr => {
401
+ const newArr = [...arr]
402
+ let i = newArr.length
403
+ let j, tmp
404
+ while (--i > 0) {
405
+ j = Math.floor(Math.random() * (i + 1))
406
+ tmp = newArr[j]
407
+ newArr[j] = newArr[i]
408
+ newArr[i] = tmp
409
+ }
410
+ return newArr
411
+ }
412
+
413
+ // specific to data structure
414
+ export const getChildByType = (obj, childType) =>
415
+ obj && Object.values(obj).find(child => child.type === childType)
416
+
417
+ export const getUserName = user => {
418
+ if (!user) return '-'
419
+ if (user.attrs?.name) return user.attrs.name
420
+ const firstName = user.firstName || '-'
421
+ const lastName = user.lastName || '-'
422
+
423
+ return `${firstName} ${lastName}`
424
+ }
425
+
426
+ export const eq = (a, b) => {
427
+ if (a === b) return true
428
+ if (typeof a !== typeof b) return false
429
+ if (typeof a === 'number' && Number.isNaN(a) && Number.isNaN(b)) return true
430
+ if (typeof a === 'object') {
431
+ if (!a || !b) return false
432
+ if (a.constructor !== b.constructor) return false
433
+ // todo: handle regexp
434
+ const entries = Object.entries(a)
435
+ if (entries.length !== Object.values(b).length) return false
436
+ for (const [k, v] of entries) {
437
+ if (!eq(b[k], v)) return false
438
+ }
439
+ return true
440
+ }
441
+ return false
442
+ }
443
+
444
+ export const getStatus = progress => {
445
+ if (!progress || !progress.isDone) return
446
+ return progress.succeeded ? 'succeeded' : 'failed'
447
+ }
448
+
449
+ const isAccessBlocked = record =>
450
+ !record.type.canAccessPlatform &&
451
+ (new Date(record.endAt) > Date.now() ||
452
+ record.type.isPermanent ||
453
+ !record.endAt) &&
454
+ isFinished(record.startAt)
455
+
456
+ // TODO: could maybe check user.public.canAccessPlatform?
457
+ export const isBanned = user => {
458
+ const records = user?.records
459
+ const record = records?.find(isAccessBlocked)
460
+ if (!record) return
461
+ const err = Error('Banned')
462
+ err.record = record
463
+ return err
464
+ }
465
+
466
+ export const generateCode = () =>
467
+ Math.random()
468
+ .toString(36)
469
+ .slice(2, 7)
470
+ .padEnd(5, '#')
471
+ .replace(/0/gi, '_')
472
+ .replace(/o/gi, '-')
473
+ .replace(/I/gi, '?')
474
+ .replace(/l/gi, '$')
475
+
476
+ export const base64Encode = str => {
477
+ const encoder = new TextEncoder()
478
+ const data = encoder.encode(str)
479
+ return btoa(String.fromCharCode.apply(null, data))
480
+ }
481
+
482
+ export const timePlusDelay = (time, delay) =>
483
+ new Date(new Date(time).getTime() + delay).toISOString()