/**
* 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);
});
});
}
})();