@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,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
import CourseCard from "./CourseCard.astro";
|
|
3
|
+
import type { CourseCard as CourseCardData, Page } from "@anantagnanam/lms/types";
|
|
4
|
+
interface Props {
|
|
5
|
+
page: Page<CourseCardData>;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
const { page, title, description, error } = Astro.props;
|
|
11
|
+
const query = Astro.url.searchParams;
|
|
12
|
+
const nextUrl = new URL(Astro.url);
|
|
13
|
+
nextUrl.searchParams.set("cursor", page.cursor ?? "");
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<section class="lms-hero">
|
|
17
|
+
<p class="lms-eyebrow">LEARNING THAT STAYS WITH YOU</p>
|
|
18
|
+
<h1>{title}</h1>
|
|
19
|
+
<p class="lms-lead">{description}</p>
|
|
20
|
+
<div class="lms-hero-notes">
|
|
21
|
+
<span>◎ Free courses</span>
|
|
22
|
+
<span>✓ Guided progress</span>
|
|
23
|
+
<span>∞ Lifelong curiosity</span>
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
26
|
+
<section class="lms-catalog" aria-label="Course catalog">
|
|
27
|
+
<div class="lms-section-heading">
|
|
28
|
+
<div>
|
|
29
|
+
<p class="lms-eyebrow">CHOOSE YOUR NEXT STEP</p>
|
|
30
|
+
<h2>Find your learning path</h2>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<form class="lms-filters" method="get" action={Astro.url.pathname}>
|
|
34
|
+
<label>
|
|
35
|
+
<span>Course title</span>
|
|
36
|
+
<input
|
|
37
|
+
type="search"
|
|
38
|
+
name="q"
|
|
39
|
+
value={query.get("q") ?? ""}
|
|
40
|
+
placeholder="Start typing a course title…"
|
|
41
|
+
/>
|
|
42
|
+
</label>
|
|
43
|
+
<label>
|
|
44
|
+
<span>Level</span>
|
|
45
|
+
<select name="level">
|
|
46
|
+
<option value="all" selected={!query.get("level") || query.get("level") === "all"}>
|
|
47
|
+
All levels
|
|
48
|
+
</option>
|
|
49
|
+
{["beginner", "intermediate", "advanced"].map((level) => (
|
|
50
|
+
<option value={level} selected={query.get("level") === level}>
|
|
51
|
+
{level}
|
|
52
|
+
</option>
|
|
53
|
+
))}
|
|
54
|
+
</select>
|
|
55
|
+
</label>
|
|
56
|
+
<label>
|
|
57
|
+
<span>Category</span>
|
|
58
|
+
<input name="category" value={query.get("category") ?? ""} placeholder="Any category" />
|
|
59
|
+
</label>
|
|
60
|
+
<button class="lms-btn" type="submit">
|
|
61
|
+
Find courses
|
|
62
|
+
</button>
|
|
63
|
+
</form>
|
|
64
|
+
{error && (
|
|
65
|
+
<p class="lms-notice lms-notice-error" role="alert">
|
|
66
|
+
{error}
|
|
67
|
+
</p>
|
|
68
|
+
)}
|
|
69
|
+
<div class="lms-cards">
|
|
70
|
+
{page.items.map((course) => (
|
|
71
|
+
<CourseCard course={course} />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
{!page.items.length && !error && (
|
|
75
|
+
<div class="lms-empty">
|
|
76
|
+
<span aria-hidden="true">◎</span>
|
|
77
|
+
<h3>
|
|
78
|
+
{query.get("q") || query.get("category")
|
|
79
|
+
? "No matching courses"
|
|
80
|
+
: "A new chapter is coming"}
|
|
81
|
+
</h3>
|
|
82
|
+
<p>
|
|
83
|
+
{query.get("q") || query.get("category")
|
|
84
|
+
? "Try another course title or category."
|
|
85
|
+
: "New courses will appear here when they are published."}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{page.hasMore && page.cursor && (
|
|
90
|
+
<nav class="lms-pagination" aria-label="Catalog pagination">
|
|
91
|
+
<a class="lms-btn lms-btn-outline" href={nextUrl.href}>
|
|
92
|
+
More courses →
|
|
93
|
+
</a>
|
|
94
|
+
</nav>
|
|
95
|
+
)}
|
|
96
|
+
</section>
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Image } from "emdash/ui";
|
|
3
|
+
import LessonAvailabilityNote from "./LessonAvailabilityNote.astro";
|
|
4
|
+
import type {
|
|
5
|
+
CourseDetail,
|
|
6
|
+
PublicEnrollment,
|
|
7
|
+
ProgressSummary,
|
|
8
|
+
Page,
|
|
9
|
+
Review,
|
|
10
|
+
CohortMembership,
|
|
11
|
+
} from "@anantagnanam/lms/types";
|
|
12
|
+
import { courseUrl, lessonUrl, certificateUrl, loginUrl } from "@anantagnanam/lms/server";
|
|
13
|
+
import { basePath, loginPath } from "virtual:anantagnanam-lms/config";
|
|
14
|
+
interface Props {
|
|
15
|
+
course: CourseDetail;
|
|
16
|
+
enrollment?: PublicEnrollment | null;
|
|
17
|
+
progress?: ProgressSummary;
|
|
18
|
+
reviews?: Page<Review>;
|
|
19
|
+
cohortId?: string;
|
|
20
|
+
cohortMembership?: CohortMembership | null;
|
|
21
|
+
}
|
|
22
|
+
const {
|
|
23
|
+
course,
|
|
24
|
+
enrollment = null,
|
|
25
|
+
progress,
|
|
26
|
+
reviews,
|
|
27
|
+
cohortId,
|
|
28
|
+
cohortMembership = null,
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
const active =
|
|
31
|
+
enrollment &&
|
|
32
|
+
["active", "completed"].includes(enrollment.status) &&
|
|
33
|
+
enrollment.effectiveAccess === "granted";
|
|
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;
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<nav class="lms-breadcrumbs" aria-label="Breadcrumb">
|
|
44
|
+
<a href={basePath}>Courses</a>
|
|
45
|
+
<span aria-hidden="true">/</span>
|
|
46
|
+
<span>{course.title}</span>
|
|
47
|
+
</nav>
|
|
48
|
+
<header class="lms-course-hero">
|
|
49
|
+
<div>
|
|
50
|
+
<p class="lms-eyebrow">{course.category || "YOUR NEXT LEARNING JOURNEY"}</p>
|
|
51
|
+
<h1>{course.title}</h1>
|
|
52
|
+
<p class="lms-lead">{course.summary}</p>
|
|
53
|
+
<div class="lms-tags">
|
|
54
|
+
<span>{course.level === "all" ? "All levels" : course.level}</span>
|
|
55
|
+
<span>{course.lessonCount} lessons</span>
|
|
56
|
+
<span>{course.durationMinutes} minutes</span>
|
|
57
|
+
<span>{course.language}</span>
|
|
58
|
+
</div>
|
|
59
|
+
{course.instructorName && (
|
|
60
|
+
<p class="lms-instructor">
|
|
61
|
+
Guided by <strong>{course.instructorName}</strong>
|
|
62
|
+
</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<aside class="lms-enroll-card">
|
|
66
|
+
{course.featuredImage && <Image image={course.featuredImage} width={600} height={360} />}
|
|
67
|
+
<span class="lms-eyebrow">FREE TO LEARN</span>
|
|
68
|
+
<h2>{active ? "Your next step awaits" : "Begin your learning journey"}</h2>
|
|
69
|
+
{active && progress ? (
|
|
70
|
+
<>
|
|
71
|
+
<label class="lms-progress-label" for="course-progress">
|
|
72
|
+
{progress.completed} of {progress.total} required lessons complete
|
|
73
|
+
</label>
|
|
74
|
+
<progress id="course-progress" max="100" value={progress.percent}>
|
|
75
|
+
{progress.percent}%
|
|
76
|
+
</progress>
|
|
77
|
+
{next && (
|
|
78
|
+
<a class="lms-btn" href={lessonUrl(basePath, course.slug, next)}>
|
|
79
|
+
{enrollment.status === "completed" ? "Revisit lessons" : "Continue learning"} →
|
|
80
|
+
</a>
|
|
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
|
+
)}
|
|
90
|
+
{enrollment.certificate && (
|
|
91
|
+
<a
|
|
92
|
+
class="lms-btn lms-btn-outline"
|
|
93
|
+
href={certificateUrl(basePath, enrollment.certificate.id)}
|
|
94
|
+
>
|
|
95
|
+
View certificate
|
|
96
|
+
</a>
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
99
|
+
) : enrollment ? (
|
|
100
|
+
<div class="lms-notice">
|
|
101
|
+
<strong>
|
|
102
|
+
{enrollment.status === "pending"
|
|
103
|
+
? "Your request is awaiting approval"
|
|
104
|
+
: enrollment.status === "active" || enrollment.status === "completed"
|
|
105
|
+
? `Course access ${enrollment.effectiveAccess}`
|
|
106
|
+
: `Enrollment ${enrollment.status}`}
|
|
107
|
+
</strong>
|
|
108
|
+
<p>
|
|
109
|
+
{enrollment.status === "pending"
|
|
110
|
+
? "An administrator will review your request. Check My Learning for updates."
|
|
111
|
+
: enrollment.decisionReason || "Contact an administrator to manage your access."}
|
|
112
|
+
</p>
|
|
113
|
+
<a href={`${basePath}/my-learning`}>Open My Learning →</a>
|
|
114
|
+
{progress?.blockedLessonId && (
|
|
115
|
+
<LessonAvailabilityNote availability={progress.availability[progress.blockedLessonId]} />
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
) : !Astro.locals.user ? (
|
|
119
|
+
<a class="lms-btn" href={loginUrl(loginPath, Astro.url.pathname + Astro.url.search)}>
|
|
120
|
+
Sign in to enroll →
|
|
121
|
+
</a>
|
|
122
|
+
) : cohortId && cohortMembership?.status !== "active" ? (
|
|
123
|
+
<div class="lms-notice">
|
|
124
|
+
<p>
|
|
125
|
+
{cohortMembership
|
|
126
|
+
? `Cohort membership ${cohortMembership.status}.`
|
|
127
|
+
: "This cohort requires administrator approval."}
|
|
128
|
+
</p>
|
|
129
|
+
<a class="lms-btn" href={`${basePath}/cohorts/${encodeURIComponent(cohortId)}`}>
|
|
130
|
+
{cohortMembership ? "View cohort membership" : "Request cohort membership"} →
|
|
131
|
+
</a>
|
|
132
|
+
</div>
|
|
133
|
+
) : course.enrollmentMode === "invite" && !cohortId ? (
|
|
134
|
+
<p class="lms-notice">An administrator will enroll invited learners in this course.</p>
|
|
135
|
+
) : (
|
|
136
|
+
<form data-lms-route="learner/enroll" data-lms-reload>
|
|
137
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
138
|
+
{cohortId && <input type="hidden" name="cohortId" value={cohortId} />}
|
|
139
|
+
<button class="lms-btn" type="submit">
|
|
140
|
+
{course.enrollmentMode === "approval" && !cohortId
|
|
141
|
+
? "Request enrollment"
|
|
142
|
+
: "Enroll for free"}{" "}
|
|
143
|
+
→
|
|
144
|
+
</button>
|
|
145
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
146
|
+
</form>
|
|
147
|
+
)}
|
|
148
|
+
<ul class="lms-benefits">
|
|
149
|
+
<li>Learn at your own pace</li>
|
|
150
|
+
{course.sequential && <li>Follow a clear lesson sequence</li>}
|
|
151
|
+
{course.certificateEnabled && <li>Earn a completion certificate</li>}
|
|
152
|
+
{course.accessDays > 0 && <li>{course.accessDays} days of access</li>}
|
|
153
|
+
</ul>
|
|
154
|
+
</aside>
|
|
155
|
+
</header>
|
|
156
|
+
<div class="lms-course-content">
|
|
157
|
+
<section class="lms-content-section">
|
|
158
|
+
<p class="lms-eyebrow">A LITTLE CURIOSITY GOES A LONG WAY</p>
|
|
159
|
+
<h2>About this course</h2>
|
|
160
|
+
{course.description
|
|
161
|
+
.split(/\n\s*\n/)
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.map((paragraph) => (
|
|
164
|
+
<p class="lms-text">{paragraph}</p>
|
|
165
|
+
))}
|
|
166
|
+
</section>
|
|
167
|
+
{course.outcomes.length > 0 && (
|
|
168
|
+
<section class="lms-content-section lms-outcomes">
|
|
169
|
+
<h2>What you'll learn</h2>
|
|
170
|
+
<ul>
|
|
171
|
+
{course.outcomes.map((outcome) => (
|
|
172
|
+
<li>
|
|
173
|
+
<span aria-hidden="true">✓</span>
|
|
174
|
+
{outcome}
|
|
175
|
+
</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
</section>
|
|
179
|
+
)}
|
|
180
|
+
{course.requirements.length > 0 && (
|
|
181
|
+
<section class="lms-content-section">
|
|
182
|
+
<h2>Before you begin</h2>
|
|
183
|
+
<ul>
|
|
184
|
+
{course.requirements.map((requirement) => (
|
|
185
|
+
<li>{requirement}</li>
|
|
186
|
+
))}
|
|
187
|
+
</ul>
|
|
188
|
+
</section>
|
|
189
|
+
)}
|
|
190
|
+
{course.prerequisites.length > 0 && (
|
|
191
|
+
<section class="lms-content-section">
|
|
192
|
+
<h2>Complete these courses first</h2>
|
|
193
|
+
<ul>
|
|
194
|
+
{course.prerequisites.map((prerequisite) => (
|
|
195
|
+
<li>
|
|
196
|
+
{prerequisite.slug ? (
|
|
197
|
+
<a href={courseUrl(basePath, prerequisite.slug)}>{prerequisite.title}</a>
|
|
198
|
+
) : (
|
|
199
|
+
prerequisite.title
|
|
200
|
+
)}
|
|
201
|
+
</li>
|
|
202
|
+
))}
|
|
203
|
+
</ul>
|
|
204
|
+
</section>
|
|
205
|
+
)}
|
|
206
|
+
<section class="lms-content-section">
|
|
207
|
+
<div class="lms-section-heading">
|
|
208
|
+
<div>
|
|
209
|
+
<p class="lms-eyebrow">ONE STEP AT A TIME</p>
|
|
210
|
+
<h2>Your course curriculum</h2>
|
|
211
|
+
</div>
|
|
212
|
+
<span>{course.modules.length} modules</span>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="lms-curriculum">
|
|
215
|
+
{course.modules.map((module, moduleIndex) => (
|
|
216
|
+
<details open={moduleIndex === 0}>
|
|
217
|
+
<summary>
|
|
218
|
+
<span>
|
|
219
|
+
<small>MODULE {moduleIndex + 1}</small>
|
|
220
|
+
<strong>{module.title}</strong>
|
|
221
|
+
</span>
|
|
222
|
+
<span>{module.lessons.length} lessons</span>
|
|
223
|
+
</summary>
|
|
224
|
+
{module.description && <p>{module.description}</p>}
|
|
225
|
+
{(module.releaseDays > 0 || module.unlockAt) && (
|
|
226
|
+
<p class="lms-muted">
|
|
227
|
+
{module.releaseDays > 0 ? `Unlocks ${module.releaseDays} days after approval` : ""}
|
|
228
|
+
{module.unlockAt
|
|
229
|
+
? ` · Available from ${new Date(module.unlockAt).toLocaleDateString("en-IN")}`
|
|
230
|
+
: ""}
|
|
231
|
+
</p>
|
|
232
|
+
)}
|
|
233
|
+
<ol>
|
|
234
|
+
{module.lessons.map((lesson, index) => (
|
|
235
|
+
<li>
|
|
236
|
+
<span class="lms-lesson-number">
|
|
237
|
+
{enrollment?.progress[lesson.id]?.completedAt
|
|
238
|
+
? "✓"
|
|
239
|
+
: String(index + 1).padStart(2, "0")}
|
|
240
|
+
</span>
|
|
241
|
+
<div>
|
|
242
|
+
{(
|
|
243
|
+
enrollment && ["active", "completed"].includes(enrollment.status)
|
|
244
|
+
? progress?.availability[lesson.id]?.available
|
|
245
|
+
: lesson.preview
|
|
246
|
+
) ? (
|
|
247
|
+
<a href={lessonUrl(basePath, course.slug, lesson.id)}>{lesson.title}</a>
|
|
248
|
+
) : (
|
|
249
|
+
<strong>{lesson.title}</strong>
|
|
250
|
+
)}
|
|
251
|
+
<small>
|
|
252
|
+
{lesson.kind} · {lesson.durationMinutes} min
|
|
253
|
+
{!lesson.required ? " · optional" : ""}
|
|
254
|
+
</small>
|
|
255
|
+
<LessonAvailabilityNote availability={progress?.availability[lesson.id]} />
|
|
256
|
+
</div>
|
|
257
|
+
{lesson.preview && <span class="lms-badge">Preview</span>}
|
|
258
|
+
</li>
|
|
259
|
+
))}
|
|
260
|
+
</ol>
|
|
261
|
+
</details>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
</section>
|
|
265
|
+
{reviews && reviews.items.length > 0 && (
|
|
266
|
+
<section class="lms-content-section">
|
|
267
|
+
<h2>From fellow learners</h2>
|
|
268
|
+
<div class="lms-reviews">
|
|
269
|
+
{reviews.items.map((review) => (
|
|
270
|
+
<article>
|
|
271
|
+
<p class="lms-review-stars" aria-label={`${review.rating} out of 5 stars`}>
|
|
272
|
+
{"★".repeat(review.rating)}
|
|
273
|
+
{"☆".repeat(5 - review.rating)}
|
|
274
|
+
</p>
|
|
275
|
+
<p class="lms-text">{review.body}</p>
|
|
276
|
+
<small>{review.authorName}</small>
|
|
277
|
+
</article>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
</section>
|
|
281
|
+
)}
|
|
282
|
+
{active && progress && progress.completed > 0 && (
|
|
283
|
+
<section class="lms-content-section">
|
|
284
|
+
<h2>Share your experience</h2>
|
|
285
|
+
<form class="lms-form" data-lms-route="learner/review">
|
|
286
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
287
|
+
<label>
|
|
288
|
+
<span>Your rating</span>
|
|
289
|
+
<select name="rating" required>
|
|
290
|
+
<option value="5">5 · Excellent</option>
|
|
291
|
+
<option value="4">4 · Very good</option>
|
|
292
|
+
<option value="3">3 · Good</option>
|
|
293
|
+
<option value="2">2 · Could improve</option>
|
|
294
|
+
<option value="1">1 · Needs work</option>
|
|
295
|
+
</select>
|
|
296
|
+
</label>
|
|
297
|
+
<label>
|
|
298
|
+
<span>Your review</span>
|
|
299
|
+
<textarea name="body" required rows={4} maxlength={3000}></textarea>
|
|
300
|
+
</label>
|
|
301
|
+
<button class="lms-btn" type="submit">
|
|
302
|
+
Submit review
|
|
303
|
+
</button>
|
|
304
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
305
|
+
</form>
|
|
306
|
+
</section>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { CourseCard, PublicEnrollment, ProgressSummary, Page } from "@anantagnanam/lms/types";
|
|
3
|
+
import { courseUrl, lessonUrl, certificateUrl } from "@anantagnanam/lms/server";
|
|
4
|
+
import { basePath } from "virtual:anantagnanam-lms/config";
|
|
5
|
+
import LessonAvailabilityNote from "./LessonAvailabilityNote.astro";
|
|
6
|
+
interface Props {
|
|
7
|
+
page: Page<{ course: CourseCard; enrollment: PublicEnrollment; progress: ProgressSummary }>;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
const { page, error } = Astro.props;
|
|
11
|
+
const next = new URL(Astro.url);
|
|
12
|
+
next.searchParams.set("cursor", page.cursor ?? "");
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<header class="lms-dashboard-header">
|
|
16
|
+
<p class="lms-eyebrow">YOUR LEARNING JOURNEY</p>
|
|
17
|
+
<h1>Welcome back{Astro.locals.user?.name ? `, ${Astro.locals.user.name.split(" ")[0]}` : ""}.</h1>
|
|
18
|
+
<p class="lms-lead">A little progress today becomes lasting knowledge tomorrow.</p>
|
|
19
|
+
</header>
|
|
20
|
+
{error && (
|
|
21
|
+
<p role="alert" class="lms-notice lms-notice-error">
|
|
22
|
+
{error}
|
|
23
|
+
</p>
|
|
24
|
+
)}
|
|
25
|
+
<div class="lms-dashboard-grid">
|
|
26
|
+
<section>
|
|
27
|
+
<div class="lms-section-heading">
|
|
28
|
+
<h2>My courses</h2>
|
|
29
|
+
<a href={basePath}>Explore more →</a>
|
|
30
|
+
</div>
|
|
31
|
+
{!page.items.length && !error && (
|
|
32
|
+
<div class="lms-empty">
|
|
33
|
+
<span aria-hidden="true">◎</span>
|
|
34
|
+
<h3>Your first step starts here</h3>
|
|
35
|
+
<p>Choose a free course and make room for a little curiosity.</p>
|
|
36
|
+
<a class="lms-btn" href={basePath}>
|
|
37
|
+
Find a course →
|
|
38
|
+
</a>
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
{page.items.map(({ course, enrollment, progress }) => (
|
|
42
|
+
<article class="lms-learning-card">
|
|
43
|
+
<div class="lms-learning-card-top">
|
|
44
|
+
<span class="lms-eyebrow">{course.category || "LEARNING PATH"}</span>
|
|
45
|
+
<span class="lms-badge">{enrollment.status}</span>
|
|
46
|
+
</div>
|
|
47
|
+
<h3>
|
|
48
|
+
<a href={courseUrl(basePath, course.slug)}>{course.title}</a>
|
|
49
|
+
</h3>
|
|
50
|
+
<p>{course.summary}</p>
|
|
51
|
+
<div class="lms-progress-label">
|
|
52
|
+
<span>
|
|
53
|
+
{progress.completed}/{progress.total} required lessons
|
|
54
|
+
</span>
|
|
55
|
+
<strong>{progress.percent}%</strong>
|
|
56
|
+
</div>
|
|
57
|
+
<progress value={progress.percent} max="100">
|
|
58
|
+
{progress.percent}%
|
|
59
|
+
</progress>
|
|
60
|
+
{enrollment.status === "pending" && (
|
|
61
|
+
<p class="lms-notice">Your request is awaiting administrator approval.</p>
|
|
62
|
+
)}
|
|
63
|
+
{enrollment.decisionReason && <p class="lms-notice">{enrollment.decisionReason}</p>}
|
|
64
|
+
{enrollment.status !== "pending" && enrollment.effectiveAccess !== "granted" && (
|
|
65
|
+
<p class="lms-notice">
|
|
66
|
+
Course access {enrollment.effectiveAccess}. Your learning progress is saved.
|
|
67
|
+
</p>
|
|
68
|
+
)}
|
|
69
|
+
<div class="lms-actions">
|
|
70
|
+
{!progress.nextLessonId && progress.blockedLessonId && (
|
|
71
|
+
<LessonAvailabilityNote
|
|
72
|
+
availability={progress.availability[progress.blockedLessonId]}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
{["active", "completed"].includes(enrollment.status) &&
|
|
76
|
+
enrollment.effectiveAccess === "granted" &&
|
|
77
|
+
progress.nextLessonId && (
|
|
78
|
+
<a class="lms-btn" href={lessonUrl(basePath, course.slug, progress.nextLessonId)}>
|
|
79
|
+
Continue learning →
|
|
80
|
+
</a>
|
|
81
|
+
)}
|
|
82
|
+
<a class="lms-btn lms-btn-outline" href={courseUrl(basePath, course.slug)}>
|
|
83
|
+
Course overview
|
|
84
|
+
</a>
|
|
85
|
+
{enrollment.certificate && (
|
|
86
|
+
<a
|
|
87
|
+
class="lms-btn lms-btn-outline"
|
|
88
|
+
href={certificateUrl(basePath, enrollment.certificate.id)}
|
|
89
|
+
>
|
|
90
|
+
Certificate ✧
|
|
91
|
+
</a>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
{enrollment.status === "pending" && (
|
|
95
|
+
<form
|
|
96
|
+
data-lms-route="learner/cancel"
|
|
97
|
+
data-lms-reload
|
|
98
|
+
data-lms-confirm="Cancel this enrollment request?"
|
|
99
|
+
>
|
|
100
|
+
<input type="hidden" name="courseId" value={course.id} />
|
|
101
|
+
<button class="lms-text-button" type="submit">
|
|
102
|
+
Cancel request
|
|
103
|
+
</button>
|
|
104
|
+
<p data-lms-feedback aria-live="polite"></p>
|
|
105
|
+
</form>
|
|
106
|
+
)}
|
|
107
|
+
</article>
|
|
108
|
+
))}
|
|
109
|
+
{page.hasMore && page.cursor && (
|
|
110
|
+
<a class="lms-btn lms-btn-outline" href={next.href}>
|
|
111
|
+
More learning records →
|
|
112
|
+
</a>
|
|
113
|
+
)}
|
|
114
|
+
</section>
|
|
115
|
+
<aside class="lms-notifications">
|
|
116
|
+
<p class="lms-eyebrow">STAY IN THE LOOP</p>
|
|
117
|
+
<h2>Your updates</h2>
|
|
118
|
+
<div data-lms-notifications>
|
|
119
|
+
<p class="lms-muted">Loading your updates…</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="lms-learning-tip">
|
|
122
|
+
<span aria-hidden="true">∞</span>
|
|
123
|
+
<h3>Small steps. Deep understanding.</h3>
|
|
124
|
+
<p>Return to a lesson, write down a question, or explain a new idea in your own words.</p>
|
|
125
|
+
</div>
|
|
126
|
+
</aside>
|
|
127
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { basePath } from "virtual:anantagnanam-lms/config";
|
|
3
|
+
import "../styles.css";
|
|
4
|
+
const path = Astro.url.pathname;
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<div class="lms-root">
|
|
8
|
+
<nav class="lms-subnav" aria-label="Learning">
|
|
9
|
+
<a href={basePath} aria-current={path === basePath ? "page" : undefined}>
|
|
10
|
+
Explore courses
|
|
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>
|
|
21
|
+
<a
|
|
22
|
+
href={`${basePath}/my-learning`}
|
|
23
|
+
aria-current={path === `${basePath}/my-learning` ? "page" : undefined}
|
|
24
|
+
>
|
|
25
|
+
My learning
|
|
26
|
+
</a>
|
|
27
|
+
{Astro.locals.user && Astro.locals.user.role >= 20 && (
|
|
28
|
+
<a href={`${basePath}/teach`}>Course studio</a>
|
|
29
|
+
)}
|
|
30
|
+
</nav>
|
|
31
|
+
<main id="lms-main" class="lms-container">
|
|
32
|
+
<slot />
|
|
33
|
+
</main>
|
|
34
|
+
</div>
|
|
35
|
+
<script>
|
|
36
|
+
import "../client";
|
|
37
|
+
</script>
|
|
@@ -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,53 @@
|
|
|
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">
|
|
22
|
+
<summary>Read transcript</summary>
|
|
23
|
+
<p class="lms-text">{lesson.transcript}</p>
|
|
24
|
+
</details>
|
|
25
|
+
)}
|
|
26
|
+
<div class="lms-lesson-body">
|
|
27
|
+
{lesson.portableText.length > 0 ? (
|
|
28
|
+
<PortableText value={lesson.portableText as TypedObject[]} />
|
|
29
|
+
) : (
|
|
30
|
+
lesson.body
|
|
31
|
+
.split(/\n\s*\n/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((paragraph) => <p class="lms-text">{paragraph}</p>)
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
{lesson.resources.length > 0 && (
|
|
37
|
+
<section class="lms-resources">
|
|
38
|
+
<h2>Keep exploring</h2>
|
|
39
|
+
<ul>
|
|
40
|
+
{lesson.resources.map((resource) => (
|
|
41
|
+
<li>
|
|
42
|
+
<a href={resource.url} target="_blank" rel="noopener noreferrer">
|
|
43
|
+
<span aria-hidden="true">↗</span>
|
|
44
|
+
<span>
|
|
45
|
+
{resource.title}
|
|
46
|
+
{resource.description && <small>{resource.description}</small>}
|
|
47
|
+
</span>
|
|
48
|
+
</a>
|
|
49
|
+
</li>
|
|
50
|
+
))}
|
|
51
|
+
</ul>
|
|
52
|
+
</section>
|
|
53
|
+
)}
|