max / makenotwork
- Co-Authored-By
- Claude Opus 4.8 <noreply@anthropic.com>
100 files changed,
+3140 insertions,
-3092 deletions
| @@ -134,27 +134,5 @@ | |||
| 134 | 134 | </div> | |
| 135 | 135 | </div> | |
| 136 | 136 | ||
| 137 | - | <script> | |
| 138 | - | function exportAll() { | |
| 139 | - | var btn = document.getElementById('export-all-btn'); | |
| 140 | - | var buttons = document.querySelectorAll('.export-card button.secondary'); | |
| 141 | - | if (buttons.length === 0) return; | |
| 142 | - | ||
| 143 | - | btn.disabled = true; | |
| 144 | - | btn.textContent = 'Exporting...'; | |
| 145 | - | var i = 0; | |
| 146 | - | ||
| 147 | - | function next() { | |
| 148 | - | if (i >= buttons.length) { | |
| 149 | - | btn.textContent = 'Done'; | |
| 150 | - | setTimeout(function() { btn.textContent = 'Export All'; btn.disabled = false; }, 3000); | |
| 151 | - | return; | |
| 152 | - | } | |
| 153 | - | buttons[i].click(); | |
| 154 | - | i++; | |
| 155 | - | setTimeout(next, 1500); | |
| 156 | - | } | |
| 157 | - | next(); | |
| 158 | - | } | |
| 159 | - | </script> | |
| 137 | + | <script src="/static/dashboard-export-inline.js?v=0623" defer></script> | |
| 160 | 138 | {% endblock %} |
| @@ -117,218 +117,6 @@ | |||
| 117 | 117 | </div> | |
| 118 | 118 | </div> | |
| 119 | 119 | ||
| 120 | - | <script> | |
| 121 | - | (function() { | |
| 122 | - | const fileInput = document.getElementById('import-file'); | |
| 123 | - | const previewEl = document.getElementById('csv-preview'); | |
| 124 | - | const previewTable = document.getElementById('preview-table'); | |
| 125 | - | const mappingEl = document.getElementById('column-mapping'); | |
| 126 | - | const startBtn = document.getElementById('start-import-btn'); | |
| 127 | - | const progressEl = document.getElementById('import-progress'); | |
| 128 | - | const resultEl = document.getElementById('import-result'); | |
| 129 | - | ||
| 130 | - | let csvBase64 = ''; | |
| 131 | - | let csvHeaders = []; | |
| 132 | - | ||
| 133 | - | fileInput.addEventListener('change', function(e) { | |
| 134 | - | const file = e.target.files[0]; | |
| 135 | - | if (!file) return; | |
| 136 | - | ||
| 137 | - | const reader = new FileReader(); | |
| 138 | - | reader.onload = function(ev) { | |
| 139 | - | const text = ev.target.result; | |
| 140 | - | csvBase64 = btoa(unescape(encodeURIComponent(text))); | |
| 141 | - | ||
| 142 | - | const lines = text.split('\n').filter(l => l.trim()); | |
| 143 | - | if (lines.length < 2) { | |
| 144 | - | alert('CSV must have a header row and at least one data row.'); | |
| 145 | - | return; | |
| 146 | - | } | |
| 147 | - | ||
| 148 | - | csvHeaders = parseCSVLine(lines[0]); | |
| 149 | - | ||
| 150 | - | // Build preview table | |
| 151 | - | let html = '<thead><tr>'; | |
| 152 | - | csvHeaders.forEach(h => { html += '<th>' + escapeHtml(h) + '</th>'; }); | |
| 153 | - | html += '</tr></thead><tbody>'; | |
| 154 | - | for (let i = 1; i < Math.min(lines.length, 4); i++) { | |
| 155 | - | const cols = parseCSVLine(lines[i]); | |
| 156 | - | html += '<tr>'; | |
| 157 | - | csvHeaders.forEach((_, ci) => { | |
| 158 | - | html += '<td>' + escapeHtml(cols[ci] || '') + '</td>'; | |
| 159 | - | }); | |
| 160 | - | html += '</tr>'; | |
| 161 | - | } | |
| 162 | - | html += '</tbody>'; | |
| 163 | - | previewTable.innerHTML = html; | |
| 164 | - | previewEl.classList.remove('hidden'); | |
| 165 | - | ||
| 166 | - | // Populate mapping selects | |
| 167 | - | const selects = mappingEl.querySelectorAll('select'); | |
| 168 | - | selects.forEach(sel => { | |
| 169 | - | const current = sel.value; | |
| 170 | - | sel.innerHTML = '<option value="">-- skip --</option>'; | |
| 171 | - | csvHeaders.forEach((h, i) => { | |
| 172 | - | const opt = document.createElement('option'); | |
| 173 | - | opt.value = i; | |
| 174 | - | opt.textContent = h; | |
| 175 | - | sel.appendChild(opt); | |
| 176 | - | }); | |
| 177 | - | // Auto-detect common column names | |
| 178 | - | autoDetectColumn(sel, csvHeaders); | |
| 179 | - | }); | |
| 180 | - | mappingEl.classList.remove('hidden'); | |
| 181 | - | }; | |
| 182 | - | reader.readAsText(file); | |
| 183 | - | }); | |
| 184 | - | ||
| 185 | - | function autoDetectColumn(select, headers) { | |
| 186 | - | const id = select.id; | |
| 187 | - | const lower = headers.map(h => h.toLowerCase().trim()); | |
| 188 | - | const patterns = { | |
| 189 | - | 'map-email': ['email', 'e-mail', 'email_address', 'subscriber_email'], | |
| 190 | - | 'map-name': ['name', 'full_name', 'display_name', 'subscriber_name'], | |
| 191 | - | 'map-amount': ['amount', 'total', 'price', 'payment', 'lifetime_amount'], | |
| 192 | - | 'map-date': ['date', 'created_at', 'joined', 'start_date', 'signup_date'], | |
| 193 | - | 'map-item-title': ['item', 'product', 'title', 'item_title', 'product_name'], | |
| 194 | - | 'map-tier': ['tier', 'plan', 'level', 'membership'], | |
| 195 | - | 'map-status': ['status', 'state', 'active'] | |
| 196 | - | }; | |
| 197 | - | const p = patterns[id] || []; | |
| 198 | - | for (let i = 0; i < lower.length; i++) { | |
| 199 | - | if (p.includes(lower[i])) { | |
| 200 | - | select.value = i; | |
| 201 | - | return; | |
| 202 | - | } | |
| 203 | - | } | |
| 204 | - | } | |
| 205 | - | ||
| 206 | - | startBtn.addEventListener('click', function() { | |
| 207 | - | const projectId = document.getElementById('import-project').value; | |
| 208 | - | if (!csvBase64) { | |
| 209 | - | alert('Please select a CSV file first.'); | |
| 210 | - | return; | |
| 211 | - | } | |
| 212 | - | ||
| 213 | - | const mapping = {}; | |
| 214 | - | const mapEmail = document.getElementById('map-email').value; | |
| 215 | - | const mapName = document.getElementById('map-name').value; | |
| 216 | - | const mapAmount = document.getElementById('map-amount').value; | |
| 217 | - | const mapDate = document.getElementById('map-date').value; | |
| 218 | - | const mapItem = document.getElementById('map-item-title').value; | |
| 219 | - | const mapTier = document.getElementById('map-tier').value; | |
| 220 | - | const mapStatus = document.getElementById('map-status').value; | |
| 221 | - | ||
| 222 | - | if (mapEmail) mapping.email = parseInt(mapEmail); | |
| 223 | - | if (mapName) mapping.name = parseInt(mapName); | |
| 224 | - | if (mapAmount) mapping.amount = parseInt(mapAmount); | |
| 225 | - | if (mapDate) mapping.date = parseInt(mapDate); | |
| 226 | - | if (mapItem) mapping.item_title = parseInt(mapItem); | |
| 227 | - | if (mapTier) mapping.tier = parseInt(mapTier); | |
| 228 | - | if (mapStatus) mapping.status = parseInt(mapStatus); | |
| 229 | - | ||
| 230 | - | if (!mapping.email && !mapping.amount) { | |
| 231 | - | alert('Please map at least an email or amount column.'); | |
| 232 | - | return; | |
| 233 | - | } | |
| 234 | - | ||
| 235 | - | startBtn.disabled = true; | |
| 236 | - | startBtn.textContent = 'Starting...'; | |
| 237 | - | ||
| 238 | - | fetch('/api/users/me/import', { | |
| 239 | - | method: 'POST', | |
| 240 | - | headers: { | |
| 241 | - | 'Content-Type': 'application/json', | |
| 242 | - | 'X-CSRF-Token': '{{ csrf_token.as_deref().unwrap_or_default() }}' | |
| 243 | - | }, | |
| 244 | - | body: JSON.stringify({ | |
| 245 | - | project_id: projectId, | |
| 246 | - | source: 'generic_csv', | |
| 247 | - | csv_data: csvBase64, | |
| 248 | - | column_mapping: mapping | |
| 249 | - | }) | |
| 250 | - | }) | |
| 251 | - | .then(r => r.json()) | |
| 252 | - | .then(data => { | |
| 253 | - | if (data.error) { | |
| 254 | - | resultEl.className = 'import-result error'; | |
| 255 | - | resultEl.textContent = data.error; | |
| 256 | - | startBtn.disabled = false; | |
| 257 | - | startBtn.textContent = 'Start Import'; | |
| 258 | - | return; | |
| 259 | - | } | |
| 260 | - | progressEl.classList.remove('hidden'); | |
| 261 | - | pollProgress(data.job_id); | |
| 262 | - | }) | |
| 263 | - | .catch(err => { | |
| 264 | - | resultEl.className = 'import-result error'; | |
| 265 | - | resultEl.textContent = 'Failed to start import: ' + err.message; | |
| 266 | - | startBtn.disabled = false; | |
| 267 | - | startBtn.textContent = 'Start Import'; | |
| 268 | - | }); | |
| 269 | - | }); | |
| 270 | - | ||
| 271 | - | function pollProgress(jobId) { | |
| 272 | - | const interval = setInterval(() => { | |
| 273 | - | fetch('/api/users/me/import/' + jobId) | |
| 274 | - | .then(r => r.json()) | |
| 275 | - | .then(data => { | |
| 276 | - | const pct = data.total_rows > 0 | |
| 277 | - | ? Math.round((data.processed_rows / data.total_rows) * 100) | |
| 278 | - | : 0; | |
| 279 | - | document.getElementById('progress-bar').style.width = pct + '%'; | |
| 280 | - | document.getElementById('stat-processed').textContent = data.processed_rows; | |
| 281 | - | document.getElementById('stat-created').textContent = data.created_rows; | |
| 282 | - | document.getElementById('stat-skipped').textContent = data.skipped_rows; | |
| 283 | - | ||
| 284 | - | if (data.status === 'completed' || data.status === 'failed') { | |
| 285 | - | clearInterval(interval); | |
| 286 | - | progressEl.classList.add('hidden'); | |
| 287 | - | ||
| 288 | - | resultEl.classList.remove('hidden'); | |
| 289 | - | if (data.status === 'completed') { | |
| 290 | - | resultEl.className = 'import-result success'; | |
| 291 | - | resultEl.innerHTML = '<strong>Import complete.</strong> ' + | |
| 292 | - | data.created_rows + ' created, ' + | |
| 293 | - | data.skipped_rows + ' skipped.'; | |
| 294 | - | } else { | |
| 295 | - | resultEl.className = 'import-result error'; | |
| 296 | - | resultEl.innerHTML = '<strong>Import failed.</strong>'; | |
| 297 | - | } | |
| 298 | - | if (data.error_log) { | |
| 299 | - | resultEl.innerHTML += '<div class="error-log">' + | |
| 300 | - | escapeHtml(data.error_log) + '</div>'; | |
| 301 | - | } | |
| 302 | - | startBtn.disabled = false; | |
| 303 | - | startBtn.textContent = 'Start Import'; | |
| 304 | - | } | |
| 305 | - | }); | |
| 306 | - | }, 2000); | |
| 307 | - | } | |
| 308 | - | ||
| 309 | - | function parseCSVLine(line) { | |
| 310 | - | const result = []; | |
| 311 | - | let current = ''; | |
| 312 | - | let inQuotes = false; | |
| 313 | - | for (let i = 0; i < line.length; i++) { | |
| 314 | - | const ch = line[i]; | |
| 315 | - | if (inQuotes) { | |
| 316 | - | if (ch === '"' && line[i+1] === '"') { current += '"'; i++; } | |
| 317 | - | else if (ch === '"') { inQuotes = false; } | |
| 318 | - | else { current += ch; } | |
| 319 | - | } else { | |
| 320 | - | if (ch === '"') { inQuotes = true; } | |
| 321 | - | else if (ch === ',') { result.push(current.trim()); current = ''; } | |
| 322 | - | else { current += ch; } | |
| 323 | - | } | |
| 324 | - | } | |
| 325 | - | result.push(current.trim()); | |
| 326 | - | return result; | |
| 327 | - | } | |
| 328 | - | ||
| 329 | - | function escapeHtml(s) { | |
| 330 | - | return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| 331 | - | } | |
| 332 | - | })(); | |
| 333 | - | </script> | |
| 120 | + | <div id="dashboard-import-inline-cfg" hidden data-csrf-token="{{ csrf_token.as_deref().unwrap_or_default() }}"></div> | |
| 121 | + | <script src="/static/dashboard-import-inline.js?v=0623" defer></script> | |
| 334 | 122 | {% endblock %} |
| @@ -66,33 +66,6 @@ | |||
| 66 | 66 | </div> | |
| 67 | 67 | </div> | |
| 68 | 68 | </div> | |
| 69 | - | <script> | |
| 70 | - | const audio = new Audio(); | |
| 71 | - | let loaded = false; | |
| 72 | - | // Read the preview URL from the data- attribute rather than interpolating it | |
| 73 | - | // into this JS string. Askama autoescapes for HTML, which is the correct | |
| 74 | - | // escaper for an attribute value but NOT for a JS string literal — keeping the | |
| 75 | - | // URL in the attribute keeps escaping correct even once preview URLs derive | |
| 76 | - | // from user-influenced filenames. | |
| 77 | - | const previewUrl = document.querySelector('.player').dataset.previewUrl; | |
| 78 | - | function togglePlay() { | |
| 79 | - | if (!loaded) { audio.src = previewUrl; loaded = true; } | |
| 80 | - | if (audio.paused) { audio.play(); document.getElementById('play').innerHTML = '▮▮'; } | |
| 81 | - | else { audio.pause(); document.getElementById('play').innerHTML = '▶'; } | |
| 82 | - | } | |
| 83 | - | audio.ontimeupdate = () => { | |
| 84 | - | const pct = (audio.currentTime / audio.duration) * 100; | |
| 85 | - | document.getElementById('progress').style.width = pct + '%'; | |
| 86 | - | const m = Math.floor(audio.currentTime / 60); | |
| 87 | - | const s = Math.floor(audio.currentTime % 60); | |
| 88 | - | document.getElementById('time').textContent = m + ':' + (s < 10 ? '0' : '') + s; | |
| 89 | - | }; | |
| 90 | - | audio.onended = () => { document.getElementById('play').innerHTML = '▶'; }; | |
| 91 | - | function seek(e) { | |
| 92 | - | if (!audio.duration) return; | |
| 93 | - | const rect = e.currentTarget.getBoundingClientRect(); | |
| 94 | - | audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration; | |
| 95 | - | } | |
| 96 | - | </script> | |
| 69 | + | <script src="/static/embed-item-player.js?v=0623" defer></script> | |
| 97 | 70 | </body> | |
| 98 | 71 | </html> |
| @@ -52,40 +52,8 @@ | |||
| 52 | 52 | </div> | |
| 53 | 53 | ||
| 54 | 54 | {% if !item.is_free %} | |
| 55 | - | <script> | |
| 56 | - | function buy() { | |
| 57 | - | const btn = document.getElementById('buy-btn'); | |
| 58 | - | btn.disabled = true; | |
| 59 | - | btn.textContent = 'Redirecting...'; | |
| 60 | - | ||
| 61 | - | const body = {}; | |
| 62 | - | {% if pwyw_enabled %} | |
| 63 | - | const amount = parseFloat(document.getElementById('pwyw_amount').value); | |
| 64 | - | body.amount_cents = Math.round(amount * 100); | |
| 65 | - | {% endif %} | |
| 66 | - | ||
| 67 | - | fetch('{{ host_url }}/api/checkout/guest/{{ item.id }}', { | |
| 68 | - | method: 'POST', | |
| 69 | - | headers: { 'Content-Type': 'application/json' }, | |
| 70 | - | body: JSON.stringify(body), | |
| 71 | - | }) | |
| 72 | - | .then(r => r.json()) | |
| 73 | - | .then(data => { | |
| 74 | - | if (data.checkout_url) { | |
| 75 | - | window.location.href = data.checkout_url; | |
| 76 | - | } else { | |
| 77 | - | btn.disabled = false; | |
| 78 | - | btn.textContent = 'Buy Now'; | |
| 79 | - | alert(data.error || 'Something went wrong'); | |
| 80 | - | } | |
| 81 | - | }) | |
| 82 | - | .catch(() => { | |
| 83 | - | btn.disabled = false; | |
| 84 | - | btn.textContent = 'Buy Now'; | |
| 85 | - | alert('Something went wrong. Please try again.'); | |
| 86 | - | }); | |
| 87 | - | } | |
| 88 | - | </script> | |
| 55 | + | <div id="page-buy-cfg" hidden data-host-url="{{ host_url }}" data-item-id="{{ item.id }}" data-pwyw-enabled="{% if pwyw_enabled %}true{% else %}false{% endif %}"></div> | |
| 56 | + | <script src="/static/page-buy.js?v=0623" defer></script> | |
| 89 | 57 | {% endif %} | |
| 90 | 58 | </body> | |
| 91 | 59 | </html> |
| @@ -169,55 +169,5 @@ | |||
| 169 | 169 | {% endif %} | |
| 170 | 170 | </div> | |
| 171 | 171 | ||
| 172 | - | <script> | |
| 173 | - | (function() { | |
| 174 | - | document.querySelectorAll('.pwyw-cart-input').forEach(function(input) { | |
| 175 | - | var debounce; | |
| 176 | - | input.addEventListener('change', function() { | |
| 177 | - | clearTimeout(debounce); | |
| 178 | - | var itemId = this.dataset.itemId; | |
| 179 | - | var dollars = parseFloat(this.value) || 0; | |
| 180 | - | var cents = Math.round(dollars * 100); | |
| 181 | - | debounce = setTimeout(function() { | |
| 182 | - | fetch('/api/cart/' + itemId, { | |
| 183 | - | method: 'PUT', | |
| 184 | - | headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()), | |
| 185 | - | body: JSON.stringify({ amount_cents: cents }) | |
| 186 | - | }).then(function(r) { | |
| 187 | - | if (!r.ok) return r.json().then(function(d) { showToast(d.error || 'Invalid amount'); }); | |
| 188 | - | }).catch(function() { showToast('Failed to update amount'); }); | |
| 189 | - | }, 300); | |
| 190 | - | }); | |
| 191 | - | }); | |
| 192 | - | })(); | |
| 193 | - | ||
| 194 | - | window.removeCartGroup = function(btn) { | |
| 195 | - | var name = btn.dataset.sellerName || 'this creator'; | |
| 196 | - | if (!confirm('Remove all items from ' + name + '?')) return; | |
| 197 | - | var group = btn.closest('.cart-group'); | |
| 198 | - | if (!group) return; | |
| 199 | - | var rows = group.querySelectorAll('tr[id^="cart-row-"]'); | |
| 200 | - | if (rows.length === 0) return; | |
| 201 | - | btn.disabled = true; | |
| 202 | - | var headers = csrfHeaders(); | |
| 203 | - | var pending = rows.length; | |
| 204 | - | var failed = false; | |
| 205 | - | function finish() { | |
| 206 | - | if (--pending !== 0) return; | |
| 207 | - | if (failed) { | |
| 208 | - | showToast('Some items could not be removed. Refreshing.'); | |
| 209 | - | window.location.reload(); | |
| 210 | - | } else { | |
| 211 | - | window.location.reload(); | |
| 212 | - | } | |
| 213 | - | } | |
| 214 | - | rows.forEach(function(row) { | |
| 215 | - | var id = row.id.replace('cart-row-', ''); | |
| 216 | - | fetch('/api/cart/' + id, { method: 'DELETE', headers: headers }) | |
| 217 | - | .then(function(r) { if (!r.ok) failed = true; }) | |
| 218 | - | .catch(function() { failed = true; }) | |
| 219 | - | .finally(finish); | |
| 220 | - | }); | |
| 221 | - | }; | |
| 222 | - | </script> | |
| 172 | + | <script src="/static/page-cart.js?v=0623" defer></script> | |
| 223 | 173 | {% endblock %} |
| @@ -247,120 +247,5 @@ | |||
| 247 | 247 | {% endblock %} | |
| 248 | 248 | ||
| 249 | 249 | {% block scripts %} | |
| 250 | - | <script> | |
| 251 | - | // Sync filter UI state on filter click: update active highlight and write | |
| 252 | - | // the selected filter value into the hidden form input so subsequent | |
| 253 | - | // search/sort submissions include it. | |
| 254 | - | document.body.addEventListener('htmx:beforeRequest', function(evt) { | |
| 255 | - | if (evt.detail.elt.classList.contains('filter-item')) { | |
| 256 | - | // Only deactivate siblings in the same filter section | |
| 257 | - | var section = evt.detail.elt.closest('.filter-list'); | |
| 258 | - | if (section) { | |
| 259 | - | section.querySelectorAll('.filter-item').forEach(function(i) { i.classList.remove('is-selected'); }); | |
| 260 | - | } | |
| 261 | - | evt.detail.elt.classList.add('is-selected'); | |
| 262 | - | ||
| 263 | - | var hxVals = JSON.parse(evt.detail.elt.getAttribute('hx-vals') || '{}'); | |
| 264 | - | if ('item_type' in hxVals) { | |
| 265 | - | document.getElementById('type-input').value = hxVals.item_type || ''; | |
| 266 | - | } | |
| 267 | - | if ('tag' in hxVals) { | |
| 268 | - | document.getElementById('tag-input').value = hxVals.tag || ''; | |
| 269 | - | } | |
| 270 | - | if ('category' in hxVals) { | |
| 271 | - | document.getElementById('category-input').value = hxVals.category || ''; | |
| 272 | - | } | |
| 273 | - | if ('label' in hxVals) { | |
| 274 | - | document.getElementById('label-input').value = hxVals.label || ''; | |
| 275 | - | } | |
| 276 | - | if ('ai_tier' in hxVals) { | |
| 277 | - | document.getElementById('ai-tier-input').value = hxVals.ai_tier || ''; | |
| 278 | - | } | |
| 279 | - | } | |
| 280 | - | }); | |
| 281 | - | ||
| 282 | - | // View toggle (list vs grid) with localStorage persistence. | |
| 283 | - | // Re-applied after HTMX swaps because new content replaces the container. | |
| 284 | - | (function() { | |
| 285 | - | function applyView(view) { | |
| 286 | - | var container = document.getElementById('results-container-inner'); | |
| 287 | - | if (container) { | |
| 288 | - | container.className = 'results-container results-' + view; | |
| 289 | - | } | |
| 290 | - | document.querySelectorAll('.view-btn').forEach(function(btn) { | |
| 291 | - | btn.classList.toggle('is-selected', btn.dataset.view === view); | |
| 292 | - | }); | |
| 293 | - | } | |
| 294 | - | ||
| 295 | - | // Load saved preference on page load | |
| 296 | - | document.addEventListener('DOMContentLoaded', function() { | |
| 297 | - | var saved = safeStorageGet('discoverViewPref') || 'grid'; | |
| 298 | - | applyView(saved); | |
| 299 | - | }); | |
| 300 | - | ||
| 301 | - | // Handle view button clicks | |
| 302 | - | document.querySelectorAll('.view-btn').forEach(function(btn) { | |
| 303 | - | btn.addEventListener('click', function() { | |
| 304 | - | var view = btn.dataset.view; | |
| 305 | - | applyView(view); | |
| 306 | - | safeStorageSet('discoverViewPref', view); | |
| 307 | - | }); | |
| 308 | - | }); | |
| 309 | - | ||
| 310 | - | // Re-apply view preference after HTMX swaps new content | |
| 311 | - | document.body.addEventListener('htmx:afterSwap', function(evt) { | |
| 312 | - | if (evt.detail.target.id === 'results-container') { | |
| 313 | - | var saved = safeStorageGet('discoverViewPref') || 'grid'; | |
| 314 | - | applyView(saved); | |
| 315 | - | } | |
| 316 | - | }); | |
| 317 | - | })(); | |
| 318 | - | ||
| 319 | - | // Search suggestions autocomplete | |
| 320 | - | (function() { | |
| 321 | - | var input = document.getElementById('search-input'); | |
| 322 | - | var box = document.getElementById('search-suggestions'); | |
| 323 | - | var timer = null; | |
| 324 | - | var selectedIdx = -1; | |
| 325 | - | ||
| 326 | - | input.addEventListener('input', function() { | |
| 327 | - | clearTimeout(timer); | |
| 328 | - | var q = input.value.trim(); | |
| 329 | - | if (q.length < 2) { box.innerHTML = ''; box.style.display = 'none'; return; } | |
| 330 | - | timer = setTimeout(function() { | |
| 331 | - | fetch('/discover/suggestions?q=' + encodeURIComponent(q)) | |
| 332 | - | .then(function(r) { return r.json(); }) | |
| 333 | - | .then(function(items) { | |
| 334 | - | if (items.length === 0) { box.innerHTML = ''; box.style.display = 'none'; return; } | |
| 335 | - | selectedIdx = -1; | |
| 336 | - | box.innerHTML = items.map(function(s, i) { | |
| 337 | - | return '<a href="' + escapeHtml(s.url) + '" class="suggestion-item" data-idx="' + i + '">' | |
| 338 | - | + '<span class="suggestion-label">' + escapeHtml(s.label) + '</span>' | |
| 339 | - | + '<span class="suggestion-category">' + escapeHtml(s.category) + '</span></a>'; | |
| 340 | - | }).join(''); | |
| 341 | - | box.style.display = 'block'; | |
| 342 | - | }); | |
| 343 | - | }, 200); | |
| 344 | - | }); | |
| 345 | - | ||
| 346 | - | input.addEventListener('keydown', function(e) { | |
| 347 | - | var items = box.querySelectorAll('.suggestion-item'); | |
| 348 | - | if (!items.length) return; | |
| 349 | - | if (e.key === 'ArrowDown') { e.preventDefault(); selectedIdx = Math.min(selectedIdx + 1, items.length - 1); updateHighlight(items); } | |
| 350 | - | else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIdx = Math.max(selectedIdx - 1, -1); updateHighlight(items); } | |
| 351 | - | else if (e.key === 'Enter' && selectedIdx >= 0) { e.preventDefault(); items[selectedIdx].click(); } | |
| 352 | - | else if (e.key === 'Escape') { box.style.display = 'none'; } | |
| 353 | - | }); | |
| 354 | - | ||
| 355 | - | function updateHighlight(items) { | |
| 356 | - | items.forEach(function(el, i) { el.classList.toggle('highlighted', i === selectedIdx); }); | |
| 357 | - | } | |
| 358 | - | ||
| 359 | - | function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } | |
| 360 | - | ||
| 361 | - | document.addEventListener('click', function(e) { | |
| 362 | - | if (!box.contains(e.target) && e.target !== input) { box.style.display = 'none'; } | |
| 363 | - | }); | |
| 364 | - | })(); | |
| 365 | - | </script> | |
| 250 | + | <script src="/static/page-discover.js?v=0623" defer></script> | |
| 366 | 251 | {% endblock %} |
| @@ -224,36 +224,5 @@ | |||
| 224 | 224 | {% endblock %} | |
| 225 | 225 | ||
| 226 | 226 | {% block scripts %} | |
| 227 | - | <script> | |
| 228 | - | function submitNotify(e) { | |
| 229 | - | e.preventDefault(); | |
| 230 | - | var form = document.getElementById('notify-form'); | |
| 231 | - | var status = document.getElementById('notify-status'); | |
| 232 | - | var email = form.querySelector('input[name="email"]').value; | |
| 233 | - | var btn = form.querySelector('button'); | |
| 234 | - | btn.disabled = true; | |
| 235 | - | btn.textContent = 'Sending...'; | |
| 236 | - | status.textContent = ''; | |
| 237 | - | fetch('/api/email-signup', { | |
| 238 | - | method: 'POST', | |
| 239 | - | headers: {'Content-Type': 'application/json'}, | |
| 240 | - | body: JSON.stringify({email: email}) | |
| 241 | - | }).then(function(r) { | |
| 242 | - | if (r.ok) { | |
| 243 | - | status.textContent = 'You\'re on the list.'; | |
| 244 | - | status.className = 'notify-status success'; | |
| 245 | - | form.querySelector('input[name="email"]').value = ''; | |
| 246 | - | } else { | |
| 247 | - | return r.json().then(function(d) { throw new Error(d.error || 'Something went wrong'); }); | |
| 248 | - | } | |
| 249 | - | }).catch(function(err) { | |
| 250 | - | status.textContent = err.message; | |
| 251 | - | status.className = 'notify-status error'; | |
| 252 | - | }).finally(function() { | |
| 253 | - | btn.disabled = false; | |
| 254 | - | btn.textContent = 'Notify Me'; | |
| 255 | - | }); | |
| 256 | - | return false; | |
| 257 | - | } | |
| 258 | - | </script> | |
| 227 | + | <script src="/static/page-index.js?v=0623" defer></script> | |
| 259 | 228 | {% endblock %} |
| @@ -260,21 +260,8 @@ | |||
| 260 | 260 | <details> | |
| 261 | 261 | <summary class="license-summary">View full license text</summary> | |
| 262 | 262 | <pre class="license-text" id="license-text-content">Loading...</pre> | |
| 263 | - | <script> | |
| 264 | - | (function() { | |
| 265 | - | var details = document.currentScript.closest('details'); | |
| 266 | - | var loaded = false; | |
| 267 | - | details.addEventListener('toggle', function() { | |
| 268 | - | if (details.open && !loaded) { | |
| 269 | - | loaded = true; | |
| 270 | - | fetch('/api/items/{{ item.id }}/license.txt') | |
| 271 | - | .then(function(r) { return r.text(); }) | |
| 272 | - | .then(function(t) { document.getElementById('license-text-content').textContent = t; }) | |
| 273 | - | .catch(function() { document.getElementById('license-text-content').textContent = 'Failed to load license text.'; }); | |
| 274 | - | } | |
| 275 | - | }); | |
| 276 | - | })(); | |
| 277 | - | </script> | |
| 263 | + | <div id="page-item-1-cfg" hidden data-item-id="{{ item.id }}"></div> | |
| 264 | + | <script src="/static/page-item-1.js?v=0623" defer></script> | |
| 278 | 265 | </details> | |
| 279 | 266 | <p class="license-download"> | |
| 280 | 267 | <a href="/api/items/{{ item.id }}/license.txt" download="LICENSE.txt">Download LICENSE.txt</a> | |
| @@ -343,82 +330,7 @@ | |||
| 343 | 330 | {% endblock %} | |
| 344 | 331 | ||
| 345 | 332 | {% block scripts %} | |
| 346 | - | <script> | |
| 347 | - | function switchSectionTab(btn, panelId) { | |
| 348 | - | document.querySelectorAll('.section-tab').forEach(function(t) { t.classList.remove('is-selected'); }); | |
| 349 | - | document.querySelectorAll('.section-panel').forEach(function(p) { p.classList.remove('active'); }); | |
| 350 | - | btn.classList.add('is-selected'); | |
| 351 | - | var panel = document.getElementById(panelId); | |
| 352 | - | if (panel) panel.classList.add('active'); | |
| 353 | - | history.replaceState(null, '', '#' + panelId); | |
| 354 | - | } | |
| 355 | - | ||
| 356 | - | (function() { | |
| 357 | - | var hash = window.location.hash.replace('#', ''); | |
| 358 | - | if (hash) { | |
| 359 | - | var panel = document.getElementById(hash); | |
| 360 | - | var tab = document.querySelector('[data-tab="' + hash + '"]'); | |
| 361 | - | if (panel && tab) switchSectionTab(tab, hash); | |
| 362 | - | } | |
| 363 | - | })(); | |
| 364 | - | ||
| 365 | - | (function() { | |
| 366 | - | var player = document.getElementById('item-player'); | |
| 367 | - | if (player) { | |
| 368 | - | var loaded = false; | |
| 369 | - | player.addEventListener('play', function loadSrc() { | |
| 370 | - | if (!loaded) { | |
| 371 | - | loaded = true; | |
| 372 | - | player.pause(); | |
| 373 | - | fetch('/api/stream/{{ item.id }}') | |
| 374 | - | .then(function(r) { | |
| 375 | - | if (!r.ok) throw new Error('Stream unavailable'); | |
| 376 | - | return r.json(); | |
| 377 | - | }) | |
| 378 | - | .then(function(data) { | |
| 379 | - | player.src = data.stream_url; | |
| 380 | - | player.play(); | |
| 381 | - | }) | |
| 382 | - | .catch(function(err) { | |
| 383 | - | showToast(err.message || 'Could not load video'); | |
| 384 | - | }); | |
| 385 | - | } | |
| 386 | - | }, { once: true }); | |
| 387 | - | } | |
| 388 | - | })(); | |
| 389 | - | </script> | |
| 390 | - | ||
| 391 | - | <script> | |
| 392 | - | (function() { | |
| 393 | - | window.toggleWishlist = function(itemId) { | |
| 394 | - | var btn = document.getElementById('wishlist-btn'); | |
| 395 | - | fetch('/api/wishlists/' + itemId, { method: 'POST', headers: csrfHeaders() }) | |
| 396 | - | .then(function(r) { return r.json(); }) | |
| 397 | - | .then(function(data) { | |
| 398 | - | if (data.wishlisted) { | |
| 399 | - | btn.textContent = 'Wishlisted'; | |
| 400 | - | } else { | |
| 401 | - | btn.textContent = 'Add to Wishlist'; | |
| 402 | - | } | |
| 403 | - | }); | |
| 404 | - | }; | |
| 405 | - | ||
| 406 | - | window.toggleCart = function(itemId) { | |
| 407 | - | var btn = document.getElementById('cart-btn'); | |
| 408 | - | fetch('/api/cart/' + itemId, { method: 'POST', headers: csrfHeaders() }) | |
| 409 | - | .then(function(r) { return r.json(); }) | |
| 410 | - | .then(function(data) { | |
| 411 | - | if (data.in_cart) { | |
| 412 | - | btn.textContent = 'In Cart'; | |
| 413 | - | showToast('Added to cart. Buying multiple items together saves the creator on processing fees.', 'info'); | |
| 414 | - | } else { | |
| 415 | - | btn.textContent = 'Add to Cart'; | |
| 416 | - | } | |
| 417 | - | }) | |
| 418 | - | .catch(function(err) { | |
| 419 | - | showToast(err.message || 'Failed to update cart'); | |
| 420 | - | }); | |
| 421 | - | }; | |
| 422 | - | })(); | |
| 423 | - | </script> | |
| 333 | + | <div id="page-item-2-cfg" hidden data-item-id="{{ item.id }}"></div> | |
| 334 | + | <script src="/static/page-item-2.js?v=0623" defer></script> | |
| 335 | + | <script src="/static/page-item-3.js?v=0623" defer></script> | |
| 424 | 336 | {% endblock %} |
| @@ -154,19 +154,5 @@ | |||
| 154 | 154 | } | |
| 155 | 155 | </script> | |
| 156 | 156 | <script src="/static/media-player.js"></script> | |
| 157 | - | <script> | |
| 158 | - | function downloadVersion(versionId) { | |
| 159 | - | fetch('/api/versions/' + versionId + '/download') | |
| 160 | - | .then(function(res) { | |
| 161 | - | if (!res.ok) throw new Error('Failed to get download URL'); | |
| 162 | - | return res.json(); | |
| 163 | - | }) | |
| 164 | - | .then(function(data) { | |
| 165 | - | window.location.href = data.download_url; | |
| 166 | - | }) | |
| 167 | - | .catch(function(err) { | |
| 168 | - | showToast(err.message || 'Download failed'); | |
| 169 | - | }); | |
| 170 | - | } | |
| 171 | - | </script> | |
| 157 | + | <script src="/static/page-library-audio.js?v=0623" defer></script> | |
| 172 | 158 | {% endblock %} |
| @@ -145,37 +145,5 @@ | |||
| 145 | 145 | {% endblock %} | |
| 146 | 146 | ||
| 147 | 147 | {% block scripts %} | |
| 148 | - | <script> | |
| 149 | - | function switchSectionTab(btn, panelId) { | |
| 150 | - | document.querySelectorAll('.section-tab').forEach(function(t) { t.classList.remove('is-selected'); }); | |
| 151 | - | document.querySelectorAll('.section-panel').forEach(function(p) { p.classList.remove('active'); }); | |
| 152 | - | btn.classList.add('is-selected'); | |
| 153 | - | var panel = document.getElementById(panelId); | |
| 154 | - | if (panel) panel.classList.add('active'); | |
| 155 | - | history.replaceState(null, '', '#' + panelId); | |
| 156 | - | } | |
| 157 | - | ||
| 158 | - | (function() { | |
| 159 | - | var hash = window.location.hash.replace('#', ''); | |
| 160 | - | if (hash) { | |
| 161 | - | var panel = document.getElementById(hash); | |
| 162 | - | var tab = document.querySelector('[data-tab="' + hash + '"]'); | |
| 163 | - | if (panel && tab) switchSectionTab(tab, hash); | |
| 164 | - | } | |
| 165 | - | })(); | |
| 166 | - | ||
| 167 | - | function downloadVersion(versionId) { | |
| 168 | - | fetch('/api/versions/' + versionId + '/download') | |
| 169 | - | .then(function(res) { | |
| 170 | - | if (!res.ok) throw new Error('Failed to get download URL'); | |
| 171 | - | return res.json(); | |
| 172 | - | }) | |
| 173 | - | .then(function(data) { | |
| 174 | - | window.location.href = data.download_url; | |
| 175 | - | }) | |
| 176 | - | .catch(function(err) { | |
| 177 | - | showToast(err.message || 'Download failed'); | |
| 178 | - | }); | |
| 179 | - | } | |
| 180 | - | </script> | |
| 148 | + | <script src="/static/page-library-downloads.js?v=0623" defer></script> | |
| 181 | 149 | {% endblock %} |
| @@ -146,19 +146,5 @@ | |||
| 146 | 146 | } | |
| 147 | 147 | </script> | |
| 148 | 148 | <script src="/static/media-player.js"></script> | |
| 149 | - | <script> | |
| 150 | - | function downloadVersion(versionId) { | |
| 151 | - | fetch('/api/versions/' + versionId + '/download') | |
| 152 | - | .then(function(res) { | |
| 153 | - | if (!res.ok) throw new Error('Failed to get download URL'); | |
| 154 | - | return res.json(); | |
| 155 | - | }) | |
| 156 | - | .then(function(data) { | |
| 157 | - | window.location.href = data.download_url; | |
| 158 | - | }) | |
| 159 | - | .catch(function(err) { | |
| 160 | - | showToast(err.message || 'Download failed'); | |
| 161 | - | }); | |
| 162 | - | } | |
| 163 | - | </script> | |
| 149 | + | <script src="/static/page-library-video.js?v=0623" defer></script> | |
| 164 | 150 | {% endblock %} |