Skip to main content

max / makenotwork

Add "Save to collection" UI on item pages and library Two new UI entry points for adding items to collections: 1. Item page: "Save to collection" button below purchase area (logged-in users only). Dropdown shows user's collections as checkboxes with membership state. Includes inline "New collection" form. 2. Library purchases: "Add to collection" in context menu. Same checkbox dropdown pattern. Backend was already complete — this is frontend-only (JS + HTML). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-03 03:03 UTC
Commit: 68e4600500e2a02ea8b1a9621d4a7c0c34d7d585
Parent: d86a0c2
3 files changed, +151 insertions, -2 deletions
@@ -31,7 +31,7 @@ Usability audit grade: B. Complexity C+, Completeness B+, Learnability B, Discov
31 31
32 32 ### Critical (broken or high-dropout flows)
33 33
34 - - [ ] **[HIGH]** Add "Add to collection" UI — fans can create collections but cannot add items to them. Add dropdown/button on library purchase rows and on item pages (`library_purchases.html`, `item.html`)
34 + - [x] **[HIGH]** Add "Add to collection" UI — "Save to collection" dropdown on item pages + "Add to collection" in library purchase context menus. Checkbox toggle, inline create, click-outside-to-close
35 35 - [x] **[HIGH]** Consolidate item creation wizard from 8 steps to 6 — merged Details+Appearance into "Basics" step, removed Distribution step (already in dashboard Pricing tab)
36 36 - [ ] **[HIGH]** Add global search to site header — search only exists on /discover page. Add input to `site_header.html` with Cmd+K shortcut
37 37
@@ -240,6 +240,27 @@
240 240 </div>
241 241 {% endif %}
242 242 {% endif %}
243 +
244 + {% if session_user.is_some() %}
245 + <div style="margin-top: 1rem; position: relative;">
246 + <button class="secondary" style="width: 100%; font-size: 0.9rem;"
247 + onclick="toggleCollectionDropdown('{{ item.id }}')">Save to collection</button>
248 + <div id="collection-dropdown"
249 + style="display: none; position: absolute; left: 0; right: 0; top: 100%; z-index: 10;
250 + background: var(--background); border: 1px solid var(--border);
251 + box-shadow: 0 2px 8px rgba(0,0,0,0.1); max-height: 240px; overflow-y: auto;">
252 + <div id="collection-list" style="padding: 0.5rem;"></div>
253 + <div style="border-top: 1px solid var(--border); padding: 0.5rem;">
254 + <form onsubmit="createAndAddCollection('{{ item.id }}', this); return false;"
255 + style="display: flex; gap: 0.5rem;">
256 + <input type="text" name="title" placeholder="New collection" required
257 + style="flex: 1; padding: 0.3rem 0.5rem; font-size: 0.85rem;">
258 + <button class="secondary" type="submit" style="font-size: 0.85rem; white-space: nowrap;">Create</button>
259 + </form>
260 + </div>
261 + </div>
262 + </div>
263 + {% endif %}
243 264 </div>
244 265 </div>
245 266
@@ -457,4 +478,86 @@ function downloadVersion(versionId) {
457 478 }
458 479 })();
459 480 </script>
481 +
482 + <script>
483 + (function() {
484 + var dropdownOpen = false;
485 +
486 + window.toggleCollectionDropdown = function(itemId) {
487 + var dd = document.getElementById('collection-dropdown');
488 + if (!dd) return;
489 + dropdownOpen = !dropdownOpen;
490 + dd.style.display = dropdownOpen ? 'block' : 'none';
491 + if (dropdownOpen) loadCollections(itemId);
492 + };
493 +
494 + function loadCollections(itemId) {
495 + var list = document.getElementById('collection-list');
496 + list.innerHTML = '<div style="padding: 0.5rem; opacity: 0.6; font-size: 0.85rem;">Loading...</div>';
497 + fetch('/api/collections/for-item/' + itemId, { headers: csrfHeaders() })
498 + .then(function(r) { return r.json(); })
499 + .then(function(collections) {
500 + if (collections.length === 0) {
501 + list.innerHTML = '<div style="padding: 0.5rem; opacity: 0.6; font-size: 0.85rem;">No collections yet. Create one below.</div>';
502 + return;
503 + }
504 + var html = '';
505 + for (var i = 0; i < collections.length; i++) {
506 + var c = collections[i];
507 + html += '<label style="display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; cursor: pointer; font-size: 0.9rem;">'
508 + + '<input type="checkbox"' + (c.in_collection ? ' checked' : '')
509 + + ' onchange="toggleCollectionItem(\'' + c.id + '\', \'' + itemId + '\', this.checked)">'
510 + + escapeHtml(c.title) + '</label>';
511 + }
512 + list.innerHTML = html;
513 + })
514 + .catch(function() {
515 + list.innerHTML = '<div style="padding: 0.5rem; color: var(--danger); font-size: 0.85rem;">Failed to load collections.</div>';
516 + });
517 + }
518 +
519 + window.toggleCollectionItem = function(collectionId, itemId, add) {
520 + fetch('/api/collections/' + collectionId + '/items/' + itemId, {
521 + method: add ? 'POST' : 'DELETE',
522 + headers: csrfHeaders()
523 + });
524 + };
525 +
526 + window.createAndAddCollection = function(itemId, form) {
527 + var title = form.title.value.trim();
528 + if (!title) return;
529 + var slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
530 + fetch('/api/collections', {
531 + method: 'POST',
532 + headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
533 + body: JSON.stringify({ title: title, slug: slug, description: '', is_public: false })
534 + })
535 + .then(function(r) { return r.json(); })
536 + .then(function(col) {
537 + return fetch('/api/collections/' + col.id + '/items/' + itemId, {
538 + method: 'POST',
539 + headers: csrfHeaders()
540 + });
541 + })
542 + .then(function() {
543 + form.title.value = '';
544 + loadCollections(itemId);
545 + });
546 + };
547 +
548 + function escapeHtml(s) {
549 + var d = document.createElement('div');
550 + d.textContent = s;
551 + return d.innerHTML;
552 + }
553 +
554 + document.addEventListener('click', function(e) {
555 + var dd = document.getElementById('collection-dropdown');
556 + if (dd && dropdownOpen && !dd.parentElement.contains(e.target)) {
557 + dd.style.display = 'none';
558 + dropdownOpen = false;
559 + }
560 + });
561 + })();
562 + </script>
460 563 {% endblock %}
@@ -110,6 +110,8 @@
110 110 <button class="context-menu-btn" onclick="toggleContextMenu(event, '{{ item.item_id }}')">...</button>
111 111 <div class="context-menu" id="menu-{{ item.item_id }}">
112 112 <a href="/i/{{ item.item_id }}" class="context-menu-item">View</a>
113 + <button class="context-menu-item"
114 + onclick="openLibraryCollectionPicker('{{ item.item_id }}', this)">Add to collection</button>
113 115 {% if item.is_free %}
114 116 <button class="context-menu-item danger"
115 117 hx-delete="/api/library/remove/{{ item.item_id }}"
@@ -137,7 +139,51 @@
137 139 document.querySelectorAll('.context-menu').forEach(function(m) { m.classList.remove('open'); });
138 140 if (!wasOpen) { menu.classList.add('open'); }
139 141 }
140 - document.addEventListener('click', function() {
142 + document.addEventListener('click', function(e) {
141 143 document.querySelectorAll('.context-menu').forEach(function(m) { m.classList.remove('open'); });
144 + var picker = document.getElementById('library-collection-picker');
145 + if (picker && !picker.contains(e.target)) picker.remove();
142 146 });
147 +
148 + function openLibraryCollectionPicker(itemId, btn) {
149 + var existing = document.getElementById('library-collection-picker');
150 + if (existing) { existing.remove(); return; }
151 +
152 + var picker = document.createElement('div');
153 + picker.id = 'library-collection-picker';
154 + picker.style.cssText = 'position:absolute; z-index:20; background:var(--background); border:1px solid var(--border); box-shadow:0 2px 8px rgba(0,0,0,0.1); min-width:200px; max-height:240px; overflow-y:auto; right:0; top:100%;';
155 + picker.innerHTML = '<div style="padding:0.5rem; opacity:0.6; font-size:0.85rem;">Loading...</div>';
156 + btn.closest('.context-menu-wrapper').appendChild(picker);
157 +
158 + fetch('/api/collections/for-item/' + itemId, { headers: csrfHeaders() })
159 + .then(function(r) { return r.json(); })
160 + .then(function(cols) {
161 + var html = '';
162 + if (cols.length === 0) {
163 + html = '<div style="padding:0.5rem; opacity:0.6; font-size:0.85rem;">No collections. Create one in the Collections tab.</div>';
164 + } else {
165 + for (var i = 0; i < cols.length; i++) {
166 + var c = cols[i];
167 + html += '<label style="display:flex; align-items:center; gap:0.5rem; padding:0.4rem 0.5rem; cursor:pointer; font-size:0.9rem;">'
168 + + '<input type="checkbox"' + (c.in_collection ? ' checked' : '')
169 + + ' onchange="toggleLibraryCollectionItem(\'' + c.id + '\',\'' + itemId + '\',this.checked)">'
170 + + escapeHtml(c.title) + '</label>';
171 + }
172 + }
173 + picker.innerHTML = html;
174 + });
175 + }
176 +
177 + function toggleLibraryCollectionItem(collectionId, itemId, add) {
178 + fetch('/api/collections/' + collectionId + '/items/' + itemId, {
179 + method: add ? 'POST' : 'DELETE',
180 + headers: csrfHeaders()
181 + });
182 + }
183 +
184 + function escapeHtml(s) {
185 + var d = document.createElement('div');
186 + d.textContent = s;
187 + return d.innerHTML;
188 + }
143 189 </script>