max / makenotwork
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 ↓</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 @@ 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> · | |
| 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> |
| @@ -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 — 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 — 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 — 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 — 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 →</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 →</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 →</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 →</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 →</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 →</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 →</a></p> |