import { useMemo, useState } from 'react'; import { EmbeddableShell } from '../components/shared/EmbeddableShell'; import { EntityListView, StatusBadge } from '../components/shared/list'; import { ButtonPrimary, LinkButtonPrimary } from '../components/shared/buttons'; import type { Column } from '../components/shared/DataTable'; import { appViewHref } from '../lib/appUrl'; import { useAdminRouting } from '../lib/adminRouting'; import { formatDisplaySlug } from '../lib/formatDisplaySlug'; import { formatPostDate } from '../lib/formatPostDate'; import type { SikshyaReactConfig, WpPost } from '../types'; import { getWpApi } from '../api'; import { NavIcon } from '../components/NavIcon'; import { FeaturedThumb } from '../components/shared/list/FeaturedThumb'; import { AddContentTypePickerModal, type ContentPickerType, defaultTitleFor, } from '../components/shared/AddContentTypePickerModal'; import { CreateCertificateModal } from '../components/shared/CreateCertificateModal'; import { term } from '../lib/terminology'; import { navIconForCurriculumRow } from '../lib/curriculumIcons'; import { contentLibraryChapterLearnFallback, contentLibraryLearnViewFallback, } from '../lib/learnContentViewUrl'; import { __ } from '../lib/i18n'; /** Map a picker selection to the WordPress post type the new item lives under. */ function postTypeForPickerType(t: ContentPickerType): 'sik_lesson' | 'sik_quiz' | 'sik_assignment' { if (t === 'quiz') return 'sik_quiz'; if (t === 'assignment') return 'sik_assignment'; return 'sik_lesson'; } /** Lesson sub-kind written into `_sikshya_lesson_type` meta (empty for non-lessons). */ function lessonSubtypeForPickerType(t: ContentPickerType): '' | 'text' | 'video' | 'live' | 'scorm' | 'h5p' { switch (t) { case 'lesson_text': return 'text'; case 'lesson_video': return 'video'; case 'lesson_live': return 'live'; case 'lesson_scorm': return 'scorm'; case 'lesson_h5p': return 'h5p'; default: return ''; } } function slugify(s: string): string { return s .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); } export function WpEntityListPage(props: { config: SikshyaReactConfig; title: string; subtitle: string; restBase: string; embedded?: boolean; }) { const { config, title, subtitle, restBase, embedded } = props; const { navigateHref } = useAdminRouting(); const newHref = appViewHref(config, 'edit-content', { post_type: restBase }); const addonsHref = appViewHref(config, 'addons'); const isLessonList = restBase === 'sik_lesson'; const isCertificateList = restBase === 'sikshya_certificate'; const isQuizList = restBase === 'sik_quiz'; const isQuestionList = restBase === 'sik_question'; const isChapterList = restBase === 'sik_chapter'; const isAssignmentList = restBase === 'sik_assignment'; const T = { lesson: term(config, 'lesson'), quiz: term(config, 'quiz'), assignment: term(config, 'assignment'), course: term(config, 'course'), }; const [addLessonOpen, setAddLessonOpen] = useState(false); const [addLessonBusy, setAddLessonBusy] = useState(false); const [addLessonError, setAddLessonError] = useState(null); const [addLessonType, setAddLessonType] = useState('lesson_text'); const [addLessonTitle, setAddLessonTitle] = useState(defaultTitleFor('lesson_text')); const [addCertificateOpen, setAddCertificateOpen] = useState(false); const openAddLesson = () => { setAddLessonError(null); setAddLessonType('lesson_text'); setAddLessonTitle(defaultTitleFor('lesson_text', config)); setAddLessonOpen(true); }; const submitNewLesson = () => { const name = addLessonTitle.trim(); if (!name) { return; } const slug = slugify(name) || undefined; const targetPostType = postTypeForPickerType(addLessonType); const lessonKind = lessonSubtypeForPickerType(addLessonType); setAddLessonBusy(true); setAddLessonError(null); void getWpApi() .post<{ id: number }>(`/${targetPostType}`, { title: name, ...(slug ? { slug } : null), status: 'draft', ...(lessonKind ? { meta: { _sikshya_lesson_type: lessonKind } } : null), }) .then((created) => { if (!created?.id) { throw new Error(__('Could not create item.', 'sikshya')); } navigateHref( appViewHref(config, 'edit-content', { post_type: targetPostType, post_id: String(created.id), }) ); }) .catch((e) => { setAddLessonError(e); }) .finally(() => { setAddLessonBusy(false); }); }; const columns: Column[] = useMemo( () => { const editHref = (id: number) => appViewHref(config, 'edit-content', { post_type: restBase, post_id: String(id) }); const previewCol: Column = { id: 'preview', header: '', columnPickerLabel: 'Image', alwaysVisible: true, defaultHidden: false, headerClassName: 'w-16', cellClassName: 'w-16', render: (r) => { return ( ); }, }; /** Featured image column for lessons, quizzes, assignments, chapters, questions. */ const thumbCol: Column = { id: 'thumb', header: '', columnPickerLabel: 'Image', alwaysVisible: true, headerClassName: 'w-16', cellClassName: 'w-16', render: (r) => { return ( ); }, }; const titleCol: Column = { id: 'title', header: 'Title', sortKey: 'title', render: (r) => { const meta = r.meta as Record | undefined; const orient = meta ? String(meta._sikshya_certificate_orientation || meta.sikshya_certificate_orientation || '') : ''; return (
{isLessonList ? ( ) : null} {isCertificateList && orient ? (
{orient}
) : null} {r.slug ? (
{formatDisplaySlug(r.slug, r.status)}
) : null}
); }, }; const courseLink = (cid: number) => cid > 0 ? ( #{cid} ) : ( '—' ); const idCol: Column = { id: 'id', header: 'ID', sortKey: 'id', alwaysVisible: true, cellClassName: 'whitespace-nowrap tabular-nums text-slate-600 dark:text-slate-400', render: (r) => r.id, }; const dateCol: Column = { id: 'date', header: 'Published', sortKey: 'date', cellClassName: 'whitespace-nowrap text-slate-600 dark:text-slate-400', render: (r) => formatPostDate(r.date), }; const detailCols: Column[] = []; if (isLessonList) { detailCols.push( { id: 'course', header: T.course, cellClassName: 'whitespace-nowrap', render: (r) => { const m = r.meta as Record | undefined; return courseLink(Number(m?._sikshya_lesson_course ?? 0)); }, }, { id: 'lesson_type', header: 'Type', cellClassName: 'whitespace-nowrap text-slate-600 dark:text-slate-400', render: (r) => { const t = String((r.meta as Record | undefined)?._sikshya_lesson_type || ''); return t || '—'; }, }, { id: 'duration', header: 'Duration', defaultHidden: true, cellClassName: 'whitespace-nowrap text-slate-600 dark:text-slate-400', render: (r) => String((r.meta as Record | undefined)?._sikshya_lesson_duration || '—'), }, { id: 'video_url', header: 'Video URL', defaultHidden: true, cellClassName: 'max-w-[14rem]', render: (r) => { const u = String((r.meta as Record | undefined)?._sikshya_lesson_video_url || ''); return u ? ( {u} ) : ( '—' ); }, } ); } else if (isQuizList) { detailCols.push( { id: 'course', header: T.course, cellClassName: 'whitespace-nowrap', render: (r) => { const m = r.meta as Record | undefined; return courseLink(Number(m?._sikshya_quiz_course ?? 0)); }, }, { id: 'passing', header: 'Pass %', cellClassName: 'whitespace-nowrap tabular-nums', render: (r) => { const v = (r.meta as Record | undefined)?._sikshya_quiz_passing_score; return v != null && String(v) !== '' ? String(v) : '—'; }, }, { id: 'time_limit', header: 'Time (min)', defaultHidden: true, cellClassName: 'whitespace-nowrap tabular-nums', render: (r) => { const v = (r.meta as Record | undefined)?._sikshya_quiz_time_limit; return v != null && String(v) !== '' && String(v) !== '0' ? String(v) : '—'; }, } ); } else if (isQuestionList) { detailCols.push( { id: 'qtype', header: 'Question type', cellClassName: 'whitespace-nowrap text-slate-600 dark:text-slate-400', render: (r) => String((r.meta as Record | undefined)?._sikshya_question_type || '—'), }, { id: 'points', header: 'Points', cellClassName: 'whitespace-nowrap tabular-nums', render: (r) => { const v = (r.meta as Record | undefined)?._sikshya_question_points; return v != null && String(v) !== '' ? String(v) : '—'; }, } ); } else if (isChapterList) { detailCols.push( { id: 'course', header: T.course, cellClassName: 'whitespace-nowrap', render: (r) => { const m = r.meta as Record | undefined; return courseLink(Number(m?._sikshya_chapter_course_id ?? 0)); }, }, { id: 'order', header: 'Order', cellClassName: 'whitespace-nowrap tabular-nums', render: (r) => { const v = (r.meta as Record | undefined)?._sikshya_chapter_order; return v != null && String(v) !== '' ? String(v) : '—'; }, } ); } else if (isAssignmentList) { detailCols.push( { id: 'course', header: T.course, cellClassName: 'whitespace-nowrap', render: (r) => { const m = r.meta as Record | undefined; return courseLink(Number(m?._sikshya_assignment_course ?? 0)); }, }, { id: 'assign_pts', header: 'Points', cellClassName: 'whitespace-nowrap tabular-nums', render: (r) => { const v = (r.meta as Record | undefined)?._sikshya_assignment_points; return v != null && String(v) !== '' ? String(v) : '—'; }, }, { id: 'assign_type', header: 'Type', cellClassName: 'whitespace-nowrap text-slate-600 dark:text-slate-400', render: (r) => String((r.meta as Record | undefined)?._sikshya_assignment_type || '—'), } ); } const rest: Column[] = [ ...detailCols, { id: 'modified', header: 'Updated', sortKey: 'modified', cellClassName: 'whitespace-nowrap text-slate-600 dark:text-slate-400', render: (r) => formatPostDate(r.modified || r.date), }, { id: 'status', header: 'Status', render: (r) => , }, ]; return isCertificateList ? [idCol, previewCol, titleCol, dateCol, ...rest] : [idCol, thumbCol, titleCol, dateCol, ...rest]; }, [config, restBase, isLessonList, isCertificateList, isQuizList, isQuestionList, isChapterList, isAssignmentList] ); const postRowActions = useMemo( () => ({ buildLeadingItems: (r: WpPost) => { const items = [ { key: 'edit', label: 'Edit', href: appViewHref(config, 'edit-content', { post_type: restBase, post_id: String(r.id) }), }, ]; // Certificate templates already expose a public preview URL via `buildViewHref`, // which renders as the standard "View" row action. Avoid adding a redundant // second button ("Preview") to keep the row actions clean. return items; }, // Certificate templates should “View” as a public hash link (preview hash stored in meta). buildViewHref: (r: WpPost) => { if (isCertificateList) { const href = String(r.sikshya_certificate_preview_url || '').trim(); // If the REST field isn't present for some reason, fall back to the standard permalink. return href || r.link || null; } // Prefer server-computed URL; client rebuild matches {@see PublicPageUrls::learnContent()} (incl. public-id segment). const learn = String(r.sikshya_learn_view_url || '').trim(); if (learn) { return learn; } const lessonish = contentLibraryLearnViewFallback(config, restBase, r); if (lessonish) { return lessonish; } if (restBase === 'sik_chapter') { const chapterLearn = contentLibraryChapterLearnFallback(config, r); if (chapterLearn) { return chapterLearn; } } return r.link; }, }), [config, restBase, isCertificateList] ); const searchPh = `Search ${title.toLowerCase()}…`; const pickerKey = `post_${restBase.replace(/[^a-z0-9_-]/gi, '_')}`; const openAddCertificate = () => setAddCertificateOpen(true); return ( + Add {T.lesson.toLowerCase()} ) : isCertificateList ? ( + Add new certificate ) : ( + Add new ) } > {isCertificateList ? ( setAddCertificateOpen(false)} /> ) : null} {isLessonList ? ( { setAddLessonType(t); setAddLessonTitle(defaultTitleFor(t, config)); }} title={addLessonTitle} onTitleChange={setAddLessonTitle} config={config} onClose={() => { if (!addLessonBusy) { setAddLessonOpen(false); } }} onSubmit={submitNewLesson} busy={addLessonBusy} error={addLessonError} /> ) : null} + Add {T.lesson.toLowerCase()} ) : isCertificateList ? ( + Add new certificate ) : ( + Add new ) } skeletonHeaders={ isCertificateList ? ['ID', '', 'Title', 'Published', 'Updated', 'Status'] : isLessonList ? ['ID', '', 'Title', 'Published', 'Course', 'Type', 'Updated', 'Status'] : isQuizList ? ['ID', '', 'Title', 'Published', 'Course', 'Pass %', 'Updated', 'Status'] : ['ID', '', 'Title', 'Published', 'Updated', 'Status'] } /> ); }