From ba67ddd411bb78682214b94fcca716dd4bf066bf Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:13:04 +0100 Subject: [PATCH 01/25] feat: drafted `CourseContentOverview` component This component may be used to show the contents associated to a given check, e.g. within the `OverviewSection` when creating a new course. --- .../create/(sections)/ContentSection.tsx | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/components/courses/create/(sections)/ContentSection.tsx b/src/components/courses/create/(sections)/ContentSection.tsx index a465c1a7..b9951856 100644 --- a/src/components/courses/create/(sections)/ContentSection.tsx +++ b/src/components/courses/create/(sections)/ContentSection.tsx @@ -1,14 +1,18 @@ 'use client' import { generateHTML } from '@tiptap/core' -import { PenIcon, PlusCircleIcon, TrashIcon } from 'lucide-react' +import { Info, PenIcon, PlusCircleIcon, TrashIcon } from 'lucide-react' import CourseContentDialog from '@/src/components/courses/create/(sections)/CourseContentDialog' import { useCourseStore } from '@/src/components/courses/create/CreateCourseProvider' import { Button } from '@/src/components/shadcn/button' import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/shadcn/card' +import GenericCard from '@/src/components/Shared/Card' +import { CardStageJumpButton } from '@/src/components/Shared/CardStageJumpButton' import ConfirmationDialog from '@/src/components/Shared/ConfirmationDialog/ConfirmationDialog' import { RichTextEditor, RichTextEditorExtensions } from '@/src/components/tiptap-examples/RichTextEditor' import { useScopedI18n } from '@/src/i18n/client-localization' +import { cn } from '@/src/lib/Shared/utils' +import { CourseContent } from '@/src/schemas/CourseContentSchema' export default function ContentSection() { const { contents, removeCourseContent } = useCourseStore((store) => store) @@ -66,3 +70,53 @@ export default function ContentSection() { ) } + +export function CourseContentOverview({ jumpBackButton = true }: { jumpBackButton?: boolean }) { + const { contents } = useCourseStore((store) => store) + const t = useScopedI18n('Courses.Create.ContentSection') + + function Element(content: CourseContent) { + return ( + +

{content.title}

+

{content.description}

