Skip to content

Commit 85dd1fb

Browse files
rajat1saxenaRajat
andauthored
Re-arrange sections and lessons (#747)
* Re-arrange sections * Cross section lesson drag-n-drop; Section d-n-d replaced with buttons; * Corrected drip links; profile update regression fix; * Curriculum block respects section ranks * Docs updated * corrected screenshots --------- Co-authored-by: Rajat <hi@rajatsaxena.dev>
1 parent 9d85021 commit 85dd1fb

File tree

34 files changed

+3833
-531
lines changed

34 files changed

+3833
-531
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Use **Cursors** (`.cursor()`) to stream data from MongoDB, ensuring the script remains memory-efficient regardless of dataset size.
1717
- Use **Batching** with `bulkWrite` (e.g., batches of 500) to maximize performance and minimize network roundtrips.
1818
- Ensure **Idempotency** (safe to re-run) by using upserts or `$setOnInsert` where applicable.
19+
- When making changes to the structure of the Course, consider how it affects its representation on its public page (`apps/web/app/(with-contexts)/(with-layout)/p/[id]/page.tsx`) and the course viewer (`apps/web/app/(with-contexts)/course/[slug]/[id]/page.tsx`).
1920

2021
### Workspace map (core modules):
2122

@@ -48,6 +49,7 @@
4849
- Always add or update test when introducing changes to `apps/web/graphql` folder, even if nobody asked.
4950
- Run `pnpm test` to run the tests.
5051
- Fix any test or type errors until the whole suite is green.
52+
- Refrain from creating new files when adding tests in `apps/web/graphql` subdirectories. Re-use `logic.test.ts` files for adding new test suites i.e. describe blocks.
5153

5254
## PR instructions
5355

154 KB
Loading
155 KB
Loading

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ layout: ../../../layouts/MainLayout.astro
66

77
CourseLit uses the concept of a `Lesson`. It is very similar to what we generally see in books, i.e., a large piece of information is divided into smaller chunks called lessons.
88

9-
Similarly, you can break down your course into `Lessons` and group the lessons into [Sections](/en/products/section).
9+
Similarly, you can break down your course into `Lessons` and group the lessons into [Sections](/en/courses/section).
1010

1111
## Sections
1212

apps/docs/src/pages/en/courses/section.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ Here’s how sections look in various parts of the platform.
4646

4747
![Edit Section Settings](/assets/products/edit-section-settings.png)
4848

49+
## Rearranging Sections
50+
51+
You can move sections up or down as you like. Click the chevron up or down buttons to move a section.
52+
53+
![Move a section](/assets/products/section-reordering.png)
54+
55+
## Moving a Lesson Between Sections
56+
57+
Use the drag-and-drop handles on the left side of a lesson's listing to move it to any section.
58+
59+
![Move a lesson](/assets/products/lesson-reordering.png)
60+
4961
## Drip a Section
5062

5163
You can release a section on a **specific date** or **after a certain number of days have elapsed since the time a student enrolls**.
@@ -76,6 +88,8 @@ If drip configuration is enabled for a section, a student won't be able to acces
7688
4. Select the number of days.
7789
5. Click `Continue` to save it.
7890

91+
> Rearranging a section with drip enabled may affect its drip schedule; use caution.
92+
7993
### Notify Users When a Section Has Dripped
8094

8195
1. Click on the `Email Notification` checkbox.
@@ -98,7 +112,7 @@ On the course viewer, the customer will see the clock icon against the section n
98112

99113
2. Click `Delete` on the confirmation dialog.
100114

101-
> A section must be empty (i.e., have no lessons attached to it) in order to be deleted.
115+
> A section must be empty (i.e., have no lessons attached) before it can be deleted. Move any lessons to another section to make it empty.
102116
103117
## Next Step
104118

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { sortCourseGroups } from "@ui-lib/utils";
21
import { Course, Group, Lesson } from "@courselit/common-models";
32
import { FetchBuilder } from "@courselit/utils";
43

@@ -92,15 +91,16 @@ export const getProduct = async (
9291
export function formatCourse(
9392
post: Course & { lessons: Lesson[]; firstLesson: string; groups: Group[] },
9493
): CourseFrontend {
95-
for (const group of sortCourseGroups(post as Course)) {
96-
(group as GroupWithLessons).lessons = post.lessons
94+
const groupsWithLessons = post.groups.map((group) => ({
95+
...group,
96+
lessons: post.lessons
9797
.filter((lesson: Lesson) => lesson.groupId === group.id)
9898
.sort(
9999
(a: any, b: any) =>
100100
group.lessonsOrder?.indexOf(a.lessonId) -
101101
group.lessonsOrder?.indexOf(b.lessonId),
102-
);
103-
}
102+
),
103+
}));
104104

105105
return {
106106
title: post.title,
@@ -111,7 +111,7 @@ export function formatCourse(
111111
slug: post.slug,
112112
cost: post.cost,
113113
courseId: post.courseId,
114-
groups: post.groups as GroupWithLessons[],
114+
groups: groupsWithLessons as GroupWithLessons[],
115115
tags: post.tags,
116116
firstLesson: post.firstLesson,
117117
paymentPlans: post.paymentPlans,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { formatCourse } from "../helpers";
2+
3+
describe("course helpers formatCourse", () => {
4+
const makeCourse = () =>
5+
({
6+
title: "Course",
7+
description: "{}",
8+
featuredImage: undefined,
9+
updatedAt: new Date().toISOString(),
10+
creatorId: "creator",
11+
slug: "course",
12+
cost: 0,
13+
courseId: "course-1",
14+
tags: [],
15+
paymentPlans: [],
16+
defaultPaymentPlan: "",
17+
firstLesson: "lesson-1",
18+
groups: [
19+
{
20+
id: "group-2",
21+
name: "Group 2",
22+
rank: 2000,
23+
lessonsOrder: ["lesson-3", "lesson-2"],
24+
},
25+
{
26+
id: "group-1",
27+
name: "Group 1",
28+
rank: 1000,
29+
lessonsOrder: ["lesson-1"],
30+
},
31+
],
32+
lessons: [
33+
{
34+
lessonId: "lesson-2",
35+
title: "Lesson 2",
36+
groupId: "group-2",
37+
},
38+
{
39+
lessonId: "lesson-1",
40+
title: "Lesson 1",
41+
groupId: "group-1",
42+
},
43+
{
44+
lessonId: "lesson-3",
45+
title: "Lesson 3",
46+
groupId: "group-2",
47+
},
48+
],
49+
}) as any;
50+
51+
it("preserves group order from the backend response", () => {
52+
const formatted = formatCourse(makeCourse());
53+
54+
expect(formatted.groups.map((group) => group.id)).toEqual([
55+
"group-2",
56+
"group-1",
57+
]);
58+
});
59+
60+
it("sorts lessons within each group by lessonsOrder", () => {
61+
const formatted = formatCourse(makeCourse());
62+
63+
expect(
64+
formatted.groups[0].lessons.map((lesson) => lesson.lessonId),
65+
).toEqual(["lesson-3", "lesson-2"]);
66+
});
67+
});

apps/web/app/(with-contexts)/course/[slug]/[id]/__tests__/layout-with-sidebar.test.tsx

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ describe("generateSideBarItems", () => {
214214
status: true,
215215
type: Constants.dripType[1].split("-")[0].toUpperCase(),
216216
dateInUTC: new Date(
217-
"2026-03-24T00:00:00.000Z",
217+
"2099-03-24T00:00:00.000Z",
218218
).getTime(),
219219
},
220220
},
@@ -374,7 +374,7 @@ describe("generateSideBarItems", () => {
374374
status: true,
375375
type: Constants.dripType[1].split("-")[0].toUpperCase(),
376376
dateInUTC: new Date(
377-
"2026-03-24T00:00:00.000Z",
377+
"2099-03-24T00:00:00.000Z",
378378
).getTime(),
379379
},
380380
},
@@ -458,7 +458,7 @@ describe("generateSideBarItems", () => {
458458
);
459459

460460
expect(items[2].badge?.text).toBe("Mar 22, 2026");
461-
expect(items[2].badge?.description).toBe("Available on Mar 22, 2026");
461+
expect(items[2].badge?.description).toBe("");
462462
});
463463

464464
it("uses purchase createdAt as the relative drip anchor when lastDripAt is absent", () => {
@@ -648,4 +648,81 @@ describe("generateSideBarItems", () => {
648648

649649
expect(items[3].badge?.text).toBe("3 days");
650650
});
651+
652+
it("renders reordered lessons under the destination section in sidebar order", () => {
653+
const course = {
654+
title: "Course",
655+
description: "",
656+
featuredImage: undefined,
657+
updatedAt: new Date().toISOString(),
658+
creatorId: "creator-1",
659+
slug: "test-course",
660+
cost: 0,
661+
courseId: "course-1",
662+
tags: [],
663+
paymentPlans: [],
664+
defaultPaymentPlan: "",
665+
firstLesson: "lesson-2",
666+
groups: [
667+
{
668+
id: "group-1",
669+
name: "First Section",
670+
lessons: [
671+
{
672+
lessonId: "lesson-1",
673+
title: "Text 1",
674+
requiresEnrollment: false,
675+
},
676+
],
677+
},
678+
{
679+
id: "group-2",
680+
name: "Second Section",
681+
lessons: [
682+
{
683+
lessonId: "lesson-2",
684+
title: "Chapter 5 - Text 2",
685+
requiresEnrollment: false,
686+
},
687+
{
688+
lessonId: "lesson-3",
689+
title: "Text 3",
690+
requiresEnrollment: false,
691+
},
692+
],
693+
},
694+
],
695+
} as unknown as CourseFrontend;
696+
697+
const profile = {
698+
userId: "user-1",
699+
purchases: [
700+
{
701+
courseId: "course-1",
702+
accessibleGroups: ["group-1", "group-2"],
703+
},
704+
],
705+
} as unknown as Profile;
706+
707+
const items = generateSideBarItems(
708+
course,
709+
profile,
710+
"/course/test-course/course-1",
711+
);
712+
713+
const firstSectionItems = items.find(
714+
(item) => item.title === "First Section",
715+
)?.items;
716+
const secondSectionItems = items.find(
717+
(item) => item.title === "Second Section",
718+
)?.items;
719+
720+
expect(firstSectionItems?.map((item) => item.title)).toEqual([
721+
"Text 1",
722+
]);
723+
expect(secondSectionItems?.map((item) => item.title)).toEqual([
724+
"Chapter 5 - Text 2",
725+
"Text 3",
726+
]);
727+
});
651728
});

apps/web/app/(with-contexts)/course/[slug]/[id]/helpers.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { sortCourseGroups } from "@ui-lib/utils";
21
import { Course, Group, Lesson } from "@courselit/common-models";
32
import { FetchBuilder } from "@courselit/utils";
43

@@ -92,15 +91,16 @@ export const getProduct = async (
9291
export function formatCourse(
9392
post: Course & { lessons: Lesson[]; firstLesson: string; groups: Group[] },
9493
): CourseFrontend {
95-
for (const group of sortCourseGroups(post as Course)) {
96-
(group as GroupWithLessons).lessons = post.lessons
94+
const groupsWithLessons = post.groups.map((group) => ({
95+
...group,
96+
lessons: post.lessons
9797
.filter((lesson: Lesson) => lesson.groupId === group.id)
9898
.sort(
9999
(a: any, b: any) =>
100100
group.lessonsOrder?.indexOf(a.lessonId) -
101101
group.lessonsOrder?.indexOf(b.lessonId),
102-
);
103-
}
102+
),
103+
}));
104104

105105
return {
106106
title: post.title,
@@ -111,7 +111,7 @@ export function formatCourse(
111111
slug: post.slug,
112112
cost: post.cost,
113113
courseId: post.courseId,
114-
groups: post.groups as GroupWithLessons[],
114+
groups: groupsWithLessons as GroupWithLessons[],
115115
tags: post.tags,
116116
firstLesson: post.firstLesson,
117117
paymentPlans: post.paymentPlans,

0 commit comments

Comments
 (0)