performance improvements
This commit is contained in:
@@ -100,6 +100,14 @@ export default function Editor({ editSlug }: Props) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [uploadingCount, setUploadingCount] = useState(0);
|
||||
const dragDepthRef = useRef(0);
|
||||
const assetsCacheRef = useRef<Asset[] | null>(null);
|
||||
|
||||
async function getCachedAssets(): Promise<Asset[]> {
|
||||
if (assetsCacheRef.current) return assetsCacheRef.current;
|
||||
const assets = await getAssets();
|
||||
assetsCacheRef.current = assets;
|
||||
return assets;
|
||||
}
|
||||
|
||||
function showAlertMsg(msg: string, type: 'success' | 'error') {
|
||||
setAlert({ msg, type });
|
||||
@@ -245,7 +253,7 @@ export default function Editor({ editSlug }: Props) {
|
||||
|
||||
async function triggerAutocomplete(view: EditorView) {
|
||||
try {
|
||||
const assets = await getAssets();
|
||||
const assets = await getCachedAssets();
|
||||
setAutocompleteAssets(assets.slice(0, 8));
|
||||
const pos = view.state.selection.main.head;
|
||||
const coords = view.coordsAtPos(pos);
|
||||
@@ -292,22 +300,41 @@ export default function Editor({ editSlug }: Props) {
|
||||
return;
|
||||
}
|
||||
setUploadingCount(c => c + images.length);
|
||||
|
||||
// Fire all uploads in parallel; the browser caps per-origin concurrency.
|
||||
// Insert results in submission order so the markdown reflects user intent.
|
||||
const uploads = images.map(file =>
|
||||
uploadAsset(file).then(
|
||||
asset => ({ ok: true as const, asset }),
|
||||
err => ({ ok: false as const, err }),
|
||||
),
|
||||
);
|
||||
|
||||
let pos = typeof insertAt === 'number' ? insertAt : view.state.selection.main.head;
|
||||
for (const file of images) {
|
||||
try {
|
||||
const asset = await uploadAsset(file);
|
||||
const newAssets: Asset[] = [];
|
||||
for (const promise of uploads) {
|
||||
const result = await promise;
|
||||
setUploadingCount(c => Math.max(0, c - 1));
|
||||
if (result.ok) {
|
||||
const { asset } = result;
|
||||
newAssets.push(asset);
|
||||
const md = ``;
|
||||
const line = view.state.doc.lineAt(pos);
|
||||
const atLineEnd = pos === line.to;
|
||||
const insertText = atLineEnd ? `\n\n${md}\n` : `${md}\n\n`;
|
||||
view.dispatch({ changes: { from: pos, insert: insertText } });
|
||||
pos += insertText.length;
|
||||
} catch (e) {
|
||||
} else {
|
||||
const e = result.err;
|
||||
showAlertMsg(e instanceof ApiError ? `Upload failed: ${e.message}` : 'Upload failed.', 'error');
|
||||
} finally {
|
||||
setUploadingCount(c => Math.max(0, c - 1));
|
||||
}
|
||||
}
|
||||
|
||||
if (newAssets.length > 0) {
|
||||
assetsCacheRef.current = assetsCacheRef.current
|
||||
? [...newAssets, ...assetsCacheRef.current]
|
||||
: null;
|
||||
}
|
||||
view.focus();
|
||||
}
|
||||
|
||||
@@ -316,6 +343,11 @@ export default function Editor({ editSlug }: Props) {
|
||||
setShowModal(false);
|
||||
}
|
||||
|
||||
function closeAssetModal() {
|
||||
assetsCacheRef.current = null;
|
||||
setShowModal(false);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const content = viewRef.current?.state.doc.toString() || '';
|
||||
if (!title.trim() || !slug || !content) {
|
||||
@@ -647,7 +679,7 @@ export default function Editor({ editSlug }: Props) {
|
||||
<h2 className="font-display italic text-2xl md:text-3xl text-[var(--text)] leading-tight">Add image</h2>
|
||||
<p className="text-xs text-[var(--subtext0)] font-display italic mt-1">Click an image to insert it. Drag new files in to upload.</p>
|
||||
</div>
|
||||
<button onClick={() => setShowModal(false)} className="p-2 text-[var(--subtext0)] hover:text-[var(--red)] transition-colors">
|
||||
<button onClick={closeAssetModal} className="p-2 text-[var(--subtext0)] hover:text-[var(--red)] transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@@ -7,6 +7,10 @@ const { slug } = Astro.params;
|
||||
const API_URL = (typeof process !== 'undefined' ? process.env.PUBLIC_API_URL : import.meta.env.PUBLIC_API_URL) || 'http://localhost:3000';
|
||||
|
||||
interface CoverImage { url: string; alt: string }
|
||||
interface PostNeighbor {
|
||||
slug: string;
|
||||
title?: string;
|
||||
}
|
||||
interface PostDetail {
|
||||
slug: string;
|
||||
date: string;
|
||||
@@ -18,10 +22,8 @@ interface PostDetail {
|
||||
reading_time: number;
|
||||
cover_image?: CoverImage;
|
||||
image_count: number;
|
||||
}
|
||||
interface PostInfo {
|
||||
slug: string;
|
||||
title?: string;
|
||||
prev?: PostNeighbor;
|
||||
next?: PostNeighbor;
|
||||
}
|
||||
|
||||
function formatDate(d: string) {
|
||||
@@ -36,35 +38,26 @@ function formatSlug(s: string) {
|
||||
let post: PostDetail | null = null;
|
||||
let html = '';
|
||||
let error = '';
|
||||
let neighbors: { prev?: PostInfo; next?: PostInfo } = {};
|
||||
|
||||
try {
|
||||
const [postRes, listRes] = await Promise.all([
|
||||
fetch(`${API_URL}/api/posts/${encodeURIComponent(slug ?? '')}`),
|
||||
fetch(`${API_URL}/api/posts`),
|
||||
]);
|
||||
const postRes = await fetch(`${API_URL}/api/posts/${encodeURIComponent(slug ?? '')}`);
|
||||
if (postRes.ok) {
|
||||
post = await postRes.json();
|
||||
html = renderMarkdown(post!.content);
|
||||
} else {
|
||||
error = 'Work not found in the catalogue';
|
||||
}
|
||||
if (listRes.ok) {
|
||||
const list: PostInfo[] = await listRes.json();
|
||||
const i = list.findIndex(p => p.slug === slug);
|
||||
if (i >= 0) {
|
||||
neighbors = {
|
||||
prev: i > 0 ? list[i - 1] : undefined,
|
||||
next: i < list.length - 1 ? list[i + 1] : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const cause = (e as any)?.cause;
|
||||
error = `Could not connect to backend at ${API_URL}: ${e instanceof Error ? e.message : String(e)}${cause ? ' (Cause: ' + (cause.message || cause.code || JSON.stringify(cause)) + ')' : ''}`;
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
const neighbors = {
|
||||
prev: post?.prev,
|
||||
next: post?.next,
|
||||
};
|
||||
|
||||
const isAdmin = Astro.cookies.get('admin_session')?.value === '1';
|
||||
const displayTitle = post ? (post.title || formatSlug(post.slug)) : 'Work';
|
||||
---
|
||||
|
||||
@@ -8,10 +8,16 @@ if (!response.ok) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
return new Response(await response.blob(), {
|
||||
headers: {
|
||||
'content-type': response.headers.get('content-type') || 'application/octet-stream',
|
||||
'cache-control': 'public, max-age=3600'
|
||||
}
|
||||
});
|
||||
const headers = new Headers();
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType) headers.set('content-type', contentType);
|
||||
const contentLength = response.headers.get('content-length');
|
||||
if (contentLength) headers.set('content-length', contentLength);
|
||||
const etag = response.headers.get('etag');
|
||||
if (etag) headers.set('etag', etag);
|
||||
const lastModified = response.headers.get('last-modified');
|
||||
if (lastModified) headers.set('last-modified', lastModified);
|
||||
headers.set('cache-control', 'public, max-age=3600');
|
||||
|
||||
return new Response(response.body, { headers });
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user