diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx
index d1fc860..caaab48 100644
--- a/apps/admin/src/App.tsx
+++ b/apps/admin/src/App.tsx
@@ -2,6 +2,8 @@ import { useEffect, useState } from 'react';
import AuthGate from './components/AuthGate';
import EditorShell from './components/EditorShell';
import PostsList from './components/PostsList';
+import AdminTopBar from './components/layout/AdminTopBar';
+import { Box } from '@mui/material';
import './App.css';
function App() {
@@ -15,46 +17,60 @@ function App() {
const handleLogout = () => {
localStorage.removeItem('voxblog_authed');
+ localStorage.removeItem('voxblog_post_id');
localStorage.removeItem('voxblog_draft_id');
setAuthenticated(false);
setSelectedPostId(null);
};
+ const createNewPost = async () => {
+ try {
+ const res = await fetch('/api/posts', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ title: 'Untitled', contentHtml: '
', status: 'editing' }),
+ });
+ const data = await res.json();
+ if (res.ok && data.id) {
+ setSelectedPostId(data.id);
+ localStorage.setItem('voxblog_post_id', data.id);
+ } else {
+ alert('Failed to create post');
+ }
+ } catch (e: any) {
+ alert('Failed to create post: ' + (e?.message || 'unknown error'));
+ }
+ };
+
return (
{authenticated ? (
- selectedPostId ? (
-
+ setSelectedPostId(null)}
+ onNewPost={createNewPost}
+ onSettings={undefined}
onLogout={handleLogout}
- initialPostId={selectedPostId}
- onBack={() => setSelectedPostId(null)}
/>
- ) : (
- {
- setSelectedPostId(id);
- localStorage.setItem('voxblog_draft_id', id);
- }}
- onNew={async () => {
- try {
- const res = await fetch('/api/posts', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ title: 'Untitled', contentHtml: '', status: 'editing' }),
- });
- const data = await res.json();
- if (res.ok && data.id) {
- setSelectedPostId(data.id);
- localStorage.setItem('voxblog_draft_id', data.id);
- } else {
- alert('Failed to create post');
- }
- } catch (e: any) {
- alert('Failed to create post: ' + (e?.message || 'unknown error'));
- }
- }}
- />
- )
+
+ {selectedPostId ? (
+ setSelectedPostId(null)}
+ />
+ ) : (
+ {
+ setSelectedPostId(id);
+ localStorage.setItem('voxblog_post_id', id);
+ }}
+ onNew={createNewPost}
+ />
+ )}
+
+
) : (
{
localStorage.setItem('voxblog_authed', '1');
diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx
index ec5b97a..d0f64ab 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, Stepper, Step, StepLabel, StepButton } from '@mui/material';
+import { Box, Snackbar, Alert, Stepper, Step, StepLabel, StepButton, Stack, Button } from '@mui/material';
import type { RichEditorHandle } from './RichEditor';
import StepAssets from './steps/StepAssets';
import StepAiPrompt from './steps/StepAiPrompt';
@@ -9,8 +9,11 @@ import StepPublish from './steps/StepPublish';
import StepContainer from './steps/StepContainer';
import { usePostEditor } from '../hooks/usePostEditor';
import { useRef } from 'react';
+import PageContainer from './layout/PageContainer';
+import PostSidebar from './layout/PostSidebar';
+import StepNavigation from './layout/StepNavigation';
-export default function EditorShell({ onLogout, initialPostId, onBack }: { onLogout?: () => void; initialPostId?: string | null; onBack?: () => void }) {
+export default function EditorShell({ onLogout: _onLogout, initialPostId, onBack }: { onLogout?: () => void; initialPostId?: string | null; onBack?: () => void }) {
const editorRef = useRef(null);
const {
// state
@@ -57,44 +60,29 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
{toast?.message || ''}
-
- {/* Left sticky sidebar: Post controls */}
-
- Post
-
- setMeta((m) => ({ ...m, title: e.target.value }))}
- fullWidth
- />
- setPostStatus(e.target.value as any)} fullWidth>
-
-
-
-
-
-
-
-
- {postId && (
-
- )}
-
- {onBack && }
-
-
-
+ { void savePost(); }}
+ onDelete={async () => {
+ if (!postId) return;
+ if (!confirm('Delete this post? This will remove its audio clips too.')) return;
+ try {
+ await deletePost();
+ if (onBack) onBack();
+ } catch (e: any) {
+ alert('Delete failed: ' + (e?.message || 'unknown error'));
+ }
+ }}
+ onBack={onBack}
+ showDelete={!!postId}
+ />
+ }
+ >
{/* Right content: Stepper and step panels */}
@@ -175,17 +163,14 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
)}
- {/* Bottom nav fixed to panel bottom via grid */}
-
-
-
-
-
-
-
-
+ {/* Step navigation (not page navigation) */}
+ setActiveStep((s) => Math.max(0, s - 1))}
+ onNext={() => setActiveStep((s) => Math.min(5, s + 1))}
+ />
-
+
);
}
diff --git a/apps/admin/src/components/layout/AdminTopBar.tsx b/apps/admin/src/components/layout/AdminTopBar.tsx
new file mode 100644
index 0000000..03a427f
--- /dev/null
+++ b/apps/admin/src/components/layout/AdminTopBar.tsx
@@ -0,0 +1,35 @@
+import { AppBar, Box, Button, Toolbar, Typography } from '@mui/material';
+
+export default function AdminTopBar({
+ userName,
+ onGoPosts,
+ onNewPost,
+ onSettings,
+ onLogout,
+}: {
+ userName?: string;
+ onGoPosts?: () => void;
+ onNewPost?: () => void;
+ onSettings?: () => void;
+ onLogout?: () => void;
+}) {
+ return (
+
+
+
+ {userName ? `Hi, ${userName}` : 'VoxBlog Admin'}
+
+
+ {onGoPosts && }
+ {onNewPost && }
+ {onSettings && }
+ {onLogout && }
+
+
+
+ );
+}
diff --git a/apps/admin/src/components/layout/PageContainer.tsx b/apps/admin/src/components/layout/PageContainer.tsx
new file mode 100644
index 0000000..f582258
--- /dev/null
+++ b/apps/admin/src/components/layout/PageContainer.tsx
@@ -0,0 +1,17 @@
+import { Box } from '@mui/material';
+import type { ReactNode } from 'react';
+
+export default function PageContainer({ left, children, leftWidth = 300 }: { left: ReactNode; children: ReactNode; leftWidth?: number }) {
+ return (
+
+
+
+ {left}
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/apps/admin/src/components/layout/PostSidebar.tsx b/apps/admin/src/components/layout/PostSidebar.tsx
new file mode 100644
index 0000000..8145b0b
--- /dev/null
+++ b/apps/admin/src/components/layout/PostSidebar.tsx
@@ -0,0 +1,51 @@
+import { Box, Button, MenuItem, Stack, TextField, Typography } from '@mui/material';
+import type { Metadata } from '../MetadataPanel';
+
+export default function PostSidebar({
+ meta,
+ postStatus,
+ onChangeMeta,
+ onChangeStatus,
+ onSave,
+ onDelete,
+ onBack,
+ showDelete,
+}: {
+ meta: Metadata;
+ postStatus: 'inbox' | 'editing' | 'ready_for_publish' | 'published' | 'archived';
+ onChangeMeta: (m: Metadata) => void;
+ onChangeStatus: (s: 'inbox' | 'editing' | 'ready_for_publish' | 'published' | 'archived') => void;
+ onSave: () => void | Promise;
+ onDelete: () => void | Promise;
+ onBack?: () => void;
+ showDelete?: boolean;
+}) {
+ return (
+
+ Post
+
+ onChangeMeta({ ...meta, title: e.target.value })}
+ fullWidth
+ />
+ onChangeStatus(e.target.value as any)} fullWidth>
+
+
+
+
+
+
+
+
+ {showDelete && (
+
+ )}
+
+ {onBack && }
+
+
+ );
+}
diff --git a/apps/admin/src/components/layout/StepNavigation.tsx b/apps/admin/src/components/layout/StepNavigation.tsx
new file mode 100644
index 0000000..f29870f
--- /dev/null
+++ b/apps/admin/src/components/layout/StepNavigation.tsx
@@ -0,0 +1,25 @@
+import { Box, Button, Stack } from '@mui/material';
+
+export default function StepNavigation({
+ disableBack,
+ onBack,
+ onNext,
+ right,
+}: {
+ disableBack?: boolean;
+ onBack: () => void;
+ onNext: () => void;
+ right?: React.ReactNode;
+}) {
+ return (
+
+
+
+
+ {right}
+
+
+
+
+ );
+}