Skip to main content

max / makenotwork

5.5 KB · 130 lines History Blame Raw
1 /**
2 * Blog post editor: save draft, publish, auto-save.
3 *
4 * Full page (not HTMX partial), no re-init needed.
5 * Reads project/post data from data attributes on #blog-editor.
6 * Depends on: the core module (csrfHeaders).
7 */
8 (function() {
9 var editor = document.getElementById('blog-editor');
10 if (!editor) return;
11
12 var projectId = editor.dataset.projectId;
13 var projectSlug = editor.dataset.projectSlug;
14 var editingPostId = editor.dataset.postId || null;
15 var postStatus = document.getElementById('post-status');
16
17 function getFields() {
18 return {
19 title: document.getElementById('post-title').value.trim(),
20 slug: document.getElementById('post-slug').value.trim(),
21 body: document.getElementById('post-body').value
22 };
23 }
24
25 // Present only on the changelog project editor; null elsewhere.
26 var landingToggle = document.getElementById('post-show-on-landing');
27
28 function goBack() {
29 window.location.href = '/dashboard/project/' + projectSlug;
30 }
31
32 function saveBlogPost(publish) {
33 var f = getFields();
34 if (!f.title) {
35 postStatus.innerHTML = '<span style="color: var(--danger);">Title is required</span>';
36 return;
37 }
38 var payload = { title: f.title, body_markdown: f.body, is_published: publish };
39 if (f.slug) payload.slug = f.slug;
40 if (landingToggle) payload.show_on_landing = landingToggle.checked;
41
42 fetch('/api/projects/' + projectId + '/blog', {
43 method: 'POST',
44 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
45 body: JSON.stringify(payload)
46 })
47 .then(function(res) {
48 if (!res.ok) return apiErrorMessage(res, 'Failed to create post').then(function(m) { throw new Error(m); });
49 return res.json();
50 })
51 .then(function() { goBack(); })
52 .catch(function(err) {
53 postStatus.style.color = 'var(--danger)';
54 postStatus.textContent = err.message;
55 });
56 }
57
58 function updateBlogPost(postId, publish) {
59 var f = getFields();
60 if (!f.title) {
61 postStatus.innerHTML = '<span style="color: var(--danger);">Title is required</span>';
62 return;
63 }
64 if (!f.slug) {
65 postStatus.innerHTML = '<span style="color: var(--danger);">Slug is required</span>';
66 return;
67 }
68 var updatePayload = { title: f.title, slug: f.slug, body_markdown: f.body, is_published: publish };
69 if (landingToggle) updatePayload.show_on_landing = landingToggle.checked;
70 fetch('/api/blog/' + postId, {
71 method: 'PUT',
72 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
73 body: JSON.stringify(updatePayload)
74 })
75 .then(function(res) {
76 if (!res.ok) return apiErrorMessage(res, 'Failed to update post').then(function(m) { throw new Error(m); });
77 return res.json();
78 })
79 .then(function() { goBack(); })
80 .catch(function(err) {
81 postStatus.style.color = 'var(--danger)';
82 postStatus.textContent = err.message;
83 });
84 }
85
86 document.getElementById('save-draft-btn').addEventListener('click', function() {
87 if (editingPostId) updateBlogPost(editingPostId, false);
88 else saveBlogPost(false);
89 });
90 document.getElementById('publish-btn').addEventListener('click', function() {
91 if (editingPostId) updateBlogPost(editingPostId, true);
92 else saveBlogPost(true);
93 });
94
95 // Auto-save for edit mode (30s debounce)
96 if (editingPostId) {
97 ['post-title', 'post-slug', 'post-body'].forEach(function(id) {
98 document.getElementById(id).addEventListener('input', function() {
99 // Thirty seconds is a save cadence rather than a wait for the
100 // typing to stop, and `makeover-timing` names no such intent, so
101 // the number stays here. The closure that spent it does not.
102 window.timing.debounce('blog-autosave', function() {
103 var f = getFields();
104 if (!f.title || !f.slug) return;
105 postStatus.innerHTML = '<span style="opacity: 0.5;">Saving...</span>';
106 // Omit is_published so a background auto-save never changes
107 // the post's live/draft state (the server treats a missing
108 // field as "no publish-state change").
109 fetch('/api/blog/' + editingPostId, {
110 method: 'PUT',
111 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
112 body: JSON.stringify({ title: f.title, slug: f.slug, body_markdown: f.body })
113 })
114 .then(function(res) {
115 if (!res.ok) return apiErrorMessage(res, 'Auto-save failed').then(function(m) { throw new Error(m); });
116 postStatus.innerHTML = '<span style="color: var(--text-muted);">Auto-saved</span>';
117 window.timing.clearStatusLater(postStatus, 'Auto-saved');
118 })
119 .catch(function(err) {
120 var span = document.createElement('span');
121 span.style.color = 'var(--danger)';
122 span.textContent = err.message || 'Auto-save failed';
123 postStatus.replaceChildren(span);
124 });
125 }, 30000);
126 });
127 });
128 }
129 })();
130