@contractspec/module.learning-journey 1.57.0 → 1.58.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/dist/browser/contracts/index.js +578 -0
- package/dist/browser/contracts/models.js +193 -0
- package/dist/browser/contracts/onboarding.js +417 -0
- package/dist/browser/contracts/operations.js +326 -0
- package/dist/browser/contracts/shared.js +5 -0
- package/dist/browser/docs/index.js +124 -0
- package/dist/browser/docs/learning-journey.docblock.js +124 -0
- package/dist/browser/engines/index.js +526 -0
- package/dist/browser/engines/srs.js +198 -0
- package/dist/browser/engines/streak.js +159 -0
- package/dist/browser/engines/xp.js +171 -0
- package/dist/browser/entities/ai.js +343 -0
- package/dist/browser/entities/course.js +276 -0
- package/dist/browser/entities/flashcard.js +222 -0
- package/dist/browser/entities/gamification.js +340 -0
- package/dist/browser/entities/index.js +2136 -0
- package/dist/browser/entities/learner.js +329 -0
- package/dist/browser/entities/onboarding.js +301 -0
- package/dist/browser/entities/quiz.js +304 -0
- package/dist/browser/events.js +423 -0
- package/dist/browser/index.js +3833 -0
- package/dist/browser/learning-journey.capability.js +40 -0
- package/dist/browser/learning-journey.feature.js +56 -0
- package/dist/browser/track-spec.js +0 -0
- package/dist/contracts/index.d.ts +5 -5
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +578 -5
- package/dist/contracts/models.d.ts +426 -431
- package/dist/contracts/models.d.ts.map +1 -1
- package/dist/contracts/models.js +178 -372
- package/dist/contracts/onboarding.d.ts +621 -627
- package/dist/contracts/onboarding.d.ts.map +1 -1
- package/dist/contracts/onboarding.js +404 -388
- package/dist/contracts/operations.d.ts +243 -249
- package/dist/contracts/operations.d.ts.map +1 -1
- package/dist/contracts/operations.js +324 -148
- package/dist/contracts/shared.d.ts +1 -4
- package/dist/contracts/shared.d.ts.map +1 -1
- package/dist/contracts/shared.js +6 -6
- package/dist/docs/index.d.ts +2 -1
- package/dist/docs/index.d.ts.map +1 -0
- package/dist/docs/index.js +125 -1
- package/dist/docs/learning-journey.docblock.d.ts +2 -1
- package/dist/docs/learning-journey.docblock.d.ts.map +1 -0
- package/dist/docs/learning-journey.docblock.js +47 -58
- package/dist/engines/index.d.ts +4 -4
- package/dist/engines/index.d.ts.map +1 -0
- package/dist/engines/index.js +526 -4
- package/dist/engines/srs.d.ts +89 -92
- package/dist/engines/srs.d.ts.map +1 -1
- package/dist/engines/srs.js +197 -217
- package/dist/engines/streak.d.ts +84 -87
- package/dist/engines/streak.d.ts.map +1 -1
- package/dist/engines/streak.js +158 -192
- package/dist/engines/xp.d.ts +80 -83
- package/dist/engines/xp.d.ts.map +1 -1
- package/dist/engines/xp.js +170 -211
- package/dist/entities/ai.d.ts +199 -204
- package/dist/entities/ai.d.ts.map +1 -1
- package/dist/entities/ai.js +336 -368
- package/dist/entities/course.d.ts +149 -154
- package/dist/entities/course.d.ts.map +1 -1
- package/dist/entities/course.js +267 -306
- package/dist/entities/flashcard.d.ts +144 -149
- package/dist/entities/flashcard.d.ts.map +1 -1
- package/dist/entities/flashcard.js +217 -243
- package/dist/entities/gamification.d.ts +197 -202
- package/dist/entities/gamification.d.ts.map +1 -1
- package/dist/entities/gamification.js +331 -382
- package/dist/entities/index.d.ts +613 -618
- package/dist/entities/index.d.ts.map +1 -1
- package/dist/entities/index.js +2135 -43
- package/dist/entities/learner.d.ts +191 -196
- package/dist/entities/learner.d.ts.map +1 -1
- package/dist/entities/learner.js +322 -357
- package/dist/entities/onboarding.d.ts +164 -169
- package/dist/entities/onboarding.d.ts.map +1 -1
- package/dist/entities/onboarding.js +296 -301
- package/dist/entities/quiz.d.ts +184 -189
- package/dist/entities/quiz.d.ts.map +1 -1
- package/dist/entities/quiz.js +296 -361
- package/dist/events.d.ts +608 -614
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +421 -687
- package/dist/index.d.ts +8 -20
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3834 -22
- package/dist/learning-journey.capability.d.ts +3 -8
- package/dist/learning-journey.capability.d.ts.map +1 -1
- package/dist/learning-journey.capability.js +41 -46
- package/dist/learning-journey.feature.d.ts +1 -6
- package/dist/learning-journey.feature.d.ts.map +1 -1
- package/dist/learning-journey.feature.js +55 -155
- package/dist/node/contracts/index.js +578 -0
- package/dist/node/contracts/models.js +193 -0
- package/dist/node/contracts/onboarding.js +417 -0
- package/dist/node/contracts/operations.js +326 -0
- package/dist/node/contracts/shared.js +5 -0
- package/dist/node/docs/index.js +124 -0
- package/dist/node/docs/learning-journey.docblock.js +124 -0
- package/dist/node/engines/index.js +526 -0
- package/dist/node/engines/srs.js +198 -0
- package/dist/node/engines/streak.js +159 -0
- package/dist/node/engines/xp.js +171 -0
- package/dist/node/entities/ai.js +343 -0
- package/dist/node/entities/course.js +276 -0
- package/dist/node/entities/flashcard.js +222 -0
- package/dist/node/entities/gamification.js +340 -0
- package/dist/node/entities/index.js +2136 -0
- package/dist/node/entities/learner.js +329 -0
- package/dist/node/entities/onboarding.js +301 -0
- package/dist/node/entities/quiz.js +304 -0
- package/dist/node/events.js +423 -0
- package/dist/node/index.js +3833 -0
- package/dist/node/learning-journey.capability.js +40 -0
- package/dist/node/learning-journey.feature.js +56 -0
- package/dist/node/track-spec.js +0 -0
- package/dist/track-spec.d.ts +115 -118
- package/dist/track-spec.d.ts.map +1 -1
- package/dist/track-spec.js +1 -0
- package/package.json +237 -60
- package/dist/contracts/models.js.map +0 -1
- package/dist/contracts/onboarding.js.map +0 -1
- package/dist/contracts/operations.js.map +0 -1
- package/dist/contracts/shared.js.map +0 -1
- package/dist/docs/learning-journey.docblock.js.map +0 -1
- package/dist/engines/srs.js.map +0 -1
- package/dist/engines/streak.js.map +0 -1
- package/dist/engines/xp.js.map +0 -1
- package/dist/entities/ai.js.map +0 -1
- package/dist/entities/course.js.map +0 -1
- package/dist/entities/flashcard.js.map +0 -1
- package/dist/entities/gamification.js.map +0 -1
- package/dist/entities/index.js.map +0 -1
- package/dist/entities/learner.js.map +0 -1
- package/dist/entities/onboarding.js.map +0 -1
- package/dist/entities/quiz.js.map +0 -1
- package/dist/events.js.map +0 -1
- package/dist/learning-journey.capability.js.map +0 -1
- package/dist/learning-journey.feature.js.map +0 -1
package/dist/engines/streak.d.ts
CHANGED
|
@@ -1,100 +1,97 @@
|
|
|
1
|
-
//#region src/engines/streak.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* Streak Tracking Engine
|
|
4
3
|
*
|
|
5
4
|
* Manages daily learning streaks with timezone support and freeze protection.
|
|
6
5
|
*/
|
|
7
|
-
interface StreakState {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
export interface StreakState {
|
|
7
|
+
/** Current streak days */
|
|
8
|
+
currentStreak: number;
|
|
9
|
+
/** Longest streak ever */
|
|
10
|
+
longestStreak: number;
|
|
11
|
+
/** Last activity timestamp */
|
|
12
|
+
lastActivityAt: Date | null;
|
|
13
|
+
/** Last activity date (YYYY-MM-DD) */
|
|
14
|
+
lastActivityDate: string | null;
|
|
15
|
+
/** Available streak freezes */
|
|
16
|
+
freezesRemaining: number;
|
|
17
|
+
/** When a freeze was last used */
|
|
18
|
+
freezeUsedAt: Date | null;
|
|
20
19
|
}
|
|
21
|
-
interface StreakUpdateResult {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
export interface StreakUpdateResult {
|
|
21
|
+
/** Updated streak state */
|
|
22
|
+
state: StreakState;
|
|
23
|
+
/** Whether streak was maintained */
|
|
24
|
+
streakMaintained: boolean;
|
|
25
|
+
/** Whether streak was lost */
|
|
26
|
+
streakLost: boolean;
|
|
27
|
+
/** Whether a freeze was used */
|
|
28
|
+
freezeUsed: boolean;
|
|
29
|
+
/** Whether this activity started a new streak */
|
|
30
|
+
newStreak: boolean;
|
|
31
|
+
/** Days missed (if streak was lost) */
|
|
32
|
+
daysMissed: number;
|
|
34
33
|
}
|
|
35
|
-
interface StreakConfig {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
export interface StreakConfig {
|
|
35
|
+
/** Timezone for the user */
|
|
36
|
+
timezone: string;
|
|
37
|
+
/** How many streak freezes to give per month */
|
|
38
|
+
freezesPerMonth: number;
|
|
39
|
+
/** Maximum freezes that can be accumulated */
|
|
40
|
+
maxFreezes: number;
|
|
41
|
+
/** Grace period in hours after midnight */
|
|
42
|
+
gracePeriodHours: number;
|
|
44
43
|
}
|
|
45
|
-
declare const DEFAULT_STREAK_CONFIG: StreakConfig;
|
|
46
|
-
declare class StreakEngine {
|
|
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
|
-
|
|
44
|
+
export declare const DEFAULT_STREAK_CONFIG: StreakConfig;
|
|
45
|
+
export declare class StreakEngine {
|
|
46
|
+
private config;
|
|
47
|
+
constructor(config?: Partial<StreakConfig>);
|
|
48
|
+
/**
|
|
49
|
+
* Update streak based on new activity.
|
|
50
|
+
*/
|
|
51
|
+
update(state: StreakState, now?: Date): StreakUpdateResult;
|
|
52
|
+
/**
|
|
53
|
+
* Check streak status without recording activity.
|
|
54
|
+
*/
|
|
55
|
+
checkStatus(state: StreakState, now?: Date): {
|
|
56
|
+
isActive: boolean;
|
|
57
|
+
willExpireAt: Date | null;
|
|
58
|
+
canUseFreeze: boolean;
|
|
59
|
+
daysUntilExpiry: number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Manually use a freeze to protect streak.
|
|
63
|
+
*/
|
|
64
|
+
useFreeze(state: StreakState, now?: Date): StreakState | null;
|
|
65
|
+
/**
|
|
66
|
+
* Award monthly freezes.
|
|
67
|
+
*/
|
|
68
|
+
awardMonthlyFreezes(state: StreakState): StreakState;
|
|
69
|
+
/**
|
|
70
|
+
* Get initial streak state.
|
|
71
|
+
*/
|
|
72
|
+
getInitialState(): StreakState;
|
|
73
|
+
/**
|
|
74
|
+
* Calculate streak milestones.
|
|
75
|
+
*/
|
|
76
|
+
getMilestones(currentStreak: number): {
|
|
77
|
+
achieved: number[];
|
|
78
|
+
next: number | null;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Get date string in YYYY-MM-DD format.
|
|
82
|
+
*/
|
|
83
|
+
private getDateString;
|
|
84
|
+
/**
|
|
85
|
+
* Get number of days between two date strings.
|
|
86
|
+
*/
|
|
87
|
+
private getDaysBetween;
|
|
88
|
+
/**
|
|
89
|
+
* Add days to a date.
|
|
90
|
+
*/
|
|
91
|
+
private addDays;
|
|
93
92
|
}
|
|
94
93
|
/**
|
|
95
94
|
* Default streak engine instance.
|
|
96
95
|
*/
|
|
97
|
-
declare const streakEngine: StreakEngine;
|
|
98
|
-
//#endregion
|
|
99
|
-
export { DEFAULT_STREAK_CONFIG, StreakConfig, StreakEngine, StreakState, StreakUpdateResult, streakEngine };
|
|
96
|
+
export declare const streakEngine: StreakEngine;
|
|
100
97
|
//# sourceMappingURL=streak.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streak.d.ts","
|
|
1
|
+
{"version":3,"file":"streak.d.ts","sourceRoot":"","sources":["../../src/engines/streak.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IAC5B,sCAAsC;IACtC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,+BAA+B;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,kCAAkC;IAClC,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,2BAA2B;IAC3B,KAAK,EAAE,WAAW,CAAC;IACnB,oCAAoC;IACpC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8BAA8B;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,gCAAgC;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,iDAAiD;IACjD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAID,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAIF,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,GAAE,OAAO,CAAC,YAAY,CAAM;IAI9C;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,GAAE,IAAiB,GAAG,kBAAkB;IA+EtE;;OAEG;IACH,WAAW,CACT,KAAK,EAAE,WAAW,EAClB,GAAG,GAAE,IAAiB,GACrB;QACD,QAAQ,EAAE,OAAO,CAAC;QAClB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;QAC1B,YAAY,EAAE,OAAO,CAAC;QACtB,eAAe,EAAE,MAAM,CAAC;KACzB;IAkDD;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,GAAE,IAAiB,GAAG,WAAW,GAAG,IAAI;IAYzE;;OAEG;IACH,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW;IAUpD;;OAEG;IACH,eAAe,IAAI,WAAW;IAW9B;;OAEG;IACH,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG;QACpC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB;IASD;;OAEG;IACH,OAAO,CAAC,aAAa;IAQrB;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;IACH,OAAO,CAAC,OAAO;CAGhB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,cAAqB,CAAC"}
|
package/dist/engines/streak.js
CHANGED
|
@@ -1,194 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/engines/streak.ts
|
|
3
|
+
var DEFAULT_STREAK_CONFIG = {
|
|
4
|
+
timezone: "UTC",
|
|
5
|
+
freezesPerMonth: 2,
|
|
6
|
+
maxFreezes: 5,
|
|
7
|
+
gracePeriodHours: 4
|
|
7
8
|
};
|
|
8
|
-
var StreakEngine = class {
|
|
9
|
-
config;
|
|
10
|
-
constructor(config = {}) {
|
|
11
|
-
this.config = {
|
|
12
|
-
...DEFAULT_STREAK_CONFIG,
|
|
13
|
-
...config
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Update streak based on new activity.
|
|
18
|
-
*/
|
|
19
|
-
update(state, now = /* @__PURE__ */ new Date()) {
|
|
20
|
-
const todayDate = this.getDateString(now);
|
|
21
|
-
const result = {
|
|
22
|
-
state: { ...state },
|
|
23
|
-
streakMaintained: false,
|
|
24
|
-
streakLost: false,
|
|
25
|
-
freezeUsed: false,
|
|
26
|
-
newStreak: false,
|
|
27
|
-
daysMissed: 0
|
|
28
|
-
};
|
|
29
|
-
if (!state.lastActivityDate) {
|
|
30
|
-
result.state.currentStreak = 1;
|
|
31
|
-
result.state.longestStreak = Math.max(1, state.longestStreak);
|
|
32
|
-
result.state.lastActivityAt = now;
|
|
33
|
-
result.state.lastActivityDate = todayDate;
|
|
34
|
-
result.newStreak = true;
|
|
35
|
-
result.streakMaintained = true;
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
if (state.lastActivityDate === todayDate) {
|
|
39
|
-
result.state.lastActivityAt = now;
|
|
40
|
-
result.streakMaintained = true;
|
|
41
|
-
return result;
|
|
42
|
-
}
|
|
43
|
-
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
44
|
-
if (daysSinceActivity === 1) {
|
|
45
|
-
result.state.currentStreak = state.currentStreak + 1;
|
|
46
|
-
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
47
|
-
result.state.lastActivityAt = now;
|
|
48
|
-
result.state.lastActivityDate = todayDate;
|
|
49
|
-
result.streakMaintained = true;
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
result.daysMissed = daysSinceActivity - 1;
|
|
53
|
-
const freezesNeeded = result.daysMissed;
|
|
54
|
-
if (freezesNeeded <= state.freezesRemaining) {
|
|
55
|
-
result.state.freezesRemaining = state.freezesRemaining - freezesNeeded;
|
|
56
|
-
result.state.freezeUsedAt = now;
|
|
57
|
-
result.state.currentStreak = state.currentStreak + 1;
|
|
58
|
-
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
59
|
-
result.state.lastActivityAt = now;
|
|
60
|
-
result.state.lastActivityDate = todayDate;
|
|
61
|
-
result.freezeUsed = true;
|
|
62
|
-
result.streakMaintained = true;
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
result.streakLost = true;
|
|
66
|
-
result.state.currentStreak = 1;
|
|
67
|
-
result.state.lastActivityAt = now;
|
|
68
|
-
result.state.lastActivityDate = todayDate;
|
|
69
|
-
result.newStreak = true;
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Check streak status without recording activity.
|
|
74
|
-
*/
|
|
75
|
-
checkStatus(state, now = /* @__PURE__ */ new Date()) {
|
|
76
|
-
if (!state.lastActivityDate) return {
|
|
77
|
-
isActive: false,
|
|
78
|
-
willExpireAt: null,
|
|
79
|
-
canUseFreeze: false,
|
|
80
|
-
daysUntilExpiry: 0
|
|
81
|
-
};
|
|
82
|
-
const todayDate = this.getDateString(now);
|
|
83
|
-
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
84
|
-
if (daysSinceActivity === 0) {
|
|
85
|
-
const tomorrow = this.addDays(now, 1);
|
|
86
|
-
tomorrow.setHours(23, 59, 59, 999);
|
|
87
|
-
return {
|
|
88
|
-
isActive: true,
|
|
89
|
-
willExpireAt: tomorrow,
|
|
90
|
-
canUseFreeze: state.freezesRemaining > 0,
|
|
91
|
-
daysUntilExpiry: 1
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
if (daysSinceActivity === 1) {
|
|
95
|
-
const endOfDay = new Date(now);
|
|
96
|
-
endOfDay.setHours(23 + this.config.gracePeriodHours, 59, 59, 999);
|
|
97
|
-
return {
|
|
98
|
-
isActive: true,
|
|
99
|
-
willExpireAt: endOfDay,
|
|
100
|
-
canUseFreeze: state.freezesRemaining > 0,
|
|
101
|
-
daysUntilExpiry: 0
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
const missedDays = daysSinceActivity - 1;
|
|
105
|
-
return {
|
|
106
|
-
isActive: missedDays <= state.freezesRemaining,
|
|
107
|
-
willExpireAt: null,
|
|
108
|
-
canUseFreeze: missedDays <= state.freezesRemaining,
|
|
109
|
-
daysUntilExpiry: -missedDays
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Manually use a freeze to protect streak.
|
|
114
|
-
*/
|
|
115
|
-
useFreeze(state, now = /* @__PURE__ */ new Date()) {
|
|
116
|
-
if (state.freezesRemaining <= 0) return null;
|
|
117
|
-
return {
|
|
118
|
-
...state,
|
|
119
|
-
freezesRemaining: state.freezesRemaining - 1,
|
|
120
|
-
freezeUsedAt: now
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Award monthly freezes.
|
|
125
|
-
*/
|
|
126
|
-
awardMonthlyFreezes(state) {
|
|
127
|
-
return {
|
|
128
|
-
...state,
|
|
129
|
-
freezesRemaining: Math.min(state.freezesRemaining + this.config.freezesPerMonth, this.config.maxFreezes)
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Get initial streak state.
|
|
134
|
-
*/
|
|
135
|
-
getInitialState() {
|
|
136
|
-
return {
|
|
137
|
-
currentStreak: 0,
|
|
138
|
-
longestStreak: 0,
|
|
139
|
-
lastActivityAt: null,
|
|
140
|
-
lastActivityDate: null,
|
|
141
|
-
freezesRemaining: this.config.freezesPerMonth,
|
|
142
|
-
freezeUsedAt: null
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Calculate streak milestones.
|
|
147
|
-
*/
|
|
148
|
-
getMilestones(currentStreak) {
|
|
149
|
-
const milestones = [
|
|
150
|
-
3,
|
|
151
|
-
7,
|
|
152
|
-
14,
|
|
153
|
-
30,
|
|
154
|
-
60,
|
|
155
|
-
90,
|
|
156
|
-
180,
|
|
157
|
-
365,
|
|
158
|
-
500,
|
|
159
|
-
1e3
|
|
160
|
-
];
|
|
161
|
-
return {
|
|
162
|
-
achieved: milestones.filter((m) => currentStreak >= m),
|
|
163
|
-
next: milestones.find((m) => currentStreak < m) ?? null
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Get date string in YYYY-MM-DD format.
|
|
168
|
-
*/
|
|
169
|
-
getDateString(date) {
|
|
170
|
-
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Get number of days between two date strings.
|
|
174
|
-
*/
|
|
175
|
-
getDaysBetween(dateStr1, dateStr2) {
|
|
176
|
-
const date1 = new Date(dateStr1);
|
|
177
|
-
const diffTime = new Date(dateStr2).getTime() - date1.getTime();
|
|
178
|
-
return Math.floor(diffTime / (1e3 * 60 * 60 * 24));
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Add days to a date.
|
|
182
|
-
*/
|
|
183
|
-
addDays(date, days) {
|
|
184
|
-
return new Date(date.getTime() + days * 24 * 60 * 60 * 1e3);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
/**
|
|
188
|
-
* Default streak engine instance.
|
|
189
|
-
*/
|
|
190
|
-
const streakEngine = new StreakEngine();
|
|
191
9
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
10
|
+
class StreakEngine {
|
|
11
|
+
config;
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
this.config = { ...DEFAULT_STREAK_CONFIG, ...config };
|
|
14
|
+
}
|
|
15
|
+
update(state, now = new Date) {
|
|
16
|
+
const todayDate = this.getDateString(now);
|
|
17
|
+
const result = {
|
|
18
|
+
state: { ...state },
|
|
19
|
+
streakMaintained: false,
|
|
20
|
+
streakLost: false,
|
|
21
|
+
freezeUsed: false,
|
|
22
|
+
newStreak: false,
|
|
23
|
+
daysMissed: 0
|
|
24
|
+
};
|
|
25
|
+
if (!state.lastActivityDate) {
|
|
26
|
+
result.state.currentStreak = 1;
|
|
27
|
+
result.state.longestStreak = Math.max(1, state.longestStreak);
|
|
28
|
+
result.state.lastActivityAt = now;
|
|
29
|
+
result.state.lastActivityDate = todayDate;
|
|
30
|
+
result.newStreak = true;
|
|
31
|
+
result.streakMaintained = true;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
if (state.lastActivityDate === todayDate) {
|
|
35
|
+
result.state.lastActivityAt = now;
|
|
36
|
+
result.streakMaintained = true;
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
40
|
+
if (daysSinceActivity === 1) {
|
|
41
|
+
result.state.currentStreak = state.currentStreak + 1;
|
|
42
|
+
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
43
|
+
result.state.lastActivityAt = now;
|
|
44
|
+
result.state.lastActivityDate = todayDate;
|
|
45
|
+
result.streakMaintained = true;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
result.daysMissed = daysSinceActivity - 1;
|
|
49
|
+
const freezesNeeded = result.daysMissed;
|
|
50
|
+
if (freezesNeeded <= state.freezesRemaining) {
|
|
51
|
+
result.state.freezesRemaining = state.freezesRemaining - freezesNeeded;
|
|
52
|
+
result.state.freezeUsedAt = now;
|
|
53
|
+
result.state.currentStreak = state.currentStreak + 1;
|
|
54
|
+
result.state.longestStreak = Math.max(result.state.currentStreak, state.longestStreak);
|
|
55
|
+
result.state.lastActivityAt = now;
|
|
56
|
+
result.state.lastActivityDate = todayDate;
|
|
57
|
+
result.freezeUsed = true;
|
|
58
|
+
result.streakMaintained = true;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
result.streakLost = true;
|
|
62
|
+
result.state.currentStreak = 1;
|
|
63
|
+
result.state.lastActivityAt = now;
|
|
64
|
+
result.state.lastActivityDate = todayDate;
|
|
65
|
+
result.newStreak = true;
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
checkStatus(state, now = new Date) {
|
|
69
|
+
if (!state.lastActivityDate) {
|
|
70
|
+
return {
|
|
71
|
+
isActive: false,
|
|
72
|
+
willExpireAt: null,
|
|
73
|
+
canUseFreeze: false,
|
|
74
|
+
daysUntilExpiry: 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const todayDate = this.getDateString(now);
|
|
78
|
+
const daysSinceActivity = this.getDaysBetween(state.lastActivityDate, todayDate);
|
|
79
|
+
if (daysSinceActivity === 0) {
|
|
80
|
+
const tomorrow = this.addDays(now, 1);
|
|
81
|
+
tomorrow.setHours(23, 59, 59, 999);
|
|
82
|
+
return {
|
|
83
|
+
isActive: true,
|
|
84
|
+
willExpireAt: tomorrow,
|
|
85
|
+
canUseFreeze: state.freezesRemaining > 0,
|
|
86
|
+
daysUntilExpiry: 1
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (daysSinceActivity === 1) {
|
|
90
|
+
const endOfDay = new Date(now);
|
|
91
|
+
endOfDay.setHours(23 + this.config.gracePeriodHours, 59, 59, 999);
|
|
92
|
+
return {
|
|
93
|
+
isActive: true,
|
|
94
|
+
willExpireAt: endOfDay,
|
|
95
|
+
canUseFreeze: state.freezesRemaining > 0,
|
|
96
|
+
daysUntilExpiry: 0
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const missedDays = daysSinceActivity - 1;
|
|
100
|
+
return {
|
|
101
|
+
isActive: missedDays <= state.freezesRemaining,
|
|
102
|
+
willExpireAt: null,
|
|
103
|
+
canUseFreeze: missedDays <= state.freezesRemaining,
|
|
104
|
+
daysUntilExpiry: -missedDays
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
useFreeze(state, now = new Date) {
|
|
108
|
+
if (state.freezesRemaining <= 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
freezesRemaining: state.freezesRemaining - 1,
|
|
114
|
+
freezeUsedAt: now
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
awardMonthlyFreezes(state) {
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
freezesRemaining: Math.min(state.freezesRemaining + this.config.freezesPerMonth, this.config.maxFreezes)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
getInitialState() {
|
|
124
|
+
return {
|
|
125
|
+
currentStreak: 0,
|
|
126
|
+
longestStreak: 0,
|
|
127
|
+
lastActivityAt: null,
|
|
128
|
+
lastActivityDate: null,
|
|
129
|
+
freezesRemaining: this.config.freezesPerMonth,
|
|
130
|
+
freezeUsedAt: null
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
getMilestones(currentStreak) {
|
|
134
|
+
const milestones = [3, 7, 14, 30, 60, 90, 180, 365, 500, 1000];
|
|
135
|
+
const achieved = milestones.filter((m) => currentStreak >= m);
|
|
136
|
+
const next = milestones.find((m) => currentStreak < m) ?? null;
|
|
137
|
+
return { achieved, next };
|
|
138
|
+
}
|
|
139
|
+
getDateString(date) {
|
|
140
|
+
const year = date.getFullYear();
|
|
141
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
142
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
143
|
+
return `${year}-${month}-${day}`;
|
|
144
|
+
}
|
|
145
|
+
getDaysBetween(dateStr1, dateStr2) {
|
|
146
|
+
const date1 = new Date(dateStr1);
|
|
147
|
+
const date2 = new Date(dateStr2);
|
|
148
|
+
const diffTime = date2.getTime() - date1.getTime();
|
|
149
|
+
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
150
|
+
}
|
|
151
|
+
addDays(date, days) {
|
|
152
|
+
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
var streakEngine = new StreakEngine;
|
|
156
|
+
export {
|
|
157
|
+
streakEngine,
|
|
158
|
+
StreakEngine,
|
|
159
|
+
DEFAULT_STREAK_CONFIG
|
|
160
|
+
};
|