feat: add post status, delete functionality and improve metadata handling in editor
This commit is contained in:
parent
9e82fad875
commit
df1a3b726e
@ -1,4 +1,4 @@
|
||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||
import { Box, Button, Stack, Typography, TextField, MenuItem } from '@mui/material';
|
||||
import AdminLayout from '../layout/AdminLayout';
|
||||
import Recorder from '../features/recorder/Recorder';
|
||||
import RichEditor from './RichEditor';
|
||||
@ -13,6 +13,7 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
|
||||
const editorRef = useRef<RichEditorHandle | null>(null);
|
||||
const [meta, setMeta] = useState<Metadata>({ title: '', tagsText: '', canonicalUrl: '', featureImage: '' });
|
||||
const [postClips, setPostClips] = useState<Array<{ id: string; bucket: string; key: string; mime: string; transcript?: string; createdAt: string }>>([]);
|
||||
const [postStatus, setPostStatus] = useState<'inbox' | 'editing' | 'ready_for_publish' | 'published' | 'archived'>('editing');
|
||||
|
||||
useEffect(() => {
|
||||
const savedId = initialPostId || localStorage.getItem('voxblog_draft_id');
|
||||
@ -26,7 +27,17 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
|
||||
const data = await res.json();
|
||||
setDraft(data.contentHtml || '');
|
||||
setDraftId(data.id || savedId);
|
||||
localStorage.setItem('voxblog_draft_id', data.id || savedId);
|
||||
if (data.contentHtml) localStorage.setItem('voxblog_draft', data.contentHtml);
|
||||
if (Array.isArray(data.audioClips)) setPostClips(data.audioClips);
|
||||
setMeta((m) => ({
|
||||
...m,
|
||||
title: data.title || '',
|
||||
tagsText: data.tagsText || '',
|
||||
canonicalUrl: data.canonicalUrl || '',
|
||||
featureImage: data.featureImage || ''
|
||||
}));
|
||||
if (data.status) setPostStatus(data.status);
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
@ -64,9 +75,40 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
|
||||
|
||||
return (
|
||||
<AdminLayout title="VoxBlog Admin" onLogout={onLogout}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
|
||||
<Typography variant="h4">VoxBlog Editor</Typography>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2, gap: 2, flexWrap: 'wrap' }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', flex: 1, minWidth: 300 }}>
|
||||
<Typography variant="h6">Post</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Title"
|
||||
value={meta.title}
|
||||
onChange={(e) => setMeta((m) => ({ ...m, title: e.target.value }))}
|
||||
sx={{ flex: 1, minWidth: 200 }}
|
||||
/>
|
||||
<TextField size="small" select label="Status" value={postStatus} onChange={(e) => setPostStatus(e.target.value as any)} sx={{ minWidth: 200 }}>
|
||||
<MenuItem value="inbox">inbox</MenuItem>
|
||||
<MenuItem value="editing">editing</MenuItem>
|
||||
<MenuItem value="ready_for_publish">ready_for_publish</MenuItem>
|
||||
<MenuItem value="published">published</MenuItem>
|
||||
<MenuItem value="archived">archived</MenuItem>
|
||||
</TextField>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{draftId && (
|
||||
<Button color="error" variant="outlined" onClick={async () => {
|
||||
if (!draftId) return;
|
||||
if (!confirm('Delete this post? This will remove its audio clips too.')) return;
|
||||
try {
|
||||
const res = await fetch(`/api/posts/${draftId}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
localStorage.removeItem('voxblog_draft_id');
|
||||
if (onBack) onBack();
|
||||
} catch (e: any) {
|
||||
alert('Delete failed: ' + (e?.message || 'unknown error'));
|
||||
}
|
||||
}}>Delete</Button>
|
||||
)}
|
||||
<Button variant="contained" onClick={saveDraft}>Save Post</Button>
|
||||
{onBack && <Button variant="outlined" onClick={onBack}>Back to Posts</Button>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
@ -77,14 +119,11 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
|
||||
initialClips={postClips}
|
||||
/>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ mb: 1 }}>Post</Typography>
|
||||
<Typography variant="subtitle1" sx={{ mb: 1 }}>Content</Typography>
|
||||
<RichEditor ref={editorRef as any} value={draft} onChange={(html) => setDraft(html)} placeholder="Write your post..." />
|
||||
<Stack direction="row" spacing={2} sx={{ mt: 1 }}>
|
||||
<Button variant="contained" onClick={saveDraft}>Save Post</Button>
|
||||
{draftId && (
|
||||
<Typography variant="caption" sx={{ alignSelf: 'center' }}>ID: {draftId}</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{draftId && (
|
||||
<Typography variant="caption" sx={{ mt: 1, display: 'block' }}>ID: {draftId}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<MetadataPanel
|
||||
value={meta}
|
||||
|
||||
@ -160,4 +160,18 @@ router.post('/', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Delete post (and its audio clips)
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const id = req.params.id;
|
||||
if (!id) return res.status(400).json({ error: 'id is required' });
|
||||
await db.delete(audioClips).where(eq(audioClips.postId, id));
|
||||
await db.delete(posts).where(eq(posts.id, id));
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Delete post error:', err);
|
||||
return res.status(500).json({ error: 'Failed to delete post' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user