(function() { var input = document.getElementById('project-image-input'); if (!input) return; var __cfg = document.getElementById('tab-project-settings-cfg'); var projectId = __cfg ? __cfg.dataset.projectId : ''; input.addEventListener('change', function() { var file = this.files[0]; if (!file) return; var status = document.getElementById('project-image-status'); status.textContent = 'Uploading...'; fetch('/api/projects/image/presign', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ project_id: projectId, file_name: file.name, content_type: file.type || 'image/jpeg' }) }) .then(function(res) { if (!res.ok) throw new Error('Presign failed'); return res.json(); }) .then(function(data) { var xhr = new XMLHttpRequest(); xhr.open('PUT', data.upload_url); xhr.setRequestHeader('Content-Type', file.type || 'image/jpeg'); if (data.cache_control) xhr.setRequestHeader('Cache-Control', data.cache_control); return new Promise(function(resolve, reject) { xhr.onload = function() { xhr.status < 300 ? resolve(data.s3_key) : reject(new Error('Upload failed')); }; xhr.onerror = function() { reject(new Error('Network error')); }; xhr.send(file); }); }) .then(function(s3Key) { return fetch('/api/projects/image/confirm', { method: 'POST', headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), body: JSON.stringify({ project_id: projectId, s3_key: s3Key }) }); }) .then(function(res) { if (!res.ok) throw new Error('Confirm failed'); return res.json(); }) .then(function(data) { status.textContent = 'Saved.'; var preview = document.getElementById('project-image-preview'); preview.innerHTML = 'Project image'; setTimeout(function() { status.textContent = ''; }, 2000); }) .catch(function(err) { status.textContent = err.message; }); }); })(); // --------------------------------------------------------------------------- // Gallery manager (was tab-project-settings-2.js) // --------------------------------------------------------------------------- // This tab can arrive via an HTMX swap (DOMContentLoaded won't fire), so init // immediately, lazy-loading gallery.js if it isn't present yet. (function() { var __cfg = document.getElementById('tab-project-settings-cfg'); var projectId = __cfg ? __cfg.dataset.projectId : ''; function go() { initGalleryManager({ targetType: 'project', targetId: projectId, listId: 'project-gallery-list', inputId: 'project-gallery-input', statusId: 'project-gallery-status' }); } if (window.initGalleryManager) { go(); return; } var s = document.createElement('script'); s.src = '/static/gallery.js'; s.onload = go; document.head.appendChild(s); })(); // --------------------------------------------------------------------------- // Category suggestion dropdown (was tab-project-settings-3.js) // --------------------------------------------------------------------------- (function() { // Category suggestion dropdown var input = document.getElementById('settings-category'); var dropdown = document.getElementById('settings-category-dropdown'); if (!input || !dropdown) return; var debounce; function showDropdown(items, query) { dropdown.innerHTML = ''; items.forEach(function(c) { var div = document.createElement('div'); div.className = 'suggestion-item'; div.textContent = c.name; div.addEventListener('mousedown', function(e) { e.preventDefault(); input.value = c.name; dropdown.classList.remove('open'); }); dropdown.appendChild(div); }); var q = query.trim(); if (q.length > 0 && !items.some(function(c) { return c.name.toLowerCase() === q.toLowerCase(); })) { var create = document.createElement('div'); create.className = 'suggestion-item suggestion-create'; create.textContent = 'Create: ' + q; create.addEventListener('mousedown', function(e) { e.preventDefault(); input.value = q; dropdown.classList.remove('open'); }); dropdown.appendChild(create); } if (dropdown.children.length > 0) { dropdown.classList.add('open'); } else { dropdown.classList.remove('open'); } } input.addEventListener('input', function() { clearTimeout(debounce); var q = input.value.trim(); if (q.length < 1) { dropdown.classList.remove('open'); return; } debounce = setTimeout(function() { fetch('/api/categories/search?q=' + encodeURIComponent(q)) .then(function(r) { return r.json(); }) .then(function(cats) { showDropdown(cats, q); }) .catch(function() {}); }, 200); }); input.addEventListener('focus', function() { if (dropdown.children.length > 0) dropdown.classList.add('open'); }); input.addEventListener('blur', function() { setTimeout(function() { dropdown.classList.remove('open'); }, 150); }); })(); // Project info save function saveProjectInfo(e, projectId) { e.preventDefault(); var status = document.getElementById('project-save-status'); var data = { title: document.getElementById('project-name').value, description: document.getElementById('project-description').value, category: document.getElementById('settings-category').value }; fetch('/api/projects/' + projectId, { method: 'PUT', headers: {'Content-Type': 'application/json', ...csrfHeaders()}, body: JSON.stringify(data) }).then(function(r) { if (r.ok) { if (status) { status.textContent = 'Saved'; status.style.color = 'var(--success)'; } setTimeout(function() { if (status) status.textContent = ''; }, 2000); } else { r.text().then(function(body) { var msg = 'Save failed'; try { msg = JSON.parse(body).error || msg; } catch(_) {} if (status) { status.textContent = msg; status.style.color = 'var(--danger)'; } }); } }).catch(function() { if (status) { status.textContent = 'Network error'; status.style.color = 'var(--danger)'; } }); return false; } // Monetization save function updateSettingsPricingUI() { var model = document.getElementById('settings-pricing-model').value; var sections = [ { id: 'settings-buy-once-fields', active: model === 'buy_once' }, { id: 'settings-pwyw-fields', active: model === 'pwyw' }, { id: 'settings-subscription-note', active: model === 'subscription' } ]; sections.forEach(function(s) { var el = document.getElementById(s.id); if (el) el.classList.toggle('hidden', !s.active); }); } updateSettingsPricingUI(); function saveProjectPricing(e, projectId) { e.preventDefault(); var status = document.getElementById('project-pricing-status'); var model = document.getElementById('settings-pricing-model').value; var data = { pricing_model: model }; if (model === 'buy_once') { var p = parseFloat(document.getElementById('settings-price-dollars').value); if (!(p >= 0.50)) { if (status) { status.textContent = 'Price must be at least $0.50'; status.style.color = 'var(--danger)'; } return false; } data.price_dollars = p; } else if (model === 'pwyw') { var v = document.getElementById('settings-pwyw-min-dollars').value; data.pwyw_min_dollars = v === '' ? 0 : parseFloat(v); } fetch('/api/projects/' + projectId, { method: 'PUT', headers: {'Content-Type': 'application/json', ...csrfHeaders()}, body: JSON.stringify(data) }).then(function(r) { if (r.ok) { if (status) { status.textContent = 'Saved'; status.style.color = 'var(--success)'; } setTimeout(function() { if (status) status.textContent = ''; }, 2000); } else { r.text().then(function(body) { var msg = 'Save failed'; try { msg = JSON.parse(body).error || msg; } catch(_) {} if (status) { status.textContent = msg; status.style.color = 'var(--danger)'; } }); } }).catch(function() { if (status) { status.textContent = 'Network error'; status.style.color = 'var(--danger)'; } }); return false; } // Features update function updateFeatures(projectId) { var checkboxes = document.querySelectorAll('#features-grid input[type="checkbox"]'); var selected = []; checkboxes.forEach(function(cb) { if (cb.checked) selected.push(cb.value); }); var status = document.getElementById('features-save-status'); fetch('/api/projects/' + projectId, { method: 'PUT', headers: {'Content-Type': 'application/json', ...csrfHeaders()}, body: JSON.stringify({features: selected}) }).then(function(r) { if (r.ok) { if (status) { status.textContent = 'Saved'; status.style.color = 'var(--success)'; } // Reload the page so the tab bar reflects new features setTimeout(function() { window.location.reload(); }, 300); } else { if (status) { status.textContent = 'Save failed'; status.style.color = 'var(--danger)'; } } }).catch(function() { if (status) { status.textContent = 'Network error'; status.style.color = 'var(--danger)'; } }); }