@burdenoff/microfe-technospam 2026.1101.4 → 2026.1101.5
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/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +91 -90
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +21 -7
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +317 -284
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/lesson/OfflineProgressSync.d.ts +9 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.d.ts.map +1 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.js +20 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.js.map +1 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.d.ts +17 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.d.ts.map +1 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.js +73 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.js.map +1 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.d.ts +17 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.d.ts.map +1 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.js +77 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.js.map +1 -0
- package/dist/technospam/hooks/useOfflineLessons.d.ts +26 -0
- package/dist/technospam/hooks/useOfflineLessons.d.ts.map +1 -0
- package/dist/technospam/hooks/useOfflineLessons.js +70 -0
- package/dist/technospam/hooks/useOfflineLessons.js.map +1 -0
- package/dist/technospam/lib/offlineLessons.d.ts +97 -0
- package/dist/technospam/lib/offlineLessons.d.ts.map +1 -0
- package/dist/technospam/lib/offlineLessons.js +168 -0
- package/dist/technospam/lib/offlineLessons.js.map +1 -0
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +235 -197
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +216 -138
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { CheckCircle2 as t, ChevronRight as n, CloudOff as r, FileText as i } from "lucide-react";
|
|
3
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
4
|
+
import { GlassCard as s } from "@burdenoff/fe-libs/ui";
|
|
5
|
+
//#region src/technospam/components/lesson/SavedCourseOutline.tsx
|
|
6
|
+
function c(e) {
|
|
7
|
+
let t = new Date(e);
|
|
8
|
+
return Number.isNaN(t.getTime()) ? "" : ` on ${t.toLocaleDateString()}`;
|
|
9
|
+
}
|
|
10
|
+
function l({ outline: l, online: u, onOpenLesson: d }) {
|
|
11
|
+
let { course: f, lessons: p, enrollment: m, savedAt: h } = l;
|
|
12
|
+
return /* @__PURE__ */ o("div", {
|
|
13
|
+
className: "space-y-4",
|
|
14
|
+
"data-testid": "course-saved-outline",
|
|
15
|
+
children: [/* @__PURE__ */ o("div", {
|
|
16
|
+
className: "flex items-center gap-2 rounded-xl border border-status-warning-border bg-status-warning-bg-subtle px-4 py-2.5 text-sm text-status-warning-text",
|
|
17
|
+
role: "status",
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ a(r, {
|
|
20
|
+
size: 15,
|
|
21
|
+
className: "shrink-0"
|
|
22
|
+
}),
|
|
23
|
+
u ? "Couldn't reach TechnoSpam. Showing the lessons saved on this device" : "You are offline. Showing the lessons saved on this device",
|
|
24
|
+
c(h),
|
|
25
|
+
"."
|
|
26
|
+
]
|
|
27
|
+
}), /* @__PURE__ */ o(s, {
|
|
28
|
+
className: "p-5",
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ a("h2", {
|
|
31
|
+
className: "text-lg font-semibold text-text-primary",
|
|
32
|
+
children: f.title
|
|
33
|
+
}),
|
|
34
|
+
f.summary ? /* @__PURE__ */ a("p", {
|
|
35
|
+
className: "mt-1 text-sm text-text-secondary",
|
|
36
|
+
children: f.summary
|
|
37
|
+
}) : null,
|
|
38
|
+
/* @__PURE__ */ o("p", {
|
|
39
|
+
className: "mt-2 text-xs text-text-muted",
|
|
40
|
+
children: [
|
|
41
|
+
p.length,
|
|
42
|
+
" ",
|
|
43
|
+
p.length === 1 ? "lesson" : "lessons",
|
|
44
|
+
" saved for offline reading",
|
|
45
|
+
m ? ` · ${Math.round(m.progressPct)}% complete when saved` : ""
|
|
46
|
+
]
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ a("ul", {
|
|
49
|
+
className: "mt-4 divide-y divide-border-seam",
|
|
50
|
+
children: p.map((r) => /* @__PURE__ */ a("li", { children: /* @__PURE__ */ o("button", {
|
|
51
|
+
type: "button",
|
|
52
|
+
onClick: () => d(r.id),
|
|
53
|
+
className: "group flex w-full items-center gap-3 rounded-xl px-2 py-3 text-left transition-colors hover:bg-bg-sunken/60",
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ a("span", {
|
|
56
|
+
className: e("flex h-9 w-9 shrink-0 items-center justify-center rounded-lg", r.completedByMe ? "bg-status-success-bg-subtle text-status-success-text" : "bg-accent-indigo/15 text-accent-indigo"),
|
|
57
|
+
children: r.completedByMe ? /* @__PURE__ */ a(t, { size: 17 }) : /* @__PURE__ */ a(i, { size: 17 })
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ a("span", {
|
|
60
|
+
className: "min-w-0 flex-1 truncate text-sm font-medium text-text-primary",
|
|
61
|
+
children: r.title
|
|
62
|
+
}),
|
|
63
|
+
/* @__PURE__ */ a(n, {
|
|
64
|
+
size: 16,
|
|
65
|
+
className: "shrink-0 text-text-muted transition-transform group-hover:translate-x-0.5"
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
}) }, r.id))
|
|
69
|
+
})
|
|
70
|
+
]
|
|
71
|
+
})]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { l as SavedCourseOutline };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=SavedCourseOutline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SavedCourseOutline.js","names":[],"sources":["../../../../src/technospam/components/lesson/SavedCourseOutline.tsx"],"sourcesContent":["import { CheckCircle2, ChevronRight, CloudOff, FileText } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport type { SavedCourseOutline as SavedCourseOutlineData } from '../../lib/offlineLessons';\nimport { cn } from '../../utils/cn';\n\nexport interface SavedCourseOutlineProps {\n outline: SavedCourseOutlineData;\n /** True when the browser reports a connection (the server was unreachable anyway). */\n online: boolean;\n onOpenLesson: (lessonId: string) => void;\n}\n\nfunction savedOnLabel(savedAt: string): string {\n const saved = new Date(savedAt);\n return Number.isNaN(saved.getTime()) ? '' : ` on ${saved.toLocaleDateString()}`;\n}\n\n/**\n * The course page's offline fallback (BOFF-4418): when the course cannot be\n * loaded from the server, list the lessons saved on this device so a learner\n * who saved the course can still reach them from the course page after a cold\n * start without a connection. Each row opens the lesson player, which reads\n * the saved copy.\n */\nexport function SavedCourseOutline({ outline, online, onOpenLesson }: SavedCourseOutlineProps) {\n const { course, lessons, enrollment, savedAt } = outline;\n return (\n <div className=\"space-y-4\" data-testid=\"course-saved-outline\">\n <div\n className=\"flex items-center gap-2 rounded-xl border border-status-warning-border bg-status-warning-bg-subtle px-4 py-2.5 text-sm text-status-warning-text\"\n role=\"status\"\n >\n <CloudOff size={15} className=\"shrink-0\" />\n {online\n ? \"Couldn't reach TechnoSpam. Showing the lessons saved on this device\"\n : 'You are offline. Showing the lessons saved on this device'}\n {savedOnLabel(savedAt)}.\n </div>\n\n <GlassCard className=\"p-5\">\n <h2 className=\"text-lg font-semibold text-text-primary\">{course.title}</h2>\n {course.summary ? (\n <p className=\"mt-1 text-sm text-text-secondary\">{course.summary}</p>\n ) : null}\n <p className=\"mt-2 text-xs text-text-muted\">\n {lessons.length} {lessons.length === 1 ? 'lesson' : 'lessons'} saved for offline reading\n {enrollment ? ` · ${Math.round(enrollment.progressPct)}% complete when saved` : ''}\n </p>\n\n <ul className=\"mt-4 divide-y divide-border-seam\">\n {lessons.map((lesson) => (\n <li key={lesson.id}>\n <button\n type=\"button\"\n onClick={() => onOpenLesson(lesson.id)}\n className=\"group flex w-full items-center gap-3 rounded-xl px-2 py-3 text-left transition-colors hover:bg-bg-sunken/60\"\n >\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',\n lesson.completedByMe\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-accent-indigo/15 text-accent-indigo'\n )}\n >\n {lesson.completedByMe ? <CheckCircle2 size={17} /> : <FileText size={17} />}\n </span>\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-text-primary\">\n {lesson.title}\n </span>\n <ChevronRight\n size={16}\n className=\"shrink-0 text-text-muted transition-transform group-hover:translate-x-0.5\"\n />\n </button>\n </li>\n ))}\n </ul>\n </GlassCard>\n </div>\n );\n}\n"],"mappings":";;;;;AAaA,SAAS,EAAa,GAAyB;CAC7C,IAAM,IAAQ,IAAI,KAAK,EAAQ;AAC/B,QAAO,OAAO,MAAM,EAAM,SAAS,CAAC,GAAG,KAAK,OAAO,EAAM,oBAAoB;;AAU/E,SAAgB,EAAmB,EAAE,YAAS,WAAQ,mBAAyC;CAC7F,IAAM,EAAE,WAAQ,YAAS,eAAY,eAAY;AACjD,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,eAAY;YAAvC,CACE,kBAAC,OAAD;GACE,WAAU;GACV,MAAK;aAFP;IAIE,kBAAC,GAAD;KAAU,MAAM;KAAI,WAAU;KAAa,CAAA;IAC1C,IACG,wEACA;IACH,EAAa,EAAQ;IAAC;IACnB;MAEN,kBAAC,GAAD;GAAW,WAAU;aAArB;IACE,kBAAC,MAAD;KAAI,WAAU;eAA2C,EAAO;KAAW,CAAA;IAC1E,EAAO,UACN,kBAAC,KAAD;KAAG,WAAU;eAAoC,EAAO;KAAY,CAAA,GAClE;IACJ,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAQ;MAAO;MAAE,EAAQ,WAAW,IAAI,WAAW;MAAU;MAC7D,IAAa,MAAM,KAAK,MAAM,EAAW,YAAY,CAAC,yBAAyB;MAC9E;;IAEJ,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MACZ,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAa,EAAO,GAAG;MACtC,WAAU;gBAHZ;OAKE,kBAAC,QAAD;QACE,WAAW,EACT,gEACA,EAAO,gBACH,yDACA,yCACL;kBAEA,EAAO,gBAAgB,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA,GAAG,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;QACtE,CAAA;OACP,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAO;QACH,CAAA;OACP,kBAAC,GAAD;QACE,MAAM;QACN,WAAU;QACV,CAAA;OACK;SACN,EAxBI,EAAO,GAwBX,CACL;KACC,CAAA;IACK;KACR"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Live connectivity, from `navigator.onLine` and the window's online/offline events. */
|
|
2
|
+
export declare function useOnlineStatus(): boolean;
|
|
3
|
+
export interface OfflineProgress {
|
|
4
|
+
/** Lesson ids completed on this device and not yet sent. */
|
|
5
|
+
pendingLessonIds: string[];
|
|
6
|
+
/** Queue a completion made without a connection. False when the device refused to store it. */
|
|
7
|
+
queueCompletion: (courseId: string, lessonId: string) => boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Delays before re-trying a flush that left completions queued while online. */
|
|
10
|
+
export declare const FLUSH_RETRY_DELAYS_MS: readonly [5000, 15000, 30000, 60000, 120000];
|
|
11
|
+
/**
|
|
12
|
+
* Removes the saved lessons of any other user from this device whenever the
|
|
13
|
+
* signed-in user changes, so a shared phone never shows one learner's saved
|
|
14
|
+
* (possibly premium) lessons to the next.
|
|
15
|
+
*/
|
|
16
|
+
export declare function usePurgeOtherUsersLessons(userId: string | undefined): void;
|
|
17
|
+
/**
|
|
18
|
+
* Sends queued lesson completions whenever the device is (or comes back)
|
|
19
|
+
* online, and exposes what is still pending so the player can show a lesson
|
|
20
|
+
* as complete before the server has heard about it.
|
|
21
|
+
*
|
|
22
|
+
* `scope` is {@link offlineScope}(workspaceId, userId): the outbox belongs to
|
|
23
|
+
* one learner in one workspace and is only ever flushed under that session.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useOfflineProgressSync(scope: string): OfflineProgress;
|
|
26
|
+
//# sourceMappingURL=useOfflineLessons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOfflineLessons.d.ts","sourceRoot":"","sources":["../../../src/technospam/hooks/useOfflineLessons.ts"],"names":[],"mappings":"AAmBA,yFAAyF;AACzF,wBAAgB,eAAe,IAAI,OAAO,CAYzC;AAED,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,+FAA+F;IAC/F,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CAClE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,8CAAoD,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAI1E;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CA6FrE"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { notify as e } from "../lib/toast.js";
|
|
2
|
+
import { sendQueuedLessonCompletion as t } from "../api/technospamApi.js";
|
|
3
|
+
import { flushCompletions as n, isDeviceOffline as r, markSnapshotCompleted as i, pendingCompletions as a, purgeOtherUsersLessons as o, queueLessonCompletion as s } from "../lib/offlineLessons.js";
|
|
4
|
+
import { useCallback as c, useEffect as l, useRef as u, useState as d } from "react";
|
|
5
|
+
import { useApolloClient as f } from "@apollo/client/react";
|
|
6
|
+
//#region src/technospam/hooks/useOfflineLessons.ts
|
|
7
|
+
function p() {
|
|
8
|
+
let [e, t] = d(() => !r());
|
|
9
|
+
return l(() => {
|
|
10
|
+
let e = () => t(!r());
|
|
11
|
+
return window.addEventListener("online", e), window.addEventListener("offline", e), () => {
|
|
12
|
+
window.removeEventListener("online", e), window.removeEventListener("offline", e);
|
|
13
|
+
};
|
|
14
|
+
}, []), e;
|
|
15
|
+
}
|
|
16
|
+
var m = [
|
|
17
|
+
5e3,
|
|
18
|
+
15e3,
|
|
19
|
+
3e4,
|
|
20
|
+
6e4,
|
|
21
|
+
12e4
|
|
22
|
+
];
|
|
23
|
+
function h(e) {
|
|
24
|
+
l(() => {
|
|
25
|
+
e && o(e);
|
|
26
|
+
}, [e]);
|
|
27
|
+
}
|
|
28
|
+
function g(r) {
|
|
29
|
+
let o = r, h = f(), g = p(), _ = u(null), v = u(o);
|
|
30
|
+
l(() => {
|
|
31
|
+
v.current = o;
|
|
32
|
+
}, [o]);
|
|
33
|
+
let [y, b] = d(0), x = u(0), S = u(null), C = c(() => ({
|
|
34
|
+
ws: o,
|
|
35
|
+
ids: a(o).map((e) => e.lessonId)
|
|
36
|
+
}), [o]), [w, T] = d(C), E = w.ws === o ? w.ids : C().ids, D = c(() => {
|
|
37
|
+
T(C());
|
|
38
|
+
}, [C]);
|
|
39
|
+
return l(() => {
|
|
40
|
+
x.current = 0;
|
|
41
|
+
}, [g, o]), l(() => () => {
|
|
42
|
+
S.current && clearTimeout(S.current);
|
|
43
|
+
}, []), l(() => {
|
|
44
|
+
if (!g || !o || _.current !== null || a(o).length === 0) return;
|
|
45
|
+
let r = o;
|
|
46
|
+
_.current = r, n(o, (e) => t(h, e.lessonId)).then((t) => {
|
|
47
|
+
t.rejected > 0 && e.warning("Some offline progress was not saved", `${t.rejected} lesson${t.rejected === 1 ? "" : "s"} could not be marked complete (removed, or ${t.rejected === 1 ? "it needs" : "they need"} a quiz or code check). Open ${t.rejected === 1 ? "it" : "them"} again to finish.`), t.sent > 0 && e.success("Progress synced", `${t.sent} lesson${t.sent === 1 ? "" : "s"} completed offline ${t.sent === 1 ? "is" : "are"} now saved.`);
|
|
48
|
+
let n = m[x.current];
|
|
49
|
+
t.remaining > 0 && n !== void 0 ? (x.current += 1, S.current && clearTimeout(S.current), S.current = setTimeout(() => b((e) => e + 1), n)) : t.remaining === 0 && (x.current = 0);
|
|
50
|
+
}).finally(() => {
|
|
51
|
+
_.current = null, D(), v.current !== r && b((e) => e + 1);
|
|
52
|
+
});
|
|
53
|
+
}, [
|
|
54
|
+
g,
|
|
55
|
+
o,
|
|
56
|
+
h,
|
|
57
|
+
D,
|
|
58
|
+
y
|
|
59
|
+
]), {
|
|
60
|
+
pendingLessonIds: E,
|
|
61
|
+
queueCompletion: c((e, t) => {
|
|
62
|
+
let n = s(o, e, t);
|
|
63
|
+
return n && i(o, e, t), D(), n;
|
|
64
|
+
}, [o, D])
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { g as useOfflineProgressSync, p as useOnlineStatus, h as usePurgeOtherUsersLessons };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=useOfflineLessons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOfflineLessons.js","names":[],"sources":["../../../src/technospam/hooks/useOfflineLessons.ts"],"sourcesContent":["/**\n * React side of offline lesson reading and progress sync (BOFF-4418). The\n * storage rules live in `../lib/offlineLessons`; these hooks connect them to\n * the browser's connectivity events and the shell's Apollo client.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\n\nimport { sendQueuedLessonCompletion } from '../api/technospamApi';\nimport { notify } from '../lib/toast';\nimport {\n flushCompletions,\n isDeviceOffline,\n markSnapshotCompleted,\n pendingCompletions,\n purgeOtherUsersLessons,\n queueLessonCompletion,\n} from '../lib/offlineLessons';\n\n/** Live connectivity, from `navigator.onLine` and the window's online/offline events. */\nexport function useOnlineStatus(): boolean {\n const [online, setOnline] = useState<boolean>(() => !isDeviceOffline());\n useEffect(() => {\n const update = (): void => setOnline(!isDeviceOffline());\n window.addEventListener('online', update);\n window.addEventListener('offline', update);\n return () => {\n window.removeEventListener('online', update);\n window.removeEventListener('offline', update);\n };\n }, []);\n return online;\n}\n\nexport interface OfflineProgress {\n /** Lesson ids completed on this device and not yet sent. */\n pendingLessonIds: string[];\n /** Queue a completion made without a connection. False when the device refused to store it. */\n queueCompletion: (courseId: string, lessonId: string) => boolean;\n}\n\n/** Delays before re-trying a flush that left completions queued while online. */\nexport const FLUSH_RETRY_DELAYS_MS = [5_000, 15_000, 30_000, 60_000, 120_000] as const;\n\n/**\n * Removes the saved lessons of any other user from this device whenever the\n * signed-in user changes, so a shared phone never shows one learner's saved\n * (possibly premium) lessons to the next.\n */\nexport function usePurgeOtherUsersLessons(userId: string | undefined): void {\n useEffect(() => {\n if (userId) purgeOtherUsersLessons(userId);\n }, [userId]);\n}\n\n/**\n * Sends queued lesson completions whenever the device is (or comes back)\n * online, and exposes what is still pending so the player can show a lesson\n * as complete before the server has heard about it.\n *\n * `scope` is {@link offlineScope}(workspaceId, userId): the outbox belongs to\n * one learner in one workspace and is only ever flushed under that session.\n */\nexport function useOfflineProgressSync(scope: string): OfflineProgress {\n const ws = scope;\n const client = useApolloClient();\n const online = useOnlineStatus();\n // The scope whose outbox is being flushed, or null when idle. A scope\n // change mid-flush re-runs the effect once the flush settles (see below).\n const flushing = useRef<string | null>(null);\n const currentScope = useRef(ws);\n useEffect(() => {\n currentScope.current = ws;\n }, [ws]);\n // Bumped to re-run the flush effect after a retryable failure (gateway not\n // reachable yet although the browser reports online). Bounded by\n // FLUSH_RETRY_DELAYS_MS; reset on every connectivity or workspace change.\n const [retryTick, setRetryTick] = useState(0);\n const retryAttempt = useRef(0);\n const retryTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n // The outbox lives in storage; this state mirrors it for rendering and is\n // refreshed whenever this hook changes the outbox. It is tagged with the\n // workspace it was read for, so switching workspace never shows stale ids.\n const readPending = useCallback(\n (): { ws: string; ids: string[] } => ({\n ws,\n ids: pendingCompletions(ws).map((item) => item.lessonId),\n }),\n [ws]\n );\n const [pending, setPending] = useState(readPending);\n const pendingLessonIds = pending.ws === ws ? pending.ids : readPending().ids;\n\n const refreshPending = useCallback((): void => {\n setPending(readPending());\n }, [readPending]);\n\n useEffect(() => {\n retryAttempt.current = 0;\n }, [online, ws]);\n\n useEffect(\n () => () => {\n if (retryTimer.current) clearTimeout(retryTimer.current);\n },\n []\n );\n\n useEffect(() => {\n if (!online || !ws || flushing.current !== null) return;\n if (pendingCompletions(ws).length === 0) return;\n const flushedScope = ws;\n flushing.current = flushedScope;\n void flushCompletions(ws, (item) => sendQueuedLessonCompletion(client, item.lessonId))\n .then((result) => {\n if (result.rejected > 0) {\n notify.warning(\n 'Some offline progress was not saved',\n `${result.rejected} lesson${result.rejected === 1 ? '' : 's'} could not be marked complete (removed, or ${result.rejected === 1 ? 'it needs' : 'they need'} a quiz or code check). Open ${result.rejected === 1 ? 'it' : 'them'} again to finish.`\n );\n }\n if (result.sent > 0) {\n notify.success(\n 'Progress synced',\n `${result.sent} lesson${result.sent === 1 ? '' : 's'} completed offline ${result.sent === 1 ? 'is' : 'are'} now saved.`\n );\n }\n const delay = FLUSH_RETRY_DELAYS_MS[retryAttempt.current];\n if (result.remaining > 0 && delay !== undefined) {\n retryAttempt.current += 1;\n if (retryTimer.current) clearTimeout(retryTimer.current);\n retryTimer.current = setTimeout(() => setRetryTick((tick) => tick + 1), delay);\n } else if (result.remaining === 0) {\n retryAttempt.current = 0;\n }\n })\n .finally(() => {\n flushing.current = null;\n refreshPending();\n // The workspace or user changed while this flush ran: the effect run\n // for the new scope returned early, so trigger it again now.\n if (currentScope.current !== flushedScope) setRetryTick((tick) => tick + 1);\n });\n }, [online, ws, client, refreshPending, retryTick]);\n\n const queueCompletion = useCallback(\n (courseId: string, lessonId: string): boolean => {\n const stored = queueLessonCompletion(ws, courseId, lessonId);\n if (stored) markSnapshotCompleted(ws, courseId, lessonId);\n refreshPending();\n return stored;\n },\n [ws, refreshPending]\n );\n\n return { pendingLessonIds, queueCompletion };\n}\n"],"mappings":";;;;;;AAoBA,SAAgB,IAA2B;CACzC,IAAM,CAAC,GAAQ,KAAa,QAAwB,CAAC,GAAiB,CAAC;AAUvE,QATA,QAAgB;EACd,IAAM,UAAqB,EAAU,CAAC,GAAiB,CAAC;AAGxD,SAFA,OAAO,iBAAiB,UAAU,EAAO,EACzC,OAAO,iBAAiB,WAAW,EAAO,QAC7B;AAEX,GADA,OAAO,oBAAoB,UAAU,EAAO,EAC5C,OAAO,oBAAoB,WAAW,EAAO;;IAE9C,EAAE,CAAC,EACC;;AAWT,IAAa,IAAwB;CAAC;CAAO;CAAQ;CAAQ;CAAQ;CAAQ;AAO7E,SAAgB,EAA0B,GAAkC;AAC1E,SAAgB;AACd,EAAI,KAAQ,EAAuB,EAAO;IACzC,CAAC,EAAO,CAAC;;AAWd,SAAgB,EAAuB,GAAgC;CACrE,IAAM,IAAK,GACL,IAAS,GAAiB,EAC1B,IAAS,GAAiB,EAG1B,IAAW,EAAsB,KAAK,EACtC,IAAe,EAAO,EAAG;AAC/B,SAAgB;AACd,IAAa,UAAU;IACtB,CAAC,EAAG,CAAC;CAIR,IAAM,CAAC,GAAW,KAAgB,EAAS,EAAE,EACvC,IAAe,EAAO,EAAE,EACxB,IAAa,EAA6C,KAAK,EAI/D,IAAc,SACoB;EACpC;EACA,KAAK,EAAmB,EAAG,CAAC,KAAK,MAAS,EAAK,SAAS;EACzD,GACD,CAAC,EAAG,CACL,EACK,CAAC,GAAS,KAAc,EAAS,EAAY,EAC7C,IAAmB,EAAQ,OAAO,IAAK,EAAQ,MAAM,GAAa,CAAC,KAEnE,IAAiB,QAAwB;AAC7C,IAAW,GAAa,CAAC;IACxB,CAAC,EAAY,CAAC;AA4DjB,QA1DA,QAAgB;AACd,IAAa,UAAU;IACtB,CAAC,GAAQ,EAAG,CAAC,EAEhB,cACc;AACV,EAAI,EAAW,WAAS,aAAa,EAAW,QAAQ;IAE1D,EAAE,CACH,EAED,QAAgB;AAEd,MADI,CAAC,KAAU,CAAC,KAAM,EAAS,YAAY,QACvC,EAAmB,EAAG,CAAC,WAAW,EAAG;EACzC,IAAM,IAAe;AAEhB,EADL,EAAS,UAAU,GACd,EAAiB,IAAK,MAAS,EAA2B,GAAQ,EAAK,SAAS,CAAC,CACnF,MAAM,MAAW;AAOhB,GANI,EAAO,WAAW,KACpB,EAAO,QACL,uCACA,GAAG,EAAO,SAAS,SAAS,EAAO,aAAa,IAAI,KAAK,IAAI,6CAA6C,EAAO,aAAa,IAAI,aAAa,YAAY,+BAA+B,EAAO,aAAa,IAAI,OAAO,OAAO,mBACjO,EAEC,EAAO,OAAO,KAChB,EAAO,QACL,mBACA,GAAG,EAAO,KAAK,SAAS,EAAO,SAAS,IAAI,KAAK,IAAI,qBAAqB,EAAO,SAAS,IAAI,OAAO,MAAM,aAC5G;GAEH,IAAM,IAAQ,EAAsB,EAAa;AACjD,GAAI,EAAO,YAAY,KAAK,MAAU,KAAA,KACpC,EAAa,WAAW,GACpB,EAAW,WAAS,aAAa,EAAW,QAAQ,EACxD,EAAW,UAAU,iBAAiB,GAAc,MAAS,IAAO,EAAE,EAAE,EAAM,IACrE,EAAO,cAAc,MAC9B,EAAa,UAAU;IAEzB,CACD,cAAc;AAKb,GAJA,EAAS,UAAU,MACnB,GAAgB,EAGZ,EAAa,YAAY,KAAc,GAAc,MAAS,IAAO,EAAE;IAC3E;IACH;EAAC;EAAQ;EAAI;EAAQ;EAAgB;EAAU,CAAC,EAY5C;EAAE;EAAkB,iBAVH,GACrB,GAAkB,MAA8B;GAC/C,IAAM,IAAS,EAAsB,GAAI,GAAU,EAAS;AAG5D,UAFI,KAAQ,EAAsB,GAAI,GAAU,EAAS,EACzD,GAAgB,EACT;KAET,CAAC,GAAI,EAAe,CACrB;EAE2C"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { LessonPlayerData, TecCourse, TecLesson } from '../api/technospamApi';
|
|
2
|
+
|
|
3
|
+
/** Most lesson snapshots kept per scope (workspace + user); the least recently saved are evicted first. */
|
|
4
|
+
export declare const MAX_SAVED_LESSONS = 150;
|
|
5
|
+
export interface SnapshotEnrollment {
|
|
6
|
+
id: string;
|
|
7
|
+
progressPct: number;
|
|
8
|
+
}
|
|
9
|
+
export interface LessonSnapshot {
|
|
10
|
+
savedAt: string;
|
|
11
|
+
lesson: TecLesson;
|
|
12
|
+
course: TecCourse;
|
|
13
|
+
enrollment: SnapshotEnrollment | null;
|
|
14
|
+
}
|
|
15
|
+
export interface QueuedCompletion {
|
|
16
|
+
lessonId: string;
|
|
17
|
+
courseId: string;
|
|
18
|
+
queuedAt: string;
|
|
19
|
+
}
|
|
20
|
+
/** The subset of the Web Storage API this module needs (injectable for tests). */
|
|
21
|
+
export type KeyValueStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
|
|
22
|
+
/**
|
|
23
|
+
* Storage scope for one learner in one workspace. Empty (nothing is read or
|
|
24
|
+
* stored) until both are known, so data is never written under a partial key.
|
|
25
|
+
*/
|
|
26
|
+
export declare function offlineScope(workspaceId: string | null | undefined, userId: string | null | undefined): string;
|
|
27
|
+
/** True when the browser reports no network connection. */
|
|
28
|
+
export declare function isDeviceOffline(): boolean;
|
|
29
|
+
/** Whether a lesson payload can be kept for offline reading. */
|
|
30
|
+
export declare function isSnapshotable(data: LessonPlayerData | undefined | null): data is {
|
|
31
|
+
tecLesson: TecLesson;
|
|
32
|
+
tecCourse: TecCourse;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Store (or refresh) one lesson snapshot. A signed download URL expires, so
|
|
36
|
+
* the video file reference is dropped: an offline video cannot play anyway.
|
|
37
|
+
* Returns false when nothing was stored (locked lesson or storage refused).
|
|
38
|
+
*/
|
|
39
|
+
export declare function saveLessonSnapshot(scope: string, data: LessonPlayerData, enrollment: SnapshotEnrollment | null, storage?: KeyValueStorage | null, now?: Date): boolean;
|
|
40
|
+
/** Read one lesson snapshot, or null when it was never saved. */
|
|
41
|
+
export declare function readLessonSnapshot(scope: string, courseId: string, lessonId: string, storage?: KeyValueStorage | null): LessonSnapshot | null;
|
|
42
|
+
/**
|
|
43
|
+
* Mark a saved lesson as completed by the learner, so reopening it offline
|
|
44
|
+
* shows it as done even before the queued completion reaches the server.
|
|
45
|
+
*/
|
|
46
|
+
export declare function markSnapshotCompleted(scope: string, courseId: string, lessonId: string, storage?: KeyValueStorage | null): void;
|
|
47
|
+
/** The parts of the Web Storage API needed to enumerate keys. */
|
|
48
|
+
export type EnumerableStorage = KeyValueStorage & Pick<Storage, 'key' | 'length'>;
|
|
49
|
+
/**
|
|
50
|
+
* Remove every saved lesson (snapshots and their index) that belongs to a user
|
|
51
|
+
* other than `userId`. Queued completions are kept: they hold only lesson ids,
|
|
52
|
+
* are scoped to their own user and sync when that user signs in again.
|
|
53
|
+
* Returns how many keys were removed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function purgeOtherUsersLessons(userId: string, storage?: EnumerableStorage | null): number;
|
|
56
|
+
/** Ids of the lessons of one course saved on this device. */
|
|
57
|
+
export declare function savedLessonIds(scope: string, courseId: string, storage?: KeyValueStorage | null): string[];
|
|
58
|
+
/** A course rebuilt from the lessons saved on this device. */
|
|
59
|
+
export interface SavedCourseOutline {
|
|
60
|
+
course: TecCourse;
|
|
61
|
+
/** Saved lessons of the course, in curriculum order. */
|
|
62
|
+
lessons: TecLesson[];
|
|
63
|
+
/** Enrollment progress as of the most recently saved lesson, if any. */
|
|
64
|
+
enrollment: SnapshotEnrollment | null;
|
|
65
|
+
savedAt: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Rebuild a course outline from its saved lesson snapshots, so the course
|
|
69
|
+
* page can list the saved lessons when it cannot reach the server (a cold
|
|
70
|
+
* start offline has no Apollo cache). The course header comes from the most
|
|
71
|
+
* recently saved snapshot. Returns null when no lesson of the course is saved.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readSavedCourseOutline(scope: string, courseId: string, storage?: KeyValueStorage | null): SavedCourseOutline | null;
|
|
74
|
+
/** Remove every saved lesson of one course from this device. */
|
|
75
|
+
export declare function removeSavedCourse(scope: string, courseId: string, storage?: KeyValueStorage | null): void;
|
|
76
|
+
/** Completions waiting to be sent. */
|
|
77
|
+
export declare function pendingCompletions(scope: string, storage?: KeyValueStorage | null): QueuedCompletion[];
|
|
78
|
+
/** Queue a lesson completion for the next sync. Queuing the same lesson twice is a no-op. */
|
|
79
|
+
export declare function queueLessonCompletion(scope: string, courseId: string, lessonId: string, storage?: KeyValueStorage | null, now?: Date): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Outcome of sending one queued completion: `sent` and `rejected` (the server
|
|
82
|
+
* refused it for good, e.g. the lesson was removed) both leave the queue;
|
|
83
|
+
* `retry` (no connection, gateway error) stays for the next sync.
|
|
84
|
+
*/
|
|
85
|
+
export type CompletionSendOutcome = 'sent' | 'rejected' | 'retry';
|
|
86
|
+
export interface FlushResult {
|
|
87
|
+
sent: number;
|
|
88
|
+
rejected: number;
|
|
89
|
+
remaining: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Send queued completions in order. Stops at the first `retry`, since that
|
|
93
|
+
* means the connection is not usable yet, and keeps it and everything after
|
|
94
|
+
* it queued.
|
|
95
|
+
*/
|
|
96
|
+
export declare function flushCompletions(scope: string, send: (item: QueuedCompletion) => Promise<CompletionSendOutcome>, storage?: KeyValueStorage | null): Promise<FlushResult>;
|
|
97
|
+
//# sourceMappingURL=offlineLessons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offlineLessons.d.ts","sourceRoot":"","sources":["../../../src/technospam/lib/offlineLessons.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
//#region src/technospam/lib/offlineLessons.ts
|
|
2
|
+
var e = "technospam.offline.v1";
|
|
3
|
+
function t() {
|
|
4
|
+
try {
|
|
5
|
+
return typeof window < "u" && window.localStorage ? window.localStorage : null;
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function n(e, t) {
|
|
11
|
+
if (!e) return null;
|
|
12
|
+
try {
|
|
13
|
+
let n = e.getItem(t);
|
|
14
|
+
return n ? JSON.parse(n) : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function r(e, t, n) {
|
|
20
|
+
if (!e) return !1;
|
|
21
|
+
try {
|
|
22
|
+
return e.setItem(t, JSON.stringify(n)), !0;
|
|
23
|
+
} catch {
|
|
24
|
+
return !1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function i(e, t) {
|
|
28
|
+
if (e) try {
|
|
29
|
+
e.removeItem(t);
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
function a(e, t) {
|
|
33
|
+
return e && t ? `${e}:${t}` : "";
|
|
34
|
+
}
|
|
35
|
+
var o = (t) => `${e}:index:${t}`, s = (t) => `${e}:outbox:${t}`, c = (t, n, r) => `${e}:lesson:${t}:${n}:${r}`;
|
|
36
|
+
function l(e, t) {
|
|
37
|
+
let r = n(e, o(t));
|
|
38
|
+
return Array.isArray(r) ? r.filter((e) => typeof e == "string") : [];
|
|
39
|
+
}
|
|
40
|
+
function u() {
|
|
41
|
+
return typeof navigator < "u" && navigator.onLine === !1;
|
|
42
|
+
}
|
|
43
|
+
function d(e) {
|
|
44
|
+
let t = e?.tecLesson, n = e?.tecCourse;
|
|
45
|
+
return !!t && !!n && !t.isLocked && t.courseId === n.id;
|
|
46
|
+
}
|
|
47
|
+
function f(e, n, a, s = t(), u = /* @__PURE__ */ new Date()) {
|
|
48
|
+
if (!e || !d(n)) return !1;
|
|
49
|
+
let { tecLesson: f, tecCourse: p } = n, m = {
|
|
50
|
+
savedAt: u.toISOString(),
|
|
51
|
+
lesson: {
|
|
52
|
+
...f,
|
|
53
|
+
file: null
|
|
54
|
+
},
|
|
55
|
+
course: p,
|
|
56
|
+
enrollment: a ? {
|
|
57
|
+
id: a.id,
|
|
58
|
+
progressPct: a.progressPct
|
|
59
|
+
} : null
|
|
60
|
+
};
|
|
61
|
+
if (!r(s, c(e, p.id, f.id), m)) return !1;
|
|
62
|
+
let h = `${p.id}:${f.id}`, g = l(s, e).filter((e) => e !== h);
|
|
63
|
+
for (g.push(h); g.length > 150;) {
|
|
64
|
+
let t = g.shift();
|
|
65
|
+
if (t) {
|
|
66
|
+
let [n = "", r = ""] = t.split(":");
|
|
67
|
+
i(s, c(e, n, r));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return r(s, o(e), g), !0;
|
|
71
|
+
}
|
|
72
|
+
function p(e, r, i, a = t()) {
|
|
73
|
+
if (!e || !r || !i) return null;
|
|
74
|
+
let o = n(a, c(e, r, i));
|
|
75
|
+
return !o?.lesson || !o.course || o.lesson.id !== i || o.lesson.courseId !== r || o.course.id !== r ? null : o;
|
|
76
|
+
}
|
|
77
|
+
function m(e, n, i, a = t()) {
|
|
78
|
+
let o = p(e, n, i, a);
|
|
79
|
+
!o || o.lesson.completedByMe || r(a, c(e, n, i), {
|
|
80
|
+
...o,
|
|
81
|
+
lesson: {
|
|
82
|
+
...o.lesson,
|
|
83
|
+
completedByMe: !0
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function h() {
|
|
88
|
+
try {
|
|
89
|
+
return typeof window < "u" && window.localStorage ? window.localStorage : null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function g(t, n = h()) {
|
|
95
|
+
if (!n || !t) return 0;
|
|
96
|
+
let r = [];
|
|
97
|
+
try {
|
|
98
|
+
for (let i = 0; i < n.length; i++) {
|
|
99
|
+
let a = n.key(i);
|
|
100
|
+
if (!a || !a.startsWith(`${e}:`)) continue;
|
|
101
|
+
let [, o, , s] = a.split(":");
|
|
102
|
+
o !== "lesson" && o !== "index" || s !== t && r.push(a);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
for (let e of r) i(n, e);
|
|
108
|
+
return r.length;
|
|
109
|
+
}
|
|
110
|
+
function _(e, n, r = t()) {
|
|
111
|
+
if (!e || !n) return [];
|
|
112
|
+
let i = `${n}:`;
|
|
113
|
+
return l(r, e).filter((e) => e.startsWith(i)).map((e) => e.slice(i.length));
|
|
114
|
+
}
|
|
115
|
+
function v(e, n, r = t()) {
|
|
116
|
+
let i = _(e, n, r).map((t) => p(e, n, t, r)).filter((e) => e !== null);
|
|
117
|
+
if (i.length === 0) return null;
|
|
118
|
+
let a = i[i.length - 1];
|
|
119
|
+
return {
|
|
120
|
+
course: a.course,
|
|
121
|
+
lessons: i.map((e) => e.lesson).sort((e, t) => e.position - t.position),
|
|
122
|
+
enrollment: a.enrollment,
|
|
123
|
+
savedAt: a.savedAt
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function y(e, n, a = t()) {
|
|
127
|
+
if (!e || !n) return;
|
|
128
|
+
let s = `${n}:`, u = l(a, e);
|
|
129
|
+
for (let t of u) t.startsWith(s) && i(a, c(e, n, t.slice(s.length)));
|
|
130
|
+
r(a, o(e), u.filter((e) => !e.startsWith(s)));
|
|
131
|
+
}
|
|
132
|
+
function b(e, r = t()) {
|
|
133
|
+
if (!e) return [];
|
|
134
|
+
let i = n(r, s(e));
|
|
135
|
+
return Array.isArray(i) ? i.filter((e) => typeof e == "object" && !!e && typeof e.lessonId == "string" && typeof e.courseId == "string") : [];
|
|
136
|
+
}
|
|
137
|
+
function x(e, n, i, a = t(), o = /* @__PURE__ */ new Date()) {
|
|
138
|
+
if (!e || !n || !i) return !1;
|
|
139
|
+
let c = b(e, a);
|
|
140
|
+
return c.some((e) => e.lessonId === i) ? !0 : (c.push({
|
|
141
|
+
lessonId: i,
|
|
142
|
+
courseId: n,
|
|
143
|
+
queuedAt: o.toISOString()
|
|
144
|
+
}), r(a, s(e), c));
|
|
145
|
+
}
|
|
146
|
+
async function S(e, n, a = t()) {
|
|
147
|
+
let o = b(e, a), c = /* @__PURE__ */ new Set(), l = 0, u = 0;
|
|
148
|
+
for (let e of o) {
|
|
149
|
+
let t;
|
|
150
|
+
try {
|
|
151
|
+
t = await n(e);
|
|
152
|
+
} catch {
|
|
153
|
+
t = "retry";
|
|
154
|
+
}
|
|
155
|
+
if (t === "retry") break;
|
|
156
|
+
c.add(e.lessonId), t === "sent" ? l += 1 : u += 1;
|
|
157
|
+
}
|
|
158
|
+
let d = b(e, a).filter((e) => !c.has(e.lessonId));
|
|
159
|
+
return d.length === 0 ? i(a, s(e)) : r(a, s(e), d), {
|
|
160
|
+
sent: l,
|
|
161
|
+
rejected: u,
|
|
162
|
+
remaining: d.length
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { S as flushCompletions, u as isDeviceOffline, m as markSnapshotCompleted, a as offlineScope, b as pendingCompletions, g as purgeOtherUsersLessons, x as queueLessonCompletion, p as readLessonSnapshot, v as readSavedCourseOutline, y as removeSavedCourse, f as saveLessonSnapshot, _ as savedLessonIds };
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=offlineLessons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offlineLessons.js","names":[],"sources":["../../../src/technospam/lib/offlineLessons.ts"],"sourcesContent":["/**\n * Offline lesson reading and progress sync (BOFF-4418).\n *\n * The TechnoSpam mobile app ships the web bundle inside the native shell, so\n * the app itself opens without a connection; what it lacks offline is lesson\n * data. This module keeps that data on the device:\n *\n * - Lesson snapshots: every lesson a learner opens (and every lesson of a\n * course they choose to save for offline) is stored with its course\n * outline and the learner's enrollment progress, so the lesson player can\n * render it again without the network. Only unlocked lessons are kept —\n * a locked lesson has no content to read and must never be cached.\n * - A progress outbox: a lesson marked complete while offline is queued and\n * sent to `tecCompleteLesson` once the device is back online. The server\n * call is idempotent, so a replay after a partial flush is harmless.\n *\n * Storage is `localStorage`, scoped by workspace AND signed-in user (see\n * {@link offlineScope}), so on a shared device one learner's queued progress\n * is never sent under another learner's session and one learner's saved\n * (possibly premium) lessons are never shown to another. When a different\n * user opens the app, {@link purgeOtherUsersLessons} removes the previous\n * user's saved lessons. Lessons are text (markdown,\n * code, quiz prompts), so a capped LRU of snapshots stays far below the quota;\n * video files are never cached and need a connection. Every storage access is\n * guarded: private browsing or a full quota degrades to \"nothing saved\",\n * never to an error in the lesson player.\n */\nimport type { LessonPlayerData, TecCourse, TecLesson } from '../api/technospamApi';\n\nconst PREFIX = 'technospam.offline.v1';\n/** Most lesson snapshots kept per scope (workspace + user); the least recently saved are evicted first. */\nexport const MAX_SAVED_LESSONS = 150;\n\nexport interface SnapshotEnrollment {\n id: string;\n progressPct: number;\n}\n\nexport interface LessonSnapshot {\n savedAt: string;\n lesson: TecLesson;\n course: TecCourse;\n enrollment: SnapshotEnrollment | null;\n}\n\nexport interface QueuedCompletion {\n lessonId: string;\n courseId: string;\n queuedAt: string;\n}\n\n/** The subset of the Web Storage API this module needs (injectable for tests). */\nexport type KeyValueStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;\n\nfunction defaultStorage(): KeyValueStorage | null {\n try {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n } catch {\n return null;\n }\n}\n\nfunction readJson<T>(storage: KeyValueStorage | null, key: string): T | null {\n if (!storage) return null;\n try {\n const raw = storage.getItem(key);\n return raw ? (JSON.parse(raw) as T) : null;\n } catch {\n return null;\n }\n}\n\nfunction writeJson(storage: KeyValueStorage | null, key: string, value: unknown): boolean {\n if (!storage) return false;\n try {\n storage.setItem(key, JSON.stringify(value));\n return true;\n } catch {\n return false;\n }\n}\n\nfunction removeKey(storage: KeyValueStorage | null, key: string): void {\n if (!storage) return;\n try {\n storage.removeItem(key);\n } catch {\n /* storage unavailable: nothing to remove */\n }\n}\n\n/**\n * Storage scope for one learner in one workspace. Empty (nothing is read or\n * stored) until both are known, so data is never written under a partial key.\n */\nexport function offlineScope(\n workspaceId: string | null | undefined,\n userId: string | null | undefined\n): string {\n return workspaceId && userId ? `${workspaceId}:${userId}` : '';\n}\n\nconst indexKey = (scope: string): string => `${PREFIX}:index:${scope}`;\nconst outboxKey = (scope: string): string => `${PREFIX}:outbox:${scope}`;\nconst snapshotKey = (scope: string, courseId: string, lessonId: string): string =>\n `${PREFIX}:lesson:${scope}:${courseId}:${lessonId}`;\n\n/** Snapshot index entry: `${courseId}:${lessonId}`, most recently saved last. */\nfunction readIndex(storage: KeyValueStorage | null, scope: string): string[] {\n const index = readJson<unknown>(storage, indexKey(scope));\n return Array.isArray(index)\n ? index.filter((entry): entry is string => typeof entry === 'string')\n : [];\n}\n\n/** True when the browser reports no network connection. */\nexport function isDeviceOffline(): boolean {\n return typeof navigator !== 'undefined' && navigator.onLine === false;\n}\n\n/** Whether a lesson payload can be kept for offline reading. */\nexport function isSnapshotable(data: LessonPlayerData | undefined | null): data is {\n tecLesson: TecLesson;\n tecCourse: TecCourse;\n} {\n const lesson = data?.tecLesson;\n const course = data?.tecCourse;\n return !!lesson && !!course && !lesson.isLocked && lesson.courseId === course.id;\n}\n\n/**\n * Store (or refresh) one lesson snapshot. A signed download URL expires, so\n * the video file reference is dropped: an offline video cannot play anyway.\n * Returns false when nothing was stored (locked lesson or storage refused).\n */\nexport function saveLessonSnapshot(\n scope: string,\n data: LessonPlayerData,\n enrollment: SnapshotEnrollment | null,\n storage: KeyValueStorage | null = defaultStorage(),\n now: Date = new Date()\n): boolean {\n if (!scope || !isSnapshotable(data)) return false;\n const { tecLesson: lesson, tecCourse: course } = data;\n const snapshot: LessonSnapshot = {\n savedAt: now.toISOString(),\n lesson: { ...lesson, file: null },\n course,\n enrollment: enrollment ? { id: enrollment.id, progressPct: enrollment.progressPct } : null,\n };\n if (!writeJson(storage, snapshotKey(scope, course.id, lesson.id), snapshot)) return false;\n\n const entry = `${course.id}:${lesson.id}`;\n const index = readIndex(storage, scope).filter((e) => e !== entry);\n index.push(entry);\n while (index.length > MAX_SAVED_LESSONS) {\n const evicted = index.shift();\n if (evicted) {\n const [evictedCourse = '', evictedLesson = ''] = evicted.split(':');\n removeKey(storage, snapshotKey(scope, evictedCourse, evictedLesson));\n }\n }\n writeJson(storage, indexKey(scope), index);\n return true;\n}\n\n/** Read one lesson snapshot, or null when it was never saved. */\nexport function readLessonSnapshot(\n scope: string,\n courseId: string,\n lessonId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): LessonSnapshot | null {\n if (!scope || !courseId || !lessonId) return null;\n const snapshot = readJson<LessonSnapshot>(storage, snapshotKey(scope, courseId, lessonId));\n if (!snapshot?.lesson || !snapshot.course) return null;\n if (snapshot.lesson.id !== lessonId || snapshot.lesson.courseId !== courseId) return null;\n if (snapshot.course.id !== courseId) return null;\n return snapshot;\n}\n\n/**\n * Mark a saved lesson as completed by the learner, so reopening it offline\n * shows it as done even before the queued completion reaches the server.\n */\nexport function markSnapshotCompleted(\n scope: string,\n courseId: string,\n lessonId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): void {\n const snapshot = readLessonSnapshot(scope, courseId, lessonId, storage);\n if (!snapshot || snapshot.lesson.completedByMe) return;\n writeJson(storage, snapshotKey(scope, courseId, lessonId), {\n ...snapshot,\n lesson: { ...snapshot.lesson, completedByMe: true },\n });\n}\n\n/** The parts of the Web Storage API needed to enumerate keys. */\nexport type EnumerableStorage = KeyValueStorage & Pick<Storage, 'key' | 'length'>;\n\nfunction defaultEnumerableStorage(): EnumerableStorage | null {\n try {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Remove every saved lesson (snapshots and their index) that belongs to a user\n * other than `userId`. Queued completions are kept: they hold only lesson ids,\n * are scoped to their own user and sync when that user signs in again.\n * Returns how many keys were removed.\n */\nexport function purgeOtherUsersLessons(\n userId: string,\n storage: EnumerableStorage | null = defaultEnumerableStorage()\n): number {\n if (!storage || !userId) return 0;\n const doomed: string[] = [];\n try {\n for (let i = 0; i < storage.length; i++) {\n const key = storage.key(i);\n if (!key || !key.startsWith(`${PREFIX}:`)) continue;\n // `${PREFIX}:${kind}:${workspaceId}:${userId}...`\n const [, kind, , owner] = key.split(':');\n if (kind !== 'lesson' && kind !== 'index') continue;\n if (owner !== userId) doomed.push(key);\n }\n } catch {\n return 0;\n }\n for (const key of doomed) removeKey(storage, key);\n return doomed.length;\n}\n\n/** Ids of the lessons of one course saved on this device. */\nexport function savedLessonIds(\n scope: string,\n courseId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): string[] {\n if (!scope || !courseId) return [];\n const prefix = `${courseId}:`;\n return readIndex(storage, scope)\n .filter((entry) => entry.startsWith(prefix))\n .map((entry) => entry.slice(prefix.length));\n}\n\n/** A course rebuilt from the lessons saved on this device. */\nexport interface SavedCourseOutline {\n course: TecCourse;\n /** Saved lessons of the course, in curriculum order. */\n lessons: TecLesson[];\n /** Enrollment progress as of the most recently saved lesson, if any. */\n enrollment: SnapshotEnrollment | null;\n savedAt: string;\n}\n\n/**\n * Rebuild a course outline from its saved lesson snapshots, so the course\n * page can list the saved lessons when it cannot reach the server (a cold\n * start offline has no Apollo cache). The course header comes from the most\n * recently saved snapshot. Returns null when no lesson of the course is saved.\n */\nexport function readSavedCourseOutline(\n scope: string,\n courseId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): SavedCourseOutline | null {\n const snapshots = savedLessonIds(scope, courseId, storage)\n .map((lessonId) => readLessonSnapshot(scope, courseId, lessonId, storage))\n .filter((snapshot): snapshot is LessonSnapshot => snapshot !== null);\n if (snapshots.length === 0) return null;\n // The index lists the most recently saved entry last.\n const latest = snapshots[snapshots.length - 1] as LessonSnapshot;\n return {\n course: latest.course,\n lessons: snapshots.map((s) => s.lesson).sort((a, b) => a.position - b.position),\n enrollment: latest.enrollment,\n savedAt: latest.savedAt,\n };\n}\n\n/** Remove every saved lesson of one course from this device. */\nexport function removeSavedCourse(\n scope: string,\n courseId: string,\n storage: KeyValueStorage | null = defaultStorage()\n): void {\n if (!scope || !courseId) return;\n const prefix = `${courseId}:`;\n const index = readIndex(storage, scope);\n for (const entry of index) {\n if (entry.startsWith(prefix))\n removeKey(storage, snapshotKey(scope, courseId, entry.slice(prefix.length)));\n }\n writeJson(\n storage,\n indexKey(scope),\n index.filter((entry) => !entry.startsWith(prefix))\n );\n}\n\n/** Completions waiting to be sent. */\nexport function pendingCompletions(\n scope: string,\n storage: KeyValueStorage | null = defaultStorage()\n): QueuedCompletion[] {\n if (!scope) return [];\n const queue = readJson<unknown>(storage, outboxKey(scope));\n if (!Array.isArray(queue)) return [];\n return queue.filter(\n (item): item is QueuedCompletion =>\n typeof item === 'object' &&\n item !== null &&\n typeof (item as QueuedCompletion).lessonId === 'string' &&\n typeof (item as QueuedCompletion).courseId === 'string'\n );\n}\n\n/** Queue a lesson completion for the next sync. Queuing the same lesson twice is a no-op. */\nexport function queueLessonCompletion(\n scope: string,\n courseId: string,\n lessonId: string,\n storage: KeyValueStorage | null = defaultStorage(),\n now: Date = new Date()\n): boolean {\n if (!scope || !courseId || !lessonId) return false;\n const queue = pendingCompletions(scope, storage);\n if (queue.some((item) => item.lessonId === lessonId)) return true;\n queue.push({ lessonId, courseId, queuedAt: now.toISOString() });\n return writeJson(storage, outboxKey(scope), queue);\n}\n\n/**\n * Outcome of sending one queued completion: `sent` and `rejected` (the server\n * refused it for good, e.g. the lesson was removed) both leave the queue;\n * `retry` (no connection, gateway error) stays for the next sync.\n */\nexport type CompletionSendOutcome = 'sent' | 'rejected' | 'retry';\n\nexport interface FlushResult {\n sent: number;\n rejected: number;\n remaining: number;\n}\n\n/**\n * Send queued completions in order. Stops at the first `retry`, since that\n * means the connection is not usable yet, and keeps it and everything after\n * it queued.\n */\nexport async function flushCompletions(\n scope: string,\n send: (item: QueuedCompletion) => Promise<CompletionSendOutcome>,\n storage: KeyValueStorage | null = defaultStorage()\n): Promise<FlushResult> {\n const queue = pendingCompletions(scope, storage);\n const settled = new Set<string>();\n let sent = 0;\n let rejected = 0;\n for (const item of queue) {\n let outcome: CompletionSendOutcome;\n try {\n outcome = await send(item);\n } catch {\n outcome = 'retry';\n }\n if (outcome === 'retry') break;\n settled.add(item.lessonId);\n if (outcome === 'sent') sent += 1;\n else rejected += 1;\n }\n // Re-read the outbox: a completion queued while a request was in flight must\n // survive. Only the entries this flush settled are removed.\n const remaining = pendingCompletions(scope, storage).filter(\n (item) => !settled.has(item.lessonId)\n );\n if (remaining.length === 0) removeKey(storage, outboxKey(scope));\n else writeJson(storage, outboxKey(scope), remaining);\n return { sent, rejected, remaining: remaining.length };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesCourseDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CoursesCourseDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"names":[],"mappings":"AA+RA,wBAAgB,4BAA4B,gCA4V3C"}
|