Skip to main content

max / makenotwork

10.1 KB · 246 lines History Blame Raw
1 (function() {
2 var input = document.getElementById('project-image-input');
3 if (!input) return;
4 var __cfg = document.getElementById('tab-project-settings-cfg');
5 var projectId = __cfg ? __cfg.dataset.projectId : '';
6 input.addEventListener('change', function() {
7 var file = this.files[0];
8 if (!file) return;
9 var status = document.getElementById('project-image-status');
10 status.textContent = 'Uploading...';
11
12 fetch('/api/projects/image/presign', {
13 method: 'POST',
14 headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
15 body: JSON.stringify({ project_id: projectId, file_name: file.name, content_type: file.type || 'image/jpeg' })
16 })
17 .then(function(res) { if (!res.ok) throw new Error('Presign failed'); return res.json(); })
18 .then(function(data) {
19 var xhr = new XMLHttpRequest();
20 xhr.open('PUT', data.upload_url);
21 xhr.setRequestHeader('Content-Type', file.type || 'image/jpeg');
22 if (data.cache_control) xhr.setRequestHeader('Cache-Control', data.cache_control);
23 return new Promise(function(resolve, reject) {
24 xhr.onload = function() { xhr.status < 300 ? resolve(data.s3_key) : reject(new Error('Upload failed')); };
25 xhr.onerror = function() { reject(new Error('Network error')); };
26 xhr.send(file);
27 });
28 })
29 .then(function(s3Key) {
30 return fetch('/api/projects/image/confirm', {
31 method: 'POST',
32 headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
33 body: JSON.stringify({ project_id: projectId, s3_key: s3Key })
34 });
35 })
36 .then(function(res) { if (!res.ok) throw new Error('Confirm failed'); return res.json(); })
37 .then(function(data) {
38 status.textContent = 'Saved.';
39 var preview = document.getElementById('project-image-preview');
40 preview.innerHTML = '<img src="' + data.image_url + '" alt="Project image">';
41 window.timing.clearStatusLater(status);
42 })
43 .catch(function(err) { status.textContent = err.message; });
44 });
45 })();
46
47 // Gallery manager (was tab-project-settings-2.js)
48 // This tab can arrive via an HTMX swap (DOMContentLoaded won't fire), so init
49 // immediately, lazy-loading gallery.js if it isn't present yet.
50 (function() {
51 var __cfg = document.getElementById('tab-project-settings-cfg');
52 var projectId = __cfg ? __cfg.dataset.projectId : '';
53 function go() {
54 initGalleryManager({
55 targetType: 'project',
56 targetId: projectId,
57 listId: 'project-gallery-list',
58 inputId: 'project-gallery-input',
59 statusId: 'project-gallery-status'
60 });
61 }
62 if (window.initGalleryManager) { go(); return; }
63 var s = document.createElement('script');
64 s.src = '/static/gallery.js';
65 s.onload = go;
66 document.head.appendChild(s);
67 })();
68
69 // Category suggestion dropdown (was tab-project-settings-3.js)
70 (function() {
71 var input = document.getElementById('settings-category');
72 var dropdown = document.getElementById('settings-category-dropdown');
73 if (!input || !dropdown) return;
74
75 // The wait between keystrokes and the search is `Intent::Debounce`, spent
76 // through the shared helper on the core module. It used to be a 200ms
77 // timeout hand-rolled here, which was this typeahead disagreeing with the
78 // docs search for no reason anyone recorded.
79 var SEARCH = 'category-search';
80
81 function showDropdown(items, query) {
82 dropdown.innerHTML = '';
83 items.forEach(function(c) {
84 var div = document.createElement('div');
85 div.className = 'suggestion-item';
86 div.textContent = c.name;
87 div.addEventListener('mousedown', function(e) {
88 e.preventDefault();
89 input.value = c.name;
90 dropdown.classList.remove('open');
91 });
92 dropdown.appendChild(div);
93 });
94 var q = query.trim();
95 if (q.length > 0 && !items.some(function(c) { return c.name.toLowerCase() === q.toLowerCase(); })) {
96 var create = document.createElement('div');
97 create.className = 'suggestion-item suggestion-create';
98 create.textContent = 'Create: ' + q;
99 create.addEventListener('mousedown', function(e) {
100 e.preventDefault();
101 input.value = q;
102 dropdown.classList.remove('open');
103 });
104 dropdown.appendChild(create);
105 }
106 if (dropdown.children.length > 0) {
107 dropdown.classList.add('open');
108 } else {
109 dropdown.classList.remove('open');
110 }
111 }
112
113 input.addEventListener('input', function() {
114 var q = input.value.trim();
115 if (q.length < 1) { window.timing.cancelDebounce(SEARCH); dropdown.classList.remove('open'); return; }
116 window.timing.debounce(SEARCH, function() {
117 fetch('/api/categories/search?q=' + encodeURIComponent(q))
118 .then(function(r) { return r.json(); })
119 .then(function(cats) { showDropdown(cats, q); })
120 .catch(function() {});
121 });
122 });
123
124 input.addEventListener('focus', function() {
125 if (dropdown.children.length > 0) dropdown.classList.add('open');
126 });
127
128 input.addEventListener('blur', function() {
129 // Not a timing intent: the dropdown has to outlive the blur long enough
130 // for a mousedown on one of its rows to land. A race, so it keeps its
131 // own number.
132 setTimeout(function() { dropdown.classList.remove('open'); }, 150);
133 });
134 })();
135
136 // Project info save
137 function saveProjectInfo(e, projectId) {
138 e.preventDefault();
139 var status = document.getElementById('project-save-status');
140 var data = {
141 title: document.getElementById('project-name').value,
142 description: document.getElementById('project-description').value,
143 category: document.getElementById('settings-category').value
144 };
145 fetch('/api/projects/' + projectId, {
146 method: 'PUT',
147 headers: {'Content-Type': 'application/json', ...csrfHeaders()},
148 body: JSON.stringify(data)
149 }).then(function(r) {
150 if (r.ok) {
151 if (status) { status.textContent = 'Saved'; status.style.color = 'var(--success)'; }
152 if (status) window.timing.clearStatusLater(status);
153 } else {
154 r.text().then(function(body) {
155 var msg = 'Save failed';
156 try { msg = JSON.parse(body).error || msg; } catch(_) {}
157 if (status) { status.textContent = msg; status.style.color = 'var(--danger)'; }
158 });
159 }
160 }).catch(function() {
161 if (status) { status.textContent = 'Network error'; status.style.color = 'var(--danger)'; }
162 });
163 return false;
164 }
165
166 // Monetization save
167 function updateSettingsPricingUI() {
168 var model = document.getElementById('settings-pricing-model').value;
169 var sections = [
170 { id: 'settings-buy-once-fields', active: model === 'buy_once' },
171 { id: 'settings-pwyw-fields', active: model === 'pwyw' },
172 { id: 'settings-subscription-note', active: model === 'subscription' }
173 ];
174 sections.forEach(function(s) {
175 var el = document.getElementById(s.id);
176 if (el) el.classList.toggle('hidden', !s.active);
177 });
178 }
179 updateSettingsPricingUI();
180
181 function saveProjectPricing(e, projectId) {
182 e.preventDefault();
183 var status = document.getElementById('project-pricing-status');
184 var model = document.getElementById('settings-pricing-model').value;
185 var data = { pricing_model: model };
186
187 if (model === 'buy_once') {
188 var p = parseFloat(document.getElementById('settings-price-dollars').value);
189 if (!(p >= 0.50)) {
190 if (status) { status.textContent = 'Price must be at least $0.50'; status.style.color = 'var(--danger)'; }
191 return false;
192 }
193 data.price_dollars = p;
194 } else if (model === 'pwyw') {
195 var v = document.getElementById('settings-pwyw-min-dollars').value;
196 data.pwyw_min_dollars = v === '' ? 0 : parseFloat(v);
197 }
198
199 fetch('/api/projects/' + projectId, {
200 method: 'PUT',
201 headers: {'Content-Type': 'application/json', ...csrfHeaders()},
202 body: JSON.stringify(data)
203 }).then(function(r) {
204 if (r.ok) {
205 if (status) { status.textContent = 'Saved'; status.style.color = 'var(--success)'; }
206 if (status) window.timing.clearStatusLater(status);
207 } else {
208 r.text().then(function(body) {
209 var msg = 'Save failed';
210 try { msg = JSON.parse(body).error || msg; } catch(_) {}
211 if (status) { status.textContent = msg; status.style.color = 'var(--danger)'; }
212 });
213 }
214 }).catch(function() {
215 if (status) { status.textContent = 'Network error'; status.style.color = 'var(--danger)'; }
216 });
217 return false;
218 }
219
220 // Features update
221 function updateFeatures(projectId) {
222 var checkboxes = document.querySelectorAll('#features-grid input[type="checkbox"]');
223 var selected = [];
224 checkboxes.forEach(function(cb) {
225 if (cb.checked) selected.push(cb.value);
226 });
227 var status = document.getElementById('features-save-status');
228
229 fetch('/api/projects/' + projectId, {
230 method: 'PUT',
231 headers: {'Content-Type': 'application/json', ...csrfHeaders()},
232 body: JSON.stringify({features: selected})
233 }).then(function(r) {
234 if (r.ok) {
235 if (status) { status.textContent = 'Saved'; status.style.color = 'var(--success)'; }
236 // Reload the page so the tab bar reflects new features. The
237 // response is in hand in this branch, so there is nothing to wait for.
238 window.location.reload();
239 } else {
240 if (status) { status.textContent = 'Save failed'; status.style.color = 'var(--danger)'; }
241 }
242 }).catch(function() {
243 if (status) { status.textContent = 'Network error'; status.style.color = 'var(--danger)'; }
244 });
245 }
246