max / makenotwork
- Co-Authored-By
- Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23 files changed,
+105 insertions,
-80 deletions
| @@ -309,6 +309,7 @@ | |||
| 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 @@ | |||
| 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)); |
| @@ -72,18 +72,18 @@ | |||
| 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 ↓</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 →</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> · | |
| 406 | 410 | <a href="/dashboard/item/{{ item.id }}#embed">Embed</a> · | |
| 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> · | |
| 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> · | |
| 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> · | |
| @@ -576,10 +575,8 @@ | |||
| 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> · | |
| 102 | - | <span>{{ project.item_count }} items</span> · | |
| 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> |