Skip to content

Commit 0b78e59

Browse files
author
Rajat
committed
Publishing lessons
1 parent fee1f46 commit 0b78e59

File tree

20 files changed

+541
-79
lines changed

20 files changed

+541
-79
lines changed
145 KB
Loading
145 KB
Loading

apps/docs/src/pages/en/courses/add-content.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ A lesson is a container for the actual learning material. CourseLit supports mul
7878

7979
6. Click `Save lesson`.
8080

81+
## Preview lessons
82+
83+
By default, lessons are visible only to learners after enrollment. To offer a lesson to potential learners without requiring enrollment, toggle the `Preview` switch as shown.
84+
85+
![Preview lesson](/assets/lessons/preview.png)
86+
87+
## Control lesson visibility
88+
89+
By default, lessons are unpublished i.e., not visible to learners. To publish a lesson, toggle the `Publish` switch as shown.
90+
91+
![Publish lesson](/assets/lessons/visibility.png)
92+
8193
## Stuck somewhere?
8294

8395
We are always here for you. Come chat with us in our <a href="https://discord.com/invite/GR4bQsN" target="_blank">Discord</a> channel or send a tweet at <a href="https://twitter.com/courselit" target="_blank">@CourseLit</a>.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import mongoose from "mongoose";
2+
3+
mongoose.connect(process.env.DB_CONNECTION_STRING, {
4+
useNewUrlParser: true,
5+
useUnifiedTopology: true,
6+
});
7+
8+
const LessonSchema = new mongoose.Schema({
9+
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
10+
lessonId: { type: String, required: true },
11+
published: { type: Boolean, required: true, default: false },
12+
});
13+
14+
const Lesson = mongoose.model("Lesson", LessonSchema);
15+
16+
async function publishExistingLessons() {
17+
const lessonCount = await Lesson.countDocuments({ published: false });
18+
console.log(
19+
`🚀 Found ${lessonCount} unpublished lessons. Publishing them...`,
20+
);
21+
const result = await Lesson.updateMany(
22+
{
23+
published: false,
24+
},
25+
{
26+
$set: {
27+
published: true,
28+
},
29+
},
30+
);
31+
32+
console.log(
33+
`🏁 Lesson publish migration complete. Matched: ${result.matchedCount}, Updated: ${result.modifiedCount}`,
34+
);
35+
}
36+
37+
(async () => {
38+
await publishExistingLessons();
39+
mongoose.connection.close();
40+
})();

apps/web/app/(with-contexts)/course-old/[slug]/[id]/page.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,19 @@ export default function ProductPage(props: {
139139
</WidgetErrorBoundary>
140140
</div>
141141
</div>
142-
{isEnrolled(product.courseId, profile as Profile) && (
143-
<div className="self-end">
144-
<Link
145-
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
146-
>
147-
<Button2 className="flex gap-1 items-center">
148-
{COURSE_PROGRESS_START}
149-
<ArrowRight />
150-
</Button2>
151-
</Link>
152-
</div>
153-
)}
142+
{isEnrolled(product.courseId, profile as Profile) &&
143+
product.firstLesson && (
144+
<div className="self-end">
145+
<Link
146+
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
147+
>
148+
<Button2 className="flex gap-1 items-center">
149+
{COURSE_PROGRESS_START}
150+
<ArrowRight />
151+
</Button2>
152+
</Link>
153+
</div>
154+
)}
154155
</div>
155156
);
156157
}

apps/web/app/(with-contexts)/course/[slug]/[id]/page.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,22 @@ export default function ProductPage(props: {
143143
</WidgetErrorBoundary>
144144
</div>
145145
</div>
146-
{isEnrolled(product.courseId, profile as Profile) && (
147-
<div className="self-end">
148-
<Link
149-
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
150-
>
151-
<Button
152-
theme={theme.theme}
153-
className="flex gap-1 items-center"
146+
{isEnrolled(product.courseId, profile as Profile) &&
147+
product.firstLesson && (
148+
<div className="self-end">
149+
<Link
150+
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
154151
>
155-
{COURSE_PROGRESS_START}
156-
<ArrowRight />
157-
</Button>
158-
</Link>
159-
</div>
160-
)}
152+
<Button
153+
theme={theme.theme}
154+
className="flex gap-1 items-center"
155+
>
156+
{COURSE_PROGRESS_START}
157+
<ArrowRight />
158+
</Button>
159+
</Link>
160+
</div>
161+
)}
161162
</div>
162163
);
163164
}

apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/page.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useContext, useState } from "react";
44
import { useRouter, useParams } from "next/navigation";
55
import { Button } from "@/components/ui/button";
6+
import { Badge } from "@/components/ui/badge";
67
import {
78
DropdownMenu,
89
DropdownMenuContent,
@@ -350,7 +351,17 @@ export default function ContentPage() {
350351
{lesson.title}
351352
</span>
352353
</div>
353-
<ChevronRight className="h-4 w-4 text-muted-foreground" />
354+
<div className="flex items-center space-x-3">
355+
{!lesson.published && (
356+
<Badge
357+
variant="outline"
358+
className="ml-2 text-xs"
359+
>
360+
Draft
361+
</Badge>
362+
)}
363+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
364+
</div>
354365
</div>
355366
)}
356367
key={JSON.stringify(product.lessons)}

apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/page.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ import {
3030
} from "@/components/ui/dialog";
3131
import {
3232
APP_MESSAGE_LESSON_DELETED,
33+
BTN_PUBLISH,
34+
BTN_UNPUBLISH,
3335
BUTTON_NEW_LESSON_TEXT,
3436
COURSE_CONTENT_HEADER,
3537
EDIT_LESSON_TEXT,
3638
LESSON_EMBED_URL_LABEL,
3739
LESSON_CONTENT_LABEL,
40+
LESSON_VISIBILITY,
41+
LESSON_VISIBILITY_TOOLTIP,
3842
MANAGE_COURSES_PAGE_HEADING,
3943
TOAST_TITLE_ERROR,
4044
TOAST_TITLE_SUCCESS,
@@ -124,6 +128,7 @@ export default function LessonPage() {
124128
media: undefined,
125129
downloadable: false,
126130
requiresEnrollment: true,
131+
published: false,
127132
courseId: productId,
128133
groupId: sectionId,
129134
});
@@ -177,6 +182,7 @@ export default function LessonPage() {
177182
caption
178183
},
179184
requiresEnrollment,
185+
published,
180186
lessonId
181187
}
182188
}
@@ -194,6 +200,7 @@ export default function LessonPage() {
194200
const loadedLesson = {
195201
...response.lesson,
196202
type: response.lesson.type.toLowerCase() as LessonType,
203+
published: response.lesson.published ?? false,
197204
};
198205

199206
// Store the loaded lesson in ref for future comparison
@@ -300,6 +307,7 @@ export default function LessonPage() {
300307
downloadable: lesson?.downloadable,
301308
content: JSON.stringify(content),
302309
requiresEnrollment: lesson?.requiresEnrollment,
310+
published: !!lesson?.published,
303311
},
304312
},
305313
})
@@ -344,6 +352,7 @@ export default function LessonPage() {
344352
courseId: lesson?.courseId,
345353
requiresEnrollment: lesson?.requiresEnrollment,
346354
groupId: lesson?.groupId,
355+
published: !!lesson?.published,
347356
},
348357
},
349358
})
@@ -604,6 +613,7 @@ export default function LessonPage() {
604613
</Label>
605614
<p className="text-sm text-muted-foreground">
606615
Allow students to preview this lesson
616+
without enrolling
607617
</p>
608618
</div>
609619
<Switch
@@ -616,6 +626,35 @@ export default function LessonPage() {
616626
}
617627
/>
618628
</div>
629+
<div className="flex items-center justify-between">
630+
<div className="space-y-0.5">
631+
<Label
632+
htmlFor="published"
633+
className="font-semibold"
634+
>
635+
{LESSON_VISIBILITY}
636+
</Label>
637+
<p className="text-sm text-muted-foreground">
638+
{LESSON_VISIBILITY_TOOLTIP}
639+
</p>
640+
</div>
641+
<div className="flex items-center gap-2">
642+
<span className="text-sm text-muted-foreground">
643+
{lesson.published
644+
? BTN_UNPUBLISH
645+
: BTN_PUBLISH}
646+
</span>
647+
<Switch
648+
id="published"
649+
checked={!!lesson.published}
650+
onCheckedChange={(checked) =>
651+
updateLesson({
652+
published: checked,
653+
})
654+
}
655+
/>
656+
</div>
657+
</div>
619658
</div>
620659

621660
<div className="flex items-center justify-between pt-6">

apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/customers/page.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export default function CustomersPage() {
8585
.includes(searchTerm.toLowerCase()) ||
8686
member.user.email.toLowerCase().includes(searchTerm.toLowerCase()),
8787
);
88+
const publishedLessons =
89+
product?.lessons?.filter((lesson) => lesson.published) || [];
8890

8991
const fetchStudents = async () => {
9092
setLoading(true);
@@ -137,16 +139,23 @@ export default function CustomersPage() {
137139
.build();
138140
try {
139141
const response = await fetch.exec();
142+
const publishedLessonIds = new Set(
143+
publishedLessons.map((lesson) => lesson.lessonId),
144+
);
145+
const publishedLessonsCount = publishedLessonIds.size;
140146
setMembers(
141147
response.members.map((member: any) => ({
142148
...member,
143149
progressInPercentage:
144150
product?.type?.toLowerCase() ===
145151
Constants.CourseType.COURSE &&
146-
product?.lessons?.length! > 0
152+
publishedLessonsCount > 0
147153
? Math.round(
148-
((member.completedLessons?.length || 0) /
149-
(product?.lessons?.length || 0)) *
154+
((member.completedLessons || []).filter(
155+
(lessonId: string) =>
156+
publishedLessonIds.has(lessonId),
157+
).length /
158+
publishedLessonsCount) *
150159
100,
151160
)
152161
: undefined,
@@ -409,8 +418,7 @@ export default function CustomersPage() {
409418
{product?.type?.toLowerCase() ===
410419
Constants.CourseType.COURSE ? (
411420
<>
412-
{product?.lessons?.length! >
413-
0 && (
421+
{publishedLessons.length > 0 && (
414422
<div className="flex items-center space-x-2">
415423
<div className="w-20 bg-gray-200 rounded-full h-2.5">
416424
<div
@@ -447,7 +455,7 @@ export default function CustomersPage() {
447455
</DialogTitle>
448456
</DialogHeader>
449457
<DialogDescription className="max-h-[400px] overflow-y-scroll">
450-
{product?.lessons?.map(
458+
{publishedLessons.map(
451459
(
452460
lesson: any,
453461
) => (

apps/web/graphql/courses/logic.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ export const getCourseOrThrow = async (
9999
return course;
100100
};
101101

102-
async function formatCourse(courseId: string, ctx: GQLContext) {
102+
async function formatCourse(
103+
courseId: string,
104+
ctx: GQLContext,
105+
includeUnpublishedLessons: boolean = false,
106+
) {
103107
const course: InternalCourse | null = (await CourseModel.findOne({
104108
courseId,
105109
domain: ctx.subdomain._id,
@@ -118,6 +122,8 @@ async function formatCourse(courseId: string, ctx: GQLContext) {
118122
const { nextLesson } = await getPrevNextCursor(
119123
course.courseId,
120124
ctx.subdomain._id,
125+
undefined,
126+
!includeUnpublishedLessons,
121127
);
122128
(course as any).firstLesson = nextLesson;
123129
}
@@ -154,12 +160,15 @@ export const getCourse = async (
154160
]) || checkOwnershipWithoutModel(course, ctx);
155161

156162
if (isOwner) {
157-
return await formatCourse(course.courseId, ctx);
163+
return await formatCourse(course.courseId, ctx, true);
158164
}
159165
}
160166

161167
if (course.published) {
162-
return await formatCourse(course.courseId, ctx);
168+
const formattedCourse = await formatCourse(course.courseId, ctx);
169+
return asGuest
170+
? { ...formattedCourse, __forcePublishedLessons: true }
171+
: formattedCourse;
163172
} else {
164173
return null;
165174
}

0 commit comments

Comments
 (0)