ultimate-jekyll-manager 1.0.3 → 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/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
|
@@ -42,40 +42,6 @@
|
|
|
42
42
|
['data-wm-bind', '@show auth.user'],
|
|
43
43
|
['hidden', '']
|
|
44
44
|
],
|
|
45
|
-
dropdown: [
|
|
46
|
-
{
|
|
47
|
-
label: 'Account',
|
|
48
|
-
href: '/account#profile',
|
|
49
|
-
icon: 'user-gear'
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
label: 'Dashboard',
|
|
53
|
-
href: '/dashboard',
|
|
54
|
-
icon: 'gauge-high'
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
divider: true,
|
|
58
|
-
attributes: [
|
|
59
|
-
['data-wm-bind', '@show auth.account.roles.admin']
|
|
60
|
-
]
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
label: 'Admin Panel',
|
|
64
|
-
href: '/admin',
|
|
65
|
-
icon: 'shield-halved',
|
|
66
|
-
attributes: [
|
|
67
|
-
['data-wm-bind', '@show auth.account.roles.admin']
|
|
68
|
-
]
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
divider: true,
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
label: 'Sign Out',
|
|
75
|
-
icon: 'arrow-right-from-bracket',
|
|
76
|
-
class: 'auth-signout-btn text-danger'
|
|
77
|
-
},
|
|
78
|
-
]
|
|
79
45
|
}
|
|
80
46
|
]
|
|
81
47
|
}
|
|
@@ -65,40 +65,6 @@
|
|
|
65
65
|
['data-wm-bind', '@show auth.user'],
|
|
66
66
|
['hidden', '']
|
|
67
67
|
],
|
|
68
|
-
dropdown: [
|
|
69
|
-
{
|
|
70
|
-
label: 'Account',
|
|
71
|
-
href: '/account#profile',
|
|
72
|
-
icon: 'user-gear'
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
label: 'Dashboard',
|
|
76
|
-
href: '/dashboard',
|
|
77
|
-
icon: 'gauge-high'
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
divider: true,
|
|
81
|
-
attributes: [
|
|
82
|
-
['data-wm-bind', '@show auth.account.roles.admin']
|
|
83
|
-
]
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
label: 'Admin Panel',
|
|
87
|
-
href: '/admin/dashboard',
|
|
88
|
-
icon: 'shield-halved',
|
|
89
|
-
attributes: [
|
|
90
|
-
['data-wm-bind', '@show auth.account.roles.admin']
|
|
91
|
-
]
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
divider: true,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
label: 'Sign Out',
|
|
98
|
-
icon: 'arrow-right-from-bracket',
|
|
99
|
-
class: 'auth-signout-btn text-danger'
|
|
100
|
-
}
|
|
101
|
-
]
|
|
102
68
|
}
|
|
103
69
|
]
|
|
104
70
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
dropdown: [
|
|
3
|
+
{
|
|
4
|
+
label: 'Account',
|
|
5
|
+
href: '/account#profile',
|
|
6
|
+
icon: 'user-gear'
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
label: 'Dashboard',
|
|
10
|
+
href: '/dashboard',
|
|
11
|
+
icon: 'gauge-high'
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
divider: true,
|
|
15
|
+
attributes: [
|
|
16
|
+
['data-wm-bind', '@show auth.account.roles.admin']
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'Admin Panel',
|
|
21
|
+
href: '/admin/dashboard',
|
|
22
|
+
icon: 'shield-halved',
|
|
23
|
+
attributes: [
|
|
24
|
+
['data-wm-bind', '@show auth.account.roles.admin']
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
divider: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: 'Sign Out',
|
|
32
|
+
icon: 'arrow-right-from-bracket',
|
|
33
|
+
class: 'auth-signout-btn text-danger'
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-jekyll-manager",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Ultimate Jekyll dependency manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"json5": "^2.2.3",
|
|
96
96
|
"libsodium-wrappers": "^0.8.2",
|
|
97
97
|
"lodash": "^4.17.23",
|
|
98
|
+
"markdown-it": "^14.1.1",
|
|
98
99
|
"minimatch": "^10.2.4",
|
|
99
100
|
"node-powertools": "^3.0.0",
|
|
100
101
|
"npm-api": "^1.0.1",
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Notifications Index Page JavaScript
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// Libraries
|
|
6
|
-
import { setStatValue } from '__main_assets__/js/libs/admin-helpers.js';
|
|
7
|
-
|
|
8
|
-
// State
|
|
9
|
-
let webManager = null;
|
|
10
|
-
|
|
11
|
-
// Module
|
|
12
|
-
export default (Manager) => {
|
|
13
|
-
return new Promise(async function (resolve) {
|
|
14
|
-
webManager = Manager.webManager;
|
|
15
|
-
|
|
16
|
-
await webManager.dom().ready();
|
|
17
|
-
|
|
18
|
-
webManager.auth().listen({ once: true }, async (state) => {
|
|
19
|
-
if (!state.user) {
|
|
20
|
-
showUnauthenticated();
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
loadStats();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
return resolve();
|
|
28
|
-
});
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// Show unauthenticated state
|
|
32
|
-
function showUnauthenticated() {
|
|
33
|
-
document.querySelectorAll('.spinner-border').forEach((spinner) => {
|
|
34
|
-
spinner.replaceWith(Object.assign(document.createElement('span'), {
|
|
35
|
-
className: 'text-muted small',
|
|
36
|
-
textContent: 'Sign in to view',
|
|
37
|
-
}));
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Load stat card counts
|
|
42
|
-
async function loadStats() {
|
|
43
|
-
const { collection, getCountFromServer } = await import('firebase/firestore');
|
|
44
|
-
const db = webManager.firebaseFirestore;
|
|
45
|
-
|
|
46
|
-
const [pushSubscribers, totalUsers] = await Promise.allSettled([
|
|
47
|
-
getCountFromServer(collection(db, 'notifications')),
|
|
48
|
-
getCountFromServer(collection(db, 'users')),
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
setStatValue('stat-push-subscribers', pushSubscribers);
|
|
52
|
-
setStatValue('stat-total-users', totalUsers);
|
|
53
|
-
}
|
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Notifications Page JavaScript
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// Libraries
|
|
6
|
-
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
|
|
7
|
-
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
8
|
-
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
|
|
9
|
-
|
|
10
|
-
let webManager = null;
|
|
11
|
-
|
|
12
|
-
// Module
|
|
13
|
-
export default (Manager) => {
|
|
14
|
-
return new Promise(async function (resolve) {
|
|
15
|
-
// Shortcuts
|
|
16
|
-
webManager = Manager.webManager;
|
|
17
|
-
|
|
18
|
-
// Initialize when DOM is ready
|
|
19
|
-
await webManager.dom().ready();
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await initializeNotificationCreator();
|
|
23
|
-
} catch (error) {
|
|
24
|
-
webManager.sentry().captureException(new Error('Failed to initialize notification creator', { cause: error }));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Resolve after initialization
|
|
28
|
-
return resolve();
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Global state
|
|
33
|
-
let formManager = null;
|
|
34
|
-
let stats = {
|
|
35
|
-
totalUsers: 0,
|
|
36
|
-
filteredUsers: 0,
|
|
37
|
-
};
|
|
38
|
-
let autoSubmitTimer = null;
|
|
39
|
-
let isInitialized = false;
|
|
40
|
-
|
|
41
|
-
// Main initialization
|
|
42
|
-
async function initializeNotificationCreator() {
|
|
43
|
-
// Prevent re-initialization
|
|
44
|
-
if (isInitialized) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
isInitialized = true;
|
|
48
|
-
|
|
49
|
-
// Initialize FormManager
|
|
50
|
-
initializeFormManager();
|
|
51
|
-
|
|
52
|
-
// Initialize UI elements
|
|
53
|
-
initializeUI();
|
|
54
|
-
|
|
55
|
-
// Show loading spinner on user counts initially
|
|
56
|
-
showUserCountLoading();
|
|
57
|
-
|
|
58
|
-
// Setup auth listener
|
|
59
|
-
webManager.auth().listen({}, async (state) => {
|
|
60
|
-
console.log('Auth state for notification creator:', state);
|
|
61
|
-
|
|
62
|
-
// Show loading spinner while fetching
|
|
63
|
-
showUserCountLoading();
|
|
64
|
-
|
|
65
|
-
// Fetch initial user stats
|
|
66
|
-
await fetchUserStats();
|
|
67
|
-
|
|
68
|
-
// Update UI with stats
|
|
69
|
-
updateUserCount();
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Initialize FormManager
|
|
74
|
-
function initializeFormManager() {
|
|
75
|
-
formManager = new FormManager('#notification-form', {
|
|
76
|
-
allowResubmit: false,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
formManager.on('change', ({ data }) => {
|
|
80
|
-
// Update preview in real-time
|
|
81
|
-
updatePreview(data);
|
|
82
|
-
|
|
83
|
-
// Update character counts
|
|
84
|
-
updateCharacterCounts(data);
|
|
85
|
-
|
|
86
|
-
// Check auto-submit status
|
|
87
|
-
checkAutoSubmit(data);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
formManager.on('submit', async ({ data }) => {
|
|
91
|
-
// Transform data for API
|
|
92
|
-
const payload = transformDataForAPI(data);
|
|
93
|
-
|
|
94
|
-
// Add user stats
|
|
95
|
-
payload.reach = {
|
|
96
|
-
available: { total: stats.totalUsers },
|
|
97
|
-
filtered: { total: stats.filteredUsers },
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Call API
|
|
101
|
-
await sendNotification(payload);
|
|
102
|
-
|
|
103
|
-
// Track success
|
|
104
|
-
trackNotificationSent(payload);
|
|
105
|
-
|
|
106
|
-
// Success
|
|
107
|
-
formManager.showSuccess(`Notification sent successfully to ${stats.filteredUsers.toLocaleString()} users!`);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Initialize UI elements
|
|
112
|
-
function initializeUI() {
|
|
113
|
-
// Icon preview updater
|
|
114
|
-
const $iconInput = document.querySelector('input[name="notification.icon"]');
|
|
115
|
-
if ($iconInput) {
|
|
116
|
-
$iconInput.addEventListener('input', (e) => {
|
|
117
|
-
updateIconPreview(e.target.value);
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Title character counter
|
|
122
|
-
const $titleInput = document.querySelector('input[name="notification.title"]');
|
|
123
|
-
if ($titleInput) {
|
|
124
|
-
// Set default value in dev mode
|
|
125
|
-
if (webManager.isDevelopment()) {
|
|
126
|
-
$titleInput.value = 'Test Notification Title';
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
$titleInput.addEventListener('input', (e) => {
|
|
130
|
-
const length = e.target.value.length;
|
|
131
|
-
const $titleCount = document.querySelector('#title-char-count');
|
|
132
|
-
if ($titleCount) {
|
|
133
|
-
$titleCount.textContent = `${length} / 60`;
|
|
134
|
-
$titleCount.className = length > 60 ? 'text-danger' : 'text-muted';
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Body character counter
|
|
140
|
-
const $bodyInput = document.querySelector('textarea[name="notification.body"]');
|
|
141
|
-
if ($bodyInput) {
|
|
142
|
-
// Set default value in dev mode
|
|
143
|
-
if (webManager.isDevelopment()) {
|
|
144
|
-
$bodyInput.value = 'This is a test notification body message for development.';
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
$bodyInput.addEventListener('input', (e) => {
|
|
148
|
-
const length = e.target.value.length;
|
|
149
|
-
const $bodyCount = document.querySelector('#body-char-count');
|
|
150
|
-
if ($bodyCount) {
|
|
151
|
-
$bodyCount.textContent = `${length} / 200`;
|
|
152
|
-
$bodyCount.className = length > 200 ? 'text-danger' : 'text-muted';
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Auto-submit button
|
|
158
|
-
const $autoSubmitBtn = document.querySelector('.btn-set-auto-submit');
|
|
159
|
-
if ($autoSubmitBtn) {
|
|
160
|
-
$autoSubmitBtn.addEventListener('click', () => {
|
|
161
|
-
setAutoSubmitTime();
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Clear auto-submit button
|
|
166
|
-
const $clearAutoSubmitBtn = document.querySelector('.btn-clear-auto-submit');
|
|
167
|
-
if ($clearAutoSubmitBtn) {
|
|
168
|
-
$clearAutoSubmitBtn.addEventListener('click', () => {
|
|
169
|
-
clearAutoSubmit();
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Notification preview click handler
|
|
174
|
-
const $notificationPreview = document.querySelector('#notification-preview-clickable');
|
|
175
|
-
if ($notificationPreview) {
|
|
176
|
-
$notificationPreview.addEventListener('click', () => {
|
|
177
|
-
const $clickActionInput = document.querySelector('input[name="notification.clickAction"]');
|
|
178
|
-
if ($clickActionInput && $clickActionInput.value) {
|
|
179
|
-
// Open the URL in a new tab
|
|
180
|
-
window.open($clickActionInput.value, '_blank', 'noopener,noreferrer');
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Initialize preview with default values and trigger form change to update everything
|
|
186
|
-
updatePreview();
|
|
187
|
-
|
|
188
|
-
// Trigger initial form change to update character counts and preview with form data
|
|
189
|
-
if (formManager) {
|
|
190
|
-
setTimeout(() => {
|
|
191
|
-
const data = formManager.getData();
|
|
192
|
-
updatePreview(data);
|
|
193
|
-
updateCharacterCounts(data);
|
|
194
|
-
checkAutoSubmit(data);
|
|
195
|
-
}, 100);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Transform data for API
|
|
200
|
-
function transformDataForAPI(formData) {
|
|
201
|
-
console.log('[Debug] transformDataForAPI called');
|
|
202
|
-
console.log('[Debug] formData received:', formData);
|
|
203
|
-
console.log('[Debug] formData.notification:', formData.notification);
|
|
204
|
-
|
|
205
|
-
const now = new Date();
|
|
206
|
-
const notification = formData.notification || {};
|
|
207
|
-
|
|
208
|
-
console.log('[Debug] notification object:', notification);
|
|
209
|
-
console.log('[Debug] notification.clickAction:', notification.clickAction);
|
|
210
|
-
console.log('[Debug] typeof notification:', typeof notification);
|
|
211
|
-
console.log('[Debug] Object.keys(notification):', Object.keys(notification));
|
|
212
|
-
|
|
213
|
-
// Generate ID
|
|
214
|
-
const id = now.getTime();
|
|
215
|
-
|
|
216
|
-
// Build click action URL with tracking
|
|
217
|
-
const redirectUrl = new URL('https://promo-server.itwcreativeworks.com/redirect/notification');
|
|
218
|
-
redirectUrl.searchParams.set('id', id);
|
|
219
|
-
redirectUrl.searchParams.set('type', 'notification');
|
|
220
|
-
|
|
221
|
-
// Make sure we have the actual clickAction value
|
|
222
|
-
const clickActionValue = notification.clickAction || '';
|
|
223
|
-
console.log('[Debug] clickActionValue to be set in URL:', clickActionValue);
|
|
224
|
-
redirectUrl.searchParams.set('url', clickActionValue);
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
id: id,
|
|
228
|
-
notification: {
|
|
229
|
-
icon: notification.icon || '',
|
|
230
|
-
title: notification.title || '',
|
|
231
|
-
body: notification.body || '',
|
|
232
|
-
clickAction: redirectUrl.toString(),
|
|
233
|
-
},
|
|
234
|
-
created: now.toISOString(),
|
|
235
|
-
channels: formData.channels || {},
|
|
236
|
-
audience: formData.audience || {},
|
|
237
|
-
schedule: formData.schedule || {},
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Send notification via API
|
|
242
|
-
async function sendNotification(payload) {
|
|
243
|
-
const functionsUrl = `${getAPIFunctionsUrl()}/admin/notification`;
|
|
244
|
-
|
|
245
|
-
return authorizedFetch(functionsUrl, {
|
|
246
|
-
method: 'POST',
|
|
247
|
-
timeout: 60000,
|
|
248
|
-
response: 'json',
|
|
249
|
-
tries: 1,
|
|
250
|
-
log: true,
|
|
251
|
-
body: payload,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Fetch user statistics
|
|
256
|
-
async function fetchUserStats() {
|
|
257
|
-
try {
|
|
258
|
-
const response = await authorizedFetch(`${getAPIFunctionsUrl()}/admin/firestore?path=meta/stats`, {
|
|
259
|
-
method: 'GET',
|
|
260
|
-
timeout: 60000,
|
|
261
|
-
response: 'json',
|
|
262
|
-
tries: 2,
|
|
263
|
-
log: true,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Extract user count from response
|
|
267
|
-
const total = response?.notifications?.total || 0;
|
|
268
|
-
stats.totalUsers = total;
|
|
269
|
-
stats.filteredUsers = total; // Initially all users
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.error('Failed to fetch user stats:', error);
|
|
272
|
-
webManager.sentry().captureException(new Error('Failed to fetch user stats', { cause: error }));
|
|
273
|
-
|
|
274
|
-
// Use default values
|
|
275
|
-
stats.totalUsers = 0;
|
|
276
|
-
stats.filteredUsers = 0;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Update user count display
|
|
281
|
-
function updateUserCount() {
|
|
282
|
-
const $countElements = document.querySelectorAll('.notification-user-count');
|
|
283
|
-
$countElements.forEach(el => {
|
|
284
|
-
// Remove loading spinner and show the count
|
|
285
|
-
el.innerHTML = `${stats.filteredUsers.toLocaleString()} / ${stats.totalUsers.toLocaleString()}`;
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Show loading spinner on user count elements
|
|
290
|
-
function showUserCountLoading() {
|
|
291
|
-
const $countElements = document.querySelectorAll('.notification-user-count');
|
|
292
|
-
$countElements.forEach(el => {
|
|
293
|
-
// Add spinner with loading text
|
|
294
|
-
el.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status"></span>Loading...';
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Update preview
|
|
299
|
-
function updatePreview(formData = null) {
|
|
300
|
-
if (!formData) {
|
|
301
|
-
formData = formManager?.getData() || {};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const notification = formData.notification || {};
|
|
305
|
-
|
|
306
|
-
// Update preview elements
|
|
307
|
-
const $previewIcon = document.querySelector('#preview-icon');
|
|
308
|
-
const $previewTitle = document.querySelector('#preview-title');
|
|
309
|
-
const $previewBody = document.querySelector('#preview-body');
|
|
310
|
-
const $previewTime = document.querySelector('#preview-time');
|
|
311
|
-
|
|
312
|
-
if ($previewIcon && notification.icon) {
|
|
313
|
-
$previewIcon.src = notification.icon;
|
|
314
|
-
$previewIcon.onerror = () => {
|
|
315
|
-
$previewIcon.src = 'https://via.placeholder.com/50';
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if ($previewTitle) {
|
|
320
|
-
$previewTitle.textContent = notification.title || 'Notification Title';
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if ($previewBody) {
|
|
324
|
-
$previewBody.textContent = notification.body || 'This is what your notification body will look like...';
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if ($previewTime) {
|
|
328
|
-
$previewTime.textContent = 'Now';
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Update icon preview
|
|
333
|
-
function updateIconPreview(url) {
|
|
334
|
-
const $previewIcon = document.querySelector('#preview-icon');
|
|
335
|
-
if ($previewIcon) {
|
|
336
|
-
if (url && url.match(/^https?:\/\/.+/)) {
|
|
337
|
-
$previewIcon.src = url;
|
|
338
|
-
$previewIcon.onerror = () => {
|
|
339
|
-
$previewIcon.src = 'https://via.placeholder.com/50';
|
|
340
|
-
};
|
|
341
|
-
} else {
|
|
342
|
-
$previewIcon.src = 'https://via.placeholder.com/50';
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Update character counts
|
|
348
|
-
function updateCharacterCounts(formData) {
|
|
349
|
-
const notification = formData.notification || {};
|
|
350
|
-
|
|
351
|
-
// Update title count
|
|
352
|
-
const $titleCount = document.querySelector('#title-char-count');
|
|
353
|
-
if ($titleCount) {
|
|
354
|
-
const length = (notification.title || '').length;
|
|
355
|
-
$titleCount.textContent = `${length} / 60`;
|
|
356
|
-
$titleCount.className = length > 60 ? 'text-danger' : 'text-muted';
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Update body count
|
|
360
|
-
const $bodyCount = document.querySelector('#body-char-count');
|
|
361
|
-
if ($bodyCount) {
|
|
362
|
-
const length = (notification.body || '').length;
|
|
363
|
-
$bodyCount.textContent = `${length} / 200`;
|
|
364
|
-
$bodyCount.className = length > 200 ? 'text-danger' : 'text-muted';
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Set auto-submit time
|
|
369
|
-
function setAutoSubmitTime() {
|
|
370
|
-
// Set to tomorrow at 10 AM
|
|
371
|
-
const tomorrow = new Date();
|
|
372
|
-
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
373
|
-
tomorrow.setHours(10, 0, 0, 0);
|
|
374
|
-
|
|
375
|
-
// Format for input fields
|
|
376
|
-
const dateStr = tomorrow.toISOString().split('T')[0];
|
|
377
|
-
const timeStr = '10:00';
|
|
378
|
-
|
|
379
|
-
// Set values
|
|
380
|
-
const $dateInput = document.querySelector('input[name="schedule.date"]');
|
|
381
|
-
const $timeInput = document.querySelector('input[name="schedule.time"]');
|
|
382
|
-
|
|
383
|
-
if ($dateInput) $dateInput.value = dateStr;
|
|
384
|
-
if ($timeInput) $timeInput.value = timeStr;
|
|
385
|
-
|
|
386
|
-
// Start countdown
|
|
387
|
-
startAutoSubmitCountdown(tomorrow);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Clear auto-submit
|
|
391
|
-
function clearAutoSubmit() {
|
|
392
|
-
// Clear inputs
|
|
393
|
-
const $dateInput = document.querySelector('input[name="schedule.date"]');
|
|
394
|
-
const $timeInput = document.querySelector('input[name="schedule.time"]');
|
|
395
|
-
|
|
396
|
-
if ($dateInput) $dateInput.value = '';
|
|
397
|
-
if ($timeInput) $timeInput.value = '';
|
|
398
|
-
|
|
399
|
-
// Clear countdown
|
|
400
|
-
clearAutoSubmitCountdown();
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Check auto-submit status
|
|
404
|
-
function checkAutoSubmit(formData) {
|
|
405
|
-
if (formData.schedule?.date && formData.schedule?.time) {
|
|
406
|
-
const scheduledDate = new Date(`${formData.schedule.date}T${formData.schedule.time}`);
|
|
407
|
-
if (scheduledDate > new Date()) {
|
|
408
|
-
startAutoSubmitCountdown(scheduledDate);
|
|
409
|
-
} else {
|
|
410
|
-
clearAutoSubmitCountdown();
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
clearAutoSubmitCountdown();
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Start auto-submit countdown
|
|
418
|
-
function startAutoSubmitCountdown(targetDate) {
|
|
419
|
-
clearAutoSubmitCountdown();
|
|
420
|
-
|
|
421
|
-
const $countdownEl = document.querySelector('#auto-submit-countdown');
|
|
422
|
-
if (!$countdownEl) {
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
$countdownEl.classList.remove('d-none');
|
|
427
|
-
|
|
428
|
-
autoSubmitTimer = setInterval(() => {
|
|
429
|
-
const now = new Date();
|
|
430
|
-
const diff = targetDate - now;
|
|
431
|
-
|
|
432
|
-
if (diff <= 0) {
|
|
433
|
-
// Time to submit!
|
|
434
|
-
clearAutoSubmitCountdown();
|
|
435
|
-
|
|
436
|
-
// Fetch updated stats before submitting
|
|
437
|
-
fetchUserStats()
|
|
438
|
-
.then(() => updateUserCount())
|
|
439
|
-
.finally(() => {
|
|
440
|
-
// Programmatically submit the form
|
|
441
|
-
formManager.$form.requestSubmit();
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
448
|
-
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
449
|
-
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
|
450
|
-
|
|
451
|
-
$countdownEl.innerHTML = `<i class="bi bi-clock"></i> Auto-submit in: ${hours}h ${minutes}m ${seconds}s`;
|
|
452
|
-
}, 1000);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Clear auto-submit countdown
|
|
456
|
-
function clearAutoSubmitCountdown() {
|
|
457
|
-
if (autoSubmitTimer) {
|
|
458
|
-
clearInterval(autoSubmitTimer);
|
|
459
|
-
autoSubmitTimer = null;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const $countdownEl = document.querySelector('#auto-submit-countdown');
|
|
463
|
-
if ($countdownEl) {
|
|
464
|
-
$countdownEl.classList.add('d-none');
|
|
465
|
-
$countdownEl.innerHTML = '';
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Get API functions URL
|
|
470
|
-
function getAPIFunctionsUrl() {
|
|
471
|
-
return `${webManager.getApiUrl()}/backend-manager`;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Tracking functions
|
|
475
|
-
function trackNotificationSent(payload) {
|
|
476
|
-
gtag('event', 'notification_sent', {
|
|
477
|
-
notification_type: 'admin',
|
|
478
|
-
user_count: stats.filteredUsers,
|
|
479
|
-
channels: Object.keys(payload.channels || {}).filter(c => payload.channels[c]?.enabled).join(','),
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
fbq('trackCustom', 'AdminNotificationSent', {
|
|
483
|
-
users: stats.filteredUsers,
|
|
484
|
-
channels: Object.keys(payload.channels || {}).filter(c => payload.channels[c]?.enabled),
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
ttq.track('SubmitForm', {
|
|
488
|
-
content_id: 'admin-notification',
|
|
489
|
-
content_type: 'product',
|
|
490
|
-
content_name: 'Admin Notification',
|
|
491
|
-
});
|
|
492
|
-
}
|