'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import type { JournalEntrySummary } from '@/lib/journal'; import { serializeJournalMarkdown, sanitizeSlug, slugFieldValue } 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'; import { useNotify } from '../Notifications'; import { useAdminFetch } from '../useAdminFetch'; import { useContentRestored } from '../contentRestored'; import AdminLoadState from '../AdminLoadState'; import PageHeader from '../PageHeader'; import { useConfirm } from '../ConfirmDialog'; import { useModalDialog } from '@/hooks/useModalDialog'; 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 confirm = useConfirm(); const notify = useNotify(); const router = useRouter(); const list = useAdminFetch<{ entries?: JournalEntrySummary[] }>('/api/admin/journal'); const entries = list.data?.entries ?? []; const fetchEntries = list.reload; // A restored entry may be one that was deleted, or have a new title. useContentRestored(({ target }) => { if (target === 'journal') fetchEntries(); }); // 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 closeCreateModal = () => { setShowCreateModal(false); setSelectedTemplateId(null); }; const createCardRef = useModalDialog(closeCreateModal, showCreateModal); 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); fetchEntries(); if (data.entry?.slug) { router.push(`/admin/journal/${data.entry.slug}`); } } else { const data = await res.json(); notify('error', data.error || 'Failed to create journal entry'); } } catch { notify('error', 'Could not create the journal entry. Check the connection and try again.'); } finally { setCreating(false); } }; const handleDelete = async (slug: string, title?: string) => { // Named by title, as the page delete is; the slug only when there is none. const ok = await confirm({ title: `Delete “${title || slug}”?`, message: 'A copy is kept and can be restored under Backups.', confirmLabel: 'Delete entry', danger: true, }); if (!ok) return; try { const res = await fetch(`/api/admin/journal/${slug}`, { method: 'DELETE', }); if (res.ok) { fetchEntries(); if (activeSlug === slug) router.push('/admin/journal'); } else { notify('error', 'Failed to delete journal entry'); } } catch { notify('error', 'Could not delete the journal entry. Check the connection and try again.'); } }; if (activeSlug) { return ( router.push('/admin/journal')} /> ); } return (
setShowCreateModal(true)} > New Journal Entry } /> {(list.loading && !list.data) || list.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(slugFieldValue(e.target.value))} required /> Will be accessible at /journal/{newSlug ? sanitizeSlug(newSlug) : 'slug'}
{JOURNAL_TEMPLATES.map((template) => ( ))}
)}
); }