/** * Blog post editor: save draft, publish, auto-save. * * Full page (not HTMX partial), no re-init needed. * Reads project/post data from data attributes on #blog-editor. * Depends on: mnw.js (csrfHeaders). */ (function() { var editor = document.getElementById('blog-editor'); if (!editor) return; var projectId = editor.dataset.projectId; var projectSlug = editor.dataset.projectSlug; var editingPostId = editor.dataset.postId || null; var blogAutoSaveTimer = null; var postStatus = document.getElementById('post-status'); function getFields() { return { title: document.getElementById('post-title').value.trim(), slug: document.getElementById('post-slug').value.trim(), body: document.getElementById('post-body').value }; } // Present only on the changelog project editor; null elsewhere. var landingToggle = document.getElementById('post-show-on-landing'); function goBack() { window.location.href = '/dashboard/project/' + projectSlug; } function saveBlogPost(publish) { var f = getFields(); if (!f.title) { postStatus.innerHTML = 'Title is required'; return; } var payload = { title: f.title, body_markdown: f.body, is_published: publish }; if (f.slug) payload.slug = f.slug; if (landingToggle) payload.show_on_landing = landingToggle.checked; fetch('/api/projects/' + projectId + '/blog', { method: 'POST', headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, body: JSON.stringify(payload) }) .then(function(res) { if (!res.ok) return apiErrorMessage(res, 'Failed to create post').then(function(m) { throw new Error(m); }); return res.json(); }) .then(function() { goBack(); }) .catch(function(err) { postStatus.style.color = 'var(--danger)'; postStatus.textContent = err.message; }); } function updateBlogPost(postId, publish) { var f = getFields(); if (!f.title) { postStatus.innerHTML = 'Title is required'; return; } if (!f.slug) { postStatus.innerHTML = 'Slug is required'; return; } var updatePayload = { title: f.title, slug: f.slug, body_markdown: f.body, is_published: publish }; if (landingToggle) updatePayload.show_on_landing = landingToggle.checked; fetch('/api/blog/' + postId, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, body: JSON.stringify(updatePayload) }) .then(function(res) { if (!res.ok) return apiErrorMessage(res, 'Failed to update post').then(function(m) { throw new Error(m); }); return res.json(); }) .then(function() { goBack(); }) .catch(function(err) { postStatus.style.color = 'var(--danger)'; postStatus.textContent = err.message; }); } document.getElementById('save-draft-btn').addEventListener('click', function() { if (editingPostId) updateBlogPost(editingPostId, false); else saveBlogPost(false); }); document.getElementById('publish-btn').addEventListener('click', function() { if (editingPostId) updateBlogPost(editingPostId, true); else saveBlogPost(true); }); // Auto-save for edit mode (30s debounce) if (editingPostId) { ['post-title', 'post-slug', 'post-body'].forEach(function(id) { document.getElementById(id).addEventListener('input', function() { clearTimeout(blogAutoSaveTimer); blogAutoSaveTimer = setTimeout(function() { var f = getFields(); if (!f.title || !f.slug) return; postStatus.innerHTML = 'Saving...'; fetch('/api/blog/' + editingPostId, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...csrfHeaders() }, body: JSON.stringify({ title: f.title, slug: f.slug, body_markdown: f.body, is_published: false }) }) .then(function(res) { if (!res.ok) return apiErrorMessage(res, 'Auto-save failed').then(function(m) { throw new Error(m); }); postStatus.innerHTML = 'Auto-saved'; setTimeout(function() { if (postStatus.textContent === 'Auto-saved') postStatus.innerHTML = ''; }, 3000); }) .catch(function(err) { var span = document.createElement('span'); span.style.color = 'var(--danger)'; span.textContent = err.message || 'Auto-save failed'; postStatus.replaceChildren(span); }); }, 30000); }); }); } })();