new web ract
This commit is contained in:
@@ -52,22 +52,62 @@ const DEFAULT_META = {
|
||||
status: 'draft',
|
||||
}
|
||||
|
||||
/* ── Загрузчик: сначала данные, потом редактор ── */
|
||||
export default function App() {
|
||||
const [meta, setMeta] = useState(DEFAULT_META)
|
||||
const [initContent, setInitContent] = useState(IS_NEW ? '' : null) // null = ещё не загружено
|
||||
const [initMeta, setInitMeta] = useState(DEFAULT_META)
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_NEW) return
|
||||
api.getArticle(ARTICLE_ID, TOKEN)
|
||||
.then(data => {
|
||||
setInitMeta({
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
slug: data.slug,
|
||||
excerpt: data.excerpt || '',
|
||||
cover_url: data.cover_url || null,
|
||||
font_family: data.font_family || 'Merriweather',
|
||||
category_id: data.category?.id || null,
|
||||
tag_names: (data.tags || []).map(t => t.name),
|
||||
status: data.status,
|
||||
})
|
||||
const html = data.content?.startsWith('<')
|
||||
? data.content
|
||||
: marked.parse(data.content || '')
|
||||
setInitContent(html || '')
|
||||
})
|
||||
.catch(() => setInitContent(''))
|
||||
}, [])
|
||||
|
||||
// Пока данные не загружены — показываем лоадер
|
||||
if (initContent === null) {
|
||||
return (
|
||||
<div style={{ display: 'flex', height: '100vh', alignItems: 'center', justifyContent: 'center',
|
||||
background: '#f8fafc', color: '#94a3b8', fontSize: 14 }}>
|
||||
Загрузка статьи…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <EditorApp initContent={initContent} initMeta={initMeta} />
|
||||
}
|
||||
|
||||
/* ── Сам редактор — рендерится только когда данные готовы ── */
|
||||
function EditorApp({ initContent, initMeta }) {
|
||||
const [meta, setMeta] = useState(initMeta)
|
||||
const [categories, setCategories] = useState([])
|
||||
const [saveStatus, setSaveStatus] = useState('saved')
|
||||
const [mediaState, setMediaState] = useState(null)
|
||||
|
||||
/* Refs so callbacks always see latest values without re-creating them */
|
||||
const metaRef = useRef(meta)
|
||||
metaRef.current = meta
|
||||
const currentIdRef = useRef(IS_NEW ? null : ARTICLE_ID)
|
||||
const saveTimerRef = useRef(null)
|
||||
const editorRef = useRef(null) // always points to live editor instance
|
||||
const articleLoadedRef = useRef(false)
|
||||
const doSaveRef = useRef(null) // always points to latest doSave
|
||||
const metaRef = useRef(meta)
|
||||
metaRef.current = meta
|
||||
const currentIdRef = useRef(initMeta.id ?? (IS_NEW ? null : ARTICLE_ID))
|
||||
const saveTimerRef = useRef(null)
|
||||
const editorRef = useRef(null)
|
||||
const doSaveRef = useRef(null)
|
||||
|
||||
/* ── Editor ── */
|
||||
/* ── Editor создаётся сразу с готовым контентом ── */
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
@@ -88,54 +128,22 @@ export default function App() {
|
||||
Placeholder.configure({ placeholder: 'Начните писать статью…' }),
|
||||
CharacterCount,
|
||||
],
|
||||
content: initContent,
|
||||
onUpdate: () => {
|
||||
setSaveStatus('unsaved')
|
||||
clearTimeout(saveTimerRef.current)
|
||||
// Always call the latest doSave via ref — avoids stale closure
|
||||
saveTimerRef.current = setTimeout(() => doSaveRef.current?.(), 3000)
|
||||
},
|
||||
})
|
||||
|
||||
// Keep editorRef current every render so doSave never uses a stale instance
|
||||
editorRef.current = editor
|
||||
|
||||
/* ── Load categories once on mount ── */
|
||||
/* ── Категории ── */
|
||||
useEffect(() => {
|
||||
api.getCategories(TOKEN).then(setCategories).catch(() => {})
|
||||
}, [])
|
||||
|
||||
/* ── Load article once the editor is ready ── */
|
||||
useEffect(() => {
|
||||
if (!editor || IS_NEW || articleLoadedRef.current) return
|
||||
articleLoadedRef.current = true
|
||||
|
||||
api.getArticle(ARTICLE_ID, TOKEN)
|
||||
.then(data => {
|
||||
setMeta({
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
slug: data.slug,
|
||||
excerpt: data.excerpt || '',
|
||||
cover_url: data.cover_url || null,
|
||||
font_family: data.font_family || 'Merriweather',
|
||||
category_id: data.category?.id || null,
|
||||
tag_names: (data.tags || []).map(t => t.name),
|
||||
status: data.status,
|
||||
})
|
||||
const html = data.content?.startsWith('<')
|
||||
? data.content
|
||||
: marked.parse(data.content || '')
|
||||
editor.commands.setContent(html, false)
|
||||
setSaveStatus('saved')
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [editor])
|
||||
|
||||
/* ── Save ─────────────────────────────────────────────────────────────────
|
||||
Uses editorRef + metaRef so this callback is stable (no deps that change).
|
||||
doSaveRef is updated every render so the auto-save timer always calls the
|
||||
freshest version.
|
||||
── */
|
||||
/* ── Сохранение ── */
|
||||
const doSave = useCallback(async overrideStatus => {
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
@@ -143,15 +151,15 @@ export default function App() {
|
||||
setSaveStatus('saving')
|
||||
|
||||
const payload = {
|
||||
title: m.title || 'Без названия',
|
||||
slug: m.slug || undefined,
|
||||
content: editor.getHTML(),
|
||||
excerpt: m.excerpt,
|
||||
cover_url: m.cover_url || null,
|
||||
title: m.title || 'Без названия',
|
||||
slug: m.slug || undefined,
|
||||
content: editor.getHTML(),
|
||||
excerpt: m.excerpt,
|
||||
cover_url: m.cover_url || null,
|
||||
font_family: m.font_family,
|
||||
category_id: m.category_id || null,
|
||||
tag_names: m.tag_names,
|
||||
status: overrideStatus ?? m.status,
|
||||
tag_names: m.tag_names,
|
||||
status: overrideStatus ?? m.status,
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -165,15 +173,12 @@ export default function App() {
|
||||
saved = await api.updateArticle(currentIdRef.current, payload, TOKEN)
|
||||
}
|
||||
setSaveStatus('saved')
|
||||
if (overrideStatus) {
|
||||
setMeta(m => ({ ...m, status: overrideStatus }))
|
||||
}
|
||||
if (overrideStatus) setMeta(m => ({ ...m, status: overrideStatus }))
|
||||
} catch {
|
||||
setSaveStatus('error')
|
||||
}
|
||||
}, []) // stable: reads from editorRef + metaRef
|
||||
}, [])
|
||||
|
||||
// Keep ref current so the auto-save timer always calls the latest version
|
||||
doSaveRef.current = doSave
|
||||
|
||||
const handleUploadCover = async file => {
|
||||
@@ -183,7 +188,6 @@ export default function App() {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/* ── Render ── */
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-hidden bg-slate-50" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user