@anantagnanam/lms 0.1.0 → 0.3.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 -21
- package/README.md +40 -2
- package/astro/assignment-player.ts +231 -0
- package/astro/client.ts +97 -152
- package/astro/components/AssignmentFeedback.astro +32 -0
- package/astro/components/AssignmentRubric.astro +27 -0
- package/astro/components/CourseOverview.astro +25 -3
- package/astro/components/LearnerDashboard.astro +9 -0
- package/astro/components/LearningShell.astro +16 -1
- package/astro/components/LessonAvailabilityNote.astro +26 -0
- package/astro/components/LessonContent.astro +67 -0
- package/astro/components/LessonPlayer.astro +166 -99
- package/astro/components/MediaPlayer.astro +55 -4
- package/astro/draft-queue.ts +95 -0
- package/astro/media-player.ts +124 -0
- package/astro/pages/author-preview.astro +73 -0
- package/astro/pages/course.astro +10 -0
- package/astro/pages/lesson.astro +3 -0
- package/astro/pages/library.astro +173 -0
- package/astro/pages/paths.astro +73 -0
- package/astro/pages/study.astro +180 -0
- package/astro/quiz-drafts.ts +23 -0
- package/astro/quiz-player.ts +362 -0
- package/astro/study-tools.ts +386 -0
- package/astro/styles.css +30 -0
- package/astro/transcript-search.ts +56 -0
- package/dist/admin.d.ts +3 -1
- package/dist/admin.js +756 -127
- package/dist/admin.js.map +1 -1
- package/dist/assignments-CsuAjxGO.js +75 -0
- package/dist/assignments-CsuAjxGO.js.map +1 -0
- package/dist/astro.js +4 -0
- package/dist/astro.js.map +1 -1
- package/dist/{constants-C0kT94tc.js → constants-DyQbwNbP.js} +25 -3
- package/dist/constants-DyQbwNbP.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime.d.ts +129 -16
- package/dist/runtime.js +1201 -130
- package/dist/runtime.js.map +1 -1
- package/dist/schemas.d.ts +136 -3
- package/dist/schemas.js +80 -7
- package/dist/schemas.js.map +1 -1
- package/dist/server.d.ts +18 -1
- package/dist/server.js +17 -3
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +172 -4
- package/package.json +1 -1
- package/dist/constants-C0kT94tc.js.map +0 -1
- package/dist/errors-ChzJS9al.js +0 -16
- package/dist/errors-ChzJS9al.js.map +0 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AssignmentSubmission } from "@anantagnanam/lms/types";
|
|
3
|
+
import AssignmentRubric from "./AssignmentRubric.astro";
|
|
4
|
+
interface Props {
|
|
5
|
+
submission: AssignmentSubmission;
|
|
6
|
+
}
|
|
7
|
+
const { submission } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div class="lms-notice">
|
|
11
|
+
<strong>
|
|
12
|
+
{submission.status === "submitted"
|
|
13
|
+
? "Submitted · awaiting feedback"
|
|
14
|
+
: `Graded · ${submission.points}/${submission.maxPoints} · ${submission.points! >= submission.passingPoints ? "Passed" : "Below passing grade"}`}
|
|
15
|
+
</strong>
|
|
16
|
+
<p>Submitted {new Date(submission.submittedAt).toLocaleString("en-IN")}</p>
|
|
17
|
+
{submission.gradedAt && <p>Graded {new Date(submission.gradedAt).toLocaleString("en-IN")}</p>}
|
|
18
|
+
{submission.feedback && <p class="lms-text">{submission.feedback}</p>}
|
|
19
|
+
<AssignmentRubric
|
|
20
|
+
rubric={submission.rubric}
|
|
21
|
+
scores={submission.status === "graded" ? submission.rubricScores : undefined}
|
|
22
|
+
/>
|
|
23
|
+
<details>
|
|
24
|
+
<summary>Your submitted work</summary>
|
|
25
|
+
<p class="lms-text">{submission.body}</p>
|
|
26
|
+
{submission.attachmentUrl && (
|
|
27
|
+
<a href={submission.attachmentUrl} target="_blank" rel="noopener noreferrer">
|
|
28
|
+
Open attachment ↗
|
|
29
|
+
</a>
|
|
30
|
+
)}
|
|
31
|
+
</details>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { RubricCriterion, RubricScore } from "@anantagnanam/lms/types";
|
|
3
|
+
interface Props {
|
|
4
|
+
rubric?: RubricCriterion[];
|
|
5
|
+
scores?: RubricScore[];
|
|
6
|
+
}
|
|
7
|
+
const { rubric = [], scores } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
{rubric.length > 0 && (
|
|
11
|
+
<div class="lms-rubric">
|
|
12
|
+
<h3>{scores ? "Criterion feedback" : "Grading rubric"}</h3>
|
|
13
|
+
<ul>
|
|
14
|
+
{rubric.map((criterion) => {
|
|
15
|
+
const score = scores?.find((item) => item.criterionId === criterion.id);
|
|
16
|
+
return (
|
|
17
|
+
<li>
|
|
18
|
+
<strong>{criterion.title}</strong> · {score ? `${score.points}/` : ""}
|
|
19
|
+
{criterion.maxPoints} points
|
|
20
|
+
{criterion.description && <p class="lms-text">{criterion.description}</p>}
|
|
21
|
+
{score?.feedback && <p class="lms-text">{score.feedback}</p>}
|
|
22
|
+
</li>
|
|
23
|
+
);
|
|
24
|
+
})}
|
|
25
|
+
</ul>
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Image } from "emdash/ui";
|
|
3
|
+
import LessonAvailabilityNote from "./LessonAvailabilityNote.astro";
|
|
3
4
|
import type {
|
|
4
5
|
CourseDetail,
|
|
5
6
|
PublicEnrollment,
|
|
@@ -26,12 +27,17 @@ const {
|
|
|
26
27
|
cohortId,
|
|
27
28
|
cohortMembership = null,
|
|
28
29
|
} = Astro.props;
|
|
29
|
-
const firstLesson = course.modules.flatMap((module) => module.lessons)[0];
|
|
30
30
|
const active =
|
|
31
31
|
enrollment &&
|
|
32
32
|
["active", "completed"].includes(enrollment.status) &&
|
|
33
33
|
enrollment.effectiveAccess === "granted";
|
|
34
|
-
const next = progress?.nextLessonId
|
|
34
|
+
const next = progress?.nextLessonId;
|
|
35
|
+
const revisit =
|
|
36
|
+
!next && enrollment?.status === "completed"
|
|
37
|
+
? course.modules
|
|
38
|
+
.flatMap((module) => module.lessons)
|
|
39
|
+
.find((lesson) => progress?.availability[lesson.id]?.available)
|
|
40
|
+
: undefined;
|
|
35
41
|
---
|
|
36
42
|
|
|
37
43
|
<nav class="lms-breadcrumbs" aria-label="Breadcrumb">
|
|
@@ -73,6 +79,14 @@ const next = progress?.nextLessonId ?? firstLesson?.id;
|
|
|
73
79
|
{enrollment.status === "completed" ? "Revisit lessons" : "Continue learning"} →
|
|
74
80
|
</a>
|
|
75
81
|
)}
|
|
82
|
+
{revisit && (
|
|
83
|
+
<a class="lms-btn" href={lessonUrl(basePath, course.slug, revisit.id)}>
|
|
84
|
+
Revisit lessons →
|
|
85
|
+
</a>
|
|
86
|
+
)}
|
|
87
|
+
{!next && progress.blockedLessonId && (
|
|
88
|
+
<LessonAvailabilityNote availability={progress.availability[progress.blockedLessonId]} />
|
|
89
|
+
)}
|
|
76
90
|
{enrollment.certificate && (
|
|
77
91
|
<a
|
|
78
92
|
class="lms-btn lms-btn-outline"
|
|
@@ -97,6 +111,9 @@ const next = progress?.nextLessonId ?? firstLesson?.id;
|
|
|
97
111
|
: enrollment.decisionReason || "Contact an administrator to manage your access."}
|
|
98
112
|
</p>
|
|
99
113
|
<a href={`${basePath}/my-learning`}>Open My Learning →</a>
|
|
114
|
+
{progress?.blockedLessonId && (
|
|
115
|
+
<LessonAvailabilityNote availability={progress.availability[progress.blockedLessonId]} />
|
|
116
|
+
)}
|
|
100
117
|
</div>
|
|
101
118
|
) : !Astro.locals.user ? (
|
|
102
119
|
<a class="lms-btn" href={loginUrl(loginPath, Astro.url.pathname + Astro.url.search)}>
|
|
@@ -222,7 +239,11 @@ const next = progress?.nextLessonId ?? firstLesson?.id;
|
|
|
222
239
|
: String(index + 1).padStart(2, "0")}
|
|
223
240
|
</span>
|
|
224
241
|
<div>
|
|
225
|
-
{
|
|
242
|
+
{(
|
|
243
|
+
enrollment && ["active", "completed"].includes(enrollment.status)
|
|
244
|
+
? progress?.availability[lesson.id]?.available
|
|
245
|
+
: lesson.preview
|
|
246
|
+
) ? (
|
|
226
247
|
<a href={lessonUrl(basePath, course.slug, lesson.id)}>{lesson.title}</a>
|
|
227
248
|
) : (
|
|
228
249
|
<strong>{lesson.title}</strong>
|
|
@@ -231,6 +252,7 @@ const next = progress?.nextLessonId ?? firstLesson?.id;
|
|
|
231
252
|
{lesson.kind} · {lesson.durationMinutes} min
|
|
232
253
|
{!lesson.required ? " · optional" : ""}
|
|
233
254
|
</small>
|
|
255
|
+
<LessonAvailabilityNote availability={progress?.availability[lesson.id]} />
|
|
234
256
|
</div>
|
|
235
257
|
{lesson.preview && <span class="lms-badge">Preview</span>}
|
|
236
258
|
</li>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { CourseCard, PublicEnrollment, ProgressSummary, Page } from "@anantagnanam/lms/types";
|
|
3
3
|
import { courseUrl, lessonUrl, certificateUrl } from "@anantagnanam/lms/server";
|
|
4
4
|
import { basePath } from "virtual:anantagnanam-lms/config";
|
|
5
|
+
import LessonAvailabilityNote from "./LessonAvailabilityNote.astro";
|
|
5
6
|
interface Props {
|
|
6
7
|
page: Page<{ course: CourseCard; enrollment: PublicEnrollment; progress: ProgressSummary }>;
|
|
7
8
|
error?: string;
|
|
@@ -66,6 +67,11 @@ next.searchParams.set("cursor", page.cursor ?? "");
|
|
|
66
67
|
</p>
|
|
67
68
|
)}
|
|
68
69
|
<div class="lms-actions">
|
|
70
|
+
{!progress.nextLessonId && progress.blockedLessonId && (
|
|
71
|
+
<LessonAvailabilityNote
|
|
72
|
+
availability={progress.availability[progress.blockedLessonId]}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
69
75
|
{["active", "completed"].includes(enrollment.status) &&
|
|
70
76
|
enrollment.effectiveAccess === "granted" &&
|
|
71
77
|
progress.nextLessonId && (
|
|
@@ -76,6 +82,9 @@ next.searchParams.set("cursor", page.cursor ?? "");
|
|
|
76
82
|
<a class="lms-btn lms-btn-outline" href={courseUrl(basePath, course.slug)}>
|
|
77
83
|
Course overview
|
|
78
84
|
</a>
|
|
85
|
+
<a href={`${basePath}/my-learning/study?courseId=${encodeURIComponent(course.id)}`}>
|
|
86
|
+
Notebook & agenda
|
|
87
|
+
</a>
|
|
79
88
|
{enrollment.certificate && (
|
|
80
89
|
<a
|
|
81
90
|
class="lms-btn lms-btn-outline"
|
|
@@ -9,15 +9,30 @@ const path = Astro.url.pathname;
|
|
|
9
9
|
<a href={basePath} aria-current={path === basePath ? "page" : undefined}>
|
|
10
10
|
Explore courses
|
|
11
11
|
</a>
|
|
12
|
+
<a href={`${basePath}/paths`} aria-current={path === `${basePath}/paths` ? "page" : undefined}>
|
|
13
|
+
Learning paths
|
|
14
|
+
</a>
|
|
15
|
+
<a
|
|
16
|
+
href={`${basePath}/my-learning/library`}
|
|
17
|
+
aria-current={path === `${basePath}/my-learning/library` ? "page" : undefined}
|
|
18
|
+
>
|
|
19
|
+
My library & preferences
|
|
20
|
+
</a>
|
|
12
21
|
<a
|
|
13
22
|
href={`${basePath}/my-learning`}
|
|
14
|
-
aria-current={path
|
|
23
|
+
aria-current={path === `${basePath}/my-learning` ? "page" : undefined}
|
|
15
24
|
>
|
|
16
25
|
My learning
|
|
17
26
|
</a>
|
|
18
27
|
{Astro.locals.user && Astro.locals.user.role >= 20 && (
|
|
19
28
|
<a href={`${basePath}/teach`}>Course studio</a>
|
|
20
29
|
)}
|
|
30
|
+
<a
|
|
31
|
+
href={`${basePath}/my-learning/study`}
|
|
32
|
+
aria-current={path === `${basePath}/my-learning/study` ? "page" : undefined}
|
|
33
|
+
>
|
|
34
|
+
Notebook & agenda
|
|
35
|
+
</a>
|
|
21
36
|
</nav>
|
|
22
37
|
<main id="lms-main" class="lms-container">
|
|
23
38
|
<slot />
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { LessonAvailability } from "@anantagnanam/lms/types";
|
|
3
|
+
interface Props {
|
|
4
|
+
availability?: LessonAvailability;
|
|
5
|
+
}
|
|
6
|
+
const { availability } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
{availability && !availability.available && (
|
|
10
|
+
<small class="lms-lock-note">
|
|
11
|
+
{availability.reason}
|
|
12
|
+
{availability.releaseAt && (
|
|
13
|
+
<span>
|
|
14
|
+
{" "}
|
|
15
|
+
Available from{" "}
|
|
16
|
+
<time datetime={availability.releaseAt}>
|
|
17
|
+
{new Date(availability.releaseAt).toLocaleString("en-IN", {
|
|
18
|
+
timeZone: "UTC",
|
|
19
|
+
timeZoneName: "short",
|
|
20
|
+
})}
|
|
21
|
+
</time>
|
|
22
|
+
.
|
|
23
|
+
</span>
|
|
24
|
+
)}
|
|
25
|
+
</small>
|
|
26
|
+
)}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { PortableText, type TypedObject } from "emdash/ui";
|
|
3
|
+
import MediaPlayer from "./MediaPlayer.astro";
|
|
4
|
+
import type { PublicLesson } from "@anantagnanam/lms/types";
|
|
5
|
+
interface Props {
|
|
6
|
+
lesson: PublicLesson;
|
|
7
|
+
}
|
|
8
|
+
const { lesson } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
{lesson.mediaUrl && ["video", "audio", "live"].includes(lesson.kind) && (
|
|
12
|
+
<MediaPlayer
|
|
13
|
+
url={lesson.mediaUrl}
|
|
14
|
+
kind={lesson.kind as "video" | "audio" | "live"}
|
|
15
|
+
title={lesson.title}
|
|
16
|
+
startsAt={lesson.liveStartsAt}
|
|
17
|
+
captions={lesson.captions ?? []}
|
|
18
|
+
/>
|
|
19
|
+
)}
|
|
20
|
+
{lesson.transcript && (
|
|
21
|
+
<details class="lms-resources" data-lms-transcript>
|
|
22
|
+
<summary>Read transcript</summary>
|
|
23
|
+
<div class="lms-actions">
|
|
24
|
+
<label>
|
|
25
|
+
Search transcript <input type="search" maxlength={200} data-lms-transcript-search />
|
|
26
|
+
</label>
|
|
27
|
+
<button type="button" class="lms-btn lms-btn-outline" data-lms-transcript-previous disabled>
|
|
28
|
+
Previous match
|
|
29
|
+
</button>
|
|
30
|
+
<button type="button" class="lms-btn lms-btn-outline" data-lms-transcript-next disabled>
|
|
31
|
+
Next match
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<p data-lms-transcript-status aria-live="polite"></p>
|
|
35
|
+
<p class="lms-text" data-lms-transcript-text>
|
|
36
|
+
{lesson.transcript}
|
|
37
|
+
</p>
|
|
38
|
+
</details>
|
|
39
|
+
)}
|
|
40
|
+
<div class="lms-lesson-body">
|
|
41
|
+
{lesson.portableText.length > 0 ? (
|
|
42
|
+
<PortableText value={lesson.portableText as TypedObject[]} />
|
|
43
|
+
) : (
|
|
44
|
+
lesson.body
|
|
45
|
+
.split(/\n\s*\n/)
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((paragraph) => <p class="lms-text">{paragraph}</p>)
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
{lesson.resources.length > 0 && (
|
|
51
|
+
<section class="lms-resources">
|
|
52
|
+
<h2>Keep exploring</h2>
|
|
53
|
+
<ul>
|
|
54
|
+
{lesson.resources.map((resource) => (
|
|
55
|
+
<li>
|
|
56
|
+
<a href={resource.url} target="_blank" rel="noopener noreferrer">
|
|
57
|
+
<span aria-hidden="true">↗</span>
|
|
58
|
+
<span>
|
|
59
|
+
{resource.title}
|
|
60
|
+
{resource.description && <small>{resource.description}</small>}
|
|
61
|
+
</span>
|
|
62
|
+
</a>
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
</section>
|
|
67
|
+
)}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import LessonContent from "./LessonContent.astro";
|
|
3
|
+
import LessonAvailabilityNote from "./LessonAvailabilityNote.astro";
|
|
4
|
+
import AssignmentRubric from "./AssignmentRubric.astro";
|
|
5
|
+
import AssignmentFeedback from "./AssignmentFeedback.astro";
|
|
4
6
|
import type {
|
|
5
7
|
CourseDetail,
|
|
6
8
|
PublicLesson,
|
|
7
9
|
PublicEnrollment,
|
|
8
10
|
ProgressSummary,
|
|
9
11
|
} from "@anantagnanam/lms/types";
|
|
10
|
-
import { courseUrl, lessonUrl, certificateUrl } from "@anantagnanam/lms/server";
|
|
12
|
+
import { courseUrl, lessonUrl, certificateUrl, assignmentBlock } from "@anantagnanam/lms/server";
|
|
11
13
|
import { basePath } from "virtual:anantagnanam-lms/config";
|
|
12
14
|
interface Props {
|
|
13
15
|
course: CourseDetail;
|
|
@@ -19,14 +21,24 @@ interface Props {
|
|
|
19
21
|
const { course, lesson, enrollment = null, progress, preview = false } = Astro.props;
|
|
20
22
|
const all = course.modules.flatMap((module) => module.lessons);
|
|
21
23
|
const index = all.findIndex((item) => item.id === lesson.id);
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const available = (id: string) =>
|
|
25
|
+
preview
|
|
26
|
+
? Boolean(all.find((item) => item.id === id)?.preview)
|
|
27
|
+
: progress?.availability[id]?.available === true;
|
|
28
|
+
const previous = all.slice(0, index).findLast((item) => available(item.id));
|
|
29
|
+
const nextLesson = all.slice(index + 1).find((item) => available(item.id));
|
|
26
30
|
const current = enrollment?.progress[lesson.id];
|
|
27
31
|
const complete = Boolean(current?.completedAt);
|
|
28
32
|
const attempt = current?.quizAttempts.at(-1);
|
|
29
33
|
const assignment = current?.assignments.at(-1);
|
|
34
|
+
const draft = current?.assignmentDraft;
|
|
35
|
+
const assignmentClosed = lesson.assignment
|
|
36
|
+
? assignmentBlock(
|
|
37
|
+
lesson.assignment,
|
|
38
|
+
current ?? { completedAt: null, assignments: [] },
|
|
39
|
+
new Date(),
|
|
40
|
+
)
|
|
41
|
+
: null;
|
|
30
42
|
---
|
|
31
43
|
|
|
32
44
|
<nav class="lms-breadcrumbs" aria-label="Breadcrumb">
|
|
@@ -42,6 +54,7 @@ const assignment = current?.assignments.at(-1);
|
|
|
42
54
|
data-course-id={course.id}
|
|
43
55
|
data-lesson-id={lesson.id}
|
|
44
56
|
data-preview={preview ? "true" : "false"}
|
|
57
|
+
data-playback={current?.playback ? JSON.stringify(current.playback) : undefined}
|
|
45
58
|
>
|
|
46
59
|
<aside class="lms-course-sidebar">
|
|
47
60
|
<a href={courseUrl(basePath, course.slug)} class="lms-sidebar-course">
|
|
@@ -64,20 +77,28 @@ const assignment = current?.assignments.at(-1);
|
|
|
64
77
|
<ol>
|
|
65
78
|
{module.lessons.map((entry) => (
|
|
66
79
|
<li>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
{available(entry.id) ? (
|
|
81
|
+
<a
|
|
82
|
+
href={lessonUrl(basePath, course.slug, entry.id)}
|
|
83
|
+
aria-current={entry.id === lesson.id ? "page" : undefined}
|
|
84
|
+
>
|
|
85
|
+
<span aria-hidden="true">
|
|
86
|
+
{enrollment?.progress[entry.id]?.completedAt ? "✓" : "○"}
|
|
87
|
+
</span>
|
|
88
|
+
<span>
|
|
89
|
+
{entry.title}
|
|
90
|
+
<small>
|
|
91
|
+
{entry.kind} · {entry.durationMinutes} min
|
|
92
|
+
</small>
|
|
93
|
+
</span>
|
|
94
|
+
</a>
|
|
95
|
+
) : (
|
|
96
|
+
<div class="lms-locked-lesson">
|
|
97
|
+
<span>{entry.title}</span>
|
|
98
|
+
<LessonAvailabilityNote availability={progress?.availability[entry.id]} />
|
|
99
|
+
{preview && <small>Enroll to access this lesson.</small>}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
81
102
|
</li>
|
|
82
103
|
))}
|
|
83
104
|
</ol>
|
|
@@ -99,42 +120,7 @@ const assignment = current?.assignments.at(-1);
|
|
|
99
120
|
<a href={courseUrl(basePath, course.slug)}>Enroll to save your progress →</a>
|
|
100
121
|
</p>
|
|
101
122
|
)}
|
|
102
|
-
{lesson
|
|
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
|
-
)}
|
|
123
|
+
<LessonContent lesson={lesson} />
|
|
138
124
|
{!preview && lesson.kind === "quiz" && lesson.quiz && (
|
|
139
125
|
<section class="lms-assessment">
|
|
140
126
|
<p class="lms-eyebrow">CHECK YOUR UNDERSTANDING</p>
|
|
@@ -153,7 +139,7 @@ const assignment = current?.assignments.at(-1);
|
|
|
153
139
|
data-lms-quiz
|
|
154
140
|
data-course-id={course.id}
|
|
155
141
|
data-lesson-id={lesson.id}
|
|
156
|
-
data-
|
|
142
|
+
data-course-url={courseUrl(basePath, course.slug)}
|
|
157
143
|
>
|
|
158
144
|
<button class="lms-btn" type="button" data-lms-quiz-start>
|
|
159
145
|
{attempt?.status === "in_progress" ? "Resume quiz" : "Start quiz"}
|
|
@@ -192,45 +178,90 @@ const assignment = current?.assignments.at(-1);
|
|
|
192
178
|
? ` · Due ${new Date(lesson.assignment.dueAt).toLocaleString("en-IN")}`
|
|
193
179
|
: ""}
|
|
194
180
|
</p>
|
|
195
|
-
{assignment
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<a href={assignment.attachmentUrl} target="_blank" rel="noopener noreferrer">
|
|
208
|
-
Open attachment ↗
|
|
209
|
-
</a>
|
|
210
|
-
)}
|
|
211
|
-
</details>
|
|
212
|
-
</div>
|
|
181
|
+
<AssignmentRubric rubric={lesson.assignment.rubric} />
|
|
182
|
+
{assignment && <AssignmentFeedback submission={assignment} />}
|
|
183
|
+
{(current?.assignments.length ?? 0) > 1 && (
|
|
184
|
+
<details>
|
|
185
|
+
<summary>Earlier submissions and feedback ({current!.assignments.length - 1})</summary>
|
|
186
|
+
{current!.assignments
|
|
187
|
+
.slice(0, -1)
|
|
188
|
+
.toReversed()
|
|
189
|
+
.map((item) => (
|
|
190
|
+
<AssignmentFeedback submission={item} />
|
|
191
|
+
))}
|
|
192
|
+
</details>
|
|
213
193
|
)}
|
|
214
|
-
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
194
|
+
{assignmentClosed ? (
|
|
195
|
+
<>
|
|
196
|
+
<p class="lms-notice">{assignmentClosed.message}</p>
|
|
197
|
+
{(draft?.body || draft?.attachmentUrl) && (
|
|
198
|
+
<details open>
|
|
199
|
+
<summary>Your saved draft (not submitted)</summary>
|
|
200
|
+
<p class="lms-text">{draft.body}</p>
|
|
201
|
+
{draft.attachmentUrl && (
|
|
202
|
+
<a href={draft.attachmentUrl} target="_blank" rel="noopener noreferrer">
|
|
203
|
+
Open draft attachment ↗
|
|
204
|
+
</a>
|
|
205
|
+
)}
|
|
206
|
+
</details>
|
|
207
|
+
)}
|
|
208
|
+
</>
|
|
209
|
+
) : (
|
|
210
|
+
<form
|
|
211
|
+
class="lms-form"
|
|
212
|
+
data-lms-assignment
|
|
213
|
+
data-course-id={course.id}
|
|
214
|
+
data-lesson-id={lesson.id}
|
|
215
|
+
data-draft-revision={draft?.revision ?? 0}
|
|
216
|
+
>
|
|
217
|
+
<p>Drafts save automatically. Only you can see your draft until you submit it.</p>
|
|
218
|
+
<label>
|
|
219
|
+
<span>Your work</span>
|
|
220
|
+
<textarea
|
|
221
|
+
name="body"
|
|
222
|
+
required
|
|
223
|
+
rows={8}
|
|
224
|
+
maxlength={20_000}
|
|
225
|
+
>{draft?.body ?? ""}</textarea>
|
|
226
|
+
</label>
|
|
227
|
+
<label>
|
|
228
|
+
<span>Attachment link (optional)</span>
|
|
229
|
+
<input
|
|
230
|
+
name="attachmentUrl"
|
|
231
|
+
type="url"
|
|
232
|
+
placeholder="https://…"
|
|
233
|
+
value={draft?.attachmentUrl ?? ""}
|
|
234
|
+
/>
|
|
235
|
+
</label>
|
|
236
|
+
<button class="lms-btn lms-btn-outline" type="button" data-lms-save-draft>
|
|
237
|
+
Save draft
|
|
238
|
+
</button>
|
|
239
|
+
<button class="lms-btn" type="submit">
|
|
240
|
+
Submit assignment
|
|
241
|
+
</button>
|
|
242
|
+
<p data-lms-feedback aria-live="polite">
|
|
243
|
+
{draft?.updatedAt ? "Saved draft restored." : "Your draft is empty."}
|
|
244
|
+
</p>
|
|
245
|
+
<button class="lms-btn lms-btn-outline" type="button" data-lms-review-draft hidden>
|
|
246
|
+
Review saved work
|
|
247
|
+
</button>
|
|
248
|
+
<section
|
|
249
|
+
class="lms-notice"
|
|
250
|
+
data-lms-assignment-conflict
|
|
251
|
+
hidden
|
|
252
|
+
aria-label="Review saved work"
|
|
253
|
+
>
|
|
254
|
+
<h3>Saved work</h3>
|
|
255
|
+
<p class="lms-text" data-lms-saved-draft></p>
|
|
256
|
+
<button class="lms-btn lms-btn-outline" type="button" data-lms-use-saved>
|
|
257
|
+
Use saved draft
|
|
230
258
|
</button>
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
259
|
+
<button class="lms-btn lms-btn-outline" type="button" data-lms-keep-local>
|
|
260
|
+
Keep my local draft
|
|
261
|
+
</button>
|
|
262
|
+
</section>
|
|
263
|
+
</form>
|
|
264
|
+
)}
|
|
234
265
|
</section>
|
|
235
266
|
)}
|
|
236
267
|
{!preview && !["quiz", "assignment"].includes(lesson.kind) && (
|
|
@@ -268,21 +299,25 @@ const assignment = current?.assignments.at(-1);
|
|
|
268
299
|
</div>
|
|
269
300
|
)}
|
|
270
301
|
<nav class="lms-lesson-navigation" aria-label="Lesson navigation">
|
|
271
|
-
{
|
|
272
|
-
<a href={lessonUrl(basePath, course.slug,
|
|
302
|
+
{previous ? (
|
|
303
|
+
<a href={lessonUrl(basePath, course.slug, previous.id)}>← Previous available lesson</a>
|
|
273
304
|
) : (
|
|
274
305
|
<a href={courseUrl(basePath, course.slug)}>← Course overview</a>
|
|
275
306
|
)}
|
|
276
|
-
{
|
|
277
|
-
<a href={lessonUrl(basePath, course.slug,
|
|
307
|
+
{nextLesson && (
|
|
308
|
+
<a href={lessonUrl(basePath, course.slug, nextLesson.id)}>Next available lesson →</a>
|
|
278
309
|
)}
|
|
279
310
|
</nav>
|
|
311
|
+
{!nextLesson && progress?.blockedLessonId && (
|
|
312
|
+
<LessonAvailabilityNote availability={progress.availability[progress.blockedLessonId]} />
|
|
313
|
+
)}
|
|
280
314
|
{!preview && (
|
|
281
315
|
<>
|
|
282
316
|
<section class="lms-content-section">
|
|
283
317
|
<h2>A note to your future self</h2>
|
|
284
318
|
<p class="lms-muted">Only you can see these notes.</p>
|
|
285
319
|
<form class="lms-form" data-lms-route="learner/note">
|
|
320
|
+
<input type="hidden" name="expectedRevision" value={current?.noteRevision ?? 0} />
|
|
286
321
|
<input type="hidden" name="courseId" value={course.id} />
|
|
287
322
|
<input type="hidden" name="lessonId" value={lesson.id} />
|
|
288
323
|
<label>
|
|
@@ -294,6 +329,38 @@ const assignment = current?.assignments.at(-1);
|
|
|
294
329
|
</button>
|
|
295
330
|
<p data-lms-feedback aria-live="polite"></p>
|
|
296
331
|
</form>
|
|
332
|
+
<p>
|
|
333
|
+
<a href={`${basePath}/my-learning/study?courseId=${encodeURIComponent(course.id)}`}>
|
|
334
|
+
Open course notebook & agenda →
|
|
335
|
+
</a>
|
|
336
|
+
</p>
|
|
337
|
+
<section data-lms-quick-note data-course-id={course.id} data-lesson-id={lesson.id}>
|
|
338
|
+
<h3>Notes & bookmarks</h3>
|
|
339
|
+
<p>
|
|
340
|
+
Save a separate thought or bookmark the current moment in native audio/video. Only you
|
|
341
|
+
can see it.
|
|
342
|
+
</p>
|
|
343
|
+
<form class="lms-form">
|
|
344
|
+
<label>
|
|
345
|
+
<span>New note</span>
|
|
346
|
+
<textarea name="body" rows={3} maxlength={5000}></textarea>
|
|
347
|
+
</label>
|
|
348
|
+
<label>
|
|
349
|
+
<input type="checkbox" name="starred" /> Star this note
|
|
350
|
+
</label>
|
|
351
|
+
<button type="button" class="lms-btn lms-btn-outline" data-lms-capture-time hidden>
|
|
352
|
+
Use current playback time
|
|
353
|
+
</button>
|
|
354
|
+
<p data-lms-note-time></p>
|
|
355
|
+
<button type="button" class="lms-text-button" data-lms-clear-time hidden>
|
|
356
|
+
Remove timestamp
|
|
357
|
+
</button>
|
|
358
|
+
<button type="submit" class="lms-btn lms-btn-outline">
|
|
359
|
+
Add to notebook
|
|
360
|
+
</button>
|
|
361
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
362
|
+
</form>
|
|
363
|
+
</section>
|
|
297
364
|
</section>
|
|
298
365
|
<section
|
|
299
366
|
class="lms-content-section"
|