diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 84aa61e..86bebf5 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -616,11 +616,17 @@ export const updateExamPrepModuleLesson = ( data, ) -/** PUT /exam-prep/lessons/:lessonId — set publish_status only (draft or published). */ +/** PUT /exam-prep/lessons/:lessonId — set publish_status only. */ +export const setExamPrepModuleLessonPublishStatus = ( + lessonId: number, + data: PublishStatusOnlyRequest, +) => http.put(`/exam-prep/lessons/${lessonId}`, data) + +/** @deprecated Use setExamPrepModuleLessonPublishStatus */ export const publishExamPrepModuleLesson = ( lessonId: number, data: PublishExamPrepModuleLessonRequest, -) => http.put(`/exam-prep/lessons/${lessonId}`, data) +) => setExamPrepModuleLessonPublishStatus(lessonId, data) /** PUT /exam-prep/lessons/:lessonId — set access_tier only. */ export const setExamPrepModuleLessonAccessTier = ( @@ -840,16 +846,22 @@ export const updateParentLinkedPractice = ( data: UpdateParentLinkedPracticeRequest, ) => http.put(`/practices/${practiceId}`, data) -/** PUT /practices/:id — set publish_status only. */ -export const setParentLinkedPracticePublishStatus = ( +/** PUT /practices/:id — set publish_status only (Learn English practice). */ +export const setLearnEnglishPracticePublishStatus = ( practiceId: number, - data: PublishParentLinkedPracticeRequest, + data: PublishStatusOnlyRequest, ) => http.put(`/practices/${practiceId}`, data) +/** @deprecated Use setLearnEnglishPracticePublishStatus */ +export const setParentLinkedPracticePublishStatus = ( + practiceId: number, + data: PublishParentLinkedPracticeRequest, +) => setLearnEnglishPracticePublishStatus(practiceId, data) + /** PUT /practices/:id — publish a draft practice. */ export const publishParentLinkedPractice = (practiceId: number) => - setParentLinkedPracticePublishStatus(practiceId, { + setLearnEnglishPracticePublishStatus(practiceId, { publish_status: "PUBLISHED", }) diff --git a/src/pages/content-management/CourseDetailPage.tsx b/src/pages/content-management/CourseDetailPage.tsx index 4469cc7..4b74922 100644 --- a/src/pages/content-management/CourseDetailPage.tsx +++ b/src/pages/content-management/CourseDetailPage.tsx @@ -29,7 +29,7 @@ import { getPracticesByParentCourse, getProgramCourses, getTopLevelCourseModules, - setParentLinkedPracticePublishStatus, + setLearnEnglishPracticePublishStatus, setTopLevelCourseModuleAccessTier, setTopLevelCourseModulePublishStatus, updateTopLevelCourseModule, @@ -359,7 +359,7 @@ export function CourseDetailPage() { ) => { setPublishStatusPracticeId(practiceId); try { - await setParentLinkedPracticePublishStatus(practiceId, { + await setLearnEnglishPracticePublishStatus(practiceId, { publish_status: nextStatus, }); setPractices((prev) => diff --git a/src/pages/content-management/CourseManagementPage.tsx b/src/pages/content-management/CourseManagementPage.tsx index 4c208b6..711559a 100644 --- a/src/pages/content-management/CourseManagementPage.tsx +++ b/src/pages/content-management/CourseManagementPage.tsx @@ -28,6 +28,8 @@ import { toast } from "sonner"; import { ResolvedImage } from "../../components/media/ResolvedImage"; import { createExamPrepCatalogUnit, + getExamPrepCatalogCourses, + setExamPrepCatalogCoursePublishStatus, setExamPrepCatalogUnitAccessTier, setExamPrepCatalogUnitPublishStatus, updateExamPrepCatalogUnit, @@ -38,6 +40,7 @@ import { uploadImageFile } from "../../api/files.api"; import { ContentPublishStatusChip } from "./components/ContentPublishStatusChip"; import { ContentAccessTierChip } from "./components/ContentAccessTierChip"; import { ContentListSearchFilterBar } from "./components/ContentListSearchFilterBar"; +import { ContentPageDescription } from "./components/ContentPageDescription"; import type { ContentAccessTier, PracticePublishStatus } from "../../types/course.types"; import { filterBySearchAndPublishStatus, @@ -92,6 +95,13 @@ export function CourseManagementPage() { const [listSearch, setListSearch] = useState(""); const [publishStatusFilter, setPublishStatusFilter] = useState("all"); + const [catalogCourseName, setCatalogCourseName] = useState("Course"); + const [catalogCourseDescription, setCatalogCourseDescription] = useState(""); + const [catalogCoursePublishStatus, setCatalogCoursePublishStatus] = useState< + PracticePublishStatus | string | null + >(null); + const [catalogCoursePublishStatusUpdating, setCatalogCoursePublishStatusUpdating] = + useState(false); const filteredUnits = useMemo( () => @@ -104,14 +114,24 @@ export function CourseManagementPage() { [listSearch, publishStatusFilter, units], ); - // Mock data for display titles - const courseTitles: Record = { - duolingo: "Duolingo English Test", - ielts: "IELTS Academic", - }; + const courseDisplayName = catalogCourseName; - const courseDisplayName = - courseTitles[courseId || ""] || "Duolingo English Test"; + const loadCatalogCourse = useCallback(async () => { + if (!Number.isFinite(catalogCourseId) || catalogCourseId < 1) return; + try { + const response = await getExamPrepCatalogCourses({ limit: 100, offset: 0 }); + const rows = response.data?.data?.catalog_courses; + const list = Array.isArray(rows) ? rows : []; + const row = list.find((c) => Number(c.id) === catalogCourseId); + if (row) { + setCatalogCourseName(row.name?.trim() || `Course ${catalogCourseId}`); + setCatalogCourseDescription(row.description?.trim() || ""); + setCatalogCoursePublishStatus(row.publish_status ?? null); + } + } catch (error) { + console.error(error); + } + }, [catalogCourseId]); const loadUnits = useCallback(async () => { if (!Number.isFinite(catalogCourseId) || catalogCourseId < 1) { @@ -155,10 +175,37 @@ export function CourseManagementPage() { } }, [catalogCourseId]); + useEffect(() => { + void loadCatalogCourse(); + }, [loadCatalogCourse]); + useEffect(() => { void loadUnits(); }, [loadUnits]); + const handleCatalogCoursePublishStatus = async ( + nextStatus: PracticePublishStatus, + ) => { + if (!Number.isFinite(catalogCourseId) || catalogCourseId < 1) return; + setCatalogCoursePublishStatusUpdating(true); + try { + await setExamPrepCatalogCoursePublishStatus(catalogCourseId, { + publish_status: nextStatus, + }); + setCatalogCoursePublishStatus(nextStatus); + toast.success( + nextStatus === "PUBLISHED" ? "Course published" : "Course saved as draft", + ); + } catch (error: unknown) { + const message = + (error as { response?: { data?: { message?: string } } })?.response?.data + ?.message ?? "Failed to update course status"; + toast.error(message); + } finally { + setCatalogCoursePublishStatusUpdating(false); + } + }; + const handleUnitPublishStatus = async ( unitId: number, nextStatus: PracticePublishStatus, @@ -484,12 +531,28 @@ export function CourseManagementPage() { {/* Header section */}
+
+ + void handleCatalogCoursePublishStatus(nextStatus) + } + /> +

{courseDisplayName}

-

- Manage units and modules inside the {courseDisplayName} -

+ {catalogCourseDescription ? ( + + {catalogCourseDescription} + + ) : ( +

+ Manage units and modules inside {courseDisplayName} +

+ )}
diff --git a/src/pages/content-management/CourseModuleDetailPage.tsx b/src/pages/content-management/CourseModuleDetailPage.tsx index 1438804..2dd1422 100644 --- a/src/pages/content-management/CourseModuleDetailPage.tsx +++ b/src/pages/content-management/CourseModuleDetailPage.tsx @@ -23,8 +23,10 @@ import { updateExamPrepModuleLesson, deleteExamPrepModuleLesson, getExamPrepModuleLessons, - publishExamPrepModuleLesson, + getExamPrepUnitModules, setExamPrepModuleLessonAccessTier, + setExamPrepModuleLessonPublishStatus, + setExamPrepUnitModulePublishStatus, } from "../../api/courses.api"; import { uploadImageFile, uploadVideoFile } from "../../api/files.api"; import { resolveThumbnailForPreview } from "../../lib/videoPreview"; @@ -34,6 +36,7 @@ import type { } from "../../types/course.types"; import { ContentListSearchFilterBar } from "./components/ContentListSearchFilterBar"; import { ContentPageDescription } from "./components/ContentPageDescription"; +import { ContentPublishStatusChip } from "./components/ContentPublishStatusChip"; import { filterBySearchAndPublishStatus, type PublishStatusFilter, @@ -72,8 +75,16 @@ export function CourseModuleDetailPage() { moduleId: string; }>(); const parsedModuleId = Number(moduleId); + const parsedUnitId = Number(unitId); const [activeTab, setActiveTab] = useState<"video" | "practice">("video"); + const [moduleTitle, setModuleTitle] = useState("Module"); + const [moduleDescription, setModuleDescription] = useState("—"); + const [modulePublishStatus, setModulePublishStatus] = useState< + PracticePublishStatus | string | null + >(null); + const [modulePublishStatusUpdating, setModulePublishStatusUpdating] = + useState(false); const [lessonsLoading, setLessonsLoading] = useState(false); const [lessons, setLessons] = useState< Array<{ @@ -133,8 +144,42 @@ export function CourseModuleDetailPage() { const [deletingLessonId, setDeletingLessonId] = useState(null); const [deletingLesson, setDeletingLesson] = useState(false); - const moduleTitle = "Module 1: Basic Phrases"; - const moduleDescription = "Learn essential phrases for daily conversations."; + const loadModule = useCallback(async () => { + if ( + !Number.isFinite(parsedUnitId) || + parsedUnitId < 1 || + !Number.isFinite(parsedModuleId) || + parsedModuleId < 1 + ) { + return; + } + try { + const response = await getExamPrepUnitModules(parsedUnitId, { + limit: 100, + offset: 0, + }); + const rows = response.data?.data?.modules; + const list = Array.isArray(rows) ? rows : []; + const row = list.find((m) => Number(m.id) === parsedModuleId); + if (row) { + setModuleTitle(row.name?.trim() || `Module ${parsedModuleId}`); + setModuleDescription(row.description?.trim() || "—"); + setModulePublishStatus(row.publish_status ?? null); + } else { + setModuleTitle(`Module ${parsedModuleId}`); + setModuleDescription("—"); + setModulePublishStatus(null); + } + } catch (error) { + console.error(error); + setModuleTitle(`Module ${parsedModuleId}`); + setModuleDescription("—"); + } + }, [parsedModuleId, parsedUnitId]); + + useEffect(() => { + void loadModule(); + }, [loadModule]); const isHttpUrl = (value: string) => value.startsWith("http://") || value.startsWith("https://"); @@ -501,13 +546,37 @@ export function CourseModuleDetailPage() { } }; + const handleModulePublishStatus = async (nextStatus: PracticePublishStatus) => { + if (!Number.isFinite(parsedModuleId) || parsedModuleId < 1) return; + setModulePublishStatusUpdating(true); + try { + await setExamPrepUnitModulePublishStatus(parsedModuleId, { + publish_status: nextStatus, + }); + setModulePublishStatus(nextStatus); + toast.success( + nextStatus === "PUBLISHED" + ? "Module published" + : "Module saved as draft", + ); + } catch (error: unknown) { + console.error(error); + const message = + (error as { response?: { data?: { message?: string } } })?.response?.data + ?.message ?? "Failed to update module status"; + toast.error(message); + } finally { + setModulePublishStatusUpdating(false); + } + }; + const handleToggleLessonPublishStatus = async ( lessonId: number, nextStatus: PracticePublishStatus, ) => { setPublishStatusLessonId(lessonId); try { - await publishExamPrepModuleLesson(lessonId, { + await setExamPrepModuleLessonPublishStatus(lessonId, { publish_status: nextStatus, }); setLessons((prev) => @@ -582,6 +651,14 @@ export function CourseModuleDetailPage() { {/* Header section */}
+
+ void handleModulePublishStatus(nextStatus)} + /> +

{moduleTitle}

diff --git a/src/pages/content-management/LessonPracticesPage.tsx b/src/pages/content-management/LessonPracticesPage.tsx index d5c9eb9..8076c12 100644 --- a/src/pages/content-management/LessonPracticesPage.tsx +++ b/src/pages/content-management/LessonPracticesPage.tsx @@ -18,7 +18,7 @@ import { getExamPrepLessonPractices, getPracticesByParentLesson, setExamPrepPracticePublishStatus, - setParentLinkedPracticePublishStatus, + setLearnEnglishPracticePublishStatus, } from "../../api/courses.api"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; @@ -332,7 +332,7 @@ export function LessonPracticesPage() { publish_status: nextStatus, }); } else { - await setParentLinkedPracticePublishStatus(practiceId, { + await setLearnEnglishPracticePublishStatus(practiceId, { publish_status: nextStatus, }); } diff --git a/src/pages/content-management/ModuleDetailPage.tsx b/src/pages/content-management/ModuleDetailPage.tsx index fc3de2e..46fbe92 100644 --- a/src/pages/content-management/ModuleDetailPage.tsx +++ b/src/pages/content-management/ModuleDetailPage.tsx @@ -8,7 +8,7 @@ import { getPracticesByParentModule, getTopLevelCourseModules, publishTopLevelModuleLesson, - setParentLinkedPracticePublishStatus, + setLearnEnglishPracticePublishStatus, setTopLevelModuleLessonAccessTier, updateTopLevelModuleLesson, } from "../../api/courses.api"; @@ -290,7 +290,7 @@ export function ModuleDetailPage() { ) => { setPublishStatusPracticeId(practiceId); try { - await setParentLinkedPracticePublishStatus(practiceId, { + await setLearnEnglishPracticePublishStatus(practiceId, { publish_status: nextStatus, }); setPractices((prev) => diff --git a/src/pages/content-management/UnitManagementPage.tsx b/src/pages/content-management/UnitManagementPage.tsx index ac78416..6e539e8 100644 --- a/src/pages/content-management/UnitManagementPage.tsx +++ b/src/pages/content-management/UnitManagementPage.tsx @@ -27,7 +27,9 @@ import { toast } from "sonner"; import { ResolvedImage } from "../../components/media/ResolvedImage"; import { createExamPrepUnitModule, + getExamPrepCatalogUnits, getExamPrepUnitModules, + setExamPrepCatalogUnitPublishStatus, setExamPrepUnitModuleAccessTier, setExamPrepUnitModulePublishStatus, updateExamPrepUnitModule, @@ -37,6 +39,7 @@ import { uploadImageFile } from "../../api/files.api"; import { ContentPublishStatusChip } from "./components/ContentPublishStatusChip"; import { ContentAccessTierChip } from "./components/ContentAccessTierChip"; import { ContentListSearchFilterBar } from "./components/ContentListSearchFilterBar"; +import { ContentPageDescription } from "./components/ContentPageDescription"; import type { ContentAccessTier, PracticePublishStatus } from "../../types/course.types"; import { filterBySearchAndPublishStatus, @@ -51,17 +54,15 @@ export function UnitManagementPage() { unitId: string; }>(); - // Mock titles - const unitTitles: Record = { - unit1: "Greetings & Introductions", - unit2: "Speaking", - unit3: "Reading", - }; - - const unitDisplayName = - unitTitles[unitId || ""] || "Greetings & Introductions"; - const parsedUnitId = Number(unitId); + const catalogCourseId = Number(courseId); + const [unitDisplayName, setUnitDisplayName] = useState("Unit"); + const [unitDescription, setUnitDescription] = useState(""); + const [unitPublishStatus, setUnitPublishStatus] = useState< + PracticePublishStatus | string | null + >(null); + const [unitPublishStatusUpdating, setUnitPublishStatusUpdating] = + useState(false); const [addModuleOpen, setAddModuleOpen] = useState(false); const [createName, setCreateName] = useState(""); const [createThumbnail, setCreateThumbnail] = useState(""); @@ -142,6 +143,38 @@ export function UnitManagementPage() { return uploadedUrl; }; + const loadUnit = useCallback(async () => { + if ( + !Number.isFinite(catalogCourseId) || + catalogCourseId < 1 || + !Number.isFinite(parsedUnitId) || + parsedUnitId < 1 + ) { + return; + } + try { + const response = await getExamPrepCatalogUnits(catalogCourseId, { + limit: 100, + offset: 0, + }); + const rows = response.data?.data?.units; + const list = Array.isArray(rows) ? rows : []; + const row = list.find((u) => Number(u.id) === parsedUnitId); + if (row) { + setUnitDisplayName(row.name?.trim() || `Unit ${parsedUnitId}`); + setUnitDescription(row.description?.trim() || ""); + setUnitPublishStatus(row.publish_status ?? null); + } else { + setUnitDisplayName(`Unit ${parsedUnitId}`); + setUnitDescription(""); + setUnitPublishStatus(null); + } + } catch (error) { + console.error(error); + setUnitDisplayName(`Unit ${parsedUnitId}`); + } + }, [catalogCourseId, parsedUnitId]); + const loadModules = useCallback(async () => { if (!Number.isFinite(parsedUnitId) || parsedUnitId < 1) { setModules([]); @@ -184,10 +217,35 @@ export function UnitManagementPage() { } }, [parsedUnitId]); + useEffect(() => { + void loadUnit(); + }, [loadUnit]); + useEffect(() => { void loadModules(); }, [loadModules]); + const handleUnitPublishStatus = async (nextStatus: PracticePublishStatus) => { + if (!Number.isFinite(parsedUnitId) || parsedUnitId < 1) return; + setUnitPublishStatusUpdating(true); + try { + await setExamPrepCatalogUnitPublishStatus(parsedUnitId, { + publish_status: nextStatus, + }); + setUnitPublishStatus(nextStatus); + toast.success( + nextStatus === "PUBLISHED" ? "Unit published" : "Unit saved as draft", + ); + } catch (error: unknown) { + const message = + (error as { response?: { data?: { message?: string } } })?.response?.data + ?.message ?? "Failed to update unit status"; + toast.error(message); + } finally { + setUnitPublishStatusUpdating(false); + } + }; + const handleModulePublishStatus = async ( moduleId: number, nextStatus: PracticePublishStatus, @@ -530,10 +588,25 @@ export function UnitManagementPage() { {/* Header section */} -
-

- {unitDisplayName} -

+
+
+
+ void handleUnitPublishStatus(nextStatus)} + /> +
+

+ {unitDisplayName} +

+ {unitDescription ? ( + + {unitDescription} + + ) : null} +
(null); const [previewOpen, setPreviewOpen] = useState(false); - const [publishConfirmOpen, setPublishConfirmOpen] = useState(false); - const [pendingPublishStatus, setPendingPublishStatus] = - useState(null); /** Iframe players ignore URL limits in many cases — unmount after real time. */ const [iframeSessionDone, setIframeSessionDone] = useState(false); const [iframeSessionKey, setIframeSessionKey] = useState(0); @@ -152,23 +142,6 @@ export function VideoCard({ const previewLengthLabel = formatPreviewLength( DEFAULT_PREVIEW_MAX_SECONDS, ); - const requestPublishStatusChange = ( - nextStatus: PracticePublishStatus, - e?: React.MouseEvent, - ) => { - e?.stopPropagation(); - if (publishStatusUpdating) return; - setPendingPublishStatus(nextStatus); - setPublishConfirmOpen(true); - }; - - const confirmPublishStatusChange = () => { - if (!pendingPublishStatus || !onTogglePublishStatus) return; - onTogglePublishStatus(pendingPublishStatus); - setPublishConfirmOpen(false); - setPendingPublishStatus(null); - }; - const publishBadge = resolvePublishBadge( publishStatus, status, @@ -487,22 +460,32 @@ export function VideoCard({ >
{publishBadge ? ( -
+ onTogglePublishStatus ? ( + + ) : (
- {publishBadge.label} -
+ > +
+ {publishBadge.label} +
+ ) ) : (
@@ -518,42 +501,7 @@ export function VideoCard({ /> ) : null}
- {hoverModuleActions && onTogglePublishStatus ? ( - - - - - - { - requestPublishStatusChange( - publishBadge?.isPublished ? "DRAFT" : "PUBLISHED", - e, - ); - }} - > - {publishBadge?.isPublished - ? "Save as draft" - : "Publish lesson"} - - - - ) : !hoverModuleActions ? ( + {!hoverModuleActions ? (
- { - setPublishConfirmOpen(open); - if (!open) setPendingPublishStatus(null); - }} - nextStatus={pendingPublishStatus} - contentLabel="lesson" - confirming={publishStatusUpdating} - onConfirm={confirmPublishStatusChange} - /> ); }