feat: add post status, delete functionality and improve metadata handling in editor

This commit is contained in:
Ender 2025-10-24 15:53:34 +02:00
parent 9e82fad875
commit df1a3b726e
2 changed files with 63 additions and 10 deletions

View File

@ -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}

View File

@ -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;