122 lines
3.3 KiB
TypeScript
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,
|
|
};
|
|
}
|