Skip to main content

max / makenotwork

Add shopping cart, UX audit remediation, and upload improvements Shopping cart with multi-item Stripe checkout sessions, wishlist-to-cart flow, cross-seller sequential checkout, promo code support, and PWYW custom amounts. UX audit: learnability fixes (jargon, empty states, onboarding checklists), complexity reductions (item dashboard 7->5 tabs, wizard 6->5 steps), discoverability (wishlists tab, RSS, search filter), and upload flow improvements (client-side pre-validation, speed/ETA).
Co-Authored-By
Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-07 00:42 UTC
Commit: 85c9938c720cdbcb6ed60f0e8ab480b18e136bce
Parent: 2883844
62 files changed, +2053 insertions, -126 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.4.12"
3 + version = "0.5.0"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -1,9 +1,9 @@
1 1 # Makenotwork TODO
2 2
3 3 ## Status
4 - Done: All pre-beta phases, UX audit remediation, creator trust audit remediation. Active: Creator setup (Stripe), manual testing. Next: Soft launch.
4 + Done: All pre-beta phases, UX audit remediation, creator trust audit remediation, shopping cart + wishlist checkout. Active: Final pre-launch testing. Next: Soft launch (target 2026-05-09).
5 5
6 - v0.4.10 deployed 2026-05-04. Audit grade A (Run 20, 2026-05-04). ~83K LOC, 1,214+ test annotations, 0 cargo warnings, 2 cold spots. CI on astra operational. Mutation kill rate 99.4%. Property-based testing active (proptest). `cargo test --features fast-tests` for fast runs. Doc fuzz (2026-05-06): deleted stale database_schema.md, fixed MT README, updated SyncKit version in docs. Usability audit (2026-05-06): grade B-, 18 easy wins + 9 medium items + 3 deferred added.
6 + v0.5.0 deployed 2026-05-06. Audit grade A (Run 20, 2026-05-04). ~85K LOC, 1,912 tests (1220 unit + 664 integration + 28 load), 0 cargo warnings, 2 flaky sandbox tests. CI on astra operational. Mutation kill rate 99.4%. Property-based testing active (proptest). UX audit (2026-05-06): Learnability A-, Complexity B+, Discoverability B+, Feature Completeness B. Shopping cart implemented: multi-item Stripe checkout sessions, wishlist-to-cart flow, per-seller grouping, savings nudges. Migration 096.
7 7
8 8 Human tasks (manual testing, outreach, legal, infrastructure) moved to `human_todo.md`.
9 9 Completed items moved to `todo_done.md`.
@@ -86,12 +86,28 @@
86 86 - Conversion analytics: Views + Conversion stat cards on both user and project Analytics tabs. Period-over-period % for views.
87 87 - Subscription → Membership terminology: project dashboard tab, wizard, project page, 7 docs files.
88 88
89 - ### UX — Deferred (post-beta table stakes)
89 + ### UX Audit Remediation (2026-05-06) — DONE (4 rounds)
90 +
91 + **Learnability (B+ → A-):** Join preamble, TOTP/passkey labels, SyncKit jargon fix, empty state CTAs (subscriptions, collections), project onboarding checklist, tab hover hints (all dashboards), passkey login label.
92 +
93 + **Complexity (B- → B+):** Pitch step simplified (removed trial/tier fields), item wizard sections step removed (6→5 steps), item Settings merged into Details, item Embed folded into Overview (7→5 tabs), item tab hints.
94 +
95 + **Upload flow:** Client-side file size pre-validation with tier limit from presign response, tier-aware error messages (HTTP 413), retry preserves file selection, upload speed + ETA display.
96 +
97 + **Discoverability (B- → B+):** Wishlists tab in Library (new endpoint + template), data export link in dashboard header, changelog link de-muted, RSS/copy-link moved to profile header, library purchases search filter, quick-edit links confirmed present.
98 +
99 + **Shopping cart + wishlist checkout (Feature Completeness B → B+):** Migration 096 (cart_items table), cart DB module, cart API (toggle/remove/count), multi-line-item Stripe Checkout Sessions per seller (Direct Charges), cart checkout route, cart webhook handler (batch transaction completion), cart page grouped by seller with savings nudge, wishlist-to-cart flow, "Add to Cart" on item pages + wishlists + purchase page, cart link in nav, scheduler cleanup.
100 +
101 + ### UX — Deferred (post-launch)
102 + - [ ] Storefront customization (creator themes/colors — 24 TOML themes exist in shared/themes/)
90 103 - [ ] Reviews/ratings system for items
104 + - [ ] Wishlist price-drop alerts
91 105 - [ ] Gift purchases at checkout
92 106 - [ ] HTML rich email for creator broadcasts (currently plain text only)
93 - - [ ] Creator-to-fan broadcast email composer (full system, not just contact export)
94 107 - [ ] In-app notification center (beyond email-only notifications)
108 + - [ ] Cross-seller cart checkout (currently one Stripe session per seller)
109 + - [ ] Promo codes in cart checkout (currently single-item only)
110 + - [ ] PWYW custom amounts in cart (currently uses minimum price)
95 111
96 112 ---
97 113
@@ -274,7 +290,7 @@
274 290 import/ (CSV converter, pipeline, intermediate format)
275 291 MNW/server/tests/
276 292 integration.rs, harness/, workflows/*.rs
277 - MNW/server/migrations/ (001-090)
293 + MNW/server/migrations/ (001-096)
278 294 MNW/server/templates/
279 295 MNW/server/deploy/
280 296 MNW/server/site-docs/public/, MNW/server/site-docs/unpublished/
@@ -23,6 +23,7 @@
23 23 filenameEl: document.getElementById('upload-filename'),
24 24 percentEl: document.getElementById('upload-percent'),
25 25 progressBar: document.getElementById('progress-bar'),
26 + speedEl: document.getElementById('upload-speed'),
26 27 });
27 28
28 29 initDropzone(
@@ -45,14 +46,24 @@
45 46 });
46 47 }
47 48
49 + var lastFile = null;
50 +
48 51 document.getElementById('cancel-upload-btn').addEventListener('click', function() {
49 52 uploader.cancel();
50 53 resetUpload();
51 54 });
52 55
53 - document.getElementById('retry-upload-btn').addEventListener('click', resetUpload);
56 + document.getElementById('retry-upload-btn').addEventListener('click', function() {
57 + if (lastFile) {
58 + document.getElementById('upload-error').classList.add('hidden');
59 + uploadAudio(lastFile);
60 + } else {
61 + resetUpload();
62 + }
63 + });
54 64
55 65 function resetUpload() {
66 + lastFile = null;
56 67 document.getElementById('audio-dropzone').classList.remove('hidden');
57 68 document.getElementById('upload-progress').classList.add('hidden');
58 69 document.getElementById('upload-success').classList.add('hidden');
@@ -61,6 +72,7 @@
61 72 }
62 73
63 74 function uploadAudio(file) {
75 + lastFile = file;
64 76 document.getElementById('audio-dropzone').classList.add('hidden');
65 77 document.getElementById('upload-progress').classList.remove('hidden');
66 78
@@ -81,6 +93,11 @@
81 93 return res.json();
82 94 })
83 95 .then(function(data) {
96 + if (data.max_file_bytes && file.size > data.max_file_bytes) {
97 + var limitMB = (data.max_file_bytes / (1024 * 1024)).toFixed(0);
98 + var fileMB = (file.size / (1024 * 1024)).toFixed(1);
99 + throw new Error('File is ' + fileMB + ' MB but your plan allows up to ' + limitMB + ' MB per file. Upgrade your tier or use a smaller file.');
100 + }
84 101 return uploader.upload(data.upload_url, file, data.s3_key, 'audio/mpeg', data.cache_control);
85 102 })
86 103 .then(function(s3Key) {
@@ -125,6 +142,7 @@
125 142 filenameEl: document.getElementById('version-upload-filename'),
126 143 percentEl: document.getElementById('version-upload-percent'),
127 144 progressBar: document.getElementById('version-progress-bar'),
145 + speedEl: document.getElementById('version-upload-speed'),
128 146 });
129 147
130 148 var versionDropzone = document.getElementById('version-dropzone');
@@ -223,6 +241,11 @@
223 241 return res.json();
224 242 })
225 243 .then(function(data) {
244 + if (data.max_file_bytes && file.size > data.max_file_bytes) {
245 + var limitMB = (data.max_file_bytes / (1024 * 1024)).toFixed(0);
246 + var fileMB = (file.size / (1024 * 1024)).toFixed(1);
247 + throw new Error('File is ' + fileMB + ' MB but your plan allows up to ' + limitMB + ' MB per file. Upgrade your tier or use a smaller file.');
248 + }
226 249 return uploader.upload(data.upload_url, file, data.s3_key, 'application/octet-stream', data.cache_control);
227 250 })
228 251 .then(function(s3Key) {
@@ -17,16 +17,19 @@
17 17 this.filenameEl = opts.filenameEl;
18 18 this.percentEl = opts.percentEl;
19 19 this.progressBar = opts.progressBar;
20 + this.speedEl = opts.speedEl || null;
20 21 this._xhr = null;
21 22 }
22 23
23 24 /** Start an upload. Returns a Promise resolving to the s3Key. */
24 25 S3Uploader.prototype.upload = function(url, file, s3Key, fallbackContentType, cacheControl) {
25 26 var self = this;
27 + var startTime = Date.now();
26 28
27 29 if (self.filenameEl) self.filenameEl.textContent = file.name;
28 30 if (self.percentEl) self.percentEl.textContent = '0%';
29 31 if (self.progressBar) self.progressBar.style.width = '0%';
32 + if (self.speedEl) self.speedEl.textContent = '';
30 33
31 34 return new Promise(function(resolve, reject) {
32 35 var xhr = new XMLHttpRequest();
@@ -37,6 +40,20 @@
37 40 var percent = Math.round((e.loaded / e.total) * 100);
38 41 if (self.percentEl) self.percentEl.textContent = percent + '%';
39 42 if (self.progressBar) self.progressBar.style.width = percent + '%';
43 + if (self.speedEl && e.loaded > 0) {
44 + var elapsed = (Date.now() - startTime) / 1000;
45 + if (elapsed > 0.5) {
46 + var speed = e.loaded / elapsed;
47 + var remaining = (e.total - e.loaded) / speed;
48 + var speedStr = speed > 1024 * 1024
49 + ? (speed / (1024 * 1024)).toFixed(1) + ' MB/s'
50 + : (speed / 1024).toFixed(0) + ' KB/s';
51 + var etaStr = remaining < 60
52 + ? Math.ceil(remaining) + 's'
53 + : Math.ceil(remaining / 60) + 'm ' + Math.ceil(remaining % 60) + 's';
54 + self.speedEl.textContent = speedStr + ' \u2014 ' + etaStr + ' remaining';
55 + }
56 + }
40 57 }
41 58 });
42 59