@anantagnanam/lms 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +98 -2
- package/astro/client.ts +355 -0
- package/astro/components/CertificateCard.astro +83 -0
- package/astro/components/CourseCard.astro +61 -0
- package/astro/components/CourseCatalog.astro +96 -0
- package/astro/components/CourseOverview.astro +308 -0
- package/astro/components/LearnerDashboard.astro +127 -0
- package/astro/components/LearningShell.astro +37 -0
- package/astro/components/LessonAvailabilityNote.astro +26 -0
- package/astro/components/LessonContent.astro +53 -0
- package/astro/components/LessonPlayer.astro +302 -0
- package/astro/components/MediaPlayer.astro +111 -0
- package/astro/components/index.ts +7 -0
- package/astro/layouts/AcademyLayout.astro +84 -0
- package/astro/media-player.ts +99 -0
- package/astro/pages/author-preview.astro +71 -0
- package/astro/pages/certificate.astro +57 -0
- package/astro/pages/cohort.astro +102 -0
- package/astro/pages/course.astro +90 -0
- package/astro/pages/dashboard.astro +34 -0
- package/astro/pages/index.astro +40 -0
- package/astro/pages/lesson.astro +103 -0
- package/astro/pages/library.astro +173 -0
- package/astro/pages/paths.astro +73 -0
- package/astro/pages/teach.astro +15 -0
- package/astro/quiz-drafts.ts +87 -0
- package/astro/quiz-player.ts +362 -0
- package/astro/styles.css +1300 -0
- package/dist/admin.d.ts +34 -0
- package/dist/admin.js +3231 -0
- package/dist/admin.js.map +1 -0
- package/dist/astro.d.ts +11 -0
- package/dist/astro.js +52 -0
- package/dist/astro.js.map +1 -0
- package/dist/commerce.d.ts +72 -0
- package/dist/commerce.js +0 -0
- package/dist/config-BX3beTpM.js +15 -0
- package/dist/config-BX3beTpM.js.map +1 -0
- package/dist/constants-Cnx7k82Z.js +161 -0
- package/dist/constants-Cnx7k82Z.js.map +1 -0
- package/dist/csv-DKq_4OWU.js +55 -0
- package/dist/csv-DKq_4OWU.js.map +1 -0
- package/dist/errors-ChzJS9al.js +16 -0
- package/dist/errors-ChzJS9al.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime.d.ts +773 -0
- package/dist/runtime.js +2840 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schemas.d.ts +651 -0
- package/dist/schemas.js +347 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +40 -0
- package/dist/server.js +65 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +387 -0
- package/dist/types.js +0 -0
- package/package.json +83 -4
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import CertificateCard from "../components/CertificateCard.astro";
|
|
5
|
+
import { lmsPublic, lmsLearner, LmsError } from "@anantagnanam/lms/server";
|
|
6
|
+
import type { Certificate } from "@anantagnanam/lms/types";
|
|
7
|
+
export const prerender = false;
|
|
8
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
9
|
+
let verification:
|
|
10
|
+
| {
|
|
11
|
+
id: string;
|
|
12
|
+
valid: boolean;
|
|
13
|
+
shared: boolean;
|
|
14
|
+
learnerName: string | null;
|
|
15
|
+
courseTitle: string;
|
|
16
|
+
issuerName: string;
|
|
17
|
+
issuedAt: string;
|
|
18
|
+
courseId?: string;
|
|
19
|
+
}
|
|
20
|
+
| undefined;
|
|
21
|
+
let certificate: Certificate | null = null;
|
|
22
|
+
let error = "";
|
|
23
|
+
try {
|
|
24
|
+
verification = await lmsPublic<typeof verification>(
|
|
25
|
+
Astro.locals,
|
|
26
|
+
"certificates/verify",
|
|
27
|
+
{ id: Astro.params.certificateId },
|
|
28
|
+
Astro.url.origin,
|
|
29
|
+
);
|
|
30
|
+
if (Astro.locals.user) {
|
|
31
|
+
try {
|
|
32
|
+
certificate = await lmsLearner<Certificate>(
|
|
33
|
+
Astro.locals,
|
|
34
|
+
"learner/certificate-by-id",
|
|
35
|
+
{ id: Astro.params.certificateId },
|
|
36
|
+
Astro.url.origin,
|
|
37
|
+
);
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
} catch (cause) {
|
|
41
|
+
Astro.response.status = cause instanceof LmsError ? cause.status : 503;
|
|
42
|
+
error = cause instanceof Error ? cause.message : "Certificate unavailable.";
|
|
43
|
+
}
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<Layout title="Learning certificate" robots="noindex, follow">
|
|
47
|
+
<LearningShell>
|
|
48
|
+
{verification ? (
|
|
49
|
+
<CertificateCard verification={verification} certificate={certificate} />
|
|
50
|
+
) : (
|
|
51
|
+
<section class="lms-gate">
|
|
52
|
+
<h1>Certificate unavailable</h1>
|
|
53
|
+
<p>{error}</p>
|
|
54
|
+
</section>
|
|
55
|
+
)}
|
|
56
|
+
</LearningShell>
|
|
57
|
+
</Layout>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import { lmsPublic, lmsLearner, loginUrl, courseUrl, LmsError } from "@anantagnanam/lms/server";
|
|
5
|
+
import { basePath, loginPath } from "virtual:anantagnanam-lms/config";
|
|
6
|
+
import type { CohortMembership, CourseCard } from "@anantagnanam/lms/types";
|
|
7
|
+
export const prerender = false;
|
|
8
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
9
|
+
let cohort:
|
|
10
|
+
| {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
startsAt: string | null;
|
|
14
|
+
endsAt: string | null;
|
|
15
|
+
courses: CourseCard[];
|
|
16
|
+
}
|
|
17
|
+
| undefined;
|
|
18
|
+
let membership: CohortMembership | null = null;
|
|
19
|
+
let error = "";
|
|
20
|
+
try {
|
|
21
|
+
cohort = await lmsPublic<typeof cohort>(
|
|
22
|
+
Astro.locals,
|
|
23
|
+
"cohort",
|
|
24
|
+
{ id: Astro.params.cohortId },
|
|
25
|
+
Astro.url.origin,
|
|
26
|
+
);
|
|
27
|
+
if (Astro.locals.user)
|
|
28
|
+
membership = await lmsLearner<CohortMembership | null>(
|
|
29
|
+
Astro.locals,
|
|
30
|
+
"learner/cohort",
|
|
31
|
+
{ cohortId: Astro.params.cohortId },
|
|
32
|
+
Astro.url.origin,
|
|
33
|
+
);
|
|
34
|
+
} catch (cause) {
|
|
35
|
+
Astro.response.status = cause instanceof LmsError ? cause.status : 503;
|
|
36
|
+
error = cause instanceof Error ? cause.message : "Cohort unavailable.";
|
|
37
|
+
}
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<Layout title={cohort?.title ?? "Cohort unavailable"} robots="noindex, follow">
|
|
41
|
+
<LearningShell>
|
|
42
|
+
{cohort ? (
|
|
43
|
+
<>
|
|
44
|
+
<header class="lms-dashboard-header">
|
|
45
|
+
<p class="lms-eyebrow">LEARN WITH A COHORT</p>
|
|
46
|
+
<h1>{cohort.title}</h1>
|
|
47
|
+
<p class="lms-lead">
|
|
48
|
+
A shared learning journey, with membership approved by an administrator.
|
|
49
|
+
</p>
|
|
50
|
+
{cohort.startsAt && (
|
|
51
|
+
<p>
|
|
52
|
+
Starts {new Date(cohort.startsAt).toLocaleDateString("en-IN")}
|
|
53
|
+
{cohort.endsAt
|
|
54
|
+
? ` · Ends ${new Date(cohort.endsAt).toLocaleDateString("en-IN")}`
|
|
55
|
+
: ""}
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
</header>
|
|
59
|
+
{membership ? (
|
|
60
|
+
<div class="lms-notice">
|
|
61
|
+
<strong>Membership {membership.status}</strong>
|
|
62
|
+
{membership.reason && <p>{membership.reason}</p>}
|
|
63
|
+
{membership.status === "pending" && (
|
|
64
|
+
<p>Your request is awaiting administrator approval.</p>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
) : Astro.locals.user ? (
|
|
68
|
+
<form data-lms-route="learner/cohort/request" data-lms-reload>
|
|
69
|
+
<input type="hidden" name="cohortId" value={cohort.id} />
|
|
70
|
+
<button class="lms-btn" type="submit">
|
|
71
|
+
Request cohort membership →
|
|
72
|
+
</button>
|
|
73
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
74
|
+
</form>
|
|
75
|
+
) : (
|
|
76
|
+
<a class="lms-btn" href={loginUrl(loginPath, Astro.url.pathname)}>
|
|
77
|
+
Sign in to request membership →
|
|
78
|
+
</a>
|
|
79
|
+
)}
|
|
80
|
+
<section class="lms-content-section">
|
|
81
|
+
<h2>Learning paths in this cohort</h2>
|
|
82
|
+
{cohort.courses.map((course) => (
|
|
83
|
+
<article class="lms-learning-card">
|
|
84
|
+
<h3>{course.title}</h3>
|
|
85
|
+
<p>{course.summary}</p>
|
|
86
|
+
<a
|
|
87
|
+
href={`${courseUrl(basePath, course.slug)}?cohort=${encodeURIComponent(cohort.id)}`}
|
|
88
|
+
>
|
|
89
|
+
Open course →
|
|
90
|
+
</a>
|
|
91
|
+
</article>
|
|
92
|
+
))}
|
|
93
|
+
</section>
|
|
94
|
+
</>
|
|
95
|
+
) : (
|
|
96
|
+
<section class="lms-gate">
|
|
97
|
+
<h1>Cohort unavailable</h1>
|
|
98
|
+
<p>{error}</p>
|
|
99
|
+
</section>
|
|
100
|
+
)}
|
|
101
|
+
</LearningShell>
|
|
102
|
+
</Layout>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import CourseOverview from "../components/CourseOverview.astro";
|
|
5
|
+
import { lmsPublic, lmsLearner, LmsError } from "@anantagnanam/lms/server";
|
|
6
|
+
import { basePath } from "virtual:anantagnanam-lms/config";
|
|
7
|
+
import type {
|
|
8
|
+
CourseDetail,
|
|
9
|
+
PublicEnrollment,
|
|
10
|
+
ProgressSummary,
|
|
11
|
+
Review,
|
|
12
|
+
Page,
|
|
13
|
+
CohortMembership,
|
|
14
|
+
} from "@anantagnanam/lms/types";
|
|
15
|
+
export const prerender = false;
|
|
16
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
17
|
+
const slug = Astro.params.slug ?? "";
|
|
18
|
+
let course: CourseDetail | undefined;
|
|
19
|
+
let enrollment: PublicEnrollment | null = null;
|
|
20
|
+
let progress: ProgressSummary | undefined;
|
|
21
|
+
let reviews: Page<Review> | undefined;
|
|
22
|
+
let cohortMembership: CohortMembership | null = null;
|
|
23
|
+
const cohortId = Astro.url.searchParams.get("cohort") ?? undefined;
|
|
24
|
+
let error = "";
|
|
25
|
+
try {
|
|
26
|
+
if (Astro.locals.user) {
|
|
27
|
+
const result = await lmsLearner<{
|
|
28
|
+
course: CourseDetail;
|
|
29
|
+
enrollment: PublicEnrollment | null;
|
|
30
|
+
progress: ProgressSummary;
|
|
31
|
+
}>(Astro.locals, "learner/course-by-slug", { slug }, Astro.url.origin);
|
|
32
|
+
course = result.course;
|
|
33
|
+
enrollment = result.enrollment;
|
|
34
|
+
progress = result.progress;
|
|
35
|
+
if (cohortId)
|
|
36
|
+
cohortMembership = await lmsLearner<CohortMembership | null>(
|
|
37
|
+
Astro.locals,
|
|
38
|
+
"learner/cohort",
|
|
39
|
+
{ cohortId },
|
|
40
|
+
Astro.url.origin,
|
|
41
|
+
);
|
|
42
|
+
} else course = await lmsPublic<CourseDetail>(Astro.locals, "course", { slug }, Astro.url.origin);
|
|
43
|
+
try {
|
|
44
|
+
reviews = await lmsPublic<Page<Review>>(
|
|
45
|
+
Astro.locals,
|
|
46
|
+
"reviews",
|
|
47
|
+
{ courseId: course.id },
|
|
48
|
+
Astro.url.origin,
|
|
49
|
+
);
|
|
50
|
+
} catch {}
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
Astro.response.status = cause instanceof LmsError ? cause.status : 503;
|
|
53
|
+
error = cause instanceof Error ? cause.message : "Course unavailable.";
|
|
54
|
+
}
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
<Layout
|
|
58
|
+
title={course?.title ?? "Course unavailable"}
|
|
59
|
+
description={course?.summary}
|
|
60
|
+
robots={course ? undefined : "noindex, follow"}
|
|
61
|
+
>
|
|
62
|
+
<LearningShell>
|
|
63
|
+
{course && Astro.locals.user && (
|
|
64
|
+
<form class="lms-actions" data-lms-route="learner/saved/save">
|
|
65
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
66
|
+
<input type="hidden" name="saved" value="true" />
|
|
67
|
+
<button class="lms-btn lms-btn-outline" type="submit">
|
|
68
|
+
Save course to my library
|
|
69
|
+
</button>
|
|
70
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
71
|
+
</form>
|
|
72
|
+
)}
|
|
73
|
+
{course ? (
|
|
74
|
+
<CourseOverview
|
|
75
|
+
course={course}
|
|
76
|
+
enrollment={enrollment}
|
|
77
|
+
progress={progress}
|
|
78
|
+
reviews={reviews}
|
|
79
|
+
cohortId={cohortId}
|
|
80
|
+
cohortMembership={cohortMembership}
|
|
81
|
+
/>
|
|
82
|
+
) : (
|
|
83
|
+
<section class="lms-gate">
|
|
84
|
+
<h1>Course unavailable</h1>
|
|
85
|
+
<p>{error}</p>
|
|
86
|
+
<a href={basePath}>Explore courses →</a>
|
|
87
|
+
</section>
|
|
88
|
+
)}
|
|
89
|
+
</LearningShell>
|
|
90
|
+
</Layout>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import LearnerDashboard from "../components/LearnerDashboard.astro";
|
|
5
|
+
import { lmsLearner, loginUrl } from "@anantagnanam/lms/server";
|
|
6
|
+
import { loginPath } from "virtual:anantagnanam-lms/config";
|
|
7
|
+
import type { CourseCard, PublicEnrollment, ProgressSummary, Page } from "@anantagnanam/lms/types";
|
|
8
|
+
export const prerender = false;
|
|
9
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
10
|
+
if (!Astro.locals.user)
|
|
11
|
+
return Astro.redirect(loginUrl(loginPath, Astro.url.pathname + Astro.url.search));
|
|
12
|
+
let page: Page<{ course: CourseCard; enrollment: PublicEnrollment; progress: ProgressSummary }> = {
|
|
13
|
+
items: [],
|
|
14
|
+
hasMore: false,
|
|
15
|
+
};
|
|
16
|
+
let error = "";
|
|
17
|
+
try {
|
|
18
|
+
page = await lmsLearner<typeof page>(
|
|
19
|
+
Astro.locals,
|
|
20
|
+
"learner/dashboard",
|
|
21
|
+
{ cursor: Astro.url.searchParams.get("cursor") ?? undefined },
|
|
22
|
+
Astro.url.origin,
|
|
23
|
+
);
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
Astro.response.status = 503;
|
|
26
|
+
error = cause instanceof Error ? cause.message : "Could not load your learning records.";
|
|
27
|
+
}
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<Layout title="My learning" robots="noindex, follow">
|
|
31
|
+
<LearningShell>
|
|
32
|
+
<LearnerDashboard page={page} error={error} />
|
|
33
|
+
</LearningShell>
|
|
34
|
+
</Layout>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import CourseCatalog from "../components/CourseCatalog.astro";
|
|
5
|
+
import { lmsPublic, LmsError } from "@anantagnanam/lms/server";
|
|
6
|
+
import type { Page, CourseCard } from "@anantagnanam/lms/types";
|
|
7
|
+
export const prerender = false;
|
|
8
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
9
|
+
let page: Page<CourseCard> = { items: [], hasMore: false };
|
|
10
|
+
let academy = {
|
|
11
|
+
title: "Ananta Gnanam Academy",
|
|
12
|
+
description: "Build knowledge, one lesson at a time.",
|
|
13
|
+
};
|
|
14
|
+
let error = "";
|
|
15
|
+
try {
|
|
16
|
+
[page, academy] = await Promise.all([
|
|
17
|
+
lmsPublic<Page<CourseCard>>(
|
|
18
|
+
Astro.locals,
|
|
19
|
+
"catalog",
|
|
20
|
+
Object.fromEntries(Astro.url.searchParams),
|
|
21
|
+
Astro.url.origin,
|
|
22
|
+
),
|
|
23
|
+
lmsPublic<typeof academy>(Astro.locals, "academy", {}, Astro.url.origin),
|
|
24
|
+
]);
|
|
25
|
+
} catch (cause) {
|
|
26
|
+
Astro.response.status = cause instanceof LmsError ? cause.status : 503;
|
|
27
|
+
error = cause instanceof Error ? cause.message : "The academy is temporarily unavailable.";
|
|
28
|
+
}
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<Layout title={academy.title} description={academy.description}>
|
|
32
|
+
<LearningShell>
|
|
33
|
+
<CourseCatalog
|
|
34
|
+
page={page}
|
|
35
|
+
title={academy.title}
|
|
36
|
+
description={academy.description}
|
|
37
|
+
error={error}
|
|
38
|
+
/>
|
|
39
|
+
</LearningShell>
|
|
40
|
+
</Layout>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import LessonPlayer from "../components/LessonPlayer.astro";
|
|
5
|
+
import LessonAvailabilityNote from "../components/LessonAvailabilityNote.astro";
|
|
6
|
+
import { lmsPublic, lmsLearner, courseUrl, loginUrl, LmsError } from "@anantagnanam/lms/server";
|
|
7
|
+
import { basePath, loginPath } from "virtual:anantagnanam-lms/config";
|
|
8
|
+
import type {
|
|
9
|
+
CourseDetail,
|
|
10
|
+
PublicLesson,
|
|
11
|
+
PublicEnrollment,
|
|
12
|
+
ProgressSummary,
|
|
13
|
+
} from "@anantagnanam/lms/types";
|
|
14
|
+
export const prerender = false;
|
|
15
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
16
|
+
let course: CourseDetail | undefined;
|
|
17
|
+
let lesson: PublicLesson | undefined;
|
|
18
|
+
let enrollment: PublicEnrollment | null = null;
|
|
19
|
+
let progress: ProgressSummary | undefined;
|
|
20
|
+
let error = "";
|
|
21
|
+
let preview = false;
|
|
22
|
+
try {
|
|
23
|
+
if (Astro.locals.user) {
|
|
24
|
+
const result = await lmsLearner<{
|
|
25
|
+
course: CourseDetail;
|
|
26
|
+
enrollment: PublicEnrollment | null;
|
|
27
|
+
progress: ProgressSummary;
|
|
28
|
+
}>(Astro.locals, "learner/course-by-slug", { slug: Astro.params.slug }, Astro.url.origin);
|
|
29
|
+
course = result.course;
|
|
30
|
+
enrollment = result.enrollment;
|
|
31
|
+
progress = result.progress;
|
|
32
|
+
} else
|
|
33
|
+
course = await lmsPublic<CourseDetail>(
|
|
34
|
+
Astro.locals,
|
|
35
|
+
"course",
|
|
36
|
+
{ slug: Astro.params.slug },
|
|
37
|
+
Astro.url.origin,
|
|
38
|
+
);
|
|
39
|
+
if (enrollment && ["active", "completed"].includes(enrollment.status)) {
|
|
40
|
+
const result = await lmsLearner<{
|
|
41
|
+
course: CourseDetail;
|
|
42
|
+
lesson: PublicLesson;
|
|
43
|
+
enrollment: PublicEnrollment;
|
|
44
|
+
progress: ProgressSummary;
|
|
45
|
+
}>(
|
|
46
|
+
Astro.locals,
|
|
47
|
+
"learner/lesson",
|
|
48
|
+
{ courseId: course.id, lessonId: Astro.params.lessonId },
|
|
49
|
+
Astro.url.origin,
|
|
50
|
+
);
|
|
51
|
+
lesson = result.lesson;
|
|
52
|
+
enrollment = result.enrollment;
|
|
53
|
+
progress = result.progress;
|
|
54
|
+
} else {
|
|
55
|
+
const result = await lmsPublic<{ course: CourseDetail; lesson: PublicLesson }>(
|
|
56
|
+
Astro.locals,
|
|
57
|
+
"preview",
|
|
58
|
+
{ courseId: course.id, lessonId: Astro.params.lessonId },
|
|
59
|
+
Astro.url.origin,
|
|
60
|
+
);
|
|
61
|
+
lesson = result.lesson;
|
|
62
|
+
preview = true;
|
|
63
|
+
}
|
|
64
|
+
} catch (cause) {
|
|
65
|
+
Astro.response.status = cause instanceof LmsError ? cause.status : 503;
|
|
66
|
+
error = cause instanceof Error ? cause.message : "This lesson is unavailable.";
|
|
67
|
+
}
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
<Layout
|
|
71
|
+
title={lesson?.title ?? "Continue your learning"}
|
|
72
|
+
description={lesson?.summary}
|
|
73
|
+
robots="noindex, follow"
|
|
74
|
+
>
|
|
75
|
+
<LearningShell>
|
|
76
|
+
{course && lesson ? (
|
|
77
|
+
<LessonPlayer
|
|
78
|
+
course={course}
|
|
79
|
+
lesson={lesson}
|
|
80
|
+
enrollment={enrollment}
|
|
81
|
+
progress={progress}
|
|
82
|
+
preview={preview}
|
|
83
|
+
/>
|
|
84
|
+
) : (
|
|
85
|
+
<section class="lms-gate">
|
|
86
|
+
<p class="lms-eyebrow">ONE STEP AT A TIME</p>
|
|
87
|
+
<h1>Continue your learning</h1>
|
|
88
|
+
<p>{error}</p>
|
|
89
|
+
<LessonAvailabilityNote availability={progress?.availability[Astro.params.lessonId!]} />
|
|
90
|
+
{!Astro.locals.user && (
|
|
91
|
+
<a class="lms-btn" href={loginUrl(loginPath, Astro.url.pathname)}>
|
|
92
|
+
Sign in →
|
|
93
|
+
</a>
|
|
94
|
+
)}
|
|
95
|
+
{course && (
|
|
96
|
+
<p>
|
|
97
|
+
<a href={courseUrl(basePath, course.slug)}>Return to course overview →</a>
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
</section>
|
|
101
|
+
)}
|
|
102
|
+
</LearningShell>
|
|
103
|
+
</Layout>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import { lmsLearner, loginUrl, courseUrl, LmsError } from "@anantagnanam/lms/server";
|
|
5
|
+
import { basePath, loginPath } from "virtual:anantagnanam-lms/config";
|
|
6
|
+
import type { Page, CourseCard, LearnerPath, LearnerPreferences } from "@anantagnanam/lms/types";
|
|
7
|
+
export const prerender = false;
|
|
8
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
9
|
+
if (!Astro.locals.user)
|
|
10
|
+
return Astro.redirect(loginUrl(loginPath, Astro.url.pathname + Astro.url.search));
|
|
11
|
+
let saved: Page<{ courseId: string; course: CourseCard | null }> = { items: [], hasMore: false };
|
|
12
|
+
let paths: Page<LearnerPath> = { items: [], hasMore: false };
|
|
13
|
+
let preferences: { value: LearnerPreferences; revision: string | null } | undefined;
|
|
14
|
+
let error = "";
|
|
15
|
+
try {
|
|
16
|
+
[saved, paths, preferences] = await Promise.all([
|
|
17
|
+
lmsLearner<typeof saved>(
|
|
18
|
+
Astro.locals,
|
|
19
|
+
"learner/saved",
|
|
20
|
+
{ cursor: Astro.url.searchParams.get("saved") ?? undefined },
|
|
21
|
+
Astro.url.origin,
|
|
22
|
+
),
|
|
23
|
+
lmsLearner<typeof paths>(
|
|
24
|
+
Astro.locals,
|
|
25
|
+
"learner/paths",
|
|
26
|
+
{ cursor: Astro.url.searchParams.get("paths") ?? undefined },
|
|
27
|
+
Astro.url.origin,
|
|
28
|
+
),
|
|
29
|
+
lmsLearner<NonNullable<typeof preferences>>(
|
|
30
|
+
Astro.locals,
|
|
31
|
+
"learner/preferences",
|
|
32
|
+
{},
|
|
33
|
+
Astro.url.origin,
|
|
34
|
+
),
|
|
35
|
+
]);
|
|
36
|
+
} catch (cause) {
|
|
37
|
+
Astro.response.status = cause instanceof LmsError ? cause.status : 503;
|
|
38
|
+
error = cause instanceof Error ? cause.message : "Your library is unavailable.";
|
|
39
|
+
}
|
|
40
|
+
const more = (key: string, cursor: string) => {
|
|
41
|
+
const url = new URL(Astro.url);
|
|
42
|
+
url.searchParams.set(key, cursor);
|
|
43
|
+
return url.href;
|
|
44
|
+
};
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<Layout title="My library and preferences" robots="noindex, follow">
|
|
48
|
+
<LearningShell>
|
|
49
|
+
<h1>My library & preferences</h1>
|
|
50
|
+
{error && <p role="alert">{error}</p>}
|
|
51
|
+
<section>
|
|
52
|
+
<h2>Saved courses</h2>
|
|
53
|
+
{!error && !saved.items.length && (
|
|
54
|
+
<p>Save a course from its overview to find it here later.</p>
|
|
55
|
+
)}
|
|
56
|
+
{saved.items.map(({ courseId, course }) => (
|
|
57
|
+
<article class="lms-learning-card">
|
|
58
|
+
<h3>
|
|
59
|
+
{course ? (
|
|
60
|
+
<a href={courseUrl(basePath, course.slug)}>{course.title}</a>
|
|
61
|
+
) : (
|
|
62
|
+
"Course currently unavailable"
|
|
63
|
+
)}
|
|
64
|
+
</h3>
|
|
65
|
+
<form data-lms-route="learner/saved/save" data-lms-reload>
|
|
66
|
+
<input type="hidden" name="courseId" value={courseId} />
|
|
67
|
+
<input type="hidden" name="saved" value="false" />
|
|
68
|
+
<button class="lms-text-button" type="submit">
|
|
69
|
+
Remove from saved courses
|
|
70
|
+
</button>
|
|
71
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
72
|
+
</form>
|
|
73
|
+
</article>
|
|
74
|
+
))}
|
|
75
|
+
{saved.hasMore && saved.cursor && (
|
|
76
|
+
<a href={more("saved", saved.cursor)}>More saved courses →</a>
|
|
77
|
+
)}
|
|
78
|
+
</section>
|
|
79
|
+
<section>
|
|
80
|
+
<h2>My learning paths</h2>
|
|
81
|
+
<p>
|
|
82
|
+
Your joined paths keep their original course lists when administrators edit the program.
|
|
83
|
+
</p>
|
|
84
|
+
{!error && !paths.items.length && <a href={`${basePath}/paths`}>Explore learning paths</a>}
|
|
85
|
+
{paths.items.map((path) => (
|
|
86
|
+
<article class="lms-learning-card">
|
|
87
|
+
<h3>{path.title}</h3>
|
|
88
|
+
<p>
|
|
89
|
+
{path.completed} of {path.total} courses completed
|
|
90
|
+
</p>
|
|
91
|
+
<progress value={path.completed} max={path.total} aria-label={`${path.title} progress`}>
|
|
92
|
+
{path.completed}/{path.total}
|
|
93
|
+
</progress>
|
|
94
|
+
<ol>
|
|
95
|
+
{path.courses.map(({ course, completed }) => (
|
|
96
|
+
<li>
|
|
97
|
+
{course ? (
|
|
98
|
+
<a href={courseUrl(basePath, course.slug)}>{course.title}</a>
|
|
99
|
+
) : (
|
|
100
|
+
"Course currently unavailable"
|
|
101
|
+
)}
|
|
102
|
+
{completed && " · Completed"}
|
|
103
|
+
</li>
|
|
104
|
+
))}
|
|
105
|
+
</ol>
|
|
106
|
+
</article>
|
|
107
|
+
))}
|
|
108
|
+
{paths.hasMore && paths.cursor && (
|
|
109
|
+
<a href={more("paths", paths.cursor)}>More joined paths →</a>
|
|
110
|
+
)}
|
|
111
|
+
</section>
|
|
112
|
+
{preferences && (
|
|
113
|
+
<section class="lms-learning-card">
|
|
114
|
+
<h2>Learning preferences</h2>
|
|
115
|
+
<form class="lms-form" data-lms-route="learner/preferences/save" data-lms-reload>
|
|
116
|
+
<input type="hidden" name="expectedRevision" value={preferences.revision ?? ""} />
|
|
117
|
+
<label>
|
|
118
|
+
<span>Email updates</span>
|
|
119
|
+
<select name="emailNotifications">
|
|
120
|
+
<option value="true" selected={preferences.value.emailNotifications}>
|
|
121
|
+
Enabled when the academy sends email
|
|
122
|
+
</option>
|
|
123
|
+
<option value="false" selected={!preferences.value.emailNotifications}>
|
|
124
|
+
Off
|
|
125
|
+
</option>
|
|
126
|
+
</select>
|
|
127
|
+
</label>
|
|
128
|
+
<label>
|
|
129
|
+
<span>Weekly lesson goal</span>
|
|
130
|
+
<input
|
|
131
|
+
type="number"
|
|
132
|
+
name="weeklyGoalLessons"
|
|
133
|
+
min="0"
|
|
134
|
+
max="100"
|
|
135
|
+
value={preferences.value.weeklyGoalLessons}
|
|
136
|
+
/>
|
|
137
|
+
</label>
|
|
138
|
+
<p>
|
|
139
|
+
Set 0 for no goal. Your goal is a personal target; it does not change deadlines or
|
|
140
|
+
completion requirements.
|
|
141
|
+
</p>
|
|
142
|
+
<button class="lms-btn" type="submit">
|
|
143
|
+
Save preferences
|
|
144
|
+
</button>
|
|
145
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
146
|
+
</form>
|
|
147
|
+
</section>
|
|
148
|
+
)}
|
|
149
|
+
<section class="lms-learning-card" data-lms-export>
|
|
150
|
+
<h2>Download my learning data</h2>
|
|
151
|
+
<p>
|
|
152
|
+
Download each section as JSON, five records per page. Continue until all pages are
|
|
153
|
+
downloaded. Exports include only your records and omit quiz answer keys.
|
|
154
|
+
</p>
|
|
155
|
+
<label>
|
|
156
|
+
Data section{" "}
|
|
157
|
+
<select data-lms-export-section>
|
|
158
|
+
<option value="enrollments">Learning history, notes and assessments</option>
|
|
159
|
+
<option value="discussions">Discussion contributions</option>
|
|
160
|
+
<option value="reviews">Course reviews</option>
|
|
161
|
+
<option value="notifications">Notifications</option>
|
|
162
|
+
<option value="saved_courses">Saved courses</option>
|
|
163
|
+
<option value="path_enrollments">Joined paths</option>
|
|
164
|
+
<option value="cohort_enrollments">Cohort membership</option>
|
|
165
|
+
</select>
|
|
166
|
+
</label>
|
|
167
|
+
<button type="button" class="lms-btn" data-lms-export-download>
|
|
168
|
+
Download first page
|
|
169
|
+
</button>
|
|
170
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
171
|
+
</section>
|
|
172
|
+
</LearningShell>
|
|
173
|
+
</Layout>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "virtual:anantagnanam-lms/layout";
|
|
3
|
+
import LearningShell from "../components/LearningShell.astro";
|
|
4
|
+
import { lmsPublic, courseUrl, loginUrl, LmsError } from "@anantagnanam/lms/server";
|
|
5
|
+
import { basePath, loginPath } from "virtual:anantagnanam-lms/config";
|
|
6
|
+
import type { Page, PublicPath } from "@anantagnanam/lms/types";
|
|
7
|
+
export const prerender = false;
|
|
8
|
+
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
9
|
+
let page: Page<PublicPath> = { items: [], hasMore: false };
|
|
10
|
+
let error = "";
|
|
11
|
+
try {
|
|
12
|
+
page = await lmsPublic<typeof page>(
|
|
13
|
+
Astro.locals,
|
|
14
|
+
"paths",
|
|
15
|
+
{ cursor: Astro.url.searchParams.get("cursor") ?? undefined },
|
|
16
|
+
Astro.url.origin,
|
|
17
|
+
);
|
|
18
|
+
} catch (cause) {
|
|
19
|
+
Astro.response.status = cause instanceof LmsError ? cause.status : 503;
|
|
20
|
+
error = cause instanceof Error ? cause.message : "Learning paths unavailable.";
|
|
21
|
+
}
|
|
22
|
+
const next = new URL(Astro.url);
|
|
23
|
+
next.searchParams.set("cursor", page.cursor ?? "");
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<Layout title="Learning paths">
|
|
27
|
+
<LearningShell>
|
|
28
|
+
<h1>Learning paths</h1>
|
|
29
|
+
<p class="lms-lead">
|
|
30
|
+
Follow an ordered program of courses and track your progress in your library.
|
|
31
|
+
</p>
|
|
32
|
+
<p>
|
|
33
|
+
Join a path to save its course list. Enroll in each course separately; approval and
|
|
34
|
+
prerequisites still apply.
|
|
35
|
+
</p>
|
|
36
|
+
{error && <p role="alert">{error}</p>}
|
|
37
|
+
{!error && !page.items.length && <p>No learning paths have been published yet.</p>}
|
|
38
|
+
{page.items.map((path) => (
|
|
39
|
+
<article class="lms-learning-card">
|
|
40
|
+
<h2>{path.title}</h2>
|
|
41
|
+
<p class="lms-text">{path.description}</p>
|
|
42
|
+
<ol>
|
|
43
|
+
{path.courses.map((course) => (
|
|
44
|
+
<li>
|
|
45
|
+
{course ? (
|
|
46
|
+
<a href={courseUrl(basePath, course.slug)}>{course.title}</a>
|
|
47
|
+
) : (
|
|
48
|
+
"Course currently unavailable"
|
|
49
|
+
)}
|
|
50
|
+
</li>
|
|
51
|
+
))}
|
|
52
|
+
</ol>
|
|
53
|
+
{Astro.locals.user ? (
|
|
54
|
+
<form data-lms-route="learner/path/join">
|
|
55
|
+
<input type="hidden" name="pathId" value={path.id} />
|
|
56
|
+
<button class="lms-btn" type="submit">
|
|
57
|
+
Join path
|
|
58
|
+
</button>
|
|
59
|
+
<a class="lms-btn lms-btn-outline" href={`${basePath}/my-learning/library`}>
|
|
60
|
+
View my paths
|
|
61
|
+
</a>
|
|
62
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
63
|
+
</form>
|
|
64
|
+
) : (
|
|
65
|
+
<a class="lms-btn" href={loginUrl(loginPath, Astro.url.pathname)}>
|
|
66
|
+
Sign in to join
|
|
67
|
+
</a>
|
|
68
|
+
)}
|
|
69
|
+
</article>
|
|
70
|
+
))}
|
|
71
|
+
{page.hasMore && page.cursor && <a href={next.href}>More learning paths →</a>}
|
|
72
|
+
</LearningShell>
|
|
73
|
+
</Layout>
|