featurely-site-manager 1.0.0
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 +29 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/dist/index.d.mts +124 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +873 -0
- package/dist/index.mjs +848 -0
- package/package.json +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var SiteManager = class {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.siteConfig = null;
|
|
5
|
+
this.pollIntervalId = null;
|
|
6
|
+
this.messageContainers = /* @__PURE__ */ new Map();
|
|
7
|
+
this.dismissedMessages = /* @__PURE__ */ new Set();
|
|
8
|
+
this.featureFlagBuckets = /* @__PURE__ */ new Map();
|
|
9
|
+
// Consistent bucketing for percentage rollouts
|
|
10
|
+
this.analyticsQueue = [];
|
|
11
|
+
this.analyticsFlushIntervalId = null;
|
|
12
|
+
this.sessionId = this.generateSessionId();
|
|
13
|
+
var _a, _b, _c;
|
|
14
|
+
if (!config.apiKey) {
|
|
15
|
+
throw new Error("Featurely Site Manager: apiKey is required");
|
|
16
|
+
}
|
|
17
|
+
if (!config.projectId) {
|
|
18
|
+
throw new Error("Featurely Site Manager: projectId is required");
|
|
19
|
+
}
|
|
20
|
+
this.config = {
|
|
21
|
+
apiKey: config.apiKey,
|
|
22
|
+
projectId: config.projectId,
|
|
23
|
+
apiUrl: config.apiUrl || "https://featurely.com",
|
|
24
|
+
pollInterval: (_a = config.pollInterval) != null ? _a : 6e4,
|
|
25
|
+
userEmail: config.userEmail,
|
|
26
|
+
userId: config.userId,
|
|
27
|
+
bypassCheck: config.bypassCheck,
|
|
28
|
+
onMaintenanceEnabled: config.onMaintenanceEnabled,
|
|
29
|
+
onMaintenanceDisabled: config.onMaintenanceDisabled,
|
|
30
|
+
onMessageReceived: config.onMessageReceived,
|
|
31
|
+
onMessageDismissed: config.onMessageDismissed,
|
|
32
|
+
onFeatureFlagsUpdated: config.onFeatureFlagsUpdated,
|
|
33
|
+
onError: config.onError,
|
|
34
|
+
enableAnalytics: (_b = config.enableAnalytics) != null ? _b : true,
|
|
35
|
+
analyticsFlushInterval: (_c = config.analyticsFlushInterval) != null ? _c : 6e4
|
|
36
|
+
};
|
|
37
|
+
this.loadDismissedMessages();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Initialize and start the site manager
|
|
41
|
+
*/
|
|
42
|
+
async init() {
|
|
43
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
44
|
+
console.warn(
|
|
45
|
+
"Featurely Site Manager: Can only be initialized in a browser environment"
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await this.fetchConfig();
|
|
50
|
+
this.startPolling();
|
|
51
|
+
if (this.config.enableAnalytics) {
|
|
52
|
+
this.startAnalyticsFlushing();
|
|
53
|
+
}
|
|
54
|
+
this.injectStyles();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Stop the site manager and clean up
|
|
58
|
+
*/
|
|
59
|
+
destroy() {
|
|
60
|
+
this.stopPolling();
|
|
61
|
+
this.stopAnalyticsFlushing();
|
|
62
|
+
this.flushAnalytics();
|
|
63
|
+
this.clearMessages();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Update user email for whitelist checks
|
|
67
|
+
*/
|
|
68
|
+
setUser(email, userId) {
|
|
69
|
+
var _a;
|
|
70
|
+
this.config.userEmail = email;
|
|
71
|
+
if (userId) {
|
|
72
|
+
this.config.userId = userId;
|
|
73
|
+
}
|
|
74
|
+
if ((_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled) {
|
|
75
|
+
this.checkMaintenanceMode();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a feature flag is enabled for the current user
|
|
80
|
+
*/
|
|
81
|
+
isFeatureEnabled(flagKey) {
|
|
82
|
+
var _a;
|
|
83
|
+
if (!((_a = this.siteConfig) == null ? void 0 : _a.featureFlags)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const flag = this.siteConfig.featureFlags.find((f) => f.key === flagKey);
|
|
87
|
+
if (!flag || !flag.enabled) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const isEnabled = this.evaluateFeatureFlag(flag);
|
|
91
|
+
const trackKey = `feature_flag_tracked_${flagKey}`;
|
|
92
|
+
if (typeof sessionStorage !== "undefined" && !sessionStorage.getItem(trackKey)) {
|
|
93
|
+
this.trackEvent("feature_flag_evaluated", {
|
|
94
|
+
flagKey,
|
|
95
|
+
enabled: isEnabled
|
|
96
|
+
});
|
|
97
|
+
sessionStorage.setItem(trackKey, "1");
|
|
98
|
+
}
|
|
99
|
+
return isEnabled;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the variant for a feature flag (for A/B testing)
|
|
103
|
+
*/
|
|
104
|
+
getFeatureVariant(flagKey) {
|
|
105
|
+
var _a;
|
|
106
|
+
if (!((_a = this.siteConfig) == null ? void 0 : _a.featureFlags)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const flag = this.siteConfig.featureFlags.find((f) => f.key === flagKey);
|
|
110
|
+
if (!flag || !flag.enabled || !flag.variants || flag.variants.length === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (!this.evaluateFeatureFlag(flag)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const cacheKey = `variant_${flagKey}`;
|
|
117
|
+
const cached = localStorage.getItem(cacheKey);
|
|
118
|
+
if (cached) {
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
const bucket = this.getUserBucket(flagKey);
|
|
122
|
+
let cumulative = 0;
|
|
123
|
+
for (const variant of flag.variants) {
|
|
124
|
+
cumulative += variant.weight;
|
|
125
|
+
if (bucket < cumulative) {
|
|
126
|
+
localStorage.setItem(cacheKey, variant.key);
|
|
127
|
+
return variant.key;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const defaultVariant = flag.defaultVariant || flag.variants[0].key;
|
|
131
|
+
localStorage.setItem(cacheKey, defaultVariant);
|
|
132
|
+
return defaultVariant;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get all feature flags
|
|
136
|
+
*/
|
|
137
|
+
getAllFeatureFlags() {
|
|
138
|
+
var _a;
|
|
139
|
+
return ((_a = this.siteConfig) == null ? void 0 : _a.featureFlags) || [];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get all enabled feature flags for the current user
|
|
143
|
+
*/
|
|
144
|
+
getEnabledFeatures() {
|
|
145
|
+
var _a;
|
|
146
|
+
if (!((_a = this.siteConfig) == null ? void 0 : _a.featureFlags)) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
return this.siteConfig.featureFlags.filter((flag) => flag.enabled && this.evaluateFeatureFlag(flag)).map((flag) => flag.key);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Manually refresh configuration
|
|
153
|
+
*/
|
|
154
|
+
async refresh() {
|
|
155
|
+
await this.fetchConfig();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Track a custom analytics event
|
|
159
|
+
* @param eventName - Name of the event (e.g., 'button_clicked', 'feature_used')
|
|
160
|
+
* @param properties - Optional event properties (e.g., { button: 'signup', page: '/home' })
|
|
161
|
+
*/
|
|
162
|
+
trackEvent(eventName, properties) {
|
|
163
|
+
if (!this.config.enableAnalytics) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.analyticsQueue.push({
|
|
167
|
+
eventName,
|
|
168
|
+
properties,
|
|
169
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
170
|
+
});
|
|
171
|
+
if (this.analyticsQueue.length >= 10) {
|
|
172
|
+
this.flushAnalytics();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Configuration Fetching
|
|
177
|
+
// ============================================================================
|
|
178
|
+
async fetchConfig() {
|
|
179
|
+
var _a, _b;
|
|
180
|
+
try {
|
|
181
|
+
const response = await fetch(
|
|
182
|
+
`${this.config.apiUrl}/api/public/v1/site-config?projectId=${this.config.projectId}`,
|
|
183
|
+
{
|
|
184
|
+
headers: {
|
|
185
|
+
"X-API-Key": this.config.apiKey
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(`Failed to fetch site configuration: ${response.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
const newConfig = await response.json();
|
|
193
|
+
const configChanged = JSON.stringify(newConfig) !== JSON.stringify(this.siteConfig);
|
|
194
|
+
if (configChanged) {
|
|
195
|
+
const wasMaintenanceEnabled = (_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled;
|
|
196
|
+
const oldFeatureFlags = ((_b = this.siteConfig) == null ? void 0 : _b.featureFlags) || [];
|
|
197
|
+
this.siteConfig = newConfig;
|
|
198
|
+
if (newConfig.maintenance.enabled && !wasMaintenanceEnabled) {
|
|
199
|
+
this.enableMaintenanceMode();
|
|
200
|
+
} else if (!newConfig.maintenance.enabled && wasMaintenanceEnabled) {
|
|
201
|
+
this.disableMaintenanceMode();
|
|
202
|
+
} else if (newConfig.maintenance.enabled) {
|
|
203
|
+
this.checkMaintenanceMode();
|
|
204
|
+
}
|
|
205
|
+
if (JSON.stringify(newConfig.featureFlags) !== JSON.stringify(oldFeatureFlags)) {
|
|
206
|
+
if (this.config.onFeatureFlagsUpdated) {
|
|
207
|
+
this.config.onFeatureFlagsUpdated(newConfig.featureFlags);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.updateMessages();
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error("Featurely Site Manager: Failed to fetch configuration", error);
|
|
214
|
+
if (this.config.onError && error instanceof Error) {
|
|
215
|
+
this.config.onError(error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
startPolling() {
|
|
220
|
+
if (this.pollIntervalId) return;
|
|
221
|
+
this.pollIntervalId = setInterval(() => {
|
|
222
|
+
this.fetchConfig();
|
|
223
|
+
}, this.config.pollInterval);
|
|
224
|
+
}
|
|
225
|
+
stopPolling() {
|
|
226
|
+
if (this.pollIntervalId) {
|
|
227
|
+
clearInterval(this.pollIntervalId);
|
|
228
|
+
this.pollIntervalId = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Analytics
|
|
233
|
+
// ============================================================================
|
|
234
|
+
startAnalyticsFlushing() {
|
|
235
|
+
if (this.analyticsFlushIntervalId) return;
|
|
236
|
+
this.analyticsFlushIntervalId = setInterval(() => {
|
|
237
|
+
this.flushAnalytics();
|
|
238
|
+
}, this.config.analyticsFlushInterval);
|
|
239
|
+
}
|
|
240
|
+
stopAnalyticsFlushing() {
|
|
241
|
+
if (this.analyticsFlushIntervalId) {
|
|
242
|
+
clearInterval(this.analyticsFlushIntervalId);
|
|
243
|
+
this.analyticsFlushIntervalId = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
async flushAnalytics() {
|
|
247
|
+
if (this.analyticsQueue.length === 0) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const eventsToSend = [...this.analyticsQueue];
|
|
251
|
+
this.analyticsQueue = [];
|
|
252
|
+
for (const event of eventsToSend) {
|
|
253
|
+
try {
|
|
254
|
+
await fetch(
|
|
255
|
+
`${this.config.apiUrl}/api/projects/${this.config.projectId}/analytics/events`,
|
|
256
|
+
{
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: {
|
|
259
|
+
"Content-Type": "application/json"
|
|
260
|
+
},
|
|
261
|
+
body: JSON.stringify({
|
|
262
|
+
eventName: event.eventName,
|
|
263
|
+
properties: event.properties,
|
|
264
|
+
userId: this.config.userId,
|
|
265
|
+
sessionId: this.sessionId,
|
|
266
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
267
|
+
platform: typeof navigator !== "undefined" ? navigator.platform : void 0
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error("Failed to send analytics event:", error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
generateSessionId() {
|
|
277
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
|
278
|
+
}
|
|
279
|
+
// ============================================================================
|
|
280
|
+
// Feature Flags
|
|
281
|
+
// ============================================================================
|
|
282
|
+
/**
|
|
283
|
+
* Evaluate if a feature flag should be enabled for the current user
|
|
284
|
+
*/
|
|
285
|
+
evaluateFeatureFlag(flag) {
|
|
286
|
+
if (!flag.enabled) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const userEmail = this.config.userEmail;
|
|
290
|
+
if (flag.excludeEmails && userEmail && flag.excludeEmails.includes(userEmail)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (flag.targetEmails && flag.targetEmails.length > 0) {
|
|
294
|
+
if (!userEmail || !flag.targetEmails.includes(userEmail)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
if (flag.rolloutPercentage !== void 0 && flag.rolloutPercentage < 100) {
|
|
300
|
+
const bucket = this.getUserBucket(flag.key);
|
|
301
|
+
return bucket < flag.rolloutPercentage;
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get consistent bucket (0-99) for a user+flag combination
|
|
307
|
+
* This ensures the same user always gets the same result for percentage rollouts
|
|
308
|
+
*/
|
|
309
|
+
getUserBucket(flagKey) {
|
|
310
|
+
if (this.featureFlagBuckets.has(flagKey)) {
|
|
311
|
+
return this.featureFlagBuckets.get(flagKey);
|
|
312
|
+
}
|
|
313
|
+
const identifier = this.config.userId || this.config.userEmail || this.getAnonymousId();
|
|
314
|
+
const hash = this.simpleHash(`${identifier}:${flagKey}`);
|
|
315
|
+
const bucket = hash % 100;
|
|
316
|
+
this.featureFlagBuckets.set(flagKey, bucket);
|
|
317
|
+
return bucket;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Simple hash function for consistent bucketing
|
|
321
|
+
*/
|
|
322
|
+
simpleHash(str) {
|
|
323
|
+
let hash = 0;
|
|
324
|
+
for (let i = 0; i < str.length; i++) {
|
|
325
|
+
const char = str.charCodeAt(i);
|
|
326
|
+
hash = (hash << 5) - hash + char;
|
|
327
|
+
hash = hash & hash;
|
|
328
|
+
}
|
|
329
|
+
return Math.abs(hash);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get or create anonymous user ID for bucketing
|
|
333
|
+
*/
|
|
334
|
+
getAnonymousId() {
|
|
335
|
+
const storageKey = "featurely_anonymous_id";
|
|
336
|
+
let id = localStorage.getItem(storageKey);
|
|
337
|
+
if (!id) {
|
|
338
|
+
id = `anon_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
339
|
+
localStorage.setItem(storageKey, id);
|
|
340
|
+
}
|
|
341
|
+
return id;
|
|
342
|
+
}
|
|
343
|
+
// ============================================================================
|
|
344
|
+
// Maintenance Mode
|
|
345
|
+
// ============================================================================
|
|
346
|
+
checkMaintenanceMode() {
|
|
347
|
+
var _a;
|
|
348
|
+
if (!((_a = this.siteConfig) == null ? void 0 : _a.maintenance.enabled)) return;
|
|
349
|
+
if (this.shouldBypassMaintenance()) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.showMaintenancePage();
|
|
353
|
+
}
|
|
354
|
+
enableMaintenanceMode() {
|
|
355
|
+
if (!this.siteConfig) return;
|
|
356
|
+
if (this.shouldBypassMaintenance()) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.showMaintenancePage();
|
|
360
|
+
this.trackEvent("maintenance_enabled", {
|
|
361
|
+
maintenanceType: this.siteConfig.maintenance.type
|
|
362
|
+
});
|
|
363
|
+
if (this.config.onMaintenanceEnabled) {
|
|
364
|
+
this.config.onMaintenanceEnabled(this.siteConfig.maintenance);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
disableMaintenanceMode() {
|
|
368
|
+
this.hideMaintenancePage();
|
|
369
|
+
this.trackEvent("maintenance_disabled");
|
|
370
|
+
if (this.config.onMaintenanceDisabled) {
|
|
371
|
+
this.config.onMaintenanceDisabled();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
shouldBypassMaintenance() {
|
|
375
|
+
if (!this.siteConfig) return false;
|
|
376
|
+
const whitelist = this.siteConfig.maintenance.whitelist;
|
|
377
|
+
if (this.config.bypassCheck && this.config.bypassCheck()) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (whitelist.localStorageKeys && whitelist.localStorageKeys.length > 0) {
|
|
381
|
+
const hasKey = whitelist.localStorageKeys.some((key) => {
|
|
382
|
+
try {
|
|
383
|
+
return localStorage.getItem(key) !== null;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
if (hasKey) return true;
|
|
389
|
+
}
|
|
390
|
+
if (whitelist.emails && this.config.userEmail) {
|
|
391
|
+
if (whitelist.emails.includes(this.config.userEmail)) {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
showMaintenancePage() {
|
|
398
|
+
if (!this.siteConfig) return;
|
|
399
|
+
const config = this.siteConfig.maintenance;
|
|
400
|
+
const overlay = document.createElement("div");
|
|
401
|
+
overlay.id = "featurely-maintenance-overlay";
|
|
402
|
+
overlay.className = "featurely-maintenance-overlay";
|
|
403
|
+
if (config.type === "custom" && config.customHtml) {
|
|
404
|
+
overlay.innerHTML = config.customHtml;
|
|
405
|
+
} else {
|
|
406
|
+
overlay.innerHTML = this.getDefaultMaintenanceHtml(config);
|
|
407
|
+
}
|
|
408
|
+
document.body.appendChild(overlay);
|
|
409
|
+
}
|
|
410
|
+
hideMaintenancePage() {
|
|
411
|
+
const overlay = document.getElementById("featurely-maintenance-overlay");
|
|
412
|
+
if (overlay) {
|
|
413
|
+
overlay.remove();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
getDefaultMaintenanceHtml(config) {
|
|
417
|
+
const restoration = config.expectedRestoration ? new Date(config.expectedRestoration).toLocaleString() : "soon";
|
|
418
|
+
const statusLink = config.showStatusLink && config.statusPageUrl ? `<p><a href="${config.statusPageUrl}" target="_blank" rel="noopener noreferrer" class="featurely-status-link">View Status Page \u2192</a></p>` : "";
|
|
419
|
+
return `
|
|
420
|
+
<div class="featurely-maintenance-content">
|
|
421
|
+
<div class="featurely-maintenance-icon">\u{1F527}</div>
|
|
422
|
+
<h1>We'll be back soon!</h1>
|
|
423
|
+
<p>We're performing scheduled maintenance.</p>
|
|
424
|
+
<p class="featurely-maintenance-time">Expected restoration: <strong>${restoration}</strong></p>
|
|
425
|
+
${statusLink}
|
|
426
|
+
<p class="featurely-maintenance-footer">Thank you for your patience.</p>
|
|
427
|
+
</div>
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
// ============================================================================
|
|
431
|
+
// Status Messages
|
|
432
|
+
// ============================================================================
|
|
433
|
+
updateMessages() {
|
|
434
|
+
if (!this.siteConfig) return;
|
|
435
|
+
const currentTime = /* @__PURE__ */ new Date();
|
|
436
|
+
const currentPath = window.location.pathname;
|
|
437
|
+
const activeMessages = this.siteConfig.messages.filter((message) => {
|
|
438
|
+
if (this.dismissedMessages.has(message.id)) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (message.startsAt && new Date(message.startsAt) > currentTime) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (message.expiresAt && new Date(message.expiresAt) < currentTime) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
if (message.targetPages && message.targetPages.length > 0) {
|
|
448
|
+
const matches = message.targetPages.some((pattern) => {
|
|
449
|
+
const regex = new RegExp(pattern);
|
|
450
|
+
return regex.test(currentPath);
|
|
451
|
+
});
|
|
452
|
+
if (!matches) return false;
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
});
|
|
456
|
+
this.messageContainers.forEach((container, id) => {
|
|
457
|
+
if (!activeMessages.find((m) => m.id === id)) {
|
|
458
|
+
container.remove();
|
|
459
|
+
this.messageContainers.delete(id);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
activeMessages.forEach((message) => {
|
|
463
|
+
if (!this.messageContainers.has(message.id)) {
|
|
464
|
+
this.showMessage(message);
|
|
465
|
+
if (this.config.onMessageReceived) {
|
|
466
|
+
this.config.onMessageReceived(message);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
showMessage(message) {
|
|
472
|
+
var _a;
|
|
473
|
+
const messageEl = document.createElement("div");
|
|
474
|
+
messageEl.className = `featurely-message featurely-message-${message.type} featurely-message-${message.position} featurely-message-${message.style}`;
|
|
475
|
+
messageEl.dataset.messageId = message.id;
|
|
476
|
+
const icon = message.icon || this.getDefaultIcon(message.type);
|
|
477
|
+
const ctaHtml = message.cta ? `<a href="${message.cta.url}" class="featurely-message-cta">${message.cta.text}</a>` : "";
|
|
478
|
+
const closeButton = message.dismissible ? `<button class="featurely-message-close" aria-label="Close message">×</button>` : "";
|
|
479
|
+
messageEl.innerHTML = `
|
|
480
|
+
<div class="featurely-message-content">
|
|
481
|
+
<div class="featurely-message-icon">${icon}</div>
|
|
482
|
+
<div class="featurely-message-text">
|
|
483
|
+
<strong class="featurely-message-title">${message.title}</strong>
|
|
484
|
+
<span class="featurely-message-body">${message.message}</span>
|
|
485
|
+
</div>
|
|
486
|
+
${ctaHtml}
|
|
487
|
+
${closeButton}
|
|
488
|
+
</div>
|
|
489
|
+
`;
|
|
490
|
+
if (message.dismissible) {
|
|
491
|
+
const closeBtn = messageEl.querySelector(".featurely-message-close");
|
|
492
|
+
closeBtn == null ? void 0 : closeBtn.addEventListener("click", () => this.dismissMessage(message.id));
|
|
493
|
+
}
|
|
494
|
+
if ((_a = message.cta) == null ? void 0 : _a.action) {
|
|
495
|
+
const ctaEl = messageEl.querySelector(".featurely-message-cta");
|
|
496
|
+
ctaEl == null ? void 0 : ctaEl.addEventListener("click", (e) => {
|
|
497
|
+
var _a2;
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
if ((_a2 = message.cta) == null ? void 0 : _a2.action) {
|
|
500
|
+
this.handleMessageAction(message.cta.action, message);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
document.body.appendChild(messageEl);
|
|
505
|
+
setTimeout(() => messageEl.classList.add("featurely-message-show"), 10);
|
|
506
|
+
this.messageContainers.set(message.id, messageEl);
|
|
507
|
+
this.trackEvent("message_shown", {
|
|
508
|
+
messageId: message.id,
|
|
509
|
+
messageType: message.type,
|
|
510
|
+
messagePosition: message.position,
|
|
511
|
+
messageStyle: message.style
|
|
512
|
+
});
|
|
513
|
+
if (message.style === "toast" && message.dismissible) {
|
|
514
|
+
setTimeout(() => this.dismissMessage(message.id), 5e3);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
dismissMessage(messageId) {
|
|
518
|
+
var _a, _b;
|
|
519
|
+
const messageEl = this.messageContainers.get(messageId);
|
|
520
|
+
if (!messageEl) return;
|
|
521
|
+
messageEl.classList.remove("featurely-message-show");
|
|
522
|
+
setTimeout(() => {
|
|
523
|
+
messageEl.remove();
|
|
524
|
+
this.messageContainers.delete(messageId);
|
|
525
|
+
}, 300);
|
|
526
|
+
this.dismissedMessages.add(messageId);
|
|
527
|
+
this.saveDismissedMessages();
|
|
528
|
+
this.trackEvent("message_dismissed", {
|
|
529
|
+
messageId,
|
|
530
|
+
messageType: ((_b = (_a = this.siteConfig) == null ? void 0 : _a.messages.find((m) => m.id === messageId)) == null ? void 0 : _b.type) || "unknown"
|
|
531
|
+
});
|
|
532
|
+
if (this.config.onMessageDismissed) {
|
|
533
|
+
this.config.onMessageDismissed(messageId);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
clearMessages() {
|
|
537
|
+
this.messageContainers.forEach((el) => el.remove());
|
|
538
|
+
this.messageContainers.clear();
|
|
539
|
+
}
|
|
540
|
+
handleMessageAction(action, message) {
|
|
541
|
+
var _a;
|
|
542
|
+
console.log("Message action triggered:", action, message);
|
|
543
|
+
if ((_a = message.cta) == null ? void 0 : _a.url) {
|
|
544
|
+
window.location.href = message.cta.url;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
getDefaultIcon(type) {
|
|
548
|
+
const icons = {
|
|
549
|
+
info: "\u2139\uFE0F",
|
|
550
|
+
warning: "\u26A0\uFE0F",
|
|
551
|
+
error: "\u274C",
|
|
552
|
+
success: "\u2705"
|
|
553
|
+
};
|
|
554
|
+
return icons[type];
|
|
555
|
+
}
|
|
556
|
+
// ============================================================================
|
|
557
|
+
// Persistence
|
|
558
|
+
// ============================================================================
|
|
559
|
+
loadDismissedMessages() {
|
|
560
|
+
try {
|
|
561
|
+
const stored = localStorage.getItem("featurely_dismissed_messages");
|
|
562
|
+
if (stored) {
|
|
563
|
+
this.dismissedMessages = new Set(JSON.parse(stored));
|
|
564
|
+
}
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
saveDismissedMessages() {
|
|
569
|
+
try {
|
|
570
|
+
localStorage.setItem(
|
|
571
|
+
"featurely_dismissed_messages",
|
|
572
|
+
JSON.stringify(Array.from(this.dismissedMessages))
|
|
573
|
+
);
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// ============================================================================
|
|
578
|
+
// Styles
|
|
579
|
+
// ============================================================================
|
|
580
|
+
injectStyles() {
|
|
581
|
+
if (document.getElementById("featurely-site-manager-styles")) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const style = document.createElement("style");
|
|
585
|
+
style.id = "featurely-site-manager-styles";
|
|
586
|
+
style.textContent = `
|
|
587
|
+
/* Maintenance Overlay */
|
|
588
|
+
.featurely-maintenance-overlay {
|
|
589
|
+
position: fixed;
|
|
590
|
+
top: 0;
|
|
591
|
+
left: 0;
|
|
592
|
+
width: 100%;
|
|
593
|
+
height: 100%;
|
|
594
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
595
|
+
z-index: 999999;
|
|
596
|
+
display: flex;
|
|
597
|
+
align-items: center;
|
|
598
|
+
justify-content: center;
|
|
599
|
+
color: white;
|
|
600
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.featurely-maintenance-content {
|
|
604
|
+
text-align: center;
|
|
605
|
+
max-width: 600px;
|
|
606
|
+
padding: 40px;
|
|
607
|
+
background: rgba(255, 255, 255, 0.1);
|
|
608
|
+
border-radius: 16px;
|
|
609
|
+
backdrop-filter: blur(10px);
|
|
610
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.featurely-maintenance-icon {
|
|
614
|
+
font-size: 64px;
|
|
615
|
+
margin-bottom: 20px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.featurely-maintenance-content h1 {
|
|
619
|
+
font-size: 36px;
|
|
620
|
+
margin: 0 0 16px 0;
|
|
621
|
+
font-weight: 700;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.featurely-maintenance-content p {
|
|
625
|
+
font-size: 18px;
|
|
626
|
+
margin: 12px 0;
|
|
627
|
+
opacity: 0.9;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.featurely-maintenance-time {
|
|
631
|
+
margin: 24px 0;
|
|
632
|
+
padding: 16px;
|
|
633
|
+
background: rgba(255, 255, 255, 0.1);
|
|
634
|
+
border-radius: 8px;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.featurely-status-link {
|
|
638
|
+
display: inline-block;
|
|
639
|
+
margin: 20px 0;
|
|
640
|
+
padding: 12px 24px;
|
|
641
|
+
background: rgba(255, 255, 255, 0.2);
|
|
642
|
+
color: white;
|
|
643
|
+
text-decoration: none;
|
|
644
|
+
border-radius: 8px;
|
|
645
|
+
font-weight: 600;
|
|
646
|
+
transition: all 0.2s;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.featurely-status-link:hover {
|
|
650
|
+
background: rgba(255, 255, 255, 0.3);
|
|
651
|
+
transform: translateY(-2px);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.featurely-maintenance-footer {
|
|
655
|
+
margin-top: 32px;
|
|
656
|
+
font-size: 14px;
|
|
657
|
+
opacity: 0.7;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* Status Messages */
|
|
661
|
+
.featurely-message {
|
|
662
|
+
position: fixed;
|
|
663
|
+
left: 0;
|
|
664
|
+
right: 0;
|
|
665
|
+
z-index: 999998;
|
|
666
|
+
padding: 16px 20px;
|
|
667
|
+
background: white;
|
|
668
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
669
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
670
|
+
opacity: 0;
|
|
671
|
+
transform: translateY(-20px);
|
|
672
|
+
transition: all 0.3s ease;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.featurely-message-show {
|
|
676
|
+
opacity: 1;
|
|
677
|
+
transform: translateY(0);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.featurely-message-top {
|
|
681
|
+
top: 0;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.featurely-message-bottom {
|
|
685
|
+
bottom: 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.featurely-message-toast {
|
|
689
|
+
left: auto;
|
|
690
|
+
right: 20px;
|
|
691
|
+
max-width: 400px;
|
|
692
|
+
border-radius: 8px;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.featurely-message-toast.featurely-message-top {
|
|
696
|
+
top: 20px;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.featurely-message-toast.featurely-message-bottom {
|
|
700
|
+
bottom: 20px;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.featurely-message-content {
|
|
704
|
+
display: flex;
|
|
705
|
+
align-items: center;
|
|
706
|
+
gap: 12px;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.featurely-message-icon {
|
|
710
|
+
font-size: 24px;
|
|
711
|
+
flex-shrink: 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.featurely-message-text {
|
|
715
|
+
flex: 1;
|
|
716
|
+
display: flex;
|
|
717
|
+
flex-direction: column;
|
|
718
|
+
gap: 4px;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.featurely-message-title {
|
|
722
|
+
font-size: 14px;
|
|
723
|
+
font-weight: 600;
|
|
724
|
+
display: block;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.featurely-message-body {
|
|
728
|
+
font-size: 14px;
|
|
729
|
+
opacity: 0.9;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.featurely-message-cta {
|
|
733
|
+
padding: 8px 16px;
|
|
734
|
+
border-radius: 6px;
|
|
735
|
+
text-decoration: none;
|
|
736
|
+
font-weight: 600;
|
|
737
|
+
font-size: 14px;
|
|
738
|
+
white-space: nowrap;
|
|
739
|
+
transition: all 0.2s;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.featurely-message-close {
|
|
743
|
+
background: none;
|
|
744
|
+
border: none;
|
|
745
|
+
font-size: 24px;
|
|
746
|
+
line-height: 1;
|
|
747
|
+
cursor: pointer;
|
|
748
|
+
padding: 0;
|
|
749
|
+
width: 24px;
|
|
750
|
+
height: 24px;
|
|
751
|
+
display: flex;
|
|
752
|
+
align-items: center;
|
|
753
|
+
justify-content: center;
|
|
754
|
+
opacity: 0.5;
|
|
755
|
+
transition: opacity 0.2s;
|
|
756
|
+
flex-shrink: 0;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.featurely-message-close:hover {
|
|
760
|
+
opacity: 1;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Message Types */
|
|
764
|
+
.featurely-message-info {
|
|
765
|
+
background: #e3f2fd;
|
|
766
|
+
color: #1976d2;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.featurely-message-info .featurely-message-cta {
|
|
770
|
+
background: #1976d2;
|
|
771
|
+
color: white;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.featurely-message-info .featurely-message-cta:hover {
|
|
775
|
+
background: #1565c0;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.featurely-message-warning {
|
|
779
|
+
background: #fff3e0;
|
|
780
|
+
color: #f57c00;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.featurely-message-warning .featurely-message-cta {
|
|
784
|
+
background: #f57c00;
|
|
785
|
+
color: white;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.featurely-message-warning .featurely-message-cta:hover {
|
|
789
|
+
background: #ef6c00;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.featurely-message-error {
|
|
793
|
+
background: #ffebee;
|
|
794
|
+
color: #c62828;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.featurely-message-error .featurely-message-cta {
|
|
798
|
+
background: #c62828;
|
|
799
|
+
color: white;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.featurely-message-error .featurely-message-cta:hover {
|
|
803
|
+
background: #b71c1c;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.featurely-message-success {
|
|
807
|
+
background: #e8f5e9;
|
|
808
|
+
color: #2e7d32;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.featurely-message-success .featurely-message-cta {
|
|
812
|
+
background: #2e7d32;
|
|
813
|
+
color: white;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.featurely-message-success .featurely-message-cta:hover {
|
|
817
|
+
background: #1b5e20;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/* Mobile Responsive */
|
|
821
|
+
@media (max-width: 640px) {
|
|
822
|
+
.featurely-message-toast {
|
|
823
|
+
left: 10px;
|
|
824
|
+
right: 10px;
|
|
825
|
+
max-width: none;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.featurely-maintenance-content {
|
|
829
|
+
padding: 20px;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.featurely-maintenance-content h1 {
|
|
833
|
+
font-size: 28px;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.featurely-maintenance-content p {
|
|
837
|
+
font-size: 16px;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
`;
|
|
841
|
+
document.head.appendChild(style);
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
var index_default = SiteManager;
|
|
845
|
+
export {
|
|
846
|
+
SiteManager,
|
|
847
|
+
index_default as default
|
|
848
|
+
};
|