diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index 207e370..5284c8f 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -37,6 +37,7 @@ export default function EditorShell({ onLogout: _onLogout, initialPostId, onBack streamingContent, tokenCount, generationError, + selectedAuthor, // setters setDraft, setMeta, @@ -51,6 +52,7 @@ export default function EditorShell({ onLogout: _onLogout, initialPostId, onBack setStreamingContent, setTokenCount, setGenerationError, + setSelectedAuthor, // actions savePost, deletePost, @@ -233,6 +235,8 @@ export default function EditorShell({ onLogout: _onLogout, initialPostId, onBack draftHtml={draft} onRefreshPreview={refreshPreview} onGhostPublish={publishToGhost} + selectedAuthor={selectedAuthor} + onAuthorChange={setSelectedAuthor} /> )} diff --git a/apps/admin/src/components/steps/StepPublish.tsx b/apps/admin/src/components/steps/StepPublish.tsx index 956ebd1..237e1ec 100644 --- a/apps/admin/src/components/steps/StepPublish.tsx +++ b/apps/admin/src/components/steps/StepPublish.tsx @@ -1,5 +1,7 @@ -import { Alert, Box, Button, Stack } from '@mui/material'; +import { useState, useEffect } from 'react'; +import { Alert, Box, Button, Stack, FormControl, InputLabel, Select, MenuItem, CircularProgress } from '@mui/material'; import StepHeader from './StepHeader'; +import { getGhostAuthors, type GhostAuthor } from '../../services/ghost'; export default function StepPublish({ previewLoading, @@ -8,6 +10,8 @@ export default function StepPublish({ draftHtml, onRefreshPreview, onGhostPublish, + selectedAuthor, + onAuthorChange, }: { previewLoading: boolean; previewError: string | null; @@ -15,16 +19,72 @@ export default function StepPublish({ draftHtml: string; onRefreshPreview: () => void; onGhostPublish: (status: 'draft' | 'published') => void; + selectedAuthor?: string; + onAuthorChange?: (authorId: string) => void; }) { + const [authors, setAuthors] = useState([]); + const [loadingAuthors, setLoadingAuthors] = useState(false); + const [authorsError, setAuthorsError] = useState(null); + + // Load authors on mount + useEffect(() => { + const loadAuthors = async () => { + setLoadingAuthors(true); + setAuthorsError(null); + try { + const data = await getGhostAuthors(); + setAuthors(data); + // Auto-select first author if none selected + if (!selectedAuthor && data.length > 0 && onAuthorChange) { + onAuthorChange(data[0].id); + } + } catch (err: any) { + setAuthorsError(err?.message || 'Failed to load authors'); + } finally { + setLoadingAuthors(false); + } + }; + loadAuthors(); + }, []); return ( - + + + {onAuthorChange && ( + + Author + + + )} + {authorsError && ( + {authorsError} + )} {previewLoading && ( Generating preview… )} diff --git a/apps/admin/src/hooks/usePostEditor.ts b/apps/admin/src/hooks/usePostEditor.ts index 250e1bc..261820c 100644 --- a/apps/admin/src/hooks/usePostEditor.ts +++ b/apps/admin/src/hooks/usePostEditor.ts @@ -27,6 +27,7 @@ export function usePostEditor(initialPostId?: string | null) { const [streamingContent, setStreamingContent] = useState(''); const [tokenCount, setTokenCount] = useState(0); const [generationError, setGenerationError] = useState(''); + const [selectedAuthor, setSelectedAuthor] = useState(undefined); useEffect(() => { const savedId = initialPostId || localStorage.getItem('voxblog_post_id'); @@ -104,6 +105,7 @@ export function usePostEditor(initialPostId?: string | null) { canonical_url: meta.canonicalUrl || null, tags, status, + authors: selectedAuthor ? [selectedAuthor] : undefined, }); // Update post status after successful Ghost publish @@ -203,6 +205,7 @@ export function usePostEditor(initialPostId?: string | null) { streamingContent, tokenCount, generationError, + selectedAuthor, // setters setDraft, setMeta, @@ -218,6 +221,7 @@ export function usePostEditor(initialPostId?: string | null) { setStreamingContent, setTokenCount, setGenerationError, + setSelectedAuthor, // actions savePost, deletePost, diff --git a/apps/admin/src/services/ghost.ts b/apps/admin/src/services/ghost.ts index 6fec93f..17e3a0d 100644 --- a/apps/admin/src/services/ghost.ts +++ b/apps/admin/src/services/ghost.ts @@ -1,5 +1,12 @@ export type GhostPostStatus = 'draft' | 'published'; +export interface GhostAuthor { + id: string; + name: string; + email: string; + slug: string; +} + export async function ghostPreview(payload: { html: string; feature_image?: string }) { const res = await fetch('/api/ghost/preview', { method: 'POST', @@ -17,6 +24,7 @@ export async function ghostPublish(payload: { canonical_url?: string | null; tags: string[]; status: GhostPostStatus; + authors?: string[]; }) { const res = await fetch('/api/ghost/post', { method: 'POST', @@ -26,3 +34,10 @@ export async function ghostPublish(payload: { if (!res.ok) throw new Error(await res.text()); return res.json(); } + +export async function getGhostAuthors(): Promise { + const res = await fetch('/api/ghost/authors'); + if (!res.ok) throw new Error(await res.text()); + const data = await res.json(); + return data.authors || []; +} diff --git a/apps/api/src/ghost.ts b/apps/api/src/ghost.ts index 62c6094..5a94c8d 100644 --- a/apps/api/src/ghost.ts +++ b/apps/api/src/ghost.ts @@ -44,6 +44,7 @@ router.post('/post', async (req, res) => { feature_image, canonical_url, status, + authors, } = req.body as { id?: string; title: string; @@ -52,6 +53,7 @@ router.post('/post', async (req, res) => { feature_image?: string; canonical_url?: string; status?: 'draft' | 'published'; + authors?: string[]; }; if (!title || !html) { @@ -154,6 +156,7 @@ router.post('/post', async (req, res) => { tags: tags && Array.isArray(tags) ? tags : [], ...(rewrittenFeatureImage ? { feature_image: rewrittenFeatureImage } : {}), ...(canonical_url ? { canonical_url } : {}), + ...(authors && Array.isArray(authors) && authors.length > 0 ? { authors } : {}), }, ], }; @@ -260,4 +263,46 @@ router.post('/preview', async (req, res) => { } }); +// Get authors from Ghost +router.get('/authors', async (req, res) => { + try { + const apiUrl = process.env.GHOST_ADMIN_API_URL || ''; + const token = getGhostToken(); + + const base = apiUrl.replace(/\/$/, ''); + const url = `${base}/users/?limit=all`; + + const ghRes = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Accept-Version': 'v5.0', + Authorization: `Ghost ${token}`, + }, + }); + + if (!ghRes.ok) { + const text = await ghRes.text(); + console.error('Ghost API error fetching authors:', ghRes.status, text); + return res.status(ghRes.status).json({ error: 'Ghost API error', detail: text }); + } + + const data: any = await ghRes.json(); + const users = data?.users || []; + + // Return simplified author list + const authors = users.map((user: any) => ({ + id: user.id, + name: user.name, + email: user.email, + slug: user.slug, + })); + + return res.json({ authors }); + } catch (err: any) { + console.error('Fetch authors failed:', err); + return res.status(500).json({ error: 'Fetch authors failed', detail: err?.message || String(err) }); + } +}); + export default router;