diff --git a/apps/api/src/chapter/chapter.controller.ts b/apps/api/src/chapter/chapter.controller.ts index 9c470c31..3a5f04d0 100644 --- a/apps/api/src/chapter/chapter.controller.ts +++ b/apps/api/src/chapter/chapter.controller.ts @@ -13,12 +13,12 @@ import { ChapterService } from "./chapter.service"; import { CreateChapterBody, createChapterSchema, + showChapterSchema, UpdateChapterBody, updateChapterSchema, - showChapterSchema, } from "./schemas/chapter.schema"; -import type { ShowChapterResponse } from "./schemas/chapter.schema"; +import type { ChapterResponse } from "./schemas/chapter.schema"; @Controller("chapter") @UseGuards(RolesGuard) @@ -38,7 +38,7 @@ export class ChapterController { @Query("id") id: UUIDType, @CurrentUser("role") userRole: UserRole, @CurrentUser("userId") userId: UUIDType, - ): Promise> { + ): Promise> { return new BaseResponse( await this.chapterService.getChapterWithLessons(id, userId, userRole === USER_ROLES.ADMIN), ); diff --git a/apps/api/src/chapter/chapter.service.ts b/apps/api/src/chapter/chapter.service.ts index bc634f7e..1cba9b3b 100644 --- a/apps/api/src/chapter/chapter.service.ts +++ b/apps/api/src/chapter/chapter.service.ts @@ -6,7 +6,7 @@ import { LessonRepository } from "src/lesson/repositories/lesson.repository"; import { ChapterRepository } from "./repositories/chapter.repository"; -import type { ShowChapterResponse } from "src/chapter/schemas/chapter.schema"; +import type { ChapterResponse } from "src/chapter/schemas/chapter.schema"; import type { UUIDType } from "src/common"; @Injectable() @@ -22,7 +22,7 @@ export class ChapterService { id: UUIDType, userId: UUIDType, isAdmin?: boolean, - ): Promise { + ): Promise { const [courseAccess] = await this.chapterRepository.checkChapterAssignment(id, userId); const chapter = await this.chapterRepository.getChapterForUser(id, userId); diff --git a/apps/api/src/chapter/schemas/chapter.schema.ts b/apps/api/src/chapter/schemas/chapter.schema.ts index 9173d08a..b0d00848 100644 --- a/apps/api/src/chapter/schemas/chapter.schema.ts +++ b/apps/api/src/chapter/schemas/chapter.schema.ts @@ -1,13 +1,14 @@ import { type Static, Type } from "@sinclair/typebox"; import { UUIDSchema } from "src/common"; -import { lessonSchema } from "src/lesson/lesson.schema"; +import { lessonForChapterSchema, lessonSchema } from "src/lesson/lesson.schema"; import { PROGRESS_STATUSES } from "src/utils/types/progress.type"; export const chapterSchema = Type.Object({ id: UUIDSchema, title: Type.String(), lessonCount: Type.Number(), + lessons: Type.Optional(Type.Array(lessonSchema)), completedLessonCount: Type.Optional(Type.Number()), chapterProgress: Type.Optional( Type.Union([ @@ -40,23 +41,15 @@ export const chapter = Type.Object({ isFreemium: Type.Boolean(), }); -export const chapterWithLessonCount = Type.Intersect([ - Type.Omit(chapter, ["type"]), - Type.Object({ - lessonCount: Type.Number(), - }), -]); - export const allChapterSchema = Type.Array(chapterSchema); export const showChapterSchema = Type.Object({ ...chapterSchema.properties, quizCount: Type.Optional(Type.Number()), - lessons: Type.Array(lessonSchema), + lessons: lessonForChapterSchema, }); export type Chapter = Static; -export type ChapterWithLessonCount = Static; export type ChapterResponse = Static; export type ShowChapterResponse = Static; export type AllChaptersResponse = Static; diff --git a/apps/api/src/courses/course.controller.ts b/apps/api/src/courses/course.controller.ts index 10240823..0e3e2a39 100644 --- a/apps/api/src/courses/course.controller.ts +++ b/apps/api/src/courses/course.controller.ts @@ -31,7 +31,10 @@ import { CourseService } from "src/courses/course.service"; import { allCoursesSchema } from "src/courses/schemas/course.schema"; import { SortCourseFieldsOptions } from "src/courses/schemas/courseQuery"; import { CreateCourseBody, createCourseSchema } from "src/courses/schemas/createCourse.schema"; -import { commonShowCourseSchema } from "src/courses/schemas/showCourseCommon.schema"; +import { + commonShowBetaCourseSchema, + commonShowCourseSchema, +} from "src/courses/schemas/showCourseCommon.schema"; import { UpdateCourseBody, updateCourseSchema } from "src/courses/schemas/updateCourse.schema"; import { allCoursesValidation, @@ -45,7 +48,10 @@ import type { AllCoursesResponse, } from "src/courses/schemas/course.schema"; import type { CoursesFilterSchema } from "src/courses/schemas/courseQuery"; -import type { CommonShowCourse } from "src/courses/schemas/showCourseCommon.schema"; +import type { + CommonShowBetaCourse, + CommonShowCourse, +} from "src/courses/schemas/showCourseCommon.schema"; @Controller("course") @UseGuards(RolesGuard) @@ -193,23 +199,13 @@ export class CourseController { return new BaseResponse(await this.courseService.getCourse(id, currentUserId)); } - @Get("course-by-id") - @Roles(USER_ROLES.TEACHER, USER_ROLES.ADMIN) - @Validate({ - request: [{ type: "query", name: "id", schema: UUIDSchema, required: true }], - response: baseResponse(commonShowCourseSchema), - }) - async getCourseById(@Query("id") id: UUIDType): Promise> { - return new BaseResponse(await this.courseService.getCourseById(id)); - } - @Get("beta-course-by-id") @Roles(USER_ROLES.TEACHER, USER_ROLES.ADMIN) @Validate({ request: [{ type: "query", name: "id", schema: UUIDSchema, required: true }], - response: baseResponse(commonShowCourseSchema), + response: baseResponse(commonShowBetaCourseSchema), }) - async getBetaCourseById(@Query("id") id: UUIDType): Promise> { + async getBetaCourseById(@Query("id") id: UUIDType): Promise> { return new BaseResponse(await this.courseService.getBetaCourseById(id)); } diff --git a/apps/api/src/courses/course.service.ts b/apps/api/src/courses/course.service.ts index 6ecd1182..6173a1a3 100644 --- a/apps/api/src/courses/course.service.ts +++ b/apps/api/src/courses/course.service.ts @@ -57,7 +57,10 @@ import type { CommonShowCourse } from "./schemas/showCourseCommon.schema"; import type { UpdateCourseBody } from "./schemas/updateCourse.schema"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import type { Pagination, UUIDType } from "src/common"; -import type { AdminLessonWithContentSchema } from "src/lesson/lesson.schema"; +import type { + AdminLessonWithContentSchema, + LessonForChapterSchema, +} from "src/lesson/lesson.schema"; import type * as schema from "src/storage/schema"; import type { ProgressStatus } from "src/utils/types/progress.type"; @@ -378,7 +381,7 @@ export class CourseService { `, isFreemium: chapters.isFreemium, displayOrder: sql`${chapters.displayOrder}`, - lessons: sql` + lessons: sql` COALESCE( ( SELECT json_agg(lesson_data) @@ -466,15 +469,15 @@ export class CourseService { displayOrder: sql`${chapters.displayOrder}`, lessonCount: chapters.lessonCount, isFree: chapters.isFreemium, - lessons: sql` + lessons: sql` COALESCE( - ( - SELECT array_agg(${lessons.id} ORDER BY ${lessons.displayOrder}) - FROM ${lessons} - WHERE ${lessons.chapterId} = ${chapters.id} - ), - '{}' -) + ( + SELECT array_agg(${lessons.id} ORDER BY ${lessons.displayOrder}) + FROM ${lessons} + WHERE ${lessons.chapterId} = ${chapters.id} + ), + '{}' + ) `, }) .from(chapters) diff --git a/apps/api/src/courses/schemas/showCourseCommon.schema.ts b/apps/api/src/courses/schemas/showCourseCommon.schema.ts index c27c8fba..87fc9af7 100644 --- a/apps/api/src/courses/schemas/showCourseCommon.schema.ts +++ b/apps/api/src/courses/schemas/showCourseCommon.schema.ts @@ -1,9 +1,29 @@ import { type Static, Type } from "@sinclair/typebox"; -import { chapterSchema } from "src/chapter/schemas/chapter.schema"; +import { chapterSchema, showChapterSchema } from "src/chapter/schemas/chapter.schema"; import { UUIDSchema } from "src/common"; export const commonShowCourseSchema = Type.Object({ + archived: Type.Optional(Type.Boolean()), + authorId: Type.Optional(UUIDSchema), + category: Type.String(), + categoryId: Type.Optional(Type.String({ format: "uuid" })), + chapters: Type.Array(showChapterSchema), + completedChapterCount: Type.Optional(Type.Number()), + courseChapterCount: Type.Number(), + currency: Type.String(), + description: Type.String(), + enrolled: Type.Optional(Type.Boolean()), + hasFreeChapter: Type.Optional(Type.Boolean()), + id: Type.String({ format: "uuid" }), + isPublished: Type.Union([Type.Boolean(), Type.Null()]), + isScorm: Type.Optional(Type.Boolean()), + priceInCents: Type.Number(), + thumbnailUrl: Type.Optional(Type.String()), + title: Type.String(), +}); + +export const commonShowBetaCourseSchema = Type.Object({ archived: Type.Optional(Type.Boolean()), authorId: Type.Optional(UUIDSchema), category: Type.String(), @@ -26,3 +46,4 @@ export const commonShowCourseSchema = Type.Object({ }); export type CommonShowCourse = Static; +export type CommonShowBetaCourse = Static; diff --git a/apps/api/src/lesson/lesson.schema.ts b/apps/api/src/lesson/lesson.schema.ts index 00f74cd9..b19f474d 100644 --- a/apps/api/src/lesson/lesson.schema.ts +++ b/apps/api/src/lesson/lesson.schema.ts @@ -1,6 +1,7 @@ import { Type } from "@sinclair/typebox"; import { UUIDSchema } from "src/common"; +import { PROGRESS_STATUSES } from "src/utils/types/progress.type"; import { LESSON_TYPES, PhotoQuestionType, QuestionType } from "./lesson.type"; @@ -18,11 +19,11 @@ export const optionSchema = Type.Object({ export const questionSchema = Type.Object({ id: Type.Optional(UUIDSchema), type: Type.Enum(QuestionType), - description: Type.Optional(Type.String()), + description: Type.Optional(Type.Union([Type.String(), Type.Null()])), title: Type.String(), displayOrder: Type.Optional(Type.Number()), - photoQuestionType: Type.Optional(Type.Enum(PhotoQuestionType)), - photoS3Key: Type.Optional(Type.String()), + photoQuestionType: Type.Optional(Type.Union([Type.Enum(PhotoQuestionType), Type.Null()])), + photoS3Key: Type.Optional(Type.Union([Type.String(), Type.Null()])), options: Type.Optional(Type.Array(optionSchema)), }); @@ -32,8 +33,8 @@ export const lessonSchema = Type.Object({ type: Type.String(), description: Type.String(), displayOrder: Type.Number(), - fileS3Key: Type.Optional(Type.String()), - fileType: Type.Optional(Type.String()), + fileS3Key: Type.Optional(Type.Union([Type.String(), Type.Null()])), + fileType: Type.Optional(Type.Union([Type.String(), Type.Null()])), questions: Type.Optional(Type.Array(questionSchema)), updatedAt: Type.Optional(Type.String()), }); @@ -97,8 +98,28 @@ export const lessonShowSchema = Type.Object({ export const updateLessonSchema = Type.Partial(createLessonSchema); export const updateQuizLessonSchema = Type.Partial(createQuizLessonSchema); +export const lessonForChapterSchema = Type.Array( + Type.Object({ + id: UUIDSchema, + title: Type.String(), + type: Type.Union([ + Type.Literal(LESSON_TYPES.QUIZ), + Type.Literal(LESSON_TYPES.PRESENTATION), + Type.Literal(LESSON_TYPES.VIDEO), + Type.Literal(LESSON_TYPES.TEXT), + ]), + displayOrder: Type.Number(), + status: Type.Union([ + Type.Literal(PROGRESS_STATUSES.COMPLETED), + Type.Literal(PROGRESS_STATUSES.IN_PROGRESS), + Type.Literal(PROGRESS_STATUSES.NOT_STARTED), + ]), + quizQuestionCount: Type.Union([Type.Number(), Type.Null()]), + }), +); export type AdminLessonWithContentSchema = Static; +export type LessonForChapterSchema = Static; export type CreateLessonBody = Static; export type UpdateLessonBody = Static; export type UpdateQuizLessonBody = Static; @@ -108,3 +129,4 @@ export type OptionBody = Static; export type QuestionBody = Static; export type QuestionSchema = Static; export type LessonShow = Static; +export type LessonSchema = Static; diff --git a/apps/api/src/lesson/repositories/lesson.repository.ts b/apps/api/src/lesson/repositories/lesson.repository.ts index db936594..3208c0d1 100644 --- a/apps/api/src/lesson/repositories/lesson.repository.ts +++ b/apps/api/src/lesson/repositories/lesson.repository.ts @@ -54,6 +54,39 @@ export class LessonRepository { .orderBy(lessons.displayOrder); } + async getLessonsForChapter(chapterId: UUIDType) { + return this.db + .select({ + title: lessons.title, + type: lessons.type, + displayOrder: sql`${lessons.displayOrder}`, + questions: sql` + COALESCE( + ( + SELECT json_agg(questions_data) + FROM ( + SELECT + ${questions.id} AS id, + ${questions.title} AS title, + ${questions.description} AS description, + ${questions.type} AS type, + ${questions.photoQuestionType} AS photoQuestionType, + ${questions.photoS3Key} AS photoS3Key, + ${questions.solutionExplanation} AS solutionExplanation, + -- TODO: add display order + FROM ${questions} + WHERE ${lessons.id} = ${questions.lessonId} + ) AS questions_data + ), + '[]'::json + ) + `, + }) + .from(lessons) + .where(eq(lessons.chapterId, chapterId)) + .orderBy(lessons.displayOrder); + } + // async completeQuiz( // courseId: UUIDType, // lessonId: UUIDType, diff --git a/apps/api/src/lesson/services/lesson.service.ts b/apps/api/src/lesson/services/lesson.service.ts index 42d45449..2656fdf3 100644 --- a/apps/api/src/lesson/services/lesson.service.ts +++ b/apps/api/src/lesson/services/lesson.service.ts @@ -3,11 +3,7 @@ import { eq, sql } from "drizzle-orm"; import { DatabasePg } from "src/common"; import { FileService } from "src/file/file.service"; -import { - lessons, - questionAnswerOptions, - questions, -} from "src/storage/schema"; +import { lessons, questionAnswerOptions, questions } from "src/storage/schema"; import { LESSON_TYPES } from "../lesson.type"; diff --git a/apps/api/src/seed/e2e-data-seeds.ts b/apps/api/src/seed/e2e-data-seeds.ts index 78020077..8398a90a 100644 --- a/apps/api/src/seed/e2e-data-seeds.ts +++ b/apps/api/src/seed/e2e-data-seeds.ts @@ -18,6 +18,7 @@ export const e2eCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "E2E Testing Text Block", description: "E2E Testing Text Block Body", @@ -32,12 +33,14 @@ export const e2eCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "E2E Testing Quiz", description: "E2E Testing Quiz Description", displayOrder: 1, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "E2E Testing Question", description: "E2E Testing Question", @@ -50,6 +53,7 @@ export const e2eCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "E2E Testing Question 2", description: "E2E Testing Question 2", @@ -67,6 +71,7 @@ export const e2eCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.MultipleChoice, title: "E2E Testing Question 3", description: "E2E Testing Question 3", diff --git a/apps/api/src/seed/nice-data-seeds.ts b/apps/api/src/seed/nice-data-seeds.ts index 0c5f3c1a..70dcfd26 100644 --- a/apps/api/src/seed/nice-data-seeds.ts +++ b/apps/api/src/seed/nice-data-seeds.ts @@ -21,6 +21,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Introduction to HTML", description: @@ -28,12 +29,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "HTML Quiz: Importance of HTML", description: "Why is HTML considered the backbone of any website?", displayOrder: 2, questions: [ { + id: crypto.randomUUID(), type: QuestionType.BriefResponse, title: "Why is HTML considered the backbone of any website?", description: "Explain its role in web development.", @@ -41,6 +44,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "HTML Elements Video", description: @@ -48,6 +52,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "CSS and Layout Quiz", description: @@ -55,6 +60,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksText, title: "In CSS, [word] is used to style the layout, while [word] is used to change colors.", @@ -86,6 +92,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.PRESENTATION, title: "HTML Hyperlinks Presentation", description: @@ -93,12 +100,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 5, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "HTML Tag Quiz", description: "Which HTML tag is used to create a hyperlink?", displayOrder: 6, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which HTML tag is used to create a hyperlink?", options: [ @@ -135,6 +144,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "HTML Basics: Test Your Knowledge", description: @@ -142,6 +152,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which of the following HTML tags is used to create an image?", // displayOrder: 1, @@ -169,6 +180,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.MultipleChoice, title: "Which of the following are valid HTML elements for structuring content? (Select all that apply)", @@ -207,6 +219,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which HTML tag is used to create a hyperlink?", // displayOrder: 3, @@ -234,6 +247,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.MultipleChoice, title: "Which of the following attributes are commonly used with the tag? (Select all that apply)", @@ -267,6 +281,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "CSS is used to style [word], while JavaScript is used to add [word] to web pages.", @@ -312,6 +327,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksText, // displayOrder: 6, title: @@ -332,6 +348,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, // displayOrder: 7, title: @@ -362,6 +379,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "CSS Fundamentals: Put Your Skills to the Test", description: @@ -369,6 +387,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which CSS property is used to change the text color of an element?", options: [ @@ -395,6 +414,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.MultipleChoice, title: "Which of the following are valid CSS selectors? (Select all that apply)", // displayOrder: 2, @@ -427,6 +447,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "The CSS [word] property is used for creating flexible box layouts, while [word] is used for creating grid layouts.", @@ -457,6 +478,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, // displayOrder: 4, title: "To center an element horizontally, you can use 'margin: [word] [word];'.", @@ -486,6 +508,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Introduction to HTML", description: @@ -493,12 +516,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "HTML Quiz: Importance of HTML", description: "Why is HTML considered the backbone of any website?", displayOrder: 2, questions: [ { + id: crypto.randomUUID(), type: QuestionType.BriefResponse, title: "Why is HTML considered the backbone of any website? Explain its role in web development.", @@ -506,6 +531,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "HTML Elements Video", description: @@ -513,6 +539,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "CSS and Layout Quiz", description: @@ -520,6 +547,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "In CSS, [word] is used to style the layout, while [word] is used to change colors.", @@ -571,6 +599,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Introduction to Java for Android", description: @@ -578,12 +607,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Explain why Java is the preferred language for Android development", description: "", displayOrder: 2, questions: [ { + id: crypto.randomUUID(), type: QuestionType.BriefResponse, title: "Explain why Java is the preferred language for Android development.", description: "", @@ -591,12 +622,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Java Basics Video Tutorial", description: "Learn Java basics for Android development.", displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "In Java, [word] are used to define the blueprint of objects, while [word] are instances.", @@ -604,6 +637,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "In Java, [word] are used to define the blueprint of objects, while [word] are instances.", @@ -625,6 +659,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "In Android dev, [word] are used to define the user interface, while [word] handle user interactions", @@ -632,6 +667,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "In Android dev, [word] are used to define the user interface, while [word] handle user interactions.", @@ -653,18 +689,21 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.PRESENTATION, title: "Java OOP Concepts Presentation", description: "Explore Object-Oriented Programming principles in Java.", displayOrder: 6, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which keyword is used to create a new instance of a class in Java?", description: "", displayOrder: 7, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which keyword is used to create a new instance of a class in Java?", options: [ @@ -701,12 +740,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which of the following is the entry point of an Android application?", description: "", displayOrder: 1, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which of the following is the entry point of an Android application?", options: [ @@ -754,6 +795,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Introduction to Kotlin for Android", description: @@ -761,6 +803,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Kotlin Basics Video Tutorial", description: @@ -768,12 +811,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which keyword is used to declare a variable in Kotlin?", description: "", displayOrder: 3, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which keyword is used to declare a variable in Kotlin?", options: [ @@ -794,6 +839,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Setting Up Your Android Studio Environment", description: @@ -801,18 +847,21 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.PRESENTATION, title: "Creating a Simple Kotlin App", description: "A step-by-step guide to building your first Android app using Kotlin.", displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "In Kotlin, [word] are immutable variables, while [word] are mutable variables.", description: "", displayOrder: 3, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "In Kotlin, [word] are immutable variables, while [word] are mutable variables.", @@ -845,6 +894,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Introduction to Arithmetic", description: @@ -852,12 +902,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Why is arithmetic considered the foundation of mathematics? ", description: "", displayOrder: 2, questions: [ { + id: crypto.randomUUID(), type: QuestionType.BriefResponse, title: "Why is arithmetic fundamental in math? Give a real-life example of its use.", @@ -865,6 +917,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Basic Arithmetic Video Tutorial", description: @@ -872,6 +925,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "In arithmetic, [word] is the result of addition, while [word] is the result of subtraction.", @@ -879,6 +933,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "In arithmetic, [word] is the result of addition, while [word] is the result of subtraction.", @@ -900,6 +955,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Understanding Geometry", description: @@ -907,6 +963,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.PRESENTATION, title: "Geometric Shapes Presentation", description: @@ -914,12 +971,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which formula is used to calculate the area of a rectangle?", description: "", displayOrder: 3, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which formula is used to calculate the area of a rectangle?", options: [ @@ -940,6 +999,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Getting Started with Algebra", description: @@ -947,6 +1007,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "In algebra, [word] represent unknown values, while [word] are mathematical phrases", @@ -954,6 +1015,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "In algebra, [word] represent unknown values, while [word] are mathematical phrases", @@ -967,6 +1029,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Basic Algebra Video Guide", description: @@ -982,6 +1045,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Mathematics Basics Quiz: Test Your Knowledge", description: @@ -989,6 +1053,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which of the following is an example of a geometric shape?", options: [ @@ -999,6 +1064,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.MultipleChoice, title: "Which operations are included in basic arithmetic? (Select all that apply)", options: [ @@ -1010,6 +1076,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "In algebra, [word] are used to represent unknowns, while [word] can be solved to find their values.", @@ -1042,6 +1109,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Introduction to English Grammar", description: @@ -1049,6 +1117,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Sentence Structure Basics", description: @@ -1056,12 +1125,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Explain the difference between a noun and a verb in a sentence.", description: "Explain the difference between a noun and a verb in a sentence.", displayOrder: 3, questions: [ { + id: crypto.randomUUID(), type: QuestionType.BriefResponse, title: "Explain the difference between a noun and a verb in a sentence.", description: "Explain its role in sentence construction.", @@ -1069,6 +1140,7 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Grammar Rules Video Tutorial", description: @@ -1076,12 +1148,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Fill in the blanks: 'She [word] to the store yesterday.'", description: "Fill in the blanks with the correct verb.", displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "Fill in the blanks: 'She [word] to the store yesterday.'", description: "

She went to the store yesterday.

", @@ -1101,6 +1175,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Common English Words and Phrases", description: @@ -1108,6 +1183,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Synonyms and Antonyms", description: @@ -1115,18 +1191,21 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.PRESENTATION, title: "English Vocabulary Expansion Presentation", description: "A comprehensive slide presentation on expanding your vocabulary.", displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which word is the synonym of 'happy'?", description: "Choose the correct synonym for 'happy'.", displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which word is the synonym of 'happy'?", options: [ @@ -1138,12 +1217,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "I [word] to the park every day.", description: "Fill in the blank with the correct verb.", displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "I [word] to the park every day.", options: [ @@ -1162,6 +1243,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Essential Pronunciation Tips", description: @@ -1169,6 +1251,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Common Pronunciation Mistakes", description: @@ -1176,6 +1259,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which of the following sounds is most commonly mispronounced by non-native English speakers?", @@ -1183,6 +1267,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which of the following sounds is most commonly mispronounced by non-native English speakers?", @@ -1195,18 +1280,21 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Pronunciation and Accent Video Tutorial", description: "A step-by-step video guide on mastering English pronunciation.", displayOrder: 4, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "I love [word] (swimming/swim).", description: "Choose the correct verb form.", displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "I love [word] (swimming/swim).", description: "I love swimming (swimming/swim).", @@ -1223,6 +1311,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which part of speech is the word 'quickly' in the sentence 'She ran quickly to the store'?", @@ -1230,6 +1319,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which part of speech is the word 'quickly' in the sentence 'She ran quickly to the store'?", @@ -1242,12 +1332,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "She [word] to the park every day.", description: "Fill in the blank with the correct verb.", displayOrder: 2, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "She [word] to the park every day.", description: "She went to the park every day.", @@ -1259,12 +1351,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "What is the plural form of 'child'?", description: "Choose the correct plural form of 'child'.", displayOrder: 3, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "What is the plural form of 'child'?", options: [ @@ -1275,12 +1369,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which of these words is a conjunction?", description: "Choose the correct conjunction.", displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which of these words is a conjunction?", options: [ @@ -1310,6 +1406,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Complex Sentences and Their Use", description: @@ -1317,6 +1414,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Relative Clauses and Modifiers", description: @@ -1324,12 +1422,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "What is the difference between a relative clause and a noun clause?", description: "Explain the difference between relative and noun clauses.", displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Advanced Grammar Video Tutorial", description: @@ -1337,12 +1437,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Fill in the blanks: The book [word] I borrowed yesterday was fascinating.", description: "Fill in the blank with the correct word.", displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "The book [word] I borrowed yesterday was fascinating.", description: "The book that I borrowed yesterday was fascinating.", @@ -1361,6 +1463,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Academic Vocabulary and Its Application", description: @@ -1368,6 +1471,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Using Formal Language in Communication", description: @@ -1375,6 +1479,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.PRESENTATION, title: "Academic Vocabulary List", description: @@ -1382,12 +1487,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Which word is an example of academic vocabulary?", description: "Select the correct academic word.", displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "Which word is an example of academic vocabulary?", options: [ @@ -1399,12 +1506,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "The results [word] the hypothesis.", description: "Fill in the blank with the correct word.", displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "The results [word] the hypothesis.", description: "The results support the hypothesis.", @@ -1420,6 +1529,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Understanding Idioms in Context", description: @@ -1427,6 +1537,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Common Idioms and Their Meanings", description: @@ -1434,12 +1545,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "What does the idiom 'break the ice' mean?", description: "Explain the meaning of the idiom 'break the ice'.", displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "Idiomatic Expressions Video Tutorial", description: @@ -1447,12 +1560,14 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "She was [word] when she heard the good news.", description: "Fill in the blank with the correct idiom.", displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "She was [word] when she heard the good news.", description: "She was over the moon when she heard the good news.", @@ -1480,6 +1595,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, lessons: [ { + id: crypto.randomUUID(), type: LESSON_TYPES.VIDEO, title: "What is Artificial Intelligence? An Introductory Overview", description: @@ -1487,13 +1603,15 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 1, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.TEXT, title: "Key Concepts and Terminologies in AI", description: - "

Artificial Intelligence (AI) refers to the simulation of human intelligence in machines programmed to think, learn, and make decisions. Below are some key concepts and terminologies essential to understanding AI:

  • Machine Learning (ML): A subset of AI focused on creating algorithms that allow computers to learn from and make predictions based on data. Example: A recommendation system suggesting movies based on your viewing history.
  • Neural Networks: Inspired by the human brain, these are algorithms designed to recognize patterns and process data in layers, enabling tasks like image and speech recognition.
  • Natural Language Processing (NLP): This involves teaching machines to understand, interpret, and generate human language.", + "

    Artificial Intelligence (AI) refers to the simulation of human intelligence in machines programmed to think, learn, and make decisions. Below are some key concepts and terminologies essential to understanding AI:

    • Machine Learning (ML): A subset of AI focused on creating algorithms that allow computers to learn from and make predictions based on data. Example: A recommendation system suggesting movies based on your viewing history.
    • Neural Networks: Inspired by the human brain, these are algorithms designed to recognize patterns and process data in layers, enabling tasks like image and speech recognition.
    • Natural Language Processing (NLP): This involves teaching machines to understand, interpret, and generate human language.", displayOrder: 2, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.PRESENTATION, title: "AI Applications Across Industries", description: @@ -1501,6 +1619,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 3, }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "AI Quiz: Primary Goal of AI in Business", description: @@ -1508,6 +1627,7 @@ export const niceCourses: NiceCourseData[] = [ displayOrder: 4, questions: [ { + id: crypto.randomUUID(), type: QuestionType.SingleChoice, title: "What is the primary goal of AI in business?", options: [ @@ -1520,12 +1640,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "AI Quiz: Applications of AI", description: "Identify common AI applications in various business domains.", displayOrder: 5, questions: [ { + id: crypto.randomUUID(), type: QuestionType.MultipleChoice, title: "Which of the following are applications of AI in business? (Select all that apply)", @@ -1539,12 +1661,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "AI Quiz: Can AI Function Without Data?", description: "Test your understanding of AI's reliance on data.", displayOrder: 6, questions: [ { + id: crypto.randomUUID(), type: QuestionType.TrueOrFalse, title: "AI can function without any data input from humans.", options: [ @@ -1555,12 +1679,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Photo Identification: AI Solutions", description: "Identify the AI-driven solution from the provided images.", displayOrder: 7, questions: [ { + id: crypto.randomUUID(), type: QuestionType.PhotoQuestion, title: "Which image represents an AI-driven chatbot?", options: [ @@ -1572,12 +1698,14 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "AI Fill in the Blanks", description: "Complete the sentences with the correct AI-related terms.", displayOrder: 8, questions: [ { + id: crypto.randomUUID(), type: QuestionType.FillInTheBlanksDnd, title: "Complete the blanks: Artificial [word] refers to the ability of machines to mimic [word] intelligence.", @@ -1592,24 +1720,28 @@ export const niceCourses: NiceCourseData[] = [ ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Brief Response: Why Businesses Adopt AI", description: "Explain in one sentence why businesses adopt AI.", displayOrder: 9, questions: [ { + id: crypto.randomUUID(), type: QuestionType.BriefResponse, title: "In one sentence, explain why businesses are adopting AI.", }, ], }, { + id: crypto.randomUUID(), type: LESSON_TYPES.QUIZ, title: "Detailed Response: AI's Role in an Industry", description: "Describe how AI can improve decision-making in a specific industry.", displayOrder: 10, questions: [ { + id: crypto.randomUUID(), type: QuestionType.DetailedResponse, title: "Describe how AI can improve decision-making in a specific industry of your choice.", diff --git a/apps/api/src/storage/schema/index.ts b/apps/api/src/storage/schema/index.ts index 3d1beb2e..bc991d2a 100644 --- a/apps/api/src/storage/schema/index.ts +++ b/apps/api/src/storage/schema/index.ts @@ -157,7 +157,7 @@ export const lessons = pgTable("lessons", { .notNull(), type: varchar("type", { length: 20 }).notNull(), title: varchar("title", { length: 100 }).notNull(), - description: varchar("description", { length: 1000 }), + description: varchar("description", { length: 3000 }), displayOrder: integer("display_order"), fileS3Key: varchar("file_s3_key", { length: 200 }), fileType: varchar("file_type", { length: 20 }), diff --git a/apps/api/src/studentLessonProgress/studentLessonProgress.controller.ts b/apps/api/src/studentLessonProgress/studentLessonProgress.controller.ts index bc784acb..4467ba0d 100644 --- a/apps/api/src/studentLessonProgress/studentLessonProgress.controller.ts +++ b/apps/api/src/studentLessonProgress/studentLessonProgress.controller.ts @@ -2,7 +2,7 @@ import { Controller, Post, Query, UseGuards } from "@nestjs/common"; import { Type } from "@sinclair/typebox"; import { Validate } from "nestjs-typebox"; -import { UUIDSchema, baseResponse, BaseResponse, UUIDType } from "src/common"; +import { baseResponse, BaseResponse, UUIDSchema, UUIDType } from "src/common"; import { Roles } from "src/common/decorators/roles.decorator"; import { CurrentUser } from "src/common/decorators/user.decorator"; import { RolesGuard } from "src/common/guards/roles.guard"; @@ -18,15 +18,7 @@ export class StudentLessonProgressController { @Post() @Roles(USER_ROLES.STUDENT) @Validate({ - request: [ - { type: "query", name: "id", schema: UUIDSchema, required: true }, - { - type: "query", - name: "lessonId", - schema: UUIDSchema, - required: true, - }, - ], + request: [{ type: "query", name: "id", schema: UUIDSchema, required: true }], response: baseResponse(Type.Object({ message: Type.String() })), }) async markLessonAsCompleted( diff --git a/apps/api/src/swagger/api-schema.json b/apps/api/src/swagger/api-schema.json index d7183918..a1a5aee5 100644 --- a/apps/api/src/swagger/api-schema.json +++ b/apps/api/src/swagger/api-schema.json @@ -1380,33 +1380,6 @@ } } }, - "/api/course/course-by-id": { - "get": { - "operationId": "CourseController_getCourseById", - "parameters": [ - { - "name": "id", - "required": true, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetCourseByIdResponse" - } - } - } - } - } - } - }, "/api/course/beta-course-by-id": { "get": { "operationId": "CourseController_getBetaCourseById", @@ -1980,15 +1953,6 @@ "post": { "operationId": "StudentLessonProgressController_markLessonAsCompleted", "parameters": [ - { - "name": "lessonId", - "required": true, - "in": "query", - "schema": { - "format": "uuid", - "type": "string" - } - }, { "name": "id", "required": true, @@ -3647,6 +3611,78 @@ "lessonCount": { "type": "number" }, + "lessons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "anyOf": [ + { + "const": "quiz", + "type": "string" + }, + { + "const": "presentation", + "type": "string" + }, + { + "const": "video", + "type": "string" + }, + { + "const": "text", + "type": "string" + } + ] + }, + "displayOrder": { + "type": "number" + }, + "status": { + "anyOf": [ + { + "const": "completed", + "type": "string" + }, + { + "const": "in_progress", + "type": "string" + }, + { + "const": "not_started", + "type": "string" + } + ] + }, + "quizQuestionCount": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "title", + "type", + "displayOrder", + "status", + "quizQuestionCount" + ] + } + }, "completedLessonCount": { "type": "number" }, @@ -3692,6 +3728,7 @@ "id", "title", "lessonCount", + "lessons", "displayOrder" ] } @@ -3737,12 +3774,6 @@ "thumbnailUrl": { "type": "string" }, - "thumbnailS3Key": { - "type": "string" - }, - "thumbnailS3SingedUrl": { - "type": "string" - }, "title": { "type": "string" } @@ -3764,7 +3795,7 @@ "data" ] }, - "GetCourseByIdResponse": { + "GetBetaCourseByIdResponse": { "type": "object", "properties": { "data": { @@ -3799,6 +3830,197 @@ "lessonCount": { "type": "number" }, + "lessons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "displayOrder": { + "type": "number" + }, + "fileS3Key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "fileType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "questions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "type": { + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + }, + { + "const": "true_or_false", + "type": "string" + }, + { + "const": "photo_question", + "type": "string" + }, + { + "const": "fill_in_the_blanks_text", + "type": "string" + }, + { + "const": "fill_in_the_blanks_dnd", + "type": "string" + }, + { + "const": "brief_response", + "type": "string" + }, + { + "const": "detailed_response", + "type": "string" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" + }, + "displayOrder": { + "type": "number" + }, + "photoQuestionType": { + "anyOf": [ + { + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + } + ] + }, + { + "type": "null" + } + ] + }, + "photoS3Key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "optionText": { + "type": "string" + }, + "displayOrder": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "isStudentAnswer": { + "type": "boolean" + }, + "isCorrect": { + "type": "boolean" + }, + "questionId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "optionText", + "displayOrder", + "isCorrect" + ] + } + } + }, + "required": [ + "type", + "title" + ] + } + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "type", + "description", + "displayOrder" + ] + } + }, "completedLessonCount": { "type": "number" }, @@ -3916,199 +4138,47 @@ "data" ] }, - "GetBetaCourseByIdResponse": { + "CreateCourseBody": { "type": "object", - "properties": { - "data": { + "allOf": [ + { "type": "object", "properties": { - "archived": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "isPublished": { "type": "boolean" }, - "authorId": { - "format": "uuid", + "thumbnailS3Key": { "type": "string" }, - "category": { + "priceInCents": { + "type": "integer" + }, + "currency": { "type": "string" }, "categoryId": { "format": "uuid", "type": "string" }, - "chapters": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "format": "uuid", - "type": "string" - }, - "title": { - "type": "string" - }, - "lessonCount": { - "type": "number" - }, - "completedLessonCount": { - "type": "number" - }, - "chapterProgress": { - "anyOf": [ - { - "const": "completed", - "type": "string" - }, - { - "const": "in_progress", - "type": "string" - }, - { - "const": "not_started", - "type": "string" - } - ] - }, - "isFreemium": { - "type": "boolean" - }, - "enrolled": { - "type": "boolean" - }, - "isPublished": { - "type": "boolean" - }, - "isSubmitted": { - "type": "boolean" - }, - "createdAt": { - "type": "string" - }, - "quizCount": { - "type": "number" - }, - "displayOrder": { - "type": "number" - } - }, - "required": [ - "id", - "title", - "lessonCount", - "displayOrder" - ] - } - }, - "completedChapterCount": { - "type": "number" - }, - "courseChapterCount": { - "type": "number" - }, - "currency": { - "type": "string" - }, - "description": { - "type": "string" - }, - "enrolled": { - "type": "boolean" - }, - "hasFreeChapter": { - "type": "boolean" - }, - "id": { - "format": "uuid", - "type": "string" - }, - "isPublished": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - }, - "isScorm": { - "type": "boolean" - }, - "priceInCents": { - "type": "number" - }, - "thumbnailUrl": { - "type": "string" - }, - "thumbnailS3Key": { - "type": "string" - }, - "thumbnailS3SingedUrl": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "required": [ - "category", - "chapters", - "courseChapterCount", - "currency", - "description", - "id", - "isPublished", - "priceInCents", - "title" - ] - } - }, - "required": [ - "data" - ] - }, - "CreateCourseBody": { - "type": "object", - "allOf": [ - { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "isPublished": { - "type": "boolean" - }, - "thumbnailS3Key": { - "type": "string" - }, - "priceInCents": { - "type": "integer" - }, - "currency": { - "type": "string" - }, - "categoryId": { - "format": "uuid", - "type": "string" - }, - "isScorm": { - "type": "boolean" - } - }, - "required": [ - "title", - "description", - "categoryId" - ] - }, - { - "type": "object", - "properties": { + "isScorm": { + "type": "boolean" + } + }, + "required": [ + "title", + "description", + "categoryId" + ] + }, + { + "type": "object", + "properties": { "chapters": { "type": "array", "items": { @@ -4381,10 +4451,24 @@ "type": "string" }, "fileS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "fileType": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "questions": { "type": "array", @@ -4432,7 +4516,14 @@ ] }, "description": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "title": { "type": "string" @@ -4443,17 +4534,31 @@ "photoQuestionType": { "anyOf": [ { - "const": "single_choice", - "type": "string" + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + } + ] }, { - "const": "multiple_choice", - "type": "string" + "type": "null" } ] }, "photoS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "options": { "type": "array", @@ -4620,7 +4725,14 @@ ] }, "description": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "title": { "type": "string" @@ -4631,17 +4743,31 @@ "photoQuestionType": { "anyOf": [ { - "const": "single_choice", - "type": "string" + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + } + ] }, { - "const": "multiple_choice", - "type": "string" + "type": "null" } ] }, "photoS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "options": { "type": "array", @@ -4804,7 +4930,14 @@ ] }, "description": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "title": { "type": "string" @@ -4815,17 +4948,31 @@ "photoQuestionType": { "anyOf": [ { - "const": "single_choice", - "type": "string" + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + } + ] }, { - "const": "multiple_choice", - "type": "string" + "type": "null" } ] }, "photoS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "options": { "type": "array", @@ -4925,16 +5072,30 @@ "type": "string" }, "fileS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "fileType": { - "type": "string" - }, - "questions": { - "type": "array", - "items": { - "type": "object", - "properties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "questions": { + "type": "array", + "items": { + "type": "object", + "properties": { "id": { "format": "uuid", "type": "string" @@ -4976,7 +5137,14 @@ ] }, "description": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "title": { "type": "string" @@ -4987,17 +5155,31 @@ "photoQuestionType": { "anyOf": [ { - "const": "single_choice", - "type": "string" + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + } + ] }, { - "const": "multiple_choice", - "type": "string" + "type": "null" } ] }, "photoS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "options": { "type": "array", @@ -5154,6 +5336,78 @@ "lessonCount": { "type": "number" }, + "lessons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "anyOf": [ + { + "const": "quiz", + "type": "string" + }, + { + "const": "presentation", + "type": "string" + }, + { + "const": "video", + "type": "string" + }, + { + "const": "text", + "type": "string" + } + ] + }, + "displayOrder": { + "type": "number" + }, + "status": { + "anyOf": [ + { + "const": "completed", + "type": "string" + }, + { + "const": "in_progress", + "type": "string" + }, + { + "const": "not_started", + "type": "string" + } + ] + }, + "quizQuestionCount": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "title", + "type", + "displayOrder", + "status", + "quizQuestionCount" + ] + } + }, "completedLessonCount": { "type": "number" }, @@ -5193,6 +5447,29 @@ }, "displayOrder": { "type": "number" + } + }, + "required": [ + "id", + "title", + "lessonCount", + "lessons", + "displayOrder" + ] + } + }, + "required": [ + "data" + ] + }, + "BetaCreateChapterBody": { + "type": "object", + "allOf": [ + { + "type": "object", + "properties": { + "title": { + "type": "string" }, "lessons": { "type": "array", @@ -5216,10 +5493,24 @@ "type": "number" }, "fileS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "fileType": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "questions": { "type": "array", @@ -5267,7 +5558,14 @@ ] }, "description": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "title": { "type": "string" @@ -5278,17 +5576,31 @@ "photoQuestionType": { "anyOf": [ { - "const": "single_choice", - "type": "string" + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + } + ] }, { - "const": "multiple_choice", - "type": "string" + "type": "null" } ] }, "photoS3Key": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "options": { "type": "array", @@ -5349,29 +5661,6 @@ "displayOrder" ] } - } - }, - "required": [ - "id", - "title", - "lessonCount", - "displayOrder", - "lessons" - ] - } - }, - "required": [ - "data" - ] - }, - "BetaCreateChapterBody": { - "type": "object", - "allOf": [ - { - "type": "object", - "properties": { - "title": { - "type": "string" }, "chapterProgress": { "anyOf": [ @@ -5459,6 +5748,197 @@ "title": { "type": "string" }, + "lessons": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "description": { + "type": "string" + }, + "displayOrder": { + "type": "number" + }, + "fileS3Key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "fileType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "questions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "type": { + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + }, + { + "const": "true_or_false", + "type": "string" + }, + { + "const": "photo_question", + "type": "string" + }, + { + "const": "fill_in_the_blanks_text", + "type": "string" + }, + { + "const": "fill_in_the_blanks_dnd", + "type": "string" + }, + { + "const": "brief_response", + "type": "string" + }, + { + "const": "detailed_response", + "type": "string" + } + ] + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": { + "type": "string" + }, + "displayOrder": { + "type": "number" + }, + "photoQuestionType": { + "anyOf": [ + { + "anyOf": [ + { + "const": "single_choice", + "type": "string" + }, + { + "const": "multiple_choice", + "type": "string" + } + ] + }, + { + "type": "null" + } + ] + }, + "photoS3Key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "format": "uuid", + "type": "string" + }, + "optionText": { + "type": "string" + }, + "displayOrder": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "isStudentAnswer": { + "type": "boolean" + }, + "isCorrect": { + "type": "boolean" + }, + "questionId": { + "format": "uuid", + "type": "string" + } + }, + "required": [ + "optionText", + "displayOrder", + "isCorrect" + ] + } + } + }, + "required": [ + "type", + "title" + ] + } + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "type", + "description", + "displayOrder" + ] + } + }, "chapterProgress": { "anyOf": [ { @@ -5761,4 +6241,4 @@ } } } -} +} \ No newline at end of file diff --git a/apps/api/src/utils/types/test-types.ts b/apps/api/src/utils/types/test-types.ts index e923fc35..449b1d84 100644 --- a/apps/api/src/utils/types/test-types.ts +++ b/apps/api/src/utils/types/test-types.ts @@ -24,7 +24,7 @@ const niceCourseData = Type.Intersect([ displayOrder: Type.Number(), lessons: Type.Array( Type.Intersect([ - Type.Omit(lessonSchema, ["id", "fileS3Key", "fileType"]), + Type.Omit(lessonSchema, ["id", "fileS3Key", "fileType", "questions"]), Type.Partial( Type.Object({ questions: Type.Array( diff --git a/apps/web/app/api/generated-api.ts b/apps/web/app/api/generated-api.ts index df548aba..cdd6dc65 100644 --- a/apps/web/app/api/generated-api.ts +++ b/apps/web/app/api/generated-api.ts @@ -445,6 +445,15 @@ export interface GetCourseResponse { id: string; title: string; lessonCount: number; + lessons: { + /** @format uuid */ + id: string; + title: string; + type: "quiz" | "presentation" | "video" | "text"; + displayOrder: number; + status: "completed" | "in_progress" | "not_started"; + quizQuestionCount: number | null; + }[]; completedLessonCount?: number; chapterProgress?: "completed" | "in_progress" | "not_started"; isFreemium?: boolean; @@ -467,49 +476,6 @@ export interface GetCourseResponse { isScorm?: boolean; priceInCents: number; thumbnailUrl?: string; - thumbnailS3Key?: string; - thumbnailS3SingedUrl?: string; - title: string; - }; -} - -export interface GetCourseByIdResponse { - data: { - archived?: boolean; - /** @format uuid */ - authorId?: string; - category: string; - /** @format uuid */ - categoryId?: string; - chapters: { - /** @format uuid */ - id: string; - title: string; - lessonCount: number; - completedLessonCount?: number; - chapterProgress?: "completed" | "in_progress" | "not_started"; - isFreemium?: boolean; - enrolled?: boolean; - isPublished?: boolean; - isSubmitted?: boolean; - createdAt?: string; - quizCount?: number; - displayOrder: number; - }[]; - completedChapterCount?: number; - courseChapterCount: number; - currency: string; - description: string; - enrolled?: boolean; - hasFreeChapter?: boolean; - /** @format uuid */ - id: string; - isPublished: boolean | null; - isScorm?: boolean; - priceInCents: number; - thumbnailUrl?: string; - thumbnailS3Key?: string; - thumbnailS3SingedUrl?: string; title: string; }; } @@ -527,6 +493,40 @@ export interface GetBetaCourseByIdResponse { id: string; title: string; lessonCount: number; + lessons?: { + /** @format uuid */ + id: string; + title: string; + type: string; + description: string; + displayOrder: number; + fileS3Key?: string | null; + fileType?: string | null; + questions?: { + /** @format uuid */ + id?: string; + type: + | "single_choice" + | "multiple_choice" + | "true_or_false" + | "photo_question" + | "fill_in_the_blanks" + | "brief_response" + | "detailed_response"; + description?: string | null; + title: string; + photoQuestionType?: ("single_choice" | "multiple_choice") | null; + photoS3Key?: string | null; + options?: { + /** @format uuid */ + id?: string; + optionText: string; + isCorrect: boolean; + position: number; + }[]; + }[]; + updatedAt?: string; + }[]; completedLessonCount?: number; chapterProgress?: "completed" | "in_progress" | "not_started"; isFreemium?: boolean; @@ -614,7 +614,7 @@ export interface GetLessonByIdResponse { /** @format uuid */ id: string; title: string; - type: string; + type: "text" | "file" | "presentation" | "video" | "quiz"; description: string; fileType: string | null; fileUrl: string | null; @@ -625,16 +625,16 @@ export interface GetLessonByIdResponse { wrongAnswerCount: number | null; score: number | null; }; + displayOrder: number; }; } export type BetaCreateLessonBody = { - updatedAt?: string; title: string; type: string; description: string; - fileS3Key?: string; - fileType?: string; + fileS3Key?: string | null; + fileType?: string | null; questions?: { /** @format uuid */ id?: string; @@ -647,11 +647,11 @@ export type BetaCreateLessonBody = { | "fill_in_the_blanks_dnd" | "brief_response" | "detailed_response"; - description?: string; + description?: string | null; title: string; displayOrder?: number; - photoQuestionType?: "single_choice" | "multiple_choice"; - photoS3Key?: string; + photoQuestionType?: ("single_choice" | "multiple_choice") | null; + photoS3Key?: string | null; options?: { /** @format uuid */ id?: string; @@ -663,6 +663,7 @@ export type BetaCreateLessonBody = { questionId?: string; }[]; }[]; + updatedAt?: string; } & { /** @format uuid */ chapterId: string; @@ -695,11 +696,11 @@ export type BetaCreateQuizLessonBody = { | "fill_in_the_blanks_dnd" | "brief_response" | "detailed_response"; - description?: string; + description?: string | null; title: string; displayOrder?: number; - photoQuestionType?: "single_choice" | "multiple_choice"; - photoS3Key?: string; + photoQuestionType?: ("single_choice" | "multiple_choice") | null; + photoS3Key?: string | null; options?: { /** @format uuid */ id?: string; @@ -743,11 +744,11 @@ export type BetaUpdateQuizLessonBody = { | "fill_in_the_blanks_dnd" | "brief_response" | "detailed_response"; - description?: string; + description?: string | null; title: string; displayOrder?: number; - photoQuestionType?: "single_choice" | "multiple_choice"; - photoS3Key?: string; + photoQuestionType?: ("single_choice" | "multiple_choice") | null; + photoS3Key?: string | null; options?: { /** @format uuid */ id?: string; @@ -772,12 +773,11 @@ export interface BetaUpdateQuizLessonResponse { } export type BetaUpdateLessonBody = { - updatedAt?: string; title?: string; type?: string; description?: string; - fileS3Key?: string; - fileType?: string; + fileS3Key?: string | null; + fileType?: string | null; questions?: { /** @format uuid */ id?: string; @@ -790,11 +790,11 @@ export type BetaUpdateLessonBody = { | "fill_in_the_blanks_dnd" | "brief_response" | "detailed_response"; - description?: string; + description?: string | null; title: string; displayOrder?: number; - photoQuestionType?: "single_choice" | "multiple_choice"; - photoS3Key?: string; + photoQuestionType?: ("single_choice" | "multiple_choice") | null; + photoS3Key?: string | null; options?: { /** @format uuid */ id?: string; @@ -806,6 +806,7 @@ export type BetaUpdateLessonBody = { questionId?: string; }[]; }[]; + updatedAt?: string; } & { /** @format uuid */ chapterId?: string; @@ -842,6 +843,15 @@ export interface GetChapterWithLessonResponse { id: string; title: string; lessonCount: number; + lessons: { + /** @format uuid */ + id: string; + title: string; + type: "quiz" | "presentation" | "video" | "text"; + displayOrder: number; + status: "completed" | "in_progress" | "not_started"; + quizQuestionCount: number | null; + }[]; completedLessonCount?: number; chapterProgress?: "completed" | "in_progress" | "not_started"; isFreemium?: boolean; @@ -851,50 +861,48 @@ export interface GetChapterWithLessonResponse { createdAt?: string; quizCount?: number; displayOrder: number; - lessons: { - updatedAt?: string; + }; +} + +export type BetaCreateChapterBody = { + title: string; + lessons?: { + /** @format uuid */ + id: string; + title: string; + type: string; + description: string; + displayOrder: number; + fileS3Key?: string | null; + fileType?: string | null; + questions?: { /** @format uuid */ - id: string; - title: string; - type: string; - description: string; - displayOrder: number; - fileS3Key?: string; - fileType?: string; - questions?: { + id?: string; + type: + | "single_choice" + | "multiple_choice" + | "true_or_false" + | "photo_question" + | "fill_in_the_blanks_text" + | "fill_in_the_blanks_dnd" + | "brief_response" + | "detailed_response"; + description?: string | null; + title: string;displayOrder?: number; + photoQuestionType?: ("single_choice" | "multiple_choice") | null; + photoS3Key?: string | null; + options?: { /** @format uuid */ id?: string; - type: - | "single_choice" - | "multiple_choice" - | "true_or_false" - | "photo_question" - | "fill_in_the_blanks_text" - | "fill_in_the_blanks_dnd" - | "brief_response" - | "detailed_response"; - description?: string; - title: string; - displayOrder?: number; - photoQuestionType?: "single_choice" | "multiple_choice"; - photoS3Key?: string; - options?: { - /** @format uuid */ - id?: string; - optionText: string; - displayOrder: number | null; + optionText: string;displayOrder: number | null; isStudentAnswer?: boolean; - isCorrect: boolean; - /** @format uuid */ + isCorrect: boolean; + /** @format uuid */ questionId?: string; - }[]; }[]; }[]; - }; -} - -export type BetaCreateChapterBody = { - title: string; + updatedAt?: string; + }[]; chapterProgress?: "completed" | "in_progress" | "not_started"; isFreemium?: boolean; enrolled?: boolean; @@ -917,6 +925,40 @@ export interface BetaCreateChapterResponse { export type UpdateChapterBody = { title?: string; + lessons?: { + /** @format uuid */ + id: string; + title: string; + type: string; + description: string; + displayOrder: number; + fileS3Key?: string | null; + fileType?: string | null; + questions?: { + /** @format uuid */ + id?: string; + type: + | "single_choice" + | "multiple_choice" + | "true_or_false" + | "photo_question" + | "fill_in_the_blanks" + | "brief_response" + | "detailed_response"; + description?: string | null; + title: string; + photoQuestionType?: ("single_choice" | "multiple_choice") | null; + photoS3Key?: string | null; + options?: { + /** @format uuid */ + id?: string; + optionText: string; + isCorrect: boolean; + position: number; + }[]; + }[]; + updatedAt?: string; + }[]; chapterProgress?: "completed" | "in_progress" | "not_started"; isFreemium?: boolean; enrolled?: boolean; @@ -1856,27 +1898,6 @@ export class API extends HttpClient - this.request({ - path: `/api/course/course-by-id`, - method: "GET", - query: query, - format: "json", - ...params, - }), - /** * No description * @@ -2274,8 +2295,6 @@ export class API extends HttpClient { + return useMutation({ + mutationFn: async ({ lessonId }: { lessonId: string }) => { + const response = await ApiClient.api.studentLessonProgressControllerMarkLessonAsCompleted({ + id: lessonId, + }); + + return response.data; + }, + onError: (error) => { + if (error instanceof AxiosError) { + return toast({ + description: error.response?.data.message, + variant: "destructive", + }); + } + toast({ + description: error.message, + variant: "destructive", + }); + }, + }); +}; diff --git a/apps/web/app/api/queries/admin/useCourseById.ts b/apps/web/app/api/queries/admin/useCourseById.ts deleted file mode 100644 index 81a2e808..00000000 --- a/apps/web/app/api/queries/admin/useCourseById.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; - -import { ApiClient } from "../../api-client"; - -import type { GetCourseByIdResponse } from "../../generated-api"; - -export const courseQueryOptions = (id: string) => - queryOptions({ - queryKey: ["course", "admin", { id }], - queryFn: async () => { - const response = await ApiClient.api.courseControllerGetCourseById({ - id, - }); - return response.data; - }, - select: (data: GetCourseByIdResponse) => data.data, - }); - -export function useCourseById(id: string) { - return useQuery(courseQueryOptions(id)); -} - -export function useCourseByIdSuspense(id: string) { - return useSuspenseQuery(courseQueryOptions(id)); -} diff --git a/apps/web/app/api/queries/useLesson.ts b/apps/web/app/api/queries/useLesson.ts index 4a3a79fc..80682322 100644 --- a/apps/web/app/api/queries/useLesson.ts +++ b/apps/web/app/api/queries/useLesson.ts @@ -2,25 +2,22 @@ import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query" import { ApiClient } from "../api-client"; -import type { GetLessonResponse } from "../generated-api"; +import type { GetLessonByIdResponse } from "../generated-api"; -export const lessonQueryOptions = (id: string, courseId: string) => +export const lessonQueryOptions = (id: string) => queryOptions({ - queryKey: ["lesson", id, courseId], + queryKey: ["lesson", id], queryFn: async () => { - const response = await ApiClient.api.lessonsControllerGetLesson({ - id, - courseId, - }); + const response = await ApiClient.api.lessonControllerGetLessonById(id); return response.data; }, - select: (data: GetLessonResponse) => data.data, + select: (data: GetLessonByIdResponse) => data.data, }); -export function useLesson(id: string, courseId: string) { - return useQuery(lessonQueryOptions(id, courseId)); +export function useLesson(id: string) { + return useQuery(lessonQueryOptions(id)); } -export function useLessonSuspense(id: string, courseId: string) { - return useSuspenseQuery(lessonQueryOptions(id, courseId)); +export function useLessonSuspense(id: string) { + return useSuspenseQuery(lessonQueryOptions(id)); } diff --git a/apps/web/app/components/RichText/Viever.tsx b/apps/web/app/components/RichText/Viever.tsx index cd751c12..db56e8a5 100644 --- a/apps/web/app/components/RichText/Viever.tsx +++ b/apps/web/app/components/RichText/Viever.tsx @@ -8,9 +8,23 @@ type ViewerProps = { content: string; style?: "default" | "prose"; className?: string; + variant?: "default" | "lesson"; }; -const Viewer = ({ content, style, className }: ViewerProps) => { +const defaultClasses = { + ul: "[&>div>ul]:list-disc [&>div>ul]:list-inside [&>div>ul>li>p]:inline", + ol: "[&>div>ol]:list-decimal [&>div>ol]:list-inside [&>div>ol>li>p]:inline", +}; + +const lessonVariantClasses = { + layout: "[&>div]:flex [&>div]:flex-col [&>div]:gap-y-6", + h2: "[&>div>h2]:h6 [&>div>h2]:text-neutral-950", + p: "[&>div>p]:body-base [&>div>p>strong]:body-base-md [&>div>p]:text-neutral-900", + ul: "[&>div>ul>li>p]:body-base [&>div>ul>li>p]:text-neutral-900 [&>div>ul>li>p>strong]:body-base-md [&>div>ul>li>p>strong]:text-neutral-950", + ol: "[&>div>ol>li>p]:body-base [&>div>ol>li>p]:text-neutral-900 [&>div>ol>li>p>strong]:body-base-md [&>div>ol>li>p>strong]:text-neutral-950", +}; + +const Viewer = ({ content, style, className, variant = "default" }: ViewerProps) => { const editor = useEditor({ extensions: [StarterKit], content: content, @@ -24,11 +38,18 @@ const Viewer = ({ content, style, className }: ViewerProps) => { className, ); - const unOrderedListClasses = "[&>div>ul]:list-disc [&>div>ul]:list-inside [&>div>ul>li>p]:inline"; - const orderedListClasses = - "[&>div>ol]:list-decimal [&>div>ol]:list-inside [&>div>ol>li>p]:inline"; - - const editorClasses = cn(unOrderedListClasses, orderedListClasses); + const variantClasses = + variant === "lesson" + ? [ + lessonVariantClasses.h2, + lessonVariantClasses.p, + lessonVariantClasses.layout, + lessonVariantClasses.ol, + lessonVariantClasses.ul, + ] + : []; + + const editorClasses = cn(defaultClasses.ul, defaultClasses.ol, ...variantClasses); return (
      diff --git a/apps/web/app/components/ui/badge.tsx b/apps/web/app/components/ui/badge.tsx index 3c0cbeed..5edd63bb 100644 --- a/apps/web/app/components/ui/badge.tsx +++ b/apps/web/app/components/ui/badge.tsx @@ -7,46 +7,58 @@ import type { VariantProps } from "class-variance-authority"; import type { HTMLAttributes } from "react"; import type { IconName } from "~/types/shared"; -const badgeVariants = cva( - "flex shrink-0 items-center h-min text-sm font-medium rounded-lg px-2 py-1 gap-x-2", - { - variants: { - variant: { - default: "text-neutral-900 bg-white border border-neutral-200", - success: "text-success-800 bg-success-100", - successFilled: "text-success-800 bg-success-50", - inProgress: "text-warning-800 bg-warning-100", - inProgressFilled: "text-secondary-700 bg-secondary-50", - notStarted: "text-neutral-600 bg-neutral-100", - notStartedFilled: "bg-neutral-50 text-neutral-900 details-md", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - outline: { - true: "bg-transparent border border-current", - false: "", - }, +const badgeVariants = cva("", { + variants: { + variant: { + default: "text-neutral-900 bg-white border border-neutral-200", + success: "text-success-800 bg-success-100", + successFilled: "text-success-800 bg-success-50", + inProgress: "text-warning-800 bg-warning-100", + inProgressFilled: "text-secondary-700 bg-secondary-50", + notStarted: "text-neutral-600 bg-neutral-100", + notStartedFilled: "bg-neutral-50 text-neutral-900 details-md", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + icon: "", }, - defaultVariants: { - variant: "default", - outline: false, + outline: { + true: "bg-transparent border border-current", + false: "", }, }, -); + defaultVariants: { + variant: "default", + outline: false, + }, +}); type BadgeProps = HTMLAttributes & VariantProps & { icon?: IconName; + iconClasses?: string; }; -export const Badge = ({ className, variant, outline, icon, children, ...props }: BadgeProps) => { +export const Badge = ({ + className, + variant, + outline, + icon, + children, + iconClasses, + ...props +}: BadgeProps) => { return ( -
      - {icon && } - {children} +
      + {icon && } + {children ? children : null}
      ); }; diff --git a/apps/web/app/modules/Admin/EditCourse/CourseLessons/components/LessonCardList.tsx b/apps/web/app/modules/Admin/EditCourse/CourseLessons/components/LessonCardList.tsx index 65842055..482ebd8f 100644 --- a/apps/web/app/modules/Admin/EditCourse/CourseLessons/components/LessonCardList.tsx +++ b/apps/web/app/modules/Admin/EditCourse/CourseLessons/components/LessonCardList.tsx @@ -38,7 +38,7 @@ export const LessonCardList = ({ case "video": setContentTypeToDisplay(ContentTypes.VIDEO_LESSON_FORM); break; - case "text_block": + case "text": setContentTypeToDisplay(ContentTypes.TEXT_LESSON_FORM); break; case "presentation": diff --git a/apps/web/app/modules/Admin/EditCourse/CoursePricing/hooks/useCoursePricingForm.ts b/apps/web/app/modules/Admin/EditCourse/CoursePricing/hooks/useCoursePricingForm.ts index 667d49bf..142c97bc 100644 --- a/apps/web/app/modules/Admin/EditCourse/CoursePricing/hooks/useCoursePricingForm.ts +++ b/apps/web/app/modules/Admin/EditCourse/CoursePricing/hooks/useCoursePricingForm.ts @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { useUpdateCourse } from "~/api/mutations/admin/useUpdateCourse"; -import { courseQueryOptions } from "~/api/queries/admin/useCourseById"; +import { courseQueryOptions } from "~/api/queries/admin/useBetaCourse"; import { queryClient } from "~/api/queryClient"; import { coursePricingFormSchema } from "../validators/coursePricingFormSchema"; diff --git a/apps/web/app/modules/Admin/EditCourse/CourseSettings/hooks/useCourseSettingsForm.tsx b/apps/web/app/modules/Admin/EditCourse/CourseSettings/hooks/useCourseSettingsForm.tsx index d407b52f..a679d0fd 100644 --- a/apps/web/app/modules/Admin/EditCourse/CourseSettings/hooks/useCourseSettingsForm.tsx +++ b/apps/web/app/modules/Admin/EditCourse/CourseSettings/hooks/useCourseSettingsForm.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { useUpdateCourse } from "~/api/mutations/admin/useUpdateCourse"; -import { courseQueryOptions } from "~/api/queries/admin/useCourseById"; +import { courseQueryOptions } from "~/api/queries/admin/useBetaCourse"; import { queryClient } from "~/api/queryClient"; import { courseSettingsFormSchema } from "~/modules/Admin/EditCourse/CourseSettings/validators/courseSettingsFormSchema"; diff --git a/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx b/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx index ee24b4d8..64ccb4db 100644 --- a/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx +++ b/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx @@ -1,161 +1,98 @@ -import { type ClientLoaderFunctionArgs, Link, useParams } from "@remix-run/react"; -import { useState } from "react"; -import { FormProvider, useForm } from "react-hook-form"; +import { useNavigate, useParams } from "@remix-run/react"; -import { useClearQuizProgress } from "~/api/mutations/useClearQuizProgress"; -import { useSubmitQuiz } from "~/api/mutations/useSubmitQuiz"; -import { useCourseSuspense } from "~/api/queries/useCourse"; -import { lessonQueryOptions, useLessonSuspense } from "~/api/queries/useLesson"; -import { queryClient } from "~/api/queryClient"; +import { useCourse, useLesson } from "~/api/queries"; import { PageWrapper } from "~/components/PageWrapper"; -import { Button } from "~/components/ui/button"; -import { LessonItems } from "~/modules/Courses/Lesson/LessonItems/LessonItems"; -import { QuizSummaryModal } from "~/modules/Courses/Lesson/QuizSummaryModal"; +import { LessonContent } from "~/modules/Courses/Lesson/LessonContent"; +import { LessonSidebar } from "~/modules/Courses/Lesson/LessonSidebar"; -import Breadcrumb from "./Breadcrumb"; -import Overview from "./Overview"; -import Summary from "./Summary"; -import { getOrderedLessons, getQuestionsArray, getUserAnswers } from "./utils"; - -import type { TQuestionsForm } from "./types"; -import type { MetaFunction } from "@remix-run/node"; - -export const meta: MetaFunction = () => { - return [{ title: "Lesson" }]; -}; - -export const clientLoader = async ({ params }: ClientLoaderFunctionArgs) => { - const { lessonId = "", courseId = "" } = params; - if (!lessonId) throw new Error("Lesson ID not found"); - await queryClient.prefetchQuery(lessonQueryOptions(lessonId, courseId)); - return null; -}; +import type { GetCourseResponse } from "~/api/generated-api"; export default function LessonPage() { - const { lessonId = "", courseId = "" } = useParams(); - const [isOpen, setIsOpen] = useState(false); - const { data: lesson, refetch } = useLessonSuspense(lessonId, courseId); - const { - data: { id, title, lessons }, - } = useCourseSuspense(courseId ?? ""); - - const methods = useForm({ - mode: "onChange", - defaultValues: getUserAnswers(lesson), - }); + const { courseId = "", lessonId = "" } = useParams(); + const { data: lesson } = useLesson(lessonId); + const { data: course } = useCourse(courseId); + const navigate = useNavigate(); - const submitQuiz = useSubmitQuiz({ - handleOnSuccess: async () => { - await refetch(); - setIsOpen(true); - }, - }); - const clearQuizProgress = useClearQuizProgress({ - handleOnSuccess: async () => { - methods.reset(); - await refetch(); - }, - }); + if (!lesson || !course) return null; - const lessonsIds = lessons.map((lesson) => lesson.id); - const currentLessonIndex = lessonsIds.indexOf(lessonId ?? ""); + const currentChapter = course.chapters.find((chapter) => + chapter?.lessons.some((l) => l.id === lessonId), + ); - if (!lesson || !lessonId) { - throw new Error(`Lesson with id: ${lessonId} not found`); + function findCurrentLessonIndex( + lessons: GetCourseResponse["data"]["chapters"][number]["lessons"], + currentLessonId: string, + ) { + return lessons.findIndex((lesson) => lesson.id === currentLessonId); } - const orderedLessonsItems = getOrderedLessons(lesson); - - const questionsArray = getQuestionsArray(orderedLessonsItems); - - const previousLessonId = currentLessonIndex > 0 ? lessonsIds[currentLessonIndex - 1] : null; - const nextLessonId = - currentLessonIndex < lessonsIds.length - 1 ? lessonsIds[currentLessonIndex + 1] : null; - - const isQuiz = lesson?.type === "quiz"; - const isEnrolled = lesson?.enrolled; - - const getScorePercentage = () => { - if (!lesson.quizScore || lesson.quizScore === 0 || !lesson.lessonItems.length) return "0%"; - - return `${((lesson.quizScore / lesson.lessonItems.length) * 100)?.toFixed(0)}%`; - }; - - const scorePercentage = getScorePercentage(); + function handleNextLesson( + currentLessonId: string, + chapters: GetCourseResponse["data"]["chapters"], + ) { + for (const chapter of chapters) { + const lessonIndex = findCurrentLessonIndex(chapter.lessons, currentLessonId); + if (lessonIndex !== -1) { + if (lessonIndex + 1 < chapter.lessons.length) { + const nextLessonId = chapter.lessons[lessonIndex + 1].id; + navigate(`/course/${courseId}/lesson/${nextLessonId}`); + } else { + const currentChapterIndex = chapters.indexOf(chapter); + if (currentChapterIndex + 1 < chapters.length) { + const nextChapterId = chapters[currentChapterIndex + 1].lessons[0].id; + navigate(`/course/${courseId}/lesson/${nextChapterId}`); + } + } + } + } + + return null; + } - const updateLessonItemCompletion = async () => { - await refetch(); - }; + function handlePrevLesson( + currentLessonId: string, + chapters: GetCourseResponse["data"]["chapters"], + ) { + for (const chapter of chapters) { + const lessonIndex = findCurrentLessonIndex(chapter.lessons, currentLessonId); + if (lessonIndex !== -1) { + if (lessonIndex > 0) { + const prevLessonId = chapter.lessons[lessonIndex - 1].id; + navigate(`/course/${courseId}/lesson/${prevLessonId}`); + } else { + const currentChapterIndex = chapters.indexOf(chapter); + if (currentChapterIndex > 0) { + const prevChapter = chapters[currentChapterIndex - 1]; + const prevChapterId = prevChapter.lessons[prevChapter.lessons.length - 1].id; + + navigate(`/course/${courseId}/lesson/${prevChapterId}`); + } + } + } + } + + return null; + } return ( -
      - -
      - - - {isQuiz && ( - - )} - -
      - - -
      - {isQuiz && ( - - )} - {isEnrolled && ( -
      - !previousLessonId && e.preventDefault()} - reloadDocument - replace - > - - - !nextLessonId && e.preventDefault()} - reloadDocument - replace - > - - -
      - )} + +
      +
      +
      +

      + Chapter {currentChapter?.displayOrder}:{" "} + {course?.title} +

      +
      + handlePrevLesson(lessonId, course.chapters)} + handleNext={() => handleNextLesson(lessonId, course.chapters)} + />
      - - -
      + +
      +
      ); } diff --git a/apps/web/app/modules/Courses/Lesson/LessonContent.tsx b/apps/web/app/modules/Courses/Lesson/LessonContent.tsx new file mode 100644 index 00000000..b60427ca --- /dev/null +++ b/apps/web/app/modules/Courses/Lesson/LessonContent.tsx @@ -0,0 +1,66 @@ +import { startCase } from "lodash-es"; +import { match } from "ts-pattern"; + +import { useMarkLessonAsCompleted } from "~/api/mutations"; +import { Icon } from "~/components/Icon"; +import Viewer from "~/components/RichText/Viever"; +import { Button } from "~/components/ui/button"; + +import type { GetLessonByIdResponse } from "~/api/generated-api"; + +type LessonContentProps = { + lesson: GetLessonByIdResponse["data"]; + lessonsAmount: number; + handlePrevious: () => void; + handleNext: () => void; +}; + +export const LessonContent = ({ + lesson, + lessonsAmount, + handlePrevious, + handleNext, +}: LessonContentProps) => { + const mutation = useMarkLessonAsCompleted(); + + const Content = () => + match(lesson.type) + .with("text", () => ) + .with("quiz", () => <>) + .otherwise(() => <>); + + const handleMarkLessonAsComplete = () => { + mutation.mutate({ lessonId: lesson.id }); + }; + + return ( +
      +
      +
      +
      +

      + Lesson {lesson.displayOrder}/{lessonsAmount} - {startCase(lesson.type)} +

      +

      {lesson.title}

      +
      +
      + + +
      +
      + +
      + +
      +
      +
      + ); +}; diff --git a/apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx b/apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx new file mode 100644 index 00000000..9937e9c3 --- /dev/null +++ b/apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx @@ -0,0 +1,98 @@ +import { Link } from "@remix-run/react"; +import { startCase } from "lodash-es"; + +import CourseProgress from "~/components/CourseProgress"; +import { Icon } from "~/components/Icon"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "~/components/ui/accordion"; +import { Badge } from "~/components/ui/badge"; +import { CategoryChip } from "~/components/ui/CategoryChip"; +import { cn } from "~/lib/utils"; +import { LessonTypesIcons } from "~/modules/Courses/NewCourseView/lessonTypes"; + +import type { GetCourseResponse } from "~/api/generated-api"; + +type LessonSidebarProps = { + course: GetCourseResponse["data"]; + courseId: string; +}; + +const progressBadge = { + completed: "InputRoundedMarkerSuccess", + in_progress: "InProgress", + not_started: "NotStartedRounded", +} as const; + +export const LessonSidebar = ({ course, courseId }: LessonSidebarProps) => { + return ( +
      +
      +
      +
      + +
      +

      {course.title}

      + +
      +
      +

      Table of content:

      +
      + + {course?.chapters?.map(({ id, title, lessons, chapterProgress }) => { + return ( + + + +
      {title}
      + +
      + + {lessons?.map(({ id, title, status, type }) => { + return ( + + {" "} +
      +

      {title}

      +

      {startCase(type)}

      +
      + + + ); + })} +
      +
      + ); + })} +
      +
      +
      +
      +
      + ); +}; diff --git a/apps/web/app/modules/Courses/NewCourseView/CourseChapterLesson.tsx b/apps/web/app/modules/Courses/NewCourseView/CourseChapterLesson.tsx index d3cec93c..50e28b07 100644 --- a/apps/web/app/modules/Courses/NewCourseView/CourseChapterLesson.tsx +++ b/apps/web/app/modules/Courses/NewCourseView/CourseChapterLesson.tsx @@ -1,3 +1,5 @@ +import { Link } from "@remix-run/react"; + import { ProgressBadge } from "~/components/Badges/ProgressBadge"; import { Icon } from "~/components/Icon"; import { LessonTypes, LessonTypesIcons } from "~/modules/Courses/NewCourseView/lessonTypes"; @@ -18,7 +20,7 @@ type CourseChapterLessonProps = { export const CourseChapterLesson = ({ lesson }: CourseChapterLessonProps) => { return ( -
      +

      @@ -30,6 +32,6 @@ export const CourseChapterLesson = ({ lesson }: CourseChapterLessonProps) => { {LessonTypes[lesson.type]}

      -
      + ); }; diff --git a/apps/web/app/modules/Courses/NewCourseView/lessonTypes.ts b/apps/web/app/modules/Courses/NewCourseView/lessonTypes.ts index c1665c85..d1f46f32 100644 --- a/apps/web/app/modules/Courses/NewCourseView/lessonTypes.ts +++ b/apps/web/app/modules/Courses/NewCourseView/lessonTypes.ts @@ -1,13 +1,13 @@ export const LessonTypes = { presentation: "Presentation", - text_block: "Text Block", + text: "Text", video: "Video", quiz: "Quiz", } as const; export const LessonTypesIcons = { presentation: "Presentation", - text_block: "Text", + text: "Text", video: "Video", quiz: "Quiz", } as const;