Skip to main content

max / makenotwork

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