diff --git a/src/pages/content-management/CourseCategoryPage.tsx b/src/pages/content-management/CourseCategoryPage.tsx index fc0f0dd..e27692e 100644 --- a/src/pages/content-management/CourseCategoryPage.tsx +++ b/src/pages/content-management/CourseCategoryPage.tsx @@ -24,6 +24,8 @@ export function CourseCategoryPage() { const [newCategoryName, setNewCategoryName] = useState("") const [creating, setCreating] = useState(false) const [parentCategoryId, setParentCategoryId] = useState(null) + const [newSubCategoryName, setNewSubCategoryName] = useState("") + const [pendingSubCategories, setPendingSubCategories] = useState([]) const fetchCategories = async () => { setLoading(true) @@ -154,7 +156,7 @@ export function CourseCategoryPage() { {/* Create category dialog */} - + @@ -165,34 +167,108 @@ export function CourseCategoryPage() { -
-
- - setNewCategoryName(e.target.value)} - /> +
+
+
+ + setNewCategoryName(e.target.value)} + /> +
+
+ + +

+ When left empty, this becomes a parent category. Any sub categories you add on the + right will be created under it. +

+
-
- - + +
+
+

+ Sub categories for this category (optional) +

+ {pendingSubCategories.length > 0 && ( + + {pendingSubCategories.length} sub categor + {pendingSubCategories.length === 1 ? "y" : "ies"} to create + + )} +
+
+ setNewSubCategoryName(e.target.value)} + className="h-9 text-sm" + /> + +
+ +
+ {pendingSubCategories.length === 0 ? ( +

+ Added sub categories will appear here so you can visually confirm the structure + before saving. This is optional. +

+ ) : ( +
+ {pendingSubCategories.map((name) => ( + + ))} +
+ )} +
@@ -202,6 +278,9 @@ export function CourseCategoryPage() { onClick={() => { setCreateOpen(false) setNewCategoryName("") + setParentCategoryId(null) + setNewSubCategoryName("") + setPendingSubCategories([]) }} disabled={creating} > @@ -214,15 +293,48 @@ export function CourseCategoryPage() { if (!newCategoryName.trim()) return setCreating(true) try { - await createCourseCategory({ + const name = newCategoryName.trim() + const parentPayloadId = parentCategoryId ?? null + const parentRes = await createCourseCategory({ name: newCategoryName.trim(), - parent_id: parentCategoryId ?? null, + parent_id: parentPayloadId, }) + let createdCategoryId: number | null = null + try { + const data: any = parentRes?.data + createdCategoryId = + data?.data?.category?.id ?? + data?.data?.id ?? + data?.category?.id ?? + data?.id ?? + null + } catch { + createdCategoryId = null + } + + if (createdCategoryId && pendingSubCategories.length > 0) { + await Promise.all( + pendingSubCategories.map((subName) => + createCourseCategory({ + name: subName, + parent_id: createdCategoryId, + }), + ), + ) + } + toast.success("Category created", { - description: `"${newCategoryName.trim()}" has been added.`, + description: + pendingSubCategories.length > 0 + ? `"${name}" and ${pendingSubCategories.length} sub categor${ + pendingSubCategories.length === 1 ? "y" : "ies" + } have been added.` + : `"${name}" has been added.`, }) setNewCategoryName("") setParentCategoryId(null) + setNewSubCategoryName("") + setPendingSubCategories([]) setCreateOpen(false) fetchCategories() } catch (err: any) { diff --git a/src/pages/content-management/CourseFlowBuilderPage.tsx b/src/pages/content-management/CourseFlowBuilderPage.tsx index 1869f20..1fbe580 100644 --- a/src/pages/content-management/CourseFlowBuilderPage.tsx +++ b/src/pages/content-management/CourseFlowBuilderPage.tsx @@ -61,6 +61,9 @@ export function CourseFlowBuilderPage() { // Order of sub category ids for the selected parent (scope = sub) const [subCategoryOrder, setSubCategoryOrder] = useState([]) const [dragCategoryId, setDragCategoryId] = useState(null) + const [parentOrderDirty, setParentOrderDirty] = useState(false) + const [subOrderDirty, setSubOrderDirty] = useState(false) + const [stepsDirty, setStepsDirty] = useState(false) const parentCategories = useMemo( () => categories.filter((c) => !c.parent_id), @@ -147,6 +150,7 @@ export function CourseFlowBuilderPage() { const parsed: string[] = JSON.parse(raw) if (Array.isArray(parsed) && parsed.length > 0) { setParentCategoryOrder(parsed) + setParentOrderDirty(false) return } } @@ -154,14 +158,9 @@ export function CourseFlowBuilderPage() { // ignore } setParentCategoryOrder(parentCategories.map((c) => String(c.id))) + setParentOrderDirty(false) }, [parentCategories.length]) - // Persist parent category order - useEffect(() => { - if (parentCategoryOrder.length === 0) return - window.localStorage.setItem(PARENT_ORDER_KEY, JSON.stringify(parentCategoryOrder)) - }, [parentCategoryOrder]) - // Load sub category order for selected parent useEffect(() => { if (!selectedParentCategoryId || subCategoriesForParent.length === 0) { @@ -175,6 +174,7 @@ export function CourseFlowBuilderPage() { const parsed: string[] = JSON.parse(raw) if (Array.isArray(parsed) && parsed.length > 0) { setSubCategoryOrder(parsed) + setSubOrderDirty(false) return } } @@ -182,21 +182,14 @@ export function CourseFlowBuilderPage() { // ignore } setSubCategoryOrder(subCategoriesForParent.map((c) => String(c.id))) + setSubOrderDirty(false) }, [selectedParentCategoryId, subCategoriesForParent.length]) - // Persist sub category order for selected parent - useEffect(() => { - if (!selectedParentCategoryId || subCategoryOrder.length === 0) return - window.localStorage.setItem( - `${SUB_ORDER_KEY_PREFIX}${selectedParentCategoryId}`, - JSON.stringify(subCategoryOrder), - ) - }, [selectedParentCategoryId, subCategoryOrder]) - // Load flow steps for selected sub category only (sub category structure) useEffect(() => { if (scope !== "sub" || !selectedSubCategoryId) { setSteps([]) + setStepsDirty(false) return } const key = `subcategory_flow_${selectedSubCategoryId}` @@ -205,6 +198,7 @@ export function CourseFlowBuilderPage() { if (raw) { const parsed: FlowStep[] = JSON.parse(raw) setSteps(parsed) + setStepsDirty(false) return } } catch { @@ -212,20 +206,35 @@ export function CourseFlowBuilderPage() { } const defaults: FlowStep[] = [ - { id: `${selectedSubCategoryId}-lesson`, type: "lesson", title: "Core lessons", description: "Main learning content for this sub category." }, - { id: `${selectedSubCategoryId}-practice`, type: "practice", title: "Practice sessions", description: "Speaking or practice activities to reinforce learning." }, - { id: `${selectedSubCategoryId}-exam`, type: "exam", title: "Exam / Assessment", description: "Formal evaluation of student understanding." }, - { id: `${selectedSubCategoryId}-feedback`, type: "feedback", title: "Feedback loop", description: "Collect feedback and share results with learners." }, + { + id: `${selectedSubCategoryId}-lesson`, + type: "lesson", + title: "Core lessons", + description: "Main learning content for this sub category.", + }, + { + id: `${selectedSubCategoryId}-practice`, + type: "practice", + title: "Practice sessions", + description: "Speaking or practice activities to reinforce learning.", + }, + { + id: `${selectedSubCategoryId}-exam`, + type: "exam", + title: "Exam / Assessment", + description: "Formal evaluation of student understanding.", + }, + { + id: `${selectedSubCategoryId}-feedback`, + type: "feedback", + title: "Feedback loop", + description: "Collect feedback and share results with learners.", + }, ] setSteps(defaults) + setStepsDirty(true) }, [scope, selectedSubCategoryId]) - // Persist flow steps for selected sub category - useEffect(() => { - if (scope !== "sub" || !selectedSubCategoryId) return - window.localStorage.setItem(`subcategory_flow_${selectedSubCategoryId}`, JSON.stringify(steps)) - }, [steps, scope, selectedSubCategoryId]) - const handleReorder = (targetId: string) => { if (!dragStepId || dragStepId === targetId) return setSteps((prev) => { @@ -238,6 +247,7 @@ export function CourseFlowBuilderPage() { return copy }) setDragStepId(null) + setStepsDirty(true) } const handleReorderParentCategory = (targetId: string) => { @@ -252,6 +262,7 @@ export function CourseFlowBuilderPage() { return copy }) setDragCategoryId(null) + setParentOrderDirty(true) } const handleReorderSubCategory = (targetId: string) => { @@ -266,6 +277,28 @@ export function CourseFlowBuilderPage() { return copy }) setDragCategoryId(null) + setSubOrderDirty(true) + } + + const handleSaveParentOrder = () => { + if (orderedParentCategories.length === 0 || parentCategoryOrder.length === 0) return + window.localStorage.setItem(PARENT_ORDER_KEY, JSON.stringify(parentCategoryOrder)) + setParentOrderDirty(false) + } + + const handleSaveSubOrder = () => { + if (!selectedParentCategoryId || subCategoryOrder.length === 0) return + window.localStorage.setItem( + `${SUB_ORDER_KEY_PREFIX}${selectedParentCategoryId}`, + JSON.stringify(subCategoryOrder), + ) + setSubOrderDirty(false) + } + + const handleSaveSteps = () => { + if (scope !== "sub" || !selectedSubCategoryId) return + window.localStorage.setItem(`subcategory_flow_${selectedSubCategoryId}`, JSON.stringify(steps)) + setStepsDirty(false) } const getDefaultDescription = (type: StepType): string => { @@ -299,14 +332,17 @@ export function CourseFlowBuilderPage() { description: getDefaultDescription(type), } setSteps((prev) => [...prev, newStep]) + setStepsDirty(true) } const handleUpdateStep = (id: string, changes: Partial) => { setSteps((prev) => prev.map((s) => (s.id === id ? { ...s, ...changes } : s))) + setStepsDirty(true) } const handleRemoveStep = (id: string) => { setSteps((prev) => prev.filter((s) => s.id !== id)) + setStepsDirty(true) } if (loading) { @@ -424,12 +460,26 @@ export function CourseFlowBuilderPage() { {scope === "parent" && ( - - Parent category sequence - -

- Drag to reorder the sequence in which parent categories appear. No courses or steps—order only. -

+
+
+ + Parent category sequence + +

+ Drag to reorder the sequence in which parent categories appear. No courses or + steps—order only. +

+
+ +
{orderedParentCategories.length === 0 ? ( @@ -466,12 +516,25 @@ export function CourseFlowBuilderPage() { <> - - Sub category sequence - -

- Drag to reorder sub categories under this parent. -

+
+
+ + Sub category sequence + +

+ Drag to reorder sub categories under this parent. +

+
+ +
{orderedSubCategories.length === 0 ? ( @@ -507,12 +570,25 @@ export function CourseFlowBuilderPage() {
- - Sub category structure - -

- Courses, questions, and feedback steps for “{selectedSubCategory.name}”. -

+
+
+ + Sub category structure + +

+ Courses, questions, and feedback steps for “{selectedSubCategory.name}”. +

+
+ +
{steps.length === 0 && (