'use client'; import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import type { JournalEntrySummary } from '@/lib/journal'; import { serializeJournalMarkdown, sanitizeSlug } from '@/lib/journal'; import { JOURNAL_TEMPLATES } from '@/lib/journalTemplates'; import { IconTrash, IconLink, IconPlus, IconBook, IconClock, IconCalendar, IconLock, } from '../Icons'; import './journal-studio.css'; import { JournalEditor } from './JournalEditor'; interface JournalStudioProps { /** Entry to open, taken from the /admin/journal/[slug] route. */ slug?: string; /** `config.map`, passed by the server route: a map block renders only when the site has a map. */ mapEnabled?: boolean; } export function JournalStudio({ slug: activeSlug, mapEnabled }: JournalStudioProps) { const router = useRouter(); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // New Entry Modal const [showCreateModal, setShowCreateModal] = useState(false); const [newTitle, setNewTitle] = useState(''); const [newSlug, setNewSlug] = useState(''); const [creating, setCreating] = useState(false); /** null = start blank, matching the previous (only) behavior. */ const [selectedTemplateId, setSelectedTemplateId] = useState(null); const fetchEntries = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch('/api/admin/journal'); if (res.ok) { const data = await res.json(); setEntries(data.entries || []); } else { setError('Failed to load journal entries'); } } catch { setError('Failed to load journal entries'); } finally { setLoading(false); } }, []); useEffect(() => { fetchEntries(); }, [fetchEntries]); const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); if (!newTitle.trim()) return; setCreating(true); try { const slug = sanitizeSlug(newSlug || newTitle); const template = selectedTemplateId ? JOURNAL_TEMPLATES.find((t) => t.id === selectedTemplateId) : undefined; const content = template ? serializeJournalMarkdown({ frontmatter: { title: newTitle.trim(), date: new Date().toISOString().slice(0, 10), draft: true, }, blocks: template.blocks, referencedAssetIds: [], }) : undefined; const res = await fetch('/api/admin/journal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newTitle.trim(), slug, ...(content ? { content } : {}), }), }); if (res.ok) { const data = await res.json(); setShowCreateModal(false); setNewTitle(''); setNewSlug(''); setSelectedTemplateId(null); await fetchEntries(); if (data.entry?.slug) { router.push(`/admin/journal/${data.entry.slug}`); } } else { const data = await res.json(); alert(data.error || 'Failed to create journal entry'); } } catch { alert('Error creating journal entry'); } finally { setCreating(false); } }; const handleDelete = async (slug: string) => { if ( !confirm( `Delete "${slug}"?\n\nA copy is kept and can be restored from Backups on the dashboard.`, ) ) return; try { const res = await fetch(`/api/admin/journal/${slug}`, { method: 'DELETE', }); if (res.ok) { await fetchEntries(); if (activeSlug === slug) router.push('/admin/journal'); } else { alert('Failed to delete journal entry'); } } catch { alert('Error deleting journal entry'); } }; if (activeSlug) { return ( router.push('/admin/journal')} /> ); } return (

Journal & Photo Essays

Author visual stories, field notes, and longform photo essays with live preview.

{loading ? (
Loading journal entries...
) : error ? (
{error}
) : entries.length === 0 ? (

No journal entries yet

Create your first story to share travel journals or wedding reportages.

) : (
{entries.map((entry) => (
{entry.frontmatter.coverAssetId ? ( // eslint-disable-next-line @next/next/no-img-element {entry.frontmatter.title ) : ( )}

{entry.frontmatter.title || entry.slug}

/journal/{entry.slug}
{entry.frontmatter.date && ( {entry.frontmatter.date} )} {entry.readingTimeMinutes} min {entry.frontmatter.draft ? ( Draft ) : ( Published )} {entry.frontmatter.password && ( Password )}
))}
)} {/* Create Modal */} {showCreateModal && (

Create New Journal Entry

{ setNewTitle(e.target.value); if (!newSlug || newSlug === sanitizeSlug(newTitle)) { setNewSlug(sanitizeSlug(e.target.value)); } }} autoFocus required />
setNewSlug(sanitizeSlug(e.target.value))} required /> Will be accessible at /journal/{newSlug || 'slug'}
{JOURNAL_TEMPLATES.map((template) => ( ))}
)}
); }