ui redesign, markdown fix + metadata and auth header

This commit is contained in:
2026-05-09 05:09:07 +02:00
parent 7f8a66f360
commit bc6a34cf1f
42 changed files with 3093 additions and 517 deletions
+61 -64
View File
@@ -7,9 +7,7 @@ import { defaultKeymap, indentWithTab } from '@codemirror/commands';
import { vim } from '@replit/codemirror-vim';
import { search, searchKeymap } from '@codemirror/search';
import { closeBrackets } from '@codemirror/autocomplete';
import { marked } from 'marked';
import hljs from 'highlight.js';
import katex from 'katex';
import { renderMarkdown } from '../../../lib/markdown';
import { getPost, savePost, deletePost, getAssets, ApiError } from '../../../lib/api';
import type { Asset } from '../../../lib/types';
import AssetManager from './AssetManager';
@@ -58,43 +56,6 @@ const narlblogTheme = EditorView.theme({
},
}, { dark: true });
function renderMathInElement(element: HTMLElement) {
const delimiters = [
{ left: '$$', right: '$$', display: true },
{ left: '$', right: '$', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: true },
];
const walk = (node: Node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as HTMLElement;
if (el.tagName === 'CODE' || el.tagName === 'PRE') return;
for (const child of Array.from(el.childNodes)) walk(child);
} else if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent || '';
for (const { left, right, display } of delimiters) {
const idx = text.indexOf(left);
if (idx === -1) continue;
const end = text.indexOf(right, idx + left.length);
if (end === -1) continue;
const tex = text.slice(idx + left.length, end);
try {
const rendered = katex.renderToString(tex, { displayMode: display, throwOnError: false });
const span = document.createElement('span');
span.innerHTML = rendered;
const range = document.createRange();
range.setStart(node, idx);
range.setEnd(node, end + right.length);
range.deleteContents();
range.insertNode(span);
} catch { /* skip */ }
return;
}
}
};
for (let i = 0; i < 3; i++) walk(element);
}
// Compartment for hot-swapping vim mode without recreating the editor
const vimCompartment = new Compartment();
@@ -104,8 +65,12 @@ export default function Editor({ editSlug }: Props) {
const previewRef = useRef<HTMLDivElement>(null);
const previewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const updatePreviewRef = useRef<() => void>(() => {});
const today = new Date().toISOString().slice(0, 10);
const [slug, setSlug] = 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 || '');
const [alert, setAlert] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
const [showModal, setShowModal] = useState(false);
@@ -126,19 +91,7 @@ export default function Editor({ editSlug }: Props) {
const updatePreview = useCallback(() => {
if (!showPreview || !viewRef.current || !previewRef.current) return;
const content = viewRef.current.state.doc.toString();
const result = marked.parse(content);
if (typeof result === 'string') {
previewRef.current.innerHTML = result;
} else {
result.then(h => { if (previewRef.current) previewRef.current.innerHTML = h; });
}
requestAnimationFrame(() => {
if (!previewRef.current) return;
renderMathInElement(previewRef.current);
previewRef.current.querySelectorAll<HTMLElement>('pre code').forEach(block => {
hljs.highlightElement(block);
});
});
previewRef.current.innerHTML = renderMarkdown(content);
}, [showPreview]);
useEffect(() => { updatePreviewRef.current = updatePreview; }, [updatePreview]);
@@ -194,6 +147,9 @@ export default function Editor({ editSlug }: Props) {
if (!editSlug) return;
getPost(editSlug).then(post => {
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 && viewRef.current) {
viewRef.current.dispatch({
changes: { from: 0, to: viewRef.current.state.doc.length, insert: post.content },
@@ -257,11 +213,18 @@ export default function Editor({ editSlug }: Props) {
showAlertMsg('Title and content are required.', 'error');
return;
}
const tags = tagsInput
.split(',')
.map(t => t.trim())
.filter(Boolean);
try {
await savePost({
slug,
old_slug: originalSlug || null,
date,
summary: summary || null,
tags,
draft,
content,
});
showAlertMsg('Post saved!', 'success');
@@ -323,17 +286,51 @@ export default function Editor({ editSlug }: Props) {
</div>
<div className="space-y-6">
{/* Slug */}
<div>
<label className="block text-sm font-medium text-subtext1 mb-2">Post Title (URL identifier)</label>
<input
type="text"
value={slug}
onChange={e => setSlug(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"
/>
{/* Slug + 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>
<input
type="text"
value={slug}
onChange={e => setSlug(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"
/>
</div>
<div>
<label className="block text-sm font-medium text-subtext1 mb-2">Date</label>
<input
type="date"
value={date}
onChange={e => setDate(e.target.value)}
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>
</div>
{/* Tags + Draft */}
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-4 md:items-end">
<div>
<label className="block text-sm font-medium text-subtext1 mb-2">Tags (comma-separated)</label>
<input
type="text"
value={tagsInput}
onChange={e => setTagsInput(e.target.value)}
placeholder="rust, astro, design"
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>
<label className="flex items-center gap-2 px-4 py-3 bg-crust border border-surface1 rounded-lg cursor-pointer hover:border-peach/40 transition-colors select-none">
<input
type="checkbox"
checked={draft}
onChange={e => setDraft(e.target.checked)}
className="accent-peach"
/>
<span className="text-sm font-medium text-subtext1">Draft</span>
</label>
</div>
{/* Summary */}