max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
18 files changed,
+478 insertions,
-225 deletions
| @@ -11281,7 +11281,6 @@ | |||
| 11281 | 11281 | =========================================== */ | |
| 11282 | 11282 | ||
| 11283 | 11283 | /* Tab-spinner loading indicator (used by 3+ dashboards). */ | |
| 11284 | - | .tab-spinner-indicator { margin-left: var(--gap-section); } | |
| 11285 | 11284 | ||
| 11286 | 11285 | /* Inline-block secondary action link styled as a button. */ | |
| 11287 | 11286 | .btn-link { |
| @@ -16,7 +16,7 @@ | |||
| 16 | 16 | body: body.toString() | |
| 17 | 17 | }).then(function(r) { | |
| 18 | 18 | if (r.ok) { | |
| 19 | - | htmx.ajax('GET', '/dashboard/project/' + projectSlug + '/tabs/code', {target: '#tab-content', swap: 'innerHTML'}); | |
| 19 | + | htmx.ajax('GET', '/dashboard/project/' + projectSlug + '/tabs/code', {target: '#project-code', swap: 'innerHTML'}); | |
| 20 | 20 | } else { | |
| 21 | 21 | return r.json().then(function(data) { | |
| 22 | 22 | alert(data.error || 'Failed to add collaborator'); |
| @@ -15,7 +15,7 @@ | |||
| 15 | 15 | body: JSON.stringify({name: name}) | |
| 16 | 16 | }).then(function(r) { | |
| 17 | 17 | if (r.ok) { | |
| 18 | - | htmx.ajax('GET', '/dashboard/project/' + projectSlug + '/tabs/code', {target: '#tab-content', swap: 'innerHTML'}); | |
| 18 | + | htmx.ajax('GET', '/dashboard/project/' + projectSlug + '/tabs/code', {target: '#project-code', swap: 'innerHTML'}); | |
| 19 | 19 | } else { | |
| 20 | 20 | return r.json().then(function(data) { | |
| 21 | 21 | status.textContent = data.error || 'Failed to create repository'; |
| @@ -13,7 +13,7 @@ | |||
| 13 | 13 | headers: {'Content-Type': 'application/json', ...csrfHeaders()}, | |
| 14 | 14 | body: JSON.stringify({name: name}) | |
| 15 | 15 | }).then(function() { | |
| 16 | - | htmx.ajax('GET', '/dashboard/project/' + projectSlug + '/tabs/code', {target: '#tab-content', swap: 'innerHTML'}); | |
| 16 | + | htmx.ajax('GET', '/dashboard/project/' + projectSlug + '/tabs/code', {target: '#project-code', swap: 'innerHTML'}); | |
| 17 | 17 | }).catch(function() { | |
| 18 | 18 | var status = document.getElementById('create-repo-status'); | |
| 19 | 19 | if (status) { |
| @@ -65,7 +65,7 @@ | |||
| 65 | 65 | }); | |
| 66 | 66 | var label = action === 'delete' ? 'deleted' : action === 'publish' ? 'published' : 'unpublished'; | |
| 67 | 67 | showToast(count + ' item(s) ' + label + '.', 'success'); | |
| 68 | - | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#tab-content'); | |
| 68 | + | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content'); | |
| 69 | 69 | }) | |
| 70 | 70 | .catch(function(err) { | |
| 71 | 71 | showToast(err.message || 'Bulk operation failed'); | |
| @@ -86,7 +86,7 @@ | |||
| 86 | 86 | throw new Error(msg); | |
| 87 | 87 | }); | |
| 88 | 88 | showToast('Item published.', 'success'); | |
| 89 | - | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#tab-content'); | |
| 89 | + | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content'); | |
| 90 | 90 | }).catch(function(err) { | |
| 91 | 91 | showToast(err.message || 'Publish failed'); | |
| 92 | 92 | }); | |
| @@ -126,7 +126,7 @@ | |||
| 126 | 126 | throw new Error(msg); | |
| 127 | 127 | }); | |
| 128 | 128 | document.getElementById('bulk-price-form').classList.add('hidden'); | |
| 129 | - | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#tab-content'); | |
| 129 | + | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content'); | |
| 130 | 130 | }) | |
| 131 | 131 | .catch(function(err) { showToast(err.message || 'Could not update prices. Try again, or refresh the page.'); }); | |
| 132 | 132 | } | |
| @@ -154,7 +154,7 @@ | |||
| 154 | 154 | }); | |
| 155 | 155 | document.getElementById('bulk-tag-form').classList.add('hidden'); | |
| 156 | 156 | document.getElementById('bulk-tag-input').value = ''; | |
| 157 | - | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#tab-content'); | |
| 157 | + | htmx.ajax('GET', '/dashboard/project/' + PROJECT_SLUG + '/tabs/content', '#project-content'); | |
| 158 | 158 | }) | |
| 159 | 159 | .catch(function(err) { showToast(err.message || 'Could not add the tag. Try again, or refresh the page.'); }); | |
| 160 | 160 | } |
| @@ -36,6 +36,7 @@ | |||
| 36 | 36 | pub mod item_tabs; | |
| 37 | 37 | pub mod library_contacts; | |
| 38 | 38 | pub mod library_tabs; | |
| 39 | + | pub mod project_tabs; | |
| 39 | 40 | pub mod ssh_keys; | |
| 40 | 41 | pub mod user_analytics; | |
| 41 | 42 | pub mod widgets; |
| @@ -5,9 +5,8 @@ | |||
| 5 | 5 | use crate::auth::SessionUser; | |
| 6 | 6 | use crate::types::{ | |
| 7 | 7 | AdminAppealRow, AdminAuditLogRow, AdminCompCodeRow, AdminHeldUploadRow, AdminReportRow, | |
| 8 | - | AdminSignupRow, AdminUserRow, AdminWaitlistRow, ContentItem, Item, OnboardingChecklist, | |
| 9 | - | Project, ProjectCard, ReportStats, ScanHistoryDisplay, StatCard, Transaction, User, | |
| 10 | - | WaitlistStats, | |
| 8 | + | AdminSignupRow, AdminUserRow, AdminWaitlistRow, Item, OnboardingChecklist, Project, | |
| 9 | + | ProjectCard, ReportStats, ScanHistoryDisplay, Transaction, User, WaitlistStats, | |
| 11 | 10 | }; | |
| 12 | 11 | ||
| 13 | 12 | use super::CsrfTokenOption; | |
| @@ -55,17 +54,14 @@ | |||
| 55 | 54 | pub csrf_token: CsrfTokenOption, | |
| 56 | 55 | pub session_user: Option<SessionUser>, | |
| 57 | 56 | pub project: Project, | |
| 58 | - | pub creator_username: String, | |
| 59 | - | pub stats: Vec<StatCard>, | |
| 60 | - | pub items: Vec<ContentItem>, | |
| 61 | - | /// Whether this creator has connected their Stripe account (controls Subscriptions tab visibility). | |
| 62 | - | pub stripe_connected: bool, | |
| 63 | - | /// Whether this project has the "blog" feature enabled (controls Blog tab visibility). | |
| 64 | - | pub has_blog: bool, | |
| 65 | - | /// Whether git hosting is configured on this server (controls Code tab visibility). | |
| 66 | - | pub git_enabled: bool, | |
| 67 | - | /// Whether this project has the "cloud_sync" feature enabled (controls Cloud Sync tab visibility). | |
| 68 | - | pub synckit_enabled: bool, | |
| 57 | + | /// The tab strip and its panels, described rather than written out here. | |
| 58 | + | /// | |
| 59 | + | /// `6b24f2df`. Built by `crate::quasi::project_tabs`, which needs the shown | |
| 60 | + | /// panel's markup and the two feature tests. Six fields left with the strip: | |
| 61 | + | /// `creator_username`, `stats`, `items`, `stripe_connected`, `has_blog` and | |
| 62 | + | /// the two gates. The template read none of them -- the gates decided which | |
| 63 | + | /// buttons it wrote, and the rest were built per request for nothing. | |
| 64 | + | pub tabs: String, | |
| 69 | 65 | } | |
| 70 | 66 | ||
| 71 | 67 | /// Item management dashboard shell with tabbed navigation. |
| @@ -29,100 +29,8 @@ | |||
| 29 | 29 | </div> | |
| 30 | 30 | </header> | |
| 31 | 31 | ||
| 32 | - | <div class="tabs" role="tablist" aria-label="Project sections"> | |
| 33 | - | <button class="tab chosen" | |
| 34 | - | role="tab" | |
| 35 | - | aria-selected="true" | |
| 36 | - | aria-controls="tab-content" | |
| 37 | - | id="tab-overview" | |
| 38 | - | title="Stats and quick actions" | |
| 39 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/overview" | |
| 40 | - | hx-target="#tab-content" | |
| 41 | - | hx-swap="innerHTML" | |
| 42 | - | hx-indicator="#tab-spinner" | |
| 43 | - | data-action="onSetActiveTab">Overview</button> | |
| 44 | - | <button class="tab" | |
| 45 | - | role="tab" | |
| 46 | - | aria-selected="false" | |
| 47 | - | aria-controls="tab-content" | |
| 48 | - | id="tab-content-btn" | |
| 49 | - | title="Items, files, and blog posts" | |
| 50 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/content" | |
| 51 | - | hx-target="#tab-content" | |
| 52 | - | hx-swap="innerHTML" | |
| 53 | - | hx-indicator="#tab-spinner" | |
| 54 | - | data-action="onSetActiveTab">Content</button> | |
| 55 | - | <button class="tab" | |
| 56 | - | role="tab" | |
| 57 | - | aria-selected="false" | |
| 58 | - | aria-controls="tab-content" | |
| 59 | - | id="tab-analytics" | |
| 60 | - | title="Sales, revenue, and views over time" | |
| 61 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/analytics" | |
| 62 | - | hx-target="#tab-content" | |
| 63 | - | hx-swap="innerHTML" | |
| 64 | - | hx-indicator="#tab-spinner" | |
| 65 | - | data-action="onSetActiveTab">Analytics</button> | |
| 66 | - | <button class="tab" | |
| 67 | - | role="tab" | |
| 68 | - | aria-selected="false" | |
| 69 | - | aria-controls="tab-content" | |
| 70 | - | id="tab-monetization" | |
| 71 | - | title="Tiers, promo codes, and revenue splits" | |
| 72 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/monetization" | |
| 73 | - | hx-target="#tab-content" | |
| 74 | - | hx-swap="innerHTML" | |
| 75 | - | hx-indicator="#tab-spinner" | |
| 76 | - | data-action="onSetActiveTab">Monetization</button> | |
| 77 | - | {% if git_enabled %} | |
| 78 | - | <button class="tab" | |
| 79 | - | role="tab" | |
| 80 | - | aria-selected="false" | |
| 81 | - | aria-controls="tab-content" | |
| 82 | - | id="tab-code" | |
| 83 | - | title="Linked Git repositories and source browser" | |
| 84 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/code" | |
| 85 | - | hx-target="#tab-content" | |
| 86 | - | hx-swap="innerHTML" | |
| 87 | - | hx-indicator="#tab-spinner" | |
| 88 | - | data-action="onSetActiveTab">Code</button> | |
| 89 | - | {% endif %} | |
| 90 | - | {% if synckit_enabled %} | |
| 91 | - | <button class="tab" | |
| 92 | - | role="tab" | |
| 93 | - | aria-selected="false" | |
| 94 | - | aria-controls="tab-content" | |
| 95 | - | id="tab-synckit" | |
| 96 | - | title="End-to-end encrypted sync for your apps" | |
| 97 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/synckit" | |
| 98 | - | hx-target="#tab-content" | |
| 99 | - | hx-swap="innerHTML" | |
| 100 | - | hx-indicator="#tab-spinner" | |
| 101 | - | data-action="onSetActiveTab">Cloud Sync</button> | |
| 102 | - | {% endif %} | |
| 103 | - | <button class="tab" | |
| 104 | - | role="tab" | |
| 105 | - | aria-selected="false" | |
| 106 | - | aria-controls="tab-content" | |
| 107 | - | id="tab-settings" | |
| 108 | - | title="Visibility, URL name, deletion, and project options" | |
| 109 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/settings" | |
| 110 | - | hx-target="#tab-content" | |
| 111 | - | hx-swap="innerHTML" | |
| 112 | - | hx-indicator="#tab-spinner" | |
| 113 | - | data-action="onSetActiveTab">Settings</button> | |
| 114 | - | <span id="tab-spinner" class="htmx-indicator tab-spinner-indicator" aria-live="polite"> Loading...</span> | |
| 115 | - | </div> | |
| 32 | + | {{ tabs|safe }} | |
| 116 | 33 | ||
| 117 | - | <!-- Tab Content Container - initially loaded with Overview --> | |
| 118 | - | <div id="tab-content" class="tab-content active" | |
| 119 | - | role="tabpanel" | |
| 120 | - | aria-labelledby="tab-overview" | |
| 121 | - | hx-get="/dashboard/project/{{ project.slug }}/tabs/overview" | |
| 122 | - | hx-trigger="load" | |
| 123 | - | hx-swap="innerHTML"> | |
| 124 | - | <!-- Content loaded via HTMX --> | |
| 125 | - | </div> | |
| 126 | 34 | </div> | |
| 127 | 35 | {% endblock %} | |
| 128 | 36 |