Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/docs-new/content/docs/courses/add-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ A lesson is a container for the actual learning material. CourseLit supports mul

See the [guide to add a SCORM package](/lessons/scorm).

Most lesson types can also carry a rich text description, downloadable resources, or both, alongside their main content.

See the [guide to add a description and resources](/lessons/description-and-resources).

## Steps to add a new lesson

1. From the `Products` section in the dashboard, select your product to open its dashboard.
Expand Down
65 changes: 65 additions & 0 deletions apps/docs-new/content/docs/lessons/description-and-resources.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: Add a description and resources to a lesson
description: Add a description and resources to a lesson
---

Lessons can carry more than just their main content. You can add a rich-text `Description` to give your students notes, instructions or links, and attach `Resources` they can download, such as worksheets, slides or exercise files.

## Which lesson types support what

Not every lesson type offers both. Text lessons already hold their own rich text, so they take resources only, and PDF and File lessons already give students something to download, so they take a description only.

| Lesson type | Description | Resources |
| ----------- | ----------- | --------- |
| Text | — | Yes |
| Video | Yes | Yes |
| Audio | Yes | Yes |
| Embed | Yes | Yes |
| PDF | Yes | — |
| File | Yes | — |
| Quiz | — | — |
| SCORM | — | — |

Sections you can't use simply don't appear on the lesson screen.

## Add a description

1. Go to the `Products` page and click on the course you want to edit. Click on `Edit content`.

2. Click on an existing lesson, or click `Add lesson` and pick a type that supports a description.

3. Below the lesson's main content, you'll find the `Description` section. Write your notes in the editor.

The editor supports the same rich text formatting used elsewhere in CourseLit, so you can add headings, lists and links.

4. Click `Save lesson` to save the description along with the rest of the lesson.

## Add resources

Resources are files your students can download from the lesson page.

1. Open a lesson that supports resources, as described above.

2. Scroll to the `Resources` section and click `Add a resource`.

If the lesson has not been saved yet, this button is disabled and shows `Save the lesson to enable resource uploads`. Save the lesson first. A new lesson reopens once it's saved, so you can add resources straight away.

3. Upload the file you want to share.

4. Repeat for as many resources as you need. To remove one, use the remove option on that resource.

Removing a resource deletes its file. Each resource belongs to one lesson, so a file already used by another lesson can't be added again; upload a copy instead.

Unlike the description, resources are saved as soon as you add or remove one, so there is no need to click `Save lesson` afterwards.

The caption you set on a file is used as its download button label. If you don't set one, the original file name is used instead, so a descriptive caption is worth adding.

## What your students see

On the lesson page, the description appears below the lesson's main content, and the resources appear under a `Resources` heading as download buttons.

An empty description is not shown at all, so lessons you don't write a description for look exactly as they did before.

## Stuck somewhere?

We are always there 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>.
2 changes: 1 addition & 1 deletion apps/docs-new/content/docs/lessons/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Lessons",
"pages": ["add-quiz", "embed", "scorm"]
"pages": ["add-quiz", "description-and-resources", "embed", "scorm"]
}
4 changes: 4 additions & 0 deletions apps/docs/src/pages/en/courses/add-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ A lesson is a container for the actual learning material. CourseLit supports mul

See the [guide to add a SCORM package](/en/lessons/scorm).

Most lesson types can also carry a rich text description, downloadable resources, or both, alongside their main content.

See the [guide to add a description and resources](/en/lessons/description-and-resources).

## Steps to add a new lesson

1. From the `Products` section in the dashboard, select your product to open its dashboard.
Expand Down
66 changes: 66 additions & 0 deletions apps/docs/src/pages/en/lessons/description-and-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Add a description and resources to a lesson
description: Add a description and resources to a lesson
layout: ../../../layouts/MainLayout.astro
---

Lessons can carry more than just their main content. You can add a rich-text `Description` to give your students notes, instructions or links, and attach `Resources` they can download, such as worksheets, slides or exercise files.

## Which lesson types support what

Not every lesson type offers both. Text lessons already hold their own rich text, so they take resources only, and PDF and File lessons already give students something to download, so they take a description only.

