@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 +5 -0
- package/event-utils.js +58 -0
- package/lodash.deburr.js +255 -0
- package/onboarding.js +38 -0
- package/package.json +29 -0
- package/path.js +63 -0
- package/programming-languages.js +27 -0
- package/score.js +80 -0
- package/skill-definitions.js +272 -0
- package/toolbox.js +483 -0
package/README.md
ADDED
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()
|
package/lodash.deburr.js
ADDED
|
@@ -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()
|