diff --git a/apps/api/src/lessons/lessons.service.ts b/apps/api/src/lessons/lessons.service.ts index 74750c123..fa1a95126 100644 --- a/apps/api/src/lessons/lessons.service.ts +++ b/apps/api/src/lessons/lessons.service.ts @@ -281,7 +281,7 @@ export class LessonsService { } private async getLessonItems(lesson: Lesson, courseId: UUIDType, userId: UUIDType) { - const lessonItemsList = await this.lessonsRepository.getLessonItems(lesson.id); + const lessonItemsList = await this.lessonsRepository.getLessonItems(lesson.id, courseId); const validLessonItemsList = lessonItemsList.filter(this.isValidItem); return await Promise.all( @@ -337,7 +337,7 @@ export class LessonsService { userId: UUIDType, quizCompleted: boolean, ) { - const lessonItemsList = await this.lessonsRepository.getLessonItems(lesson.id); + const lessonItemsList = await this.lessonsRepository.getLessonItems(lesson.id, courseId); const validLessonItemsList = lessonItemsList.filter(this.isValidItem); return await Promise.all( @@ -409,6 +409,7 @@ export class LessonsService { lessonItemId: item.lessonItemId, lessonItemType: item.lessonItemType, displayOrder: item.displayOrder, + isCompleted: item.isCompleted, content, }; } diff --git a/apps/api/src/lessons/repositories/lessons.repository.ts b/apps/api/src/lessons/repositories/lessons.repository.ts index 0bf84ff19..a35666b28 100644 --- a/apps/api/src/lessons/repositories/lessons.repository.ts +++ b/apps/api/src/lessons/repositories/lessons.repository.ts @@ -72,6 +72,32 @@ export class LessonsRepository { return lesson; } + async getLesson(courseId: UUIDType, lessonId: UUIDType) { + const [lesson] = await this.db + .select({ + id: lessons.id, + title: lessons.title, + description: sql`${lessons.description}`, + imageUrl: sql`${lessons.imageUrl}`, + type: sql`${lessons.type}`, + isFree: courseLessons.isFree, + }) + .from(lessons) + .innerJoin( + courseLessons, + and(eq(courseLessons.lessonId, lessons.id), eq(courseLessons.courseId, courseId)), + ) + .where( + and( + eq(lessons.id, lessonId), + eq(lessons.archived, false), + eq(lessons.state, STATES.published), + ), + ); + + return lesson; + } + async getQuestionItems( lessonId: UUIDType, studentId: UUIDType, @@ -113,7 +139,7 @@ export class LessonsRepository { .orderBy(lessonItems.displayOrder); } - async getLessonItems(lessonId: UUIDType) { + async getLessonItems(lessonId: UUIDType, courseId: UUIDType) { return await this.db .select({ lessonItemType: lessonItems.lessonItemType, @@ -122,6 +148,7 @@ export class LessonsRepository { textBlockData: textBlocks, fileData: files, displayOrder: lessonItems.displayOrder, + isCompleted: sql`CASE WHEN ${studentCompletedLessonItems.id} IS NOT NULL THEN true ELSE false END`, }) .from(lessonItems) .leftJoin( @@ -148,6 +175,14 @@ export class LessonsRepository { eq(files.state, STATES.published), ), ) + .leftJoin( + studentCompletedLessonItems, + and( + eq(studentCompletedLessonItems.lessonItemId, lessonItems.id), + eq(studentCompletedLessonItems.lessonId, lessonId), + eq(studentCompletedLessonItems.courseId, courseId), + ), + ) .where(and(eq(lessonItems.lessonId, lessonId))) .orderBy(lessonItems.displayOrder); } diff --git a/apps/api/src/lessons/schemas/lessonItem.schema.ts b/apps/api/src/lessons/schemas/lessonItem.schema.ts index 195dda024..d92edbbee 100644 --- a/apps/api/src/lessons/schemas/lessonItem.schema.ts +++ b/apps/api/src/lessons/schemas/lessonItem.schema.ts @@ -64,6 +64,7 @@ export const lessonItemSchema = Type.Object({ export const lessonItemWithContent = Type.Object({ ...lessonItemSchema.properties, + isCompleted: Type.Boolean(), questionData: Type.Union([questionSchema, Type.Null()]), textBlockData: Type.Union([textBlockSchema, Type.Null()]), fileData: Type.Union([lessonItemFileSchema, Type.Null()]), @@ -142,6 +143,7 @@ export const lessonItemSelectSchema = Type.Object({ lessonItemType: Type.String(), displayOrder: Type.Union([Type.Number(), Type.Null()]), passQuestion: Type.Optional(Type.Union([Type.Null(), Type.Unknown()])), + isCompleted: Type.Optional(Type.Boolean()), content: Type.Union([questionContentResponse, textBlockContentResponse, fileContentResponse]), }); diff --git a/apps/api/src/questions/questions.module.ts b/apps/api/src/questions/questions.module.ts index 0539e69a5..46fda4485 100644 --- a/apps/api/src/questions/questions.module.ts +++ b/apps/api/src/questions/questions.module.ts @@ -4,12 +4,18 @@ import { LessonsRepository } from "src/lessons/repositories/lessons.repository"; import { StudentCompletedLessonItemsService } from "src/studentCompletedLessonItem/studentCompletedLessonItems.service"; import { QuestionsController } from "./api/questions.controller"; +import { QuestionsRepository } from "./questions.repository"; import { QuestionsService } from "./questions.service"; @Module({ imports: [], controllers: [QuestionsController], - providers: [QuestionsService, StudentCompletedLessonItemsService, LessonsRepository], + providers: [ + QuestionsService, + StudentCompletedLessonItemsService, + QuestionsRepository, + LessonsRepository, + ], exports: [], }) export class QuestionsModule {} diff --git a/apps/api/src/questions/questions.repository.ts b/apps/api/src/questions/questions.repository.ts new file mode 100644 index 000000000..fc3197046 --- /dev/null +++ b/apps/api/src/questions/questions.repository.ts @@ -0,0 +1,146 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { and, eq, inArray, sql } from "drizzle-orm"; + +import { DatabasePg, type UUIDType } from "src/common"; +import { + lessonItems, + lessons, + questionAnswerOptions, + questions, + studentQuestionAnswers, +} from "src/storage/schema"; + +import type { AnswerQuestionSchema, QuestionSchema } from "./schema/question.schema"; +import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import type * as schema from "src/storage/schema"; + +@Injectable() +export class QuestionsRepository { + constructor(@Inject("DB") private readonly db: DatabasePg) {} + + async fetchQuestionData( + answerQuestion: AnswerQuestionSchema, + trx?: PostgresJsDatabase, + ): Promise { + const dbInstance = trx ?? this.db; + + const [questionData] = await dbInstance + .select({ + lessonId: lessons.id, + questionId: sql`${questions.id}`, + questionType: sql`${questions.questionType}`, + lessonItemAssociationId: lessonItems.id, + }) + .from(lessons) + .innerJoin( + lessonItems, + and( + eq(lessonItems.lessonId, answerQuestion.lessonId), + eq(lessonItems.lessonItemId, answerQuestion.questionId), + ), + ) + .leftJoin( + questions, + and( + eq(questions.id, lessonItems.lessonItemId), + eq(questions.archived, false), + eq(questions.state, "published"), + ), + ) + .where( + and( + eq(lessons.id, lessonItems.lessonId), + eq(lessons.archived, false), + eq(lessons.state, "published"), + ), + ); + + return questionData; + } + + async findExistingAnswer( + userId: UUIDType, + questionId: UUIDType, + lessonId: UUIDType, + courseId: UUIDType, + trx?: PostgresJsDatabase, + ): Promise { + const dbInstance = trx ?? this.db; + const [existingAnswer] = await dbInstance + .select({ + id: studentQuestionAnswers.id, + }) + .from(studentQuestionAnswers) + .where( + and( + eq(studentQuestionAnswers.studentId, userId), + eq(studentQuestionAnswers.questionId, questionId), + eq(studentQuestionAnswers.lessonId, lessonId), + eq(studentQuestionAnswers.courseId, courseId), + ), + ); + + return existingAnswer?.id; + } + + async getQuestionAnswers( + questionId: UUIDType, + answerList: string[], + trx?: PostgresJsDatabase, + ) { + const dbInstance = trx ?? this.db; + + return await dbInstance + .select({ + answer: questionAnswerOptions.optionText, + }) + .from(questionAnswerOptions) + .where( + and( + eq(questionAnswerOptions.questionId, questionId), + inArray(questionAnswerOptions.id, answerList), + ), + ); + } + + async deleteAnswer(answerId: UUIDType, trx?: PostgresJsDatabase) { + const dbInstance = trx ?? this.db; + + return await dbInstance + .delete(studentQuestionAnswers) + .where(eq(studentQuestionAnswers.id, answerId)); + } + + async upsertAnswer( + courseId: UUIDType, + lessonId: UUIDType, + questionId: UUIDType, + userId: UUIDType, + answerId: UUIDType | null, + answer: string[], + trx?: PostgresJsDatabase, + ): Promise { + const jsonBuildObjectArgs = answer.join(","); + const dbInstance = trx ?? this.db; + + if (answerId) { + await dbInstance + .update(studentQuestionAnswers) + .set({ + answer: sql`json_build_object(${sql.raw(jsonBuildObjectArgs)})`, + }) + .where(eq(studentQuestionAnswers.id, answerId)); + return; + } + + await dbInstance.insert(studentQuestionAnswers).values({ + questionId, + answer: sql`json_build_object(${sql.raw(jsonBuildObjectArgs)})`, + studentId: userId, + lessonId, + courseId, + }); + + return; + } +} diff --git a/apps/api/src/questions/questions.service.ts b/apps/api/src/questions/questions.service.ts index 6bbc08ede..8fa797723 100644 --- a/apps/api/src/questions/questions.service.ts +++ b/apps/api/src/questions/questions.service.ts @@ -6,53 +6,42 @@ import { NotAcceptableException, NotFoundException, } from "@nestjs/common"; -import { and, eq, inArray, sql } from "drizzle-orm"; -import { DatabasePg, type UUIDType } from "src/common"; +import { DatabasePg } from "src/common"; import { LessonsRepository } from "src/lessons/repositories/lessons.repository"; -import { - courseLessons, - lessonItems, - lessons, - questionAnswerOptions, - questions, - studentQuestionAnswers, -} from "src/storage/schema"; import { StudentCompletedLessonItemsService } from "src/studentCompletedLessonItem/studentCompletedLessonItems.service"; +import { QuestionsRepository } from "./questions.repository"; import { QUESTION_TYPE } from "./schema/questions.types"; import type { AnswerQuestionSchema, QuestionSchema } from "./schema/question.schema"; +import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import type * as schema from "src/storage/schema"; @Injectable() export class QuestionsService { constructor( @Inject("DB") private readonly db: DatabasePg, private readonly studentCompletedLessonItemsService: StudentCompletedLessonItemsService, + private readonly questionsRepository: QuestionsRepository, private readonly lessonsRepository: LessonsRepository, ) {} async questionAnswer(answerQuestion: AnswerQuestionSchema, userId: string) { return await this.db.transaction(async (trx) => { - const questionData: QuestionSchema = await this.fetchQuestionData(trx, answerQuestion); + const questionData: QuestionSchema = await this.questionsRepository.fetchQuestionData( + answerQuestion, + trx, + ); if (!questionData || !questionData.questionId) { throw new NotFoundException("Question not found"); } - const [lesson] = await trx - .select({ - lessonType: lessons.type, - isFree: sql`COALESCE(${courseLessons.isFree}, FALSE)`, - }) - .from(lessons) - .leftJoin(courseLessons, eq(courseLessons.lessonId, lessons.id)) - .where( - and( - eq(lessons.id, answerQuestion.lessonId), - eq(courseLessons.courseId, answerQuestion.courseId), - ), - ); + const lesson = await this.lessonsRepository.getLesson( + answerQuestion.courseId, + answerQuestion.lessonId, + ); const quizProgress = await this.lessonsRepository.getQuizProgress( answerQuestion.courseId, @@ -60,15 +49,15 @@ export class QuestionsService { userId, ); - if (lesson.lessonType === "quiz" && quizProgress?.quizCompleted) + if (lesson.type === "quiz" && quizProgress?.quizCompleted) throw new ConflictException("Quiz already completed"); - const lastAnswerId = await this.findExistingAnswer( - trx, + const lastAnswerId = await this.questionsRepository.findExistingAnswer( userId, questionData.questionId, questionData.lessonId, answerQuestion.courseId, + trx, ); const questionTypeHandlers = { @@ -97,70 +86,8 @@ export class QuestionsService { }); } - private async fetchQuestionData( - trx: any, - answerQuestion: AnswerQuestionSchema, - ): Promise { - const [questionData] = await trx - .select({ - lessonId: lessons.id, - questionId: questions.id, - questionType: questions.questionType, - lessonItemAssociationId: lessonItems.id, - }) - .from(lessons) - .innerJoin( - lessonItems, - and( - eq(lessonItems.lessonId, answerQuestion.lessonId), - eq(lessonItems.lessonItemId, answerQuestion.questionId), - ), - ) - .leftJoin( - questions, - and( - eq(questions.id, lessonItems.lessonItemId), - eq(questions.archived, false), - eq(questions.state, "published"), - ), - ) - .where( - and( - eq(lessons.id, lessonItems.lessonId), - eq(lessons.archived, false), - eq(lessons.state, "published"), - ), - ); - - return questionData; - } - - private async findExistingAnswer( - trx: any, - userId: UUIDType, - questionId: UUIDType, - lessonId: UUIDType, - courseId: UUIDType, - ): Promise { - const [existingAnswer] = await trx - .select({ - id: studentQuestionAnswers.id, - }) - .from(studentQuestionAnswers) - .where( - and( - eq(studentQuestionAnswers.studentId, userId), - eq(studentQuestionAnswers.questionId, questionId), - eq(studentQuestionAnswers.lessonId, lessonId), - eq(studentQuestionAnswers.courseId, courseId), - ), - ); - - return existingAnswer?.id; - } - private async handleChoiceAnswer( - trx: any, + trx: PostgresJsDatabase, questionData: QuestionSchema, answerQuestion: AnswerQuestionSchema, lastAnswerId: string | null, @@ -171,36 +98,32 @@ export class QuestionsService { } if (answerQuestion.answer.length < 1) - return await this.upsertAnswer( - trx, + return await this.questionsRepository.upsertAnswer( answerQuestion.courseId, questionData.lessonId, questionData.questionId, userId, lastAnswerId, [], + trx, ); - const answers: { answer: string }[] = await trx - .select({ - answer: questionAnswerOptions.optionText, - }) - .from(questionAnswerOptions) - .where( - and( - eq(questionAnswerOptions.questionId, questionData.questionId), - inArray( - questionAnswerOptions.id, - answerQuestion.answer.map((a) => { - if (typeof a !== "string") { - return a.value; - } - - return a; - }), - ), - ), - ); + const answerList = answerQuestion.answer.map((a) => { + if (typeof a !== "string") { + return a.value; + } + + return a; + }); + + if (!answerList || answerList.length === 0) + throw new NotFoundException("User answers not found"); + + const answers: { answer: string }[] = await this.questionsRepository.getQuestionAnswers( + answerQuestion.questionId, + answerList, + trx, + ); if (!answers || answers.length !== answerQuestion.answer.length) throw new NotFoundException("Answers not found"); @@ -210,19 +133,19 @@ export class QuestionsService { return acc; }, [] as string[]); - await this.upsertAnswer( - trx, + await this.questionsRepository.upsertAnswer( answerQuestion.courseId, questionData.lessonId, questionData.questionId, userId, lastAnswerId, studentAnswer, + trx, ); } private async handleFillInTheBlanksAnswer( - trx: any, + trx: PostgresJsDatabase, questionData: QuestionSchema, answerQuestion: AnswerQuestionSchema, lastAnswerId: string | null, @@ -239,19 +162,19 @@ export class QuestionsService { return acc; }, [] as string[]); - await this.upsertAnswer( - trx, + await this.questionsRepository.upsertAnswer( answerQuestion.courseId, questionData.lessonId, questionData.questionId, userId, lastAnswerId, studentAnswer, + trx, ); } private async handleOpenAnswer( - trx: any, + trx: PostgresJsDatabase, questionData: QuestionSchema, answerQuestion: AnswerQuestionSchema, lastAnswerId: string | null, @@ -265,50 +188,21 @@ export class QuestionsService { if (answerQuestion.answer.length < 1) { if (!lastAnswerId) return; - return await trx - .delete(studentQuestionAnswers) - .where(eq(studentQuestionAnswers.id, lastAnswerId)); + await this.questionsRepository.deleteAnswer(lastAnswerId, trx); + return; } - const studentAnswer = [`'1'`, `'${answerQuestion.answer}'`]; + const studentAnswer = [`'0'`, `'${answerQuestion.answer}'`]; - await this.upsertAnswer( - trx, + await this.questionsRepository.upsertAnswer( answerQuestion.courseId, questionData.lessonId, questionData.questionId, userId, lastAnswerId, studentAnswer, + trx, ); - } - - private async upsertAnswer( - trx: any, - courseId: UUIDType, - lessonId: UUIDType, - questionId: UUIDType, - userId: UUIDType, - answerId: UUIDType | null, - answer: string[], - ): Promise { - const jsonBuildObjectArgs = answer.join(","); - if (answerId) { - await trx - .update(studentQuestionAnswers) - .set({ - answer: sql`json_build_object(${sql.raw(jsonBuildObjectArgs)})`, - }) - .where(eq(studentQuestionAnswers.id, answerId)); - return; - } - - await trx.insert(studentQuestionAnswers).values({ - questionId, - answer: sql`json_build_object(${sql.raw(jsonBuildObjectArgs)})`, - studentId: userId, - lessonId, - courseId, - }); + return; } } diff --git a/apps/api/src/swagger/api-schema.json b/apps/api/src/swagger/api-schema.json index c9401ca90..a043c4190 100644 --- a/apps/api/src/swagger/api-schema.json +++ b/apps/api/src/swagger/api-schema.json @@ -4709,6 +4709,9 @@ {} ] }, + "isCompleted": { + "type": "boolean" + }, "content": { "anyOf": [ { @@ -4980,6 +4983,9 @@ {} ] }, + "isCompleted": { + "type": "boolean" + }, "content": { "anyOf": [ { diff --git a/apps/web/app/api/generated-api.ts b/apps/web/app/api/generated-api.ts index 27862bba8..fba1d8e46 100644 --- a/apps/web/app/api/generated-api.ts +++ b/apps/web/app/api/generated-api.ts @@ -626,6 +626,7 @@ export interface GetLessonResponse { lessonItemType: string; displayOrder: number | null; passQuestion?: null; + isCompleted?: boolean; content: | { /** @format uuid */ @@ -686,6 +687,7 @@ export interface GetLessonByIdResponse { lessonItemType: string; displayOrder: number | null; passQuestion?: null; + isCompleted?: boolean; content: | { /** @format uuid */ diff --git a/apps/web/app/modules/Courses/Lesson/Breadcrumb.tsx b/apps/web/app/modules/Courses/Lesson/Breadcrumb.tsx index a908a30d8..e39517437 100644 --- a/apps/web/app/modules/Courses/Lesson/Breadcrumb.tsx +++ b/apps/web/app/modules/Courses/Lesson/Breadcrumb.tsx @@ -1,7 +1,3 @@ -import { useParams } from "@remix-run/react"; - -import { useCourseSuspense } from "~/api/queries/useCourse"; -import { useLessonSuspense } from "~/api/queries/useLesson"; import { BreadcrumbItem, BreadcrumbLink, @@ -9,20 +5,15 @@ import { BreadcrumbSeparator, } from "~/components/ui/breadcrumb"; -export default function Breadcrumb() { - const { courseId, lessonId } = useParams(); - const { data: courseData } = useCourseSuspense(courseId!); - const { data: lessonData } = useLessonSuspense(lessonId!, courseId!); - - if (!courseData) { - throw new Error(`Course with id: ${courseId} not found`); - } +import type { GetLessonResponse } from "~/api/generated-api"; - if (!lessonData) { - throw new Error(`Lesson with id: ${lessonId} not found`); - } +type BreadcrumbProps = { + lessonData: GetLessonResponse["data"]; + courseId: string; + courseTitle: string; +}; - const courseTitle = courseData?.title || "Course"; +export default function Breadcrumb({ lessonData, courseId, courseTitle }: BreadcrumbProps) { const lessonTitle = lessonData?.title || "Lesson"; return ( diff --git a/apps/web/app/modules/Courses/Lesson/Lesson.layout.tsx b/apps/web/app/modules/Courses/Lesson/Lesson.layout.tsx deleted file mode 100644 index f0cadaa21..000000000 --- a/apps/web/app/modules/Courses/Lesson/Lesson.layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Outlet } from "@remix-run/react"; - -import Breadcrumb from "./Breadcrumb"; -import Overview from "./Overview"; -import Summary from "./Summary"; - -export default function LessonLayout() { - return ( -
-
- - - -
- -
- ); -} diff --git a/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx b/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx index 23aca4caf..34bb577a9 100644 --- a/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx +++ b/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx @@ -5,13 +5,15 @@ import { FormProvider, useForm } from "react-hook-form"; import { useClearQuizProgress } from "~/api/mutations/useClearQuizProgress"; import { useSubmitQuiz } from "~/api/mutations/useSubmitQuiz"; import { useCourseSuspense } from "~/api/queries/useCourse"; -import { allCoursesQueryOptions } from "~/api/queries/useCourses"; import { lessonQueryOptions, useLessonSuspense } from "~/api/queries/useLesson"; import { queryClient } from "~/api/queryClient"; import { Button } from "~/components/ui/button"; import { LessonItems } from "~/modules/Courses/Lesson/LessonItems/LessonItems"; import { QuizSummaryModal } from "~/modules/Courses/Lesson/QuizSummaryModal"; +import Breadcrumb from "./Breadcrumb"; +import Overview from "./Overview"; +import Summary from "./Summary"; import { getOrderedLessons, getQuestionsArray, getUserAnswers } from "./utils"; import type { TQuestionsForm } from "./types"; @@ -24,7 +26,6 @@ export const meta: MetaFunction = () => { export const clientLoader = async ({ params }: ClientLoaderFunctionArgs) => { const { lessonId = "", courseId = "" } = params; if (!lessonId) throw new Error("Lesson ID not found"); - await queryClient.prefetchQuery(allCoursesQueryOptions()); await queryClient.prefetchQuery(lessonQueryOptions(lessonId, courseId)); return null; }; @@ -34,7 +35,7 @@ export default function LessonPage() { const [isOpen, setIsOpen] = useState(false); const { data, refetch } = useLessonSuspense(lessonId, courseId); const { - data: { lessons }, + data: { id, title, lessons }, } = useCourseSuspense(courseId ?? ""); const methods = useForm({ @@ -81,62 +82,92 @@ export default function LessonPage() { const scorePercentage = getScorePercentage(); + const [lesson, setLesson] = useState(data); + + const updateLessonItemCompletion = (lessonItemId: string) => { + setLesson((prevLesson) => { + const updatedLessonItems = prevLesson.lessonItems.map((item) => { + if (item.lessonItemId === lessonItemId) { + return { ...item, isCompleted: true }; + } + return item; + }); + + return { + ...prevLesson, + lessonItems: updatedLessonItems, + itemsCompletedCount: (prevLesson?.itemsCompletedCount ?? 0) + 1, + }; + }); + }; + return ( - <> - {isQuiz && ( - - )} - -
- + +
+ + + {isQuiz && ( + - - - {isQuiz && ( - - )} - {isEnrolled && ( -
- !previousLessonId && e.preventDefault()} - reloadDocument - replace - > - - - !nextLessonId && e.preventDefault()} - reloadDocument - replace + )} + +
+ + +
+ {isQuiz && ( + - -
- )} - + {data?.isSubmitted ? "Clear progress" : "Check answers"} + + )} + {isEnrolled && ( +
+ !previousLessonId && e.preventDefault()} + reloadDocument + replace + > + + + !nextLessonId && e.preventDefault()} + reloadDocument + replace + > + + +
+ )} +
+ ); } diff --git a/apps/web/app/modules/Courses/Lesson/LessonItems/File/File.tsx b/apps/web/app/modules/Courses/Lesson/LessonItems/File/File.tsx index 811df26e6..c731c9f36 100644 --- a/apps/web/app/modules/Courses/Lesson/LessonItems/File/File.tsx +++ b/apps/web/app/modules/Courses/Lesson/LessonItems/File/File.tsx @@ -12,9 +12,16 @@ type FileProps = { type: string; url: string; }; + isCompleted: boolean; + updateLessonItemCompletion: (lessonItemId: string) => void; }; -export const File = ({ content, lessonItemId }: FileProps) => { +export const File = ({ + content, + lessonItemId, + isCompleted, + updateLessonItemCompletion, +}: FileProps) => { const { isAdmin } = useUserRole(); const isPresentation = @@ -24,9 +31,24 @@ export const File = ({ content, lessonItemId }: FileProps) => {
{content.title}
{isPresentation ? ( - + ) : ( -
); diff --git a/apps/web/app/modules/Courses/Lesson/LessonItems/File/Presentation.tsx b/apps/web/app/modules/Courses/Lesson/LessonItems/File/Presentation.tsx index d538769f0..6adae3849 100644 --- a/apps/web/app/modules/Courses/Lesson/LessonItems/File/Presentation.tsx +++ b/apps/web/app/modules/Courses/Lesson/LessonItems/File/Presentation.tsx @@ -6,24 +6,28 @@ import { useIntersection } from "react-use"; import { useMarkLessonItemAsCompleted } from "~/api/mutations/useMarkLessonItemAsCompleted"; -import { useCompletedLessonItemsStore } from "../LessonItemStore"; - type PresentationProps = { url: string; presentationId: string; isAdmin: boolean; + isCompleted: boolean; + lessonItemId: string; + updateLessonItemCompletion: (lessonItemId: string) => void; }; -export default function Presentation({ url, presentationId, isAdmin }: PresentationProps) { +export default function Presentation({ + url, + presentationId, + isAdmin, + isCompleted, + lessonItemId, + updateLessonItemCompletion, +}: PresentationProps) { const intersectionRef = useRef(null); const { lessonId, courseId = "" } = useParams<{ lessonId: string; courseId: string; }>(); - const { - isLessonItemCompleted: isPresentationCompleted, - markLessonItemAsCompleted: markPresentationAsCompleted, - } = useCompletedLessonItemsStore(); const { mutate: markLessonItemAsCompleted } = useMarkLessonItemAsCompleted(); const intersection = useIntersection(intersectionRef, { root: null, @@ -34,17 +38,12 @@ export default function Presentation({ url, presentationId, isAdmin }: Presentat useEffect(() => { if (!lessonId) throw new Error("Lesson ID not found"); - const isCompleted = isPresentationCompleted(presentationId); const isInViewport = intersection && intersection.intersectionRatio === 1; const loadTimeout = setTimeout(() => { if (isInViewport && !isCompleted && !isAdmin) { - markPresentationAsCompleted({ - lessonItemId: presentationId, - lessonId, - courseId, - }); markLessonItemAsCompleted({ id: presentationId, lessonId, courseId }); + updateLessonItemCompletion(lessonItemId); } }, 200); @@ -55,9 +54,10 @@ export default function Presentation({ url, presentationId, isAdmin }: Presentat presentationId, markLessonItemAsCompleted, intersection, - isPresentationCompleted, - markPresentationAsCompleted, courseId, + isCompleted, + lessonItemId, + updateLessonItemCompletion, ]); const docs = [ diff --git a/apps/web/app/modules/Courses/Lesson/LessonItems/File/Video.tsx b/apps/web/app/modules/Courses/Lesson/LessonItems/File/Video.tsx index 74ff3532c..6a36f9f25 100644 --- a/apps/web/app/modules/Courses/Lesson/LessonItems/File/Video.tsx +++ b/apps/web/app/modules/Courses/Lesson/LessonItems/File/Video.tsx @@ -3,33 +3,38 @@ import ReactPlayer from "react-player"; import { useMarkLessonItemAsCompleted } from "~/api/mutations/useMarkLessonItemAsCompleted"; -import { useCompletedLessonItemsStore } from "../LessonItemStore"; - type VideoProps = { url: string; videoId: string; isAdmin: boolean; type: string; + isCompleted: boolean; + lessonItemId: string; + updateLessonItemCompletion: (lessonItemId: string) => void; }; -export default function Video({ url, videoId, isAdmin, type }: VideoProps) { +export default function Video({ + url, + videoId, + isAdmin, + type, + isCompleted, + lessonItemId, + updateLessonItemCompletion, +}: VideoProps) { const { lessonId, courseId = "" } = useParams<{ lessonId: string; courseId: string; }>(); const { mutate: markLessonItemAsCompleted } = useMarkLessonItemAsCompleted(); - const { - isLessonItemCompleted: isVideoCompleted, - markLessonItemAsCompleted: markVideoAsCompleted, - } = useCompletedLessonItemsStore(); if (!lessonId) throw new Error("Lesson ID not found"); const handleMarkLessonItemAsCompleted = () => { - if (isVideoCompleted(videoId)) return; + if (isCompleted) return; - markVideoAsCompleted({ lessonItemId: videoId, lessonId, courseId }); markLessonItemAsCompleted({ id: videoId, lessonId, courseId }); + updateLessonItemCompletion(lessonItemId); }; const isExternalVideo = type === "external_video"; @@ -37,7 +42,11 @@ export default function Video({ url, videoId, isAdmin, type }: VideoProps) { return (
{isExternalVideo ? ( - + ) : (
diff --git a/apps/web/app/modules/Courses/Lesson/Summary/SingleLessonSummary.tsx b/apps/web/app/modules/Courses/Lesson/Summary/SingleLessonSummary.tsx index fbb4aef37..0341d102c 100644 --- a/apps/web/app/modules/Courses/Lesson/Summary/SingleLessonSummary.tsx +++ b/apps/web/app/modules/Courses/Lesson/Summary/SingleLessonSummary.tsx @@ -1,31 +1,23 @@ import { cn } from "~/lib/utils"; -import { useCompletedLessonItemsStore } from "../LessonItems/LessonItemStore"; - type TProps = { lesson: { displayOrder: number | null; id: string; title: string; + isCompleted: boolean; }; isLast: boolean; }; export default function SingleLessonSummary({ lesson, isLast }: TProps) { - const { isLessonItemCompleted } = useCompletedLessonItemsStore(); - return (
- +
); diff --git a/apps/web/app/modules/Courses/Lesson/Summary/index.tsx b/apps/web/app/modules/Courses/Lesson/Summary/index.tsx index d5e795fcb..c1219883b 100644 --- a/apps/web/app/modules/Courses/Lesson/Summary/index.tsx +++ b/apps/web/app/modules/Courses/Lesson/Summary/index.tsx @@ -1,6 +1,5 @@ import { useParams } from "@remix-run/react"; -import { useLessonSuspense } from "~/api/queries/useLesson"; import CourseProgress from "~/components/CourseProgress"; import { Card, CardContent } from "~/components/ui/card"; import { QuizSummary } from "~/modules/Courses/Lesson/Summary/QuizSummary"; @@ -8,20 +7,25 @@ import SingleLessonSummary from "~/modules/Courses/Lesson/Summary/SingleLessonSu import { getSummaryItems } from "../utils"; -export default function Summary() { - const { lessonId = "", courseId = "" } = useParams(); - const { data } = useLessonSuspense(lessonId, courseId); +import type { GetLessonResponse } from "~/api/generated-api"; - const isQuiz = data.type === "quiz"; +type SummaryProps = { + lesson: GetLessonResponse["data"]; +}; - const lessonItemsCount = data.itemsCount; - const lessonItemsCompletedCount = data.itemsCompletedCount ?? 0; +export default function Summary({ lesson }: SummaryProps) { + const { courseId = "" } = useParams(); - const lessonItemsSummary = getSummaryItems(data); + const isQuiz = lesson.type === "quiz"; + + const lessonItemsCount = lesson.itemsCount; + const lessonItemsCompletedCount = lesson.itemsCompletedCount ?? 0; + + const lessonItemsSummary = getSummaryItems(lesson); return ( - {isQuiz && } + {isQuiz && } {!isQuiz && (
Lesson Summary
diff --git a/apps/web/app/modules/Courses/Lesson/utils.ts b/apps/web/app/modules/Courses/Lesson/utils.ts index bea855f58..dfa0be597 100644 --- a/apps/web/app/modules/Courses/Lesson/utils.ts +++ b/apps/web/app/modules/Courses/Lesson/utils.ts @@ -12,12 +12,14 @@ export const getSummaryItems = (lesson: GetLessonResponse["data"]) => { title: lessonItem.content.title, displayOrder: lessonItem.displayOrder, id: lessonItem.lessonItemId, + isCompleted: !!lessonItem.isCompleted, }; } else { return { title: lessonItem.content.questionBody, displayOrder: lessonItem.displayOrder, id: lessonItem.content.id, + isCompleted: !!lessonItem.isCompleted, }; } }) diff --git a/apps/web/app/modules/Dashboard/Dashboard.layout.tsx b/apps/web/app/modules/Dashboard/Dashboard.layout.tsx index 7ff3127e4..f6a91bf4a 100644 --- a/apps/web/app/modules/Dashboard/Dashboard.layout.tsx +++ b/apps/web/app/modules/Dashboard/Dashboard.layout.tsx @@ -29,7 +29,7 @@ export default function DashboardLayout() {
-
+
diff --git a/apps/web/routes.ts b/apps/web/routes.ts index 5d860bc3e..ca95d2642 100644 --- a/apps/web/routes.ts +++ b/apps/web/routes.ts @@ -15,11 +15,7 @@ export const routes: ( index: true, }); route("course/:id", "modules/Courses/CourseView/CourseView.page.tsx"); - route("course/:courseId/lesson/:lessonId", "modules/Courses/Lesson/Lesson.layout.tsx", () => { - route("", "modules/Courses/Lesson/Lesson.page.tsx", { - index: true, - }); - }); + route("course/:courseId/lesson/:lessonId", "modules/Courses/Lesson/Lesson.page.tsx"); route("settings", "modules/Dashboard/Settings/Settings.layout.tsx", () => { route("", "modules/Dashboard/Settings/Settings.page.tsx", { index: true,