Skip to main content

max / makenotwork

7.8 KB · 161 lines History Blame Raw
1 // Project sections (Pages) editor — markdown content blocks for a project.
2 // Mirrors item sections but scoped to projects. Loaded by the Settings tab.
3 (function() {
4 'use strict';
5
6 function csrfHeaders() {
7 var token = document.querySelector('meta[name="csrf-token"]');
8 return token ? { 'X-CSRF-Token': token.content } : {};
9 }
10
11 function escapeHtml(s) {
12 var d = document.createElement('div');
13 d.textContent = s;
14 return d.innerHTML;
15 }
16
17 function showToast(msg) {
18 if (window.showToast) { window.showToast(msg); return; }
19 alert(msg);
20 }
21
22 function bodyFor(id) {
23 var el = document.querySelector('textarea[data-body-for="' + id + '"]');
24 return el ? el.value : '';
25 }
26
27 function updateCount(delta) {
28 var el = document.getElementById('psection-count');
29 if (el) el.textContent = parseInt(el.textContent || '0') + delta;
30 }
31
32 function attachRowHandlers(row) {
33 var delBtn = row.querySelector('.psection-del-btn');
34 var editBtn = row.querySelector('.psection-edit-btn');
35
36 delBtn.addEventListener('click', function() {
37 var id = this.dataset.id;
38 if (!confirm('Delete this page?')) return;
39 fetch('/api/project-sections/' + id, { method: 'DELETE', headers: csrfHeaders() })
40 .then(function(res) {
41 if (res.ok) {
42 var hidden = document.querySelector('textarea[data-body-for="' + id + '"]');
43 if (hidden) hidden.remove();
44 row.remove();
45 updateCount(-1);
46 } else {
47 apiErrorMessage(res, 'Failed to delete').then(function(m) { showToast(m); });
48 }
49 })
50 .catch(function() { showToast('Failed to delete'); });
51 });
52
53 editBtn.addEventListener('click', function() {
54 var id = this.dataset.id;
55 var title = this.dataset.title || row.querySelector('span').textContent;
56 document.getElementById('edit-psec-id').value = id;
57 document.getElementById('edit-psec-title').value = title;
58 document.getElementById('edit-psec-body').value = bodyFor(id);
59 document.getElementById('psec-edit-status').textContent = '';
60 document.getElementById('psection-edit-modal').classList.remove('hidden');
61 });
62 }
63
64 function init() {
65 var addBtn = document.getElementById('add-psec-btn');
66 if (!addBtn) return;
67 var projectId = addBtn.dataset.projectId;
68
69 addBtn.addEventListener('click', function() {
70 var title = document.getElementById('new-psec-title').value.trim();
71 var body = document.getElementById('new-psec-body').value;
72 var status = document.getElementById('psec-add-status');
73 if (!title) { status.textContent = 'Title is required'; return; }
74 this.disabled = true;
75 status.textContent = '';
76
77 fetch('/api/projects/' + projectId + '/sections', {
78 method: 'POST',
79 headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
80 body: JSON.stringify({ title: title, body: body })
81 })
82 .then(function(res) {
83 if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); });
84 return res.json();
85 })
86 .then(function(sec) {
87 var empty = document.getElementById('psections-empty');
88 if (empty) empty.remove();
89 var list = document.getElementById('psections-list');
90 var row = document.createElement('div');
91 row.className = 'psection-row';
92 row.dataset.id = sec.id;
93 row.innerHTML =
94 '<span class="psection-row-title">' + escapeHtml(sec.title) + '</span>' +
95 '<code class="psection-row-anchor">#section-' + escapeHtml(sec.slug) + '</code>' +
96 '<span class="psection-row-length">' + (sec.body || '').length + ' chars</span>' +
97 '<button type="button" class="btn-secondary psection-edit-btn" data-id="' + sec.id + '" data-title="' + escapeHtml(sec.title) + '">Edit</button>' +
98 '<button type="button" class="btn-secondary psection-del-btn" data-id="' + sec.id + '">Delete</button>';
99 list.appendChild(row);
100 var hidden = document.createElement('textarea');
101 hidden.className = 'hidden';
102 hidden.dataset.bodyFor = sec.id;
103 hidden.value = sec.body || '';
104 list.appendChild(hidden);
105 attachRowHandlers(row);
106 updateCount(1);
107 document.getElementById('new-psec-title').value = '';
108 document.getElementById('new-psec-body').value = '';
109 document.getElementById('psection-add-details').removeAttribute('open');
110 })
111 .catch(function(err) { status.textContent = err.message; })
112 .finally(function() { addBtn.disabled = false; });
113 });
114
115 document.getElementById('save-psec-btn').addEventListener('click', function() {
116 var id = document.getElementById('edit-psec-id').value;
117 var title = document.getElementById('edit-psec-title').value.trim();
118 var body = document.getElementById('edit-psec-body').value;
119 var status = document.getElementById('psec-edit-status');
120 if (!title) { status.textContent = 'Title is required'; return; }
121 this.disabled = true;
122
123 fetch('/api/project-sections/' + id, {
124 method: 'PUT',
125 headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
126 body: JSON.stringify({ title: title, body: body })
127 })
128 .then(function(res) {
129 if (!res.ok) return res.json().then(function(d) { throw new Error(d.error || 'Failed'); });
130 return res.json();
131 })
132 .then(function(sec) {
133 var row = document.querySelector('.psection-row[data-id="' + id + '"]');
134 if (row) {
135 row.querySelector('.psection-row-title').textContent = sec.title;
136 row.querySelector('.psection-row-anchor').textContent = '#section-' + sec.slug;
137 row.querySelector('.psection-row-length').textContent = (sec.body || '').length + ' chars';
138 row.querySelector('.psection-edit-btn').dataset.title = sec.title;
139 }
140 var hidden = document.querySelector('textarea[data-body-for="' + id + '"]');
141 if (hidden) hidden.value = sec.body || '';
142 document.getElementById('psection-edit-modal').classList.add('hidden');
143 })
144 .catch(function(err) { status.textContent = err.message; })
145 .finally(function() { document.getElementById('save-psec-btn').disabled = false; });
146 });
147
148 document.getElementById('cancel-psec-btn').addEventListener('click', function() {
149 document.getElementById('psection-edit-modal').classList.add('hidden');
150 });
151
152 document.querySelectorAll('.psection-row').forEach(attachRowHandlers);
153 }
154
155 // HTMX swaps in the settings partial; bind on swap + on initial load.
156 if (document.getElementById('add-psec-btn')) init();
157 document.body.addEventListener('htmx:afterSettle', function(e) {
158 if (e.target && e.target.querySelector && e.target.querySelector('#add-psec-btn')) init();
159 });
160 })();
161