fix(admin): clickable lesson publish chips and exam-prep status APIs
Wire exam-prep and Learn English publish-status PUT helpers, load real catalog metadata on detail pages, and make lesson card publish chips interactive via ContentPublishStatusChip. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
39312bf509
commit
a10d7684d5
|
|
@ -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<UpdateParentLinkedPracticeResponse>(`/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<UpdateParentLinkedPracticeResponse>(`/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",
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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<PublishStatusFilter>("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<string, string> = {
|
||||
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 */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<ContentPublishStatusChip
|
||||
publishStatus={catalogCoursePublishStatus}
|
||||
updating={catalogCoursePublishStatusUpdating}
|
||||
contentLabel="course"
|
||||
onToggle={(nextStatus) =>
|
||||
void handleCatalogCoursePublishStatus(nextStatus)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-[28px] font-medium tracking-tight text-grayScale-900">
|
||||
{courseDisplayName}
|
||||
</h1>
|
||||
<p className="max-w-2xl text-[15px] font-medium leading-relaxed text-grayScale-500">
|
||||
Manage units and modules inside the {courseDisplayName}
|
||||
</p>
|
||||
{catalogCourseDescription ? (
|
||||
<ContentPageDescription className="text-[15px] font-medium text-grayScale-500">
|
||||
{catalogCourseDescription}
|
||||
</ContentPageDescription>
|
||||
) : (
|
||||
<p className="max-w-2xl text-[15px] font-medium leading-relaxed text-grayScale-500">
|
||||
Manage units and modules inside {courseDisplayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
|
|
|
|||
|
|
@ -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<number | null>(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 */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<ContentPublishStatusChip
|
||||
publishStatus={modulePublishStatus}
|
||||
updating={modulePublishStatusUpdating}
|
||||
contentLabel="module"
|
||||
onToggle={(nextStatus) => void handleModulePublishStatus(nextStatus)}
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-[32px] font-extrabold tracking-tight text-[#0D1421]">
|
||||
{moduleTitle}
|
||||
</h1>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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() {
|
|||
</Link>
|
||||
|
||||
{/* Header section */}
|
||||
<div className="flex items-start justify-between">
|
||||
<h1 className="text-[28px] font-medium tracking-tight text-grayScale-900">
|
||||
{unitDisplayName}
|
||||
</h1>
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<ContentPublishStatusChip
|
||||
publishStatus={unitPublishStatus}
|
||||
updating={unitPublishStatusUpdating}
|
||||
contentLabel="unit"
|
||||
onToggle={(nextStatus) => void handleUnitPublishStatus(nextStatus)}
|
||||
/>
|
||||
</div>
|
||||
<h1 className="text-[28px] font-medium tracking-tight text-grayScale-900">
|
||||
{unitDisplayName}
|
||||
</h1>
|
||||
{unitDescription ? (
|
||||
<ContentPageDescription className="text-[15px] font-medium text-grayScale-500">
|
||||
{unitDescription}
|
||||
</ContentPageDescription>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={addModuleOpen}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import { toast } from "sonner"
|
|||
import {
|
||||
getPracticesByParentCourse,
|
||||
getPracticesByParentModule,
|
||||
publishParentLinkedPractice,
|
||||
updateParentLinkedPractice,
|
||||
setLearnEnglishPracticePublishStatus,
|
||||
} from "../../../api/courses.api"
|
||||
import type { PracticeParentKind } from "../../../types/course.types"
|
||||
import { Button } from "../../../components/ui/button"
|
||||
|
|
@ -86,7 +85,9 @@ export function PublishPracticeButton({
|
|||
return
|
||||
}
|
||||
for (const practice of drafts) {
|
||||
await publishParentLinkedPractice(practice.id)
|
||||
await setLearnEnglishPracticePublishStatus(practice.id, {
|
||||
publish_status: "PUBLISHED",
|
||||
})
|
||||
}
|
||||
toast.success(
|
||||
drafts.length === 1
|
||||
|
|
@ -120,7 +121,7 @@ export function PublishPracticeButton({
|
|||
return
|
||||
}
|
||||
for (const practice of toDraft) {
|
||||
await updateParentLinkedPractice(practice.id, {
|
||||
await setLearnEnglishPracticePublishStatus(practice.id, {
|
||||
publish_status: "DRAFT",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,19 +3,12 @@ import {
|
|||
BookOpen,
|
||||
Calendar,
|
||||
Edit2,
|
||||
Loader2,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Play,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../../components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -33,12 +26,12 @@ import {
|
|||
isDirectVideoFileUrl,
|
||||
} from "../../../lib/videoPreview";
|
||||
import { PreviewLimitedFileVideo } from "./PreviewLimitedFileVideo";
|
||||
import { PublishStatusConfirmDialog } from "./PublishStatusConfirmDialog";
|
||||
import type {
|
||||
ContentAccessTier,
|
||||
PracticePublishStatus,
|
||||
} from "../../../types/course.types";
|
||||
import { ContentAccessTierChip } from "./ContentAccessTierChip";
|
||||
import { ContentPublishStatusChip } from "./ContentPublishStatusChip";
|
||||
|
||||
function resolvePublishBadge(
|
||||
publishStatus?: PracticePublishStatus | string | null,
|
||||
|
|
@ -129,9 +122,6 @@ export function VideoCard({
|
|||
number | null
|
||||
>(null);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [publishConfirmOpen, setPublishConfirmOpen] = useState(false);
|
||||
const [pendingPublishStatus, setPendingPublishStatus] =
|
||||
useState<PracticePublishStatus | null>(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({
|
|||
>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{publishBadge ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 items-center gap-1.5 rounded-full border px-3 py-1 text-[11px] font-bold uppercase tracking-wider",
|
||||
publishBadge.isPublished
|
||||
? "border-[#D1FAE5] bg-[#ECFDF5] text-[#059669]"
|
||||
: "border-[#E5E7EB] bg-grayScale-50 text-grayScale-500",
|
||||
)}
|
||||
>
|
||||
onTogglePublishStatus ? (
|
||||
<ContentPublishStatusChip
|
||||
publishStatus={publishStatus ?? publishBadge.label}
|
||||
updating={publishStatusUpdating}
|
||||
contentLabel="lesson"
|
||||
onToggle={onTogglePublishStatus}
|
||||
className="text-[11px]"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"h-1.5 w-1.5 flex-shrink-0 rounded-full",
|
||||
publishBadge.isPublished ? "bg-[#10B981]" : "bg-[#9CA3AF]",
|
||||
"flex min-w-0 items-center gap-1.5 rounded-full border px-3 py-1 text-[11px] font-bold uppercase tracking-wider",
|
||||
publishBadge.isPublished
|
||||
? "border-[#D1FAE5] bg-[#ECFDF5] text-[#059669]"
|
||||
: "border-[#E5E7EB] bg-grayScale-50 text-grayScale-500",
|
||||
)}
|
||||
/>
|
||||
{publishBadge.label}
|
||||
</div>
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-1.5 w-1.5 flex-shrink-0 rounded-full",
|
||||
publishBadge.isPublished ? "bg-[#10B981]" : "bg-[#9CA3AF]",
|
||||
)}
|
||||
/>
|
||||
{publishBadge.label}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex min-w-0 items-center gap-1.5 rounded-full border border-[#E5E7EB] bg-grayScale-50 px-3 py-1 text-[11px] font-bold uppercase tracking-wider text-grayScale-500">
|
||||
<div className="h-1.5 w-1.5 flex-shrink-0 rounded-full bg-[#9CA3AF]" />
|
||||
|
|
@ -518,42 +501,7 @@ export function VideoCard({
|
|||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{hoverModuleActions && onTogglePublishStatus ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 flex-shrink-0 rounded-full text-grayScale-400 hover:bg-grayScale-50 hover:text-grayScale-600"
|
||||
disabled={publishStatusUpdating || accessTierUpdating}
|
||||
aria-label={`Lesson options: ${title}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{publishStatusUpdating || accessTierUpdating ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<MoreVertical className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
disabled={publishStatusUpdating || accessTierUpdating}
|
||||
onClick={(e) => {
|
||||
requestPublishStatusChange(
|
||||
publishBadge?.isPublished ? "DRAFT" : "PUBLISHED",
|
||||
e,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{publishBadge?.isPublished
|
||||
? "Save as draft"
|
||||
: "Publish lesson"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : !hoverModuleActions ? (
|
||||
{!hoverModuleActions ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 w-8 flex flex-shrink-0 items-center justify-center rounded-full hover:bg-grayScale-50 transition-colors text-grayScale-400"
|
||||
|
|
@ -619,17 +567,6 @@ export function VideoCard({
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<PublishStatusConfirmDialog
|
||||
open={publishConfirmOpen}
|
||||
onOpenChange={(open) => {
|
||||
setPublishConfirmOpen(open);
|
||||
if (!open) setPendingPublishStatus(null);
|
||||
}}
|
||||
nextStatus={pendingPublishStatus}
|
||||
contentLabel="lesson"
|
||||
confirming={publishStatusUpdating}
|
||||
onConfirm={confirmPublishStatusChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user