Skip to main content

max / makenotwork

Usability audit fixes: UX improvements across dashboard, discover, and public pages - Remove "Coming soon" from Everything tier (landing page + creator plan tab) - Fix dead "Subscribe for updates" link on project pages - Move Library nav link inside auth block, point to /library - Rename "Products" to "Items" in discover mode toggle - Move wishlist button from page footer to purchase box area - Replace "Slug" with "URL name" in wizard labels and validation messages - Add title hint to dashboard More overflow button - Clarify post-signup wizard complete step messaging - Add keyboard shortcut hint to ? button, tooltip to Feed link - Improve discover empty states with actionable suggestions - Add Stripe context for non-technical users in join wizard - Remove generic "Downloads for Windows/macOS/Linux" claims from purchase boxes - Add missing docs links to Blog, Team, Code, Cloud Sync project tabs - Move analytics time range selector above stats with human-readable label - Add sort dropdown for projects mode on discover page (newest, most items) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-06 20:50 UTC
Commit: ee663957f9000cc1dfa0e2f86b70e6b5764d2483
Parent: e0c1aec
23 files changed, +105 insertions, -80 deletions
@@ -309,6 +309,7 @@ pub async fn discover_projects(
309 309 pool: &PgPool,
310 310 search: Option<&str>,
311 311 category_slug: Option<&str>,
312 + sort_by: Option<DiscoverSort>,
312 313 limit: i64,
313 314 offset: i64,
314 315 ) -> Result<Vec<DbDiscoverProjectRow>> {
@@ -399,10 +400,13 @@ pub async fn discover_projects(
399 400
400 401 query.push_str(" GROUP BY p.id, u.username, pc.name, pc.slug");
401 402
402 - let order = if has_search {
403 + let order = if has_search && (sort_by.is_none() || sort_by == Some(DiscoverSort::Newest)) {
403 404 "match_score DESC NULLS LAST, p.created_at DESC"
404 405 } else {
405 - "p.created_at DESC"
406 + match sort_by {
407 + Some(DiscoverSort::MostSold) => "item_count DESC, p.created_at DESC",
408 + _ => "p.created_at DESC",
409 + }
406 410 };
407 411
408 412 query.push_str(&format!(" ORDER BY {} LIMIT $3 OFFSET $4", order));
@@ -140,7 +140,7 @@ struct PublicProject {
140 140 async fn public_projects(
141 141 State(state): State<AppState>,
142 142 ) -> Result<impl IntoResponse> {
143 - let rows = db::discover::discover_projects(&state.db, None, None, 50, 0).await?;
143 + let rows = db::discover::discover_projects(&state.db, None, None, None, 50, 0).await?;
144 144 let data: Vec<PublicProject> = rows
145 145 .into_iter()
146 146 .map(|r| PublicProject {
@@ -98,10 +98,15 @@ async fn fetch_discover_data(pool: &PgPool, query: &DiscoverQuery) -> Result<Dis
98 98 .and_then(|s| s.parse().ok());
99 99
100 100 let (items, projects, total_count) = if mode == "projects" {
101 + let sort_filter: Option<DiscoverSort> = query.sort.as_deref()
102 + .filter(|s| !s.is_empty())
103 + .and_then(|s| s.parse().ok());
104 +
101 105 let db_projects = db::discover::discover_projects(
102 106 pool,
103 107 search_filter,
104 108 category_filter,
109 + sort_filter,
105 110 limit,
106 111 offset,
107 112 )
@@ -72,18 +72,18 @@ pub fn validate_slug(slug: &str) -> Result<(), AppError> {
72 72 let len = slug.chars().count();
73 73 if len < 2 {
74 74 return Err(AppError::Validation(
75 - "Slug must be at least 2 characters".to_string(),
75 + "URL name must be at least 2 characters".to_string(),
76 76 ));
77 77 }
78 78 if len > limits::PROJECT_SLUG_MAX {
79 79 return Err(AppError::Validation(format!(
80 - "Slug must be {} characters or less",
80 + "URL name must be {} characters or less",
81 81 limits::PROJECT_SLUG_MAX
82 82 )));
83 83 }
84 84 if !slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
85 85 return Err(AppError::Validation(
86 - "Slug can only contain letters, numbers, and hyphens".to_string(),
86 + "URL name can only contain letters, numbers, and hyphens".to_string(),
87 87 ));
88 88 }
89 89 Ok(())
@@ -180,7 +180,7 @@
180 180
181 181 {% if let Some(su) = session_user %}{% if su.can_create_projects && !projects.is_empty() %}
182 182 <div class="tab-overflow" style="position: relative; display: inline-block;">
183 - <button class="tab" onclick="var m=this.nextElementSibling; m.style.display=m.style.display==='block'?'none':'block';" type="button">More</button>
183 + <button class="tab" onclick="var m=this.nextElementSibling; m.style.display=m.style.display==='block'?'none':'block';" type="button" title="Media, SSH Keys, Forums, Support">More &darr;</button>
184 184 <div class="tab-overflow-menu" style="display: none; position: absolute; top: 100%; left: 0; z-index: 10; background: var(--background); border: 1px solid var(--border); min-width: 160px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
185 185 <button class="tab" style="display: block; width: 100%; text-align: left; padding: 0.5rem 1rem;"
186 186 hx-get="/dashboard/tabs/media"
@@ -124,7 +124,7 @@
124 124 hx-get="/discover"
125 125 hx-target="body"
126 126 hx-push-url="true"
127 - hx-vals='{"mode": "items"}'>Products</button>
127 + hx-vals='{"mode": "items"}'>Items</button>
128 128 <button class="toggle-btn{% if mode == "projects" %} active{% endif %}"
129 129 hx-get="/discover"
130 130 hx-target="body"
@@ -154,7 +154,6 @@
154 154 hx-include=".discover-filter">
155 155 <span id="search-spinner" class="htmx-indicator" aria-live="polite">Searching...</span>
156 156 <span class="table-meta" id="total-count">{{ total_items }} {% if mode == "projects" %}projects{% else %}items{% endif %}</span>
157 - {% if mode == "items" %}
158 157 <label for="sort-select" class="sr-only">Sort by</label>
159 158 <select class="sort-select discover-filter"
160 159 id="sort-select"
@@ -165,12 +164,16 @@
165 164 hx-target="#results-container"
166 165 hx-indicator="#search-spinner"
167 166 hx-include=".discover-filter">
167 + {% if mode == "projects" %}
168 + <option value="newest"{% if sort_by == "newest" || sort_by == "" %} selected{% endif %}>Newest</option>
169 + <option value="most_sold"{% if sort_by == "most_sold" %} selected{% endif %}>Most items</option>
170 + {% else %}
168 171 <option value="most_sold"{% if sort_by == "most_sold" %} selected{% endif %}>Most sold</option>
169 172 <option value="newest"{% if sort_by == "newest" || sort_by == "" %} selected{% endif %}>Newest</option>
170 173 <option value="price_asc"{% if sort_by == "price_asc" %} selected{% endif %}>Price asc</option>
171 174 <option value="price_desc"{% if sort_by == "price_desc" %} selected{% endif %}>Price desc</option>
175 + {% endif %}
172 176 </select>
173 - {% endif %}
174 177 <input type="hidden" id="type-input" name="item_type" value="{{ current_type }}" class="discover-filter">
175 178 <input type="hidden" id="tag-input" name="tag" value="{{ current_tag }}" class="discover-filter">
176 179 <input type="hidden" id="category-input" name="category" value="{{ current_category }}" class="discover-filter">
@@ -57,11 +57,10 @@
57 57 <div class="tier-price">$30/mo</div>
58 58 <div class="tier-desc">Video, games, large software. 500GB storage, 20GB/file.</div>
59 59 </div>
60 - <div class="tier-card planned">
60 + <div class="tier-card">
61 61 <div class="tier-name">Everything</div>
62 62 <div class="tier-price">$60/mo</div>
63 63 <div class="tier-desc">Live streaming, all features, current and future. 500GB storage, 20GB/file.</div>
64 - <div class="tier-planned-label">Coming soon</div>
65 64 </div>
66 65 </div>
67 66 <a class="section-link" href="/pricing">Pricing calculator &rarr;</a>
@@ -210,10 +210,8 @@
210 210 <li>{{ bundle_items.len() }} items in this bundle</li>
211 211 {% endif %}
212 212 <li>Lifetime access to all versions</li>
213 - <li>Downloads for Windows, macOS, Linux</li>
214 213 <li>Automatic update notifications</li>
215 214 <li>Direct creator support</li>
216 - <li>Complete documentation</li>
217 215 </ul>
218 216 </div>
219 217
@@ -247,7 +245,13 @@
247 245 {% endif %}
248 246
249 247 {% if session_user.is_some() %}
250 - <div style="margin-top: 1rem; position: relative;">
248 + {% if !is_owner %}
249 + <div style="margin-top: 1rem;">
250 + <button class="secondary" id="wishlist-btn" style="width: 100%; font-size: 0.9rem;"
251 + onclick="toggleWishlist('{{ item.id }}')">{% if is_wishlisted %}Wishlisted{% else %}Add to Wishlist{% endif %}</button>
252 + </div>
253 + {% endif %}
254 + <div style="margin-top: 0.5rem; position: relative;">
251 255 <button class="secondary" style="width: 100%; font-size: 0.9rem;"
252 256 onclick="toggleCollectionDropdown('{{ item.id }}')">Save to collection</button>
253 257 <div id="collection-dropdown"
@@ -405,11 +409,6 @@
405 409 <a href="/dashboard/item/{{ item.id }}">Edit</a> &middot;
406 410 <a href="/dashboard/item/{{ item.id }}#embed">Embed</a> &middot;
407 411 {% endif %}
408 - {% if session_user.is_some() && !is_owner %}
409 - <a href="javascript:void(0)" id="wishlist-btn"
410 - onclick="toggleWishlist('{{ item.id }}')"
411 - style="{% if is_wishlisted %}font-weight: bold;{% endif %}">{% if is_wishlisted %}Wishlisted{% else %}Wishlist{% endif %}</a> &middot;
412 - {% endif %}
413 412 <a href="javascript:void(0)" onclick="navigator.clipboard.writeText(window.location.origin + '/i/{{ item.id }}').then(() => { this.textContent = 'Copied!'; setTimeout(() => this.textContent = 'Copy link', 1500) })">Copy link</a> &middot;
414 413 {% if session_user.is_some() %}
415 414 <a href="javascript:void(0)" onclick="document.getElementById('report-modal').style.display='flex'">Report this item</a> &middot;
@@ -576,10 +575,8 @@ function downloadVersion(versionId) {
576 575 .then(function(data) {
577 576 if (data.wishlisted) {
578 577 btn.textContent = 'Wishlisted';
579 - btn.style.fontWeight = 'bold';
580 578 } else {
581 - btn.textContent = 'Wishlist';
582 - btn.style.fontWeight = '';
579 + btn.textContent = 'Add to Wishlist';
583 580 }
584 581 });
585 582 };
@@ -99,8 +99,7 @@
99 99 <h1 class="store-title">{{ project.title }}<span class="dot">.</span></h1>
100 100 <div class="store-meta">
101 101 by <a href="/u/{{ creator_username }}">{{ creator_username }}</a> &middot;
102 - <span>{{ project.item_count }} items</span> &middot;
103 - <a href="#">Subscribe for updates</a>
102 + <span>{{ project.item_count }} items</span>
104 103 </div>
105 104 <p class="store-description">{{ project.description }}</p>
106 105 <div class="store-actions">
@@ -62,7 +62,6 @@
62 62 <h3>What you'll get:</h3>
63 63 <ul>
64 64 <li>Lifetime access to all current and future versions</li>
65 - <li>Downloads for Windows, macOS, and Linux</li>
66 65 <li>Email notifications for updates</li>
67 66 <li>Direct support from the creator</li>
68 67 </ul>
@@ -12,8 +12,8 @@
12 12 {% endfor %}
13 13 {% if projects.is_empty() %}
14 14 <div class="results-empty">
15 - <p>No projects found.</p>
16 - <p class="results-empty-hint">Creators are setting up shop &mdash; check back soon.</p>
15 + <p>No projects found matching your filters.</p>
16 + <p class="results-empty-hint">Try broadening your search or <a href="/discover">clearing all filters</a>.</p>
17 17 </div>
18 18 {% endif %}
19 19 {% else %}
@@ -28,8 +28,8 @@
28 28 {% endfor %}
29 29 {% if items.is_empty() %}
30 30 <div class="results-empty">
31 - <p>No items found.</p>
32 - <p class="results-empty-hint">Creators are setting up shop &mdash; check back soon.</p>
31 + <p>No items found matching your filters.</p>
32 + <p class="results-empty-hint">Try broadening your search or <a href="/discover">clearing all filters</a>.</p>
33 33 </div>
34 34 {% endif %}
35 35 {% endif %}
@@ -52,8 +52,8 @@
52 52 {% endfor %}
53 53 {% if projects.is_empty() %}
54 54 <div class="results-empty">
55 - <p>No projects found.</p>
56 - <p class="results-empty-hint">Creators are setting up shop &mdash; check back soon.</p>
55 + <p>No projects found matching your filters.</p>
56 + <p class="results-empty-hint">Try broadening your search or <a href="/discover">clearing all filters</a>.</p>
57 57 </div>
58 58 {% endif %}
59 59 {% else %}
@@ -72,8 +72,8 @@
72 72 {% endfor %}
73 73 {% if items.is_empty() %}
74 74 <div class="results-empty">
75 - <p>No items found.</p>
76 - <p class="results-empty-hint">Creators are setting up shop &mdash; check back soon.</p>
75 + <p>No items found matching your filters.</p>
76 + <p class="results-empty-hint">Try broadening your search or <a href="/discover">clearing all filters</a>.</p>
77 77 </div>
78 78 {% endif %}
79 79 {% endif %}
@@ -13,10 +13,10 @@
13 13 </form>
14 14 <nav aria-label="Main navigation">
15 15 <div class="nav-links">
16 - <a href="/">Library</a>
17 16 <a href="/discover">Discover</a>
18 17 {% if let Some(user) = session_user %}
19 - <a href="/feed">Feed</a>
18 + <a href="/library">Library</a>
19 + <a href="/feed" title="Updates from creators you follow">Feed</a>
20 20 <a href="/u/{{ user.username }}">Profile</a>
21 21 <a href="/dashboard">Dashboard</a>
22 22 <a href="/changelog" style="font-size: 0.85em; opacity: 0.7;">Changelog</a>
@@ -32,7 +32,7 @@
32 32 <a href="/login">Login</a>
33 33 <a href="/join">Join</a>
34 34 {% endif %}
35 - <button type="button" class="link-button shortcuts-help-btn" onclick="toggleShortcutsHelp()" aria-label="Keyboard shortcuts" title="Keyboard shortcuts">?</button>
35 + <button type="button" class="link-button shortcuts-help-btn" onclick="toggleShortcutsHelp()" aria-label="Keyboard shortcuts" title="Keyboard shortcuts (press ?)">?</button>
36 36 </div>
37 37 </nav>
38 38 </header>
@@ -1,5 +1,5 @@
1 1 {% if available %}
2 - <span class="slug-status available" style="color: var(--success); font-size: 0.85rem;">Slug available</span>
2 + <span class="slug-status available" style="color: var(--success); font-size: 0.85rem;">Available</span>
3 3 {% else %}
4 - <span class="slug-status taken" style="color: var(--error); font-size: 0.85rem;">Slug already taken</span>
4 + <span class="slug-status taken" style="color: var(--error); font-size: 0.85rem;">Already taken</span>
5 5 {% endif %}
@@ -1,5 +1,27 @@
1 1 <div class="tab-docs"><a href="/docs/analytics">Docs: Analytics &rarr;</a></div>
2 2
3 + <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
4 + <h2 style="font-size: 1.1rem; margin: 0;">{% if active_range == "7d" %}Last 7 days{% else if active_range == "30d" %}Last 30 days{% else if active_range == "90d" %}Last 90 days{% else %}All time{% endif %}</h2>
5 + <div class="time-selector">
6 + <button class="{% if active_range == "7d" %}active{% endif %}"
7 + hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=7d"
8 + hx-target="#tab-content"
9 + hx-swap="innerHTML">7d</button>
10 + <button class="{% if active_range == "30d" %}active{% endif %}"
11 + hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=30d"
12 + hx-target="#tab-content"
13 + hx-swap="innerHTML">30d</button>
14 + <button class="{% if active_range == "90d" %}active{% endif %}"
15 + hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=90d"
16 + hx-target="#tab-content"
17 + hx-swap="innerHTML">90d</button>
18 + <button class="{% if active_range == "all" %}active{% endif %}"
19 + hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=all"
20 + hx-target="#tab-content"
21 + hx-swap="innerHTML">All</button>
22 + </div>
23 + </div>
24 +
3 25 <div class="stats-grid">
4 26 {% for stat in stats %}
5 27 <div class="stat-card">
@@ -15,24 +37,6 @@
15 37 <div class="chart-container">
16 38 <div class="chart-header">
17 39 <h2>Revenue Over Time</h2>
18 - <div class="time-selector">
19 - <button class="{% if active_range == "7d" %}active{% endif %}"
20 - hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=7d"
21 - hx-target="#tab-content"
22 - hx-swap="innerHTML">7d</button>
23 - <button class="{% if active_range == "30d" %}active{% endif %}"
24 - hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=30d"
25 - hx-target="#tab-content"
26 - hx-swap="innerHTML">30d</button>
27 - <button class="{% if active_range == "90d" %}active{% endif %}"
28 - hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=90d"
29 - hx-target="#tab-content"
30 - hx-swap="innerHTML">90d</button>
31 - <button class="{% if active_range == "all" %}active{% endif %}"
32 - hx-get="/dashboard/project/{{ project_slug }}/tabs/analytics?range=all"
33 - hx-target="#tab-content"
34 - hx-swap="innerHTML">All</button>
35 - </div>
36 40 </div>
37 41 {% if bars.is_empty() %}
38 42 <div class="chart-empty">No revenue data yet</div>
@@ -1,3 +1,5 @@
1 + <div class="tab-docs"><a href="/docs/blog">Docs: Blog &rarr;</a></div>
2 +
1 3 <div class="content-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
2 4 <h2>Blog Posts</h2>
3 5 <a href="/dashboard/project/{{ project_slug }}/blog/new">
@@ -1,4 +1,6 @@
1 1 {% if git_enabled %}
2 + <div class="tab-docs"><a href="/docs/git">Docs: Git &rarr;</a></div>
3 +
2 4 <div class="form-section" id="git-repos-section">
3 5 <h2>Git Repositories</h2>
4 6
@@ -1,3 +1,5 @@
1 + <div class="tab-docs"><a href="/docs/splits">Docs: Collaborators &rarr;</a></div>
2 +
1 3 <div class="data-section">
2 4 <h2>Members & Payouts</h2>
3 5 <p style="margin-bottom: 1.5rem; opacity: 0.7; text-align: left;">
@@ -1,3 +1,5 @@
1 + <div class="tab-docs"><a href="/docs/developer/synckit">Docs: Cloud Sync &rarr;</a></div>
2 +
1 3 <div class="content-section">
2 4 <div class="section-header">
3 5 <h2>Cloud Sync</h2>
@@ -1,5 +1,27 @@
1 1 <div class="tab-docs"><a href="/docs/analytics">Docs: Analytics &rarr;</a></div>
2 2
3 + <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
4 + <h2 style="font-size: 1.1rem; margin: 0;">{% if active_range == "7d" %}Last 7 days{% else if active_range == "30d" %}Last 30 days{% else if active_range == "90d" %}Last 90 days{% else %}All time{% endif %}</h2>
5 + <div class="time-selector">
6 + <button class="{% if active_range == "7d" %}active{% endif %}"
7 + hx-get="/dashboard/tabs/analytics?range=7d"
8 + hx-target="#tab-content"
9 + hx-swap="innerHTML">7d</button>
10 + <button class="{% if active_range == "30d" %}active{% endif %}"
11 + hx-get="/dashboard/tabs/analytics?range=30d"
12 + hx-target="#tab-content"
13 + hx-swap="innerHTML">30d</button>
14 + <button class="{% if active_range == "90d" %}active{% endif %}"
15 + hx-get="/dashboard/tabs/analytics?range=90d"
16 + hx-target="#tab-content"
17 + hx-swap="innerHTML">90d</button>
18 + <button class="{% if active_range == "all" %}active{% endif %}"
19 + hx-get="/dashboard/tabs/analytics?range=all"
20 + hx-target="#tab-content"
21 + hx-swap="innerHTML">All</button>
22 + </div>
23 + </div>
24 +
3 25 <div class="stats-grid">
4 26 {% for stat in stats %}
5 27 <div class="stat-card">
@@ -15,24 +37,6 @@
15 37 <div class="chart-container">
16 38 <div class="chart-header">
17 39 <h2>Revenue Over Time</h2>
18 - <div class="time-selector">
19 - <button class="{% if active_range == "7d" %}active{% endif %}"
20 - hx-get="/dashboard/tabs/analytics?range=7d"
21 - hx-target="#tab-content"
22 - hx-swap="innerHTML">7d</button>
23 - <button class="{% if active_range == "30d" %}active{% endif %}"
24 - hx-get="/dashboard/tabs/analytics?range=30d"
25 - hx-target="#tab-content"
26 - hx-swap="innerHTML">30d</button>
27 - <button class="{% if active_range == "90d" %}active{% endif %}"
28 - hx-get="/dashboard/tabs/analytics?range=90d"
29 - hx-target="#tab-content"
30 - hx-swap="innerHTML">90d</button>
31 - <button class="{% if active_range == "all" %}active{% endif %}"
32 - hx-get="/dashboard/tabs/analytics?range=all"
33 - hx-target="#tab-content"
34 - hx-swap="innerHTML">All</button>
35 - </div>
36 40 </div>
37 41 {% if bars.is_empty() %}
38 42 <div class="chart-empty">Once you publish items and make sales, revenue data will appear here.</div>
@@ -64,11 +64,14 @@
64 64 <button type="submit" class="primary" style="font-size: 0.85rem;">Subscribe</button>
65 65 </form>
66 66 </div>
67 - <div style="background: var(--light-background); padding: 1rem; text-align: center; opacity: 0.6;">
67 + <div style="background: var(--light-background); padding: 1rem; text-align: center;">
68 68 <div style="font-family: var(--font-heading); font-weight: bold; font-size: 1rem; margin-bottom: 0.25rem;"><a href="/docs/guide/tiers" style="color: inherit; text-decoration: none; border-bottom: 1px solid var(--border);">Everything</a></div>
69 69 <div class="meta" style="margin-bottom: 0.25rem;">$60/mo</div>
70 70 <div class="meta" style="margin-bottom: 0.75rem; font-size: 0.75rem;">500GB, 20GB/file, all features</div>
71 - <div class="meta" style="font-size: 0.8rem;">Coming soon</div>
71 + <form method="post" action="/stripe/creator-tier">
72 + <input type="hidden" name="tier" value="everything">
73 + <button type="submit" class="primary" style="font-size: 0.85rem;">Subscribe</button>
74 + </form>
72 75 </div>
73 76 </div>
74 77 <p style="text-align: center; margin-bottom: 1rem;"><a href="/docs/guide/tiers" style="font-size: 0.85rem;">Full tier details &rarr;</a></p>