ultimate-jekyll-manager 1.6.4 → 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/CHANGELOG.md +25 -0
- package/dist/assets/css/pages/admin/calendar/index.scss +23 -9
- package/dist/assets/js/pages/account/sections/notifications.js +88 -0
- package/dist/assets/js/pages/admin/calendar/calendar-core.js +80 -14
- package/dist/assets/js/pages/admin/calendar/calendar-events.js +166 -48
- package/dist/assets/js/pages/admin/calendar/calendar-renderer.js +120 -16
- package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +84 -53
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +30 -0
- package/dist/gulp/tasks/imagemin.js +12 -12
- package/dist/gulp/tasks/translation.js +95 -158
- package/dist/service-worker.js +21 -0
- package/package.json +2 -2
|
@@ -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,7 +46,12 @@ 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
|
+
document.getElementById('campaign-local-time-row').style.display = 'none';
|
|
50
51
|
});
|
|
52
|
+
|
|
53
|
+
document.getElementById('campaign-date').addEventListener('input', () => this._updateLocalTimeHint());
|
|
54
|
+
document.getElementById('campaign-time').addEventListener('input', () => this._updateLocalTimeHint());
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
_initResultsModal() {
|
|
@@ -82,15 +86,57 @@ export default class CalendarEvents {
|
|
|
82
86
|
allowResubmit: true,
|
|
83
87
|
});
|
|
84
88
|
|
|
85
|
-
this.formManager.on('
|
|
86
|
-
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;
|
|
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
|
+
}
|
|
87
132
|
|
|
133
|
+
// Default: save
|
|
134
|
+
const payload = this._buildPayload(data);
|
|
88
135
|
if (this.editingCampaignId) {
|
|
89
136
|
await this._updateCampaign(this.editingCampaignId, payload);
|
|
90
137
|
} else {
|
|
91
138
|
await this._createCampaign(payload);
|
|
92
139
|
}
|
|
93
|
-
|
|
94
140
|
this._getEditorModal().hide();
|
|
95
141
|
});
|
|
96
142
|
}
|
|
@@ -105,7 +151,7 @@ export default class CalendarEvents {
|
|
|
105
151
|
const isEmail = $radio.value === 'email';
|
|
106
152
|
$emailFields.classList.toggle('d-none', !isEmail);
|
|
107
153
|
$pushFields.classList.toggle('d-none', isEmail);
|
|
108
|
-
$subjectHint.textContent = isEmail ? '(email subject line)' : '(notification body
|
|
154
|
+
$subjectHint.textContent = isEmail ? '(email subject line)' : '(notification body — campaign name is used as title)';
|
|
109
155
|
});
|
|
110
156
|
});
|
|
111
157
|
}
|
|
@@ -168,6 +214,8 @@ export default class CalendarEvents {
|
|
|
168
214
|
const $patternSelect = document.getElementById('campaign-recurrence-pattern');
|
|
169
215
|
const $dayHint = document.getElementById('recurrence-day-hint');
|
|
170
216
|
const $monthRow = document.getElementById('recurrence-month-row');
|
|
217
|
+
const $nthRow = document.getElementById('recurrence-nth-row');
|
|
218
|
+
const $dayInput = document.getElementById('campaign-recurrence-day');
|
|
171
219
|
|
|
172
220
|
$checkbox.addEventListener('change', () => {
|
|
173
221
|
$fields.classList.toggle('d-none', !$checkbox.checked);
|
|
@@ -175,14 +223,21 @@ export default class CalendarEvents {
|
|
|
175
223
|
|
|
176
224
|
$patternSelect.addEventListener('change', () => {
|
|
177
225
|
const pattern = $patternSelect.value;
|
|
178
|
-
|
|
179
|
-
|
|
226
|
+
|
|
227
|
+
const isWeekday = pattern === 'weekly' || pattern === 'monthly-weekday';
|
|
228
|
+
|
|
229
|
+
if (isWeekday) {
|
|
180
230
|
$dayHint.textContent = '(of week, 0=Sun)';
|
|
231
|
+
$dayInput.min = 0;
|
|
232
|
+
$dayInput.max = 6;
|
|
181
233
|
} else {
|
|
182
234
|
$dayHint.textContent = '(of month)';
|
|
235
|
+
$dayInput.min = 1;
|
|
236
|
+
$dayInput.max = 31;
|
|
183
237
|
}
|
|
184
|
-
|
|
238
|
+
|
|
185
239
|
$monthRow.classList.toggle('d-none', pattern !== 'yearly');
|
|
240
|
+
$nthRow.classList.toggle('d-none', pattern !== 'monthly-weekday');
|
|
186
241
|
});
|
|
187
242
|
}
|
|
188
243
|
|
|
@@ -191,27 +246,10 @@ export default class CalendarEvents {
|
|
|
191
246
|
const now = new Date();
|
|
192
247
|
document.getElementById('campaign-date').value = formatDateUTC(now);
|
|
193
248
|
document.getElementById('campaign-time').value = formatTimeUTC(now);
|
|
249
|
+
this._updateLocalTimeHint();
|
|
194
250
|
});
|
|
195
251
|
}
|
|
196
252
|
|
|
197
|
-
_initDeleteButton() {
|
|
198
|
-
document.getElementById('btn-delete-campaign').addEventListener('click', async () => {
|
|
199
|
-
if (!this.editingCampaignId) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!confirm('Delete this campaign? This cannot be undone.')) {
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
await this._deleteCampaign(this.editingCampaignId);
|
|
209
|
-
this._getEditorModal().hide();
|
|
210
|
-
} catch (error) {
|
|
211
|
-
this.formManager.showError(`Delete failed: ${error.message}`);
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
253
|
|
|
216
254
|
// ============================================
|
|
217
255
|
// Modal Operations
|
|
@@ -220,12 +258,14 @@ export default class CalendarEvents {
|
|
|
220
258
|
this.editingCampaignId = null;
|
|
221
259
|
this.formManager.reset();
|
|
222
260
|
this._toggleDeleteButton(false);
|
|
261
|
+
this._showCampaignId(null);
|
|
223
262
|
this._setType('email');
|
|
224
263
|
document.getElementById('campaign-modal-title-text').textContent = 'Create Campaign';
|
|
225
264
|
|
|
226
265
|
// Pre-fill date and time
|
|
227
266
|
document.getElementById('campaign-date').value = date || '';
|
|
228
267
|
document.getElementById('campaign-time').value = time || '09:00';
|
|
268
|
+
this._updateLocalTimeHint();
|
|
229
269
|
|
|
230
270
|
this._getEditorModal().show();
|
|
231
271
|
}
|
|
@@ -246,6 +286,7 @@ export default class CalendarEvents {
|
|
|
246
286
|
}
|
|
247
287
|
this.editingCampaignId = template.id;
|
|
248
288
|
this._toggleDeleteButton(true);
|
|
289
|
+
this._showCampaignId(template.id);
|
|
249
290
|
document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
|
|
250
291
|
this._populateFormFromCampaign(template, true);
|
|
251
292
|
this._getEditorModal().show();
|
|
@@ -260,10 +301,15 @@ export default class CalendarEvents {
|
|
|
260
301
|
return;
|
|
261
302
|
}
|
|
262
303
|
|
|
263
|
-
// Recurring template: editable
|
|
304
|
+
// Recurring template: editable only if still pending
|
|
264
305
|
if (displayType === DISPLAY_TYPES.RECURRING_TEMPLATE) {
|
|
306
|
+
if (campaign.status === 'sent' || campaign.status === 'failed') {
|
|
307
|
+
this._openResultsModal(campaign);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
265
310
|
this.editingCampaignId = campaignId;
|
|
266
311
|
this._toggleDeleteButton(true);
|
|
312
|
+
this._showCampaignId(campaignId);
|
|
267
313
|
document.getElementById('campaign-modal-title-text').textContent = 'Edit Recurring Campaign';
|
|
268
314
|
this._populateFormFromCampaign(campaign, true);
|
|
269
315
|
this._getEditorModal().show();
|
|
@@ -273,6 +319,7 @@ export default class CalendarEvents {
|
|
|
273
319
|
// One-off pending: fully editable
|
|
274
320
|
this.editingCampaignId = campaignId;
|
|
275
321
|
this._toggleDeleteButton(true);
|
|
322
|
+
this._showCampaignId(campaignId);
|
|
276
323
|
document.getElementById('campaign-modal-title-text').textContent = 'Edit Campaign';
|
|
277
324
|
this._populateFormFromCampaign(campaign, true);
|
|
278
325
|
this._getEditorModal().show();
|
|
@@ -309,7 +356,8 @@ export default class CalendarEvents {
|
|
|
309
356
|
document.getElementById('campaign-subject').value = settings.subject || '';
|
|
310
357
|
document.getElementById('campaign-date').value = formatDateUTC(d);
|
|
311
358
|
document.getElementById('campaign-time').value = formatTimeUTC(d);
|
|
312
|
-
|
|
359
|
+
const contentData = settings.data?.content || {};
|
|
360
|
+
document.getElementById('campaign-discount-code').value = contentData.discountCode || '';
|
|
313
361
|
document.getElementById('campaign-test').checked = !!settings.test;
|
|
314
362
|
|
|
315
363
|
// Targeting
|
|
@@ -319,8 +367,8 @@ export default class CalendarEvents {
|
|
|
319
367
|
|
|
320
368
|
// Email fields
|
|
321
369
|
document.getElementById('campaign-preheader').value = settings.preheader || '';
|
|
322
|
-
document.getElementById('campaign-content').value =
|
|
323
|
-
document.getElementById('campaign-template').value = settings.template || '
|
|
370
|
+
document.getElementById('campaign-content').value = contentData.message || '';
|
|
371
|
+
document.getElementById('campaign-template').value = settings.template || 'card';
|
|
324
372
|
document.getElementById('campaign-sender').value = settings.sender || 'marketing';
|
|
325
373
|
|
|
326
374
|
// Push fields
|
|
@@ -359,23 +407,32 @@ export default class CalendarEvents {
|
|
|
359
407
|
const $recurrenceFields = document.getElementById('recurrence-fields');
|
|
360
408
|
|
|
361
409
|
if (recurrence) {
|
|
410
|
+
const pattern = recurrence.pattern || 'monthly';
|
|
411
|
+
const isWeekday = pattern === 'weekly' || pattern === 'monthly-weekday';
|
|
412
|
+
|
|
362
413
|
$recurringCheckbox.checked = true;
|
|
363
414
|
$recurrenceFields.classList.remove('d-none');
|
|
364
|
-
document.getElementById('campaign-recurrence-pattern').value =
|
|
415
|
+
document.getElementById('campaign-recurrence-pattern').value = pattern;
|
|
365
416
|
document.getElementById('campaign-recurrence-hour').value = recurrence.hour || 0;
|
|
417
|
+
document.getElementById('campaign-recurrence-minute').value = recurrence.minute || 0;
|
|
366
418
|
document.getElementById('campaign-recurrence-day').value = recurrence.day || 1;
|
|
419
|
+
document.getElementById('campaign-recurrence-nth').value = recurrence.nth || 2;
|
|
367
420
|
document.getElementById('campaign-recurrence-month').value = recurrence.month || 1;
|
|
368
421
|
|
|
369
|
-
|
|
370
|
-
document.getElementById('recurrence-
|
|
422
|
+
document.getElementById('recurrence-month-row').classList.toggle('d-none', pattern !== 'yearly');
|
|
423
|
+
document.getElementById('recurrence-nth-row').classList.toggle('d-none', pattern !== 'monthly-weekday');
|
|
371
424
|
|
|
372
|
-
// Update day hint
|
|
373
425
|
const $dayHint = document.getElementById('recurrence-day-hint');
|
|
374
|
-
$
|
|
426
|
+
const $dayInput = document.getElementById('campaign-recurrence-day');
|
|
427
|
+
$dayHint.textContent = isWeekday ? '(of week, 0=Sun)' : '(of month)';
|
|
428
|
+
$dayInput.min = isWeekday ? 0 : 1;
|
|
429
|
+
$dayInput.max = isWeekday ? 6 : 31;
|
|
375
430
|
} else {
|
|
376
431
|
$recurringCheckbox.checked = false;
|
|
377
432
|
$recurrenceFields.classList.add('d-none');
|
|
378
433
|
}
|
|
434
|
+
|
|
435
|
+
this._updateLocalTimeHint();
|
|
379
436
|
}
|
|
380
437
|
|
|
381
438
|
_setType(type) {
|
|
@@ -386,6 +443,45 @@ export default class CalendarEvents {
|
|
|
386
443
|
}
|
|
387
444
|
}
|
|
388
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
|
+
|
|
456
|
+
_updateLocalTimeHint() {
|
|
457
|
+
const $row = document.getElementById('campaign-local-time-row');
|
|
458
|
+
const $hint = document.getElementById('campaign-time-local');
|
|
459
|
+
const date = document.getElementById('campaign-date').value;
|
|
460
|
+
const time = document.getElementById('campaign-time').value;
|
|
461
|
+
|
|
462
|
+
if (!date || !time) {
|
|
463
|
+
$row.style.display = 'none';
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const [y, mo, d] = date.split('-').map(Number);
|
|
468
|
+
const [h, m] = time.split(':').map(Number);
|
|
469
|
+
const utc = new Date(Date.UTC(y, mo - 1, d, h, m));
|
|
470
|
+
|
|
471
|
+
const localStr = utc.toLocaleString('en', {
|
|
472
|
+
weekday: 'short',
|
|
473
|
+
month: 'short',
|
|
474
|
+
day: 'numeric',
|
|
475
|
+
year: 'numeric',
|
|
476
|
+
hour: 'numeric',
|
|
477
|
+
minute: '2-digit',
|
|
478
|
+
timeZoneName: 'short',
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
$hint.textContent = `Local: ${localStr}`;
|
|
482
|
+
$row.style.display = '';
|
|
483
|
+
}
|
|
484
|
+
|
|
389
485
|
// ============================================
|
|
390
486
|
// Payload Building
|
|
391
487
|
// ============================================
|
|
@@ -404,9 +500,6 @@ export default class CalendarEvents {
|
|
|
404
500
|
};
|
|
405
501
|
|
|
406
502
|
// Config
|
|
407
|
-
if (c.discountCode) {
|
|
408
|
-
payload.discountCode = c.discountCode.trim();
|
|
409
|
-
}
|
|
410
503
|
if (c.test) {
|
|
411
504
|
payload.test = true;
|
|
412
505
|
}
|
|
@@ -440,15 +533,23 @@ export default class CalendarEvents {
|
|
|
440
533
|
if (c.preheader) {
|
|
441
534
|
payload.preheader = c.preheader.trim();
|
|
442
535
|
}
|
|
443
|
-
if (c.
|
|
444
|
-
payload.content = c.content;
|
|
445
|
-
}
|
|
446
|
-
if (c.template && c.template !== 'default') {
|
|
536
|
+
if (c.template && c.template !== 'card') {
|
|
447
537
|
payload.template = c.template;
|
|
448
538
|
}
|
|
449
539
|
if (c.sender) {
|
|
450
540
|
payload.sender = c.sender;
|
|
451
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
|
+
}
|
|
452
553
|
}
|
|
453
554
|
|
|
454
555
|
// Push-specific
|
|
@@ -498,12 +599,17 @@ export default class CalendarEvents {
|
|
|
498
599
|
// Recurrence
|
|
499
600
|
if (c.recurring) {
|
|
500
601
|
const rec = c.recurrence || {};
|
|
602
|
+
const pattern = rec.pattern || 'monthly';
|
|
501
603
|
payload.recurrence = {
|
|
502
|
-
pattern
|
|
604
|
+
pattern,
|
|
503
605
|
hour: parseInt(rec.hour, 10) || 0,
|
|
606
|
+
minute: parseInt(rec.minute, 10) || 0,
|
|
504
607
|
day: parseInt(rec.day, 10) || 1,
|
|
505
608
|
};
|
|
506
|
-
if (
|
|
609
|
+
if (pattern === 'monthly-weekday') {
|
|
610
|
+
payload.recurrence.nth = parseInt(rec.nth, 10) || 1;
|
|
611
|
+
}
|
|
612
|
+
if (pattern === 'yearly') {
|
|
507
613
|
payload.recurrence.month = parseInt(rec.month, 10) || 1;
|
|
508
614
|
}
|
|
509
615
|
}
|
|
@@ -616,8 +722,13 @@ export default class CalendarEvents {
|
|
|
616
722
|
|
|
617
723
|
// Update recurrence metadata for the backend cron
|
|
618
724
|
recurrence.hour = d.getUTCHours();
|
|
725
|
+
recurrence.minute = d.getUTCMinutes();
|
|
619
726
|
if (recurrence.pattern === 'weekly') {
|
|
620
727
|
recurrence.day = d.getUTCDay();
|
|
728
|
+
} else if (recurrence.pattern === 'monthly-weekday') {
|
|
729
|
+
recurrence.day = d.getUTCDay();
|
|
730
|
+
// Calculate which occurrence of this weekday falls on this date (1st, 2nd, 3rd, 4th)
|
|
731
|
+
recurrence.nth = Math.ceil(d.getUTCDate() / 7);
|
|
621
732
|
} else if (recurrence.pattern === 'monthly' || recurrence.pattern === 'quarterly') {
|
|
622
733
|
recurrence.day = d.getUTCDate();
|
|
623
734
|
} else if (recurrence.pattern === 'yearly') {
|
|
@@ -670,9 +781,12 @@ export default class CalendarEvents {
|
|
|
670
781
|
html += `<span class="badge bg-${campaign.type === 'email' ? 'primary' : 'success'}">${campaign.type === 'email' ? 'Email' : 'Push'}</span>`;
|
|
671
782
|
html += `</div>`;
|
|
672
783
|
html += `<table class="table table-sm table-borderless mb-0">`;
|
|
673
|
-
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>`;
|
|
674
786
|
html += `<tr><td class="text-muted">Subject</td><td>${webManager.utilities().escapeHTML(settings.subject || '')}</td></tr>`;
|
|
675
|
-
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>`;
|
|
676
790
|
|
|
677
791
|
if (campaign.type === 'email') {
|
|
678
792
|
if (settings.preheader) {
|
|
@@ -705,10 +819,11 @@ export default class CalendarEvents {
|
|
|
705
819
|
html += '</div>';
|
|
706
820
|
|
|
707
821
|
// Content (email only)
|
|
708
|
-
|
|
822
|
+
const resultContent = settings.data?.content?.message;
|
|
823
|
+
if (campaign.type === 'email' && resultContent) {
|
|
709
824
|
html += '<div class="mb-4">';
|
|
710
825
|
html += '<h6>Content</h6>';
|
|
711
|
-
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>`;
|
|
712
827
|
html += '</div>';
|
|
713
828
|
}
|
|
714
829
|
|
|
@@ -735,9 +850,12 @@ export default class CalendarEvents {
|
|
|
735
850
|
document.getElementById('recurrence-fields').classList.add('d-none');
|
|
736
851
|
document.getElementById('campaign-recurrence-pattern').value = 'monthly';
|
|
737
852
|
document.getElementById('campaign-recurrence-hour').value = '14';
|
|
853
|
+
document.getElementById('campaign-recurrence-minute').value = '0';
|
|
738
854
|
document.getElementById('campaign-recurrence-day').value = '1';
|
|
855
|
+
document.getElementById('campaign-recurrence-nth').value = '2';
|
|
739
856
|
document.getElementById('campaign-recurrence-month').value = '1';
|
|
740
857
|
document.getElementById('recurrence-month-row').classList.add('d-none');
|
|
858
|
+
document.getElementById('recurrence-nth-row').classList.add('d-none');
|
|
741
859
|
}
|
|
742
860
|
|
|
743
861
|
}
|
|
@@ -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
|
|
|
@@ -110,10 +123,9 @@ export default class CalendarRenderer {
|
|
|
110
123
|
html += `<div class="calendar-cell-date">${cell.date.getUTCDate()}</div>`;
|
|
111
124
|
html += '<div class="calendar-cell-events">';
|
|
112
125
|
|
|
113
|
-
|
|
114
|
-
campaigns.
|
|
115
|
-
|
|
116
|
-
html += `<div class="calendar-cell-more" data-date="${dateStr}">+${campaigns.length - maxVisible} more</div>`;
|
|
126
|
+
campaigns.forEach((c) => { html += this._renderEventPill(c); });
|
|
127
|
+
if (campaigns.length > 0) {
|
|
128
|
+
html += `<div class="calendar-cell-more" data-date="${dateStr}" style="display:none"></div>`;
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
html += '</div></div>';
|
|
@@ -121,11 +133,81 @@ export default class CalendarRenderer {
|
|
|
121
133
|
html += '</div>';
|
|
122
134
|
|
|
123
135
|
this.$grid.innerHTML = html;
|
|
136
|
+
requestAnimationFrame(() => this._fitMonthEvents());
|
|
124
137
|
this._bindCellClicks();
|
|
125
138
|
this._bindCampaignClicks();
|
|
126
139
|
this._bindDragAndDrop();
|
|
127
140
|
}
|
|
128
141
|
|
|
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
|
+
|
|
156
|
+
this.$grid.querySelectorAll('.calendar-cell').forEach(($cell) => {
|
|
157
|
+
const $events = $cell.querySelector('.calendar-cell-events');
|
|
158
|
+
if (!$events) { return; }
|
|
159
|
+
|
|
160
|
+
const $pills = $events.querySelectorAll('.calendar-event');
|
|
161
|
+
const $more = $events.querySelector('.calendar-cell-more');
|
|
162
|
+
if ($pills.length === 0) { return; }
|
|
163
|
+
|
|
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
|
+
|
|
168
|
+
const cellRect = $cell.getBoundingClientRect();
|
|
169
|
+
const eventsRect = $events.getBoundingClientRect();
|
|
170
|
+
const available = cellRect.bottom - eventsRect.top;
|
|
171
|
+
|
|
172
|
+
const pillHeight = this._pillHeight || 20;
|
|
173
|
+
const gap = 1;
|
|
174
|
+
|
|
175
|
+
// Measure "+more" line height
|
|
176
|
+
if ($more) {
|
|
177
|
+
$more.textContent = '+1 more';
|
|
178
|
+
$more.style.display = '';
|
|
179
|
+
}
|
|
180
|
+
const moreLineHeight = $more ? $more.getBoundingClientRect().height + gap : 0;
|
|
181
|
+
if ($more) { $more.style.display = 'none'; }
|
|
182
|
+
|
|
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;
|
|
191
|
+
} else {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Apply visibility
|
|
197
|
+
$pills.forEach(($p, i) => {
|
|
198
|
+
$p.style.display = i < maxVisible ? '' : 'none';
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if ($more) {
|
|
202
|
+
const hidden = $pills.length - maxVisible;
|
|
203
|
+
if (hidden > 0) {
|
|
204
|
+
$more.textContent = `+${hidden} more`;
|
|
205
|
+
$more.style.display = '';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
129
211
|
// ============================================
|
|
130
212
|
// Week View
|
|
131
213
|
// ============================================
|
|
@@ -141,12 +223,6 @@ export default class CalendarRenderer {
|
|
|
141
223
|
});
|
|
142
224
|
html += '</div>';
|
|
143
225
|
|
|
144
|
-
html += '<div class="calendar-week-allday"><div class="calendar-week-time-label">all-day</div>';
|
|
145
|
-
weekDates.forEach((date) => {
|
|
146
|
-
html += `<div class="calendar-cell" data-date="${formatDateUTC(date)}" data-allday="true" style="min-height:auto;border-bottom:none;"></div>`;
|
|
147
|
-
});
|
|
148
|
-
html += '</div>';
|
|
149
|
-
|
|
150
226
|
html += '<div class="calendar-week-body"><div class="calendar-week-time-col">';
|
|
151
227
|
hours.forEach((hour) => { html += `<div class="calendar-week-time-label">${hour.label}</div>`; });
|
|
152
228
|
html += '</div>';
|
|
@@ -277,7 +353,7 @@ export default class CalendarRenderer {
|
|
|
277
353
|
html += `<tr class="calendar-list-date-header"><td colspan="4">${dayName}, ${monthName} ${d.getUTCDate()}, ${d.getUTCFullYear()}${todayBadge}</td></tr>`;
|
|
278
354
|
}
|
|
279
355
|
|
|
280
|
-
const timeStr = this.
|
|
356
|
+
const timeStr = this._formatLocalTime(campaign.sendAt);
|
|
281
357
|
const name = (campaign.settings && campaign.settings.name) || 'Untitled';
|
|
282
358
|
const statusStyle = core.campaignStatusStyle(campaign);
|
|
283
359
|
const isRecurring = core.isRecurring(campaign);
|
|
@@ -320,7 +396,7 @@ export default class CalendarRenderer {
|
|
|
320
396
|
|
|
321
397
|
_renderEventPill(campaign) {
|
|
322
398
|
const core = this.core;
|
|
323
|
-
const timeStr = this.
|
|
399
|
+
const timeStr = this._formatLocalTime(campaign.sendAt);
|
|
324
400
|
const color = core.campaignColor(campaign);
|
|
325
401
|
const statusStyle = core.campaignStatusStyle(campaign);
|
|
326
402
|
const name = (campaign.settings && campaign.settings.name) || 'Untitled';
|
|
@@ -356,11 +432,11 @@ export default class CalendarRenderer {
|
|
|
356
432
|
|
|
357
433
|
_renderTimeEvent(campaign, layout) {
|
|
358
434
|
const core = this.core;
|
|
359
|
-
const
|
|
360
|
-
const [hours, minutes] =
|
|
435
|
+
const utcTime = formatTimeUTC(campaign.sendAt);
|
|
436
|
+
const [hours, minutes] = utcTime.split(':').map(Number);
|
|
361
437
|
const topPx = (hours * 60 + minutes);
|
|
362
438
|
const heightPx = Math.max(core.campaignDuration(), 15);
|
|
363
|
-
const timeStr = this.
|
|
439
|
+
const timeStr = this._formatLocalTime(campaign.sendAt);
|
|
364
440
|
const color = core.campaignColor(campaign);
|
|
365
441
|
const statusStyle = core.campaignStatusStyle(campaign);
|
|
366
442
|
const name = (campaign.settings && campaign.settings.name) || 'Untitled';
|
|
@@ -452,7 +528,7 @@ export default class CalendarRenderer {
|
|
|
452
528
|
// ============================================
|
|
453
529
|
_startNowLine() {
|
|
454
530
|
clearInterval(this._nowLineInterval);
|
|
455
|
-
if (this.core.viewMode !== 'day' && this.core.viewMode !== 'week') {
|
|
531
|
+
if (this.core.viewMode !== 'day' && this.core.viewMode !== 'week' && this.core.viewMode !== 'month') {
|
|
456
532
|
return;
|
|
457
533
|
}
|
|
458
534
|
this._updateNowLine();
|
|
@@ -466,6 +542,7 @@ export default class CalendarRenderer {
|
|
|
466
542
|
const todayStr = formatDateUTC(now);
|
|
467
543
|
const minutesSinceMidnight = now.getUTCHours() * 60 + now.getUTCMinutes();
|
|
468
544
|
|
|
545
|
+
// Week/day views: absolute position in px (1px per minute)
|
|
469
546
|
const $cols = this.$grid.querySelectorAll(
|
|
470
547
|
`.calendar-week-day-col[data-date="${todayStr}"], .calendar-day-col[data-date="${todayStr}"]`
|
|
471
548
|
);
|
|
@@ -476,6 +553,24 @@ export default class CalendarRenderer {
|
|
|
476
553
|
$line.style.top = `${minutesSinceMidnight}px`;
|
|
477
554
|
$col.appendChild($line);
|
|
478
555
|
});
|
|
556
|
+
|
|
557
|
+
// Month view: position on the grid container so the dot isn't clipped by cell overflow
|
|
558
|
+
if (this.core.viewMode === 'month') {
|
|
559
|
+
const $cell = this.$grid.querySelector(`.calendar-cell[data-date="${todayStr}"]`);
|
|
560
|
+
if ($cell) {
|
|
561
|
+
const gridRect = this.$grid.getBoundingClientRect();
|
|
562
|
+
const cellRect = $cell.getBoundingClientRect();
|
|
563
|
+
const pct = minutesSinceMidnight / 1440;
|
|
564
|
+
const topPx = (cellRect.top - gridRect.top) + (cellRect.height * pct);
|
|
565
|
+
|
|
566
|
+
const $line = document.createElement('div');
|
|
567
|
+
$line.className = 'calendar-now-line';
|
|
568
|
+
$line.style.top = `${topPx}px`;
|
|
569
|
+
$line.style.left = `${cellRect.left - gridRect.left}px`;
|
|
570
|
+
$line.style.width = `${cellRect.width}px`;
|
|
571
|
+
this.$grid.appendChild($line);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
479
574
|
}
|
|
480
575
|
|
|
481
576
|
// ============================================
|
|
@@ -679,5 +774,14 @@ export default class CalendarRenderer {
|
|
|
679
774
|
const display = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
680
775
|
return `${display}${m > 0 ? ':' + String(m).padStart(2, '0') : ''}${period}`;
|
|
681
776
|
}
|
|
777
|
+
|
|
778
|
+
_formatLocalTime(sendAt) {
|
|
779
|
+
const d = new Date(sendAt * 1000);
|
|
780
|
+
const h = d.getHours();
|
|
781
|
+
const m = d.getMinutes();
|
|
782
|
+
const period = h >= 12 ? 'p' : 'a';
|
|
783
|
+
const display = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
784
|
+
return `${display}${m > 0 ? ':' + String(m).padStart(2, '0') : ''}${period}`;
|
|
785
|
+
}
|
|
682
786
|
}
|
|
683
787
|
|