ultimate-jekyll-manager 0.0.269 → 0.0.271

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CLAUDE.md +36 -0
  3. package/README.md +10 -0
  4. package/dist/assets/css/pages/account/index.scss +23 -6
  5. package/dist/assets/js/libs/auth.js +3 -0
  6. package/dist/assets/js/pages/account/index.js +35 -2
  7. package/dist/assets/js/pages/account/sections/data-request.js +253 -0
  8. package/dist/assets/js/pages/account/sections/delete.js +12 -17
  9. package/dist/assets/js/pages/payment/checkout/index.js +12 -0
  10. package/dist/build.js +6 -0
  11. package/dist/commands/clean.js +10 -0
  12. package/dist/commands/setup.js +13 -0
  13. package/dist/defaults/dist/_includes/core/body.html +2 -4
  14. package/dist/defaults/dist/_layouts/blueprint/legal/privacy.md +22 -1
  15. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +227 -50
  16. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/reset.html +1 -1
  17. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signin.html +1 -1
  18. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html +1 -1
  19. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/index.html +1 -1
  20. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html +1 -1
  21. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +1 -1
  22. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/index.html +2 -2
  23. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/checkout.html +5 -3
  24. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/status.html +1 -1
  25. package/dist/gulp/tasks/jekyll.js +9 -0
  26. package/dist/gulp/tasks/sass.js +10 -0
  27. package/dist/gulp/tasks/webpack.js +10 -0
  28. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -14,6 +14,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ ---
18
+ ## [Unreleased]
19
+ ### Added
20
+ - Quick boot mode (`UJ_QUICK=true`) for faster dev server startup (~5s vs ~20s) by skipping clean, slow setup operations, and deferring webpack/sass compilation until after Jekyll's first build
21
+
22
+ ### Changed
23
+ - Add `onsubmit="return false"` to all JS-managed forms as a safety net against native submission before FormManager loads
24
+ - Checkout payment method buttons start hidden and are revealed via `data-wm-bind` when payment methods load
25
+ - Remove development-only guard from click prevention logging in body.html
26
+
27
+ ### Fixed
28
+ - Add dev-only artificial pre-delay support to checkout page for testing form protection timing
29
+
17
30
  ---
18
31
  ## [1.0.0] - 2024-06-19
19
32
  ### Added
package/CLAUDE.md CHANGED
@@ -595,6 +595,42 @@ Selectively protect non-standard elements that trigger important actions:
595
595
  - **Click Prevention:** `src/defaults/dist/_includes/core/body.html` - Inline script
596
596
  - **State Removal:** `src/assets/js/core/complete.js` - Removes loading state
597
597
 
598
+ ### Form Protection Standards
599
+
600
+ All JS-managed forms use a layered protection strategy to prevent native form submission before JavaScript takes control:
601
+
602
+ #### Layer 1: `onsubmit="return false"` on ALL JS-managed forms
603
+
604
+ Every `<form>` that will be managed by FormManager MUST include `onsubmit="return false"`:
605
+
606
+ ```html
607
+ <form id="my-form" onsubmit="return false">
608
+ ```
609
+
610
+ This is a zero-cost safety net that prevents native form submission if a user clicks submit before FormManager attaches its `e.preventDefault()` handler. FormManager's own submit handling overrides this — there is no conflict.
611
+
612
+ **Exception:** Traditional forms with an `action` attribute that intentionally navigate (e.g., search forms, external form submissions) should NOT include this.
613
+
614
+ #### Layer 2: Button initial state based on use case
615
+
616
+ | Use Case | Initial State | Mechanism |
617
+ |----------|---------------|-----------|
618
+ | Buttons dependent on async data (checkout payment methods) | `hidden` | `data-wm-bind="@show ..."` reveals when data loads |
619
+ | Buttons on auth/sensitive forms | `disabled` | FormManager's `ready()` removes `disabled` |
620
+ | Buttons on simple forms (contact, newsletter) | Default (visible) | FormManager's `autoReady: true` enables quickly |
621
+
622
+ #### Layer 3: FormManager `autoReady` configuration
623
+
624
+ | Scenario | `autoReady` | `ready()` call |
625
+ |----------|-------------|----------------|
626
+ | No async work before form init | `true` (default) | Automatic on DOM ready |
627
+ | Async work before form init (API calls, redirects) | `false` | Explicit call after async completes |
628
+
629
+ **Reference implementations:**
630
+ - Simple form: `src/assets/js/pages/contact/index.js`
631
+ - Auth form: `src/assets/js/libs/auth.js`
632
+ - Async data form: `src/assets/js/pages/payment/checkout/index.js`
633
+
598
634
  ## Lazy Loading System
599
635
 
600
636
  Ultimate Jekyll uses a custom lazy loading system powered by web-manager.
package/README.md CHANGED
@@ -314,6 +314,16 @@ Add the `.btn-action` class to protect custom elements that trigger important ac
314
314
  **Use `.btn-action` for:** API calls, form submissions, data modifications, payments, destructive actions
315
315
  **Don't use for:** Navigation, UI toggles, modals, accordions, harmless interactions
316
316
 
317
+ #### Form Protection Standards
318
+
319
+ All JS-managed forms use a layered protection strategy:
320
+
321
+ 1. **`onsubmit="return false"`** on every `<form>` managed by FormManager — prevents native submission before JS loads
322
+ 2. **Button initial state** — buttons dependent on async data start `hidden` (revealed by `data-wm-bind`); auth buttons start `disabled` (enabled by FormManager's `ready()`)
323
+ 3. **FormManager `autoReady`** — use `autoReady: false` when async work happens before form init, call `ready()` explicitly after
324
+
325
+ **Exception:** Traditional forms with an `action` attribute that intentionally navigate should NOT include `onsubmit="return false"`.
326
+
317
327
  ### Ad Units (Verts)
318
328
 
319
329
  UJ provides ad unit includes that display Google AdSense ads with automatic fallback to in-house promo-server ads when AdSense is blocked or unfilled.
@@ -64,21 +64,38 @@
64
64
  }
65
65
  }
66
66
 
67
- // Delete account section styles
67
+ // Shared styles for data-request and delete sections
68
+ #data-request-section,
68
69
  #delete-section {
69
- .card.border-danger {
70
+ .card-form-border {
70
71
  border-width: 2px;
71
72
  }
72
73
 
74
+ .form-check-label {
75
+ user-select: none;
76
+ }
77
+
78
+ .accordion-trigger {
79
+ background: none;
80
+ border: none;
81
+ padding: 0;
82
+ font: inherit;
83
+ color: inherit;
84
+ cursor: pointer;
85
+
86
+ &:focus {
87
+ outline: none;
88
+ }
89
+ }
90
+ }
91
+
92
+ // Delete account section styles
93
+ #delete-section {
73
94
  #delete-account-btn {
74
95
  &:disabled {
75
96
  opacity: 0.5;
76
97
  cursor: not-allowed;
77
98
  }
78
99
  }
79
-
80
- .form-check-label {
81
- user-select: none;
82
- }
83
100
  }
84
101
 