| Lesson type | Description | Resources |
| ----------- | ----------- | --------- |
| Text | — | Yes |
| Video | Yes | Yes |
| Audio | Yes | Yes |
| Embed | Yes | Yes |
| PDF | Yes | — |
| File | Yes | — |
| Quiz | — | — |
| SCORM | — | — |

Sections you can't use simply don't appear on the lesson screen.

## Add a description

1. Go to the `Products` page and click on the course you want to edit. Click on `Edit content`.

2. Click on an existing lesson, or click `Add lesson` and pick a type that supports a description.

3. Below the lesson's main content, you'll find the `Description` section. Write your notes in the editor.

The editor supports the same rich text formatting used elsewhere in CourseLit, so you can add headings, lists and links.

4. Click `Save lesson` to save the description along with the rest of the lesson.

## Add resources

Resources are files your students can download from the lesson page.

1. Open a lesson that supports resources, as described above.

2. Scroll to the `Resources` section and click `Add a resource`.

If the lesson has not been saved yet, this button is disabled and shows `Save the lesson to enable resource uploads`. Save the lesson first. A new lesson reopens once it's saved, so you can add resources straight away.

3. Upload the file you want to share.

4. Repeat for as many resources as you need. To remove one, use the remove option on that resource.

Removing a resource deletes its file. Each resource belongs to one lesson, so a file already used by another lesson can't be added again; upload a copy instead.

Unlike the description, resources are saved as soon as you add or remove one, so there is no need to click `Save lesson` afterwards.

The caption you set on a file is used as its download button label. If you don't set one, the original file name is used instead, so a descriptive caption is worth adding.

## What your students see

On the lesson page, the description appears below the lesson's main content, and the resources appear under a `Resources` heading as download buttons.

An empty description is not shown at all, so lessons you don't write a description for look exactly as they did before.

## Stuck somewhere?

We are always there 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>.
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import "@testing-library/jest-dom";
// Mock dependencies
// Module-level variable to control lesson ID for edit mode
let mockLessonId: string | null = null;
const mockReplace = jest.fn();

