pediatric-ai-scribe-v3/client/src/pages/Cms.tsx
Daniel 1beabe40e4 feat(cms): port Learning Hub Content Manager (admin/moderator only)
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
2026-04-24 05:24:09 +02:00

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