ui redesign, markdown fix + metadata and auth header
This commit is contained in:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user