/** * 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: the core module (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 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() { // Thirty seconds is a save cadence rather than a wait for the // typing to stop, and `makeover-timing` names no such intent, so // the number stays here. The closure that spent it does not. window.timing.debounce('blog-autosave', function() { var f = getFields(); if (!f.title || !f.slug) return; postStatus.innerHTML = 'Saving...'; // Omit is_published so a background auto-save never changes // the post's live/draft state (the server treats a missing // field as "no publish-state change"). 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 }) }) .then(function(res) { if (!res.ok) return apiErrorMessage(res, 'Auto-save failed').then(function(m) { throw new Error(m); }); postStatus.innerHTML = 'Auto-saved'; window.timing.clearStatusLater(postStatus, 'Auto-saved'); }) .catch(function(err) { var span = document.createElement('span'); span.style.color = 'var(--danger)'; span.textContent = err.message || 'Auto-save failed'; postStatus.replaceChildren(span); }); }, 30000); }); }); } })();