diff --git a/src/pages/content-management/HumanLanguagePage.tsx b/src/pages/content-management/HumanLanguagePage.tsx index 419aee0..2607dbd 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -93,6 +93,14 @@ type PracticeDialogState = practiceId?: number } +type LessonDialogState = + | { open: false } + | { + open: true + subModuleId: number + defaultIndex: number + } + type QuestionDialogState = | { open: false } | { @@ -346,7 +354,13 @@ export function HumanLanguagePage() { const [subModuleCardSelection, setSubModuleCardSelection] = useState>({}) const [practiceQuestionsState, setPracticeQuestionsState] = useState>({}) const [practiceDialog, setPracticeDialog] = useState({ open: false }) + const [lessonDialog, setLessonDialog] = useState({ open: false }) const [questionDialog, setQuestionDialog] = useState({ open: false }) + const [lessonForm, setLessonForm] = useState({ + title: "", + description: "", + introVideoUrl: "", + }) const [practiceForm, setPracticeForm] = useState({ title: "", description: "", @@ -361,6 +375,7 @@ export function HumanLanguagePage() { const [practiceTargetDelete, setPracticeTargetDelete] = useState<{ id: number; title: string } | null>(null) const [questionTargetDelete, setQuestionTargetDelete] = useState<{ id: number; practiceId: number; text: string } | null>(null) const [savingPractice, setSavingPractice] = useState(false) + const [savingLesson, setSavingLesson] = useState(false) const [savingQuestion, setSavingQuestion] = useState(false) const [deletingPractice, setDeletingPractice] = useState(false) const [deletingQuestion, setDeletingQuestion] = useState(false) @@ -368,11 +383,14 @@ export function HumanLanguagePage() { const [loadingQuestionEditId, setLoadingQuestionEditId] = useState(null) /** Show inline field errors only after an explicit save attempt (avoids noisy empty-state on open). */ const [practiceSubmitAttempted, setPracticeSubmitAttempted] = useState(false) + const [lessonSubmitAttempted, setLessonSubmitAttempted] = useState(false) const [questionSubmitAttempted, setQuestionSubmitAttempted] = useState(false) + const [lessonFormTouched, setLessonFormTouched] = useState(false) const [practiceFormTouched, setPracticeFormTouched] = useState(false) const [questionFormTouched, setQuestionFormTouched] = useState(false) const [loadingPracticeForm, setLoadingPracticeForm] = useState(false) const [uploadingPracticeIntroVideo, setUploadingPracticeIntroVideo] = useState(false) + const [uploadingLessonIntroVideo, setUploadingLessonIntroVideo] = useState(false) const renderMediaPreview = ( urlRaw: string, @@ -566,23 +584,84 @@ export function HumanLanguagePage() { } } - const handleCreateLesson = async (subModuleId: number, currentLessonsCount: number) => { - const key = `lesson-${subModuleId}` - setCreatingKey(key) + const resetLessonForm = () => + setLessonForm({ + title: "", + description: "", + introVideoUrl: "", + }) + + const openCreateLessonDialog = (subModuleId: number, currentLessonsCount: number) => { + setLessonSubmitAttempted(false) + setLessonFormTouched(false) + const next = (currentLessonsCount || 0) + 1 + setLessonForm({ + title: `Lesson ${next}`, + description: "", + introVideoUrl: "", + }) + setLessonDialog({ open: true, subModuleId, defaultIndex: next }) + } + + const lessonFieldErrors = useMemo(() => { + const title = lessonForm.title.trim() + return { + title: title ? undefined : "Title is required.", + } + }, [lessonForm.title]) + + const lessonCanSave = !lessonFieldErrors.title + + const handleSaveLesson = async () => { + if (!lessonDialog.open) return + if (!lessonCanSave) { + setLessonSubmitAttempted(true) + return + } + try { - const next = (currentLessonsCount || 0) + 1 + setSavingLesson(true) await createLesson({ - sub_module_id: subModuleId, - title: `Lesson ${next}`, - description: `Auto-created lesson ${next}`, + sub_module_id: lessonDialog.subModuleId, + title: lessonForm.title.trim(), + description: lessonForm.description.trim() || undefined, + intro_video_url: lessonForm.introVideoUrl.trim() || undefined, }) toast.success("Lesson created") + setLessonDialog({ open: false }) + setLessonSubmitAttempted(false) + setLessonFormTouched(false) + resetLessonForm() await loadHierarchy(false) } catch (error) { console.error("Failed to create lesson:", error) toast.error("Failed to create lesson") } finally { - setCreatingKey(null) + setSavingLesson(false) + } + } + + const handleLessonIntroVideoFileChange = async (event: ChangeEvent) => { + const file = event.target.files?.[0] + event.target.value = "" + if (!file) return + setUploadingLessonIntroVideo(true) + try { + const uploadRes = await uploadVideoFile(file, { + title: lessonForm.title.trim() || file.name.replace(/\.[^.]+$/, "") || "Lesson intro", + description: lessonForm.description.trim() || undefined, + }) + const finalUrl = uploadRes.data?.data?.embed_url?.trim() + ? `${uploadRes.data.data.embed_url}?h=${uploadRes.data.data.url?.split("/").filter(Boolean).at(-1) ?? ""}` + : uploadRes.data?.data?.url?.trim() + if (!finalUrl) throw new Error("Missing uploaded video url") + setLessonForm((prev) => ({ ...prev, introVideoUrl: finalUrl })) + toast.success("Lesson intro video uploaded") + } catch (error) { + console.error("Failed to upload lesson intro video:", error) + toast.error("Failed to upload lesson intro video") + } finally { + setUploadingLessonIntroVideo(false) } } @@ -1643,14 +1722,9 @@ export function HumanLanguagePage() { size="sm" variant="outline" className="h-8 border-grayScale-200 bg-white px-2 text-[11px] hover:border-brand-200 hover:bg-brand-50/40" - onClick={() => handleCreateLesson(subModule.id, lessonRows.length)} - disabled={creatingKey === `lesson-${subModule.id}`} + onClick={() => openCreateLessonDialog(subModule.id, lessonRows.length)} > - {creatingKey === `lesson-${subModule.id}` ? ( - - ) : ( - - )} + New lesson ) : null} @@ -2119,6 +2193,110 @@ export function HumanLanguagePage() { + { + if (!open) { + setLessonDialog({ open: false }) + setLessonSubmitAttempted(false) + setLessonFormTouched(false) + resetLessonForm() + } + }} + > + + + Create Lesson + + Create a lesson as a `sub_module_lessons` entry linked to a QUIZ question set. + {!lessonCanSave ? ( + Required fields must be completed before you can save. + ) : null} + + +
+
+ + { + setLessonFormTouched(true) + setLessonForm((p) => ({ ...p, title: e.target.value })) + }} + className={cn( + "h-10 w-full rounded-md border px-3 text-sm", + (lessonSubmitAttempted || lessonFormTouched) && lessonFieldErrors.title + ? "border-red-300 ring-1 ring-red-200" + : "border-grayScale-200", + )} + placeholder={lessonDialog.open ? `Lesson ${lessonDialog.defaultIndex}` : "Lesson title"} + aria-invalid={Boolean((lessonSubmitAttempted || lessonFormTouched) && lessonFieldErrors.title)} + /> + {(lessonSubmitAttempted || lessonFormTouched) && lessonFieldErrors.title ? ( +

{lessonFieldErrors.title}

+ ) : null} +
+
+ +