From 7ca9b130c3cf6138477c1b4a46d9b7fc8b80e2b7 Mon Sep 17 00:00:00 2001 From: Ender Date: Fri, 24 Oct 2025 20:18:03 +0200 Subject: [PATCH] refactor: split editor shell into separate step components with shared interfaces --- apps/admin/src/components/EditorShell.tsx | 203 +++++------------- .../src/components/steps/SelectedImages.tsx | 26 +++ .../src/components/steps/StepAiPrompt.tsx | 24 +++ .../admin/src/components/steps/StepAssets.tsx | 41 ++++ .../src/components/steps/StepContainer.tsx | 10 + apps/admin/src/components/steps/StepEdit.tsx | 32 +++ .../src/components/steps/StepGenerate.tsx | 64 ++++++ .../src/components/steps/StepMetadata.tsx | 10 + .../src/components/steps/StepPublish.tsx | 57 +++++ 9 files changed, 315 insertions(+), 152 deletions(-) create mode 100644 apps/admin/src/components/steps/SelectedImages.tsx create mode 100644 apps/admin/src/components/steps/StepAiPrompt.tsx create mode 100644 apps/admin/src/components/steps/StepAssets.tsx create mode 100644 apps/admin/src/components/steps/StepContainer.tsx create mode 100644 apps/admin/src/components/steps/StepEdit.tsx create mode 100644 apps/admin/src/components/steps/StepGenerate.tsx create mode 100644 apps/admin/src/components/steps/StepMetadata.tsx create mode 100644 apps/admin/src/components/steps/StepPublish.tsx diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index 19092a8..5afcf82 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -1,10 +1,14 @@ import { Box, Button, Stack, Typography, TextField, MenuItem, Snackbar, Alert, Stepper, Step, StepLabel, StepButton } 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 MetadataPanel, { type Metadata } from './MetadataPanel'; +import { type Metadata } from './MetadataPanel'; +import StepAssets from './steps/StepAssets'; +import StepAiPrompt from './steps/StepAiPrompt'; +import StepGenerate from './steps/StepGenerate'; +import StepEdit from './steps/StepEdit'; +import StepMetadata from './steps/StepMetadata'; +import StepPublish from './steps/StepPublish'; +import StepContainer from './steps/StepContainer'; import { useEffect, useRef, useState } from 'react'; export default function EditorShell({ onLogout, initialPostId, onBack }: { onLogout?: () => void; initialPostId?: string | null; onBack?: () => void }) { @@ -208,174 +212,69 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog {activeStep === 0 && ( - - Assets (Audio & Images) - - - editorRef.current?.insertHtmlAtCursor(html)} - initialClips={postClips} - /> - - - { - if (editorRef.current) { - editorRef.current.insertHtmlAtCursor(``); - } else { - setDraft((prev) => `${prev || ''}

`); - } - }} - onSetFeature={(url) => setMeta(m => ({ ...m, featureImage: url }))} - showSetFeature - /> -
-
-
+ + editorRef.current?.insertHtmlAtCursor(html)} + onInsertImage={(url: string) => { + if (editorRef.current) { + editorRef.current.insertHtmlAtCursor(``); + } else { + setDraft((prev) => `${prev || ''}

`); + } + }} + onSetFeature={(url: string) => setMeta(m => ({ ...m, featureImage: url }))} + /> +
)} {activeStep === 1 && ( - - AI Prompt - setPromptText(e.target.value)} - fullWidth - multiline - minRows={6} - placeholder="Describe the goal, audience, tone, outline, and reference transcript/image context to guide AI content generation." - /> - + + + )} {activeStep === 2 && ( - - Generate - - Select images as generation assets, review audio transcriptions, and set the prompt to guide AI. - - - {/* Audio transcriptions in order */} - - Audio Transcriptions - - {[...postClips].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()).map((clip, idx) => ( - - #{idx + 1} · {new Date(clip.createdAt).toLocaleString()} - {clip.transcript || '(no transcript yet)'} - - ))} - {postClips.length === 0 && ( - (No audio clips) - )} - - - - {/* Images selected for generation */} - - Selected Images - - {genImageKeys.map((k) => ( - - {k.split('/').slice(-1)[0]} - - - ))} - {genImageKeys.length === 0 && ( - (No images selected) - )} - - - - {/* Media library for selecting images */} - + - - {/* AI prompt used for generation */} - - AI Prompt - setPromptText(e.target.value)} - fullWidth - multiline - minRows={4} - /> - - - + )} {activeStep === 3 && ( - - Edit Content - - setDraft(html)} placeholder="Write your post..." /> - - {draftId && ( - ID: {draftId} - )} - + + + )} {activeStep === 4 && ( - + + + )} {activeStep === 5 && ( - - Publish - - Preview reflects Ghost media URL rewriting. Layout may differ from your Ghost theme. - - - - - - {previewLoading && ( - Generating preview… - )} - {previewError && ( - {previewError} - )} - {!previewLoading && !previewError && ( - - )} - - - - - + + + )} {/* Sticky bottom nav so Back/Next don't move */} diff --git a/apps/admin/src/components/steps/SelectedImages.tsx b/apps/admin/src/components/steps/SelectedImages.tsx new file mode 100644 index 0000000..08a9cd2 --- /dev/null +++ b/apps/admin/src/components/steps/SelectedImages.tsx @@ -0,0 +1,26 @@ +import { Box, Button, Typography } from '@mui/material'; + +export default function SelectedImages({ + imageKeys, + onRemove, +}: { + imageKeys: string[]; + onRemove: (key: string) => void; +}) { + return ( + + Selected Images + + {imageKeys.map((k) => ( + + {k.split('/').slice(-1)[0]} + + + ))} + {imageKeys.length === 0 && ( + (No images selected) + )} + + + ); +} diff --git a/apps/admin/src/components/steps/StepAiPrompt.tsx b/apps/admin/src/components/steps/StepAiPrompt.tsx new file mode 100644 index 0000000..7efa112 --- /dev/null +++ b/apps/admin/src/components/steps/StepAiPrompt.tsx @@ -0,0 +1,24 @@ +import { Box, TextField, Typography } from '@mui/material'; + +export default function StepAiPrompt({ + promptText, + onChangePrompt, +}: { + promptText: string; + onChangePrompt: (v: string) => void; +}) { + return ( + + AI Prompt + onChangePrompt(e.target.value)} + fullWidth + multiline + minRows={6} + placeholder="Describe the goal, audience, tone, outline, and reference transcript/image context to guide AI content generation." + /> + + ); +} diff --git a/apps/admin/src/components/steps/StepAssets.tsx b/apps/admin/src/components/steps/StepAssets.tsx new file mode 100644 index 0000000..b232e51 --- /dev/null +++ b/apps/admin/src/components/steps/StepAssets.tsx @@ -0,0 +1,41 @@ +import { Box, Stack, Typography } from '@mui/material'; +import Recorder from '../../features/recorder/Recorder'; +import MediaLibrary from '../MediaLibrary'; + +export type Clip = { id: string; bucket: string; key: string; mime: string; transcript?: string; createdAt: string }; + +export default function StepAssets({ + draftId, + postClips, + onInsertAtCursor, + onInsertImage, + onSetFeature, +}: { + draftId?: string | null; + postClips: Clip[]; + onInsertAtCursor: (html: string) => void; + onInsertImage: (url: string) => void; + onSetFeature: (url: string) => void; +}) { + return ( + + Assets (Audio & Images) + + + + + + + + + + ); +} diff --git a/apps/admin/src/components/steps/StepContainer.tsx b/apps/admin/src/components/steps/StepContainer.tsx new file mode 100644 index 0000000..fdb4ae8 --- /dev/null +++ b/apps/admin/src/components/steps/StepContainer.tsx @@ -0,0 +1,10 @@ +import { Box, type SxProps, type Theme } from '@mui/material'; +import type { PropsWithChildren } from 'react'; + +export default function StepContainer({ children, sx }: PropsWithChildren<{ sx?: SxProps }>) { + return ( + + {children} + + ); +} diff --git a/apps/admin/src/components/steps/StepEdit.tsx b/apps/admin/src/components/steps/StepEdit.tsx new file mode 100644 index 0000000..ea50551 --- /dev/null +++ b/apps/admin/src/components/steps/StepEdit.tsx @@ -0,0 +1,32 @@ +import { Box, Typography } from '@mui/material'; +import RichEditor, { type RichEditorHandle } from '../RichEditor'; +import type { ForwardedRef } from 'react'; + +export default function StepEdit({ + editorRef, + draftHtml, + onChangeDraft, + draftId, +}: { + editorRef: ForwardedRef | any; + draftHtml: string; + onChangeDraft: (html: string) => void; + draftId?: string | null; +}) { + return ( + + Edit Content + + + + {draftId && ( + ID: {draftId} + )} + + ); +} diff --git a/apps/admin/src/components/steps/StepGenerate.tsx b/apps/admin/src/components/steps/StepGenerate.tsx new file mode 100644 index 0000000..7cf048f --- /dev/null +++ b/apps/admin/src/components/steps/StepGenerate.tsx @@ -0,0 +1,64 @@ +import { Box, Stack, TextField, Typography } from '@mui/material'; +import MediaLibrary from '../MediaLibrary'; +import SelectedImages from './SelectedImages'; +import type { Clip } from './StepAssets'; + +export default function StepGenerate({ + postClips, + genImageKeys, + onToggleGenImage, + promptText, + onChangePrompt, +}: { + postClips: Clip[]; + genImageKeys: string[]; + onToggleGenImage: (key: string) => void; + promptText: string; + onChangePrompt: (v: string) => void; +}) { + return ( + + Generate + + Select images as generation assets, review audio transcriptions, and set the prompt to guide AI. + + + {/* Audio transcriptions in order */} + + Audio Transcriptions + + {[...postClips] + .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) + .map((clip, idx) => ( + + #{idx + 1} · {new Date(clip.createdAt).toLocaleString()} + {clip.transcript || '(no transcript yet)'} + + ))} + {postClips.length === 0 && ( + (No audio clips) + )} + + + + {/* Selected images */} + + + {/* Media library */} + + + {/* Prompt */} + + AI Prompt + onChangePrompt(e.target.value)} + fullWidth + multiline + minRows={4} + /> + + + ); +} diff --git a/apps/admin/src/components/steps/StepMetadata.tsx b/apps/admin/src/components/steps/StepMetadata.tsx new file mode 100644 index 0000000..30e6bd2 --- /dev/null +++ b/apps/admin/src/components/steps/StepMetadata.tsx @@ -0,0 +1,10 @@ +import { Box } from '@mui/material'; +import MetadataPanel, { type Metadata } from '../MetadataPanel'; + +export default function StepMetadata({ value, onChange }: { value: Metadata; onChange: (v: Metadata) => void }) { + return ( + + + + ); +} diff --git a/apps/admin/src/components/steps/StepPublish.tsx b/apps/admin/src/components/steps/StepPublish.tsx new file mode 100644 index 0000000..0652856 --- /dev/null +++ b/apps/admin/src/components/steps/StepPublish.tsx @@ -0,0 +1,57 @@ +import { Alert, Box, Button, Stack, Typography } from '@mui/material'; + +export default function StepPublish({ + previewLoading, + previewError, + previewHtml, + draftHtml, + onRefreshPreview, + onSaveDraft, + onGhostPublish, +}: { + previewLoading: boolean; + previewError: string | null; + previewHtml: string; + draftHtml: string; + onRefreshPreview: () => void; + onSaveDraft: () => void; + onGhostPublish: (status: 'draft' | 'published') => void; +}) { + return ( + + Publish + + Preview reflects Ghost media URL rewriting. Layout may differ from your Ghost theme. + + + + + + {previewLoading && ( + Generating preview… + )} + {previewError && ( + {previewError} + )} + {!previewLoading && !previewError && ( + + )} + + + + + + ); +}