jest.mock("next/navigation", () => ({
useRouter: () => ({
push: jest.fn(),
replace: jest.fn(),
replace: mockReplace,
}),
useParams: () => ({
id: "product-1",
Expand Down Expand Up @@ -47,6 +48,10 @@ jest.mock("@/hooks/use-product", () => ({
}));

jest.mock("@courselit/utils", () => ({
lessonTypeSupportsDescription:
jest.requireActual("@courselit/utils").lessonTypeSupportsDescription,
lessonTypeSupportsAttachments:
jest.requireActual("@courselit/utils").lessonTypeSupportsAttachments,
FetchBuilder: jest.fn().mockImplementation(() => ({
setUrl: jest.fn().mockReturnThis(),
setPayload: jest.fn().mockReturnThis(),
Expand All @@ -58,6 +63,9 @@ jest.mock("@courselit/utils", () => ({

jest.mock("@courselit/text-editor", () => ({
emptyDoc: { type: "doc", content: [] },
Editor: ({ placeholder }: any) => (
<div data-testid="description-editor">{placeholder}</div>
),
}));

jest.mock("@courselit/components-library", () => ({
Expand All @@ -70,6 +78,9 @@ jest.mock("@courselit/components-library", () => ({
isUploading: false,
uploadProgress: 0,
}),
MediaSelector: ({ title, srcTitle }: any) => (
<div data-testid="media-selector">{title || srcTitle}</div>
),
}));

jest.mock("../lesson-content-renderer", () => ({
Expand Down Expand Up @@ -179,6 +190,7 @@ jest.mock("@ui-lib/utils", () => ({
return !!content && content.value !== "";
},
truncate: (str: string) => str,
toMediaInput: (media: any) => media,
}));

describe("LessonPage", () => {
Expand Down Expand Up @@ -312,6 +324,34 @@ describe("LessonPage", () => {
});
});

it("reopens a new text lesson for editing so resources can be added", async () => {
mockReplace.mockClear();
const mockExec = jest.fn().mockResolvedValue({
lesson: { lessonId: "new-lesson-id" },
});
(FetchBuilder as unknown as jest.Mock).mockImplementation(() => ({
setUrl: jest.fn().mockReturnThis(),
setPayload: jest.fn().mockReturnThis(),
setIsGraphQLEndpoint: jest.fn().mockReturnThis(),
build: jest.fn().mockReturnThis(),
exec: mockExec,
}));

render(<LessonPage />, { wrapper });

fireEvent.change(screen.getByPlaceholderText("Enter lesson title"), {
target: { value: "New Lesson" },
});
fireEvent.click(screen.getByText("Update Content"));
fireEvent.click(screen.getByText("Save Lesson"));

await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith(
"/dashboard/product/product-1/content/section/section-1/lesson?id=new-lesson-id",
);
});
});

it("handles API error during save", async () => {
const mockExec = jest.fn().mockRejectedValue(new Error("API Error"));
(FetchBuilder as unknown as jest.Mock).mockImplementation(() => ({
Expand Down Expand Up @@ -944,4 +984,32 @@ describe("LessonPage", () => {
}
});
});
describe("description and resources sections", () => {
it.each([
[Constants.LessonType.TEXT, false, true],
[Constants.LessonType.VIDEO, true, true],
[Constants.LessonType.AUDIO, true, true],
[Constants.LessonType.EMBED, true, true],
[Constants.LessonType.PDF, true, false],
[Constants.LessonType.FILE, true, false],
[Constants.LessonType.QUIZ, false, false],
[Constants.LessonType.SCORM, false, false],
])(
"on a %s lesson shows description=%s and resources=%s",
async (type, expectsDescription, expectsResources) => {
render(<LessonPage />, { wrapper });

fireEvent.click(screen.getByTestId(`radio-item-${type}`));

await waitFor(() => {
expect(
screen.queryByTestId("description-editor") !== null,
).toBe(expectsDescription);
});
expect(screen.queryByText("Add a resource") !== null).toBe(
expectsResources,
);
},
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Media, Profile } from "@courselit/common-models";
import { MediaSelector } from "@courselit/components-library";
import { useContext } from "react";
import { AddressContext, ProfileContext } from "@components/contexts";
import { LESSON_RESOURCES_ADD } from "@ui-config/strings";

interface LessonAttachmentsProps {
attachments: Partial<Media>[];
disabled?: boolean;
onChange: (attachments: Partial<Media>[]) => void;
}

export function LessonAttachments({
attachments,
disabled = false,
onChange,
}: LessonAttachmentsProps) {
const address = useContext(AddressContext);
const { profile } = useContext(ProfileContext);

return (
<div className="space-y-2">
{attachments.map((attachment, index) => (
<MediaSelector
key={attachment.mediaId || index}
title=""
src={attachment.thumbnail || ""}
srcTitle={attachment.originalFileName || ""}
mediaId={attachment.mediaId}
profile={profile as Profile}
address={address}
strings={{}}
type="lesson"
deleteOnRemove={false}
disabled={disabled}
onSelection={() => {}}
onRemove={() => {
onChange(attachments.filter((_, i) => i !== index));
}}
/>
))}
<MediaSelector
key={`add-resource-${attachments.length}`}
title={LESSON_RESOURCES_ADD}
src=""
srcTitle=""
hidePreview={true}
disabled={disabled}
profile={profile as Profile}
address={address}
strings={{}}
type="lesson"
onSelection={(media?: Media) => {
if (media) {
onChange([...attachments, media]);
}
}}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { useContext, useState } from "react";
import { AddressContext, ProfileContext } from "@components/contexts";
import { FetchBuilder } from "@courselit/utils";
import { toMediaInput } from "@ui-lib/utils";
import { Textarea } from "@components/ui/textarea";
import dynamic from "next/dynamic";
const LessonEmbedViewer = dynamic(
Expand Down Expand Up @@ -65,12 +66,7 @@ export function LessonContentRenderer({
query,
variables: {
id: lesson?.lessonId,
media: media
? Object.assign({}, media, {
file:
media.access === "public" ? media.file : null,
})
: null,
media: media ? toMediaInput(media) : null,
},
})
.setIsGraphQLEndpoint(true)
Expand Down
Loading