From 41f35ddca328a3d4926e6aaece417be68c39a4bb Mon Sep 17 00:00:00 2001 From: Ender Date: Fri, 24 Oct 2025 04:00:22 +0200 Subject: [PATCH] feat: add rich text editor toolbar and media library management --- PLAN.md | 18 +- apps/admin/package.json | 5 + apps/admin/src/components/EditorShell.tsx | 14 +- apps/admin/src/components/RichEditor.tsx | 19 +- apps/api/src/media.ts | 34 +- apps/api/src/storage/s3.ts | 25 +- .../31ba935b-4424-4226-9f8b-803d401022a2.json | 4 +- pnpm-lock.yaml | 1974 +++++++++++++++++ 8 files changed, 2076 insertions(+), 17 deletions(-) diff --git a/PLAN.md b/PLAN.md index 40e06fa..e0a3f6e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -27,8 +27,8 @@ Voice-first authoring tool for single-user Ghost blog. Capture audio, refine wit - [x] Surface transcript in rich editor state with status feedback. - [x] Log conversion lifecycle for debug. - **M4 · Rich Editor Enhancements** (Scope: Goal 4) - - [ ] Integrate block-based editor (e.g., TipTap/Rich text) with custom nodes. - - [ ] Implement file/image upload widget wired to storage. + - [x] Integrate block-based editor (e.g., TipTap/Rich text) with custom nodes. + - [x] Implement file/image upload widget wired to storage. - [ ] Support color picker, code blocks, and metadata fields. - **M5 · AI Editing Tools** (Scope: Goal 5) - [ ] Prompt templates for tone/style suggestions via OpenAI. @@ -38,13 +38,17 @@ Voice-first authoring tool for single-user Ghost blog. Capture audio, refine wit - [ ] Implement publish/draft triggers with status reports. - [ ] Handle tags, feature image, and canonical URL settings. - **M7 · Media Management** (Scope: Goal 7) - - [ ] Centralize media library view with reuse. + - [x] Centralize media library view with reuse. - [ ] Background cleanup/retention policies. - **M8 · UX Polish & Hardening** (Scope: Goal 8) - - [ ] Loading/error states across workflows. - - [ ] Responsive layout tuning & accessibility audit. - - [ ] Smoke test scripts for manual verification. - - [x] Recorder playback compatibility (MediaRecorder mime selection, webm/mp4). + - [ ] Redesign admin layout (persistent sidebar navigation + header actions) + - [x] RichEditor toolbar (bold/italic/underline, headings, links, lists, code) + - [x] Insert images at cursor from MediaLibrary (use editor ref instead of appending) + - [ ] Snackbar toasts for upload/transcribe/save success & errors + - [ ] Loading skeletons/placeholders for MediaLibrary and Drafts + - [ ] Responsive layout tuning & accessibility audit + - [ ] Smoke test scripts for manual verification + - [x] Recorder playback compatibility (MediaRecorder mime selection, webm/mp4) ## Environment & Tooling TODOs - **Core tooling** diff --git a/apps/admin/package.json b/apps/admin/package.json index 1967cb5..7ded928 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -14,6 +14,11 @@ "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", + "@tiptap/extension-image": "^3.7.2", + "@tiptap/extension-link": "^3.7.2", + "@tiptap/extension-placeholder": "^3.7.2", + "@tiptap/react": "^3.7.2", + "@tiptap/starter-kit": "^3.7.2", "react": "^19.1.1", "react-dom": "^19.1.1" }, diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index 6547246..bd5fece 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -2,13 +2,15 @@ import { Box, Button, Stack, Typography } from '@mui/material'; import AdminLayout from '../layout/AdminLayout'; import Recorder from '../features/recorder/Recorder'; import RichEditor from './RichEditor'; +import type { RichEditorHandle } from './RichEditor'; import MediaLibrary from './MediaLibrary'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; export default function EditorShell({ onLogout }: { onLogout?: () => void }) { const [draft, setDraft] = useState(''); const [draftId, setDraftId] = useState(null); const [drafts, setDrafts] = useState([]); + const editorRef = useRef(null); useEffect(() => { const savedId = localStorage.getItem('voxblog_draft_id'); @@ -90,7 +92,7 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) { Draft - setDraft(html)} placeholder="Write your post..." /> + setDraft(html)} placeholder="Write your post..." /> {draftId && ( @@ -99,8 +101,12 @@ export default function EditorShell({ onLogout }: { onLogout?: () => void }) { { - // naive append an image block to current HTML - setDraft((prev) => `${prev || ''}

`); + if (editorRef.current) { + editorRef.current.insertImage(url); + } else { + // fallback + setDraft((prev) => `${prev || ''}

`); + } }} /> diff --git a/apps/admin/src/components/RichEditor.tsx b/apps/admin/src/components/RichEditor.tsx index 3fac8ea..523932d 100644 --- a/apps/admin/src/components/RichEditor.tsx +++ b/apps/admin/src/components/RichEditor.tsx @@ -44,7 +44,24 @@ const RichEditor = forwardRef(({ value, onChange, place return (
- + + + + + + + + + +