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:
Yared Yemane 2026-06-10 05:58:19 -07:00
parent 39312bf509
commit a10d7684d5
9 changed files with 295 additions and 132 deletions

View File

@ -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",
})

View File

@ -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) =>

View File

@ -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">

View File

@ -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>

View File

@ -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,
});
}

View File

@ -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) =>

View File

@ -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}

View File

@ -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",
})
}

View File

@ -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}
/>
</>
);
}