diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index f05ca09..da5d2e9 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Stack, Typography, TextField, MenuItem, Snackbar, Alert } from '@mui/material'; +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'; @@ -16,6 +16,7 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog const [postStatus, setPostStatus] = useState<'inbox' | 'editing' | 'ready_for_publish' | 'published' | 'archived'>('editing'); const [toast, setToast] = useState<{ open: boolean; message: string; severity: 'success' | 'error' } | null>(null); const [promptText, setPromptText] = useState(''); + const [activeStep, setActiveStep] = useState(0); useEffect(() => { const savedId = initialPostId || localStorage.getItem('voxblog_draft_id'); @@ -81,6 +82,28 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog } }; + const ghostPublish = async (status: 'draft' | 'published') => { + try { + const tags = meta.tagsText.split(',').map(t => t.trim()).filter(Boolean); + const res = await fetch('/api/ghost/post', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: meta.title || 'Untitled', + html: draft, + feature_image: meta.featureImage || null, + canonical_url: meta.canonicalUrl || null, + tags, + status, + }) + }); + if (!res.ok) throw new Error(await res.text()); + setToast({ open: true, message: status === 'published' ? 'Published to Ghost' : 'Draft saved to Ghost', severity: 'success' }); + } catch (e: any) { + setToast({ open: true, message: `Ghost ${status} failed: ${e?.message || 'unknown error'}`, severity: 'error' }); + } + }; + // No inline post switching here; selection happens on Posts page return ( @@ -95,106 +118,147 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog {toast?.message || ''} - - - Post - setMeta((m) => ({ ...m, title: e.target.value }))} - sx={{ flex: 1, minWidth: 200 }} - /> - setPostStatus(e.target.value as any)} sx={{ minWidth: 200 }}> - inbox - editing - ready_for_publish - published - archived - - - - {draftId && ( - - )} - - {onBack && } - - - - editorRef.current?.insertHtmlAtCursor(html)} - initialClips={postClips} - /> - - Content - setDraft(html)} placeholder="Write your post..." /> - {draftId && ( - ID: {draftId} - )} + + {/* Left sticky sidebar: Post controls */} + + Post + + setMeta((m) => ({ ...m, title: e.target.value }))} + fullWidth + /> + setPostStatus(e.target.value as any)} fullWidth> + inbox + editing + ready_for_publish + published + archived + + + + {draftId && ( + + )} + + {onBack && } + + + {/* Right content: Stepper and step panels */} - 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." - /> + + {[ 'Assets', 'AI Prompt', 'Generate', 'Edit', 'Metadata', 'Publish' ].map((label, idx) => ( + + setActiveStep(idx)}> + {label} + + + ))} + + + {activeStep === 0 && ( + + Assets (Audio & Images) + + + editorRef.current?.insertHtmlAtCursor(html)} + initialClips={postClips} + /> + + + editorRef.current?.insertHtmlAtCursor(``)} + onSetFeature={(url) => setMeta(m => ({ ...m, featureImage: url }))} + showSetFeature + /> + + + + )} + + {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 + + AI content generation will use your prompt and assets. This step is coming soon. + + + + + + + )} + + {activeStep === 3 && ( + + Edit Content + setDraft(html)} placeholder="Write your post..." /> + {draftId && ( + ID: {draftId} + )} + + )} + + {activeStep === 4 && ( + + )} + + {activeStep === 5 && ( + + Publish + Preview below is based on current editor HTML. + + + + + + + )} + + {/* Sticky bottom nav so Back/Next don't move */} + + + + + + + + - { - try { - const tags = meta.tagsText.split(',').map(t => t.trim()).filter(Boolean); - const res = await fetch('/api/ghost/post', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - title: meta.title || 'Untitled', - html: draft || '', - tags, - feature_image: meta.featureImage || undefined, - canonical_url: meta.canonicalUrl || undefined, - status, - }), - }); - const data = await res.json(); - if (!res.ok) { - console.error('Ghost error', data); - alert('Ghost error: ' + (data?.detail || data?.error || res.status)); - } else { - alert(`${status === 'published' ? 'Published' : 'Saved post'} (id: ${data.id})`); - } - } catch (e: any) { - alert('Ghost error: ' + (e?.message || String(e))); - } - }} - /> - { - if (editorRef.current) { - editorRef.current.insertImage(url); - } else { - // fallback - setDraft((prev) => `${prev || ''}

`); - } - }} showSetFeature onSetFeature={(url) => setMeta((m) => ({ ...m, featureImage: url }))} />
); diff --git a/apps/admin/src/components/MetadataPanel.tsx b/apps/admin/src/components/MetadataPanel.tsx index a7ed7b3..c5dc45c 100644 --- a/apps/admin/src/components/MetadataPanel.tsx +++ b/apps/admin/src/components/MetadataPanel.tsx @@ -1,5 +1,4 @@ -import { Box, Button, Stack, TextField, Typography, IconButton } from '@mui/material'; -import { useState } from 'react'; +import { Box, Stack, TextField, Typography, IconButton } from '@mui/material'; export type Metadata = { title: string; @@ -8,12 +7,10 @@ export type Metadata = { featureImage?: string; }; -export default function MetadataPanel({ value, onChange, onPublish }: { +export default function MetadataPanel({ value, onChange }: { value: Metadata; onChange: (v: Metadata) => void; - onPublish: (status: 'draft' | 'published') => void; }) { - const [busy, setBusy] = useState(false); const set = (patch: Partial) => onChange({ ...value, ...patch }); @@ -42,10 +39,6 @@ export default function MetadataPanel({ value, onChange, onPublish }: {
)} - - - -
);