seperated url and title + escape
This commit is contained in:
@@ -59,6 +59,15 @@ const narlblogTheme = EditorView.theme({
|
||||
// Compartment for hot-swapping vim mode without recreating the editor
|
||||
const vimCompartment = new Compartment();
|
||||
|
||||
function clientSlugify(s: string): string {
|
||||
return s
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[̀-ͯ]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
export default function Editor({ editSlug }: Props) {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
@@ -66,7 +75,9 @@ export default function Editor({ editSlug }: Props) {
|
||||
const previewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const updatePreviewRef = useRef<() => void>(() => {});
|
||||
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('');
|
||||
@@ -146,6 +157,7 @@ export default function Editor({ editSlug }: Props) {
|
||||
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(', '));
|
||||
@@ -158,6 +170,12 @@ export default function Editor({ editSlug }: Props) {
|
||||
}).catch(() => showAlertMsg('Failed to load post.', 'error'));
|
||||
}, [editSlug]);
|
||||
|
||||
// Auto-derive slug from title until user edits the slug field
|
||||
useEffect(() => {
|
||||
if (slugTouched) return;
|
||||
setSlug(clientSlugify(title));
|
||||
}, [title, slugTouched]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPreview) updatePreview();
|
||||
}, [showPreview, updatePreview]);
|
||||
@@ -209,8 +227,8 @@ export default function Editor({ editSlug }: Props) {
|
||||
|
||||
async function handleSave() {
|
||||
const content = viewRef.current?.state.doc.toString() || '';
|
||||
if (!slug || !content) {
|
||||
showAlertMsg('Title and content are required.', 'error');
|
||||
if (!title.trim() || !slug || !content) {
|
||||
showAlertMsg('Title, slug, and content are required.', 'error');
|
||||
return;
|
||||
}
|
||||
const tags = tagsInput
|
||||
@@ -221,6 +239,7 @@ export default function Editor({ editSlug }: Props) {
|
||||
const saved = await savePost({
|
||||
slug,
|
||||
old_slug: originalSlug || null,
|
||||
title: title.trim(),
|
||||
date,
|
||||
summary: summary || null,
|
||||
tags,
|
||||
@@ -228,7 +247,10 @@ export default function Editor({ editSlug }: Props) {
|
||||
content,
|
||||
});
|
||||
showAlertMsg('Post saved!', 'success');
|
||||
if (saved?.slug && saved.slug !== slug) setSlug(saved.slug);
|
||||
if (saved?.slug && saved.slug !== slug) {
|
||||
setSlug(saved.slug);
|
||||
setSlugTouched(true);
|
||||
}
|
||||
setOriginalSlug(saved?.slug ?? slug);
|
||||
} catch (e) {
|
||||
showAlertMsg(e instanceof ApiError ? `Error: ${e.message}` : 'Failed to connect to server.', 'error');
|
||||
@@ -287,17 +309,17 @@ export default function Editor({ editSlug }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Slug + Date */}
|
||||
{/* Title + Date */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_180px] gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-subtext1 mb-2">Post Title (URL identifier)</label>
|
||||
<label className="block text-sm font-medium text-subtext1 mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={slug}
|
||||
onChange={e => setSlug(e.target.value)}
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
required
|
||||
placeholder="my-awesome-post"
|
||||
className="w-full bg-crust border border-surface1 rounded-lg px-4 py-3 text-text focus:outline-none focus:border-mauve transition-colors font-mono"
|
||||
placeholder="My Awesome Post"
|
||||
className="w-full bg-crust border border-surface1 rounded-lg px-4 py-3 text-text focus:outline-none focus:border-mauve transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -311,6 +333,21 @@ export default function Editor({ editSlug }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slug */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-subtext1 mb-2">
|
||||
Slug <span className="text-overlay0 font-normal">(URL identifier — auto-derived from title)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={slug}
|
||||
onChange={e => { setSlug(e.target.value); setSlugTouched(true); }}
|
||||
required
|
||||
placeholder="my-awesome-post"
|
||||
className="w-full bg-crust border border-surface1 rounded-lg px-4 py-3 text-text focus:outline-none focus:border-mauve transition-colors font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags + Draft */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-4 md:items-end">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user