From eee577195748e21b486589117474507f7714804e Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 14 Apr 2026 07:48:36 -0700 Subject: [PATCH] add bulk lesson delete and preserve scroll position Enable selecting and deleting multiple lessons at once in the human language panel, and persist page scroll so users return to their previous position after reload. Made-with: Cursor --- .../content-management/HumanLanguagePage.tsx | 149 ++++++++++++++++-- 1 file changed, 138 insertions(+), 11 deletions(-) diff --git a/src/pages/content-management/HumanLanguagePage.tsx b/src/pages/content-management/HumanLanguagePage.tsx index 7264d01..af86384 100644 --- a/src/pages/content-management/HumanLanguagePage.tsx +++ b/src/pages/content-management/HumanLanguagePage.tsx @@ -72,6 +72,7 @@ import { } from "../../components/content-management/PracticeQuestionEditorFields" const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const +const HUMAN_LANGUAGE_SCROLL_KEY = "human-language-page:scroll-y" type SubModulePanelTab = "lessons" | "practices" type SubModuleCardSelection = { lessonId: number | null; practiceId: number | null } @@ -99,6 +100,16 @@ type LessonDialogState = questionSetId: number } +type LessonListItem = { + id: number + question_set_id: number + title: string + display_order: number + status: string + question_count: number + intro_video_url: string +} + type QuestionDialogState = | { open: false } | { @@ -373,6 +384,12 @@ export function HumanLanguagePage() { const [questionDetailById, setQuestionDetailById] = useState>({}) const [practiceTargetDelete, setPracticeTargetDelete] = useState<{ id: number; title: string } | null>(null) const [lessonTargetDelete, setLessonTargetDelete] = useState<{ id: number; questionSetId: number; title: string } | null>(null) + const [lessonBulkTargetDelete, setLessonBulkTargetDelete] = useState<{ + title: string + lessons: { id: number; questionSetId: number; title: string }[] + subModuleKey: string + } | null>(null) + const [selectedLessonIdsBySubModule, setSelectedLessonIdsBySubModule] = useState>({}) const [questionTargetDelete, setQuestionTargetDelete] = useState<{ id: number; practiceId: number; text: string } | null>(null) const [savingPractice, setSavingPractice] = useState(false) const [savingLesson, setSavingLesson] = useState(false) @@ -430,6 +447,12 @@ export function HumanLanguagePage() { setLoading(true) try { await loadHierarchy() + const saved = sessionStorage.getItem(HUMAN_LANGUAGE_SCROLL_KEY) + const targetY = saved ? Number(saved) : 0 + if (Number.isFinite(targetY) && targetY > 0) { + window.requestAnimationFrame(() => window.scrollTo({ top: targetY, behavior: "auto" })) + setTimeout(() => window.scrollTo({ top: targetY, behavior: "auto" }), 250) + } } finally { setLoading(false) } @@ -437,6 +460,18 @@ export function HumanLanguagePage() { run().catch(() => undefined) }, []) + useEffect(() => { + const save = () => sessionStorage.setItem(HUMAN_LANGUAGE_SCROLL_KEY, String(window.scrollY || 0)) + const onBeforeUnload = () => save() + window.addEventListener("scroll", save, { passive: true }) + window.addEventListener("beforeunload", onBeforeUnload) + return () => { + save() + window.removeEventListener("scroll", save) + window.removeEventListener("beforeunload", onBeforeUnload) + } + }, []) + const filteredSubCategories = useMemo( () => selectedSubCategoryId === "ALL" @@ -1185,6 +1220,25 @@ export function HumanLanguagePage() { } } + const handleDeleteSelectedLessonsConfirmed = async () => { + if (!lessonBulkTargetDelete || lessonBulkTargetDelete.lessons.length === 0) return + setDeletingLesson(true) + try { + for (const lesson of lessonBulkTargetDelete.lessons) { + await deleteQuestionSet(lesson.questionSetId) + } + toast.success(`${lessonBulkTargetDelete.lessons.length} lesson(s) deleted`) + setSelectedLessonIdsBySubModule((prev) => ({ ...prev, [lessonBulkTargetDelete.subModuleKey]: [] })) + setLessonBulkTargetDelete(null) + await loadHierarchy() + } catch (error) { + console.error("Failed to delete selected lessons:", error) + toast.error("Failed to delete selected lessons") + } finally { + setDeletingLesson(false) + } + } + const handleDeleteQuestionConfirmed = async () => { if (!questionTargetDelete) return setDeletingQuestion(true) @@ -1212,6 +1266,14 @@ export function HumanLanguagePage() { }) } + const toggleLessonSelection = (smKey: string, lessonId: number) => { + setSelectedLessonIdsBySubModule((prev) => { + const current = prev[smKey] ?? [] + const next = current.includes(lessonId) ? current.filter((id) => id !== lessonId) : [...current, lessonId] + return { ...prev, [smKey]: next } + }) + } + const togglePracticeCard = (smKey: string, practiceId: number) => { const currentPracticeId = subModuleCardSelection[smKey]?.practiceId ?? null const nextPracticeId = currentPracticeId === practiceId ? null : practiceId @@ -1590,7 +1652,7 @@ export function HumanLanguagePage() { const smKey = `${course.course_id}-${subModule.id}` const panelTab = subModulePanelTab[smKey] ?? "lessons" const cardSel = getSubModuleSelection(smKey) - const lessonRows = [ + const lessonRows: LessonListItem[] = [ ...(subModule.lessons ?? []).map((lesson) => ({ id: lesson.id, question_set_id: lesson.question_set_id, @@ -1619,6 +1681,8 @@ export function HumanLanguagePage() { cardSel.lessonId !== null ? lessonRows.find((v) => v.id === cardSel.lessonId) ?? null : null + const selectedLessonIds = selectedLessonIdsBySubModule[smKey] ?? [] + const selectedLessonRows = lessonRows.filter((row) => selectedLessonIds.includes(row.id)) const selectedPracticeMeta = cardSel.practiceId !== null ? practiceRows.find((p) => p.id === cardSel.practiceId) ?? null @@ -1728,16 +1792,42 @@ export function HumanLanguagePage() { New practice ) : panelTab === "lessons" ? ( - +
+ {selectedLessonRows.length > 0 ? ( + + ) : null} + +
) : null} @@ -1787,6 +1877,17 @@ export function HumanLanguagePage() { {v.question_set_id > 0 ? (
+ + + + + + !open && setPracticeTargetDelete(null)}>