From b524962409664ed11c824872c598eaf7cb61861d Mon Sep 17 00:00:00 2001 From: Rajat Date: Fri, 28 Nov 2025 17:49:25 +0000 Subject: [PATCH 1/6] Removed circular flow introduced by useEffect --- .../admin/products/quiz-builder/index.tsx | 128 ++++++++++-------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/apps/web/components/admin/products/quiz-builder/index.tsx b/apps/web/components/admin/products/quiz-builder/index.tsx index 894dfc1a9..ab1562f06 100644 --- a/apps/web/components/admin/products/quiz-builder/index.tsx +++ b/apps/web/components/admin/products/quiz-builder/index.tsx @@ -1,4 +1,4 @@ -import React, { startTransition, useEffect, useState } from "react"; +import React, { useState } from "react"; import { Section } from "@courselit/components-library"; import { LESSON_QUIZ_ADD_QUESTION, @@ -34,31 +34,17 @@ export function QuizBuilder({ content, onChange }: QuizBuilderProps) { (content && content.passingGrade) || DEFAULT_PASSING_GRADE, ); - useEffect(() => { - startTransition(() => { - if (content.questions) { - setQuestions(content.questions); - } - if (content.passingGrade) { - setPassingGradePercentage(content.passingGrade); - } - if (content.requiresPassingGrade) { - setPassingGradeRequired(content.requiresPassingGrade); - } - }); - }, [content]); - - useEffect(() => { + const notifyChange = (updatedQuestions: Question[]) => { onChange({ - questions, + questions: updatedQuestions, requiresPassingGrade: passingGradeRequired, passingGrade: passingGradePercentage, }); - }, [questions, passingGradeRequired, passingGradePercentage, onChange]); + }; const addNewOption = (questionIndex: number) => { - setQuestions((prevQuestions) => - prevQuestions.map((question, index) => + setQuestions((prevQuestions) => { + const updatedQuestions = prevQuestions.map((question, index) => index === questionIndex ? { ...question, @@ -68,14 +54,16 @@ export function QuizBuilder({ content, onChange }: QuizBuilderProps) { ], } : question, - ), - ); + ); + notifyChange(updatedQuestions); + return updatedQuestions; + }); }; const setCorrectAnswer = (questionIndex: number) => (index: number, checked: boolean) => { - setQuestions((prevQuestions) => - prevQuestions.map((question, qIdx) => + setQuestions((prevQuestions) => { + const updatedQuestions = prevQuestions.map((question, qIdx) => qIdx === questionIndex ? { ...question, @@ -86,14 +74,16 @@ export function QuizBuilder({ content, onChange }: QuizBuilderProps) { ), } : question, - ), - ); + ); + notifyChange(updatedQuestions); + return updatedQuestions; + }); }; const setOptionText = (questionIndex: number) => (index: number, text: string) => { - setQuestions((prevQuestions) => - prevQuestions.map((question, qIdx) => + setQuestions((prevQuestions) => { + const updatedQuestions = prevQuestions.map((question, qIdx) => qIdx === questionIndex ? { ...question, @@ -104,21 +94,25 @@ export function QuizBuilder({ content, onChange }: QuizBuilderProps) { ), } : question, - ), - ); + ); + notifyChange(updatedQuestions); + return updatedQuestions; + }); }; const setQuestionText = (index: number) => (text: string) => { - setQuestions((prevQuestions) => - prevQuestions.map((question, qIdx) => + setQuestions((prevQuestions) => { + const updatedQuestions = prevQuestions.map((question, qIdx) => qIdx === index ? { ...question, text } : question, - ), - ); + ); + notifyChange(updatedQuestions); + return updatedQuestions; + }); }; const removeOption = (questionIndex: number) => (index: number) => { - setQuestions((prevQuestions) => - prevQuestions.map((question, qIdx) => + setQuestions((prevQuestions) => { + const updatedQuestions = prevQuestions.map((question, qIdx) => qIdx === questionIndex ? { ...question, @@ -127,26 +121,35 @@ export function QuizBuilder({ content, onChange }: QuizBuilderProps) { ), } : question, - ), - ); + ); + notifyChange(updatedQuestions); + return updatedQuestions; + }); }; const deleteQuestion = (questionIndex: number) => { - setQuestions((prevQuestions) => - prevQuestions.filter((_, idx) => idx !== questionIndex), - ); + setQuestions((prevQuestions) => { + const updatedQuestions = prevQuestions.filter( + (_, idx) => idx !== questionIndex, + ); + notifyChange(updatedQuestions); + return updatedQuestions; + }); }; - const addNewQuestion = () => - setQuestions((prevQuestions) => [ - ...prevQuestions, - { - text: `${LESSON_QUIZ_QUESTION_PLACEHOLDER} #${ - prevQuestions.length + 1 - }`, - options: [{ text: "", correctAnswer: false }], - }, - ]); + const addNewQuestion = () => { + setQuestions((prevQuestions) => { + const updatedQuestions = [ + ...prevQuestions, + { + text: `${LESSON_QUIZ_QUESTION_PLACEHOLDER} #${prevQuestions.length + 1}`, + options: [{ text: "", correctAnswer: false }], + }, + ]; + notifyChange(updatedQuestions); + return updatedQuestions; + }); + }; return (
@@ -190,17 +193,28 @@ export function QuizBuilder({ content, onChange }: QuizBuilderProps) { - setPassingGradeRequired(checked) - } + onCheckedChange={(checked) => { + setPassingGradeRequired(checked); + onChange({ + questions, + requiresPassingGrade: checked, + passingGrade: passingGradePercentage, + }); + }} />
- setPassingGradePercentage(parseInt(e.target.value)) - } + onChange={(e) => { + const newValue = parseInt(e.target.value); + setPassingGradePercentage(newValue); + onChange({ + questions, + requiresPassingGrade: passingGradeRequired, + passingGrade: newValue, + }); + }} disabled={!passingGradeRequired} min={0} max={100} From d01da1d9f09a6feee5ed6bdeb68b720cc6f9c57b Mon Sep 17 00:00:00 2001 From: Rajat Date: Fri, 28 Nov 2025 17:50:51 +0000 Subject: [PATCH 2/6] WIP: leaner lesson builder and embed lesson supports iframes in addition to youtube --- .../lesson/lesson-content-renderer.tsx | 236 ++++++ .../content/section/[section]/lesson/page.tsx | 749 ++---------------- .../section/[section]/lesson/skeleton.tsx | 32 + apps/web/ui-config/strings.ts | 1 + 4 files changed, 351 insertions(+), 667 deletions(-) create mode 100644 apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/lesson-content-renderer.tsx create mode 100644 apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/skeleton.tsx diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/lesson-content-renderer.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/lesson-content-renderer.tsx new file mode 100644 index 000000000..0c820885f --- /dev/null +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/lesson-content-renderer.tsx @@ -0,0 +1,236 @@ +import { + Constants, + Lesson, + Media, + Profile, + TextEditorContent, + UIConstants, +} from "@courselit/common-models"; +import { MediaSelector, useToast } from "@courselit/components-library"; +import { Editor, emptyDoc as TextEditorEmptyDoc } from "@courselit/text-editor"; +import { QuizBuilder } from "@components/admin/products/quiz-builder"; +import { Info } from "lucide-react"; +import { + TEXT_EDITOR_PLACEHOLDER, + TOAST_TITLE_ERROR, + TOAST_TITLE_SUCCESS, +} from "@ui-config/strings"; +import { + MIMETYPE_VIDEO, + MIMETYPE_AUDIO, + MIMETYPE_PDF, +} from "@ui-config/constants"; +import { useContext, useEffect, useState } from "react"; +import { AddressContext, ProfileContext } from "@components/contexts"; +import { FetchBuilder } from "@courselit/utils"; +import { Textarea } from "@components/ui/textarea"; + +interface LessonContentRendererProps { + lesson: Partial; + errors: Partial>; + onContentChange: (content: { value: string }) => void; + onLessonChange: (updates: Partial) => void; +} + +export function LessonContentRenderer({ + lesson, + errors, + onContentChange, + onLessonChange, +}: LessonContentRendererProps) { + const address = useContext(AddressContext); + const { profile } = useContext(ProfileContext); + const [embedURL, setEmbedURL] = useState( + (lesson.content as any)?.value ?? "", + ); + const { toast } = useToast(); + + useEffect(() => { + if ( + JSON.stringify(lesson.content) !== + JSON.stringify({ value: embedURL }) + ) { + onContentChange({ value: embedURL }); + } + }, [embedURL]); + + const saveMediaContent = async (media?: Media) => { + const query = ` + mutation ($id: ID!, $media: MediaInput) { + lesson: updateLesson(lessonData: { + id: $id + media: $media + }) { + lessonId + } + } + `; + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ + query, + variables: { + id: lesson?.lessonId, + media: media + ? Object.assign({}, media, { + file: + media.access === "public" ? media.file : null, + }) + : null, + }, + }) + .setIsGraphQLEndpoint(true) + .build(); + + try { + await fetch.exec(); + toast({ + title: TOAST_TITLE_SUCCESS, + description: "Lesson updated", + }); + } catch (err: any) { + toast({ + title: TOAST_TITLE_ERROR, + description: err.message, + variant: "destructive", + }); + } + }; + + switch (lesson.type) { + case Constants.LessonType.TEXT: + return ( +
+ { + onContentChange(state); + }} + url={address.backend} + placeholder={TEXT_EDITOR_PLACEHOLDER} + /> + {errors.content && ( +

{errors.content}

+ )} +
+ ); + case Constants.LessonType.EMBED: + return ( +
+
+