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
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
---
|
|
18
|
+
## [1.6.5] - 2026-06-04
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Dynamic event fitting in month view.** Calendar cells now show as many events as physically fit instead of a hardcoded max of 3; "+N more" only appears when events genuinely overflow.
|
|
23
|
+
- **Local time display on calendar events.** Event pills show local time (data layer remains UTC). Editor modal shows a local date+time badge below the UTC inputs.
|
|
24
|
+
- **Hover states on event pills.** Brightness + box-shadow effect on hover for month, week, and day views.
|
|
25
|
+
- **Now line on month view.** Red progress line shows current time of day in today's cell, with dot rendered above cell borders.
|
|
26
|
+
- **Left tab accent on all event pills.** Consistent dark left border matches week/day view style.
|
|
27
|
+
- **Monthly-weekday (Nth weekday) recurrence pattern.** Calendar supports "2nd Wednesday of every month" style recurring campaigns with calendar-relative date stepping.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **Per-string translation caching replaces all-or-nothing page caching.** Each page's cache is now a `hash→translation` map (`es/pages/index.html.json`). Only strings whose content hash changed are sent to the API — unchanged strings are served from cache. Dramatically reduces API calls and cost on incremental builds.
|
|
32
|
+
- **Prompt hash mismatch now wipes page cache files** (not just meta entries) for a clean slate.
|
|
33
|
+
- **Translation stats now track cached vs new strings** instead of whole-page hit/miss.
|
|
34
|
+
- **Outside-month calendar cells** now fade only content (date number + events), keeping borders at full opacity for consistent grid lines.
|
|
35
|
+
- **Imagemin constants renamed** (`MAX_SOURCE_DIMENSION` → `IMAGE_MAX_DIMENSION`) and default max dimension lowered from 4096 to 2048.
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
- **`RECHECK_DAYS`** — per-string content hashes make age-based invalidation unnecessary.
|
|
40
|
+
- **All-day row** removed from week view (no all-day event concept in marketing calendar).
|
|
41
|
+
|
|
17
42
|
---
|
|
18
43
|
## [1.6.4] - 2026-06-03
|
|
19
44
|
|
|
@@ -79,7 +79,7 @@ $campaign-push-color: #4CAF50;
|
|
|
79
79
|
.calendar-month {
|
|
80
80
|
display: grid;
|
|
81
81
|
grid-template-columns: repeat(7, 1fr);
|
|
82
|
-
grid-
|
|
82
|
+
grid-auto-rows: 1fr;
|
|
83
83
|
flex-grow: 1;
|
|
84
84
|
min-height: 0;
|
|
85
85
|
}
|
|
@@ -88,12 +88,13 @@ $campaign-push-color: #4CAF50;
|
|
|
88
88
|
border-right: 1px solid var(--bs-border-color);
|
|
89
89
|
border-bottom: 1px solid var(--bs-border-color);
|
|
90
90
|
padding: 0.25rem;
|
|
91
|
-
overflow
|
|
92
|
-
|
|
93
|
-
min-height: 80px;
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
min-height: 0;
|
|
94
93
|
position: relative;
|
|
95
94
|
cursor: pointer;
|
|
96
95
|
transition: background-color 0.15s;
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
97
98
|
|
|
98
99
|
&:nth-child(7n) {
|
|
99
100
|
border-right: none;
|
|
@@ -120,7 +121,10 @@ $campaign-push-color: #4CAF50;
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
.calendar-cell--outside {
|
|
123
|
-
|
|
124
|
+
.calendar-cell-date,
|
|
125
|
+
.calendar-cell-events {
|
|
126
|
+
opacity: 0.35;
|
|
127
|
+
}
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
.calendar-cell--drag-over {
|
|
@@ -142,6 +146,9 @@ $campaign-push-color: #4CAF50;
|
|
|
142
146
|
display: flex;
|
|
143
147
|
flex-direction: column;
|
|
144
148
|
gap: 1px;
|
|
149
|
+
flex: 1;
|
|
150
|
+
min-height: 0;
|
|
151
|
+
overflow: hidden;
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
.calendar-cell-more {
|
|
@@ -253,7 +260,7 @@ $campaign-push-color: #4CAF50;
|
|
|
253
260
|
right: 0;
|
|
254
261
|
height: 2px;
|
|
255
262
|
background-color: $calendar-now-color;
|
|
256
|
-
z-index:
|
|
263
|
+
z-index: 3;
|
|
257
264
|
pointer-events: none;
|
|
258
265
|
|
|
259
266
|
&::before {
|
|
@@ -268,6 +275,7 @@ $campaign-push-color: #4CAF50;
|
|
|
268
275
|
}
|
|
269
276
|
}
|
|
270
277
|
|
|
278
|
+
|
|
271
279
|
.calendar-week-event {
|
|
272
280
|
position: absolute;
|
|
273
281
|
left: 2px;
|
|
@@ -280,9 +288,11 @@ $campaign-push-color: #4CAF50;
|
|
|
280
288
|
z-index: 1;
|
|
281
289
|
color: #fff;
|
|
282
290
|
border-left: 3px solid rgba(0, 0, 0, 0.2);
|
|
291
|
+
transition: filter 0.15s, box-shadow 0.15s;
|
|
283
292
|
|
|
284
293
|
&:hover {
|
|
285
|
-
|
|
294
|
+
filter: brightness(1.2);
|
|
295
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
286
296
|
}
|
|
287
297
|
}
|
|
288
298
|
|
|
@@ -402,15 +412,17 @@ $campaign-push-color: #4CAF50;
|
|
|
402
412
|
height: 1.25rem;
|
|
403
413
|
box-sizing: border-box;
|
|
404
414
|
border: 1px solid transparent;
|
|
415
|
+
border-left: 3px solid rgba(0, 0, 0, 0.2);
|
|
405
416
|
white-space: nowrap;
|
|
406
417
|
overflow: hidden;
|
|
407
418
|
text-overflow: ellipsis;
|
|
408
419
|
cursor: pointer;
|
|
409
420
|
color: #fff;
|
|
410
|
-
transition:
|
|
421
|
+
transition: filter 0.15s, box-shadow 0.15s;
|
|
411
422
|
|
|
412
423
|
&:hover {
|
|
413
|
-
|
|
424
|
+
filter: brightness(1.2);
|
|
425
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
|
414
426
|
}
|
|
415
427
|
}
|
|
416
428
|
|
|
@@ -466,7 +478,9 @@ $campaign-push-color: #4CAF50;
|
|
|
466
478
|
// Virtual recurring occurrences: dashed border, slightly transparent
|
|
467
479
|
.calendar-event--virtual {
|
|
468
480
|
border-style: dashed;
|
|
481
|
+
border-left-style: solid;
|
|
469
482
|
border-color: rgba(255, 255, 255, 0.5);
|
|
483
|
+
border-left-color: rgba(0, 0, 0, 0.2);
|
|
470
484
|
opacity: 0.7;
|
|
471
485
|
}
|
|
472
486
|
|
|
@@ -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
|
}
|
|
@@ -358,15 +358,23 @@ export default class CalendarCore {
|
|
|
358
358
|
|
|
359
359
|
/**
|
|
360
360
|
* Generate virtual occurrences using the template's sendAt as seed.
|
|
361
|
-
*
|
|
361
|
+
* Fixed-interval patterns (daily, weekly) use pure unix math.
|
|
362
|
+
* Calendar-relative patterns (monthly, monthly-weekday, quarterly, yearly)
|
|
363
|
+
* step through actual dates so they land correctly every month.
|
|
362
364
|
*/
|
|
363
365
|
_generateOccurrences(template, startUNIX, endUNIX) {
|
|
364
|
-
const
|
|
366
|
+
const { pattern } = template.recurrence;
|
|
365
367
|
const occurrences = [];
|
|
366
368
|
const seedUNIX = template.sendAt;
|
|
367
369
|
|
|
368
|
-
|
|
370
|
+
if (pattern === 'monthly-weekday') {
|
|
371
|
+
return this._generateNthWeekdayOccurrences(template, startUNIX, endUNIX);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Fixed-interval patterns
|
|
375
|
+
const interval = this._getIntervalSeconds(pattern);
|
|
369
376
|
let cursorUNIX = seedUNIX;
|
|
377
|
+
|
|
370
378
|
while (cursorUNIX > startUNIX + interval) {
|
|
371
379
|
cursorUNIX -= interval;
|
|
372
380
|
}
|
|
@@ -376,23 +384,81 @@ export default class CalendarCore {
|
|
|
376
384
|
|
|
377
385
|
let maxIterations = 400;
|
|
378
386
|
while (cursorUNIX <= endUNIX && maxIterations-- > 0) {
|
|
379
|
-
occurrences.push(
|
|
380
|
-
id: `${template.id}__virtual__${cursorUNIX}`,
|
|
381
|
-
sendAt: cursorUNIX,
|
|
382
|
-
status: 'pending',
|
|
383
|
-
type: template.type,
|
|
384
|
-
settings: template.settings,
|
|
385
|
-
recurrence: template.recurrence,
|
|
386
|
-
_virtual: true,
|
|
387
|
-
_recurringSourceId: template.id,
|
|
388
|
-
});
|
|
389
|
-
|
|
387
|
+
occurrences.push(this._buildVirtualEvent(template, cursorUNIX));
|
|
390
388
|
cursorUNIX += interval;
|
|
391
389
|
}
|
|
392
390
|
|
|
393
391
|
return occurrences;
|
|
394
392
|
}
|
|
395
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Generate Nth-weekday-of-month occurrences (e.g., 2nd Wednesday).
|
|
396
|
+
* Walks month by month, computing the actual calendar date each time.
|
|
397
|
+
*/
|
|
398
|
+
_generateNthWeekdayOccurrences(template, startUNIX, endUNIX) {
|
|
399
|
+
const { nth = 1, day: dayOfWeek = 0, hour = 0, minute = 0 } = template.recurrence;
|
|
400
|
+
const occurrences = [];
|
|
401
|
+
|
|
402
|
+
// Start scanning 2 months before the visible range
|
|
403
|
+
const startDate = new Date(startUNIX * 1000);
|
|
404
|
+
let year = startDate.getUTCFullYear();
|
|
405
|
+
let month = startDate.getUTCMonth() - 2;
|
|
406
|
+
|
|
407
|
+
let maxIterations = 100;
|
|
408
|
+
while (maxIterations-- > 0) {
|
|
409
|
+
const ts = this._nthWeekdayOfMonth(year, month, nth, dayOfWeek, hour, minute);
|
|
410
|
+
|
|
411
|
+
if (ts > endUNIX) {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (ts >= startUNIX) {
|
|
416
|
+
occurrences.push(this._buildVirtualEvent(template, ts));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
month++;
|
|
420
|
+
if (month > 11) {
|
|
421
|
+
month = 0;
|
|
422
|
+
year++;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return occurrences;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Find the Nth occurrence of a weekday in a given month (UTC).
|
|
431
|
+
* Returns unix timestamp.
|
|
432
|
+
*/
|
|
433
|
+
_nthWeekdayOfMonth(year, month, nth, dayOfWeek, hour, minute) {
|
|
434
|
+
const first = new Date(Date.UTC(year, month, 1));
|
|
435
|
+
let dateNum = 1;
|
|
436
|
+
|
|
437
|
+
// Advance to the first matching weekday
|
|
438
|
+
while (first.getUTCDay() !== dayOfWeek) {
|
|
439
|
+
dateNum++;
|
|
440
|
+
first.setUTCDate(dateNum);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Advance to the Nth occurrence
|
|
444
|
+
dateNum += (nth - 1) * 7;
|
|
445
|
+
|
|
446
|
+
return Date.UTC(year, month, dateNum, hour, minute, 0) / 1000;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_buildVirtualEvent(template, sendAtUNIX) {
|
|
450
|
+
return {
|
|
451
|
+
id: `${template.id}__virtual__${sendAtUNIX}`,
|
|
452
|
+
sendAt: sendAtUNIX,
|
|
453
|
+
status: 'pending',
|
|
454
|
+
type: template.type,
|
|
455
|
+
settings: template.settings,
|
|
456
|
+
recurrence: template.recurrence,
|
|
457
|
+
_virtual: true,
|
|
458
|
+
_recurringSourceId: template.id,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
396
462
|
_getIntervalSeconds(pattern) {
|
|
397
463
|
const DAY = 86400;
|
|
398
464
|
switch (pattern) {
|