max / makenotwork
- Co-Authored-By
- Claude Opus 4.8 <noreply@anthropic.com>
75 files changed,
+806 insertions,
-238 deletions
| @@ -376,9 +376,14 @@ | |||
| 376 | 376 | // user-pages host, so that origin must be a permitted frame source. | |
| 377 | 377 | let scheme = if state.config.host_url.starts_with("https") { "https" } else { "http" }; | |
| 378 | 378 | let user_pages_origin = format!("{scheme}://{}", state.config.user_pages_host); | |
| 379 | + | // script-src is 'self' (+ Stripe) with NO 'unsafe-inline': all inline | |
| 380 | + | // on*/hx-on handlers were moved to delegated listeners in static/*.js | |
| 381 | + | // (the data-action / data-hx-* dispatchers in mnw.js), so any injected | |
| 382 | + | // markup can no longer execute script. style-src keeps 'unsafe-inline' | |
| 383 | + | // because inline style="" attributes are still used throughout. | |
| 379 | 384 | let csp = format!( | |
| 380 | 385 | "default-src 'self'; \ | |
| 381 | - | script-src 'self' 'unsafe-inline' https://js.stripe.com; \ | |
| 386 | + | script-src 'self' https://js.stripe.com; \ | |
| 382 | 387 | style-src 'self' 'unsafe-inline'; \ | |
| 383 | 388 | img-src 'self' data: https:; \ | |
| 384 | 389 | font-src 'self'; \ |
| @@ -24,3 +24,13 @@ | |||
| 24 | 24 | const rect = e.currentTarget.getBoundingClientRect(); | |
| 25 | 25 | audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration; | |
| 26 | 26 | } | |
| 27 | + | ||
| 28 | + | // Wire the play/seek controls without inline on* handlers (the embed page runs | |
| 29 | + | // under the same CSP that forbids script-src 'unsafe-inline'). This page loads | |
| 30 | + | // only this file, not mnw.js's data-action dispatcher, so bind directly. | |
| 31 | + | document.addEventListener('DOMContentLoaded', function () { | |
| 32 | + | var play = document.getElementById('play'); | |
| 33 | + | if (play) play.addEventListener('click', togglePlay); | |
| 34 | + | var bar = document.getElementById('progress-bar'); | |
| 35 | + | if (bar) bar.addEventListener('click', seek); | |
| 36 | + | }); |
| @@ -674,3 +674,181 @@ | |||
| 674 | 674 | window.prompt('Copy this link:', url); | |
| 675 | 675 | } | |
| 676 | 676 | }); | |
| 677 | + | ||
| 678 | + | /* =========================================== | |
| 679 | + | ACTION DISPATCHER — delegated on* replacement | |
| 680 | + | =========================================== * | |
| 681 | + | * Lets templates drop inline on* handlers so the CSP can drop | |
| 682 | + | * script-src 'unsafe-inline'. An element declares behavior via a data-* | |
| 683 | + | * attribute whose value is either a built-in verb or a global function name: | |
| 684 | + | * | |
| 685 | + | * data-action -> click | |
| 686 | + | * data-change -> change | |
| 687 | + | * data-input -> input | |
| 688 | + | * data-submit -> submit (preventDefault is automatic for submit) | |
| 689 | + | * | |
| 690 | + | * Modifiers (presence of the attribute): data-prevent -> preventDefault, | |
| 691 | + | * data-stop -> stopPropagation. Applied before the verb runs. | |
| 692 | + | * | |
| 693 | + | * Built-in verbs operate on data-target (one or more element ids, space | |
| 694 | + | * separated); data-href supplies a URL: | |
| 695 | + | * show / hide / toggle -> classList remove/add/toggle 'hidden' on target(s) | |
| 696 | + | * click -> .click() the target(s) | |
| 697 | + | * remove -> .remove() the target(s) | |
| 698 | + | * remove-self -> .remove() the dispatching element | |
| 699 | + | * nav -> window.location.href = data-href | |
| 700 | + | * | |
| 701 | + | * Any other value is treated as a global function name and invoked as | |
| 702 | + | * fn.apply(element, args) with `this` === the element and args drawn from | |
| 703 | + | * data-arg / data-arg2 (both optional). So a former `fn()` becomes | |
| 704 | + | * data-action="fn"; `fn('x')` becomes data-action="fn" data-arg="x"; and a | |
| 705 | + | * former `fn(this)` becomes data-action="fn" with fn reading `this`. | |
| 706 | + | */ | |
| 707 | + | (function () { | |
| 708 | + | function targets(el) { | |
| 709 | + | var raw = el.getAttribute('data-target'); | |
| 710 | + | if (!raw) return []; | |
| 711 | + | return raw.split(/\s+/).filter(Boolean).map(function (id) { | |
| 712 | + | return document.getElementById(id); | |
| 713 | + | }).filter(Boolean); | |
| 714 | + | } | |
| 715 | + | ||
| 716 | + | var BUILTINS = { | |
| 717 | + | show: function (el) { targets(el).forEach(function (t) { t.classList.remove('hidden'); }); }, | |
| 718 | + | hide: function (el) { targets(el).forEach(function (t) { t.classList.add('hidden'); }); }, | |
| 719 | + | toggle: function (el) { targets(el).forEach(function (t) { t.classList.toggle('hidden'); }); }, | |
| 720 | + | click: function (el) { targets(el).forEach(function (t) { t.click(); }); }, | |
| 721 | + | remove: function (el) { targets(el).forEach(function (t) { t.remove(); }); }, | |
| 722 | + | 'remove-self': function (el) { el.remove(); } | |
| 723 | + | }; | |
| 724 | + | ||
| 725 | + | function run(el, verb, evt) { | |
| 726 | + | if (el.hasAttribute('data-prevent')) evt.preventDefault(); | |
| 727 | + | if (el.hasAttribute('data-stop')) evt.stopPropagation(); | |
| 728 | + | if (verb === 'nav') { | |
| 729 | + | var href = el.getAttribute('data-href'); | |
| 730 | + | if (href) window.location.href = href; | |
| 731 | + | return; | |
| 732 | + | } | |
| 733 | + | if (BUILTINS[verb]) { BUILTINS[verb](el, evt); return; } | |
| 734 | + | var fn = window[verb]; | |
| 735 | + | if (typeof fn !== 'function') { | |
| 736 | + | if (window.console) console.warn('data-action: no global function', verb); | |
| 737 | + | return; | |
| 738 | + | } | |
| 739 | + | var args = []; | |
| 740 | + | if (el.hasAttribute('data-arg')) args.push(el.getAttribute('data-arg')); | |
| 741 | + | if (el.hasAttribute('data-arg2')) args.push(el.getAttribute('data-arg2')); | |
| 742 | + | fn.apply(el, args); | |
| 743 | + | } | |
| 744 | + | ||
| 745 | + | function listen(eventName, attr, autoPrevent) { | |
| 746 | + | document.addEventListener(eventName, function (evt) { | |
| 747 | + | var el = evt.target.closest('[' + attr + ']'); | |
| 748 | + | if (!el) return; | |
| 749 | + | if (autoPrevent) evt.preventDefault(); | |
| 750 | + | run(el, el.getAttribute(attr), evt); | |
| 751 | + | }); | |
| 752 | + | } | |
| 753 | + | ||
| 754 | + | listen('click', 'data-action', false); | |
| 755 | + | listen('change', 'data-change', false); | |
| 756 | + | listen('input', 'data-input', false); | |
| 757 | + | listen('submit', 'data-submit', true); | |
| 758 | + | })(); | |
| 759 | + | ||
| 760 | + | /* =========================================== | |
| 761 | + | HTMX CALLBACK DISPATCHER — hx-on:: replacement | |
| 762 | + | =========================================== * | |
| 763 | + | * Replaces inline `hx-on::after-request` / `hx-on:htmx:config-request` | |
| 764 | + | * attributes (which are inline scripts and would keep script-src pinned to | |
| 765 | + | * 'unsafe-inline') with delegated listeners driven by data attributes. | |
| 766 | + | * | |
| 767 | + | * On the element that fired the request (evt.detail.elt): | |
| 768 | + | * data-hx-always behavior name that runs regardless of outcome | |
| 769 | + | * data-hx-always-click=ID .click() that element id regardless of outcome | |
| 770 | + | * ...and, only when the request succeeded (evt.detail.successful): | |
| 771 | + | * data-hx-reset this.reset() (form) | |
| 772 | + | * data-hx-click=ID .click() the element with that id | |
| 773 | + | * data-hx-get=URL + data-hx-target=SEL htmx.ajax('GET', URL, SEL) | |
| 774 | + | * data-hx-nav=URL window.location.href = URL | |
| 775 | + | * data-hx-reload window.location.reload() | |
| 776 | + | * data-hx-toast=MSG (+ data-hx-toast-type) showToast(MSG, type) | |
| 777 | + | * data-hx-behavior=NAME run a named behavior (custom multi-step cases) | |
| 778 | + | * | |
| 779 | + | * configRequest (before send): | |
| 780 | + | * data-hx-config="publish-at-iso" normalize publish_at to ISO 8601 | |
| 781 | + | */ | |
| 782 | + | (function () { | |
| 783 | + | function byId(id) { return id && document.getElementById(id); } | |
| 784 | + | ||
| 785 | + | function refreshProfileTab() { | |
| 786 | + | var t = document.getElementById('settings-body'); | |
| 787 | + | if (t) { | |
| 788 | + | htmx.ajax('GET', '/dashboard/tabs/profile', { target: t, swap: 'innerHTML' }); | |
| 789 | + | } else { | |
| 790 | + | var b = document.getElementById('tab-profile'); | |
| 791 | + | if (b) b.click(); | |
| 792 | + | } | |
| 793 | + | } | |
| 794 | + | ||
| 795 | + | var BEHAVIORS = { | |
| 796 | + | 'mark-added': function (el) { el.textContent = 'Added'; el.disabled = true; }, | |
| 797 | + | // Faithful to the original inline handler: the row fades only on success, | |
| 798 | + | // but the label flips to 'Added' unconditionally. Registered under | |
| 799 | + | // data-hx-always so it sees both outcomes. | |
| 800 | + | 'fade-row-added': function (el, evt) { | |
| 801 | + | if (evt.detail.successful) { | |
| 802 | + | var tr = el.closest('tr'); | |
| 803 | + | if (tr) tr.classList.add('is-faded'); | |
| 804 | + | } | |
| 805 | + | el.textContent = 'Added'; | |
| 806 | + | }, | |
| 807 | + | 'refresh-profile': function () { refreshProfileTab(); }, | |
| 808 | + | 'refresh-profile-if-verified': function (el, evt) { | |
| 809 | + | if (evt.detail.xhr && evt.detail.xhr.responseText.indexOf('verified successfully') !== -1) { | |
| 810 | + | refreshProfileTab(); | |
| 811 | + | } | |
| 812 | + | } | |
| 813 | + | }; | |
| 814 | + | ||
| 815 | + | document.body.addEventListener('htmx:afterRequest', function (evt) { | |
| 816 | + | var el = evt.detail.elt; | |
| 817 | + | if (!el || !el.getAttribute) return; | |
| 818 | + | ||
| 819 | + | var always = el.getAttribute('data-hx-always'); | |
| 820 | + | if (always && BEHAVIORS[always]) BEHAVIORS[always](el, evt); | |
| 821 | + | if (el.hasAttribute('data-hx-always-click')) { | |
| 822 | + | var ac = byId(el.getAttribute('data-hx-always-click')); | |
| 823 | + | if (ac) ac.click(); | |
| 824 | + | } | |
| 825 | + | ||
| 826 | + | if (!evt.detail.successful) return; | |
| 827 | + | ||
| 828 | + | if (el.hasAttribute('data-hx-reset') && typeof el.reset === 'function') el.reset(); | |
| 829 | + | if (el.hasAttribute('data-hx-click')) { | |
| 830 | + | var t = byId(el.getAttribute('data-hx-click')); | |
| 831 | + | if (t) t.click(); | |
| 832 | + | } | |
| 833 | + | if (el.hasAttribute('data-hx-get')) { | |
| 834 | + | var url = el.getAttribute('data-hx-get'); | |
| 835 | + | var target = el.getAttribute('data-hx-target'); | |
| 836 | + | if (url && target) htmx.ajax('GET', url, target); | |
| 837 | + | } | |
| 838 | + | if (el.hasAttribute('data-hx-nav')) window.location.href = el.getAttribute('data-hx-nav'); | |
| 839 | + | if (el.hasAttribute('data-hx-reload')) window.location.reload(); | |
| 840 | + | if (el.hasAttribute('data-hx-toast')) { | |
| 841 | + | showToast(el.getAttribute('data-hx-toast'), el.getAttribute('data-hx-toast-type') || 'info'); | |
| 842 | + | } | |
| 843 | + | var beh = el.getAttribute('data-hx-behavior'); | |
| 844 | + | if (beh && BEHAVIORS[beh]) BEHAVIORS[beh](el, evt); | |
| 845 | + | }); | |
| 846 | + | ||
| 847 | + | document.body.addEventListener('htmx:configRequest', function (evt) { | |
| 848 | + | var el = evt.detail.elt; | |
| 849 | + | if (el && el.getAttribute && el.getAttribute('data-hx-config') === 'publish-at-iso') { | |
| 850 | + | var v = evt.detail.parameters.publish_at; | |
| 851 | + | if (v) evt.detail.parameters.publish_at = new Date(v).toISOString(); | |
| 852 | + | } | |
| 853 | + | }); | |
| 854 | + | })(); |
| @@ -35,8 +35,8 @@ | |||
| 35 | 35 | <a href="/changelog">Changelog</a> | |
| 36 | 36 | <a href="mailto:info@makenot.work">Contact</a> | |
| 37 | 37 | <a href="/health" title="Service status and uptime">Status</a> | |
| 38 | - | <a href="#" onclick="event.preventDefault(); showWhatsNewModal();">What's new</a> | |
| 39 | - | <a href="#" onclick="event.preventDefault(); toggleShortcutsHelp();" title="Keyboard shortcuts (?)">Shortcuts</a> | |
| 38 | + | <a href="#" data-prevent data-action="showWhatsNewModal">What's new</a> | |
| 39 | + | <a href="#" data-prevent data-action="toggleShortcutsHelp" title="Keyboard shortcuts (?)">Shortcuts</a> | |
| 40 | 40 | </div> | |
| 41 | 41 | <p>© 2026 Make Creative, LLC</p> | |
| 42 | 42 | </footer> | |
| @@ -44,7 +44,14 @@ | |||
| 44 | 44 | <!-- Toast notification container --> | |
| 45 | 45 | <div id="notifications" class="toast-container" role="alert" aria-live="polite"></div> | |
| 46 | 46 | ||
| 47 | - | <script src="/static/mnw.js?v=0531"></script> | |
| 47 | + | <script src="/static/mnw.js?v=0701"></script> | |
| 48 | + | <!-- Delegated on* handlers, extracted from inline attributes so the CSP can | |
| 49 | + | drop script-src 'unsafe-inline'. Loaded globally after mnw.js (which | |
| 50 | + | defines the data-action dispatcher). --> | |
| 51 | + | <script src="/static/actions-pages.js?v=0701"></script> | |
| 52 | + | <script src="/static/actions-partials.js?v=0701"></script> | |
| 53 | + | <script src="/static/actions-tabs.js?v=0701"></script> | |
| 54 | + | <script src="/static/actions-dashboards.js?v=0701"></script> | |
| 48 | 55 | <script src="/static/carousel.js?v=0605"></script> | |
| 49 | 56 | <script src="/static/collections.js?v=0514"></script> | |
| 50 | 57 | <script src="/static/synckit-billing.js?v=0620"></script> |
| @@ -86,7 +86,7 @@ | |||
| 86 | 86 | </section> | |
| 87 | 87 | ||
| 88 | 88 | <form class="cp-reset" method="post" action="{{ base_path }}/reset" | |
| 89 | - | onsubmit="return confirm('Reset this page to the platform default? This clears your custom HTML and CSS.');"> | |
| 89 | + | data-submit="confirmResetPage"> | |
| 90 | 90 | {% if let Some(token) = csrf_token %}<input type="hidden" name="_csrf" value="{{ token }}">{% endif %} | |
| 91 | 91 | <button type="submit" class="btn-danger">Reset to default</button> | |
| 92 | 92 | </form> |
| @@ -10,7 +10,7 @@ | |||
| 10 | 10 | <div class="breadcrumb"> | |
| 11 | 11 | <a href="/dashboard">Dashboard</a> / | |
| 12 | 12 | <a href="/dashboard/project/{{ project_slug }}">{{ project_slug }}</a> / | |
| 13 | - | <a href="/dashboard/project/{{ project_slug }}" onclick="setTimeout(function(){document.getElementById('tab-blog').click()},100)">Blog</a> / | |
| 13 | + | <a href="/dashboard/project/{{ project_slug }}" data-action="blogTabNav">Blog</a> / | |
| 14 | 14 | {% if editing %}Edit{% else %}New Post{% endif %} | |
| 15 | 15 | </div> | |
| 16 | 16 | ||
| @@ -37,7 +37,7 @@ | |||
| 37 | 37 | <div class="form-group"> | |
| 38 | 38 | <label for="post-body">Content (Markdown)</label> | |
| 39 | 39 | <textarea id="post-body" rows="20" class="blog-editor-input input--sm w-full" placeholder="Write your post in Markdown...">{{ post_body }}</textarea> | |
| 40 | - | <button type="button" class="btn-secondary blog-editor-media-btn" onclick="mediaPickerOpen('post-body')">Insert Image</button> | |
| 40 | + | <button type="button" class="btn-secondary blog-editor-media-btn" data-action="mediaPickerOpen" data-arg="post-body">Insert Image</button> | |
| 41 | 41 | </div> | |
| 42 | 42 | {% if is_changelog_project %} | |
| 43 | 43 | <div class="form-group blog-editor-landing-toggle"> | |
| @@ -51,7 +51,7 @@ | |||
| 51 | 51 | <div class="blog-editor-actions"> | |
| 52 | 52 | <button class="btn-primary" id="save-draft-btn">{% if editing %}Save as Draft{% else %}Save Draft{% endif %}</button> | |
| 53 | 53 | <button class="btn-secondary" id="publish-btn">{% if editing && post_is_published %}Update{% else %}Publish{% endif %}</button> | |
| 54 | - | <a href="/dashboard/project/{{ project_slug }}" onclick="setTimeout(function(){document.getElementById('tab-blog').click()},100)"> | |
| 54 | + | <a href="/dashboard/project/{{ project_slug }}" data-action="blogTabNav"> | |
| 55 | 55 | <button class="btn-secondary" type="button">Cancel</button> | |
| 56 | 56 | </a> | |
| 57 | 57 | </div> |
| @@ -17,7 +17,7 @@ | |||
| 17 | 17 | <h1 class="page-title">Export Your Data</h1> | |
| 18 | 18 | <p class="subtitle">Download your content, projects, and transaction history.</p> | |
| 19 | 19 | </div> | |
| 20 | - | <button class="btn-primary export-all-btn" id="export-all-btn" onclick="exportAll()"> | |
| 20 | + | <button class="btn-primary export-all-btn" id="export-all-btn" data-action="exportAll"> | |
| 21 | 21 | Export All | |
| 22 | 22 | </button> | |
| 23 | 23 | </header> |
| @@ -33,7 +33,7 @@ | |||
| 33 | 33 | <span>{{ item.sales_count }} sales</span> | |
| 34 | 34 | <button class="btn-secondary small dashboard-item-share-btn" | |
| 35 | 35 | title="Copy item ID for use in embeds or integrations" | |
| 36 | - | onclick="navigator.clipboard.writeText('{{ item.id }}'); this.textContent='Copied!'; setTimeout(()=>this.textContent='Copy Item ID', 1500);">Copy Item ID</button> | |
| 36 | + | data-action="onCopyItemId" data-arg="{{ item.id }}">Copy Item ID</button> | |
| 37 | 37 | </div> | |
| 38 | 38 | </header> | |
| 39 | 39 | ||
| @@ -48,7 +48,7 @@ | |||
| 48 | 48 | hx-target="#tab-content" | |
| 49 | 49 | hx-swap="innerHTML" | |
| 50 | 50 | hx-indicator="#tab-spinner" | |
| 51 | - | onclick="setActiveTab(this)">Overview</button> | |
| 51 | + | data-action="onSetActiveTab">Overview</button> | |
| 52 | 52 | <button class="tab" | |
| 53 | 53 | role="tab" | |
| 54 | 54 | aria-selected="false" | |
| @@ -59,7 +59,7 @@ | |||
| 59 | 59 | hx-target="#tab-content" | |
| 60 | 60 | hx-swap="innerHTML" | |
| 61 | 61 | hx-indicator="#tab-spinner" | |
| 62 | - | onclick="setActiveTab(this)">Details</button> | |
| 62 | + | data-action="onSetActiveTab">Details</button> | |
| 63 | 63 | <button class="tab" | |
| 64 | 64 | role="tab" | |
| 65 | 65 | aria-selected="false" | |
| @@ -70,7 +70,7 @@ | |||
| 70 | 70 | hx-target="#tab-content" | |
| 71 | 71 | hx-swap="innerHTML" | |
| 72 | 72 | hx-indicator="#tab-spinner" | |
| 73 | - | onclick="setActiveTab(this)">Pricing</button> | |
| 73 | + | data-action="onSetActiveTab">Pricing</button> | |
| 74 | 74 | {% if item.item_type != "bundle" %} | |
| 75 | 75 | <button class="tab" | |
| 76 | 76 | role="tab" | |
| @@ -82,7 +82,7 @@ | |||
| 82 | 82 | hx-target="#tab-content" | |
| 83 | 83 | hx-swap="innerHTML" | |
| 84 | 84 | hx-indicator="#tab-spinner" | |
| 85 | - | onclick="setActiveTab(this)">Files</button> | |
| 85 | + | data-action="onSetActiveTab">Files</button> | |
| 86 | 86 | {% endif %} | |
| 87 | 87 | <button class="tab" | |
| 88 | 88 | role="tab" | |
| @@ -94,7 +94,7 @@ | |||
| 94 | 94 | hx-target="#tab-content" | |
| 95 | 95 | hx-swap="innerHTML" | |
| 96 | 96 | hx-indicator="#tab-spinner" | |
| 97 | - | onclick="setActiveTab(this)">Sales</button> | |
| 97 | + | data-action="onSetActiveTab">Sales</button> | |
| 98 | 98 | <span id="tab-spinner" class="htmx-indicator tab-spinner-indicator" aria-live="polite"> Loading...</span> | |
| 99 | 99 | </div> | |
| 100 | 100 |
| @@ -40,7 +40,7 @@ | |||
| 40 | 40 | hx-target="#tab-content" | |
| 41 | 41 | hx-swap="innerHTML" | |
| 42 | 42 | hx-indicator="#tab-spinner" | |
| 43 | - | onclick="setActiveTab(this)">Overview</button> | |
| 43 | + | data-action="onSetActiveTab">Overview</button> | |
| 44 | 44 | <button class="tab" | |
| 45 | 45 | role="tab" | |
| 46 | 46 | aria-selected="false" | |
| @@ -51,7 +51,7 @@ | |||
| 51 | 51 | hx-target="#tab-content" | |
| 52 | 52 | hx-swap="innerHTML" | |
| 53 | 53 | hx-indicator="#tab-spinner" | |
| 54 | - | onclick="setActiveTab(this)">Content</button> | |
| 54 | + | data-action="onSetActiveTab">Content</button> | |
| 55 | 55 | <button class="tab" | |
| 56 | 56 | role="tab" | |
| 57 | 57 | aria-selected="false" | |
| @@ -62,7 +62,7 @@ | |||
| 62 | 62 | hx-target="#tab-content" | |
| 63 | 63 | hx-swap="innerHTML" | |
| 64 | 64 | hx-indicator="#tab-spinner" | |
| 65 | - | onclick="setActiveTab(this)">Analytics</button> | |
| 65 | + | data-action="onSetActiveTab">Analytics</button> | |
| 66 | 66 | <button class="tab" | |
| 67 | 67 | role="tab" | |
| 68 | 68 | aria-selected="false" | |
| @@ -73,7 +73,7 @@ | |||
| 73 | 73 | hx-target="#tab-content" | |
| 74 | 74 | hx-swap="innerHTML" | |
| 75 | 75 | hx-indicator="#tab-spinner" | |
| 76 | - | onclick="setActiveTab(this)">Monetization</button> | |
| 76 | + | data-action="onSetActiveTab">Monetization</button> | |
| 77 | 77 | {% if git_enabled %} | |
| 78 | 78 | <button class="tab" | |
| 79 | 79 | role="tab" | |
| @@ -85,7 +85,7 @@ | |||
| 85 | 85 | hx-target="#tab-content" | |
| 86 | 86 | hx-swap="innerHTML" | |
| 87 | 87 | hx-indicator="#tab-spinner" | |
| 88 | - | onclick="setActiveTab(this)">Code</button> | |
| 88 | + | data-action="onSetActiveTab">Code</button> | |
| 89 | 89 | {% endif %} | |
| 90 | 90 | {% if synckit_enabled %} | |
| 91 | 91 | <button class="tab" | |
| @@ -98,7 +98,7 @@ | |||
| 98 | 98 | hx-target="#tab-content" | |
| 99 | 99 | hx-swap="innerHTML" | |
| 100 | 100 | hx-indicator="#tab-spinner" | |
| 101 | - | onclick="setActiveTab(this)">Cloud Sync</button> | |
| 101 | + | data-action="onSetActiveTab">Cloud Sync</button> | |
| 102 | 102 | {% endif %} | |
| 103 | 103 | <button class="tab" | |
| 104 | 104 | role="tab" | |
| @@ -110,7 +110,7 @@ | |||
| 110 | 110 | hx-target="#tab-content" | |
| 111 | 111 | hx-swap="innerHTML" | |
| 112 | 112 | hx-indicator="#tab-spinner" | |
| 113 | - | onclick="setActiveTab(this)">Settings</button> | |
| 113 | + | data-action="onSetActiveTab">Settings</button> | |
| 114 | 114 | <span id="tab-spinner" class="htmx-indicator tab-spinner-indicator" aria-live="polite"> Loading...</span> | |
| 115 | 115 | </div> | |
| 116 | 116 |
| @@ -21,7 +21,7 @@ | |||
| 21 | 21 | <p>You can:</p> | |
| 22 | 22 | <div class="account-status-actions"> | |
| 23 | 23 | <form hx-post="/api/users/me/reactivate" hx-swap="none" | |
| 24 | - | onsubmit="setTimeout(function(){ window.location.reload(); }, 500)"> | |
| 24 | + | data-submit="reactivateReload"> | |
| 25 | 25 | <button type="submit" class="btn-primary">Reactivate Account</button> | |
| 26 | 26 | </form> | |
| 27 | 27 | <a href="/dashboard/export" class="button">Export Data</a> | |
| @@ -83,7 +83,7 @@ | |||
| 83 | 83 | hx-target="#tab-content" | |
| 84 | 84 | hx-swap="innerHTML" | |
| 85 | 85 | hx-indicator="#tab-spinner" | |
| 86 | - | onclick="setActiveTab(this)">Support</button> | |
| 86 | + | data-action="onSetActiveTab">Support</button> | |
| 87 | 87 | <span id="tab-spinner" class="htmx-indicator tab-spinner" aria-live="polite"> Loading...</span> | |
| 88 | 88 | </div> | |
| 89 | 89 | ||
| @@ -106,7 +106,7 @@ | |||
| 106 | 106 | hx-target="#tab-content" | |
| 107 | 107 | hx-swap="innerHTML" | |
| 108 | 108 | hx-indicator="#tab-spinner" | |
| 109 | - | onclick="setActiveTab(this)">Projects</button> | |
| 109 | + | data-action="onSetActiveTab">Projects</button> | |
| 110 | 110 | {% endif %}{% endif %} | |
| 111 | 111 | <button class="tab{% if let Some(su) = session_user %}{% if !su.can_create_projects %} is-selected{% endif %}{% endif %}" | |
| 112 | 112 | role="tab" | |
| @@ -118,7 +118,7 @@ | |||
| 118 | 118 | hx-target="#tab-content" | |
| 119 | 119 | hx-swap="innerHTML" | |
| 120 | 120 | hx-indicator="#tab-spinner" | |
| 121 | - | onclick="setActiveTab(this)">Payments</button> | |
| 121 | + | data-action="onSetActiveTab">Payments</button> | |
| 122 | 122 | {% if let Some(su) = session_user %}{% if su.can_create_projects %} | |
| 123 | 123 | <button class="tab" | |
| 124 | 124 | role="tab" | |
| @@ -130,7 +130,7 @@ | |||
| 130 | 130 | hx-target="#tab-content" | |
| 131 | 131 | hx-swap="innerHTML" | |
| 132 | 132 | hx-indicator="#tab-spinner" | |
| 133 | - | onclick="setActiveTab(this)">Analytics</button> | |
| 133 | + | data-action="onSetActiveTab">Analytics</button> | |
| 134 | 134 | {% endif %}{% endif %} | |
| 135 | 135 | <button class="tab" | |
| 136 | 136 | role="tab" | |
| @@ -142,7 +142,7 @@ | |||
| 142 | 142 | hx-target="#tab-content" | |
| 143 | 143 | hx-swap="innerHTML" | |
| 144 | 144 | hx-indicator="#tab-spinner" | |
| 145 | - | onclick="setActiveTab(this)">Settings</button> | |
| 145 | + | data-action="onSetActiveTab">Settings</button> | |
| 146 | 146 | <button class="tab" | |
| 147 | 147 | role="tab" | |
| 148 | 148 | aria-selected="false" | |
| @@ -153,7 +153,7 @@ | |||
| 153 | 153 | hx-target="#tab-content" | |
| 154 | 154 | hx-swap="innerHTML" | |
| 155 | 155 | hx-indicator="#tab-spinner" | |
| 156 | - | onclick="setActiveTab(this)">Support</button> | |
| 156 | + | data-action="onSetActiveTab">Support</button> | |
| 157 | 157 | <span id="tab-spinner" class="htmx-indicator tab-spinner" aria-live="polite"> Loading...</span> | |
| 158 | 158 | </div> | |
| 159 | 159 |