108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
|
|
import { useState } from 'react'
|
|||
|
|
import { createPost, updatePost, type Post, type PostError } from '../api/posts'
|
|||
|
|
import { Markdown } from './Markdown'
|
|||
|
|
|
|||
|
|
interface PostEditorProps {
|
|||
|
|
token: string
|
|||
|
|
post?: Post
|
|||
|
|
onSaved: () => void
|
|||
|
|
onCancel: () => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const ERROR_TEXT: Record<PostError, string> = {
|
|||
|
|
full: '文章数已达上限(200 篇)',
|
|||
|
|
'too-large': '正文太长了(上限 64KB)',
|
|||
|
|
'bad-input': '标题不能为空,标签也别太长',
|
|||
|
|
failed: '保存失败,稍后再试',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function PostEditor({ token, post, onSaved, onCancel }: PostEditorProps) {
|
|||
|
|
const [title, setTitle] = useState(post?.title ?? '')
|
|||
|
|
const [tag, setTag] = useState(post?.tag ?? '')
|
|||
|
|
const [body, setBody] = useState(post?.body ?? '')
|
|||
|
|
const [preview, setPreview] = useState(false)
|
|||
|
|
const [saving, setSaving] = useState(false)
|
|||
|
|
const [error, setError] = useState<string | null>(null)
|
|||
|
|
|
|||
|
|
const save = async () => {
|
|||
|
|
setSaving(true)
|
|||
|
|
setError(null)
|
|||
|
|
try {
|
|||
|
|
const input = { title, tag, body }
|
|||
|
|
if (post) await updatePost(token, post.id, input)
|
|||
|
|
else await createPost(token, input)
|
|||
|
|
onSaved()
|
|||
|
|
} catch (e) {
|
|||
|
|
const reason = (e as Error).message as PostError
|
|||
|
|
setError(ERROR_TEXT[reason] ?? ERROR_TEXT.failed)
|
|||
|
|
setSaving(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<section className="w-full px-6 py-14">
|
|||
|
|
<div className="mb-8 flex items-center justify-between">
|
|||
|
|
<h2 className="text-xs font-medium uppercase tracking-[0.3em] text-muted">
|
|||
|
|
{post ? 'Edit post' : 'New post'}
|
|||
|
|
</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setPreview((p) => !p)}
|
|||
|
|
className="rounded-full border border-line px-4 py-1.5 text-xs font-medium uppercase tracking-[0.16em] text-muted transition-colors hover:border-accent hover:text-accent"
|
|||
|
|
>
|
|||
|
|
{preview ? 'Write' : 'Preview'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{preview ? (
|
|||
|
|
<article>
|
|||
|
|
<h1 className="mb-6 font-display text-3xl font-medium text-text">
|
|||
|
|
{title || '(无标题)'}
|
|||
|
|
</h1>
|
|||
|
|
<Markdown>{body || '_正文为空_'}</Markdown>
|
|||
|
|
</article>
|
|||
|
|
) : (
|
|||
|
|
<div className="flex flex-col gap-4">
|
|||
|
|
<input
|
|||
|
|
value={title}
|
|||
|
|
onChange={(e) => setTitle(e.target.value)}
|
|||
|
|
placeholder="标题"
|
|||
|
|
className="w-full rounded-lg border border-line bg-surface px-4 py-3 font-display text-2xl text-text outline-none focus:border-accent"
|
|||
|
|
/>
|
|||
|
|
<input
|
|||
|
|
value={tag}
|
|||
|
|
onChange={(e) => setTag(e.target.value)}
|
|||
|
|
placeholder="标签(如 Rust、随笔)"
|
|||
|
|
className="w-full rounded-lg border border-line bg-surface px-4 py-2 text-sm text-text outline-none focus:border-accent"
|
|||
|
|
/>
|
|||
|
|
<textarea
|
|||
|
|
value={body}
|
|||
|
|
onChange={(e) => setBody(e.target.value)}
|
|||
|
|
placeholder="正文,支持 Markdown…"
|
|||
|
|
rows={16}
|
|||
|
|
className="w-full resize-y rounded-lg border border-line bg-surface px-4 py-3 font-mono text-sm leading-relaxed text-text outline-none focus:border-accent"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{error && <p className="mt-4 text-sm text-[#ff8585]">{error}</p>}
|
|||
|
|
|
|||
|
|
<div className="mt-8 flex items-center gap-3">
|
|||
|
|
<button
|
|||
|
|
onClick={save}
|
|||
|
|
disabled={saving}
|
|||
|
|
className="rounded-full bg-accent px-6 py-2 text-sm font-medium text-bg transition-opacity hover:opacity-90 disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
{saving ? '保存中…' : '保存'}
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={onCancel}
|
|||
|
|
disabled={saving}
|
|||
|
|
className="rounded-full border border-line px-6 py-2 text-sm font-medium text-muted transition-colors hover:border-accent hover:text-accent disabled:opacity-50"
|
|||
|
|
>
|
|||
|
|
取消
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
)
|
|||
|
|
}
|