+
+ ) + } + + if (contents.length === 0) { + return ( + + {jumpBackButton && } +
+
+

{t('title')}

+
+
+
+ + {'There are currently no contents associated to this course.'} +
+
+ ) + } + + return ( + + {jumpBackButton && } +
+
+

{t('title')}

+
+
+ + {contents.length === 0 &&
} + +
+ {contents.map((c) => ( + + ))} +
+
+ ) +} From 5c2285947d2a0f2147e3e103bfea2abc72c91667 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:13:52 +0100 Subject: [PATCH 02/25] ref: added course contents to `OverviewSection` --- src/components/courses/create/(sections)/OverviewSection.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/courses/create/(sections)/OverviewSection.tsx b/src/components/courses/create/(sections)/OverviewSection.tsx index d0103ab8..6ddceedd 100644 --- a/src/components/courses/create/(sections)/OverviewSection.tsx +++ b/src/components/courses/create/(sections)/OverviewSection.tsx @@ -1,3 +1,4 @@ +import { CourseContentOverview } from '@/src/components/courses/create/(sections)/ContentSection' import GeneralSection from '@/src/components/courses/create/(sections)/GeneralSection' import QuestionsSection from '@/src/components/courses/create/(sections)/QuestionsSection' import SettingsSection from '@/src/components/courses/create/(sections)/SettingsSection' @@ -14,6 +15,7 @@ export async function OverviewSection() {
+
From 97a6bfca20bfdd4145eccddfec934cdf40ee078b Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:14:49 +0100 Subject: [PATCH 03/25] feat: drafted `/courses/contents/[courseId]` page This new page is going to show the contents of a given course so that users can familiarize themselves with the contents associated to a given course. --- .../courses/contents/[courseId]/page.tsx | 41 +++++++++++++++++++ .../tiptap-examples/RichTextEditor.tsx | 4 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/app/[locale]/courses/contents/[courseId]/page.tsx diff --git a/src/app/[locale]/courses/contents/[courseId]/page.tsx b/src/app/[locale]/courses/contents/[courseId]/page.tsx new file mode 100644 index 00000000..feb5ba51 --- /dev/null +++ b/src/app/[locale]/courses/contents/[courseId]/page.tsx @@ -0,0 +1,41 @@ +import { notFound } from 'next/navigation' +import { getCourseById } from '@/database/course/select' +import { Button } from '@/src/components/shadcn/button' +import PageHeading from '@/src/components/Shared/PageHeading' +import { RichTextEditor } from '@/src/components/tiptap-examples/RichTextEditor' +import { cn } from '@/src/lib/Shared/utils' +import { Course } from '@/src/schemas/CourseSchema' + +export default async function CourseContentsPage({ params }: { params: Promise<{ courseId: string }> }) { + const { courseId } = await params + const course = await getCourseById(courseId) + + if (!course) notFound() + + return ( + <> + + +
+
+ +
+ +
+ + +
+
+ + ) +} + +function ContentWrapper({ course }: { course: Course }) { + if (course.contents.length === 0 || !course.contents[0].content) return <>Course has no contents... + + return ( + <> + + + ) +} diff --git a/src/components/tiptap-examples/RichTextEditor.tsx b/src/components/tiptap-examples/RichTextEditor.tsx index 9657416d..5e0b6bf9 100644 --- a/src/components/tiptap-examples/RichTextEditor.tsx +++ b/src/components/tiptap-examples/RichTextEditor.tsx @@ -137,12 +137,14 @@ export function RichTextEditor({ disabled, readOnly, size = 'md', + editorPaneClassname, }: { onUpdateAction?: (content: object) => void defaultContent?: Content disabled?: boolean readOnly?: boolean size?: 'sm' | 'md' | 'lg' + editorPaneClassname?: string }) { const t = useScopedI18n('Components.RichTextEditor') const isMobile = useIsBreakpoint() @@ -199,7 +201,7 @@ export function RichTextEditor({ }} editor={editor} role='presentation' - className={cn('rounded-md border border-input-ring', 'flex flex-1 flex-col', 'min-h-72 p-5', 'cursor-text overflow-auto')} + className={cn('rounded-md border border-input-ring', 'flex flex-1 flex-col', 'min-h-72 p-5', 'cursor-text overflow-auto', editorPaneClassname)} /> From 57740ba500c6c7b2f79d13776a6123dc5610029d Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 10:22:21 +0100 Subject: [PATCH 04/25] feat: added "show contents" item to `CourseActionMenu` This way users can view the course contents associated to a given course. --- .../courses/(hamburger-menu)/CourseActionMenu.tsx | 14 +++++++++++++- src/i18n/locales/de.json | 4 ++++ src/i18n/locales/de.ts | 4 ++++ src/i18n/locales/en.json | 4 ++++ src/i18n/locales/en.ts | 4 ++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx b/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx index 467ae696..03fe08fa 100644 --- a/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx +++ b/src/components/courses/(hamburger-menu)/CourseActionMenu.tsx @@ -29,7 +29,7 @@ import { useSession } from '@/src/lib/auth/client' import { generateToken } from '@/src/lib/Shared/generateToken' import { Course } from '@/src/schemas/CourseSchema' -export default function CourseActionMenu({ id, questions, share_key, owner_id, collaborators }: {} & Course) { +export default function CourseActionMenu({ id, questions, share_key, owner_id, collaborators, contents }: {} & Course) { const t = useScopedI18n('Components.CourseActionMenu') const [menuOpen, setMenuOpen] = useState(false) @@ -106,6 +106,18 @@ export default function CourseActionMenu({ id, questions, share_key, owner_id, c + { + router.push(`${window.location.origin}/courses/contents/${id}`) + }} + disabled={contents.length === 0}> + {t('show_course_contents.label')} + + + {t('invite_to_submenu_label')} diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index bec865c4..c8586d27 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -430,6 +430,10 @@ "tooltip": "Übungs freigabelink erfolgreich in Zwischenablage kopiert.", "toast": "Erstellen des Übungslinks fehlgeschlagen." }, + "show_course_contents": { + "label": "Kursinhalte anzeigen", + "tooltip": "Dieser Kurs hat keine Inhalte die angezeigt werden können." + }, "edit_course": { "label": "Kurs bearbeiten", "tooltip": "Dir fehlen die Berechtigungen um diesen Kurs zu bearbeiten." diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 85ea860e..69e2c5eb 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -434,6 +434,10 @@ export default { tooltip: 'Übungs freigabelink erfolgreich in Zwischenablage kopiert.', toast: 'Erstellen des Übungslinks fehlgeschlagen.' }, + show_course_contents: { + label: 'Kursinhalte anzeigen', + tooltip: 'Dieser Kurs hat keine Inhalte die angezeigt werden können.' + }, edit_course: { label: 'Kurs bearbeiten', tooltip: 'Dir fehlen die Berechtigungen um diesen Kurs zu bearbeiten.' diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index ac3800e9..a36555d3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -440,6 +440,10 @@ "tooltip": "This course has no questions, examination disabled.", "toast": "Unable to start Practice" }, + "show_course_contents": { + "label": "Show Course Contents", + "tooltip": "This course has no contents, please create one first." + }, "edit_course": { "label": "Edit Course", "tooltip": "You are not allowed to edit this course!" diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index d0bb0788..a04bd5ed 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -440,6 +440,10 @@ export default { tooltip: 'This course has no questions, examination disabled.', toast: 'Unable to start Practice' }, + show_course_contents: { + label: 'Show Course Contents', + tooltip: 'This course has no contents, please create one first.' + }, edit_course: { label: 'Edit Course', tooltip: 'You are not allowed to edit this course!' From f5a34b4d8fb4677c6b45811cea44c7aaf95571e0 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:13:20 +0100 Subject: [PATCH 05/25] ref: modularized `ContentSection` into subcomponents Separated the rendering of the `ContentSection` into subcomponents to improve readability and maintainability. --- .../create/(sections)/ContentSection.tsx | 112 +++++++++++------- 1 file changed, 70 insertions(+), 42 deletions(-) diff --git a/src/components/courses/create/(sections)/ContentSection.tsx b/src/components/courses/create/(sections)/ContentSection.tsx index b9951856..173e8c72 100644 --- a/src/components/courses/create/(sections)/ContentSection.tsx +++ b/src/components/courses/create/(sections)/ContentSection.tsx @@ -1,7 +1,7 @@ 'use client' import { generateHTML } from '@tiptap/core' -import { Info, PenIcon, PlusCircleIcon, TrashIcon } from 'lucide-react' +import { Info, PenIcon, Plus, TrashIcon } from 'lucide-react' import CourseContentDialog from '@/src/components/courses/create/(sections)/CourseContentDialog' import { useCourseStore } from '@/src/components/courses/create/CreateCourseProvider' import { Button } from '@/src/components/shadcn/button' @@ -15,7 +15,7 @@ import { cn } from '@/src/lib/Shared/utils' import { CourseContent } from '@/src/schemas/CourseContentSchema' export default function ContentSection() { - const { contents, removeCourseContent } = useCourseStore((store) => store) + const { contents } = useCourseStore((store) => store) const t = useScopedI18n('Courses.Create.ContentSection') return ( @@ -25,48 +25,76 @@ export default function ContentSection() { {t('description')} -
- - - - {t('Actions.create_new_button_label')} + {contents.length > 0 ? : } + + +
+ ) +} + +function CreateContentButton({ disabled }: { disabled?: boolean }) { + const t = useScopedI18n('Courses.Create.ContentSection') + + return ( + +
+ +
+
+ ) +} + +function CourseContentRenderer() { + const t = useScopedI18n('Courses.Create.ContentSection') + const { contents, removeCourseContent } = useCourseStore((store) => store) + + return ( +
+ {contents.map((content) => { + const htmlContent = content.content ? generateHTML(content.content, RichTextEditorExtensions) : '' + + return ( + + + {content.title} + {content.description} + + + + + + removeCourseContent(content.categoryId)} + confirmLabel={t('Actions.delete_content_confirm_label')} + body={t('Actions.delete_content_dialog_body')}> + + + + + + - - {contents.map((content) => { - const htmlContent = content.content ? generateHTML(content.content, RichTextEditorExtensions) : '' - - return ( - - - {content.title} - {content.description} - - - - - - removeCourseContent(content.categoryId)} - confirmLabel={t('Actions.delete_content_confirm_label')} - body={t('Actions.delete_content_dialog_body')}> - - - - - - - - - ) - })} -
+ ) + })} + + ) +} + +function EmptyCourseContentBody() { + return ( +
+ + {'There are currently no contents associated to this course.'}
) } From 510a43fd36955e798d1d09412c9bfeb508c6ffc0 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:39:57 +0100 Subject: [PATCH 06/25] feat: redesigned `ContentSection` layout This way the `ContentSection` now mimics the existing `QuetionsSection`. This means it shows the contents in a list instead of a grid. The reason for this change is that this eliminates sudden changes in the app's layout and also makes it easier to re-use this section in the `OverviewSection`. --- .../create/(sections)/ContentSection.tsx | 102 +++++++++--------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/src/components/courses/create/(sections)/ContentSection.tsx b/src/components/courses/create/(sections)/ContentSection.tsx index 173e8c72..7b20f906 100644 --- a/src/components/courses/create/(sections)/ContentSection.tsx +++ b/src/components/courses/create/(sections)/ContentSection.tsx @@ -1,34 +1,33 @@ 'use client' -import { generateHTML } from '@tiptap/core' -import { Info, PenIcon, Plus, TrashIcon } from 'lucide-react' +import { Folder, Info, Pen, Plus, Trash2 } from 'lucide-react' import CourseContentDialog from '@/src/components/courses/create/(sections)/CourseContentDialog' import { useCourseStore } from '@/src/components/courses/create/CreateCourseProvider' import { Button } from '@/src/components/shadcn/button' -import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/shadcn/card' import GenericCard from '@/src/components/Shared/Card' import { CardStageJumpButton } from '@/src/components/Shared/CardStageJumpButton' import ConfirmationDialog from '@/src/components/Shared/ConfirmationDialog/ConfirmationDialog' -import { RichTextEditor, RichTextEditorExtensions } from '@/src/components/tiptap-examples/RichTextEditor' import { useScopedI18n } from '@/src/i18n/client-localization' import { cn } from '@/src/lib/Shared/utils' import { CourseContent } from '@/src/schemas/CourseContentSchema' -export default function ContentSection() { +export default function ContentSection({ jumpBackButton, disabled }: { jumpBackButton?: boolean; disabled?: boolean }) { const { contents } = useCourseStore((store) => store) const t = useScopedI18n('Courses.Create.ContentSection') return ( -
-
-

{t('title')}

- {t('description')} + + {jumpBackButton && } +
+
+

{t('title')}

+
- {contents.length > 0 ? : } + {contents.length > 0 ? : } - -
+ + ) } @@ -47,45 +46,52 @@ function CreateContentButton({ disabled }: { disabled?: boolean }) { ) } -function CourseContentRenderer() { +function CourseContentRenderer({ disabled }: { disabled?: boolean }) { const t = useScopedI18n('Courses.Create.ContentSection') - const { contents, removeCourseContent } = useCourseStore((store) => store) + const { contents, removeCourseContent, questionCategories } = useCourseStore((store) => store) return ( -
- {contents.map((content) => { - const htmlContent = content.content ? generateHTML(content.content, RichTextEditorExtensions) : '' - - return ( - - - {content.title} - {content.description} - - - - - - removeCourseContent(content.categoryId)} - confirmLabel={t('Actions.delete_content_confirm_label')} - body={t('Actions.delete_content_dialog_body')}> - - - - - - - - - ) - })} +
+ {contents.map((content, i) => ( + +
+
+

{content.title}

+ {} +
+
+ {content.description} +
+ + {questionCategories.find((c) => c.id === content.categoryId)?.name} +
+
+
+ + + + + removeCourseContent(content.categoryId)} confirmLabel={t('Actions.delete_content_confirm_label')} body={t('Actions.delete_content_dialog_body')}> + + +
+ ))}
) } From 26bc7a18278b3b2d0ea8a8d24f880e948ab587af Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:41:42 +0100 Subject: [PATCH 07/25] ref: re-used `ContentSection` in `OverviewSection` By redesigning the `ContentSection`'s layout it can be easily re-used in the `OverviewSection` for users to review their changes. --- .../create/(sections)/ContentSection.tsx | 51 ------------------- .../create/(sections)/OverviewSection.tsx | 4 +- 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/src/components/courses/create/(sections)/ContentSection.tsx b/src/components/courses/create/(sections)/ContentSection.tsx index 7b20f906..4aff1461 100644 --- a/src/components/courses/create/(sections)/ContentSection.tsx +++ b/src/components/courses/create/(sections)/ContentSection.tsx @@ -9,7 +9,6 @@ import { CardStageJumpButton } from '@/src/components/Shared/CardStageJumpButton import ConfirmationDialog from '@/src/components/Shared/ConfirmationDialog/ConfirmationDialog' import { useScopedI18n } from '@/src/i18n/client-localization' import { cn } from '@/src/lib/Shared/utils' -import { CourseContent } from '@/src/schemas/CourseContentSchema' export default function ContentSection({ jumpBackButton, disabled }: { jumpBackButton?: boolean; disabled?: boolean }) { const { contents } = useCourseStore((store) => store) @@ -104,53 +103,3 @@ function EmptyCourseContentBody() {
) } - -export function CourseContentOverview({ jumpBackButton = true }: { jumpBackButton?: boolean }) { - const { contents } = useCourseStore((store) => store) - const t = useScopedI18n('Courses.Create.ContentSection') - - function Element(content: CourseContent) { - return ( - -

{content.title}

-

{content.description}

-
- ) - } - - if (contents.length === 0) { - return ( - - {jumpBackButton && } -
-
-

{t('title')}

-
-
-
- - {'There are currently no contents associated to this course.'} -
-
- ) - } - - return ( - - {jumpBackButton && } -
-
-

{t('title')}

-
-
- - {contents.length === 0 &&
} - -
- {contents.map((c) => ( - - ))} -
-
- ) -} diff --git a/src/components/courses/create/(sections)/OverviewSection.tsx b/src/components/courses/create/(sections)/OverviewSection.tsx index 6ddceedd..30b1f2f1 100644 --- a/src/components/courses/create/(sections)/OverviewSection.tsx +++ b/src/components/courses/create/(sections)/OverviewSection.tsx @@ -1,4 +1,4 @@ -import { CourseContentOverview } from '@/src/components/courses/create/(sections)/ContentSection' +import ContentSection from '@/src/components/courses/create/(sections)/ContentSection' import GeneralSection from '@/src/components/courses/create/(sections)/GeneralSection' import QuestionsSection from '@/src/components/courses/create/(sections)/QuestionsSection' import SettingsSection from '@/src/components/courses/create/(sections)/SettingsSection' @@ -15,7 +15,7 @@ export async function OverviewSection() {
- +
From d5ff94968cfec77e1d2fea2dc8cc71df53d7e4a3 Mon Sep 17 00:00:00 2001 From: Marty Byrde <45905689+Marty-Byrde@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:31:14 +0100 Subject: [PATCH 08/25] ref: added course content count to `CourseCard` This way the amount of contents associated to a given course are displayed when discovering new or checks a user contributes to. --- src/components/courses/CourseCard.tsx | 4 ++-- src/i18n/locales/de.json | 1 + src/i18n/locales/de.ts | 1 + src/i18n/locales/en.json | 1 + src/i18n/locales/en.ts | 1 + 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/courses/CourseCard.tsx b/src/components/courses/CourseCard.tsx index 31258974..bcb202a1 100644 --- a/src/components/courses/CourseCard.tsx +++ b/src/components/courses/CourseCard.tsx @@ -44,7 +44,7 @@ export function CourseCard(course: Course) { {course.description}
- + } /> - q.points).reduce((prev, current) => (prev += current), 0)} /> +