Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added apps/docs/public/assets/lessons/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/docs/public/assets/lessons/visibility.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions apps/docs/src/pages/en/courses/add-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ A lesson is a container for the actual learning material. CourseLit supports mul

6. Click `Save lesson`.

## Preview lessons

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.

![Preview lesson](/assets/lessons/preview.png)

## Control lesson visibility

By default, lessons are unpublished i.e., not visible to learners. To publish a lesson, toggle the `Publish` switch as shown.

![Publish lesson](/assets/lessons/visibility.png)

## Stuck somewhere?

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>.
40 changes: 40 additions & 0 deletions apps/web/.migrations/10-02-26_23-55-publish-existing-lessons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import mongoose from "mongoose";

mongoose.connect(process.env.DB_CONNECTION_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
});

const LessonSchema = new mongoose.Schema({
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
lessonId: { type: String, required: true },
published: { type: Boolean, required: true, default: false },
});

const Lesson = mongoose.model("Lesson", LessonSchema);

async function publishExistingLessons() {
const lessonCount = await Lesson.countDocuments({ published: false });
console.log(
`🚀 Found ${lessonCount} unpublished lessons. Publishing them...`,
);
const result = await Lesson.updateMany(
{
published: false,
},
{
$set: {
published: true,
},
},
);

console.log(
`🏁 Lesson publish migration complete. Matched: ${result.matchedCount}, Updated: ${result.modifiedCount}`,
);
}

