Closes the largest remaining gap from the vanilla→React migration.
Vanilla had ~308 lines of HTML in cms.html plus ~1000 lines of CMS
logic in learningHub.js, all gone since the vanilla deletion at
be14578. Server endpoints under /api/learning-admin survived but
had no React UI.
New page at /cms (sidebar nav adminOnly):
client/src/pages/Cms.tsx — shell, list/edit views
client/src/pages/cms/StatsBar.tsx — 6-cell metrics
client/src/pages/cms/CategoriesPanel.tsx — list + add + delete +
status & category filters
client/src/pages/cms/ContentList.tsx — table with toolbar
(new article/quiz/pearl/
presentation), search,
publish toggle, delete
client/src/pages/cms/ContentEditor.tsx — title/category/type/
subject/body/published,
embeds QuestionsEditor
when type=quiz
client/src/pages/cms/QuestionsEditor.tsx — Q+options builder
(mcq/multi/true_false)
with per-option
explanation
client/src/pages/cms/cms-types.ts — shared CMS types
Destructive actions (delete category, delete content, delete
question) all use ConfirmModal — Daniel's no-native-dialog rule.
Intentionally NOT in this first cut (each is a follow-up if used):
• AI generation panel (vanilla lh-ai-panel)
• WebDAV file picker for AI sources
• Drag-and-drop file upload for AI ingest
• Rich-text body editor — body is a textarea
• Slide editor for presentations — body holds JSON
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
// ============================================================
|
|
// CMS — Learning Hub Content Manager. Faithful port of vanilla
|
|
// public/components/cms.html + the loadCms / loadCmsContent / etc.
|
|
// section of public/js/learningHub.js (@be14578).
|
|
//
|
|
// Server gates this with moderatorMiddleware (admin OR moderator),
|
|
// so the route is mounted unconditionally and the server returns
|
|
// 403 to non-moderators. The sidebar nav link is gated by
|
|
// `me.user.role` to keep it hidden from clinicians.
|
|
//
|
|
// Sub-components live under src/pages/cms/:
|
|
// StatsBar — 6-cell metrics summary
|
|
// CategoriesPanel — category list + add/delete + filters
|
|
// ContentList — table + toolbar (new article/quiz/pearl/presentation)
|
|
// ContentEditor — title/category/type/body + per-quiz QuestionsEditor
|
|
//
|
|
// What's intentionally NOT in this first cut (each of these is a
|
|
// follow-up commit if Daniel actually starts using them):
|
|
// • AI generation panel (vanilla `lh-ai-panel`)
|
|
// • WebDAV file picker for AI sources
|
|
// • Drag-and-drop file upload for AI ingest
|
|
// • Rich-text body editor (Quill toolbar) — body is a textarea
|
|
// • Slide editor for presentations — body holds JSON for now
|
|
// ============================================================
|
|
|
|
import { useState } from 'react';
|
|
import StatsBar from './cms/StatsBar';
|
|
import CategoriesPanel from './cms/CategoriesPanel';
|
|
import ContentList from './cms/ContentList';
|
|
import ContentEditor from './cms/ContentEditor';
|
|
import type { ContentType } from './cms/cms-types';
|
|
|
|
type View = { kind: 'list' } | { kind: 'edit'; id: number | null; type: ContentType };
|
|
|
|
export default function Cms() {
|
|
const [statusFilter, setStatusFilter] = useState<'all' | 'published' | 'draft'>('all');
|
|
const [categoryFilter, setCategoryFilter] = useState<number | 'all'>('all');
|
|
const [view, setView] = useState<View>({ kind: 'list' });
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto p-6 space-y-4">
|
|
<header>
|
|
<h1 className="text-2xl font-semibold">Content Manager</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
Create and manage Learning Hub content, quizzes, and categories.
|
|
</p>
|
|
</header>
|
|
|
|
<StatsBar />
|
|
|
|
<div className="flex flex-col lg:flex-row gap-4">
|
|
<CategoriesPanel
|
|
statusFilter={statusFilter}
|
|
onStatusFilter={setStatusFilter}
|
|
categoryFilter={categoryFilter}
|
|
onCategoryFilter={setCategoryFilter}
|
|
/>
|
|
|
|
{view.kind === 'list' ? (
|
|
<ContentList
|
|
statusFilter={statusFilter}
|
|
categoryFilter={categoryFilter}
|
|
onEdit={(id) => setView({ kind: 'edit', id, type: 'article' })}
|
|
onCreate={(type) => setView({ kind: 'edit', id: null, type })}
|
|
/>
|
|
) : (
|
|
<ContentEditor
|
|
id={view.id}
|
|
initialType={view.type}
|
|
onClose={() => setView({ kind: 'list' })}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|