@@ -114,6 +114,7 @@ export default function (Manager) {
114
114
  formManager = new FormManager('#auth-form', {
115
115
  autoReady: false, // We'll call ready() after checking redirect result
116
116
  allowResubmit: false,
117
+ warnOnUnsavedChanges: false,
117
118
  submittingText: 'Signing in...',
118
119
  submittedText: 'Signed In!',
119
120
  });
@@ -128,6 +129,7 @@ export default function (Manager) {
128
129
  formManager = new FormManager('#auth-form', {
129
130
  autoReady: false, // We'll call ready() after checking redirect result
130
131
  allowResubmit: false,
132
+ warnOnUnsavedChanges: false,
131
133
  submittingText: 'Creating account...',
132
134
  submittedText: 'Account Created!',
133
135
  });
@@ -142,6 +144,7 @@ export default function (Manager) {
142
144
  formManager = new FormManager('#auth-form', {
143
145
  autoReady: false, // We'll call ready() after checking redirect result
144
146
  allowResubmit: false,
147
+ warnOnUnsavedChanges: false,
145
148
  submittingText: 'Sending...',
146
149
  submittedText: 'Email Sent!',
147
150
  });
@@ -8,6 +8,7 @@ import * as teamSection from './sections/team.js';
8
8
  import * as referralsSection from './sections/referrals.js';
9
9
  import * as apiKeysSection from './sections/api-keys.js';
10
10
  import * as deleteSection from './sections/delete.js';
11
+ import * as dataRequestSection from './sections/data-request.js';
11
12
  import * as connectionsSection from './sections/connections.js';
12
13
  let webManager = null;
13
14
 
@@ -47,6 +48,7 @@ const sectionModules = {
47
48
  referrals: referralsSection,
48
49
  'api-keys': apiKeysSection,
49
50
  delete: deleteSection,
51
+ 'data-request': dataRequestSection,
50
52
  connections: connectionsSection
51
53
  };
52
54
 
@@ -62,10 +64,13 @@ async function initializeAccount() {
62
64
  // Setup navigation
63
65
  setupNavigation();
64
66
 
65
- // Check if delete hash is present on initial load
67
+ // Check if delete/data-request hash is present on initial load
66
68
  if (window.location.hash === '#delete') {
67
69
  showDeleteOption();
68
70
  }
71
+ if (window.location.hash === '#data-request') {
72
+ showDataRequestOption();
73
+ }
69
74
 
70
75
  // Initialize all section modules
71
76
  Object.values(sectionModules).forEach(module => {
@@ -180,6 +185,10 @@ function loadAllSectionData(authState) {
180
185
  sectionModules.delete.loadData(account);
181
186
  }
182
187
 
188
+ if (sectionModules['data-request'] && sectionModules['data-request'].loadData) {
189
+ sectionModules['data-request'].loadData(account);
190
+ }
191
+
183
192
  if (sectionModules.connections.loadData) {
184
193
  sectionModules.connections.loadData(account, appData);
185
194
  }
@@ -219,10 +228,13 @@ function setupNavigation() {
219
228
  function handleHashChange() {
220
229
  const hash = window.location.hash.slice(1);
221
230
 
222
- // Show delete nav item and mobile option if hash is delete
231
+ // Show hidden nav items based on hash
223
232
  if (hash === 'delete') {
224
233
  showDeleteOption();
225
234
  }
235
+ if (hash === 'data-request') {
236
+ showDataRequestOption();
237
+ }
226
238
 
227
239
  if (hash) {
228
240
  // Check if the section exists
@@ -264,6 +276,27 @@ function showDeleteOption() {
264
276
  }
265
277
  }
266
278
 
279
+ // Show data request option in navigation
280
+ function showDataRequestOption() {
281
+ // Show desktop nav item
282
+ const $dataRequestNavItem = document.getElementById('data-request-nav-item');
283
+ if ($dataRequestNavItem) {
284
+ $dataRequestNavItem.classList.remove('d-none');
285
+ }
286
+
287
+ // Add mobile dropdown option if not exists
288
+ const $mobileNavSelect = document.getElementById('mobile-nav-select');
289
+ if ($mobileNavSelect) {
290
+ const dataRequestOption = $mobileNavSelect.querySelector('option[value="data-request"]');
291
+ if (!dataRequestOption) {
292
+ const option = document.createElement('option');
293
+ option.value = 'data-request';
294
+ option.textContent = 'Data Request';
295
+ $mobileNavSelect.appendChild(option);
296
+ }
297
+ }
298
+ }
299
+
267
300
  // Hide all sections except loading
268
301
  function hideAllSectionsExceptLoading() {
269
302
  $sections.forEach(section => {
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Data Request Section JavaScript
3
+ */
4
+
5
+ // Libraries
6
+ import { FormManager } from '__main_assets__/js/libs/form-manager.js';
7
+ import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
8
+
9
+ let webManager = null;
10
+ let formManager = null;
11
+ let downloadFormManager = null;
12
+ let cancelFormManager = null;
13
+
14
+ // Initialize data-request section
15
+ export async function init(wm) {
16
+ webManager = wm;
17
+ setupDataRequestForm();
18
+ setupDownloadButton();
19
+ setupCancelButton();
20
+ }
21
+
22
+ // Setup data request form
23
+ function setupDataRequestForm() {
24
+ const $form = document.getElementById('data-request-form');
25
+ const $checkbox1 = document.getElementById('data-request-confirm-checkbox');
26
+ const $checkbox2 = document.getElementById('data-request-deletion-checkbox');
27
+ const $submitBtn = document.getElementById('data-request-submit-btn');
28
+
29
+ if (!$form || !$checkbox1 || !$checkbox2 || !$submitBtn) {
30
+ return;
31
+ }
32
+
33
+ formManager = new FormManager('#data-request-form', {
34
+ allowResubmit: false,
35
+ submittingText: 'Submitting request...',
36
+ submittedText: 'Request Submitted',
37
+ });
38
+
39
+ // Enable/disable submit button based on both checkboxes
40
+ function updateSubmitState() {
41
+ $submitBtn.disabled = !($checkbox1.checked && $checkbox2.checked);
42
+ }
43
+
44
+ $checkbox1.addEventListener('change', updateSubmitState);
45
+ $checkbox2.addEventListener('change', updateSubmitState);
46
+
47
+ formManager.on('submit', async ({ data }) => {
48
+ if (!$checkbox1.checked || !$checkbox2.checked) {
49
+ throw new Error('Please confirm all acknowledgments before submitting.');
50
+ }
51
+
52
+ trackDataRequest('submit');
53
+
54
+ const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/user/data-request`, {
55
+ method: 'POST',
56
+ timeout: 30000,
57
+ response: 'json',
58
+ tries: 2,
59
+ body: {
60
+ confirmed: true,
61
+ reason: data.reason || '',
62
+ },
63
+ });
64
+
65
+ if (response.error) {
66
+ throw new Error(response.message || 'Failed to submit data request. Please try again later.');
67
+ }
68
+
69
+ formManager.showSuccess('Your data request has been submitted. Please check back in up to 14 business days.');
70
+
71
+ showRequestStatus(response.request);
72
+ });
73
+ }
74
+
75
+ // Setup download form
76
+ function setupDownloadButton() {
77
+ const $form = document.getElementById('data-request-download-form');
78
+
79
+ if (!$form) {
80
+ return;
81
+ }
82
+
83
+ downloadFormManager = new FormManager('#data-request-download-form', {
84
+ allowResubmit: false,
85
+ submittingText: 'Downloading...',
86
+ submittedText: 'Downloaded!',
87
+ });
88
+
89
+ downloadFormManager.on('submit', async () => {
90
+ const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/user/data-request?action=download`, {
91
+ method: 'GET',
92
+ timeout: 60000,
93
+ response: 'json',
94
+ });
95
+
96
+ if (response.error || !response.data) {
97
+ throw new Error(response.message || 'Failed to download data.');
98
+ }
99
+
100
+ trackDataRequest('download');
101
+
102
+ // Trigger file download
103
+ const blob = new Blob([JSON.stringify(response.data, null, 2)], { type: 'application/json' });
104
+ const url = URL.createObjectURL(blob);
105
+ const $a = document.createElement('a');
106
+ $a.href = url;
107
+ $a.download = `my-data-${new Date().toISOString().split('T')[0]}.json`;
108
+ document.body.appendChild($a);
109
+ $a.click();
110
+ document.body.removeChild($a);
111
+ URL.revokeObjectURL(url);
112
+ });
113
+ }
114
+
115
+ // Setup cancel form
116
+ function setupCancelButton() {
117
+ const $form = document.getElementById('data-request-cancel-form');
118
+
119
+ if (!$form) {
120
+ return;
121
+ }
122
+
123
+ cancelFormManager = new FormManager('#data-request-cancel-form', {
124
+ allowResubmit: true,
125
+ submittingText: 'Withdrawing...',
126
+ });
127
+
128
+ cancelFormManager.on('submit', async () => {
129
+ const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/user/data-request`, {
130
+ method: 'DELETE',
131
+ timeout: 30000,
132
+ response: 'json',
133
+ });
134
+
135
+ if (response.error) {
136
+ throw new Error(response.message || 'Failed to withdraw request.');
137
+ }
138
+
139
+ trackDataRequest('cancel');
140
+
141
+ showRequestForm();
142
+ });
143
+ }
144
+
145
+ // Load data (called when auth state resolves)
146
+ export async function loadData() {
147
+ // Status is checked lazily in onShow
148
+ }
149
+
150
+ // Called when section is shown
151
+ export async function onShow() {
152
+ await checkRequestStatus();
153
+ }
154
+
155
+ // Check request status from backend
156
+ async function checkRequestStatus() {
157
+ try {
158
+ const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/user/data-request`, {
159
+ method: 'GET',
160
+ timeout: 30000,
161
+ response: 'json',
162
+ });
163
+
164
+ if (response.request) {
165
+ showRequestStatus(response.request);
166
+ } else {
167
+ showRequestForm();
168
+ }
169
+ } catch (error) {
170
+ // No active request or error — show form
171
+ showRequestForm();
172
+ }
173
+ }
174
+
175
+ // Show request status UI
176
+ function showRequestStatus(request) {
177
+ const $status = document.getElementById('data-request-status');
178
+ const $statusTitle = document.getElementById('data-request-status-title');
179
+ const $statusMessage = document.getElementById('data-request-status-message');
180
+ const $download = document.getElementById('data-request-download');
181
+ const $cancel = document.getElementById('data-request-cancel');
182
+ const $formContainer = document.getElementById('data-request-form-container');
183
+ const $formTrigger = document.getElementById('data-request-form-trigger');
184
+
185
+ if (!$status) {
186
+ return;
187
+ }
188
+
189
+ if (request.status === 'expired') {
190
+ showRequestForm();
191
+ return;
192
+ }
193
+
194
+ $status.classList.remove('d-none');
195
+ $formContainer.classList.add('d-none');
196
+ if ($formTrigger) {
197
+ $formTrigger.classList.add('d-none');
198
+ }
199
+
200
+ const createdDate = new Date(request.createdAt).toLocaleDateString();
201
+
202
+ if (request.status === 'complete') {
203
+ $statusTitle.textContent = 'Your data is ready';
204
+ $statusMessage.textContent = `Your data request from ${createdDate} has been processed. Click below to download your data package. This download will expire 30 days after your data became available.`;
205
+ $download.classList.remove('d-none');
206
+ if (downloadFormManager) {
207
+ downloadFormManager.reset();
208
+ }
209
+ $cancel.classList.add('d-none');
210
+
211
+ trackDataRequest('download_available');
212
+ } else {
213
+ $statusTitle.textContent = 'Request pending';
214
+ $statusMessage.textContent = `Your data request was submitted on ${createdDate}. Processing may take up to 14 business days. Please check back later.`;
215
+ $download.classList.add('d-none');
216
+ $cancel.classList.remove('d-none');
217
+ if (cancelFormManager) {
218
+ cancelFormManager.reset();
219
+ }
220
+ }
221
+ }
222
+
223
+ // Show request form UI
224
+ function showRequestForm() {
225
+ const $status = document.getElementById('data-request-status');
226
+ const $formContainer = document.getElementById('data-request-form-container');
227
+ const $formTrigger = document.getElementById('data-request-form-trigger');
228
+
229
+ if (!$status || !$formContainer) {
230
+ return;
231
+ }
232
+
233
+ $status.classList.add('d-none');
234
+ $formContainer.classList.remove('d-none');
235
+ if ($formTrigger) {
236
+ $formTrigger.classList.remove('d-none');
237
+ }
238
+ }
239
+
240
+ // Tracking
241
+ function trackDataRequest(action) {
242
+ gtag('event', 'data_request', {
243
+ action: action,
244
+ });
245
+ fbq('trackCustom', 'DataRequest', {
246
+ action: action,
247
+ });
248
+ ttq.track('ViewContent', {
249
+ content_id: `data-request-${action}`,
250
+ content_type: 'product',
251
+ content_name: `Data Request ${action}`,
252
+ });
253
+ }
@@ -18,11 +18,11 @@ export async function init(wm) {
18
18
  // Setup delete account form
19
19
  function setupDeleteAccountForm() {
20
20
  const $form = document.getElementById('delete-account-form');
21
- const $checkbox = document.getElementById('delete-confirm-checkbox');
21
+ const $checkbox1 = document.getElementById('delete-confirm-checkbox');
22
+ const $checkbox2 = document.getElementById('delete-data-request-checkbox');
22
23
  const $deleteBtn = document.getElementById('delete-account-btn');
23
- const $cancelBtn = document.getElementById('cancel-delete-btn');
24
24
 
25
- if (!$form || !$checkbox || !$deleteBtn) {
25
+ if (!$form || !$checkbox1 || !$checkbox2 || !$deleteBtn) {
26
26
  return;
27
27
  }
28
28
 
@@ -32,23 +32,18 @@ function setupDeleteAccountForm() {
32
32
  submittedText: 'Account Deleted!',
33
33
  });
34
34
 
35
- // Enable/disable delete button based on checkbox
36
- $checkbox.addEventListener('change', (e) => {
37
- $deleteBtn.disabled = !e.target.checked;
38
- });
39
-
40
- // Handle cancel button
41
- if ($cancelBtn) {
42
- $cancelBtn.addEventListener('click', () => {
43
- // Navigate back to profile
44
- window.location.hash = 'profile';
45
- });
35
+ // Enable/disable delete button based on both checkboxes
36
+ function updateDeleteState() {
37
+ $deleteBtn.disabled = !($checkbox1.checked && $checkbox2.checked);
46
38
  }
47
39
 
40
+ $checkbox1.addEventListener('change', updateDeleteState);
41
+ $checkbox2.addEventListener('change', updateDeleteState);
42
+
48
43
  formManager.on('submit', async ({ data }) => {
49
- // Check if checkbox is checked
50
- if (!$checkbox.checked) {
51
- throw new Error('Please confirm that you understand this action is permanent.');
44
+ // Check if both checkboxes are checked
45
+ if (!$checkbox1.checked || !$checkbox2.checked) {
46
+ throw new Error('Please confirm all acknowledgments before proceeding.');
52
47
  }
53
48
 
54
49
  // 1ms wait for dialog to appear properly
@@ -73,6 +73,18 @@ async function initializeCheckout() {
73
73
  initializeRecaptcha(webManager.config?.recaptcha?.['site-key'], webManager),
74
74
  ]);
75
75
 
76
+ /* @dev-only:start */
77
+ {
78
+ const _dev_preDelay = urlParams.get('_dev_preDelay');
79
+ if (_dev_preDelay) {
80
+ const delayMs = parseInt(_dev_preDelay, 10) || 5000;
81
+ console.warn(`[Checkout Dev] Artificial pre-delay: ${delayMs}ms`);
82
+ await new Promise(resolve => setTimeout(resolve, delayMs));
83
+ console.warn('[Checkout Dev] Pre-delay complete');
84
+ }
85
+ }
86
+ /* @dev-only:end */
87
+
76
88
  // App config is required
77
89
  if (appConfigResult.status === 'rejected') {
78
90
  const reason = appConfigResult.reason?.message || appConfigResult.reason || 'Unknown error';
package/dist/build.js CHANGED
@@ -98,6 +98,12 @@ Manager.isBuildMode = function () {
98
98
  }
99
99
  Manager.prototype.isBuildMode = Manager.isBuildMode;
100
100
 
101
+ // isQuickMode
102
+ Manager.isQuickMode = function () {
103
+ return process.env.UJ_QUICK === 'true';
104
+ }
105
+ Manager.prototype.isQuickMode = Manager.isQuickMode;
106
+
101
107
  // actLikeProduction - determines if we should act like production mode
102
108
  Manager.actLikeProduction = function () {
103
109
  return Boolean(Manager.isBuildMode() || process.env.UJ_AUDIT_FORCE === 'true');
@@ -18,6 +18,16 @@ const dirs = [
18
18
  module.exports = async function (options) {
19
19
  options = options || {};
20
20
 
21
+ // Quick mode: skip clean to reuse existing build artifacts
22
+ if (Manager.isQuickMode()) {
23
+ if (!jetpack.exists('dist') || !jetpack.exists('_site')) {
24
+ logger.log('Quick mode: No existing build, running full clean');
25
+ } else {
26
+ logger.log('Quick mode: Skipping clean');
27
+ return;
28
+ }
29
+ }
30
+
21
31
  // Build list of directories to clean
22
32
  const dirsToClean = [...dirs];
23
33
 
@@ -41,6 +41,19 @@ module.exports = async function (options) {
41
41
  options.deduplicatePosts = options.deduplicatePosts !== 'false';
42
42
  options.migrate = options.migrate !== 'false';
43
43
 
44
+ // Quick mode: skip slow/network operations
45
+ if (Manager.isQuickMode()) {
46
+ logger.log('Quick mode: Skipping slow setup operations');
47
+ options.checkManager = false;
48
+ options.checkNode = false;
49
+ options.checkRuby = false;
50
+ options.checkBundle = false;
51
+ options.checkPeerDependencies = false;
52
+ options.fetchFirebaseAuth = false;
53
+ options.publishGitHubToken = false;
54
+ options.deduplicatePosts = false;
55
+ }
56
+
44
57
  // Log
45
58
  logger.log(`Welcome to ${package.name} v${package.version}!`);
46
59
  logger.log(`options`, options);
@@ -101,10 +101,8 @@
101
101
  && target.closest('button, input[type="submit"], input[type="button"], input[type="reset"], .btn-action')
102
102
  )
103
103
  ) {
104
- // Log disabled click attempt in development
105
- if (window.location.hostname === 'localhost' || window.location.hostname.startsWith('192.168')) {
106
- console.log('Click prevented (disabled):', target);
107
- }
104
+ // Log the click for debugging
105
+ console.log('Click prevented (disabled):', target);
108
106
 
109
107
  // Prevent all actions
110
108
  e.preventDefault();
@@ -70,6 +70,7 @@ In the event of a change of control, if we sell or otherwise transfer part or th
70
70
 
71
71
  ### Your Choices About Your Information:
72
72
  Your account information and profile privacy settings can be updated or changed by visiting your account profile at [{{ site.url }}/account]({{ site.url }}/account) or by contacting {{ brand }} directly at [{{ site.url }}/contact]({{ site.url }}/contact).
73
+ - You may request a copy of your personal data by visiting [{{ site.url }}/account]({{ site.url }}/account#data-request).
73
74
  - You may request to have your account deleted by visiting your account profile at [{{ site.url }}/account]({{ site.url }}/account#delete).
74
75
  - You may request to unsubscribe from emails by clicking the "unsubscribe" link inside the email.
75
76
 
@@ -80,7 +81,27 @@ When it comes to the collection of personal information from children under 13,
80
81
  By opting in to receive SMS communications from {{ brand }}, you agree to receive marketing text messages, such as promotions and cart reminders, from us. Consent to receive marketing text messages is not a condition of any purchase. Message and data rates may apply, and the frequency of messages may vary. You may unsubscribe from receiving SMS messages at any time by replying “STOP” to any message or by clicking the unsubscribe link provided in our communications. For more information about our privacy practices, please refer to this Privacy Policy or our [Terms of Service]({{ site.url }}/terms/).
81
82
 
82
83
  ## Request Your Data to Be Removed or Deleted
83
- If you would like your personally identifiable information to be deleted from our database, you can request deletion here: [{{ site.url }}/account]({{ site.url }}/account#delete).
84
+ Under applicable data protection regulations including the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA), you have the right to request the deletion of the personal data we hold about you. Account deletion is a permanent, irreversible action that cannot be undone under any circumstances.
85
+
86
+ To request deletion, visit your account page here: [{{ site.url }}/account]({{ site.url }}/account#delete). Please read the following important information before proceeding:
87
+
88
+ * **Permanent and irreversible:** Once your account is deleted, all personal data associated with your account will be permanently removed from our systems. This includes your profile information, subscription and billing history, activity logs, API keys, OAuth2 connections, referral data, and all other stored information. There is no mechanism to recover any data after deletion.
89
+ * **Active subscriptions:** You must cancel any active paid subscriptions before deleting your account. Accounts with active or suspended paid subscriptions cannot be deleted until the subscription is cancelled or expires.
90
+ * **Pending data requests:** If you have a pending data request (Subject Access Request), it will be permanently cancelled upon account deletion. We will not be able to fulfill data requests after your account has been deleted. If you wish to obtain a copy of your data, you must complete the data request and download process **before** initiating account deletion.
91
+ * **Immediate effect:** Account deletion takes effect immediately. You will be signed out of all active sessions and will no longer be able to access any services associated with your account.
92
+ * **Third-party services:** While we will remove your data from our systems, we cannot guarantee the removal of data that has already been shared with or collected by third-party services you may have used in connection with your account. Please refer to the privacy policies of those services for information about their data retention practices.
93
+
94
+ ## Request a Copy of Your Data
95
+ Under applicable data protection regulations including the General Data Protection Regulation (GDPR), the California Consumer Privacy Act (CCPA), and other similar legislative frameworks, you may have the right to request a copy of the personal data we hold about you. This is commonly referred to as a Subject Access Request (SAR) or a Data Portability Request.
96
+
97
+ To submit a data request, visit your account page here: [{{ site.url }}/account]({{ site.url }}/account#data-request). Please read the following important information before submitting your request:
98
+
99
+ * **Processing time:** Data requests may take up to **14 business days** to process. The compilation and verification of your data is performed manually by our data processing team to ensure accuracy, completeness, and security.
100
+ * **Download only — no email delivery:** For security purposes, your data will **not** be sent via email or any other communication channel. You must return to your account page to download your data once it is ready. We do not consider email to be a sufficiently secure medium for the transmission of personally identifiable information.
101
+ * **Data unavailable after account deletion:** If you delete your account before downloading your data, all personal information will be permanently and irreversibly removed from our systems. We will not be able to fulfill any pending data requests after account deletion. If you wish to both request your data and delete your account, you must download your data **before** initiating account deletion.
102
+ * **Download expiration:** Completed data requests will be available for download for 30 days after processing is complete. After this period, the request will expire and you will need to submit a new request.
103
+ * **Request limits:** You are limited to one active data request at a time. After a completed request, there is a 30-day cooldown period before a new request can be submitted.
104
+ * **Data format:** Your data will be provided as a downloadable JSON file containing your account information, authentication records, subscription history, activity data, and other personally identifiable information associated with your account.
84
105
 
85
106
  ## Acceptance of This Policy
86
107
  Use of our site signifies your acceptance of this policy. If you do not accept the policy then please do not use this site. When registering, we will further request your explicit acceptance of the privacy policy.
@@ -48,6 +48,9 @@ sections:
48
48
  - id: "api-keys"
49
49
  name: "API Keys"
50
50
  icon: "key"
51
+ - id: "data-request"
52
+ name: "Data Request"
53
+ icon: "file-export"
51
54
  - id: "delete"
52
55
  name: "Delete Account"
53
56
  icon: "trash"
@@ -133,7 +136,7 @@ badges:
133
136
  <div class="d-flex align-items-center justify-content-between py-3">
134
137
  <!-- Left: Brand -->
135
138
  <a href="/" class="d-flex align-items-center text-decoration-none text-body">
136
- <span class="avatar avatar-md filter-adaptive me-3">
139
+ <span class="avatar avatar-md me-3">
137
140
  <img src="{{ site.brand.images.brandmark }}?cb={{ site.uj.cache_breaker }}" alt="{{ site.brand.name }}"/>
138
141
  </span>
139
142
  <h1 class="h5 mb-0 fw-semibold">{{ site.brand.name }} Account</h1>
@@ -153,13 +156,13 @@ badges:
153
156
  <div class="d-flex gap-2">
154
157
  <select class="form-select flex-grow-1" id="mobile-nav-select" aria-label="Select account section">
155
158
  {% for section in page.resolved.sections %}
156
- {% unless section.id == 'delete' %}
159
+ {% unless section.id == 'delete' or section.id == 'data-request' %}
157
160
  <option value="{{ section.id }}" {% if forloop.first %}selected{% endif %}>
158
161
  {{ section.name }}
159
162
  </option>
160
163
  {% endunless %}
161
164
  {% endfor %}
162
- <!-- Delete option will be added dynamically by JavaScript when needed -->
165
+ <!-- Delete and Data Request options will be added dynamically by JavaScript when needed -->
163
166
  </select>
164
167
  <button class="btn btn-danger auth-signout-btn">
165
168
  {% uj_icon "arrow-right-from-bracket", "fa-sm" %}
@@ -181,7 +184,7 @@ badges:
181
184
  <div class="card-body p-3">
182
185
  <ul class="nav nav-pills flex-column" id="account-nav" role="tablist" aria-label="Account sections">
183
186
  {% for section in page.resolved.sections %}
184
- <li class="nav-item {% if section.id == 'delete' %}d-none{% endif %}" id="{{ section.id }}-nav-item">
187
+ <li class="nav-item {% if section.id == 'delete' or section.id == 'data-request' %}d-none{% endif %}" id="{{ section.id }}-nav-item">
185
188
  <a class="nav-link d-flex align-items-center {% if forloop.first %}active{% endif %}"
186
189
  href="#{{ section.id }}"
187
190
  data-section="{{ section.id }}"
@@ -222,7 +225,7 @@ badges:
222
225
  <section id="profile-section" class="account-section d-none">
223
226
  <h2 class="h3 mb-4" >My profile</h2>
224
227
 
225
- <form id="profile-form" novalidate>
228
+ <form id="profile-form" novalidate onsubmit="return false">
226
229
  <!-- Avatar and Edit Button -->
227
230
  <div class="mb-4">
228
231
  <div class="d-flex align-items-center">
@@ -817,7 +820,7 @@ badges:
817
820
  </div>
818
821
  </div>
819
822
  <div class="flex-shrink-0">
820
- <form id="signin-method-{{ method.id }}-form" class="d-grid d-sm-inline-block" novalidate>
823
+ <form id="signin-method-{{ method.id }}-form" class="d-grid d-sm-inline-block" novalidate onsubmit="return false">
821
824
  <input type="hidden" name="method" value="{{ method.id }}">
822
825
  {% if method.id == "password" %}
823
826
  <button type="submit" class="btn btn-primary btn-sm" data-action="change">
@@ -859,7 +862,7 @@ badges:
859
862
  <div class="card-body">
860
863
  <div class="d-flex justify-content-between align-items-center mb-3">
861
864
  <h5 class="card-title mb-0">Active sessions</h5>
862
- <form id="signout-all-sessions-form" class="d-inline" novalidate>
865
+ <form id="signout-all-sessions-form" class="d-inline" novalidate onsubmit="return false">
863
866
  <input type="hidden" name="action" value="signout-all">
864
867
  <button type="submit" class="btn btn-outline-danger btn-sm">
865
868
  {% uj_icon "right-from-bracket", "me-1" %}
@@ -919,7 +922,7 @@ badges:
919
922
  </div>
920
923
  <div class="text-start text-sm-end flex-shrink-0">
921
924
  <div>
922
- <form id="connection-form-{{ connection.id }}" class="d-grid d-sm-inline-block" novalidate>
925
+ <form id="connection-form-{{ connection.id }}" class="d-grid d-sm-inline-block" novalidate onsubmit="return false">
923
926
  <input type="hidden" name="provider" value="{{ connection.id }}">
924
927
  <!-- Connect button -->
925
928
  <button type="submit" class="btn btn-sm btn-primary" data-action="connect">
@@ -1090,7 +1093,7 @@ badges:
1090
1093
  <div class="card-body">
1091
1094
  <div class="d-flex justify-content-between align-items-center mb-3">
1092
1095
  <h5 class="card-title mb-0">Your API key</h5>
1093
- <form id="reset-api-key-form" class="d-inline" novalidate>
1096
+ <form id="reset-api-key-form" class="d-inline" novalidate onsubmit="return false">
1094
1097
  <button type="submit" class="btn btn-outline-danger btn-sm" id="reset-api-key-btn">
1095
1098
  {% uj_icon "rotate", "me-1" %}
1096
1099
  <span class="button-text">
@@ -1126,54 +1129,228 @@ badges:
1126
1129
  </div>
1127
1130
  </section>
1128
1131
 
1132
+ <!-- Data Request Section (Hidden by default) -->
1133
+ <section id="data-request-section" class="account-section d-none">
1134
+ <h2 class="h3 mb-4">Request your data</h2>
1135
+
1136
+ <!-- Info card (always shown) -->
1137
+ <div class="card mb-4">
1138
+ <div class="card-body">
1139
+ <h5 class="card-title text-primary">
1140
+ {% uj_icon "file-shield", "fa-lg me-2" %}
1141
+ About Data Subject Access Requests
1142
+ </h5>
1143
+ <p class="card-text small text-muted">
1144
+ Under applicable data protection regulations including the General Data Protection Regulation (GDPR), the California Consumer Privacy Act (CCPA), and other similar legislative frameworks enacted by governmental bodies around the world, you may have the right to request a copy of the personal data we hold about you. This process is commonly referred to as a Subject Access Request (SAR) or a Data Portability Request. It is important that you understand the full scope, limitations, and procedural requirements of this request before proceeding. Please read the following information carefully and in its entirety before submitting a request.
1145
+ </p>
1146
+ <p class="card-text small text-muted">
1147
+ When you submit a data request, our data processing team will begin the process of compiling the personal information associated with your account. This may include, but is not limited to: your name, email address, account creation date, account activity logs, subscription and billing history (including payment processor identifiers and transaction records), geolocation data collected during your use of our services (such as IP address, approximate location derived from IP, continent, country, region, and city), device and browser information (including user agent strings, platform identifiers, and viewport characteristics), API usage statistics and rate limiting data, referral and affiliate information, OAuth2 connection records, and any other data points stored in connection with your account in our systems.
1148
+ </p>
1149
+ <p class="card-text small text-muted">
1150
+ The compilation and verification process is performed by our data processing team to ensure the accuracy, completeness, and security of the information provided to you. Due to the manual verification steps involved in this process and the need to ensure that data is being released to the correct account holder, this process may take up to <strong>14 business days</strong> from the date of your request submission to complete. We appreciate your patience during this time and ask that you do not submit duplicate requests, as this will not expedite the process and may in fact delay it.
1151
+ </p>
1152
+ <p class="card-text small text-muted">
1153
+ Once your data has been compiled, verified, and is ready for retrieval, you will need to <strong>return to this page</strong> to download it. For security purposes, we do not transmit personal data via email, SMS, or any other communication channel, as these methods are not considered sufficiently secure mediums for the transmission of personally identifiable information. The data will be made available exclusively through this authenticated account interface to ensure that only the verified account holder can access it. Your data package will be provided as a JSON file containing all of the aforementioned data categories.
1154
+ </p>
1155
+ <p class="card-text small text-muted">
1156
+ <strong>Important notice regarding account deletion:</strong> If you choose to delete your account at any point, all personal data associated with your account will be permanently and irreversibly removed from our systems in accordance with our data retention policy. Once your account has been deleted, we will no longer be able to process any pending data requests, nor will we be able to provide you with a copy of your data retroactively. There is no mechanism to recover data after account deletion. If you are considering both requesting your data and deleting your account, you must complete the data request process in full and download your data package <strong>before</strong> initiating the account deletion process. We strongly recommend downloading and verifying your data before taking any irreversible actions with your account.
1157
+ </p>
1158
+ <p class="card-text small text-muted">
1159
+ You are limited to one active data request at a time. If you have previously submitted a request that is still being processed, you will not be able to submit a new request until the current one has been completed or has expired. Additionally, after a completed data request, there is a 30-day cooldown period before a new request can be submitted. These limitations exist to prevent abuse of the system and to ensure that our data processing team can handle all requests in a timely and thorough manner. Excessive or abusive use of this feature may result in additional restrictions being applied to your account in accordance with our Terms of Service.
1160
+ </p>
1161
+
1162
+ <hr>
1163
+
1164
+ <h6 class="text-muted">Summary of key points:</h6>
1165
+ <ul class="small text-muted">
1166
+ <li>Processing time: up to <strong>14 business days</strong></li>
1167
+ <li>You must return to this page to download your data</li>
1168
+ <li>Data is <strong>not</strong> sent via email for security reasons</li>
1169
+ <li>Data is permanently unavailable after account deletion</li>
1170
+ <li>Only one active request at a time</li>
1171
+ <li>30-day cooldown between completed requests</li>
1172
+ <li>Data is provided as a downloadable JSON file</li>
1173
+ </ul>
1174
+
1175
+ <p id="data-request-form-trigger" class="card-text small text-muted mb-0">
1176
+ If you have read and understood all of the information above and still wish to proceed with your data subject access request, you may proceed to <button type="button" class="accordion-trigger small text-muted text-decoration-underline" data-bs-toggle="collapse" data-bs-target="#data-request-form-accordion" aria-expanded="false" aria-controls="data-request-form-accordion">submit your request &rarr;</button>.
1177
+ </p>
1178
+ </div>
1179
+ </div>
1180
+
1181
+ <!-- Status area (shown when a request exists) -->
1182
+ <div id="data-request-status" class="d-none mb-4">
1183
+ <div class="card">
1184
+ <div class="card-body">
1185
+ <h5 class="card-title">
1186
+ {% uj_icon "clock", "fa-lg me-2" %}
1187
+ <span id="data-request-status-title">Request Status</span>
1188
+ </h5>
1189
+ <p id="data-request-status-message" class="card-text text-muted"></p>
1190
+ <div id="data-request-download" class="d-none">
1191
+ <form id="data-request-download-form" onsubmit="return false">
1192
+ <button type="submit" class="btn btn-primary btn-sm" id="data-request-download-btn">
1193
+ {% uj_icon "download", "fa-sm me-2" %}
1194
+ <span class="button-text">Download your data</span>
1195
+ </button>
1196
+ </form>
1197
+ </div>
1198
+ <div id="data-request-cancel" class="d-none">
1199
+ <form id="data-request-cancel-form" onsubmit="return false">
1200
+ <button type="submit" class="btn btn-primary btn-sm" id="data-request-cancel-btn">
1201
+ {% uj_icon "rotate-left", "fa-sm me-2" %}
1202
+ <span class="button-text">Request withdrawal</span>
1203
+ </button>
1204
+ </form>
1205
+ </div>
1206
+ </div>
1207
+ </div>
1208
+ </div>
1209
+
1210
+ <!-- Request form (hidden when a request is pending) -->
1211
+ <div id="data-request-form-container">
1212
+ <div class="collapse" id="data-request-form-accordion">
1213
+ <div class="card card-form-border border-primary mt-4">
1214
+ <div class="card-body">
1215
+ <h5 class="card-title text-primary">Confirm data request</h5>
1216
+
1217
+ <form id="data-request-form" novalidate onsubmit="return false">
1218
+ <div class="mb-3">
1219
+ <div class="form-check">
1220
+ <input class="form-check-input" type="checkbox" id="data-request-confirm-checkbox" name="confirm_processing_time" required>
1221
+ <label class="form-check-label small" for="data-request-confirm-checkbox">
1222
+ I understand that this request may take up to 14 business days to process and that I must return to this page to download my data. I acknowledge that data will not be sent to me via email or any other channel.
1223
+ </label>
1224
+ </div>
1225
+ </div>
1226
+
1227
+ <div class="mb-3">
1228
+ <div class="form-check">
1229
+ <input class="form-check-input" type="checkbox" id="data-request-deletion-checkbox" name="confirm_deletion_warning" required>
1230
+ <label class="form-check-label small" for="data-request-deletion-checkbox">
1231
+ I understand that if I delete my account before downloading my data, the data will be permanently lost and cannot be recovered under any circumstances.
1232
+ </label>
1233
+ </div>
1234
+ </div>
1235
+
1236
+ <div class="mb-3">
1237
+ <label for="data-request-reason" class="form-label">Reason for request</label>
1238
+ <textarea class="form-control" id="data-request-reason" name="reason" rows="3" placeholder="Help us understand why you're requesting your data..."></textarea>
1239
+ </div>
1240
+
1241
+ <div class="row g-2">
1242
+ <div class="col-12 col-md-6 order-md-2">
1243
+ <button type="submit" class="btn btn-outline-adaptive w-100" id="data-request-submit-btn" disabled>
1244
+ {% uj_icon "file-export", "fa-sm me-2" %}
1245
+ <span class="button-text">Submit Data Request</span>
1246
+ </button>
1247
+ </div>
1248
+ <div class="col-12 col-md-6 order-md-1">
1249
+ <a href="/account" class="btn btn-primary w-100">
1250
+ {% uj_icon "arrow-left", "fa-sm me-2" %}
1251
+ <span>Return to Profile</span>
1252
+ </a>
1253
+ </div>
1254
+ </div>
1255
+ </form>
1256
+ </div>
1257
+ </div>
1258
+ </div>
1259
+ </div>
1260
+ </section>
1261
+
1129
1262
  <!-- Delete Account Section (Hidden by default) -->
1130
1263
  <section id="delete-section" class="account-section d-none">
1131
- <h2 class="h3 mb-4 text-danger" >Delete account</h2>
1132
-
1133
- <div class="alert alert-danger">
1134
- <h5 class="alert-heading">
1135
- {% uj_icon "triangle-exclamation", "fa-lg" %}
1136
- Warning: This action cannot be undone
1137
- </h5>
1138
- <p>Deleting your account will permanently remove:</p>
1139
- <ul class="mb-2">
1140
- <li>All your personal information</li>
1141
- <li>Your subscription and billing history</li>
1142
- <li>Access to all services and data</li>
1143
- <li>Any stored content or settings</li>
1144
- </ul>
1145
- <p class="mb-0"><strong>This action is immediate and irreversible.</strong></p>
1146
- </div>
1264
+ <h2 class="h3 mb-4">Delete account</h2>
1147
1265
 
1148
- <div class="card border-danger">
1266
+ <div class="card mb-4">
1149
1267
  <div class="card-body">
1150
- <h5 class="card-title text-danger">Confirm account deletion</h5>
1151
-
1152
- <form id="delete-account-form" novalidate>
1153
- <div class="mb-3">
1154
- <div class="form-check">
1155
- <input class="form-check-input" type="checkbox" id="delete-confirm-checkbox" required>
1156
- <label class="form-check-label" for="delete-confirm-checkbox">
1157
- I understand that deleting my account is permanent and cannot be reversed. All my data will be lost forever.
1158
- </label>
1268
+ <h5 class="card-title text-danger">
1269
+ {% uj_icon "triangle-exclamation", "fa-lg me-2" %}
1270
+ About Account Deletion
1271
+ </h5>
1272
+ <p class="card-text small text-muted">
1273
+ Under applicable data protection regulations including the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA), you have the right to request the deletion of the personal data we hold about you. Account deletion is a permanent, irreversible action that cannot be undone under any circumstances. Please read the following important information carefully before proceeding.
1274
+ </p>
1275
+ <p class="card-text small text-muted">
1276
+ <strong>Permanent and irreversible:</strong> Once your account is deleted, all personal data associated with your account will be permanently removed from our systems. This includes, but is not limited to, your profile information, subscription and billing history, activity logs, API keys, OAuth2 connections, referral data, saved preferences, and all other stored information. There is no mechanism to recover any data after deletion. Our support team cannot restore deleted accounts or retrieve any associated data, regardless of the circumstances.
1277
+ </p>
1278
+ <p class="card-text small text-muted">
1279
+ <strong>Active subscriptions:</strong> You must cancel any active paid subscriptions before deleting your account. Accounts with active or suspended paid subscriptions cannot be deleted until the subscription is cancelled or expires. If you have a subscription that is currently in a billing cycle, you will need to wait for the cycle to complete or cancel the subscription first. No refunds will be issued for unused portions of cancelled subscriptions in connection with account deletion.
1280
+ </p>
1281
+ <p class="card-text small text-muted">
1282
+ <strong>Pending data requests:</strong> If you have a pending data request (Subject Access Request), it will be permanently cancelled upon account deletion. We will not be able to fulfill data requests after your account has been deleted. If you wish to obtain a copy of your data, you must complete the data request and download process <strong>before</strong> initiating account deletion. You can request a copy of your data from the <a href="#data-request">data request section</a>.
1283
+ </p>
1284
+ <p class="card-text small text-muted">
1285
+ <strong>Immediate effect:</strong> Account deletion takes effect immediately upon confirmation. You will be signed out of all active sessions across all devices and platforms, and will no longer be able to access any services, features, content, or data associated with your account. Any active API integrations or third-party connections will cease to function immediately.
1286
+ </p>
1287
+ <p class="card-text small text-muted">
1288
+ <strong>Third-party services:</strong> While we will remove your data from our systems, we cannot guarantee the removal of data that has already been shared with or collected by third-party services you may have used in connection with your account. This includes, but is not limited to, analytics providers, payment processors, authentication providers, and any external services you authorized. Please refer to the privacy policies of those services for information about their data retention and deletion practices.
1289
+ </p>
1290
+
1291
+ <hr>
1292
+
1293
+ <h6 class="text-muted">Summary of consequences:</h6>
1294
+ <ul class="small text-muted">
1295
+ <li>All personal information, account data, and stored content will be permanently erased</li>
1296
+ <li>Your subscription and complete billing history will be permanently removed</li>
1297
+ <li>All API keys, OAuth2 connections, and integrations will be revoked</li>
1298
+ <li>Access to all services, features, and platforms will be terminated immediately</li>
1299
+ <li>Any pending data requests will be cancelled and cannot be fulfilled</li>
1300
+ <li>This action cannot be reversed, appealed, or undone by our support team</li>
1301
+ </ul>
1302
+
1303
+ <p class="card-text small text-muted mb-0">
1304
+ If you have read and understood all of the information above and still wish to proceed with deleting your account, you may proceed to <button type="button" class="accordion-trigger small text-muted text-decoration-underline" data-bs-toggle="collapse" data-bs-target="#delete-form-accordion" aria-expanded="false" aria-controls="delete-form-accordion">confirm account deletion &rarr;</button>.
1305
+ </p>
1306
+ </div>
1307
+ </div>
1308
+
1309
+ <div class="collapse" id="delete-form-accordion">
1310
+ <div class="card card-form-border border-danger mt-4">
1311
+ <div class="card-body">
1312
+ <h5 class="card-title text-danger">Confirm account deletion</h5>
1313
+
1314
+ <form id="delete-account-form" novalidate onsubmit="return false">
1315
+ <div class="mb-3">
1316
+ <div class="form-check">
1317
+ <input class="form-check-input" type="checkbox" id="delete-confirm-checkbox" required>
1318
+ <label class="form-check-label" for="delete-confirm-checkbox">
1319
+ I understand that deleting my account is permanent and cannot be reversed. All my data, including personal information, subscription history, API keys, and stored content, will be permanently and irreversibly erased.
1320
+ </label>
1321
+ </div>
1159
1322
  </div>
1160
- </div>
1161
1323
 
1162
- <div class="mb-3">
1163
- <label for="delete-reason" class="form-label">Reason for leaving (optional)</label>
1164
- <textarea class="form-control" id="delete-reason" rows="3" placeholder="Help us improve by sharing why you're leaving..."></textarea>
1165
- </div>
1324
+ <div class="mb-3">
1325
+ <div class="form-check">
1326
+ <input class="form-check-input" type="checkbox" id="delete-data-request-checkbox" required>
1327
+ <label class="form-check-label" for="delete-data-request-checkbox">
1328
+ I acknowledge that any pending data requests will be cancelled and I will not be able to request or download a copy of my data after my account is deleted.
1329
+ </label>
1330
+ </div>
1331
+ </div>
1166
1332
 
1167
- <div class="d-grid gap-2">
1168
- <button type="submit" class="btn btn-danger" id="delete-account-btn" disabled>
1169
- {% uj_icon "trash", "fa-sm" %}
1170
- <span class="button-text">Delete My Account Permanently</span>
1171
- </button>
1172
- <button type="button" class="btn btn-outline-secondary" id="cancel-delete-btn">
1173
- Cancel
1174
- </button>
1175
- </div>
1176
- </form>
1333
+ <div class="mb-3">
1334
+ <label for="delete-reason" class="form-label">Reason for leaving</label>
1335
+ <textarea class="form-control" id="delete-reason" name="reason" rows="3" placeholder="Help us improve by sharing why you're leaving..."></textarea>
1336
+ </div>
1337
+
1338
+ <div class="row g-2">
1339
+ <div class="col-12 col-md-6 order-md-2">
1340
+ <button type="submit" class="btn btn-outline-adaptive w-100" id="delete-account-btn" disabled>
1341
+ {% uj_icon "trash", "fa-sm me-2" %}
1342
+ <span class="button-text">Delete My Account Permanently</span>
1343
+ </button>
1344
+ </div>
1345
+ <div class="col-12 col-md-6 order-md-1">
1346
+ <a href="/account" class="btn btn-danger w-100" id="cancel-delete-btn">
1347
+ {% uj_icon "arrow-left", "fa-sm me-2" %}
1348
+ <span>Return to Profile</span>
1349
+ </a>
1350
+ </div>
1351
+ </div>
1352
+ </form>
1353
+ </div>
1177
1354
  </div>
1178
1355
  </div>
1179
1356
  </section>
@@ -25,7 +25,7 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
25
25
  {% capture initializing_spinner %}<span class="spinner-border spinner-border-sm me-2 form-initializing-spinner"></span>{% endcapture %}
26
26
 
27
27
  <!-- Reset Form -->
28
- <form id="auth-form" autocomplete="on" novalidate>
28
+ <form id="auth-form" autocomplete="on" novalidate onsubmit="return false">
29
29
  <div class="mb-4 text-start">
30
30
  <label for="email" class="form-label fw-semibold">
31
31
  Email <span class="text-danger">*</span>
@@ -40,7 +40,7 @@ social_signin:
40
40
  {% capture initializing_spinner %}<span class="spinner-border spinner-border-sm me-2 form-initializing-spinner"></span>{% endcapture %}
41
41
 
42
42
  <!-- Sign In Form -->
43
- <form id="auth-form" autocomplete="on" novalidate>
43
+ <form id="auth-form" autocomplete="on" novalidate onsubmit="return false">
44
44
  <!-- Hidden default submit button for Enter key -->
45
45
  <button type="submit" class="d-none" data-provider="email" aria-hidden="true" tabindex="-1"></button>
46
46
 
@@ -40,7 +40,7 @@ social_signup:
40
40
  {% capture initializing_spinner %}<span class="spinner-border spinner-border-sm me-2 form-initializing-spinner"></span>{% endcapture %}
41
41
 
42
42
  <!-- Sign Up Form -->
43
- <form id="auth-form" autocomplete="on" novalidate>
43
+ <form id="auth-form" autocomplete="on" novalidate onsubmit="return false">
44
44
  <!-- Hidden default submit button for Enter key -->
45
45
  <button type="submit" class="d-none" data-provider="email" aria-hidden="true" tabindex="-1"></button>
46
46
 
@@ -340,7 +340,7 @@ newsletter_cta:
340
340
  {% endiftruthy %}
341
341
 
342
342
  <!-- Newsletter Form -->
343
- <form id="newsletter-form" class="newsletter-form">
343
+ <form id="newsletter-form" class="newsletter-form" onsubmit="return false">
344
344
  <div class="row g-3 justify-content-center">
345
345
  <div class="col-md-7">
346
346
  <input
@@ -178,7 +178,7 @@ faqs:
178
178
  {% endiftruthy %}
179
179
  </div>
180
180
 
181
- <form id="contact-form" autocomplete="on">
181
+ <form id="contact-form" autocomplete="on" onsubmit="return false">
182
182
  <div class="row g-3 mb-4">
183
183
  <div class="col-md-6">
184
184
  <label for="first_name" class="form-label fw-semibold">First Name <span class="text-danger">*</span></label>
@@ -368,7 +368,7 @@ cta:
368
368
  {% else %}
369
369
  {% if platform.id == "android" or platform.id == "ios" %}
370
370
  <div class="mb-4 text-start">
371
- <form id="mobile-email-form-{{ platform.id }}" class="mobile-email-form" data-platform="{{ platform.id }}">
371
+ <form id="mobile-email-form-{{ platform.id }}" class="mobile-email-form" data-platform="{{ platform.id }}" onsubmit="return false">
372
372
  <label for="email-{{ platform.id }}" class="form-label">Email Address</label>
373
373
  <div class="row g-3">
374
374
  <div class="col-12 col-md-8">
@@ -225,7 +225,7 @@ cta:
225
225
  <!-- Input Demo: Single input with button -->
226
226
  <div class="card bg-glassy border-0 {% unless is_side_layout %}mx-auto{% endunless %} {{ demo.options.class }}">
227
227
  <div class="card-body">
228
- <form id="hero-demo-form" class="d-flex flex-column {% unless is_side_layout %}flex-sm-row justify-content-center{% endunless %} gap-3" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %}>
228
+ <form id="hero-demo-form" class="d-flex flex-column {% unless is_side_layout %}flex-sm-row justify-content-center{% endunless %} gap-3" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %} onsubmit="return false">
229
229
  <input
230
230
  type="{{ demo.options.input_type | default: 'text' }}"
231
231
  name="{{ demo.options.name | default: 'input' }}"
@@ -249,7 +249,7 @@ cta:
249
249
  {% elsif demo.type == "form" %}
250
250
  <!-- Form Demo: Multiple fields with card styling -->
251
251
  <div class="{% unless is_side_layout %}mx-auto{% endunless %} {{ demo.options.class }}">
252
- <form id="hero-demo-form" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %}>
252
+ <form id="hero-demo-form" {% iftruthy demo.options.redirect %}data-redirect="{{ demo.options.redirect }}"{% endiftruthy %} onsubmit="return false">
253
253
  <div class="row g-3 {% unless is_side_layout %}justify-content-center{% endunless %}">
254
254
  {% assign autofocus_set = false %}
255
255
  {% for field in demo.options.fields %}
@@ -24,7 +24,8 @@ web_manager:
24
24
  <button type="submit" class="btn btn-adaptive btn-md d-flex align-items-center justify-content-center payment-button"
25
25
  data-payment-method="card"
26
26
  data-action="pay-card"
27
- data-wm-bind="@show checkout.paymentMethods.card">
27
+ data-wm-bind="@show checkout.paymentMethods.card"
28
+ hidden>
28
29
  {% uj_icon "credit-card", "me-2 fa-3xl" %}
29
30
  <span class="fw-semibold">Credit/Debit</span>
30
31
  </button>
@@ -33,7 +34,8 @@ web_manager:
33
34
  <button type="submit" class="btn btn-paypal btn-md d-flex align-items-center justify-content-center payment-button"
34
35
  data-payment-method="paypal"
35
36
  data-action="pay-paypal"
36
- data-wm-bind="@show checkout.paymentMethods.paypal">
37
+ data-wm-bind="@show checkout.paymentMethods.paypal"
38
+ hidden>
37
39
  <img src="https://www.paypalobjects.com/webstatic/mktg/Logo/pp-logo-200px.png"
38
40
  alt="PayPal"
39
41
  height="28"
@@ -152,7 +154,7 @@ web_manager:
152
154
  </div> -->
153
155
 
154
156
  <!-- Main Checkout Form -->
155
- <form id="checkout-form" autocomplete="on" novalidate>
157
+ <form id="checkout-form" autocomplete="on" novalidate onsubmit="return false">
156
158
  <!-- Content Row -->
157
159
  <div class="row d-lg-flex">
158
160
  <!-- Section 1: Billing Cycle & Customer Info -->
@@ -255,7 +255,7 @@ build_info:
255
255
  {% endiftruthy %}
256
256
  </div>
257
257
  <div class="col-lg-5">
258
- <form id="status-subscribe-form" class="d-flex flex-column flex-sm-row gap-2">
258
+ <form id="status-subscribe-form" class="d-flex flex-column flex-sm-row gap-2" onsubmit="return false">
259
259
  <input
260
260
  type="email"
261
261
  class="form-control flex-grow-1"
@@ -148,6 +148,15 @@ async function jekyll(complete) {
148
148
  launchBrowserSync();
149
149
  }
150
150
 
151
+ // Quick mode: trigger deferred asset compilation after first build
152
+ if (Manager.isQuickMode() && index === 0) {
153
+ logger.log('Quick mode: Triggering deferred asset compilation...');
154
+ Manager.triggerRebuild([
155
+ 'src/assets/js/main.js',
156
+ 'src/assets/css/main.scss',
157
+ ], logger);
158
+ }
159
+
151
160
  // Reload browser
152
161
  if (global.browserSync) {
153
162
  global.browserSync.reload();
@@ -77,6 +77,9 @@ const output = 'dist/assets/css';
77
77
  const delay = 250;
78
78
  const compiled = {};
79
79
 
80
+ // Flags
81
+ let deferred = false;
82
+
80
83
  // Configuration
81
84
  const MAIN_BUNDLE_PAGE_PARTIALS = false; // Set to true to merge pages into _page-specific.scss, false to compile separately
82
85
  // Enable PurgeCSS via environment variable or in production mode
@@ -86,6 +89,13 @@ const ujmConfig = Manager.getUJMConfig();
86
89
 
87
90
  // SASS Compilation Task
88
91
  function sass(complete) {
92
+ // Quick mode: skip initial compilation, use stale cached bundles
93
+ if (Manager.isQuickMode() && !deferred) {
94
+ deferred = true;
95
+ logger.log('Quick mode: Skipping initial SASS compilation (using cached bundles)');
96
+ return complete();
97
+ }
98
+
89
99
  // Log
90
100
  logger.log('Starting...');
91
101
  Manager.logMemory(logger, 'Start');
@@ -72,6 +72,9 @@ const copy = [
72
72
 
73
73
  const delay = 250;
74
74
 
75
+ // Flags
76
+ let deferred = false;
77
+
75
78
  // Bundle naming configuration
76
79
  const bundleNaming = {
77
80
  // Files that should have stable (non-hashed) names
@@ -275,6 +278,13 @@ function getSettings() {
275
278
 
276
279
  // Task
277
280
  function webpack(complete) {
281
+ // Quick mode: skip initial compilation, use stale cached bundles
282
+ if (Manager.isQuickMode() && !deferred) {
283
+ deferred = true;
284
+ logger.log('Quick mode: Skipping initial webpack compilation (using cached bundles)');
285
+ return complete();
286
+ }
287
+
278
288
  // Get settings (loads config at runtime)
279
289
  const settings = getSettings();
280
290
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "0.0.269",
3
+ "version": "0.0.271",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -101,7 +101,7 @@
101
101
  "sass": "^1.97.3",
102
102
  "spellchecker": "^3.7.1",
103
103
  "through2": "^4.0.2",
104
- "web-manager": "^4.1.14",
104
+ "web-manager": "^4.1.15",
105
105
  "webpack": "^5.105.2",
106
106
  "wonderful-fetch": "^1.3.4",
107
107
  "wonderful-version": "^1.3.2",