ultimate-jekyll-manager 1.0.2 → 1.0.4
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 +4 -0
- package/CLAUDE.md +64 -1
- package/TODO.md +13 -0
- package/dist/assets/css/pages/admin/calendar/index.scss +212 -18
- package/dist/assets/js/pages/admin/calendar/calendar-core.js +535 -95
- package/dist/assets/js/pages/admin/calendar/calendar-events.js +631 -124
- package/dist/assets/js/pages/admin/calendar/calendar-renderer.js +238 -69
- package/dist/assets/js/pages/admin/calendar/campaign-preview.js +100 -0
- package/dist/assets/js/pages/admin/calendar/index.js +3 -16
- package/dist/assets/js/pages/contact/index.js +5 -1
- package/dist/defaults/dist/_includes/admin/sections/sidebar.json +0 -34
- package/dist/defaults/dist/_includes/admin/sections/topbar.json +0 -34
- package/dist/defaults/dist/_includes/themes/classy/backend/sections/topbar.html +1 -72
- package/dist/defaults/dist/_includes/themes/classy/frontend/sections/nav.html +7 -140
- package/dist/defaults/dist/_includes/themes/classy/global/sections/account.html +72 -0
- package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +442 -159
- package/dist/defaults/src/_includes/backend/sections/topbar.json +0 -34
- package/dist/defaults/src/_includes/frontend/sections/nav.json +0 -34
- package/dist/defaults/src/_includes/global/sections/account.json +36 -0
- package/package.json +2 -1
- package/dist/assets/js/pages/admin/notifications/index.js +0 -53
- package/dist/assets/js/pages/admin/notifications/new/index.js +0 -492
- package/dist/defaults/dist/_layouts/blueprint/admin/newsletters/index.html +0 -59
- package/dist/defaults/dist/_layouts/blueprint/admin/newsletters/new.html +0 -46
- package/dist/defaults/dist/_layouts/blueprint/admin/notifications/index.html +0 -103
- package/dist/defaults/dist/_layouts/blueprint/admin/notifications/new.html +0 -399
- package/dist/defaults/dist/pages/admin/newsletters/index.html +0 -7
- package/dist/defaults/dist/pages/admin/newsletters/new.html +0 -7
- package/dist/defaults/dist/pages/admin/notifications/index.html +0 -7
- package/dist/defaults/dist/pages/admin/notifications/new.html +0 -7
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Calendar Renderer
|
|
3
3
|
* Renders the toolbar and calendar grid for all 4 view modes.
|
|
4
|
+
* Campaigns are color-coded by type (email=blue, push=green) and
|
|
5
|
+
* styled by status (pending=normal, sent=faded+checkmark, failed=red).
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
|
|
7
|
-
import {
|
|
9
|
+
import { escapeHtml } from '__main_assets__/js/libs/admin-helpers.js';
|
|
10
|
+
import { VIEW_MODES, DAY_ABBREVS, MONTH_NAMES, TYPE_COLORS, STATUS_STYLES, DISPLAY_TYPES } from './calendar-core.js';
|
|
8
11
|
|
|
9
12
|
export default class CalendarRenderer {
|
|
10
13
|
constructor(core) {
|
|
@@ -80,6 +83,7 @@ export default class CalendarRenderer {
|
|
|
80
83
|
week: 'calendar-week',
|
|
81
84
|
month: 'calendar',
|
|
82
85
|
year: 'grid-2',
|
|
86
|
+
list: 'table-list',
|
|
83
87
|
};
|
|
84
88
|
return getPrerenderedIcon(icons[mode], 'fa-sm');
|
|
85
89
|
}
|
|
@@ -101,6 +105,9 @@ export default class CalendarRenderer {
|
|
|
101
105
|
case 'year':
|
|
102
106
|
this._renderYearView();
|
|
103
107
|
break;
|
|
108
|
+
case 'list':
|
|
109
|
+
this._renderListView();
|
|
110
|
+
break;
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
|
|
@@ -125,7 +132,7 @@ export default class CalendarRenderer {
|
|
|
125
132
|
cells.forEach((cell) => {
|
|
126
133
|
const dateStr = core.formatDate(cell.date);
|
|
127
134
|
const isToday = core.isToday(cell.date);
|
|
128
|
-
const
|
|
135
|
+
const campaigns = core.getCampaignsForDate(dateStr);
|
|
129
136
|
|
|
130
137
|
let classes = 'calendar-cell';
|
|
131
138
|
if (isToday) {
|
|
@@ -140,12 +147,12 @@ export default class CalendarRenderer {
|
|
|
140
147
|
html += '<div class="calendar-cell-events">';
|
|
141
148
|
|
|
142
149
|
const maxVisible = 3;
|
|
143
|
-
|
|
144
|
-
html += this._renderEventPill(
|
|
150
|
+
campaigns.slice(0, maxVisible).forEach((campaign) => {
|
|
151
|
+
html += this._renderEventPill(campaign);
|
|
145
152
|
});
|
|
146
153
|
|
|
147
|
-
if (
|
|
148
|
-
html += `<div class="calendar-cell-more" data-date="${dateStr}">+${
|
|
154
|
+
if (campaigns.length > maxVisible) {
|
|
155
|
+
html += `<div class="calendar-cell-more" data-date="${dateStr}">+${campaigns.length - maxVisible} more</div>`;
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
html += '</div></div>';
|
|
@@ -204,7 +211,7 @@ export default class CalendarRenderer {
|
|
|
204
211
|
// Day columns
|
|
205
212
|
weekDates.forEach((date) => {
|
|
206
213
|
const dateStr = core.formatDate(date);
|
|
207
|
-
const
|
|
214
|
+
const campaigns = core.getCampaignsForDate(dateStr);
|
|
208
215
|
|
|
209
216
|
html += `<div class="calendar-week-day-col" data-date="${dateStr}">`;
|
|
210
217
|
hours.forEach((hour) => {
|
|
@@ -212,9 +219,9 @@ export default class CalendarRenderer {
|
|
|
212
219
|
});
|
|
213
220
|
|
|
214
221
|
// Positioned events with overlap layout
|
|
215
|
-
const layout = this._calculateOverlapLayout(
|
|
216
|
-
|
|
217
|
-
html += this._renderTimeEvent(
|
|
222
|
+
const layout = this._calculateOverlapLayout(campaigns);
|
|
223
|
+
campaigns.forEach((campaign) => {
|
|
224
|
+
html += this._renderTimeEvent(campaign, layout.get(campaign.id));
|
|
218
225
|
});
|
|
219
226
|
|
|
220
227
|
html += '</div>';
|
|
@@ -235,7 +242,7 @@ export default class CalendarRenderer {
|
|
|
235
242
|
const core = this.core;
|
|
236
243
|
const date = core.currentDate;
|
|
237
244
|
const dateStr = core.formatDate(date);
|
|
238
|
-
const
|
|
245
|
+
const campaigns = core.getCampaignsForDate(dateStr);
|
|
239
246
|
const hours = this._getHours();
|
|
240
247
|
|
|
241
248
|
let html = '';
|
|
@@ -257,9 +264,9 @@ export default class CalendarRenderer {
|
|
|
257
264
|
});
|
|
258
265
|
|
|
259
266
|
// Positioned events with overlap layout
|
|
260
|
-
const layout = this._calculateOverlapLayout(
|
|
261
|
-
|
|
262
|
-
html += this._renderTimeEvent(
|
|
267
|
+
const layout = this._calculateOverlapLayout(campaigns);
|
|
268
|
+
campaigns.forEach((campaign) => {
|
|
269
|
+
html += this._renderTimeEvent(campaign, layout.get(campaign.id));
|
|
263
270
|
});
|
|
264
271
|
|
|
265
272
|
html += '</div></div>';
|
|
@@ -303,13 +310,13 @@ export default class CalendarRenderer {
|
|
|
303
310
|
const date = new Date(year, month, d);
|
|
304
311
|
const dateStr = core.formatDate(date);
|
|
305
312
|
const isToday = core.isToday(date);
|
|
306
|
-
const
|
|
313
|
+
const hasCampaigns = core.getCampaignsForDate(dateStr).length > 0;
|
|
307
314
|
|
|
308
315
|
let classes = 'calendar-mini-day';
|
|
309
316
|
if (isToday) {
|
|
310
317
|
classes += ' calendar-cell--today';
|
|
311
318
|
}
|
|
312
|
-
if (
|
|
319
|
+
if (hasCampaigns) {
|
|
313
320
|
classes += ' has-events';
|
|
314
321
|
}
|
|
315
322
|
|
|
@@ -334,28 +341,156 @@ export default class CalendarRenderer {
|
|
|
334
341
|
}
|
|
335
342
|
|
|
336
343
|
// ============================================
|
|
337
|
-
//
|
|
344
|
+
// List View
|
|
338
345
|
// ============================================
|
|
339
|
-
|
|
340
|
-
const
|
|
346
|
+
_renderListView() {
|
|
347
|
+
const core = this.core;
|
|
348
|
+
const campaigns = core.getAllCampaignsSorted();
|
|
349
|
+
|
|
350
|
+
if (campaigns.length === 0) {
|
|
351
|
+
this.$grid.innerHTML = `
|
|
352
|
+
<div class="calendar-list-empty">
|
|
353
|
+
<div class="text-center">
|
|
354
|
+
<p>No campaigns in this date range</p>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
`;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let html = '<div class="calendar-list"><table>';
|
|
362
|
+
|
|
363
|
+
let currentDateStr = null;
|
|
364
|
+
|
|
365
|
+
campaigns.forEach((campaign) => {
|
|
366
|
+
const dateStr = core._campaignDate(campaign);
|
|
367
|
+
|
|
368
|
+
// Date group header
|
|
369
|
+
if (dateStr !== currentDateStr) {
|
|
370
|
+
currentDateStr = dateStr;
|
|
371
|
+
const d = new Date(campaign.sendAt * 1000);
|
|
372
|
+
const dayName = DAY_ABBREVS[d.getDay()];
|
|
373
|
+
const monthName = MONTH_NAMES[d.getMonth()].slice(0, 3);
|
|
374
|
+
const isToday = core.isToday(d);
|
|
375
|
+
const todayBadge = isToday ? ' <span class="badge bg-danger ms-2">Today</span>' : '';
|
|
376
|
+
|
|
377
|
+
html += `<tr class="calendar-list-date-header"><td colspan="4">${dayName}, ${monthName} ${d.getDate()}, ${d.getFullYear()}${todayBadge}</td></tr>`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Campaign row
|
|
381
|
+
const time = this._formatTime(core.campaignTime(campaign));
|
|
382
|
+
const name = (campaign.settings && campaign.settings.name) || 'Untitled';
|
|
383
|
+
const color = core.campaignColor(campaign);
|
|
384
|
+
const statusStyle = core.campaignStatusStyle(campaign);
|
|
385
|
+
const isRecurring = campaign.recurrence || campaign.recurringId || campaign._virtual;
|
|
386
|
+
|
|
387
|
+
const typeBadge = campaign.type === 'email'
|
|
388
|
+
? `<span class="badge" style="background-color: ${TYPE_COLORS.email}">${getPrerenderedIcon('envelope', 'fa-xs me-1')} Email</span>`
|
|
389
|
+
: `<span class="badge" style="background-color: ${TYPE_COLORS.push}">${getPrerenderedIcon('bell', 'fa-xs me-1')} Push</span>`;
|
|
390
|
+
|
|
391
|
+
let statusBadge = '';
|
|
392
|
+
if (campaign.status === 'sent') {
|
|
393
|
+
statusBadge = `<span class="badge bg-success">${getPrerenderedIcon('circle-check', 'fa-xs me-1')} Sent</span>`;
|
|
394
|
+
} else if (campaign.status === 'failed') {
|
|
395
|
+
statusBadge = `<span class="badge bg-danger">${getPrerenderedIcon('triangle-exclamation', 'fa-xs me-1')} Failed</span>`;
|
|
396
|
+
} else {
|
|
397
|
+
statusBadge = '<span class="badge bg-secondary">Pending</span>';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const recurringIcon = isRecurring ? getPrerenderedIcon('repeat', 'fa-xs me-1 text-muted') : '';
|
|
401
|
+
|
|
402
|
+
html += `
|
|
403
|
+
<tr class="calendar-list-row" data-campaign-id="${campaign.id}" style="opacity: ${statusStyle.opacity}">
|
|
404
|
+
<td style="width: 60px">${time}</td>
|
|
405
|
+
<td>${recurringIcon}${escapeHtml(name)}</td>
|
|
406
|
+
<td style="width: 80px">${typeBadge}</td>
|
|
407
|
+
<td style="width: 80px">${statusBadge}</td>
|
|
408
|
+
</tr>
|
|
409
|
+
`;
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
html += '</table></div>';
|
|
413
|
+
|
|
414
|
+
this.$grid.innerHTML = html;
|
|
415
|
+
|
|
416
|
+
// Bind row clicks
|
|
417
|
+
this.$grid.querySelectorAll('.calendar-list-row').forEach(($row) => {
|
|
418
|
+
$row.addEventListener('click', (e) => {
|
|
419
|
+
e.stopPropagation();
|
|
420
|
+
const id = $row.dataset.campaignId;
|
|
421
|
+
core.eventsManager.openEditModal(id);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ============================================
|
|
427
|
+
// Campaign Rendering Helpers
|
|
428
|
+
// ============================================
|
|
429
|
+
_renderEventPill(campaign) {
|
|
430
|
+
const core = this.core;
|
|
431
|
+
const timeStr = this._formatTime(core.campaignTime(campaign));
|
|
432
|
+
const color = core.campaignColor(campaign);
|
|
433
|
+
const statusStyle = core.campaignStatusStyle(campaign);
|
|
434
|
+
const name = (campaign.settings && campaign.settings.name) || 'Untitled';
|
|
435
|
+
const isEditable = core.isEditable(campaign);
|
|
436
|
+
const isRecurring = campaign.recurrence || campaign.recurringId || campaign._virtual;
|
|
437
|
+
const statusIcon = statusStyle.icon
|
|
438
|
+
? getPrerenderedIcon(statusStyle.icon, 'fa-xs')
|
|
439
|
+
: '';
|
|
440
|
+
const typeIcon = campaign.type === 'email'
|
|
441
|
+
? getPrerenderedIcon('envelope', 'fa-xs')
|
|
442
|
+
: getPrerenderedIcon('bell', 'fa-xs');
|
|
443
|
+
const recurringIcon = isRecurring
|
|
444
|
+
? getPrerenderedIcon('repeat', 'fa-xs')
|
|
445
|
+
: '';
|
|
446
|
+
|
|
447
|
+
let pillClass = 'calendar-event';
|
|
448
|
+
if (campaign.status === 'failed') {
|
|
449
|
+
pillClass += ' calendar-event--failed';
|
|
450
|
+
}
|
|
451
|
+
if (campaign.status === 'sent') {
|
|
452
|
+
pillClass += ' calendar-event--sent';
|
|
453
|
+
}
|
|
454
|
+
if (campaign._virtual) {
|
|
455
|
+
pillClass += ' calendar-event--virtual';
|
|
456
|
+
}
|
|
457
|
+
if (isRecurring && !campaign._virtual) {
|
|
458
|
+
pillClass += ' calendar-event--recurring';
|
|
459
|
+
}
|
|
341
460
|
|
|
342
461
|
return `
|
|
343
|
-
<div class="
|
|
344
|
-
data-
|
|
345
|
-
draggable="true"
|
|
346
|
-
style="background-color: ${
|
|
347
|
-
title="${
|
|
462
|
+
<div class="${pillClass}"
|
|
463
|
+
data-campaign-id="${campaign.id}"
|
|
464
|
+
${isEditable && !campaign._virtual ? 'draggable="true"' : ''}
|
|
465
|
+
style="background-color: ${color}; opacity: ${statusStyle.opacity};"
|
|
466
|
+
title="${escapeHtml(name)}${isRecurring ? ' (recurring)' : ''}">
|
|
467
|
+
${statusIcon}
|
|
468
|
+
${recurringIcon}
|
|
348
469
|
<span class="calendar-event-time">${timeStr}</span>
|
|
349
|
-
<span class="calendar-event-
|
|
470
|
+
<span class="calendar-event-type-icon">${typeIcon}</span>
|
|
471
|
+
<span class="calendar-event-title">${escapeHtml(name)}</span>
|
|
350
472
|
</div>
|
|
351
473
|
`;
|
|
352
474
|
}
|
|
353
475
|
|
|
354
|
-
_renderTimeEvent(
|
|
355
|
-
const
|
|
476
|
+
_renderTimeEvent(campaign, layout) {
|
|
477
|
+
const core = this.core;
|
|
478
|
+
const time = core.campaignTime(campaign);
|
|
479
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
356
480
|
const topPx = (hours * 60 + minutes);
|
|
357
|
-
const heightPx = Math.max(
|
|
358
|
-
const timeStr = this._formatTime(
|
|
481
|
+
const heightPx = Math.max(core.campaignDuration(), 15);
|
|
482
|
+
const timeStr = this._formatTime(time);
|
|
483
|
+
const color = core.campaignColor(campaign);
|
|
484
|
+
const statusStyle = core.campaignStatusStyle(campaign);
|
|
485
|
+
const name = (campaign.settings && campaign.settings.name) || 'Untitled';
|
|
486
|
+
const isEditable = core.isEditable(campaign);
|
|
487
|
+
const isRecurring = campaign.recurrence || campaign.recurringId || campaign._virtual;
|
|
488
|
+
const statusIcon = statusStyle.icon
|
|
489
|
+
? getPrerenderedIcon(statusStyle.icon, 'fa-xs me-1')
|
|
490
|
+
: '';
|
|
491
|
+
const recurringIcon = isRecurring
|
|
492
|
+
? getPrerenderedIcon('repeat', 'fa-xs me-1')
|
|
493
|
+
: '';
|
|
359
494
|
|
|
360
495
|
// Layout: column position and total columns for side-by-side overlap
|
|
361
496
|
const col = layout ? layout.col : 0;
|
|
@@ -367,34 +502,52 @@ export default class CalendarRenderer {
|
|
|
367
502
|
? `left: ${leftPct}%; width: calc(${widthPct}% - 2px);`
|
|
368
503
|
: '';
|
|
369
504
|
|
|
505
|
+
let eventClass = 'calendar-week-event';
|
|
506
|
+
if (campaign.status === 'failed') {
|
|
507
|
+
eventClass += ' calendar-event--failed';
|
|
508
|
+
}
|
|
509
|
+
if (campaign.status === 'sent') {
|
|
510
|
+
eventClass += ' calendar-event--sent';
|
|
511
|
+
}
|
|
512
|
+
if (campaign._virtual) {
|
|
513
|
+
eventClass += ' calendar-event--virtual';
|
|
514
|
+
}
|
|
515
|
+
if (isRecurring && !campaign._virtual) {
|
|
516
|
+
eventClass += ' calendar-event--recurring';
|
|
517
|
+
}
|
|
518
|
+
|
|
370
519
|
return `
|
|
371
|
-
<div class="
|
|
372
|
-
data-
|
|
373
|
-
draggable="true"
|
|
374
|
-
style="background-color: ${
|
|
375
|
-
title="${
|
|
376
|
-
<strong>${timeStr}</strong> ${
|
|
520
|
+
<div class="${eventClass}"
|
|
521
|
+
data-campaign-id="${campaign.id}"
|
|
522
|
+
${isEditable && !campaign._virtual ? 'draggable="true"' : ''}
|
|
523
|
+
style="background-color: ${color}; opacity: ${statusStyle.opacity}; top: ${topPx}px; height: ${heightPx}px; ${sizeStyle}"
|
|
524
|
+
title="${escapeHtml(name)}${isRecurring ? ' (recurring)' : ''}">
|
|
525
|
+
${statusIcon}${recurringIcon}<strong>${timeStr}</strong> ${escapeHtml(name)}
|
|
377
526
|
</div>
|
|
378
527
|
`;
|
|
379
528
|
}
|
|
380
529
|
|
|
381
530
|
/**
|
|
382
|
-
* Calculate side-by-side layout for overlapping
|
|
383
|
-
* Returns a Map of
|
|
531
|
+
* Calculate side-by-side layout for overlapping campaigns.
|
|
532
|
+
* Returns a Map of campaignId → { col, totalCols }
|
|
384
533
|
*/
|
|
385
|
-
_calculateOverlapLayout(
|
|
534
|
+
_calculateOverlapLayout(campaigns) {
|
|
386
535
|
const layout = new Map();
|
|
387
536
|
|
|
388
|
-
if (
|
|
537
|
+
if (campaigns.length === 0) {
|
|
389
538
|
return layout;
|
|
390
539
|
}
|
|
391
540
|
|
|
541
|
+
const core = this.core;
|
|
542
|
+
const duration = core.campaignDuration();
|
|
543
|
+
|
|
392
544
|
// Convert to intervals
|
|
393
|
-
const intervals =
|
|
394
|
-
const
|
|
545
|
+
const intervals = campaigns.map((c) => {
|
|
546
|
+
const time = core.campaignTime(c);
|
|
547
|
+
const [h, m] = time.split(':').map(Number);
|
|
395
548
|
const start = h * 60 + m;
|
|
396
|
-
const end = start +
|
|
397
|
-
return { id:
|
|
549
|
+
const end = start + duration;
|
|
550
|
+
return { id: c.id, start, end };
|
|
398
551
|
}).sort((a, b) => a.start - b.start || a.end - b.end);
|
|
399
552
|
|
|
400
553
|
// Group overlapping events into clusters
|
|
@@ -417,7 +570,6 @@ export default class CalendarRenderer {
|
|
|
417
570
|
const columns = [];
|
|
418
571
|
|
|
419
572
|
cluster.forEach((interval) => {
|
|
420
|
-
// Find the first column where this event fits (no overlap)
|
|
421
573
|
let placed = false;
|
|
422
574
|
for (let c = 0; c < columns.length; c++) {
|
|
423
575
|
const lastInCol = columns[c];
|
|
@@ -487,13 +639,12 @@ export default class CalendarRenderer {
|
|
|
487
639
|
_bindCellClicks() {
|
|
488
640
|
const core = this.core;
|
|
489
641
|
|
|
490
|
-
// Month view: single click → day view, double click → create
|
|
491
|
-
// Debounce single click to avoid firing when double clicking
|
|
642
|
+
// Month view: single click → day view, double click → create campaign
|
|
492
643
|
let clickTimer = null;
|
|
493
644
|
|
|
494
645
|
this.$grid.querySelectorAll('.calendar-cell').forEach(($cell) => {
|
|
495
646
|
$cell.addEventListener('click', (e) => {
|
|
496
|
-
if (e.target.closest('
|
|
647
|
+
if (e.target.closest('[data-campaign-id]')) {
|
|
497
648
|
return;
|
|
498
649
|
}
|
|
499
650
|
|
|
@@ -511,7 +662,7 @@ export default class CalendarRenderer {
|
|
|
511
662
|
});
|
|
512
663
|
|
|
513
664
|
$cell.addEventListener('dblclick', (e) => {
|
|
514
|
-
if (e.target.closest('
|
|
665
|
+
if (e.target.closest('[data-campaign-id]')) {
|
|
515
666
|
return;
|
|
516
667
|
}
|
|
517
668
|
|
|
@@ -526,10 +677,10 @@ export default class CalendarRenderer {
|
|
|
526
677
|
});
|
|
527
678
|
});
|
|
528
679
|
|
|
529
|
-
// Time slots (week/day views): double click only to create
|
|
680
|
+
// Time slots (week/day views): double click only to create campaign
|
|
530
681
|
this.$grid.querySelectorAll('.calendar-week-time-slot').forEach(($slot) => {
|
|
531
682
|
$slot.addEventListener('dblclick', (e) => {
|
|
532
|
-
if (e.target.closest('
|
|
683
|
+
if (e.target.closest('[data-campaign-id]')) {
|
|
533
684
|
return;
|
|
534
685
|
}
|
|
535
686
|
|
|
@@ -559,24 +710,24 @@ export default class CalendarRenderer {
|
|
|
559
710
|
}
|
|
560
711
|
|
|
561
712
|
_bindEventPillClicks() {
|
|
562
|
-
this.$grid.querySelectorAll('[data-
|
|
713
|
+
this.$grid.querySelectorAll('[data-campaign-id]').forEach(($pill) => {
|
|
563
714
|
$pill.addEventListener('click', (e) => {
|
|
564
715
|
e.stopPropagation();
|
|
565
|
-
const id = $pill.dataset.
|
|
716
|
+
const id = $pill.dataset.campaignId;
|
|
566
717
|
this.core.eventsManager.openEditModal(id);
|
|
567
718
|
});
|
|
568
719
|
});
|
|
569
720
|
}
|
|
570
721
|
|
|
571
722
|
_bindDragAndDrop() {
|
|
572
|
-
|
|
573
|
-
|
|
723
|
+
const core = this.core;
|
|
724
|
+
|
|
725
|
+
// Drag start on pending campaign pills only
|
|
726
|
+
this.$grid.querySelectorAll('[data-campaign-id][draggable]').forEach(($pill) => {
|
|
574
727
|
$pill.addEventListener('dragstart', (e) => {
|
|
575
|
-
e.dataTransfer.setData('text/plain', $pill.dataset.
|
|
728
|
+
e.dataTransfer.setData('text/plain', $pill.dataset.campaignId);
|
|
576
729
|
e.dataTransfer.effectAllowed = 'move';
|
|
577
730
|
$pill.classList.add('calendar-event--dragging');
|
|
578
|
-
// Disable pointer-events on all events so drops pass through to time slots
|
|
579
|
-
// Use requestAnimationFrame so the drag has started before we disable pointer-events
|
|
580
731
|
requestAnimationFrame(() => {
|
|
581
732
|
this.$grid.classList.add('calendar-grid--dragging');
|
|
582
733
|
});
|
|
@@ -585,7 +736,6 @@ export default class CalendarRenderer {
|
|
|
585
736
|
$pill.addEventListener('dragend', () => {
|
|
586
737
|
$pill.classList.remove('calendar-event--dragging');
|
|
587
738
|
this.$grid.classList.remove('calendar-grid--dragging');
|
|
588
|
-
// Clear all drag-over states
|
|
589
739
|
this.$grid.querySelectorAll('.calendar-cell--drag-over').forEach(($el) => {
|
|
590
740
|
$el.classList.remove('calendar-cell--drag-over');
|
|
591
741
|
});
|
|
@@ -609,20 +759,44 @@ export default class CalendarRenderer {
|
|
|
609
759
|
e.preventDefault();
|
|
610
760
|
$cell.classList.remove('calendar-cell--drag-over');
|
|
611
761
|
|
|
612
|
-
const
|
|
762
|
+
const campaignId = e.dataTransfer.getData('text/plain');
|
|
613
763
|
const newDate = $cell.dataset.date;
|
|
614
|
-
if (!
|
|
764
|
+
if (!campaignId || !newDate) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Only allow rescheduling editable campaigns (pending one-offs and recurring templates)
|
|
769
|
+
const campaign = core.getCampaign(campaignId);
|
|
770
|
+
if (!campaign || !core.isEditable(campaign)) {
|
|
615
771
|
return;
|
|
616
772
|
}
|
|
617
773
|
|
|
618
|
-
|
|
774
|
+
// Build new sendAt from drop target
|
|
775
|
+
const parts = newDate.split('-');
|
|
776
|
+
const d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
|
|
619
777
|
|
|
620
|
-
// If dropping on a time slot,
|
|
778
|
+
// If dropping on a time slot, use that hour
|
|
621
779
|
if ($cell.dataset.hour) {
|
|
622
|
-
|
|
780
|
+
d.setHours(parseInt($cell.dataset.hour), 0, 0, 0);
|
|
781
|
+
} else {
|
|
782
|
+
// Keep original time
|
|
783
|
+
const originalDate = new Date(campaign.sendAt * 1000);
|
|
784
|
+
d.setHours(originalDate.getHours(), originalDate.getMinutes(), 0, 0);
|
|
623
785
|
}
|
|
624
786
|
|
|
625
|
-
|
|
787
|
+
const newSendAtUNIX = Math.floor(d.getTime() / 1000);
|
|
788
|
+
const newSendAtISO = d.toISOString();
|
|
789
|
+
|
|
790
|
+
// Optimistic update: render immediately, rollback on failure
|
|
791
|
+
const rollback = core.optimisticUpdateSendAt(campaignId, newSendAtUNIX);
|
|
792
|
+
|
|
793
|
+
core.eventsManager.rescheduleCampaign(campaignId, newSendAtISO)
|
|
794
|
+
.catch((err) => {
|
|
795
|
+
console.error('Failed to reschedule campaign:', err);
|
|
796
|
+
if (rollback) {
|
|
797
|
+
rollback();
|
|
798
|
+
}
|
|
799
|
+
});
|
|
626
800
|
});
|
|
627
801
|
});
|
|
628
802
|
}
|
|
@@ -653,9 +827,4 @@ export default class CalendarRenderer {
|
|
|
653
827
|
return `${display}${m > 0 ? ':' + String(m).padStart(2, '0') : ''}${period}`;
|
|
654
828
|
}
|
|
655
829
|
|
|
656
|
-
_escapeHtml(str) {
|
|
657
|
-
const div = document.createElement('div');
|
|
658
|
-
div.textContent = str;
|
|
659
|
-
return div.innerHTML;
|
|
660
|
-
}
|
|
661
830
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaign Preview
|
|
3
|
+
* Renders email (markdown) and push (mobile frame) previews
|
|
4
|
+
* for the campaign editor modal.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
|
|
8
|
+
import { escapeHtml } from '__main_assets__/js/libs/admin-helpers.js';
|
|
9
|
+
|
|
10
|
+
// Lazy-loaded markdown-it instance
|
|
11
|
+
let md = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render email campaign preview HTML.
|
|
15
|
+
* Lazy-loads markdown-it on first call.
|
|
16
|
+
*/
|
|
17
|
+
async function renderEmailPreview(formData) {
|
|
18
|
+
const campaign = formData.campaign || {};
|
|
19
|
+
const subject = campaign.subject || '';
|
|
20
|
+
const preheader = campaign.preheader || '';
|
|
21
|
+
const content = campaign.content || '';
|
|
22
|
+
|
|
23
|
+
// Lazy-load markdown-it
|
|
24
|
+
if (!md) {
|
|
25
|
+
const MarkdownIt = (await import('markdown-it')).default;
|
|
26
|
+
md = new MarkdownIt({ html: true, breaks: true, linkify: true });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const renderedContent = content ? md.render(content) : '<p class="text-muted">No content yet</p>';
|
|
30
|
+
|
|
31
|
+
return `
|
|
32
|
+
<div class="email-preview">
|
|
33
|
+
<div class="email-preview-header">
|
|
34
|
+
<div class="email-preview-subject">${escapeHtml(subject) || '<span class="text-muted">No subject</span>'}</div>
|
|
35
|
+
${preheader ? `<div class="email-preview-preheader">${escapeHtml(preheader)}</div>` : ''}
|
|
36
|
+
</div>
|
|
37
|
+
<div class="email-preview-body">${renderedContent}</div>
|
|
38
|
+
<div class="email-preview-disclaimer text-muted small mt-3">
|
|
39
|
+
${getPrerenderedIcon('triangle-exclamation', 'fa-xs me-1')}
|
|
40
|
+
Preview shows formatted content. Final email may vary by template.
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Render push notification preview HTML (mobile device frame).
|
|
48
|
+
*/
|
|
49
|
+
function renderPushPreview(formData) {
|
|
50
|
+
const campaign = formData.campaign || {};
|
|
51
|
+
const name = campaign.name || 'Notification Title';
|
|
52
|
+
const subject = campaign.subject || 'Notification body text...';
|
|
53
|
+
const icon = campaign.icon || '';
|
|
54
|
+
|
|
55
|
+
const iconSrc = icon && icon.match(/^https?:\/\/.+/)
|
|
56
|
+
? escapeHtml(icon)
|
|
57
|
+
: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="50" height="50"%3E%3Crect width="50" height="50" fill="%236c757d" rx="8"/%3E%3C/svg%3E';
|
|
58
|
+
|
|
59
|
+
const clickAction = campaign.clickAction || '';
|
|
60
|
+
|
|
61
|
+
return `
|
|
62
|
+
<div class="push-preview-frame">
|
|
63
|
+
<div class="push-preview-screen">
|
|
64
|
+
<div class="push-preview-status-bar">
|
|
65
|
+
<span>9:41 AM</span>
|
|
66
|
+
<span>
|
|
67
|
+
${getPrerenderedIcon('wifi', 'fa-sm me-1')}
|
|
68
|
+
${getPrerenderedIcon('battery-full', 'fa-sm')}
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="push-preview-notification"
|
|
72
|
+
${clickAction ? `role="button" title="Click to test: ${escapeHtml(clickAction)}"` : ''}
|
|
73
|
+
data-click-action="${escapeHtml(clickAction)}">
|
|
74
|
+
<div class="d-flex align-items-start">
|
|
75
|
+
<img src="${iconSrc}"
|
|
76
|
+
class="rounded me-2"
|
|
77
|
+
width="50"
|
|
78
|
+
height="50"
|
|
79
|
+
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2250%22 height=%2250%22%3E%3Crect width=%2250%22 height=%2250%22 fill=%22%236c757d%22 rx=%228%22/%3E%3C/svg%3E'">
|
|
80
|
+
<div class="flex-fill">
|
|
81
|
+
<div class="fw-semibold small">${escapeHtml(name)}</div>
|
|
82
|
+
<div class="small text-muted mt-1">${escapeHtml(subject)}</div>
|
|
83
|
+
<div class="small text-muted mt-1">
|
|
84
|
+
${getPrerenderedIcon('clock', 'fa-xs me-1')}
|
|
85
|
+
Now
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="mt-2 opacity-50">
|
|
91
|
+
<div class="bg-body-secondary rounded p-2 small">
|
|
92
|
+
<div class="text-muted">Earlier notifications...</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { renderEmailPreview, renderPushPreview };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Admin Calendar Page JavaScript
|
|
2
|
+
* Admin Marketing Calendar Page JavaScript
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
// Libraries
|
|
@@ -35,25 +35,13 @@ export default (Manager) => {
|
|
|
35
35
|
|
|
36
36
|
// Initialize the calendar
|
|
37
37
|
function initialize() {
|
|
38
|
-
const core = new CalendarCore();
|
|
38
|
+
const core = new CalendarCore(webManager);
|
|
39
39
|
const renderer = new CalendarRenderer(core);
|
|
40
40
|
const events = new CalendarEvents(core, webManager);
|
|
41
41
|
|
|
42
42
|
core.setRenderer(renderer);
|
|
43
43
|
core.setEventsManager(events);
|
|
44
44
|
core.initialize();
|
|
45
|
-
|
|
46
|
-
// Expose public API
|
|
47
|
-
window.calendarAPI = {
|
|
48
|
-
addEvent: (data) => core.addEvent(data),
|
|
49
|
-
updateEvent: (id, changes) => core.updateEvent(id, changes),
|
|
50
|
-
removeEvent: (id) => core.removeEvent(id),
|
|
51
|
-
getEvent: (id) => core.getEvent(id),
|
|
52
|
-
getEvents: (filter) => core.getEvents(filter),
|
|
53
|
-
navigate: (direction) => core.navigate(direction),
|
|
54
|
-
setView: (mode) => core.setView(mode),
|
|
55
|
-
goToToday: () => core.goToToday(),
|
|
56
|
-
};
|
|
57
45
|
}
|
|
58
46
|
|
|
59
47
|
// Show unauthenticated state
|
|
@@ -62,8 +50,7 @@ function showUnauthenticated() {
|
|
|
62
50
|
$grid.innerHTML = `
|
|
63
51
|
<div class="d-flex align-items-center justify-content-center h-100 text-muted">
|
|
64
52
|
<div class="text-center">
|
|
65
|
-
<
|
|
66
|
-
<p>Sign in to view the calendar</p>
|
|
53
|
+
<p>Sign in to view the marketing calendar</p>
|
|
67
54
|
</div>
|
|
68
55
|
</div>
|
|
69
56
|
`;
|
|
@@ -66,15 +66,19 @@ function setupForm() {
|
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
68
|
// Send request using wonderful-fetch
|
|
69
|
-
await fetch(apiEndpoint, {
|
|
69
|
+
const response = await fetch(apiEndpoint, {
|
|
70
70
|
method: 'POST',
|
|
71
71
|
body: requestData,
|
|
72
72
|
response: 'json',
|
|
73
73
|
timeout: 30000,
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
console.log('Contact form submitted successfully:', response);
|
|
77
|
+
|
|
76
78
|
formManager.showSuccess('Thank you for your message! We\'ll get back to you within 24 hours.');
|
|
77
79
|
} catch (error) {
|
|
80
|
+
console.error('Contact form submission failed:', error);
|
|
81
|
+
|
|
78
82
|
// Only capture technical errors to Sentry (network, timeout, API errors)
|
|
79
83
|
if (error.message?.includes('network') || error.message?.includes('timeout') || !error.message?.includes('Failed')) {
|
|
80
84
|
webManager.sentry().captureException(new Error('Contact form submission error', { cause: error }));
|