ultimate-jekyll-manager 1.6.5 → 1.6.6
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.
- package/dist/assets/js/pages/account/sections/notifications.js +88 -0
- package/dist/assets/js/pages/admin/calendar/calendar-events.js +91 -38
- package/dist/assets/js/pages/admin/calendar/calendar-renderer.js +47 -16
- package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +48 -44
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +30 -0
- package/dist/service-worker.js +21 -0
- package/package.json +2 -2
|
@@ -19,6 +19,7 @@ const TOGGLE_ID = 'marketing-emails';
|
|
|
19
19
|
const GRANT_DATE_ID = 'marketing-emails-grant-date';
|
|
20
20
|
|
|
21
21
|
let formManager = null;
|
|
22
|
+
let pushFormManager = null;
|
|
22
23
|
|
|
23
24
|
export function init() {
|
|
24
25
|
const $form = document.getElementById(FORM_ID);
|
|
@@ -90,4 +91,91 @@ export function loadData(account) {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
formManager.ready();
|
|
94
|
+
|
|
95
|
+
initPushNotifications();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updatePushUI() {
|
|
99
|
+
const $status = document.getElementById('push-notification-status');
|
|
100
|
+
const $tokenInput = document.getElementById('push-token-value');
|
|
101
|
+
|
|
102
|
+
if (!$status) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const notifications = webManager.notifications();
|
|
107
|
+
const stored = webManager.storage().get('notifications', {});
|
|
108
|
+
const permission = typeof Notification !== 'undefined' ? Notification.permission : 'default';
|
|
109
|
+
|
|
110
|
+
let state;
|
|
111
|
+
if (stored.subscribed && stored.token) {
|
|
112
|
+
state = 'subscribed';
|
|
113
|
+
$status.innerHTML = '<span class="badge bg-success">Subscribed</span>';
|
|
114
|
+
if ($tokenInput) { $tokenInput.value = stored.token; }
|
|
115
|
+
if (pushFormManager) { pushFormManager._setDisabled(true); }
|
|
116
|
+
} else if (!notifications.isSupported()) {
|
|
117
|
+
state = 'not-supported';
|
|
118
|
+
$status.innerHTML = '<span class="badge bg-warning">Not supported</span>';
|
|
119
|
+
if (pushFormManager) { pushFormManager._setDisabled(true); }
|
|
120
|
+
} else if (permission === 'denied') {
|
|
121
|
+
state = 'denied';
|
|
122
|
+
$status.innerHTML = '<span class="badge bg-danger">Denied</span>';
|
|
123
|
+
if (pushFormManager) { pushFormManager._setDisabled(true); }
|
|
124
|
+
} else {
|
|
125
|
+
state = 'not-subscribed';
|
|
126
|
+
$status.innerHTML = '<span class="badge bg-secondary">Not subscribed</span>';
|
|
127
|
+
if ($tokenInput) { $tokenInput.value = ''; }
|
|
128
|
+
if (pushFormManager) { pushFormManager.ready(); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('[Account:push] updatePushUI →', state, { storedSubscribed: stored.subscribed, storedToken: stored.token?.slice(-8), permission });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function initPushNotifications() {
|
|
135
|
+
const $status = document.getElementById('push-notification-status');
|
|
136
|
+
const $form = document.getElementById('push-subscribe-form');
|
|
137
|
+
const $copyBtn = document.getElementById('copy-push-token-btn');
|
|
138
|
+
|
|
139
|
+
if (!$status) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const notifications = webManager.notifications();
|
|
144
|
+
|
|
145
|
+
// Full sync: validates permission + token + Firestore, then updates localStorage
|
|
146
|
+
await notifications.syncSubscription();
|
|
147
|
+
|
|
148
|
+
// Create the form (always starts disabled), then let updatePushUI control its state
|
|
149
|
+
if (notifications.isSupported() && $form) {
|
|
150
|
+
$form.style.display = '';
|
|
151
|
+
|
|
152
|
+
pushFormManager = new FormManager('#push-subscribe-form', {
|
|
153
|
+
autoReady: false,
|
|
154
|
+
allowResubmit: true,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
pushFormManager.on('submit', async () => {
|
|
158
|
+
console.log('[Account:push] Subscribe button clicked');
|
|
159
|
+
await notifications.subscribe();
|
|
160
|
+
console.log('[Account:push] Subscribe complete — updating UI');
|
|
161
|
+
setTimeout(() => updatePushUI(), 0);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Now that the form exists, let updatePushUI set the correct state
|
|
166
|
+
updatePushUI();
|
|
167
|
+
|
|
168
|
+
if ($copyBtn) {
|
|
169
|
+
const $tokenInput = document.getElementById('push-token-value');
|
|
170
|
+
const originalHtml = $copyBtn.innerHTML;
|
|
171
|
+
$copyBtn.addEventListener('click', () => {
|
|
172
|
+
if (!$tokenInput?.value) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
navigator.clipboard.writeText($tokenInput.value).then(() => {
|
|
176
|
+
$copyBtn.textContent = 'Copied!';
|
|
177
|
+
setTimeout(() => { $copyBtn.innerHTML = originalHtml; }, 2000);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
93
181
|
}
|
|
@@ -32,7 +32,6 @@ export default class CalendarEvents {
|
|
|
32
32
|
this._initTypeToggle();
|
|
33
33
|
this._initRecurrenceToggle();
|
|
34
34
|
this._initSendNow();
|
|
35
|
-
this._initDeleteButton();
|
|
36
35
|
this._initCreateButton();
|
|
37
36
|
this._initPreview();
|
|
38
37
|
}
|
|
@@ -47,6 +46,7 @@ export default class CalendarEvents {
|
|
|
47
46
|
this._setType('email');
|
|
48
47
|
this._resetRecurrence();
|
|
49
48
|
document.getElementById('campaign-modal-title-text').textContent = 'Create Campaign';
|
|
49
|
+
document.getElementById('campaign-modal-id').classList.add('d-none');
|
|
50
50
|
document.getElementById('campaign-local-time-row').style.display = 'none';
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -86,15 +86,57 @@ export default class CalendarEvents {
|
|
|
86
86
|
allowResubmit: true,
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
this.formManager.on('
|
|
90
|
-
const
|
|
89
|
+
this.formManager.on('validation', ({ $submitButton }) => {
|
|
90
|
+
const action = $submitButton?.value || 'save';
|
|
91
|
+
if (action === 'delete' || action === 'test') {
|
|
92
|
+
this.formManager.clearFieldErrors();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.formManager.on('submit', async ({ data, $submitButton }) => {
|
|
97
|
+
const action = $submitButton?.value || 'save';
|
|
98
|
+
|
|
99
|
+
if (action === 'delete') {
|
|
100
|
+
if (!this.editingCampaignId) { return; }
|
|
101
|
+
if (!confirm('Delete this campaign? This cannot be undone.')) {
|
|
102
|
+
this.formManager.ready();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await this._deleteCampaign(this.editingCampaignId);
|
|
106
|
+
this._getEditorModal().hide();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (action === 'test') {
|
|
111
|
+
const payload = this._buildPayload(data);
|
|
112
|
+
payload.test = true;
|
|
113
|
+
payload.sendAt = 'now';
|
|
114
|
+
delete payload.recurrence;
|
|
91
115
|
|
|
116
|
+
if (this.editingCampaignId) {
|
|
117
|
+
payload.recurringId = this.editingCampaignId;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const url = `${webManager.getApiUrl()}/backend-manager/marketing/campaign`;
|
|
121
|
+
await authorizedFetch(url, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
timeout: 60000,
|
|
124
|
+
response: 'json',
|
|
125
|
+
tries: 1,
|
|
126
|
+
log: true,
|
|
127
|
+
body: payload,
|
|
128
|
+
});
|
|
129
|
+
this.formManager.showSuccess('Test sent');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Default: save
|
|
134
|
+
const payload = this._buildPayload(data);
|
|
92
135
|
if (this.editingCampaignId) {
|
|
93
136
|
await this._updateCampaign(this.editingCampaignId, payload);
|
|
94
137
|
} else {
|
|
95
138
|
await this._createCampaign(payload);
|
|
96
139
|
}
|
|
97
|
-
|
|
98
140
|
this._getEditorModal().hide();
|
|
99
141
|
});
|
|
100
142
|
}
|
|
@@ -109,7 +151,7 @@ export default class CalendarEvents {
|
|
|
109
151
|
const isEmail = $radio.value === 'email';
|
|
110
152
|
$emailFields.classList.toggle('d-none', !isEmail);
|
|
111
153
|
$pushFields.classList.toggle('d-none', isEmail);
|
|
112
|
-
$subjectHint.textContent = isEmail ? '(email subject line)' : '(notification body
|
|
154
|
+
$subjectHint.textContent = isEmail ? '(email subject line)' : '(notification body — campaign name is used as title)';
|
|
113
155
|
});
|
|
114
156
|
});
|
|
115
157
|
}
|
|
@@ -204,27 +246,10 @@ export default class CalendarEvents {
|
|
|
204
246
|
const now = new Date();
|
|
205
247
|
document.getElementById('campaign-date').value = formatDateUTC(now);
|
|
206
248
|
document.getElementById('campaign-time').value = formatTimeUTC(now);
|
|
249
|
+
this._updateLocalTimeHint();
|
|
207
250
|
});
|
|
208
251
|
}
|
|
209
252
|
|
|
210
|
-
_initDeleteButton() {
|
|
211
|
-
document.getElementById('btn-delete-campaign').addEventListener('click', async () => {
|
|
212
|
-
if (!this.editingCampaignId) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (!confirm('Delete this campaign? This cannot be undone.')) {
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
await this._deleteCampaign(this.editingCampaignId);
|
|
222
|
-
this._getEditorModal().hide();
|
|
223
|
-
} catch (error) {
|
|
224
|
-
this.formManager.showError(`Delete failed: ${error.message}`);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
253
|
|
|
229
254
|
// ============================================
|
|
230
255
|
// Modal Operations
|
|
@@ -233,6 +258,7 @@ export default class CalendarEvents {
|
|
|
233
258
|
this.editingCampaignId = null;
|
|
234
259
|
this.formManager.reset();
|
|
235
260
|
this._toggleDeleteButton(false);
|
|
261
|
+
this._showCampaignId(null);
|
|
236
262
|
this._setType('email');
|
|
237
263
|
document.getElementById('campaign-modal-title-text').textContent = 'Create Campaign';
|
|
238
264
|
|
|
@@ -260,6 +286,7 @@ export default class CalendarEvents {
|
|
|
260
286
|
}
|
|
261
287
|
this.editingCampaignId = template.id;
|
|
262
288
|
this._toggleDeleteButton(true);
|
|
289
|
+
this._showCampaignId(template.id);
|
|
263
290
|
document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
|
|
264
291
|
this._populateFormFromCampaign(template, true);
|
|
265
292
|
this._getEditorModal().show();
|
|
@@ -274,10 +301,15 @@ export default class CalendarEvents {
|
|
|
274
301
|
return;
|
|
275
302
|
}
|
|
276
303
|
|
|
277
|
-
// Recurring template: editable
|
|
304
|
+
// Recurring template: editable only if still pending
|
|
278
305
|
if (displayType === DISPLAY_TYPES.RECURRING_TEMPLATE) {
|
|
306
|
+
if (campaign.status === 'sent' || campaign.status === 'failed') {
|
|
307
|
+
this._openResultsModal(campaign);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
279
310
|
this.editingCampaignId = campaignId;
|
|
280
311
|
this._toggleDeleteButton(true);
|
|
312
|
+
this._showCampaignId(campaignId);
|
|
281
313
|
document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
|
|
282
314
|
this._populateFormFromCampaign(campaign, true);
|
|
283
315
|
this._getEditorModal().show();
|
|
@@ -287,6 +319,7 @@ export default class CalendarEvents {
|
|
|
287
319
|
// One-off pending: fully editable
|
|
288
320
|
this.editingCampaignId = campaignId;
|
|
289
321
|
this._toggleDeleteButton(true);
|
|
322
|
+
this._showCampaignId(campaignId);
|
|
290
323
|
document.getElementById('campaign-modal-title-text').textContent = 'Edit Campaign';
|
|
291
324
|
this._populateFormFromCampaign(campaign, true);
|
|
292
325
|
this._getEditorModal().show();
|
|
@@ -323,7 +356,8 @@ export default class CalendarEvents {
|
|
|
323
356
|
document.getElementById('campaign-subject').value = settings.subject || '';
|
|
324
357
|
document.getElementById('campaign-date').value = formatDateUTC(d);
|
|
325
358
|
document.getElementById('campaign-time').value = formatTimeUTC(d);
|
|
326
|
-
|
|
359
|
+
const contentData = settings.data?.content || {};
|
|
360
|
+
document.getElementById('campaign-discount-code').value = contentData.discountCode || '';
|
|
327
361
|
document.getElementById('campaign-test').checked = !!settings.test;
|
|
328
362
|
|
|
329
363
|
// Targeting
|
|
@@ -333,8 +367,8 @@ export default class CalendarEvents {
|
|
|
333
367
|
|
|
334
368
|
// Email fields
|
|
335
369
|
document.getElementById('campaign-preheader').value = settings.preheader || '';
|
|
336
|
-
document.getElementById('campaign-content').value =
|
|
337
|
-
document.getElementById('campaign-template').value = settings.template || '
|
|
370
|
+
document.getElementById('campaign-content').value = contentData.message || '';
|
|
371
|
+
document.getElementById('campaign-template').value = settings.template || 'card';
|
|
338
372
|
document.getElementById('campaign-sender').value = settings.sender || 'marketing';
|
|
339
373
|
|
|
340
374
|
// Push fields
|
|
@@ -409,6 +443,16 @@ export default class CalendarEvents {
|
|
|
409
443
|
}
|
|
410
444
|
}
|
|
411
445
|
|
|
446
|
+
_showCampaignId(id) {
|
|
447
|
+
const $el = document.getElementById('campaign-modal-id');
|
|
448
|
+
if (id) {
|
|
449
|
+
$el.textContent = id;
|
|
450
|
+
$el.classList.remove('d-none');
|
|
451
|
+
} else {
|
|
452
|
+
$el.classList.add('d-none');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
412
456
|
_updateLocalTimeHint() {
|
|
413
457
|
const $row = document.getElementById('campaign-local-time-row');
|
|
414
458
|
const $hint = document.getElementById('campaign-time-local');
|
|
@@ -456,9 +500,6 @@ export default class CalendarEvents {
|
|
|
456
500
|
};
|
|
457
501
|
|
|
458
502
|
// Config
|
|
459
|
-
if (c.discountCode) {
|
|
460
|
-
payload.discountCode = c.discountCode.trim();
|
|
461
|
-
}
|
|
462
503
|
if (c.test) {
|
|
463
504
|
payload.test = true;
|
|
464
505
|
}
|
|
@@ -492,15 +533,23 @@ export default class CalendarEvents {
|
|
|
492
533
|
if (c.preheader) {
|
|
493
534
|
payload.preheader = c.preheader.trim();
|
|
494
535
|
}
|
|
495
|
-
if (c.
|
|
496
|
-
payload.content = c.content;
|
|
497
|
-
}
|
|
498
|
-
if (c.template && c.template !== 'default') {
|
|
536
|
+
if (c.template && c.template !== 'card') {
|
|
499
537
|
payload.template = c.template;
|
|
500
538
|
}
|
|
501
539
|
if (c.sender) {
|
|
502
540
|
payload.sender = c.sender;
|
|
503
541
|
}
|
|
542
|
+
|
|
543
|
+
const content = {};
|
|
544
|
+
if (c.content) {
|
|
545
|
+
content.message = c.content;
|
|
546
|
+
}
|
|
547
|
+
if (c.discountCode) {
|
|
548
|
+
content.discountCode = c.discountCode.trim();
|
|
549
|
+
}
|
|
550
|
+
if (Object.keys(content).length) {
|
|
551
|
+
payload.data = { ...payload.data, content };
|
|
552
|
+
}
|
|
504
553
|
}
|
|
505
554
|
|
|
506
555
|
// Push-specific
|
|
@@ -732,9 +781,12 @@ export default class CalendarEvents {
|
|
|
732
781
|
html += `<span class="badge bg-${campaign.type === 'email' ? 'primary' : 'success'}">${campaign.type === 'email' ? 'Email' : 'Push'}</span>`;
|
|
733
782
|
html += `</div>`;
|
|
734
783
|
html += `<table class="table table-sm table-borderless mb-0">`;
|
|
735
|
-
html += `<tr><td class="text-muted" style="width:120px">
|
|
784
|
+
html += `<tr><td class="text-muted" style="width:120px">ID</td><td><code>${webManager.utilities().escapeHTML(campaign.id)}</code></td></tr>`;
|
|
785
|
+
html += `<tr><td class="text-muted">Name</td><td>${webManager.utilities().escapeHTML(settings.name || '')}</td></tr>`;
|
|
736
786
|
html += `<tr><td class="text-muted">Subject</td><td>${webManager.utilities().escapeHTML(settings.subject || '')}</td></tr>`;
|
|
737
|
-
html += `<tr><td class="text-muted">
|
|
787
|
+
html += `<tr><td class="text-muted">Test</td><td>${settings.test ? '<span class="badge bg-warning">Yes</span>' : 'No'}</td></tr>`;
|
|
788
|
+
const localStr = d.toLocaleString('en', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' });
|
|
789
|
+
html += `<tr><td class="text-muted">Sent At</td><td>${formatDateUTC(d)} ${formatTimeUTC(d)} UTC <span class="text-muted">(${localStr})</span></td></tr>`;
|
|
738
790
|
|
|
739
791
|
if (campaign.type === 'email') {
|
|
740
792
|
if (settings.preheader) {
|
|
@@ -767,10 +819,11 @@ export default class CalendarEvents {
|
|
|
767
819
|
html += '</div>';
|
|
768
820
|
|
|
769
821
|
// Content (email only)
|
|
770
|
-
|
|
822
|
+
const resultContent = settings.data?.content?.message;
|
|
823
|
+
if (campaign.type === 'email' && resultContent) {
|
|
771
824
|
html += '<div class="mb-4">';
|
|
772
825
|
html += '<h6>Content</h6>';
|
|
773
|
-
html += `<pre class="bg-body-tertiary p-3 rounded small" style="white-space:pre-wrap;max-height:200px;overflow-y:auto">${webManager.utilities().escapeHTML(
|
|
826
|
+
html += `<pre class="bg-body-tertiary p-3 rounded small" style="white-space:pre-wrap;max-height:200px;overflow-y:auto">${webManager.utilities().escapeHTML(resultContent)}</pre>`;
|
|
774
827
|
html += '</div>';
|
|
775
828
|
}
|
|
776
829
|
|
|
@@ -14,6 +14,18 @@ export default class CalendarRenderer {
|
|
|
14
14
|
this.$toolbar = document.getElementById('calendar-toolbar');
|
|
15
15
|
this.$grid = document.getElementById('calendar-grid');
|
|
16
16
|
this._nowLineInterval = null;
|
|
17
|
+
this._resizeObserver = null;
|
|
18
|
+
|
|
19
|
+
this._initResizeObserver();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_initResizeObserver() {
|
|
23
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
24
|
+
if (this.core.viewMode === 'month') {
|
|
25
|
+
this._fitMonthEvents();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
this._resizeObserver.observe(this.$grid);
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
// ============================================
|
|
@@ -90,6 +102,7 @@ export default class CalendarRenderer {
|
|
|
90
102
|
// Month View
|
|
91
103
|
// ============================================
|
|
92
104
|
_renderMonthView() {
|
|
105
|
+
this._pillHeight = null;
|
|
93
106
|
const core = this.core;
|
|
94
107
|
const cells = core.getMonthGrid();
|
|
95
108
|
|
|
@@ -120,13 +133,26 @@ export default class CalendarRenderer {
|
|
|
120
133
|
html += '</div>';
|
|
121
134
|
|
|
122
135
|
this.$grid.innerHTML = html;
|
|
123
|
-
this._fitMonthEvents();
|
|
136
|
+
requestAnimationFrame(() => this._fitMonthEvents());
|
|
124
137
|
this._bindCellClicks();
|
|
125
138
|
this._bindCampaignClicks();
|
|
126
139
|
this._bindDragAndDrop();
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
_fitMonthEvents() {
|
|
143
|
+
// Measure natural pill height once from a detached clone so flex
|
|
144
|
+
// compression doesn't skew the value
|
|
145
|
+
if (!this._pillHeight) {
|
|
146
|
+
const $sample = this.$grid.querySelector('.calendar-event');
|
|
147
|
+
if ($sample) {
|
|
148
|
+
const $clone = $sample.cloneNode(true);
|
|
149
|
+
$clone.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;';
|
|
150
|
+
document.body.appendChild($clone);
|
|
151
|
+
this._pillHeight = $clone.getBoundingClientRect().height;
|
|
152
|
+
$clone.remove();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
this.$grid.querySelectorAll('.calendar-cell').forEach(($cell) => {
|
|
131
157
|
const $events = $cell.querySelector('.calendar-cell-events');
|
|
132
158
|
if (!$events) { return; }
|
|
@@ -135,12 +161,15 @@ export default class CalendarRenderer {
|
|
|
135
161
|
const $more = $events.querySelector('.calendar-cell-more');
|
|
136
162
|
if ($pills.length === 0) { return; }
|
|
137
163
|
|
|
138
|
-
//
|
|
164
|
+
// Hide everything first so the cell has its natural empty height
|
|
165
|
+
$pills.forEach(($p) => { $p.style.display = 'none'; });
|
|
166
|
+
if ($more) { $more.style.display = 'none'; }
|
|
167
|
+
|
|
139
168
|
const cellRect = $cell.getBoundingClientRect();
|
|
140
169
|
const eventsRect = $events.getBoundingClientRect();
|
|
141
170
|
const available = cellRect.bottom - eventsRect.top;
|
|
142
171
|
|
|
143
|
-
const pillHeight =
|
|
172
|
+
const pillHeight = this._pillHeight || 20;
|
|
144
173
|
const gap = 1;
|
|
145
174
|
|
|
146
175
|
// Measure "+more" line height
|
|
@@ -151,24 +180,26 @@ export default class CalendarRenderer {
|
|
|
151
180
|
const moreLineHeight = $more ? $more.getBoundingClientRect().height + gap : 0;
|
|
152
181
|
if ($more) { $more.style.display = 'none'; }
|
|
153
182
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (let
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (totalNeeded <= available) {
|
|
163
|
-
$pills[i].style.display = '';
|
|
164
|
-
shown++;
|
|
183
|
+
// Calculate how many pills fit
|
|
184
|
+
let maxVisible = 0;
|
|
185
|
+
for (let n = 1; n <= $pills.length; n++) {
|
|
186
|
+
const pillsHeight = n * pillHeight + (n - 1) * gap;
|
|
187
|
+
const needsMore = n < $pills.length;
|
|
188
|
+
const total = pillsHeight + (needsMore ? moreLineHeight : 0);
|
|
189
|
+
if (total <= available) {
|
|
190
|
+
maxVisible = n;
|
|
165
191
|
} else {
|
|
166
|
-
|
|
192
|
+
break;
|
|
167
193
|
}
|
|
168
194
|
}
|
|
169
195
|
|
|
196
|
+
// Apply visibility
|
|
197
|
+
$pills.forEach(($p, i) => {
|
|
198
|
+
$p.style.display = i < maxVisible ? '' : 'none';
|
|
199
|
+
});
|
|
200
|
+
|
|
170
201
|
if ($more) {
|
|
171
|
-
const hidden = $pills.length -
|
|
202
|
+
const hidden = $pills.length - maxVisible;
|
|
172
203
|
if (hidden > 0) {
|
|
173
204
|
$more.textContent = `+${hidden} more`;
|
|
174
205
|
$more.style.display = '';
|
|
@@ -60,6 +60,7 @@ prerender_icons:
|
|
|
60
60
|
- name: "wifi"
|
|
61
61
|
- name: "battery-full"
|
|
62
62
|
- name: "table-list"
|
|
63
|
+
- name: "vial"
|
|
63
64
|
---
|
|
64
65
|
|
|
65
66
|
<!-- Calendar Root -->
|
|
@@ -79,6 +80,7 @@ prerender_icons:
|
|
|
79
80
|
<h5 class="modal-title" id="campaign-modal-title">
|
|
80
81
|
{% uj_icon "plus", "fa-md me-2" %}
|
|
81
82
|
<span id="campaign-modal-title-text">Create campaign</span>
|
|
83
|
+
<small class="text-muted fw-normal ms-2 d-none" id="campaign-modal-id" style="font-size: 0.6rem"></small>
|
|
82
84
|
</h5>
|
|
83
85
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
84
86
|
</div>
|
|
@@ -87,7 +89,7 @@ prerender_icons:
|
|
|
87
89
|
|
|
88
90
|
<!-- Campaign Type -->
|
|
89
91
|
<h6 class="mb-3">
|
|
90
|
-
{% uj_icon "bullhorn", "
|
|
92
|
+
{% uj_icon "bullhorn", "me-2 text-info" %}
|
|
91
93
|
Campaign type
|
|
92
94
|
</h6>
|
|
93
95
|
<div class="row mb-4">
|
|
@@ -186,9 +188,8 @@ prerender_icons:
|
|
|
186
188
|
<div class="col-md-6">
|
|
187
189
|
<label for="campaign-template" class="form-label">Template</label>
|
|
188
190
|
<select class="form-select" id="campaign-template" name="campaign.template">
|
|
189
|
-
<option value="
|
|
190
|
-
<option value="
|
|
191
|
-
<option value="main/basic/plain">Plain</option>
|
|
191
|
+
<option value="card" selected>Card</option>
|
|
192
|
+
<option value="plain">Plain</option>
|
|
192
193
|
</select>
|
|
193
194
|
</div>
|
|
194
195
|
<div class="col-md-6">
|
|
@@ -201,6 +202,18 @@ prerender_icons:
|
|
|
201
202
|
</select>
|
|
202
203
|
</div>
|
|
203
204
|
</div>
|
|
205
|
+
|
|
206
|
+
<div class="mb-3">
|
|
207
|
+
<label for="campaign-discount-code" class="form-label">
|
|
208
|
+
Discount code
|
|
209
|
+
<small class="text-muted fw-normal ms-1">(optional — resolves {discount.code} and {discount.percent})</small>
|
|
210
|
+
</label>
|
|
211
|
+
<input type="text"
|
|
212
|
+
class="form-control"
|
|
213
|
+
id="campaign-discount-code"
|
|
214
|
+
name="campaign.discountCode"
|
|
215
|
+
placeholder="e.g. UPGRADE15">
|
|
216
|
+
</div>
|
|
204
217
|
</div>
|
|
205
218
|
|
|
206
219
|
<!-- Push-only fields -->
|
|
@@ -271,19 +284,6 @@ prerender_icons:
|
|
|
271
284
|
</div>
|
|
272
285
|
</div>
|
|
273
286
|
|
|
274
|
-
<!-- Discount Code -->
|
|
275
|
-
<div class="mb-3">
|
|
276
|
-
<label for="campaign-discount-code" class="form-label">
|
|
277
|
-
Discount code
|
|
278
|
-
<small class="text-muted fw-normal ms-1">(optional)</small>
|
|
279
|
-
</label>
|
|
280
|
-
<input type="text"
|
|
281
|
-
class="form-control"
|
|
282
|
-
id="campaign-discount-code"
|
|
283
|
-
name="campaign.discountCode"
|
|
284
|
-
placeholder="e.g. UPGRADE15">
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
287
|
<!-- Test Mode -->
|
|
288
288
|
<div class="form-check mb-4">
|
|
289
289
|
<input class="form-check-input"
|
|
@@ -300,7 +300,7 @@ prerender_icons:
|
|
|
300
300
|
|
|
301
301
|
<!-- Schedule -->
|
|
302
302
|
<h6 class="mb-3 mt-4">
|
|
303
|
-
{% uj_icon "clock", "
|
|
303
|
+
{% uj_icon "clock", "me-2 text-info" %}
|
|
304
304
|
Schedule
|
|
305
305
|
<small class="text-muted fw-normal ms-1">(UTC)</small>
|
|
306
306
|
</h6>
|
|
@@ -330,9 +330,9 @@ prerender_icons:
|
|
|
330
330
|
<div class="invalid-feedback">Time is required</div>
|
|
331
331
|
</div>
|
|
332
332
|
<div class="col-md-3 d-flex align-items-end">
|
|
333
|
-
<button type="button" class="btn btn-
|
|
334
|
-
{% uj_icon "
|
|
335
|
-
|
|
333
|
+
<button type="button" class="btn btn-md btn-outline-primary w-100" id="btn-send-now">
|
|
334
|
+
{% uj_icon "clock", "fa-sm me-1" %}
|
|
335
|
+
Set now
|
|
336
336
|
</button>
|
|
337
337
|
</div>
|
|
338
338
|
</div>
|
|
@@ -342,7 +342,7 @@ prerender_icons:
|
|
|
342
342
|
|
|
343
343
|
<!-- Recurrence -->
|
|
344
344
|
<h6 class="mb-3 mt-4">
|
|
345
|
-
{% uj_icon "repeat", "
|
|
345
|
+
{% uj_icon "repeat", "me-2 text-info" %}
|
|
346
346
|
Recurrence
|
|
347
347
|
<small class="text-muted fw-normal ms-1">(optional)</small>
|
|
348
348
|
</h6>
|
|
@@ -445,7 +445,7 @@ prerender_icons:
|
|
|
445
445
|
|
|
446
446
|
<!-- Targeting -->
|
|
447
447
|
<h6 class="mb-3 mt-4">
|
|
448
|
-
{% uj_icon "users", "
|
|
448
|
+
{% uj_icon "users", "me-2 text-success" %}
|
|
449
449
|
Targeting
|
|
450
450
|
</h6>
|
|
451
451
|
|
|
@@ -499,32 +499,33 @@ prerender_icons:
|
|
|
499
499
|
|
|
500
500
|
<div class="mb-3">
|
|
501
501
|
<label for="campaign-segments" class="form-label">
|
|
502
|
-
|
|
503
|
-
<small class="text-muted fw-normal ms-1">(
|
|
502
|
+
Include segments
|
|
503
|
+
<small class="text-muted fw-normal ms-1">(AND — contacts must match ALL listed segments)</small>
|
|
504
504
|
</label>
|
|
505
505
|
<input type="text"
|
|
506
506
|
class="form-control"
|
|
507
507
|
id="campaign-segments"
|
|
508
508
|
name="campaign.segments"
|
|
509
|
-
placeholder="e.g.
|
|
509
|
+
placeholder="e.g. subscription_free, lifecycle_30d">
|
|
510
|
+
<small class="form-text text-muted">SSOT keys: subscription_free, subscription_paid, subscription_trialing, subscription_cancelled, subscription_churned_trial, subscription_churned_paid, lifecycle_7d, lifecycle_30d, engagement_active_30d, engagement_inactive_90d, etc.</small>
|
|
510
511
|
</div>
|
|
511
512
|
|
|
512
513
|
<div class="mb-3">
|
|
513
514
|
<label for="campaign-exclude-segments" class="form-label">
|
|
514
515
|
Exclude segments
|
|
515
|
-
<small class="text-muted fw-normal ms-1">(
|
|
516
|
+
<small class="text-muted fw-normal ms-1">(AND — contacts matching ANY excluded segment are removed)</small>
|
|
516
517
|
</label>
|
|
517
518
|
<input type="text"
|
|
518
519
|
class="form-control"
|
|
519
520
|
id="campaign-exclude-segments"
|
|
520
521
|
name="campaign.excludeSegments"
|
|
521
|
-
placeholder="e.g.
|
|
522
|
+
placeholder="e.g. subscription_paid, subscription_trialing">
|
|
522
523
|
</div>
|
|
523
524
|
|
|
524
525
|
<!-- Advanced Settings (collapsible) -->
|
|
525
526
|
<h6 class="mb-3 mt-4">
|
|
526
527
|
<a class="text-decoration-none" data-bs-toggle="collapse" href="#advanced-settings" role="button" aria-expanded="false">
|
|
527
|
-
{% uj_icon "sliders", "
|
|
528
|
+
{% uj_icon "sliders", "me-2 text-info" %}
|
|
528
529
|
Advanced settings
|
|
529
530
|
<small class="text-muted fw-normal ms-1">(optional)</small>
|
|
530
531
|
</a>
|
|
@@ -575,23 +576,26 @@ prerender_icons:
|
|
|
575
576
|
</div>
|
|
576
577
|
</div>
|
|
577
578
|
|
|
579
|
+
<div class="modal-footer d-flex justify-content-between">
|
|
580
|
+
<div>
|
|
581
|
+
<button type="submit" name="action" value="delete" formnovalidate class="btn btn-outline-danger d-none" id="btn-delete-campaign">
|
|
582
|
+
{% uj_icon "trash", "fa-sm me-1" %}
|
|
583
|
+
<span class="button-text">Delete</span>
|
|
584
|
+
</button>
|
|
585
|
+
</div>
|
|
586
|
+
<div class="d-flex gap-2">
|
|
587
|
+
<button type="submit" name="action" value="test" formnovalidate class="btn btn-outline-warning" id="btn-send-test">
|
|
588
|
+
{% uj_icon "vial", "fa-sm me-1" %}
|
|
589
|
+
<span class="button-text">Send test</span>
|
|
590
|
+
</button>
|
|
591
|
+
<button type="submit" name="action" value="save" class="btn btn-adaptive" id="btn-save-campaign">
|
|
592
|
+
{% uj_icon "paper-plane", "fa-sm me-1" %}
|
|
593
|
+
<span class="button-text">Save campaign</span>
|
|
594
|
+
</button>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
578
597
|
</form>
|
|
579
598
|
</div>
|
|
580
|
-
<div class="modal-footer d-flex justify-content-between">
|
|
581
|
-
<div>
|
|
582
|
-
<button type="button" class="btn btn-outline-danger d-none" id="btn-delete-campaign">
|
|
583
|
-
{% uj_icon "trash", "fa-sm me-1" %}
|
|
584
|
-
Delete
|
|
585
|
-
</button>
|
|
586
|
-
</div>
|
|
587
|
-
<div class="d-flex gap-2">
|
|
588
|
-
<button type="button" class="btn btn-outline-adaptive" data-bs-dismiss="modal">Cancel</button>
|
|
589
|
-
<button type="submit" form="campaign-editor-form" class="btn btn-adaptive" id="btn-save-campaign">
|
|
590
|
-
{% uj_icon "paper-plane", "fa-sm me-1" %}
|
|
591
|
-
<span class="button-text">Save campaign</span>
|
|
592
|
-
</button>
|
|
593
|
-
</div>
|
|
594
|
-
</div>
|
|
595
599
|
</div>
|
|
596
600
|
</div>
|
|
597
601
|
</div>
|
|
@@ -1277,6 +1277,36 @@ badges:
|
|
|
1277
1277
|
</form>
|
|
1278
1278
|
</div>
|
|
1279
1279
|
</div>
|
|
1280
|
+
<div class="card mt-3">
|
|
1281
|
+
<div class="card-body">
|
|
1282
|
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
1283
|
+
<h5 class="card-title mb-0">Push notifications</h5>
|
|
1284
|
+
<span id="push-notification-status"><span class="badge bg-secondary">Checking...</span></span>
|
|
1285
|
+
</div>
|
|
1286
|
+
<p class="text-muted small mb-3">Receive push notifications in your browser for important updates.</p>
|
|
1287
|
+
|
|
1288
|
+
<form id="push-subscribe-form" novalidate onsubmit="return false" style="display:none">
|
|
1289
|
+
<button type="submit" class="btn btn-adaptive btn-sm" id="push-subscribe-btn">
|
|
1290
|
+
{% uj_icon "bell", "me-1" %}
|
|
1291
|
+
<span class="button-text">Enable push notifications</span>
|
|
1292
|
+
</button>
|
|
1293
|
+
</form>
|
|
1294
|
+
|
|
1295
|
+
<div class="mt-3 pt-3 border-top">
|
|
1296
|
+
<a class="small text-muted text-decoration-none d-flex align-items-center" data-bs-toggle="collapse" href="#push-token-details" role="button" aria-expanded="false">
|
|
1297
|
+
{% uj_icon "code", "fa-xs me-1" %} Token details
|
|
1298
|
+
</a>
|
|
1299
|
+
<div class="collapse mt-2" id="push-token-details">
|
|
1300
|
+
<div class="input-group input-group-sm">
|
|
1301
|
+
<input type="text" class="form-control font-monospace bg-body-tertiary" id="push-token-value" readonly value="No token" style="font-size: 0.65rem;">
|
|
1302
|
+
<button type="button" class="btn btn-outline-adaptive" id="copy-push-token-btn">
|
|
1303
|
+
{% uj_icon "copy" %}
|
|
1304
|
+
</button>
|
|
1305
|
+
</div>
|
|
1306
|
+
</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
</div>
|
|
1280
1310
|
</section>
|
|
1281
1311
|
|
|
1282
1312
|
<!-- API Keys Section -->
|
package/dist/service-worker.js
CHANGED
|
@@ -145,6 +145,27 @@ class Manager {
|
|
|
145
145
|
// Initialize messaging
|
|
146
146
|
this.libraries.messaging = firebase.messaging();
|
|
147
147
|
|
|
148
|
+
// Handle background push messages (when the page is not focused)
|
|
149
|
+
this.libraries.messaging.onBackgroundMessage((payload) => {
|
|
150
|
+
console.log('Background message received:', payload);
|
|
151
|
+
|
|
152
|
+
const notification = payload.notification || {};
|
|
153
|
+
const data = payload.data || {};
|
|
154
|
+
|
|
155
|
+
const title = notification.title || 'New notification';
|
|
156
|
+
const options = {
|
|
157
|
+
body: notification.body || '',
|
|
158
|
+
icon: notification.icon || notification.image || '/assets/images/favicon/favicon-192x192.png',
|
|
159
|
+
data: payload,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (data.click_action) {
|
|
163
|
+
options.data.click_action = data.click_action;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
serviceWorker.registration.showNotification(title, options);
|
|
167
|
+
});
|
|
168
|
+
|
|
148
169
|
// Attach firebase to SWManager
|
|
149
170
|
this.libraries.firebase = firebase;
|
|
150
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-jekyll-manager",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
4
4
|
"description": "Ultimate Jekyll dependency manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"prettier": "^3.8.3",
|
|
109
109
|
"sass": "^1.100.0",
|
|
110
110
|
"spellchecker": "^3.7.1",
|
|
111
|
-
"web-manager": "^4.
|
|
111
|
+
"web-manager": "^4.3.0",
|
|
112
112
|
"webpack": "^5.107.2",
|
|
113
113
|
"wonderful-fetch": "^2.0.5",
|
|
114
114
|
"wonderful-version": "^1.3.2",
|