Files
narlblog/frontend/src/components/react/admin/editor/usePostMeta.ts
T
2026-05-17 14:44:45 +02:00

122 lines
3.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { ApiError, deletePost, getPost, savePost } from '../../../../lib/api';
import { confirmDialog, notify } from '../../../../lib/confirm';
import { clientSlugify } from './codemirror';
interface Opts {
editSlug?: string;
getContent: () => string;
setContent: (s: string) => void;
}
/** Post metadata form + slug derivation + load/save/delete. */
export function usePostMeta({ editSlug, getContent, setContent }: Opts) {
const today = new Date().toISOString().slice(0, 10);
const [title, setTitle] = useState('');
const [slug, setSlug] = useState(editSlug || '');
const [slugTouched, setSlugTouched] = useState(!!editSlug);
const [date, setDate] = useState(today);
const [summary, setSummary] = useState('');
const [tagsInput, setTagsInput] = useState('');
const [draft, setDraft] = useState(false);
const [originalSlug, setOriginalSlug] = useState(editSlug || '');
// Load existing post for editing.
useEffect(() => {
if (!editSlug) return;
getPost(editSlug)
.then(post => {
if (post.title) setTitle(post.title);
if (post.summary) setSummary(post.summary);
if (post.date) setDate(post.date);
if (post.tags?.length) setTagsInput(post.tags.join(', '));
setDraft(!!post.draft);
if (post.content) setContent(post.content);
})
.catch(() => notify('Failed to load post.', 'error'));
}, [editSlug, setContent]);
// Auto-derive slug from title until the user edits the slug field.
useEffect(() => {
if (slugTouched) return;
setSlug(clientSlugify(title));
}, [title, slugTouched]);
async function handleSave() {
const content = getContent();
if (!title.trim() || !slug || !content) {
notify('Title, slug, and body are required.', 'error');
return;
}
if (!/!\[[^\]]*\]\([^)]+\)/.test(content)) {
notify(
'Add at least one image before saving — drag, paste, or use the Add image button.',
'error',
);
return;
}
const tags = tagsInput
.split(',')
.map(t => t.trim())
.filter(Boolean);
try {
const saved = await savePost({
slug,
old_slug: originalSlug || null,
title: title.trim(),
date,
summary: summary || null,
tags,
draft,
content,
});
notify('Post saved!', 'success');
if (saved?.slug && saved.slug !== slug) {
setSlug(saved.slug);
setSlugTouched(true);
}
setOriginalSlug(saved?.slug ?? slug);
} catch (e) {
notify(
e instanceof ApiError ? `Error: ${e.message}` : 'Failed to connect to server.',
'error',
);
}
}
async function handleDelete() {
const target = originalSlug || slug;
const ok = await confirmDialog({
title: 'Remove from catalogue?',
message: `${target}” will be permanently removed. This cannot be undone.`,
confirmLabel: 'Remove',
});
if (!ok) return;
try {
await deletePost(target);
window.location.href = '/admin';
} catch {
notify('Error deleting post.', 'error');
}
}
return {
title,
setTitle,
slug,
setSlug,
setSlugTouched,
date,
setDate,
summary,
setSummary,
tagsInput,
setTagsInput,
draft,
setDraft,
originalSlug,
handleSave,
handleDelete,
};
}