Skip to main content

max / makenotwork

Fix wizard UX: monetization form submission when free, image upload clarity Monetization: hidden pricing inputs start disabled so HTML form validation doesn't block submission when "Free" is selected. Appearance: inline error and progress display instead of overlay that blocked the interface, visible Choose File button. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-28 18:43 UTC
Commit: 00e8d2e5b0d29b151249f1b66b05859f7d9d40ce
Parent: 1bb601b
3 files changed, +87 insertions, -50 deletions
@@ -480,6 +480,17 @@
480 480 color: var(--text-muted);
481 481 }
482 482
483 + .project-image-upload {
484 + display: flex;
485 + flex-direction: column;
486 + gap: 0.75rem;
487 + max-width: 300px;
488 + }
489 +
490 + .project-image-upload .secondary {
491 + align-self: flex-start;
492 + }
493 +
483 494 .project-image-current img {
484 495 width: 200px;
485 496 height: 200px;
@@ -487,6 +498,33 @@
487 498 border-radius: 4px;
488 499 }
489 500
501 + .upload-progress-inline {
502 + font-size: 0.85rem;
503 + display: flex;
504 + flex-wrap: wrap;
505 + align-items: center;
506 + gap: 0.5rem;
507 + }
508 +
509 + .upload-progress-inline .progress-bar-container {
510 + width: 100%;
511 + height: 6px;
512 + background: var(--border);
513 + border-radius: 3px;
514 + overflow: hidden;
515 + }
516 +
517 + .upload-progress-inline .progress-bar {
518 + height: 100%;
519 + background: var(--highlight);
520 + transition: width 0.2s;
521 + }
522 +
523 + .upload-error-inline {
524 + font-size: 0.85rem;
525 + color: var(--danger, #c0392b);
526 + }
527 +
490 528 /* Project Card Preview */
491 529
492 530 .project-card-preview {
@@ -11,34 +11,36 @@
11 11 <div class="form-group">
12 12 <label>Project Image</label>
13 13
14 - <div class="project-image-dropzone" id="image-dropzone">
15 - {% if let Some(url) = cover_image_url %}
16 - <div class="project-image-current" id="image-current">
17 - <img src="{{ url }}" alt="Project image">
18 - </div>
19 - {% else %}
20 - <div class="project-image-placeholder" id="image-placeholder">
21 - <p>Drop an image here, or click to browse</p>
22 - <p class="hint">Square, at least 400x400px. JPG, PNG, or WebP. Max 10 MB.</p>
14 + <div class="project-image-upload" id="image-upload-area">
15 + <div class="project-image-dropzone" id="image-dropzone">
16 + {% if let Some(url) = cover_image_url %}
17 + <div class="project-image-current" id="image-current">
18 + <img src="{{ url }}" alt="Project image">
19 + </div>
20 + {% else %}
21 + <div class="project-image-placeholder" id="image-placeholder">
22 + <p>Square, at least 400x400px</p>
23 + <p class="hint">JPG, PNG, or WebP. Max 10 MB.</p>
24 + </div>
25 + {% endif %}
26 + <input type="file" id="image-file-input" accept="image/jpeg,image/png,image/webp" style="display: none;">
23 27 </div>
24 - {% endif %}
25 - <input type="file" id="image-file-input" accept="image/jpeg,image/png,image/webp" style="display: none;">
26 - </div>
27 28
28 - <div class="upload-progress hidden" id="image-upload-progress">
29 - <div class="progress-info">
30 - <span id="image-upload-filename"></span>
31 - <span id="image-upload-percent">0%</span>
32 - </div>
33 - <div class="progress-bar-container">
34 - <div class="progress-bar" id="image-progress-bar" style="width: 0%;"></div>
29 + <button type="button" class="secondary" id="choose-image-btn"
30 + onclick="document.getElementById('image-file-input').click()">Choose File</button>
31 +
32 + <div class="upload-status" id="image-upload-status">
33 + <div class="upload-progress-inline hidden" id="image-upload-progress">
34 + <span id="image-upload-filename"></span>
35 + <span id="image-upload-percent">0%</span>
36 + <div class="progress-bar-container">
37 + <div class="progress-bar" id="image-progress-bar" style="width: 0%;"></div>
38 + </div>
39 + </div>
40 + <div class="upload-error-inline hidden" id="image-upload-error">
41 + <span class="error-message" id="image-error-message"></span>
42 + </div>
35 43 </div>
36 - <button type="button" class="secondary" id="cancel-image-upload">Cancel</button>
37 - </div>
38 -
39 - <div class="upload-error hidden" id="image-upload-error">
40 - <span class="error-message" id="image-error-message"></span>
41 - <button type="button" class="secondary" id="retry-image-upload">Try Again</button>
42 44 </div>
43 45
44 46 <input type="hidden" name="cover_image_url" id="cover-image-url"
@@ -85,6 +87,7 @@
85 87 var errorEl = document.getElementById('image-upload-error');
86 88 var placeholder = document.getElementById('image-placeholder');
87 89 var currentImg = document.getElementById('image-current');
90 + var chooseBtn = document.getElementById('choose-image-btn');
88 91
89 92 var uploader = new S3Uploader({
90 93 filenameEl: document.getElementById('image-upload-filename'),
@@ -96,17 +99,10 @@
96 99 uploadProjectImage(file);
97 100 });
98 101
99 - document.getElementById('cancel-image-upload').addEventListener('click', function() {
100 - uploader.cancel();
101 - resetUpload();
102 - });
103 -
104 - document.getElementById('retry-image-upload').addEventListener('click', resetUpload);
105 -
106 102 function uploadProjectImage(file) {
107 - dropzone.classList.add('hidden');
108 103 errorEl.classList.add('hidden');
109 104 progressEl.classList.remove('hidden');
105 + chooseBtn.disabled = true;
110 106
111 107 fetch('/api/projects/image/presign', {
112 108 method: 'POST',
@@ -122,7 +118,7 @@
122 118 return res.json();
123 119 })
124 120 .then(function(data) {
125 - return uploader.upload(data.upload_url, file, data.s3_key, 'image/jpeg');
121 + return uploader.upload(data.upload_url, file, data.s3_key, file.type || 'image/jpeg');
126 122 })
127 123 .then(function(s3Key) {
128 124 return fetch('/api/projects/image/confirm', {
@@ -141,16 +137,17 @@
141 137 .then(function(data) {
142 138 hiddenUrl.value = data.image_url;
143 139 progressEl.classList.add('hidden');
140 + chooseBtn.disabled = false;
144 141 showUploadedImage(data.image_url);
145 142 })
146 143 .catch(function(err) {
144 + progressEl.classList.add('hidden');
145 + chooseBtn.disabled = false;
147 146 showError(err.message || 'Upload failed');
148 147 });
149 148 }
150 149
151 150 function showUploadedImage(url) {
152 - dropzone.classList.remove('hidden');
153 - // Replace dropzone content with the uploaded image
154 151 if (placeholder) placeholder.classList.add('hidden');
155 152 if (currentImg) {
156 153 currentImg.querySelector('img').src = url;
@@ -163,22 +160,13 @@
163 160 dropzone.insertBefore(div, fileInput);
164 161 currentImg = div;
165 162 }
166 - // Update preview
167 163 var preview = document.getElementById('preview-cover');
168 164 preview.innerHTML = '<img src="' + url + '" alt="Preview">';
169 165 }
170 166
171 167 function showError(message) {
172 - progressEl.classList.add('hidden');
173 168 errorEl.classList.remove('hidden');
174 169 document.getElementById('image-error-message').textContent = message;
175 170 }
176 -
177 - function resetUpload() {
178 - progressEl.classList.add('hidden');
179 - errorEl.classList.add('hidden');
180 - dropzone.classList.remove('hidden');
181 - fileInput.value = '';
182 - }
183 171 })();
184 172 </script>
@@ -29,7 +29,7 @@
29 29 <div class="form-group">
30 30 <label for="price_dollars">Price ($)</label>
31 31 <input type="number" name="price_dollars" id="price_dollars" min="0.50" step="0.01"
32 - placeholder="9.99" value="{{ price_dollars }}">
32 + placeholder="9.99" value="{{ price_dollars }}" disabled>
33 33 </div>
34 34 </div>
35 35
@@ -37,7 +37,7 @@
37 37 <div class="form-group">
38 38 <label for="pwyw_min_dollars">Minimum price ($, 0 for no minimum)</label>
39 39 <input type="number" name="pwyw_min_dollars" id="pwyw_min_dollars" min="0" step="0.01"
40 - placeholder="0.00" value="{{ pwyw_min_dollars }}">
40 + placeholder="0.00" value="{{ pwyw_min_dollars }}" disabled>
41 41 </div>
42 42 </div>
43 43
@@ -79,9 +79,20 @@ var tierIndex = {{ tiers.len() }};
79 79
80 80 function updatePricingUI() {
81 81 var model = document.getElementById('pricing-model').value;
82 - document.getElementById('buy-once-fields').style.display = model === 'buy_once' ? '' : 'none';
83 - document.getElementById('pwyw-fields').style.display = model === 'pwyw' ? '' : 'none';
84 - document.getElementById('subscription-fields').style.display = model === 'subscription' ? '' : 'none';
82 +
83 + var sections = [
84 + { id: 'buy-once-fields', active: model === 'buy_once' },
85 + { id: 'pwyw-fields', active: model === 'pwyw' },
86 + { id: 'subscription-fields', active: model === 'subscription' }
87 + ];
88 +
89 + sections.forEach(function(s) {
90 + var el = document.getElementById(s.id);
91 + el.style.display = s.active ? '' : 'none';
92 + // Disable hidden inputs so they don't block form validation
93 + var inputs = el.querySelectorAll('input, select');
94 + inputs.forEach(function(inp) { inp.disabled = !s.active; });
95 + });
85 96 }
86 97
87 98 function addTierForm() {