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