Wiki Phase 2: TipTap rich-text editor + inline image uploads
Replaces the raw-HTML textarea in the wiki editor with a TipTap (ProseMirror)
WYSIWYG editor.
- new RichTextEditor component: bold/italic/strike, H2/H3, bullet+ordered
lists, blockquote, code block, divider, link, inline image, undo/redo
- generalized POST /admin/uploads (reuses the screenshot multer config) →
{ url }; the editor uploads inline images through it
- editor output still passes through the Phase 1 server-side sanitizer on
save and DOMPurify on render
- lazy-loaded as its own chunk so the public bundle doesn't ship TipTap
(public ~82kB gzip; editor chunk ~106kB gzip loaded only in admin)
- RTE styling added to theme.css (toolbar, active states, prose content)
Verified: uploads serve as images; H2/H3 + lists + link + inline image
round-trip through the WYSIWYG and render sanitized on the public page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { lazy, Suspense, useEffect, useState } from 'react'
|
||||
import Modal from '../../../components/Modal.jsx'
|
||||
import { api } from '../../../api/client.js'
|
||||
|
||||
// Admin-only and heavy (TipTap) — load as its own chunk so the public bundle
|
||||
// never pays for it.
|
||||
const RichTextEditor = lazy(() => import('../../../components/RichTextEditor.jsx'))
|
||||
|
||||
export default function WikiEditor({ slug, onClose, onSaved }) {
|
||||
const isEdit = Boolean(slug)
|
||||
const [form, setForm] = useState({
|
||||
@@ -170,10 +174,12 @@ export default function WikiEditor({ slug, onClose, onSaved }) {
|
||||
placeholder="One-line summary shown on the wiki home."
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span className="field-label">Body (HTML — use <h2> for the table of contents)</span>
|
||||
<textarea value={form.body} onChange={set('body')} className="textarea" style={{ minHeight: 260 }} />
|
||||
</label>
|
||||
<div>
|
||||
<span className="field-label">Body (use Heading 2 for table-of-contents sections)</span>
|
||||
<Suspense fallback={<span className="spin" />}>
|
||||
<RichTextEditor value={form.body} onChange={(html) => setForm((f) => ({ ...f, body: html }))} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user