// CMS content list — table of articles/pearls/quizzes/presentations // with toolbar (new article / new quiz / new pearl / new presentation // + search), per-row publish-toggle / edit / delete. Mirrors the // vanilla #lh-cms-content-list table. import { useMemo, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/lib/api'; import ConfirmModal from '@/components/ConfirmModal'; import type { CmsContentRow, ContentType } from './cms-types'; interface ContentListOk { success: true; content: CmsContentRow[] } interface Props { statusFilter: 'all' | 'published' | 'draft'; categoryFilter: number | 'all'; onEdit: (id: number) => void; onCreate: (type: ContentType) => void; } const btn = 'inline-flex items-center gap-1 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium hover:bg-muted disabled:opacity-50'; const btnPrimary = 'inline-flex items-center gap-1 rounded-md bg-primary text-primary-foreground px-3 py-1.5 text-xs font-medium disabled:opacity-50'; const tag = 'text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded border'; const TYPE_TAG: Record = { article: 'bg-blue-100 text-blue-700 border-blue-300', quiz: 'bg-amber-100 text-amber-800 border-amber-300', pearl: 'bg-purple-100 text-purple-700 border-purple-300', presentation: 'bg-emerald-100 text-emerald-800 border-emerald-300', }; export default function ContentList(props: Props) { const qc = useQueryClient(); const [search, setSearch] = useState(''); const [pendingDelete, setPendingDelete] = useState(null); const { data, isLoading } = useQuery({ queryKey: ['cms-content'], queryFn: () => api.get('/api/learning-admin/content'), }); const togglePublish = useMutation<{ success: true }, Error, { id: number; published: boolean }>({ mutationFn: ({ id, published }) => api.put('/api/learning-admin/content/' + id, { published }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['cms-content'] }); qc.invalidateQueries({ queryKey: ['cms-stats'] }); }, }); const del = useMutation<{ success: true }, Error, number>({ mutationFn: (id) => api.delete('/api/learning-admin/content/' + id), onSuccess: () => { qc.invalidateQueries({ queryKey: ['cms-content'] }); qc.invalidateQueries({ queryKey: ['cms-stats'] }); }, }); const filtered = useMemo(() => { const rows = data?.content || []; return rows.filter((r) => { if (props.statusFilter === 'published' && !r.published) return false; if (props.statusFilter === 'draft' && r.published) return false; if (props.categoryFilter !== 'all' && r.category_id !== props.categoryFilter) return false; if (search.trim()) { const q = search.trim().toLowerCase(); const hay = (r.title + ' ' + (r.subject || '') + ' ' + (r.category_name || '')).toLowerCase(); if (!hay.includes(q)) return false; } return true; }); }, [data, props.statusFilter, props.categoryFilter, search]); return (
setSearch(e.target.value)} data-testid="cms-search" />
Title Category Type Status Updated Actions
{isLoading &&
Loading…
} {!isLoading && filtered.length === 0 && (
No content matches.
)} {filtered.map((r) => (
{r.category_name || '—'} {r.content_type} {new Date(r.updated_at).toLocaleDateString()}
))}
setPendingDelete(null)} onConfirm={() => { if (pendingDelete) { del.mutate(pendingDelete.id, { onSettled: () => setPendingDelete(null) }); } }} />
); }