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> - inbox - editing - ready_for_publish - published - archived - - - - {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> + inbox + editing + ready_for_publish + published + archived + + + + {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} + + + + + ); +}