(async () => {
await publishExistingLessons();
mongoose.connection.close();
})();
25 changes: 13 additions & 12 deletions apps/web/app/(with-contexts)/course-old/[slug]/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,19 @@ export default function ProductPage(props: {
</WidgetErrorBoundary>
</div>
</div>
{isEnrolled(product.courseId, profile as Profile) && (
<div className="self-end">
<Link
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
>
<Button2 className="flex gap-1 items-center">
{COURSE_PROGRESS_START}
<ArrowRight />
</Button2>
</Link>
</div>
)}
{isEnrolled(product.courseId, profile as Profile) &&
product.firstLesson && (
<div className="self-end">
<Link
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
>
<Button2 className="flex gap-1 items-center">
{COURSE_PROGRESS_START}
<ArrowRight />
</Button2>
</Link>
</div>
)}
</div>
);
}
29 changes: 15 additions & 14 deletions apps/web/app/(with-contexts)/course/[slug]/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,21 +143,22 @@ export default function ProductPage(props: {
</WidgetErrorBoundary>
</div>
</div>
{isEnrolled(product.courseId, profile as Profile) && (
<div className="self-end">
<Link
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
>
<Button
theme={theme.theme}
className="flex gap-1 items-center"
{isEnrolled(product.courseId, profile as Profile) &&
product.firstLesson && (
<div className="self-end">
<Link
href={`/course/${product.slug}/${product.courseId}/${product.firstLesson}`}
>
{COURSE_PROGRESS_START}
<ArrowRight />
</Button>
</Link>
</div>
)}
<Button
theme={theme.theme}
className="flex gap-1 items-center"
>
{COURSE_PROGRESS_START}
<ArrowRight />
</Button>
</Link>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useContext, useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
Expand Down Expand Up @@ -350,7 +351,17 @@ export default function ContentPage() {
{lesson.title}
</span>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<div className="flex items-center space-x-3">
{!lesson.published && (
<Badge
variant="outline"
className="ml-2 text-xs"
>
Draft
</Badge>
)}
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</div>
</div>
)}
key={JSON.stringify(product.lessons)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ import {
} from "@/components/ui/dialog";
import {
APP_MESSAGE_LESSON_DELETED,
BTN_PUBLISH,
BTN_UNPUBLISH,
BUTTON_NEW_LESSON_TEXT,
COURSE_CONTENT_HEADER,
EDIT_LESSON_TEXT,
LESSON_EMBED_URL_LABEL,
LESSON_CONTENT_LABEL,
LESSON_VISIBILITY,
LESSON_VISIBILITY_TOOLTIP,
MANAGE_COURSES_PAGE_HEADING,
TOAST_TITLE_ERROR,
TOAST_TITLE_SUCCESS,
Expand Down Expand Up @@ -124,6 +128,7 @@ export default function LessonPage() {
media: undefined,
downloadable: false,
requiresEnrollment: true,
published: false,
courseId: productId,
groupId: sectionId,
});
Expand Down Expand Up @@ -177,6 +182,7 @@ export default function LessonPage() {
caption
},
requiresEnrollment,
published,
lessonId
}
}
Expand All @@ -194,6 +200,7 @@ export default function LessonPage() {
const loadedLesson = {
...response.lesson,
type: response.lesson.type.toLowerCase() as LessonType,
published: response.lesson.published ?? false,
};

// Store the loaded lesson in ref for future comparison
Expand Down Expand Up @@ -300,6 +307,7 @@ export default function LessonPage() {
downloadable: lesson?.downloadable,
content: JSON.stringify(content),
requiresEnrollment: lesson?.requiresEnrollment,
published: !!lesson?.published,
},
},
})
Expand Down Expand Up @@ -344,6 +352,7 @@ export default function LessonPage() {
courseId: lesson?.courseId,
requiresEnrollment: lesson?.requiresEnrollment,
groupId: lesson?.groupId,
published: !!lesson?.published,
},
},
})
Expand Down Expand Up @@ -604,6 +613,7 @@ export default function LessonPage() {
</Label>
<p className="text-sm text-muted-foreground">
Allow students to preview this lesson
without enrolling
</p>
</div>
<Switch
Expand All @@ -616,6 +626,35 @@ export default function LessonPage() {
}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label
htmlFor="published"
className="font-semibold"
>
{LESSON_VISIBILITY}
</Label>
<p className="text-sm text-muted-foreground">
{LESSON_VISIBILITY_TOOLTIP}
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{lesson.published
? BTN_UNPUBLISH
: BTN_PUBLISH}
</span>
<Switch
id="published"
checked={!!lesson.published}
onCheckedChange={(checked) =>
updateLesson({
published: checked,
})
}
/>
</div>
</div>
</div>

<div className="flex items-center justify-between pt-6">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export default function CustomersPage() {
.includes(searchTerm.toLowerCase()) ||
member.user.email.toLowerCase().includes(searchTerm.toLowerCase()),
);
const publishedLessons =
product?.lessons?.filter((lesson) => lesson.published) || [];

const fetchStudents = async () => {
setLoading(true);
Expand Down Expand Up @@ -137,16 +139,23 @@ export default function CustomersPage() {
.build();
try {
const response = await fetch.exec();
const publishedLessonIds = new Set(
publishedLessons.map((lesson) => lesson.lessonId),
);
const publishedLessonsCount = publishedLessonIds.size;
setMembers(
response.members.map((member: any) => ({
...member,
progressInPercentage:
product?.type?.toLowerCase() ===
Constants.CourseType.COURSE &&
product?.lessons?.length! > 0
publishedLessonsCount > 0
? Math.round(
((member.completedLessons?.length || 0) /
(product?.lessons?.length || 0)) *
((member.completedLessons || []).filter(
(lessonId: string) =>
publishedLessonIds.has(lessonId),
).length /
publishedLessonsCount) *
100,
)
: undefined,
Expand Down Expand Up @@ -409,8 +418,7 @@ export default function CustomersPage() {
{product?.type?.toLowerCase() ===
Constants.CourseType.COURSE ? (
<>
{product?.lessons?.length! >
0 && (
{publishedLessons.length > 0 && (
<div className="flex items-center space-x-2">
<div className="w-20 bg-gray-200 rounded-full h-2.5">
<div
Expand Down Expand Up @@ -447,7 +455,7 @@ export default function CustomersPage() {
</DialogTitle>
</DialogHeader>
<DialogDescription className="max-h-[400px] overflow-y-scroll">
{product?.lessons?.map(
{publishedLessons.map(
(
lesson: any,
) => (
Expand Down
15 changes: 12 additions & 3 deletions apps/web/graphql/courses/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ export const getCourseOrThrow = async (
return course;
};

async function formatCourse(courseId: string, ctx: GQLContext) {
async function formatCourse(
courseId: string,
ctx: GQLContext,
includeUnpublishedLessons: boolean = false,
) {
const course: InternalCourse | null = (await CourseModel.findOne({
courseId,
domain: ctx.subdomain._id,
Expand All @@ -118,6 +122,8 @@ async function formatCourse(courseId: string, ctx: GQLContext) {
const { nextLesson } = await getPrevNextCursor(
course.courseId,
ctx.subdomain._id,
undefined,
!includeUnpublishedLessons,
);
(course as any).firstLesson = nextLesson;
}
Expand Down Expand Up @@ -154,12 +160,15 @@ export const getCourse = async (
]) || checkOwnershipWithoutModel(course, ctx);

if (isOwner) {
return await formatCourse(course.courseId, ctx);
return await formatCourse(course.courseId, ctx, true);
}
}

if (course.published) {
return await formatCourse(course.courseId, ctx);
const formattedCourse = await formatCourse(course.courseId, ctx);
return asGuest
? { ...formattedCourse, __forcePublishedLessons: true }
: formattedCourse;
} else {
return null;
}
Expand Down
6 changes: 5 additions & 1 deletion apps/web/graphql/courses/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ const courseType = new GraphQLObjectType({
lessons: {
type: new GraphQLList(lessonMetaType),
resolve: (course, args, context, info) =>
getAllLessons(course, context),
getAllLessons(
course,
context,
Boolean((course as any).__forcePublishedLessons),
),
},
updatedAt: { type: new GraphQLNonNull(GraphQLString) },
slug: { type: new GraphQLNonNull(GraphQLString) },
Expand Down
1 change: 1 addition & 0 deletions apps/web/graphql/lessons/__tests__/scorm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe("SCORM Logic Integration", () => {
lessonId: id("lesson-scorm"),
title: "SCORM Lesson",
type: Constants.LessonType.SCORM,
published: true,
requiresEnrollment: true,
content: {
launchUrl: "index.html",
Expand Down
Loading
Loading