@anantagnanam/lms 0.0.0-stage → 0.1.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 +77 -2
- package/astro/client.ts +433 -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 +286 -0
- package/astro/components/LearnerDashboard.astro +121 -0
- package/astro/components/LearningShell.astro +28 -0
- package/astro/components/LessonPlayer.astro +323 -0
- package/astro/components/MediaPlayer.astro +60 -0
- package/astro/components/index.ts +7 -0
- package/astro/layouts/AcademyLayout.astro +84 -0
- package/astro/pages/certificate.astro +57 -0
- package/astro/pages/cohort.astro +102 -0
- package/astro/pages/course.astro +80 -0
- package/astro/pages/dashboard.astro +34 -0
- package/astro/pages/index.astro +40 -0
- package/astro/pages/lesson.astro +100 -0
- package/astro/pages/teach.astro +15 -0
- package/astro/styles.css +1288 -0
- package/dist/admin.d.ts +32 -0
- package/dist/admin.js +2762 -0
- package/dist/admin.js.map +1 -0
- package/dist/astro.d.ts +11 -0
- package/dist/astro.js +49 -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-C0kT94tc.js +139 -0
- package/dist/constants-C0kT94tc.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 +681 -0
- package/dist/runtime.js +2060 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schemas.d.ts +584 -0
- package/dist/schemas.js +315 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +30 -0
- package/dist/server.js +54 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +284 -0
- package/dist/types.js +0 -0
- package/package.json +83 -4
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { PortableText, type TypedObject } from "emdash/ui";
|
|
3
|
+
import MediaPlayer from "./MediaPlayer.astro";
|
|
4
|
+
import type {
|
|
5
|
+
CourseDetail,
|
|
6
|
+
PublicLesson,
|
|
7
|
+
PublicEnrollment,
|
|
8
|
+
ProgressSummary,
|
|
9
|
+
} from "@anantagnanam/lms/types";
|
|
10
|
+
import { courseUrl, lessonUrl, certificateUrl } from "@anantagnanam/lms/server";
|
|
11
|
+
import { basePath } from "virtual:anantagnanam-lms/config";
|
|
12
|
+
interface Props {
|
|
13
|
+
course: CourseDetail;
|
|
14
|
+
lesson: PublicLesson;
|
|
15
|
+
enrollment?: PublicEnrollment | null;
|
|
16
|
+
progress?: ProgressSummary;
|
|
17
|
+
preview?: boolean;
|
|
18
|
+
}
|
|
19
|
+
const { course, lesson, enrollment = null, progress, preview = false } = Astro.props;
|
|
20
|
+
const all = course.modules.flatMap((module) => module.lessons);
|
|
21
|
+
const index = all.findIndex((item) => item.id === lesson.id);
|
|
22
|
+
const nextLesson = all[index + 1];
|
|
23
|
+
const nextUrl = nextLesson
|
|
24
|
+
? lessonUrl(basePath, course.slug, nextLesson.id)
|
|
25
|
+
: `${basePath}/my-learning`;
|
|
26
|
+
const current = enrollment?.progress[lesson.id];
|
|
27
|
+
const complete = Boolean(current?.completedAt);
|
|
28
|
+
const attempt = current?.quizAttempts.at(-1);
|
|
29
|
+
const assignment = current?.assignments.at(-1);
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<nav class="lms-breadcrumbs" aria-label="Breadcrumb">
|
|
33
|
+
<a href={basePath}>Courses</a>
|
|
34
|
+
<span>/</span>
|
|
35
|
+
<a href={courseUrl(basePath, course.slug)}>{course.title}</a>
|
|
36
|
+
<span>/</span>
|
|
37
|
+
<span>{lesson.title}</span>
|
|
38
|
+
</nav>
|
|
39
|
+
<div
|
|
40
|
+
class="lms-reader-grid"
|
|
41
|
+
data-lms-lesson
|
|
42
|
+
data-course-id={course.id}
|
|
43
|
+
data-lesson-id={lesson.id}
|
|
44
|
+
data-preview={preview ? "true" : "false"}
|
|
45
|
+
>
|
|
46
|
+
<aside class="lms-course-sidebar">
|
|
47
|
+
<a href={courseUrl(basePath, course.slug)} class="lms-sidebar-course">
|
|
48
|
+
{course.title}
|
|
49
|
+
</a>
|
|
50
|
+
{progress && (
|
|
51
|
+
<>
|
|
52
|
+
<p>{progress.percent}% complete</p>
|
|
53
|
+
<progress max="100" value={progress.percent}>
|
|
54
|
+
{progress.percent}%
|
|
55
|
+
</progress>
|
|
56
|
+
</>
|
|
57
|
+
)}
|
|
58
|
+
{course.modules.map((module, moduleIndex) => (
|
|
59
|
+
<details open={module.lessons.some((entry) => entry.id === lesson.id)}>
|
|
60
|
+
<summary>
|
|
61
|
+
<small>MODULE {moduleIndex + 1}</small>
|
|
62
|
+
{module.title}
|
|
63
|
+
</summary>
|
|
64
|
+
<ol>
|
|
65
|
+
{module.lessons.map((entry) => (
|
|
66
|
+
<li>
|
|
67
|
+
<a
|
|
68
|
+
href={lessonUrl(basePath, course.slug, entry.id)}
|
|
69
|
+
aria-current={entry.id === lesson.id ? "page" : undefined}
|
|
70
|
+
>
|
|
71
|
+
<span aria-hidden="true">
|
|
72
|
+
{enrollment?.progress[entry.id]?.completedAt ? "✓" : "○"}
|
|
73
|
+
</span>
|
|
74
|
+
<span>
|
|
75
|
+
{entry.title}
|
|
76
|
+
<small>
|
|
77
|
+
{entry.kind} · {entry.durationMinutes} min
|
|
78
|
+
</small>
|
|
79
|
+
</span>
|
|
80
|
+
</a>
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ol>
|
|
84
|
+
</details>
|
|
85
|
+
))}
|
|
86
|
+
</aside>
|
|
87
|
+
<article class="lms-lesson">
|
|
88
|
+
<header>
|
|
89
|
+
<p class="lms-eyebrow">
|
|
90
|
+
LESSON {index + 1} OF {all.length} · {lesson.kind.toUpperCase()}
|
|
91
|
+
{preview ? " · FREE PREVIEW" : ""}
|
|
92
|
+
</p>
|
|
93
|
+
<h1>{lesson.title}</h1>
|
|
94
|
+
{lesson.summary && <p class="lms-lead">{lesson.summary}</p>}
|
|
95
|
+
</header>
|
|
96
|
+
{preview && (
|
|
97
|
+
<p class="lms-notice">
|
|
98
|
+
You're viewing a free preview.{" "}
|
|
99
|
+
<a href={courseUrl(basePath, course.slug)}>Enroll to save your progress →</a>
|
|
100
|
+
</p>
|
|
101
|
+
)}
|
|
102
|
+
{lesson.mediaUrl && ["video", "audio", "live"].includes(lesson.kind) && (
|
|
103
|
+
<MediaPlayer
|
|
104
|
+
url={lesson.mediaUrl}
|
|
105
|
+
kind={lesson.kind as "video" | "audio" | "live"}
|
|
106
|
+
title={lesson.title}
|
|
107
|
+
startsAt={lesson.liveStartsAt}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
<div class="lms-lesson-body">
|
|
111
|
+
{lesson.portableText.length > 0 ? (
|
|
112
|
+
<PortableText value={lesson.portableText as TypedObject[]} />
|
|
113
|
+
) : (
|
|
114
|
+
lesson.body
|
|
115
|
+
.split(/\n\s*\n/)
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
.map((paragraph) => <p class="lms-text">{paragraph}</p>)
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
{lesson.resources.length > 0 && (
|
|
121
|
+
<section class="lms-resources">
|
|
122
|
+
<h2>Keep exploring</h2>
|
|
123
|
+
<ul>
|
|
124
|
+
{lesson.resources.map((resource) => (
|
|
125
|
+
<li>
|
|
126
|
+
<a href={resource.url} target="_blank" rel="noopener noreferrer">
|
|
127
|
+
<span aria-hidden="true">↗</span>
|
|
128
|
+
<span>
|
|
129
|
+
{resource.title}
|
|
130
|
+
{resource.description && <small>{resource.description}</small>}
|
|
131
|
+
</span>
|
|
132
|
+
</a>
|
|
133
|
+
</li>
|
|
134
|
+
))}
|
|
135
|
+
</ul>
|
|
136
|
+
</section>
|
|
137
|
+
)}
|
|
138
|
+
{!preview && lesson.kind === "quiz" && lesson.quiz && (
|
|
139
|
+
<section class="lms-assessment">
|
|
140
|
+
<p class="lms-eyebrow">CHECK YOUR UNDERSTANDING</p>
|
|
141
|
+
<h2>Lesson quiz</h2>
|
|
142
|
+
<p>
|
|
143
|
+
{lesson.quiz.questionCount} questions · Pass with {lesson.quiz.passingPercent}% ·{" "}
|
|
144
|
+
{lesson.quiz.maxAttempts} attempts
|
|
145
|
+
{lesson.quiz.timeLimitMinutes
|
|
146
|
+
? ` · ${lesson.quiz.timeLimitMinutes} minutes per attempt`
|
|
147
|
+
: ""}
|
|
148
|
+
</p>
|
|
149
|
+
{complete ? (
|
|
150
|
+
<p class="lms-notice">✓ Quiz passed. {attempt?.percent}%</p>
|
|
151
|
+
) : (
|
|
152
|
+
<div
|
|
153
|
+
data-lms-quiz
|
|
154
|
+
data-course-id={course.id}
|
|
155
|
+
data-lesson-id={lesson.id}
|
|
156
|
+
data-next-url={nextUrl}
|
|
157
|
+
>
|
|
158
|
+
<button class="lms-btn" type="button" data-lms-quiz-start>
|
|
159
|
+
{attempt?.status === "in_progress" ? "Resume quiz" : "Start quiz"}
|
|
160
|
+
</button>
|
|
161
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
162
|
+
<div data-lms-quiz-content></div>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
{current?.quizAttempts.length ? (
|
|
166
|
+
<details>
|
|
167
|
+
<summary>Your quiz attempts</summary>
|
|
168
|
+
<ol>
|
|
169
|
+
{current.quizAttempts.map((item) => (
|
|
170
|
+
<li>
|
|
171
|
+
{new Date(item.startedAt).toLocaleString("en-IN")} ·{" "}
|
|
172
|
+
{item.status === "expired"
|
|
173
|
+
? "Time expired"
|
|
174
|
+
: item.status === "in_progress"
|
|
175
|
+
? "In progress"
|
|
176
|
+
: `${item.percent}% · ${item.passed ? "Passed" : "Try again"}`}
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
</ol>
|
|
180
|
+
</details>
|
|
181
|
+
) : null}
|
|
182
|
+
</section>
|
|
183
|
+
)}
|
|
184
|
+
{!preview && lesson.kind === "assignment" && lesson.assignment && (
|
|
185
|
+
<section class="lms-assessment">
|
|
186
|
+
<p class="lms-eyebrow">PUT LEARNING INTO PRACTICE</p>
|
|
187
|
+
<h2>Your assignment</h2>
|
|
188
|
+
<p class="lms-text">{lesson.assignment.instructions}</p>
|
|
189
|
+
<p>
|
|
190
|
+
{lesson.assignment.passingPoints}/{lesson.assignment.maxPoints} points to pass
|
|
191
|
+
{lesson.assignment.dueAt
|
|
192
|
+
? ` · Due ${new Date(lesson.assignment.dueAt).toLocaleString("en-IN")}`
|
|
193
|
+
: ""}
|
|
194
|
+
</p>
|
|
195
|
+
{assignment && (
|
|
196
|
+
<div class="lms-notice">
|
|
197
|
+
<strong>
|
|
198
|
+
{assignment.status === "submitted"
|
|
199
|
+
? "Submitted · awaiting feedback"
|
|
200
|
+
: `Graded · ${assignment.points}/${assignment.maxPoints}`}
|
|
201
|
+
</strong>
|
|
202
|
+
<p class="lms-text">{assignment.feedback}</p>
|
|
203
|
+
<details>
|
|
204
|
+
<summary>Your submission</summary>
|
|
205
|
+
<p class="lms-text">{assignment.body}</p>
|
|
206
|
+
{assignment.attachmentUrl && (
|
|
207
|
+
<a href={assignment.attachmentUrl} target="_blank" rel="noopener noreferrer">
|
|
208
|
+
Open attachment ↗
|
|
209
|
+
</a>
|
|
210
|
+
)}
|
|
211
|
+
</details>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
{!complete &&
|
|
215
|
+
(!assignment ||
|
|
216
|
+
(assignment.status === "graded" && lesson.assignment.allowResubmission)) && (
|
|
217
|
+
<form class="lms-form" data-lms-route="learner/assignment" data-lms-reload>
|
|
218
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
219
|
+
<input type="hidden" name="lessonId" value={lesson.id} />
|
|
220
|
+
<label>
|
|
221
|
+
<span>Your work</span>
|
|
222
|
+
<textarea name="body" required rows={8} maxlength={20_000}></textarea>
|
|
223
|
+
</label>
|
|
224
|
+
<label>
|
|
225
|
+
<span>Attachment link (optional)</span>
|
|
226
|
+
<input name="attachmentUrl" type="url" placeholder="https://…" />
|
|
227
|
+
</label>
|
|
228
|
+
<button class="lms-btn" type="submit">
|
|
229
|
+
Submit assignment
|
|
230
|
+
</button>
|
|
231
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
232
|
+
</form>
|
|
233
|
+
)}
|
|
234
|
+
</section>
|
|
235
|
+
)}
|
|
236
|
+
{!preview && !["quiz", "assignment"].includes(lesson.kind) && (
|
|
237
|
+
<section class="lms-lesson-completion">
|
|
238
|
+
{complete ? (
|
|
239
|
+
<p class="lms-notice">✓ You've completed this lesson.</p>
|
|
240
|
+
) : (
|
|
241
|
+
<form data-lms-route="learner/complete" data-lms-reload>
|
|
242
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
243
|
+
<input type="hidden" name="lessonId" value={lesson.id} />
|
|
244
|
+
<button class="lms-btn" type="submit" data-lms-complete>
|
|
245
|
+
Mark lesson complete ✓
|
|
246
|
+
</button>
|
|
247
|
+
{lesson.minStudySeconds > 0 && (
|
|
248
|
+
<p class="lms-muted">
|
|
249
|
+
Study this lesson for at least {lesson.minStudySeconds} seconds before marking it
|
|
250
|
+
complete.
|
|
251
|
+
</p>
|
|
252
|
+
)}
|
|
253
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
254
|
+
</form>
|
|
255
|
+
)}
|
|
256
|
+
</section>
|
|
257
|
+
)}
|
|
258
|
+
{enrollment?.certificate && (
|
|
259
|
+
<div class="lms-achievement">
|
|
260
|
+
<span aria-hidden="true">✧</span>
|
|
261
|
+
<div>
|
|
262
|
+
<h2>Look how far you've come</h2>
|
|
263
|
+
<p>You've completed this learning path.</p>
|
|
264
|
+
<a class="lms-btn" href={certificateUrl(basePath, enrollment.certificate.id)}>
|
|
265
|
+
View your certificate →
|
|
266
|
+
</a>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
<nav class="lms-lesson-navigation" aria-label="Lesson navigation">
|
|
271
|
+
{index > 0 ? (
|
|
272
|
+
<a href={lessonUrl(basePath, course.slug, all[index - 1]!.id)}>← Previous lesson</a>
|
|
273
|
+
) : (
|
|
274
|
+
<a href={courseUrl(basePath, course.slug)}>← Course overview</a>
|
|
275
|
+
)}
|
|
276
|
+
{index + 1 < all.length && (
|
|
277
|
+
<a href={lessonUrl(basePath, course.slug, all[index + 1]!.id)}>Next lesson →</a>
|
|
278
|
+
)}
|
|
279
|
+
</nav>
|
|
280
|
+
{!preview && (
|
|
281
|
+
<>
|
|
282
|
+
<section class="lms-content-section">
|
|
283
|
+
<h2>A note to your future self</h2>
|
|
284
|
+
<p class="lms-muted">Only you can see these notes.</p>
|
|
285
|
+
<form class="lms-form" data-lms-route="learner/note">
|
|
286
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
287
|
+
<input type="hidden" name="lessonId" value={lesson.id} />
|
|
288
|
+
<label>
|
|
289
|
+
<span>Your lesson notes</span>
|
|
290
|
+
<textarea name="note" rows={5} maxlength={10_000}>{current?.note ?? ""}</textarea>
|
|
291
|
+
</label>
|
|
292
|
+
<button class="lms-btn lms-btn-outline" type="submit">
|
|
293
|
+
Save notes
|
|
294
|
+
</button>
|
|
295
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
296
|
+
</form>
|
|
297
|
+
</section>
|
|
298
|
+
<section
|
|
299
|
+
class="lms-content-section"
|
|
300
|
+
data-lms-discussions
|
|
301
|
+
data-course-id={course.id}
|
|
302
|
+
data-lesson-id={lesson.id}
|
|
303
|
+
>
|
|
304
|
+
<h2>Learn together</h2>
|
|
305
|
+
<p class="lms-muted">Ask a question, share an insight, or help another learner.</p>
|
|
306
|
+
<div data-lms-discussion-list></div>
|
|
307
|
+
<form class="lms-form" data-lms-route="learner/discussion">
|
|
308
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
309
|
+
<input type="hidden" name="lessonId" value={lesson.id} />
|
|
310
|
+
<label>
|
|
311
|
+
<span>Your contribution</span>
|
|
312
|
+
<textarea name="body" rows={4} required maxlength={5000}></textarea>
|
|
313
|
+
</label>
|
|
314
|
+
<button class="lms-btn lms-btn-outline" type="submit">
|
|
315
|
+
Post to discussion
|
|
316
|
+
</button>
|
|
317
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
318
|
+
</form>
|
|
319
|
+
</section>
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
</article>
|
|
323
|
+
</div>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
url: string;
|
|
4
|
+
kind: "video" | "audio" | "live";
|
|
5
|
+
title: string;
|
|
6
|
+
startsAt?: string | null;
|
|
7
|
+
}
|
|
8
|
+
const { url, kind, title, startsAt } = Astro.props;
|
|
9
|
+
let embed: string | null = null;
|
|
10
|
+
try {
|
|
11
|
+
const parsed = new URL(url, Astro.url.origin);
|
|
12
|
+
if (["youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be"].includes(parsed.hostname)) {
|
|
13
|
+
const id =
|
|
14
|
+
parsed.hostname === "youtu.be"
|
|
15
|
+
? parsed.pathname.slice(1)
|
|
16
|
+
: (parsed.searchParams.get("v") ??
|
|
17
|
+
parsed.pathname.match(/^\/(?:embed|shorts)\/([^/]+)/)?.[1]);
|
|
18
|
+
if (id && /^[a-zA-Z0-9_-]{11}$/.test(id))
|
|
19
|
+
embed = `https://www.youtube-nocookie.com/embed/${id}`;
|
|
20
|
+
} else if (["vimeo.com", "www.vimeo.com", "player.vimeo.com"].includes(parsed.hostname)) {
|
|
21
|
+
const id = parsed.pathname.match(/\/(?:video\/)?(\d+)$/)?.[1];
|
|
22
|
+
if (id) embed = `https://player.vimeo.com/video/${id}`;
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<div class="lms-media">
|
|
28
|
+
{kind === "live" ? (
|
|
29
|
+
<div class="lms-live-card">
|
|
30
|
+
<p class="lms-eyebrow">LIVE LEARNING SESSION</p>
|
|
31
|
+
<h2>{title}</h2>
|
|
32
|
+
{startsAt && (
|
|
33
|
+
<p>
|
|
34
|
+
Starts{" "}
|
|
35
|
+
{new Date(startsAt).toLocaleString("en-IN", { dateStyle: "long", timeStyle: "short" })}
|
|
36
|
+
</p>
|
|
37
|
+
)}
|
|
38
|
+
<a class="lms-btn" href={url} target="_blank" rel="noopener noreferrer">
|
|
39
|
+
Open meeting ↗
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
) : kind === "audio" ? (
|
|
43
|
+
<audio src={url} controls preload="metadata" aria-label={title}></audio>
|
|
44
|
+
) : embed ? (
|
|
45
|
+
<iframe
|
|
46
|
+
src={embed}
|
|
47
|
+
title={title}
|
|
48
|
+
allow="fullscreen; picture-in-picture; encrypted-media"
|
|
49
|
+
allowfullscreen
|
|
50
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
51
|
+
></iframe>
|
|
52
|
+
) : (
|
|
53
|
+
<video src={url} controls preload="metadata" aria-label={title}></video>
|
|
54
|
+
)}
|
|
55
|
+
{kind !== "live" && (
|
|
56
|
+
<a class="lms-media-link" href={url} target="_blank" rel="noopener noreferrer">
|
|
57
|
+
Open media separately ↗
|
|
58
|
+
</a>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as LearningShell } from "./LearningShell.astro";
|
|
2
|
+
export { default as CourseCatalog } from "./CourseCatalog.astro";
|
|
3
|
+
export { default as CourseOverview } from "./CourseOverview.astro";
|
|
4
|
+
export { default as LessonPlayer } from "./LessonPlayer.astro";
|
|
5
|
+
export { default as LearnerDashboard } from "./LearnerDashboard.astro";
|
|
6
|
+
export { default as CertificateCard } from "./CertificateCard.astro";
|
|
7
|
+
export { default as CourseCard } from "./CourseCard.astro";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { basePath, loginPath } from "virtual:anantagnanam-lms/config";
|
|
3
|
+
import { loginUrl } from "@anantagnanam/lms/server";
|
|
4
|
+
import "../styles.css";
|
|
5
|
+
interface Props {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
robots?: string;
|
|
9
|
+
}
|
|
10
|
+
const {
|
|
11
|
+
title,
|
|
12
|
+
description = "Courses and guided learning from Ananta Gnanam.",
|
|
13
|
+
robots,
|
|
14
|
+
} = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<!doctype html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="utf-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
+
<title>{title} · Ananta Gnanam</title>
|
|
23
|
+
<meta name="description" content={description} />
|
|
24
|
+
{robots && <meta name="robots" content={robots} />}
|
|
25
|
+
<style is:global>
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
background: #f7faf8;
|
|
29
|
+
color: #18271e;
|
|
30
|
+
font-family:
|
|
31
|
+
ui-sans-serif,
|
|
32
|
+
system-ui,
|
|
33
|
+
-apple-system,
|
|
34
|
+
BlinkMacSystemFont,
|
|
35
|
+
"Segoe UI",
|
|
36
|
+
sans-serif;
|
|
37
|
+
}
|
|
38
|
+
a {
|
|
39
|
+
color: inherit;
|
|
40
|
+
}
|
|
41
|
+
html {
|
|
42
|
+
scroll-behavior: smooth;
|
|
43
|
+
}
|
|
44
|
+
@media (prefers-reduced-motion: reduce) {
|
|
45
|
+
html {
|
|
46
|
+
scroll-behavior: auto;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<div class="lms-root">
|
|
53
|
+
<a class="lms-skip" href="#lms-main">
|
|
54
|
+
Skip to learning content
|
|
55
|
+
</a>
|
|
56
|
+
<header class="lms-site-header">
|
|
57
|
+
<a class="lms-brand" href={basePath}>
|
|
58
|
+
<span class="lms-brand-mark" aria-hidden="true">
|
|
59
|
+
∞
|
|
60
|
+
</span>
|
|
61
|
+
<span>
|
|
62
|
+
Ananta Gnanam<small>ACADEMY</small>
|
|
63
|
+
</span>
|
|
64
|
+
</a>
|
|
65
|
+
<nav aria-label="Academy navigation">
|
|
66
|
+
<a href={basePath}>Explore courses</a>
|
|
67
|
+
<a href={`${basePath}/my-learning`}>My learning</a>
|
|
68
|
+
<a href="/">Main site ↗</a>
|
|
69
|
+
{!Astro.locals.user && (
|
|
70
|
+
<a class="lms-btn lms-btn-small" href={loginUrl(loginPath, Astro.url.pathname)}>
|
|
71
|
+
Sign in
|
|
72
|
+
</a>
|
|
73
|
+
)}
|
|
74
|
+
</nav>
|
|
75
|
+
</header>
|
|
76
|
+
<slot />
|
|
77
|
+
<footer class="lms-site-footer">
|
|
78
|
+
<a href="/">Ananta Gnanam</a>
|
|
79
|
+
<p>Infinite knowledge. Lifelong learning.</p>
|
|
80
|
+
<a href={basePath}>Keep exploring →</a>
|
|
81
|
+
</footer>
|
|
82
|
+
</div>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
@@ -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>
|