feat: complete v0.6 content and attachment experience

This commit is contained in:
2026-07-19 04:15:28 +08:00
parent d285f3e275
commit ed1a798587
17 changed files with 126 additions and 51 deletions
+8 -27
View File
@@ -1,34 +1,15 @@
"use client";
import { FormEvent, useState } from "react";
import { FormEvent, useEffect, useMemo, useState } from "react";
type Source = { id: number; name: string };
const draftKey = "mebbling:publish-draft";
export function PublishForm({ sources }: { sources: Source[] }) {
const [error, setError] = useState("");
const [submitting, setSubmitting] = useState(false);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setSubmitting(true); setError("");
try {
const response = await fetch("/api/posts", { method: "POST", body: new FormData(event.currentTarget), headers: { Accept: "application/json" } });
const result = await response.json();
if (!response.ok) throw new Error(result.error || "發佈失敗");
window.location.assign(`/posts/${result.id}`);
} catch (reason) {
setError(reason instanceof Error ? reason.message : "發佈失敗");
setSubmitting(false);
}
}
return <form onSubmit={submit} encType="multipart/form-data">
<label>Markdown<textarea name="content" required /></label>
<label><input name="tags" placeholder="旅行, 想法" /></label>
<label><select name="visibility" defaultValue="PUBLIC"><option value="PUBLIC"></option><option value="PROTECTED"></option><option value="PRIVATE"></option></select></label>
<label><select name="sourceId" required>{sources.map((source) => <option key={source.id} value={source.id}>{source.name}</option>)}</select></label>
<label> 10 MB<input name="attachments" type="file" multiple /></label>
{error && <p className="error">{error}</p>}
<button disabled={submitting}>{submitting ? "發佈中…" : "發佈並同步"}</button>
</form>;
const [error, setError] = useState(""); const [submitting, setSubmitting] = useState(false); const [content, setContent] = useState(""); const [tags, setTags] = useState(""); const [sourceId, setSourceId] = useState(String(sources[0]?.id || "")); const [preview, setPreview] = useState(false);
useEffect(() => { try { const saved = JSON.parse(localStorage.getItem(draftKey) || "{}"); setContent(saved.content || ""); setTags(saved.tags || ""); if (saved.sourceId && sources.some((source) => String(source.id) === saved.sourceId)) setSourceId(saved.sourceId); } catch {} }, [sources]);
useEffect(() => { localStorage.setItem(draftKey, JSON.stringify({ content, tags, sourceId })); }, [content, tags, sourceId]);
const draftState = useMemo(() => content ? "草稿已自動儲存於此瀏覽器" : "", [content]);
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setSubmitting(true); setError(""); try { const response = await fetch("/api/posts", { method: "POST", body: new FormData(event.currentTarget), headers: { Accept: "application/json" } }); const result = await response.json(); if (!response.ok) throw new Error(result.error || "發佈失敗"); localStorage.removeItem(draftKey); window.location.assign(`/posts/${result.id}`); } catch (reason) { setError(reason instanceof Error ? reason.message : "發佈失敗"); setSubmitting(false); } }
return <form onSubmit={submit} encType="multipart/form-data"><label>Markdown<textarea name="content" value={content} onChange={(event) => setContent(event.target.value)} required /></label><div className="row"><button type="button" onClick={() => setPreview(!preview)}>{preview ? "繼續編輯" : "預覽"}</button><span className="meta">{draftState}</span></div>{preview && <section className="card"><pre className="markdown">{content || "(尚無內容)"}</pre></section>}<label><input name="tags" value={tags} onChange={(event) => setTags(event.target.value)} placeholder="旅行, 想法" /></label><label><select name="visibility" defaultValue="PUBLIC"><option value="PUBLIC"></option><option value="PROTECTED"></option><option value="PRIVATE"></option></select></label><label><select name="sourceId" required value={sourceId} onChange={(event) => setSourceId(event.target.value)}>{sources.map((source) => <option key={source.id} value={source.id}>{source.name}</option>)}</select></label><label> 10 MB<input name="attachments" type="file" multiple /></label>{error && <p className="error">{error}</p>}<button disabled={submitting}>{submitting ? "發佈中…" : "發佈並同步"}</button></form>;
}