@nexushub/client 0.4.2 → 0.4.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/dist/react.js CHANGED
@@ -1,15 +1,2943 @@
1
- "use client";
2
1
  import {
3
- AuthProvider,
4
- NexusProvider,
5
- useNexus,
6
- useNexusAnalytics,
7
- useNexusAuth,
8
- useNexusLiveFeed
9
- } from "./chunk-G4SHBWAQ.js";
2
+ getFullConfig,
3
+ validateConfig
4
+ } from "./chunk-NOIMIIA5.js";
5
+
6
+ // src/components/NexusProvider.tsx
7
+ import React2, {
8
+ createContext as createContext2,
9
+ useEffect as useEffect2,
10
+ useRef,
11
+ useMemo,
12
+ useState as useState2,
13
+ useCallback as useCallback2,
14
+ Suspense
15
+ } from "react";
16
+ import { usePathname, useSearchParams } from "next/navigation";
17
+
18
+ // src/content/cache-implementations.ts
19
+ var CacheTags = {
20
+ project: (projectId) => `nexus_project_${projectId}`,
21
+ content: (slug) => `content_${slug}`,
22
+ collection: (collectionId) => `collection_${collectionId}`,
23
+ global: "nexus_global_config",
24
+ user: (userId) => `user_${userId}`,
25
+ media: (mediaId) => `media_${mediaId}`
26
+ };
27
+ var MemoryCache = class {
28
+ constructor(options = {}) {
29
+ this.cache = /* @__PURE__ */ new Map();
30
+ this.tagIndex = /* @__PURE__ */ new Map();
31
+ this.stats = {
32
+ size: 0,
33
+ hits: 0,
34
+ misses: 0,
35
+ hitRate: 0,
36
+ evictions: 0
37
+ };
38
+ this.maxSize = options.maxSize || 1e3;
39
+ this.defaultTTL = options.ttl || 5 * 60 * 1e3;
40
+ }
41
+ set(key, data, options = {}) {
42
+ if (this.cache.size >= this.maxSize) {
43
+ this.evictLRU();
44
+ }
45
+ const metadata = {
46
+ timestamp: Date.now(),
47
+ expiresAt: Date.now() + (options.ttl || this.defaultTTL),
48
+ tags: options.tags || [],
49
+ size: this.calculateSize(data)
50
+ };
51
+ this.cache.set(key, { data, metadata });
52
+ this.stats.size = this.cache.size;
53
+ metadata.tags.forEach((tag) => {
54
+ if (!this.tagIndex.has(tag)) {
55
+ this.tagIndex.set(tag, /* @__PURE__ */ new Set());
56
+ }
57
+ this.tagIndex.get(tag).add(key);
58
+ });
59
+ }
60
+ get(key) {
61
+ const entry = this.cache.get(key);
62
+ if (!entry) {
63
+ this.stats.misses++;
64
+ this.updateHitRate();
65
+ return null;
66
+ }
67
+ if (Date.now() > entry.metadata.expiresAt) {
68
+ this.delete(key);
69
+ this.stats.misses++;
70
+ this.updateHitRate();
71
+ return null;
72
+ }
73
+ entry.metadata.timestamp = Date.now();
74
+ this.stats.hits++;
75
+ this.updateHitRate();
76
+ return entry;
77
+ }
78
+ delete(key) {
79
+ const entry = this.cache.get(key);
80
+ if (!entry) return false;
81
+ entry.metadata.tags.forEach((tag) => {
82
+ const keys = this.tagIndex.get(tag);
83
+ if (keys) {
84
+ keys.delete(key);
85
+ if (keys.size === 0) {
86
+ this.tagIndex.delete(tag);
87
+ }
88
+ }
89
+ });
90
+ this.cache.delete(key);
91
+ this.stats.size = this.cache.size;
92
+ return true;
93
+ }
94
+ invalidateByTags(tags) {
95
+ const keysToDelete = /* @__PURE__ */ new Set();
96
+ tags.forEach((tag) => {
97
+ const keys = this.tagIndex.get(tag);
98
+ if (keys) {
99
+ keys.forEach((key) => keysToDelete.add(key));
100
+ this.tagIndex.delete(tag);
101
+ }
102
+ });
103
+ keysToDelete.forEach((key) => this.delete(key));
104
+ if (process.env.NODE_ENV === "development") {
105
+ console.log(
106
+ `[MemoryCache] Invalidated ${keysToDelete.size} entries by tags: ${tags.join(", ")}`
107
+ );
108
+ }
109
+ }
110
+ clear() {
111
+ this.cache.clear();
112
+ this.tagIndex.clear();
113
+ this.stats = {
114
+ size: 0,
115
+ hits: 0,
116
+ misses: 0,
117
+ hitRate: 0,
118
+ evictions: 0
119
+ };
120
+ }
121
+ getStats() {
122
+ return { ...this.stats };
123
+ }
124
+ evictLRU() {
125
+ let oldestKey = null;
126
+ let oldestTime = Infinity;
127
+ for (const [key, entry] of this.cache.entries()) {
128
+ if (entry.metadata.timestamp < oldestTime) {
129
+ oldestTime = entry.metadata.timestamp;
130
+ oldestKey = key;
131
+ }
132
+ }
133
+ if (oldestKey) {
134
+ this.delete(oldestKey);
135
+ this.stats.evictions++;
136
+ }
137
+ }
138
+ calculateSize(data) {
139
+ try {
140
+ const jsonString = JSON.stringify(data);
141
+ return new Blob([jsonString]).size;
142
+ } catch {
143
+ return 0;
144
+ }
145
+ }
146
+ updateHitRate() {
147
+ const total = this.stats.hits + this.stats.misses;
148
+ this.stats.hitRate = total > 0 ? this.stats.hits / total : 0;
149
+ }
150
+ };
151
+ var BrowserCache = class {
152
+ constructor(projectId) {
153
+ this.projectId = projectId;
154
+ this.prefix = "nexushub_";
155
+ this.maxSize = 5 * 1024 * 1024;
156
+ // 5MB standard LocalStorage limit
157
+ this.currentSize = 0;
158
+ if (typeof window !== "undefined") {
159
+ this.calculateCurrentSize();
160
+ }
161
+ }
162
+ set(key, data, ttl = 5 * 60 * 1e3) {
163
+ if (typeof window === "undefined") return;
164
+ const storageKey = this.getStorageKey(key);
165
+ const entry = {
166
+ data,
167
+ metadata: {
168
+ timestamp: Date.now(),
169
+ expiresAt: Date.now() + ttl,
170
+ size: this.calculateStorageSize(data)
171
+ }
172
+ };
173
+ const serialized = JSON.stringify(entry);
174
+ const newSize = new Blob([serialized]).size;
175
+ if (this.currentSize + newSize > this.maxSize) {
176
+ this.evictOldest();
177
+ }
178
+ try {
179
+ localStorage.setItem(storageKey, serialized);
180
+ this.currentSize += newSize;
181
+ } catch (error) {
182
+ console.warn("[BrowserCache] Failed to save to localStorage:", error);
183
+ this.evictOldest();
184
+ try {
185
+ localStorage.setItem(storageKey, serialized);
186
+ } catch (e) {
187
+ }
188
+ }
189
+ }
190
+ get(key) {
191
+ if (typeof window === "undefined") return null;
192
+ const storageKey = this.getStorageKey(key);
193
+ const item = localStorage.getItem(storageKey);
194
+ if (!item) return null;
195
+ try {
196
+ const entry = JSON.parse(item);
197
+ if (Date.now() > entry.metadata.expiresAt) {
198
+ this.delete(key);
199
+ return null;
200
+ }
201
+ return entry.data;
202
+ } catch {
203
+ this.delete(key);
204
+ return null;
205
+ }
206
+ }
207
+ delete(key) {
208
+ if (typeof window === "undefined") return;
209
+ const storageKey = this.getStorageKey(key);
210
+ const item = localStorage.getItem(storageKey);
211
+ if (item) {
212
+ this.currentSize -= new Blob([item]).size;
213
+ }
214
+ localStorage.removeItem(storageKey);
215
+ }
216
+ clear() {
217
+ if (typeof window === "undefined") return;
218
+ const keysToRemove = [];
219
+ for (let i = 0; i < localStorage.length; i++) {
220
+ const key = localStorage.key(i);
221
+ if (key?.startsWith(this.prefix)) {
222
+ keysToRemove.push(key);
223
+ }
224
+ }
225
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
226
+ this.currentSize = 0;
227
+ }
228
+ getStorageKey(key) {
229
+ return `${this.prefix}${this.projectId}_${key}`;
230
+ }
231
+ calculateCurrentSize() {
232
+ this.currentSize = 0;
233
+ for (let i = 0; i < localStorage.length; i++) {
234
+ const key = localStorage.key(i);
235
+ if (key?.startsWith(this.prefix)) {
236
+ const item = localStorage.getItem(key);
237
+ if (item) {
238
+ this.currentSize += new Blob([item]).size;
239
+ }
240
+ }
241
+ }
242
+ }
243
+ calculateStorageSize(data) {
244
+ return new Blob([JSON.stringify(data)]).size;
245
+ }
246
+ evictOldest() {
247
+ let oldestKey = null;
248
+ let oldestTime = Infinity;
249
+ for (let i = 0; i < localStorage.length; i++) {
250
+ const key = localStorage.key(i);
251
+ if (key?.startsWith(this.prefix)) {
252
+ const item = localStorage.getItem(key);
253
+ if (item) {
254
+ try {
255
+ const entry = JSON.parse(item);
256
+ if (entry.metadata.timestamp < oldestTime) {
257
+ oldestTime = entry.metadata.timestamp;
258
+ oldestKey = key;
259
+ }
260
+ } catch {
261
+ localStorage.removeItem(key);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ if (oldestKey) {
267
+ const item = localStorage.getItem(oldestKey);
268
+ if (item) {
269
+ this.currentSize -= new Blob([item]).size;
270
+ }
271
+ localStorage.removeItem(oldestKey);
272
+ }
273
+ }
274
+ };
275
+
276
+ // src/content/cache.ts
277
+ var LocalCacheProxy = class {
278
+ constructor(cachePath) {
279
+ this.instance = null;
280
+ this.cachePath = cachePath;
281
+ }
282
+ async getInstance() {
283
+ if (this.instance) return this.instance;
284
+ if (typeof window === "undefined") {
285
+ const mod = await import("./local-cache-server-BNLLL4NR.js");
286
+ this.instance = new mod.LocalCache(this.cachePath);
287
+ } else {
288
+ const mod = await import("./local-cache-client-I6UYVJJV.js");
289
+ this.instance = new mod.LocalCache(this.cachePath);
290
+ }
291
+ return this.instance;
292
+ }
293
+ isLoaded() {
294
+ return this.instance?.isLoaded() || false;
295
+ }
296
+ async getPage(slug) {
297
+ const inst = await this.getInstance();
298
+ return inst.getPage(slug);
299
+ }
300
+ async getCollection(id) {
301
+ const inst = await this.getInstance();
302
+ return inst.getCollection(id);
303
+ }
304
+ async getGlobals() {
305
+ const inst = await this.getInstance();
306
+ return inst.getGlobals();
307
+ }
308
+ async getAllData() {
309
+ const inst = await this.getInstance();
310
+ return inst.getAllData();
311
+ }
312
+ };
313
+
314
+ // src/content/strategies.ts
315
+ var RateLimiter = class {
316
+ constructor(config) {
317
+ this.requests = [];
318
+ this.config = config;
319
+ }
320
+ async checkLimit() {
321
+ while (true) {
322
+ const now = Date.now();
323
+ const windowStart = now - this.config.timeWindow;
324
+ this.requests = this.requests.filter((time) => time > windowStart);
325
+ if (this.requests.length < this.config.maxRequests) {
326
+ this.requests.push(now);
327
+ return;
328
+ }
329
+ const oldestRequest = this.requests[0];
330
+ const waitTime = oldestRequest + this.config.timeWindow - now;
331
+ if (waitTime > 0) {
332
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
333
+ } else {
334
+ }
335
+ }
336
+ }
337
+ getStats() {
338
+ const now = Date.now();
339
+ const windowStart = now - this.config.timeWindow;
340
+ const currentRequests = this.requests.filter(
341
+ (time) => time > windowStart
342
+ ).length;
343
+ return { currentRequests, limit: this.config.maxRequests };
344
+ }
345
+ };
346
+ var ExponentialBackoff = class {
347
+ constructor(config) {
348
+ this.config = { jitter: true, ...config };
349
+ }
350
+ async execute(fn, onRetry) {
351
+ let lastError = new Error("Unknown error");
352
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
353
+ try {
354
+ return await fn();
355
+ } catch (error) {
356
+ lastError = error;
357
+ if (this.isClientError(error) && !this.isRateLimitError(error)) {
358
+ throw error;
359
+ }
360
+ if (attempt === this.config.maxRetries) break;
361
+ const delay = this.calculateDelay(attempt);
362
+ if (onRetry) onRetry(attempt + 1, delay, error);
363
+ await new Promise((resolve) => setTimeout(resolve, delay));
364
+ }
365
+ }
366
+ throw lastError;
367
+ }
368
+ calculateDelay(attempt) {
369
+ let delay = this.config.baseDelay * Math.pow(2, attempt);
370
+ delay = Math.min(delay, this.config.maxDelay);
371
+ if (this.config.jitter) {
372
+ delay = delay * (0.5 + Math.random());
373
+ }
374
+ return delay;
375
+ }
376
+ isClientError(error) {
377
+ return error?.status >= 400 && error?.status < 500;
378
+ }
379
+ isRateLimitError(error) {
380
+ return error?.status === 429;
381
+ }
382
+ };
383
+ var CircuitBreaker = class {
384
+ constructor() {
385
+ this.state = 0 /* CLOSED */;
386
+ this.failures = 0;
387
+ this.lastFailureTime = 0;
388
+ this.failureThreshold = 5;
389
+ this.resetTimeout = 3e4;
390
+ }
391
+ // 30 seconds
392
+ isOpen() {
393
+ if (this.state === 1 /* OPEN */) {
394
+ if (Date.now() - this.lastFailureTime > this.resetTimeout) {
395
+ this.state = 2 /* HALF_OPEN */;
396
+ return false;
397
+ }
398
+ return true;
399
+ }
400
+ return false;
401
+ }
402
+ recordSuccess() {
403
+ this.failures = 0;
404
+ this.state = 0 /* CLOSED */;
405
+ }
406
+ recordFailure() {
407
+ this.failures++;
408
+ this.lastFailureTime = Date.now();
409
+ if (this.failures >= this.failureThreshold) {
410
+ this.state = 1 /* OPEN */;
411
+ if (process.env.NODE_ENV === "development") {
412
+ console.warn(
413
+ "[NexusHub] \u{1F50C} Circuit Breaker OPEN. Pausing network requests."
414
+ );
415
+ }
416
+ }
417
+ }
418
+ };
419
+ var RequestBatcher = class {
420
+ constructor(batchWindow = 10, maxBatchSize = 20) {
421
+ this.batchWindow = batchWindow;
422
+ this.maxBatchSize = maxBatchSize;
423
+ this.batch = [];
424
+ this.processing = false;
425
+ }
426
+ async schedule(key, request) {
427
+ return new Promise((resolve, reject) => {
428
+ this.batch.push({ key, resolve, reject });
429
+ if (this.batch.length >= this.maxBatchSize) {
430
+ this.processBatch(request);
431
+ } else if (!this.batchTimeout) {
432
+ this.batchTimeout = setTimeout(
433
+ () => this.processBatch(request),
434
+ this.batchWindow
435
+ );
436
+ }
437
+ });
438
+ }
439
+ async processBatch(request) {
440
+ if (this.processing || this.batch.length === 0) return;
441
+ this.processing = true;
442
+ if (this.batchTimeout) {
443
+ clearTimeout(this.batchTimeout);
444
+ this.batchTimeout = void 0;
445
+ }
446
+ const currentBatch = [...this.batch];
447
+ this.batch = [];
448
+ try {
449
+ const result = await request();
450
+ currentBatch.forEach((item) => item.resolve(result));
451
+ } catch (error) {
452
+ currentBatch.forEach((item) => item.reject(error));
453
+ } finally {
454
+ this.processing = false;
455
+ if (this.batch.length > 0) {
456
+ setTimeout(() => this.processBatch(request), 0);
457
+ }
458
+ }
459
+ }
460
+ };
461
+
462
+ // src/content/utils.ts
463
+ function validateSlug(slug) {
464
+ if (!slug || typeof slug !== "string") {
465
+ throw new Error("Slug must be a non-empty string");
466
+ }
467
+ if (!/^[a-z0-9-_]+$/.test(slug)) {
468
+ throw new Error("Slug can only contain lowercase letters, numbers, hyphens, and underscores");
469
+ }
470
+ }
471
+ function normalizeQuery(query) {
472
+ const normalized = { ...query };
473
+ normalized.page = Math.max(1, normalized.page || 1);
474
+ normalized.limit = Math.min(100, Math.max(1, normalized.limit || 10));
475
+ normalized.order = normalized.order || "desc";
476
+ if (normalized.page < 1) {
477
+ throw new Error("Page must be greater than 0");
478
+ }
479
+ if (normalized.limit < 1 || normalized.limit > 100) {
480
+ throw new Error("Limit must be between 1 and 100");
481
+ }
482
+ return normalized;
483
+ }
484
+ function buildQueryString(query) {
485
+ const params = new URLSearchParams();
486
+ if (query.page) params.append("page", query.page.toString());
487
+ if (query.limit) params.append("limit", query.limit.toString());
488
+ if (query.sort) params.append("sort", query.sort);
489
+ if (query.order) params.append("order", query.order);
490
+ if (query.search) params.append("search", query.search);
491
+ if (query.include?.length) {
492
+ params.append("include", query.include.join(","));
493
+ }
494
+ if (query.fields?.length) {
495
+ params.append("fields", query.fields.join(","));
496
+ }
497
+ if (query.filter) {
498
+ params.append("filter", JSON.stringify(query.filter));
499
+ }
500
+ return params.toString();
501
+ }
502
+ function measurePerformance(name, fn) {
503
+ const start = performance.now();
504
+ const result = fn();
505
+ const end = performance.now();
506
+ if (process.env.NODE_ENV === "development") {
507
+ console.log(`\u23F1\uFE0F ${name}: ${(end - start).toFixed(2)}ms`);
508
+ }
509
+ return { result, duration: end - start };
510
+ }
511
+
512
+ // src/content/index.ts
513
+ var ContentEngine = class {
514
+ constructor(config) {
515
+ this.config = config;
516
+ this.isServer = typeof window === "undefined";
517
+ this.localCache = new LocalCacheProxy();
518
+ this.memoryCache = new MemoryCache({
519
+ maxSize: 500,
520
+ ttl: 5 * 60 * 1e3
521
+ // 5 minutes
522
+ });
523
+ if (!this.isServer) {
524
+ this.browserCache = new BrowserCache(config.projectId);
525
+ }
526
+ this.rateLimiter = new RateLimiter({
527
+ maxRequests: config.debug ? 100 : 50,
528
+ timeWindow: 6e4
529
+ });
530
+ this.backoff = new ExponentialBackoff({
531
+ maxRetries: config.retries || 3,
532
+ baseDelay: 100,
533
+ maxDelay: 5e3,
534
+ jitter: true
535
+ });
536
+ this.requestBatcher = new RequestBatcher(20, 50);
537
+ this.circuitBreaker = new CircuitBreaker();
538
+ this.defaultRevalidate = config.revalidateTime || 60;
539
+ this.cacheStrategy = config.cacheStrategy || "memory";
540
+ if (!this.isServer) {
541
+ window.addEventListener("beforeunload", this.cleanup.bind(this));
542
+ }
543
+ }
544
+ /**
545
+ * Fetch a Single Page with full strategy pipeline
546
+ */
547
+ async getPage(slug, options = {}) {
548
+ const { result, duration } = measurePerformance(
549
+ `getPage("${slug}")`,
550
+ () => this._getPage(slug, options)
551
+ );
552
+ if (this.config.debug && duration > 100) {
553
+ console.warn(
554
+ `[NexusHub] \u26A0\uFE0F getPage("${slug}") took ${duration.toFixed(2)}ms`
555
+ );
556
+ }
557
+ return result;
558
+ }
559
+ async _getPage(slug, options = {}) {
560
+ validateSlug(slug);
561
+ const {
562
+ revalidate = this.defaultRevalidate,
563
+ tags = [],
564
+ forceRefresh = false,
565
+ includeMetadata = false
566
+ } = options;
567
+ const cacheKey = `page:${slug}`;
568
+ const cached = await this.checkCaches(
569
+ cacheKey,
570
+ forceRefresh,
571
+ includeMetadata
572
+ );
573
+ if (cached) {
574
+ if (this.config.debug) console.log(`[NexusHub] \u26A1 Cache hit: ${slug}`);
575
+ return includeMetadata ? cached : cached.data;
576
+ }
577
+ if (this.circuitBreaker.isOpen()) {
578
+ throw new Error(`[NexusHub] Circuit open. API is unavailable.`);
579
+ }
580
+ try {
581
+ await this.rateLimiter.checkLimit();
582
+ const data = await this.backoff.execute(
583
+ async () => {
584
+ return this.fetchPage(
585
+ slug,
586
+ cacheKey,
587
+ tags,
588
+ revalidate,
589
+ forceRefresh
590
+ );
591
+ },
592
+ (attempt, delay, error) => {
593
+ if (this.config.debug) {
594
+ console.log(
595
+ `[NexusHub] \u{1F504} Retry ${attempt} for '${slug}' after ${delay}ms: ${error.message}`
596
+ );
597
+ }
598
+ }
599
+ );
600
+ this.circuitBreaker.recordSuccess();
601
+ return includeMetadata ? data : data.data;
602
+ } catch (error) {
603
+ this.circuitBreaker.recordFailure();
604
+ const staleCache = this.memoryCache.get(cacheKey);
605
+ if (staleCache && !forceRefresh) {
606
+ console.warn(`[NexusHub] \u26A0\uFE0F Serving stale content for '${slug}'`);
607
+ return includeMetadata ? staleCache : staleCache.data;
608
+ }
609
+ throw this.normalizeError(error, `Failed to fetch page '${slug}'`);
610
+ }
611
+ }
612
+ /**
613
+ * Fetch a Collection (Optimized)
614
+ */
615
+ async getCollection(collectionId, query = {}, options = {}) {
616
+ const { result, duration } = measurePerformance(
617
+ `getCollection("${collectionId}")`,
618
+ () => this._getCollection(collectionId, query, options)
619
+ );
620
+ if (this.config.debug && duration > 100) {
621
+ console.warn(
622
+ `[NexusHub] \u26A0\uFE0F getCollection("${collectionId}") took ${duration.toFixed(2)}ms`
623
+ );
624
+ }
625
+ return result;
626
+ }
627
+ async _getCollection(collectionId, query = {}, options = {}) {
628
+ const normalizedQuery = normalizeQuery(query);
629
+ const {
630
+ revalidate = this.defaultRevalidate,
631
+ tags = [],
632
+ forceRefresh = false,
633
+ includeMetadata = false
634
+ } = options;
635
+ const queryString = buildQueryString(normalizedQuery);
636
+ const cacheKey = `collection:${collectionId}:${queryString}`;
637
+ const cached = await this.checkCaches(
638
+ cacheKey,
639
+ forceRefresh,
640
+ includeMetadata
641
+ );
642
+ if (cached) {
643
+ if (this.config.debug) {
644
+ console.log(
645
+ `[NexusHub] \u26A1 Served collection '${collectionId}' from memory cache.`
646
+ );
647
+ }
648
+ return includeMetadata ? cached : cached.data;
649
+ }
650
+ if (!forceRefresh && process.env.NODE_ENV === "development") {
651
+ const localCollection = await this.localCache.getCollection(collectionId);
652
+ if (localCollection) {
653
+ const result = this.applyLocalQuery(
654
+ localCollection,
655
+ normalizedQuery
656
+ );
657
+ if (this.config.debug) {
658
+ console.log(
659
+ `[NexusHub] \u{1F4C1} Served collection '${collectionId}' from local cache.`
660
+ );
661
+ }
662
+ this.memoryCache.set(cacheKey, result, {
663
+ tags: [...tags, CacheTags.collection(collectionId)],
664
+ revalidate
665
+ });
666
+ return result;
667
+ }
668
+ }
669
+ if (this.circuitBreaker.isOpen()) throw new Error("Circuit open");
670
+ try {
671
+ await this.rateLimiter.checkLimit();
672
+ const result = await this.backoff.execute(async () => {
673
+ return this.fetchCollection(
674
+ collectionId,
675
+ normalizedQuery,
676
+ cacheKey,
677
+ tags,
678
+ revalidate
679
+ );
680
+ });
681
+ this.circuitBreaker.recordSuccess();
682
+ return includeMetadata ? result : result.data;
683
+ } catch (error) {
684
+ this.circuitBreaker.recordFailure();
685
+ const staleCache = this.memoryCache.get(cacheKey);
686
+ if (staleCache && !forceRefresh) {
687
+ console.warn(
688
+ `[NexusHub] \u26A0\uFE0F Using stale cache for collection '${collectionId}'`
689
+ );
690
+ return includeMetadata ? staleCache : staleCache.data;
691
+ }
692
+ throw this.normalizeError(
693
+ error,
694
+ `Failed to fetch collection '${collectionId}'`
695
+ );
696
+ }
697
+ }
698
+ /**
699
+ * Fetch Global Settings with nested includes support
700
+ */
701
+ async getGlobals(options = {}) {
702
+ const {
703
+ include = [],
704
+ revalidate = this.defaultRevalidate,
705
+ forceRefresh = false
706
+ } = options;
707
+ const cacheKey = `globals:${include.join(",")}`;
708
+ if (!forceRefresh && this.cacheStrategy === "memory") {
709
+ const cached = this.memoryCache.get(cacheKey);
710
+ if (cached && this.isCacheValid(cached.metadata)) {
711
+ if (this.config.debug)
712
+ console.log("[NexusHub] \u26A1 Served globals from memory cache.");
713
+ return cached.data;
714
+ }
715
+ }
716
+ if (!forceRefresh && !this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
717
+ const cached = this.browserCache.get(cacheKey);
718
+ if (cached) {
719
+ return cached;
720
+ }
721
+ }
722
+ if (!forceRefresh && process.env.NODE_ENV === "development") {
723
+ const localGlobals = this.localCache.getGlobals();
724
+ if (localGlobals) {
725
+ this.memoryCache.set(cacheKey, localGlobals, {
726
+ tags: [CacheTags.global],
727
+ revalidate
728
+ });
729
+ return localGlobals;
730
+ }
731
+ }
732
+ try {
733
+ await this.rateLimiter.checkLimit();
734
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/globals`;
735
+ const params = include.length > 0 ? `?include=${include.join(",")}` : "";
736
+ const res = await this.fetchWithTimeout(`${url}${params}`, {
737
+ method: "GET",
738
+ headers: this.getHeaders()
739
+ });
740
+ if (!res.ok) {
741
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
742
+ }
743
+ const json = await res.json();
744
+ const data = json.data || json;
745
+ this.writeCache(cacheKey, data, {
746
+ revalidate,
747
+ tags: [CacheTags.project(this.config.projectId), CacheTags.global]
748
+ });
749
+ return data;
750
+ } catch (error) {
751
+ const staleCache = this.memoryCache.get(cacheKey);
752
+ if (staleCache && !forceRefresh) {
753
+ console.warn("[NexusHub] \u26A0\uFE0F Using stale cache for globals");
754
+ return staleCache.data;
755
+ }
756
+ throw this.normalizeError(error, "Failed to fetch globals");
757
+ }
758
+ }
759
+ /**
760
+ * Get a single item from a collection (Uses Request Batching)
761
+ * If you call this 10 times in a loop, it sends 1 HTTP request.
762
+ */
763
+ async getItem(collectionId, itemId, options = {}) {
764
+ const cacheKey = `item:${collectionId}:${itemId}`;
765
+ if (this.cacheStrategy === "memory") {
766
+ const cached = this.memoryCache.get(cacheKey);
767
+ if (cached && this.isCacheValid(cached.metadata)) {
768
+ if (this.config.debug) {
769
+ console.log(`[NexusHub] \u26A1 Served item '${itemId}' from cache.`);
770
+ }
771
+ return cached.data;
772
+ }
773
+ }
774
+ return this.requestBatcher.schedule(cacheKey, async () => {
775
+ const params = new URLSearchParams();
776
+ if (options.include?.length) {
777
+ params.append("include", options.include.join(","));
778
+ }
779
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}/${itemId}?${params}`;
780
+ const res = await this.fetchWithTimeout(url, {
781
+ method: "GET",
782
+ headers: this.getHeaders()
783
+ });
784
+ if (!res.ok) {
785
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
786
+ }
787
+ const json = await res.json();
788
+ const data = json.data || json;
789
+ this.writeCache(cacheKey, data, {
790
+ revalidate: options.revalidate || this.defaultRevalidate,
791
+ tags: [
792
+ CacheTags.project(this.config.projectId),
793
+ CacheTags.collection(collectionId),
794
+ `item_${itemId}`,
795
+ ...options.tags || []
796
+ ]
797
+ });
798
+ return data;
799
+ });
800
+ }
801
+ /**
802
+ * Search across collections
803
+ */
804
+ async search(query, options = {}) {
805
+ const params = new URLSearchParams({
806
+ q: query,
807
+ limit: (options.limit || 20).toString()
808
+ });
809
+ if (options.collections?.length) {
810
+ params.append("collections", options.collections.join(","));
811
+ }
812
+ if (options.fields?.length) {
813
+ params.append("fields", options.fields.join(","));
814
+ }
815
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/search?${params}`;
816
+ const res = await this.fetchWithTimeout(url, {
817
+ method: "GET",
818
+ headers: this.getHeaders()
819
+ });
820
+ if (!res.ok) {
821
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
822
+ }
823
+ return await res.json();
824
+ }
825
+ /**
826
+ * Prefetch content for better performance
827
+ */
828
+ async prefetch(urls) {
829
+ if (typeof window !== "undefined" && "requestIdleCallback" in window) {
830
+ requestIdleCallback(async () => {
831
+ await Promise.allSettled(
832
+ urls.map((url) => fetch(url, { priority: "low" }))
833
+ );
834
+ });
835
+ }
836
+ }
837
+ /**
838
+ * Robust Real-time Subscriptions (SSE) with Auto-Reconnect
839
+ */
840
+ subscribeToUpdates(callback) {
841
+ if (this.isServer || typeof EventSource === "undefined") {
842
+ console.warn("[NexusHub] EventSource not supported in this environment");
843
+ return () => {
844
+ };
845
+ }
846
+ let eventSource = null;
847
+ let retryCount = 0;
848
+ let isClosed = false;
849
+ const connect = () => {
850
+ if (isClosed) return;
851
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/updates?key=${this.config.apiKey}`;
852
+ eventSource = new EventSource(url);
853
+ eventSource.onopen = () => {
854
+ retryCount = 0;
855
+ if (this.config.debug)
856
+ console.log("[NexusHub] \u{1F7E2} Real-time stream connected");
857
+ };
858
+ eventSource.onmessage = (event) => {
859
+ try {
860
+ const data = JSON.parse(event.data);
861
+ if (data.type === "content.updated" && data.slug) {
862
+ this.invalidateCache([CacheTags.content(data.slug)]);
863
+ }
864
+ if (data.type === "collection.updated") {
865
+ this.invalidateCache([CacheTags.collection(data.collectionId)]);
866
+ }
867
+ callback(data);
868
+ } catch (e) {
869
+ console.error("[NexusHub] SSE Parse Error", e);
870
+ }
871
+ };
872
+ eventSource.onerror = () => {
873
+ eventSource?.close();
874
+ if (isClosed) return;
875
+ const timeout = Math.min(1e3 * Math.pow(2, retryCount), 1e4);
876
+ retryCount++;
877
+ console.warn(
878
+ `[NexusHub] \u{1F534} Stream disconnected. Reconnecting in ${timeout}ms...`
879
+ );
880
+ setTimeout(connect, timeout);
881
+ };
882
+ };
883
+ connect();
884
+ return () => {
885
+ isClosed = true;
886
+ eventSource?.close();
887
+ };
888
+ }
889
+ // --- CACHE MANAGEMENT ---
890
+ /**
891
+ * Check all caches in order of speed
892
+ */
893
+ async checkCaches(key, forceRefresh, includeMetadata) {
894
+ if (forceRefresh) return null;
895
+ if (this.cacheStrategy === "memory") {
896
+ const cached = this.memoryCache.get(key);
897
+ if (cached && this.isCacheValid(cached.metadata)) {
898
+ return cached;
899
+ }
900
+ }
901
+ if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
902
+ const cached = this.browserCache.get(key);
903
+ if (cached) {
904
+ return {
905
+ data: cached,
906
+ metadata: {
907
+ timestamp: 0,
908
+ expiresAt: Infinity,
909
+ tags: [],
910
+ etag: void 0
911
+ }
912
+ };
913
+ }
914
+ }
915
+ if (process.env.NODE_ENV === "development") {
916
+ if (key.startsWith("page:")) {
917
+ const slug = key.split(":")[1];
918
+ const local = await this.localCache.getPage(slug);
919
+ if (local) {
920
+ return {
921
+ data: local,
922
+ metadata: {
923
+ timestamp: 0,
924
+ expiresAt: Infinity,
925
+ tags: [],
926
+ etag: void 0
927
+ }
928
+ };
929
+ }
930
+ }
931
+ }
932
+ return null;
933
+ }
934
+ writeCache(key, data, options) {
935
+ if (this.cacheStrategy === "memory") {
936
+ this.memoryCache.set(key, data, {
937
+ tags: options.tags,
938
+ revalidate: options.revalidate
939
+ });
940
+ }
941
+ if (!this.isServer && this.cacheStrategy === "localStorage" && this.browserCache) {
942
+ this.browserCache.set(key, data, options.revalidate * 1e3);
943
+ }
944
+ }
945
+ // packages/client-sdk/src/content/index.ts
946
+ /**
947
+ * ⚡ THE PULSE: Next.js Cache Revalidation Receiver
948
+ * Used in app/api/nexus-revalidate/route.ts to handle instant updates from the NexusHub backend.
949
+ *
950
+ * @param req The incoming Request object from Next.js
951
+ * @param revalidateFn The revalidateTag function imported from 'next/cache'
952
+ */
953
+ async handleRevalidation(req, revalidateFn) {
954
+ try {
955
+ if (!req.body) {
956
+ return { success: false, message: "Missing Pulse Payload" };
957
+ }
958
+ const body = await req.json();
959
+ const { secret, tag } = body;
960
+ if (!secret || secret !== this.config.projectId) {
961
+ if (this.config.debug) {
962
+ console.warn(`[NexusHub] \u26A0\uFE0F Blocked unauthorized Pulse request.`);
963
+ }
964
+ return { success: false, message: "Invalid Pulse Signature" };
965
+ }
966
+ const targetTag = tag || "content_all";
967
+ revalidateFn(targetTag);
968
+ this.invalidateCache([targetTag]);
969
+ if (this.config.debug) {
970
+ console.log(
971
+ `[NexusHub] \u26A1 Pulse Received: Successfully revalidated tag [${targetTag}]`
972
+ );
973
+ }
974
+ return { success: true, now: Date.now() };
975
+ } catch (e) {
976
+ if (this.config.debug) {
977
+ console.error(`[NexusHub] \u274C Pulse Processing Error:`, e.message);
978
+ }
979
+ return {
980
+ success: false,
981
+ message: "Malformed Pulse Payload or Processing Error"
982
+ };
983
+ }
984
+ }
985
+ /**
986
+ * Invalidate cache by tags
987
+ */
988
+ invalidateCache(tags) {
989
+ this.memoryCache.invalidateByTags(tags);
990
+ if (this.config.debug) {
991
+ console.log(
992
+ `[NexusHub] \u267B\uFE0F Invalidated cache for tags: ${tags.join(", ")}`
993
+ );
994
+ }
995
+ }
996
+ /**
997
+ * Clear all caches
998
+ */
999
+ clearCache() {
1000
+ this.memoryCache.clear();
1001
+ if (this.browserCache) {
1002
+ this.browserCache.clear();
1003
+ }
1004
+ if (this.config.debug) {
1005
+ console.log("[NexusHub] \u{1F9F9} Cleared all caches");
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Get cache statistics
1010
+ */
1011
+ getCacheStats() {
1012
+ const stats = {
1013
+ memory: this.memoryCache.getStats(),
1014
+ local: { loaded: this.localCache.isLoaded() }
1015
+ };
1016
+ if (this.browserCache) {
1017
+ stats.browser = { size: 0 };
1018
+ }
1019
+ return stats;
1020
+ }
1021
+ // --- REQUEST METHODS ---
1022
+ async fetchPage(slug, cacheKey, tags, revalidate, forceRefresh) {
1023
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/page/${slug}`;
1024
+ if (this.config.debug) {
1025
+ console.log(`[NexusHub] \u{1F310} Fetching: ${url}`);
1026
+ }
1027
+ const res = await this.fetchWithTimeout(url, {
1028
+ method: "GET",
1029
+ headers: this.getHeaders(),
1030
+ tags,
1031
+ revalidate
1032
+ });
1033
+ if (!res.ok) {
1034
+ if (res.status === 404) {
1035
+ throw new Error(
1036
+ `Page '${slug}' not found. Check your Dashboard or Seed data.`
1037
+ );
1038
+ }
1039
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
1040
+ }
1041
+ const json = await res.json();
1042
+ const etag = res.headers.get("etag");
1043
+ const cacheEntry = {
1044
+ data: json.data,
1045
+ metadata: {
1046
+ timestamp: Date.now(),
1047
+ etag: etag || void 0,
1048
+ expiresAt: Date.now() + revalidate * 1e3,
1049
+ tags: [...tags, CacheTags.content(slug)]
1050
+ }
1051
+ };
1052
+ this.writeCache(cacheKey, cacheEntry.data, {
1053
+ revalidate,
1054
+ tags: [
1055
+ CacheTags.project(this.config.projectId),
1056
+ CacheTags.content(slug),
1057
+ ...tags
1058
+ ]
1059
+ });
1060
+ return cacheEntry;
1061
+ }
1062
+ async fetchCollection(collectionId, query, cacheKey, tags, revalidate) {
1063
+ const params = buildQueryString(query);
1064
+ const url = `${this.config.apiUrl}/content/${this.config.projectId}/collection/${collectionId}?${params}`;
1065
+ if (this.config.debug) {
1066
+ console.log(`[NexusHub] \u{1F310} Fetching Collection: ${url}`);
1067
+ }
1068
+ const res = await this.fetchWithTimeout(url, {
1069
+ method: "GET",
1070
+ headers: this.getHeaders(),
1071
+ tags,
1072
+ revalidate
1073
+ });
1074
+ if (!res.ok) {
1075
+ throw new Error(`API Error ${res.status}: ${res.statusText}`);
1076
+ }
1077
+ const json = await res.json();
1078
+ const etag = res.headers.get("etag");
1079
+ const cacheEntry = {
1080
+ data: json,
1081
+ metadata: {
1082
+ timestamp: Date.now(),
1083
+ etag: etag || void 0,
1084
+ expiresAt: Date.now() + revalidate * 1e3,
1085
+ tags: [...tags, CacheTags.collection(collectionId)]
1086
+ }
1087
+ };
1088
+ this.writeCache(cacheKey, cacheEntry.data, {
1089
+ revalidate,
1090
+ tags: [
1091
+ CacheTags.project(this.config.projectId),
1092
+ CacheTags.collection(collectionId),
1093
+ ...tags
1094
+ ]
1095
+ });
1096
+ return cacheEntry;
1097
+ }
1098
+ // --- HELPER METHODS ---
1099
+ applyLocalQuery(items, query) {
1100
+ let filtered = [...items];
1101
+ if (query.search) {
1102
+ const searchLower = query.search.toLowerCase();
1103
+ filtered = filtered.filter(
1104
+ (item) => JSON.stringify(item).toLowerCase().includes(searchLower)
1105
+ );
1106
+ }
1107
+ if (query.filter) {
1108
+ filtered = filtered.filter((item) => {
1109
+ return Object.entries(query.filter).every(([key, value]) => {
1110
+ const itemValue = item[key];
1111
+ if (itemValue === void 0) return false;
1112
+ if (Array.isArray(value)) {
1113
+ return value.includes(itemValue);
1114
+ }
1115
+ return itemValue === value;
1116
+ });
1117
+ });
1118
+ }
1119
+ if (query.sort) {
1120
+ filtered.sort((a, b) => {
1121
+ const aVal = a[query.sort];
1122
+ const bVal = b[query.sort];
1123
+ const order = query.order === "asc" ? 1 : -1;
1124
+ if (aVal < bVal) return -1 * order;
1125
+ if (aVal > bVal) return 1 * order;
1126
+ return 0;
1127
+ });
1128
+ }
1129
+ const page = query.page || 1;
1130
+ const limit = query.limit || 10;
1131
+ const start = (page - 1) * limit;
1132
+ const end = start + limit;
1133
+ const total = filtered.length;
1134
+ return {
1135
+ items: filtered.slice(start, end),
1136
+ total,
1137
+ page,
1138
+ limit,
1139
+ totalPages: Math.ceil(total / limit),
1140
+ hasNext: end < total,
1141
+ hasPrev: start > 0
1142
+ };
1143
+ }
1144
+ async fetchWithTimeout(url, options = {}) {
1145
+ const {
1146
+ timeout = this.config.timeout || 1e4,
1147
+ tags = [],
1148
+ revalidate,
1149
+ ...fetchOptions
1150
+ } = options;
1151
+ const controller = new AbortController();
1152
+ const id = setTimeout(() => controller.abort(), timeout);
1153
+ const nextConfig = this.isServer ? { next: { tags, revalidate } } : {};
1154
+ try {
1155
+ const response = await fetch(url, {
1156
+ ...fetchOptions,
1157
+ ...nextConfig,
1158
+ // Inject Next.js tags
1159
+ signal: controller.signal
1160
+ });
1161
+ clearTimeout(id);
1162
+ return response;
1163
+ } catch (error) {
1164
+ clearTimeout(id);
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ getHeaders() {
1169
+ const headers = {
1170
+ "Content-Type": "application/json",
1171
+ "X-Nexus-Client": "client-sdk/1.0.0",
1172
+ "X-Nexus-Project": this.config.projectId
1173
+ };
1174
+ if (this.config.apiKey) {
1175
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
1176
+ }
1177
+ return headers;
1178
+ }
1179
+ isCacheValid(metadata) {
1180
+ return Date.now() < metadata.expiresAt;
1181
+ }
1182
+ normalizeError(error, context) {
1183
+ if (error instanceof Error) {
1184
+ if (error.name === "AbortError") {
1185
+ return new Error(`${context}: Request timeout`);
1186
+ }
1187
+ return error;
1188
+ }
1189
+ return new Error(`${context}: ${String(error)}`);
1190
+ }
1191
+ /**
1192
+ * Cancel ongoing requests
1193
+ */
1194
+ cancelRequests() {
1195
+ if (this.abortController) {
1196
+ this.abortController.abort();
1197
+ this.abortController = new AbortController();
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Cleanup resources
1202
+ */
1203
+ cleanup() {
1204
+ this.cancelRequests();
1205
+ if (!this.isServer) {
1206
+ window.removeEventListener("beforeunload", this.cleanup.bind(this));
1207
+ }
1208
+ }
1209
+ };
1210
+
1211
+ // src/analytics/fingerprint.ts
1212
+ var cachedEntropy = null;
1213
+ var getDeviceEntropy = async () => {
1214
+ if (cachedEntropy) return cachedEntropy;
1215
+ if (typeof window === "undefined") return {};
1216
+ const nav = window.navigator;
1217
+ cachedEntropy = {
1218
+ screen_resolution: `${window.screen.width}x${window.screen.height}`,
1219
+ color_depth: window.screen.colorDepth,
1220
+ pixel_ratio: window.devicePixelRatio || 1,
1221
+ hardware_concurrency: nav.hardwareConcurrency,
1222
+ device_memory: nav.deviceMemory,
1223
+ // Chrome/Edge only
1224
+ timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
1225
+ platform: nav.platform,
1226
+ language: nav.language,
1227
+ touch_support: "ontouchstart" in window || nav.maxTouchPoints > 0,
1228
+ canvas_hash: await generateCanvasHash()
1229
+ };
1230
+ return cachedEntropy;
1231
+ };
1232
+ var generateCanvasHash = async () => {
1233
+ try {
1234
+ const canvas = document.createElement("canvas");
1235
+ const ctx = canvas.getContext("2d");
1236
+ if (!ctx) return "";
1237
+ canvas.width = 200;
1238
+ canvas.height = 50;
1239
+ ctx.textBaseline = "top";
1240
+ ctx.font = '16px "Arial"';
1241
+ ctx.textBaseline = "alphabetic";
1242
+ ctx.fillStyle = "#f60";
1243
+ ctx.fillRect(125, 1, 62, 20);
1244
+ ctx.fillStyle = "#069";
1245
+ ctx.fillText("NexusHub Rocks! <canvas> 1.0", 2, 15);
1246
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
1247
+ ctx.fillText("NexusHub Rocks! <canvas> 1.0", 4, 17);
1248
+ const dataUrl = canvas.toDataURL();
1249
+ let hash = 0;
1250
+ for (let i = 0; i < dataUrl.length; i++) {
1251
+ const char = dataUrl.charCodeAt(i);
1252
+ hash = (hash << 5) - hash + char;
1253
+ hash = hash & hash;
1254
+ }
1255
+ return hash.toString(16);
1256
+ } catch {
1257
+ return "";
1258
+ }
1259
+ };
1260
+ var getVisitorId = async () => {
1261
+ if (typeof window === "undefined") return "server_visitor";
1262
+ const STORAGE_KEY = "nexus_vid";
1263
+ let vid = localStorage.getItem(STORAGE_KEY);
1264
+ if (!vid) {
1265
+ const entropy = await getDeviceEntropy();
1266
+ const random = Math.random().toString(36).substring(2, 15);
1267
+ const timestamp = Date.now().toString(36);
1268
+ const fingerprint = [
1269
+ entropy.screen_resolution,
1270
+ entropy.hardware_concurrency,
1271
+ entropy.timezone_offset,
1272
+ entropy.platform,
1273
+ entropy.canvas_hash
1274
+ ].join("|");
1275
+ let hash = 0;
1276
+ for (let i = 0; i < fingerprint.length; i++) {
1277
+ hash = (hash << 5) - hash + fingerprint.charCodeAt(i);
1278
+ hash |= 0;
1279
+ }
1280
+ vid = `vis_${hash.toString(16)}_${timestamp}_${random}`;
1281
+ localStorage.setItem(STORAGE_KEY, vid);
1282
+ }
1283
+ return vid;
1284
+ };
1285
+
1286
+ // src/analytics/vitals.ts
1287
+ import { onCLS, onLCP, onTTFB, onINP, onFCP } from "web-vitals";
1288
+ var METRIC_KEY_MAP = {
1289
+ CLS: "cls",
1290
+ LCP: "lcp",
1291
+ INP: "inp",
1292
+ TTFB: "ttfb",
1293
+ FCP: "fcp"
1294
+ };
1295
+ var VitalsCollector = class {
1296
+ constructor() {
1297
+ this.metrics = {};
1298
+ this.hasUpdates = false;
1299
+ if (typeof window !== "undefined") {
1300
+ this.init();
1301
+ }
1302
+ }
1303
+ init() {
1304
+ const recordMetric = (metric) => {
1305
+ const key = METRIC_KEY_MAP[metric.name];
1306
+ this.metrics[key] = metric.value;
1307
+ this.hasUpdates = true;
1308
+ };
1309
+ onCLS(recordMetric);
1310
+ onLCP(recordMetric);
1311
+ onTTFB(recordMetric);
1312
+ onINP(recordMetric);
1313
+ onFCP(recordMetric);
1314
+ const navEntry = performance.getEntriesByType("navigation")[0];
1315
+ if (navEntry) {
1316
+ this.metrics.navigation_timing = {
1317
+ dns_lookup: navEntry.domainLookupEnd - navEntry.domainLookupStart,
1318
+ tcp_connect: navEntry.connectEnd - navEntry.connectStart,
1319
+ request_time: navEntry.responseEnd - navEntry.requestStart,
1320
+ dom_load: navEntry.domComplete - navEntry.domInteractive,
1321
+ // NEW: raw fields matching Rust NavigationTiming struct
1322
+ domain_lookup_start: navEntry.domainLookupStart,
1323
+ domain_lookup_end: navEntry.domainLookupEnd,
1324
+ connect_start: navEntry.connectStart,
1325
+ connect_end: navEntry.connectEnd,
1326
+ secure_connection_start: navEntry.secureConnectionStart > 0 ? navEntry.secureConnectionStart : void 0
1327
+ };
1328
+ this.hasUpdates = true;
1329
+ }
1330
+ const resourceEntries = performance.getEntriesByType(
1331
+ "resource"
1332
+ );
1333
+ if (resourceEntries.length > 0) {
1334
+ this.metrics.resources = resourceEntries.filter((r) => !r.name.includes("/api/collect")).sort((a, b) => b.duration - a.duration).slice(0, 20).map((r) => ({
1335
+ name: r.name,
1336
+ initiatorType: r.initiatorType,
1337
+ duration: Math.round(r.duration),
1338
+ transferSize: r.transferSize > 0 ? r.transferSize : void 0,
1339
+ encodedBodySize: r.encodedBodySize > 0 ? r.encodedBodySize : void 0,
1340
+ decodedBodySize: r.decodedBodySize > 0 ? r.decodedBodySize : void 0,
1341
+ startTime: Math.round(r.startTime),
1342
+ responseEnd: Math.round(r.responseEnd)
1343
+ }));
1344
+ this.hasUpdates = true;
1345
+ }
1346
+ const mem = performance.memory;
1347
+ if (mem) {
1348
+ this.metrics.memory_usage = {
1349
+ used_js_heap_size: mem.usedJSHeapSize,
1350
+ total_js_heap_size: mem.totalJSHeapSize,
1351
+ js_heap_size_limit: mem.jsHeapSizeLimit
1352
+ };
1353
+ this.hasUpdates = true;
1354
+ }
1355
+ }
1356
+ getMetricsSnapshot() {
1357
+ if (!this.hasUpdates) return null;
1358
+ this.hasUpdates = false;
1359
+ const snapshot = { ...this.metrics };
1360
+ this.metrics.resources = void 0;
1361
+ return snapshot;
1362
+ }
1363
+ };
1364
+ var vitalsCollector = new VitalsCollector();
1365
+ var initVitals = (_tracker) => {
1366
+ if (process.env.NODE_ENV === "development") {
1367
+ console.log("[NexusHub] Web Vitals monitoring active");
1368
+ }
1369
+ };
1370
+
1371
+ // src/analytics/storage.ts
1372
+ var DB_NAME = "NexusHub_Analytics";
1373
+ var STORE_NAME = "events_queue";
1374
+ var DB_VERSION = 2;
1375
+ var MAX_QUEUE_SIZE = 500;
1376
+ var EventStorage = class {
1377
+ constructor() {
1378
+ this.db = null;
1379
+ this.isReady = this.init();
1380
+ }
1381
+ init() {
1382
+ if (typeof window === "undefined" || !window.indexedDB) {
1383
+ return Promise.resolve();
1384
+ }
1385
+ return new Promise((resolve) => {
1386
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
1387
+ request.onerror = () => {
1388
+ console.warn(
1389
+ "[NexusHub] Failed to open IndexedDB. Falling back to memory."
1390
+ );
1391
+ resolve();
1392
+ };
1393
+ request.onsuccess = (event) => {
1394
+ this.db = event.target.result;
1395
+ resolve();
1396
+ };
1397
+ request.onupgradeneeded = (event) => {
1398
+ const db = event.target.result;
1399
+ if (db.objectStoreNames.contains(STORE_NAME)) {
1400
+ db.deleteObjectStore(STORE_NAME);
1401
+ }
1402
+ db.createObjectStore(STORE_NAME, {
1403
+ keyPath: "id",
1404
+ autoIncrement: true
1405
+ });
1406
+ };
1407
+ });
1408
+ }
1409
+ /**
1410
+ * Add an event to the persistent queue.
1411
+ * Evicts the oldest entry if the queue is already at MAX_QUEUE_SIZE.
1412
+ */
1413
+ async enqueue(payload) {
1414
+ await this.isReady;
1415
+ if (!this.db) return;
1416
+ const currentCount = await this.count();
1417
+ if (currentCount >= MAX_QUEUE_SIZE) {
1418
+ const oldest = await this.peek(1);
1419
+ if (oldest.length > 0 && oldest[0].id !== void 0) {
1420
+ await this.remove([oldest[0].id]);
1421
+ }
1422
+ }
1423
+ return new Promise((resolve, reject) => {
1424
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
1425
+ const store = transaction.objectStore(STORE_NAME);
1426
+ const request = store.add({
1427
+ payload,
1428
+ timestamp: Date.now(),
1429
+ retryCount: 0
1430
+ });
1431
+ request.onsuccess = () => resolve();
1432
+ request.onerror = () => reject(request.error);
1433
+ });
1434
+ }
1435
+ /**
1436
+ * Get a batch of the oldest events without removing them.
1437
+ */
1438
+ async peek(limit = 20) {
1439
+ await this.isReady;
1440
+ if (!this.db) return [];
1441
+ return new Promise((resolve) => {
1442
+ const transaction = this.db.transaction([STORE_NAME], "readonly");
1443
+ const store = transaction.objectStore(STORE_NAME);
1444
+ const request = store.getAll(null, limit);
1445
+ request.onsuccess = () => resolve(request.result);
1446
+ request.onerror = () => resolve([]);
1447
+ });
1448
+ }
1449
+ /**
1450
+ * Remove events by ID after a successful upload.
1451
+ */
1452
+ async remove(ids) {
1453
+ await this.isReady;
1454
+ if (!this.db || ids.length === 0) return;
1455
+ return new Promise((resolve, reject) => {
1456
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
1457
+ const store = transaction.objectStore(STORE_NAME);
1458
+ transaction.oncomplete = () => resolve();
1459
+ transaction.onerror = () => reject(transaction.error);
1460
+ transaction.onabort = () => reject(new Error("Delete transaction aborted"));
1461
+ ids.forEach((id) => {
1462
+ store.delete(id);
1463
+ });
1464
+ });
1465
+ }
1466
+ /**
1467
+ * Count pending events in the queue.
1468
+ */
1469
+ async count() {
1470
+ await this.isReady;
1471
+ if (!this.db) return 0;
1472
+ return new Promise((resolve) => {
1473
+ const transaction = this.db.transaction([STORE_NAME], "readonly");
1474
+ const store = transaction.objectStore(STORE_NAME);
1475
+ const request = store.count();
1476
+ request.onsuccess = () => resolve(request.result);
1477
+ request.onerror = () => resolve(0);
1478
+ });
1479
+ }
1480
+ };
1481
+ var eventStorage = new EventStorage();
1482
+
1483
+ // src/analytics/tracker.ts
1484
+ var SDK_VERSION = "0.1.0";
1485
+ var safeGetItem = (key) => {
1486
+ try {
1487
+ return localStorage.getItem(key);
1488
+ } catch {
1489
+ return null;
1490
+ }
1491
+ };
1492
+ var safeSetItem = (key, value) => {
1493
+ try {
1494
+ localStorage.setItem(key, value);
1495
+ } catch {
1496
+ }
1497
+ };
1498
+ var safeRemoveItem = (key) => {
1499
+ try {
1500
+ localStorage.removeItem(key);
1501
+ } catch {
1502
+ }
1503
+ };
1504
+ var generateUUID = () => {
1505
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1506
+ return crypto.randomUUID();
1507
+ }
1508
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1509
+ const r = Math.random() * 16 | 0;
1510
+ const v = c === "x" ? r : r & 3 | 8;
1511
+ return v.toString(16);
1512
+ });
1513
+ };
1514
+ var Tracker = class {
1515
+ constructor(config) {
1516
+ this.sessionId = "";
1517
+ this.visitorId = "";
1518
+ this.anonymousId = "";
1519
+ this.isFlushing = false;
1520
+ this.config = config;
1521
+ this.endpoint = `${this.config.analyticsUrl}/api/collect`;
1522
+ this.circuitBreaker = new CircuitBreaker();
1523
+ this.sessionStart = Date.now();
1524
+ if (typeof window !== "undefined") {
1525
+ this.initSession();
1526
+ this.startFlushing();
1527
+ }
1528
+ }
1529
+ async initSession() {
1530
+ this.visitorId = await getVisitorId();
1531
+ let anonId = safeGetItem("nexus_anon_id");
1532
+ if (!anonId) {
1533
+ anonId = `anon_${generateUUID().replace(/-/g, "")}`;
1534
+ safeSetItem("nexus_anon_id", anonId);
1535
+ }
1536
+ this.anonymousId = anonId;
1537
+ let sid = safeGetItem("nexus_sid");
1538
+ const lastActivity = safeGetItem("nexus_last_active");
1539
+ const now = Date.now();
1540
+ const SESSION_TIMEOUT = 30 * 60 * 1e3;
1541
+ const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
1542
+ if (isExpired) {
1543
+ const uuid = generateUUID().replace(/-/g, "").substring(0, 16);
1544
+ sid = `sess_${uuid}_${now}`;
1545
+ safeSetItem("nexus_sid", sid);
1546
+ this.sessionStart = now;
1547
+ }
1548
+ safeSetItem("nexus_last_active", now.toString());
1549
+ this.sessionId = sid;
1550
+ }
1551
+ async send(eventType, data = {}, eventName, ecommerce) {
1552
+ if (typeof window === "undefined") return;
1553
+ safeSetItem("nexus_last_active", Date.now().toString());
1554
+ const entropy = await getDeviceEntropy();
1555
+ const perfMetrics = vitalsCollector.getMetricsSnapshot();
1556
+ const utmParams = extractUtmParams(window.location.href);
1557
+ const payload = {
1558
+ projectId: this.config.projectId,
1559
+ sessionId: this.sessionId,
1560
+ visitorId: this.visitorId,
1561
+ anonymousId: this.anonymousId,
1562
+ messageId: generateUUID(),
1563
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
1564
+ version: SDK_VERSION,
1565
+ url: window.location.href,
1566
+ referrer: document.referrer,
1567
+ userAgent: window.navigator.userAgent,
1568
+ screenWidth: window.screen.width,
1569
+ screenHeight: window.screen.height,
1570
+ language: window.navigator.language,
1571
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1572
+ eventType,
1573
+ eventName,
1574
+ eventData: data,
1575
+ ecommerce: ecommerce ?? void 0,
1576
+ performance: perfMetrics || void 0,
1577
+ context: {
1578
+ device: {
1579
+ hardwareConcurrency: entropy.hardware_concurrency,
1580
+ deviceMemory: entropy.device_memory,
1581
+ pixelRatio: entropy.pixel_ratio,
1582
+ canvasFingerprint: entropy.canvas_hash,
1583
+ platform: entropy.platform
1584
+ },
1585
+ visitorIdLocal: this.visitorId,
1586
+ utm: utmParams
1587
+ },
1588
+ clientTimestamp: (/* @__PURE__ */ new Date()).toISOString()
1589
+ };
1590
+ await eventStorage.enqueue(payload);
1591
+ const count = await eventStorage.count();
1592
+ if (count >= 10 || eventType === "purchase" || eventType === "identify") {
1593
+ this.flushQueue();
1594
+ }
1595
+ }
1596
+ startFlushing() {
1597
+ this.flushInterval = setInterval(() => this.flushQueue(), 5e3);
1598
+ window.addEventListener("visibilitychange", () => {
1599
+ if (document.visibilityState === "hidden") this.flushQueue(true);
1600
+ });
1601
+ }
1602
+ async flushQueue(useBeacon = false) {
1603
+ if (this.isFlushing) return;
1604
+ if (this.circuitBreaker.isOpen()) return;
1605
+ this.isFlushing = true;
1606
+ try {
1607
+ const storedEvents = await eventStorage.peek(20);
1608
+ if (storedEvents.length === 0) {
1609
+ this.isFlushing = false;
1610
+ return;
1611
+ }
1612
+ const payloads = storedEvents.map((e) => e.payload);
1613
+ const promises = payloads.map(
1614
+ (event) => fetch(this.endpoint, {
1615
+ method: "POST",
1616
+ headers: {
1617
+ "Content-Type": "application/json",
1618
+ Authorization: `Bearer ${this.config.apiKey}`
1619
+ },
1620
+ body: JSON.stringify(event),
1621
+ keepalive: useBeacon
1622
+ })
1623
+ );
1624
+ const results = await Promise.allSettled(promises);
1625
+ const successIds = [];
1626
+ let failureCount = 0;
1627
+ results.forEach((res, index) => {
1628
+ if (res.status === "fulfilled" && res.value.ok) {
1629
+ successIds.push(storedEvents[index].id);
1630
+ } else {
1631
+ failureCount++;
1632
+ }
1633
+ });
1634
+ if (successIds.length > 0) {
1635
+ await eventStorage.remove(successIds);
1636
+ this.circuitBreaker.recordSuccess();
1637
+ }
1638
+ if (failureCount > 0) {
1639
+ this.circuitBreaker.recordFailure();
1640
+ }
1641
+ } catch (err) {
1642
+ console.error("[NexusHub] Network Error:", err);
1643
+ this.circuitBreaker.recordFailure();
1644
+ } finally {
1645
+ this.isFlushing = false;
1646
+ if (!this.circuitBreaker.isOpen() && await eventStorage.count() > 0) {
1647
+ setTimeout(() => this.flushQueue(), 100);
1648
+ }
1649
+ }
1650
+ }
1651
+ getSession() {
1652
+ return this.sessionId;
1653
+ }
1654
+ getVisitorId() {
1655
+ return this.visitorId;
1656
+ }
1657
+ getAnonymousId() {
1658
+ return this.anonymousId;
1659
+ }
1660
+ getSessionDuration() {
1661
+ return Date.now() - this.sessionStart;
1662
+ }
1663
+ clearIdentity() {
1664
+ safeRemoveItem("nexus_anon_id");
1665
+ this.anonymousId = "";
1666
+ }
1667
+ stop() {
1668
+ if (this.flushInterval) {
1669
+ clearInterval(this.flushInterval);
1670
+ }
1671
+ this.flushQueue(true);
1672
+ }
1673
+ };
1674
+ function extractUtmParams(url) {
1675
+ try {
1676
+ const params = new URL(url).searchParams;
1677
+ const utmKeys = [
1678
+ "utm_source",
1679
+ "utm_medium",
1680
+ "utm_campaign",
1681
+ "utm_term",
1682
+ "utm_content"
1683
+ ];
1684
+ const result = {};
1685
+ utmKeys.forEach((key) => {
1686
+ const val = params.get(key);
1687
+ if (val) result[key] = val;
1688
+ });
1689
+ return result;
1690
+ } catch {
1691
+ return {};
1692
+ }
1693
+ }
1694
+
1695
+ // src/analytics/index.ts
1696
+ var safeRemoveItem2 = (key) => {
1697
+ try {
1698
+ localStorage.removeItem(key);
1699
+ } catch {
1700
+ }
1701
+ };
1702
+ var AnalyticsEngine = class {
1703
+ constructor(config) {
1704
+ this.cleanupFns = [];
1705
+ this.isInitialized = false;
1706
+ this.maxScrollDepth = 0;
1707
+ this.scrollThresholdsFired = /* @__PURE__ */ new Set();
1708
+ this.lastPath = typeof window !== "undefined" ? window.location.pathname : "";
1709
+ this.clickBuffer = [];
1710
+ this.tracker = new Tracker(config);
1711
+ }
1712
+ start() {
1713
+ if (this.isInitialized || typeof window === "undefined") return;
1714
+ this.isInitialized = true;
1715
+ this.pageView();
1716
+ initVitals(this.tracker);
1717
+ this.setupClickTracking();
1718
+ this.setupFormTracking();
1719
+ this.setupRouteTracking();
1720
+ this.setupShareTracking();
1721
+ this.setupScrollTracking();
1722
+ this.setupOutboundTracking();
1723
+ this.setupVideoTracking();
1724
+ this.setupErrorTracking();
1725
+ if (process.env.NODE_ENV === "development") {
1726
+ console.log("[NexusHub] \u{1F680} Analytics Engine Started");
1727
+ }
1728
+ }
1729
+ pageView(customReferrer) {
1730
+ if (typeof window === "undefined") return;
1731
+ const currentPath = window.location.pathname;
1732
+ this.maxScrollDepth = 0;
1733
+ this.scrollThresholdsFired.clear();
1734
+ this.tracker.send("page_view", {
1735
+ path: currentPath,
1736
+ search: window.location.search,
1737
+ title: document.title,
1738
+ timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
1739
+ referrer: customReferrer || (this.lastPath !== currentPath ? this.lastPath : document.referrer)
1740
+ });
1741
+ this.lastPath = currentPath;
1742
+ }
1743
+ setupShareTracking() {
1744
+ if (typeof window === "undefined") return;
1745
+ const copyHandler = () => {
1746
+ this.tracker.send("social_share", {
1747
+ method: "clipboard_copy",
1748
+ url: window.location.href
1749
+ });
1750
+ };
1751
+ window.addEventListener("copy", copyHandler, { passive: true });
1752
+ this.cleanupFns.push(() => window.removeEventListener("copy", copyHandler));
1753
+ if (typeof navigator !== "undefined" && navigator.share) {
1754
+ const originalShare = navigator.share.bind(navigator);
1755
+ navigator.share = (data) => {
1756
+ this.tracker.send("social_share", {
1757
+ method: "native_share_menu",
1758
+ url: data?.url || window.location.href,
1759
+ title: data?.title
1760
+ });
1761
+ return originalShare(data);
1762
+ };
1763
+ this.cleanupFns.push(() => {
1764
+ navigator.share = originalShare;
1765
+ });
1766
+ }
1767
+ }
1768
+ setupScrollTracking() {
1769
+ if (typeof window === "undefined") return;
1770
+ const THRESHOLDS = [25, 50, 75, 100];
1771
+ const scrollHandler = () => {
1772
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
1773
+ const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
1774
+ if (docHeight <= 0) return;
1775
+ const pct = Math.round(scrollTop / docHeight * 100);
1776
+ if (pct > this.maxScrollDepth) {
1777
+ this.maxScrollDepth = pct;
1778
+ }
1779
+ clearTimeout(this.scrollTimer);
1780
+ this.scrollTimer = setTimeout(() => {
1781
+ for (const threshold of THRESHOLDS) {
1782
+ if (this.maxScrollDepth >= threshold && !this.scrollThresholdsFired.has(threshold)) {
1783
+ this.scrollThresholdsFired.add(threshold);
1784
+ this.tracker.send("scroll", {
1785
+ depth: threshold / 100,
1786
+ depth_percent: threshold,
1787
+ path: window.location.pathname
1788
+ });
1789
+ }
1790
+ }
1791
+ }, 300);
1792
+ };
1793
+ window.addEventListener("scroll", scrollHandler, { passive: true });
1794
+ this.cleanupFns.push(() => {
1795
+ window.removeEventListener("scroll", scrollHandler);
1796
+ clearTimeout(this.scrollTimer);
1797
+ });
1798
+ }
1799
+ setupClickTracking() {
1800
+ if (typeof window === "undefined") return;
1801
+ const clickHandler = (e) => {
1802
+ const target = e.target;
1803
+ const link = target.closest("a");
1804
+ if (link) {
1805
+ this.tracker.send("click", {
1806
+ element_type: "link",
1807
+ href: link.href,
1808
+ text: link.innerText?.substring(0, 50),
1809
+ id: link.id,
1810
+ classes: link.className,
1811
+ dataset: { ...link.dataset },
1812
+ coordinates: { x: e.clientX, y: e.clientY }
1813
+ });
1814
+ }
1815
+ const button = target.closest("button");
1816
+ if (button) {
1817
+ this.tracker.send("click", {
1818
+ element_type: "button",
1819
+ text: button.innerText?.substring(0, 50),
1820
+ id: button.id,
1821
+ classes: button.className,
1822
+ coordinates: { x: e.clientX, y: e.clientY }
1823
+ });
1824
+ }
1825
+ const now = Date.now();
1826
+ const ZONE = 400;
1827
+ const WINDOW_MS = 1e3;
1828
+ const RAGE_THRESHOLD = 3;
1829
+ this.clickBuffer.push({ x: e.clientX, y: e.clientY, t: now });
1830
+ this.clickBuffer = this.clickBuffer.filter((c) => now - c.t < WINDOW_MS);
1831
+ const zone = {
1832
+ x: Math.floor(e.clientX / ZONE),
1833
+ y: Math.floor(e.clientY / ZONE)
1834
+ };
1835
+ const zoneClicks = this.clickBuffer.filter(
1836
+ (c) => Math.floor(c.x / ZONE) === zone.x && Math.floor(c.y / ZONE) === zone.y
1837
+ );
1838
+ if (zoneClicks.length >= RAGE_THRESHOLD) {
1839
+ this.tracker.send(
1840
+ "click",
1841
+ {
1842
+ element_type: target.tagName.toLowerCase(),
1843
+ event_name: "rage_click",
1844
+ coordinates: { x: e.clientX, y: e.clientY },
1845
+ click_count: zoneClicks.length,
1846
+ path: window.location.pathname
1847
+ },
1848
+ "rage_click"
1849
+ );
1850
+ this.clickBuffer = [];
1851
+ }
1852
+ const isInteractive = target.closest(
1853
+ "a, button, input, select, textarea, [onclick], [role='button']"
1854
+ );
1855
+ if (!isInteractive) {
1856
+ const pathBefore = window.location.pathname;
1857
+ setTimeout(() => {
1858
+ const pathAfter = window.location.pathname;
1859
+ if (pathBefore === pathAfter) {
1860
+ this.tracker.send(
1861
+ "click",
1862
+ {
1863
+ element_type: target.tagName.toLowerCase(),
1864
+ event_name: "dead_click",
1865
+ selector: getSelector(target),
1866
+ coordinates: { x: e.clientX, y: e.clientY },
1867
+ path: window.location.pathname
1868
+ },
1869
+ "dead_click"
1870
+ );
1871
+ }
1872
+ }, 300);
1873
+ }
1874
+ };
1875
+ window.addEventListener("click", clickHandler, { passive: true });
1876
+ this.cleanupFns.push(
1877
+ () => window.removeEventListener("click", clickHandler)
1878
+ );
1879
+ }
1880
+ setupFormTracking() {
1881
+ if (typeof document === "undefined") return;
1882
+ const submitHandler = (e) => {
1883
+ const form = e.target;
1884
+ if (form) {
1885
+ this.tracker.send("form_submit", {
1886
+ form_id: form.id || form.name || "unknown_form",
1887
+ action: form.action,
1888
+ method: form.method,
1889
+ field_count: form.elements.length
1890
+ });
1891
+ }
1892
+ };
1893
+ document.addEventListener("submit", submitHandler, { passive: true });
1894
+ this.cleanupFns.push(
1895
+ () => document.removeEventListener("submit", submitHandler)
1896
+ );
1897
+ }
1898
+ setupOutboundTracking() {
1899
+ if (typeof window === "undefined") return;
1900
+ const outboundHandler = (e) => {
1901
+ const link = e.target.closest("a");
1902
+ if (!link || !link.href) return;
1903
+ try {
1904
+ const linkHost = new URL(link.href).hostname;
1905
+ if (linkHost && linkHost !== window.location.hostname) {
1906
+ this.tracker.send(
1907
+ "click",
1908
+ {
1909
+ event_name: "outbound_click",
1910
+ href: link.href,
1911
+ text: link.innerText?.substring(0, 50),
1912
+ destination_host: linkHost,
1913
+ coordinates: { x: e.clientX, y: e.clientY }
1914
+ },
1915
+ "outbound_click"
1916
+ );
1917
+ }
1918
+ } catch {
1919
+ }
1920
+ };
1921
+ window.addEventListener("click", outboundHandler, { passive: true });
1922
+ this.cleanupFns.push(
1923
+ () => window.removeEventListener("click", outboundHandler)
1924
+ );
1925
+ }
1926
+ setupVideoTracking() {
1927
+ if (typeof document === "undefined") return;
1928
+ const attachVideoListeners = (video) => {
1929
+ if (video.__nexus_tracked) return;
1930
+ video.__nexus_tracked = true;
1931
+ const src = video.src || video.currentSrc || "unknown";
1932
+ let milestone50Fired = false;
1933
+ video.addEventListener("play", () => {
1934
+ this.tracker.send(
1935
+ "custom_event",
1936
+ { event_name: "video_play", src },
1937
+ "video_play"
1938
+ );
1939
+ });
1940
+ video.addEventListener("pause", () => {
1941
+ this.tracker.send(
1942
+ "custom_event",
1943
+ {
1944
+ event_name: "video_pause",
1945
+ src,
1946
+ position_seconds: Math.round(video.currentTime)
1947
+ },
1948
+ "video_pause"
1949
+ );
1950
+ });
1951
+ video.addEventListener("timeupdate", () => {
1952
+ if (!video.duration || video.duration === Infinity) return;
1953
+ const pct = video.currentTime / video.duration;
1954
+ if (pct >= 0.5 && !milestone50Fired) {
1955
+ milestone50Fired = true;
1956
+ this.tracker.send(
1957
+ "custom_event",
1958
+ {
1959
+ event_name: "video_50_percent",
1960
+ src
1961
+ },
1962
+ "video_50_percent"
1963
+ );
1964
+ }
1965
+ });
1966
+ video.addEventListener("ended", () => {
1967
+ this.tracker.send(
1968
+ "custom_event",
1969
+ {
1970
+ event_name: "video_complete",
1971
+ src,
1972
+ duration_seconds: Math.round(video.duration)
1973
+ },
1974
+ "video_complete"
1975
+ );
1976
+ });
1977
+ };
1978
+ document.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
1979
+ const observer = new MutationObserver((mutations) => {
1980
+ mutations.forEach((m) => {
1981
+ m.addedNodes.forEach((node) => {
1982
+ if (node instanceof HTMLVideoElement) {
1983
+ attachVideoListeners(node);
1984
+ }
1985
+ if (node instanceof Element) {
1986
+ node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
1987
+ }
1988
+ });
1989
+ });
1990
+ });
1991
+ observer.observe(document.body, { childList: true, subtree: true });
1992
+ this.cleanupFns.push(() => observer.disconnect());
1993
+ }
1994
+ setupErrorTracking() {
1995
+ if (typeof window === "undefined") return;
1996
+ const errorHandler = (e) => {
1997
+ this.tracker.send("error", {
1998
+ message: e.message,
1999
+ filename: e.filename,
2000
+ lineno: e.lineno,
2001
+ colno: e.colno,
2002
+ stack: e.error?.stack,
2003
+ url: window.location.href,
2004
+ type: "uncaught_error"
2005
+ });
2006
+ };
2007
+ const rejectionHandler = (e) => {
2008
+ const reason = e.reason instanceof Error ? e.reason.message : String(e.reason);
2009
+ const stack = e.reason instanceof Error ? e.reason.stack : void 0;
2010
+ this.tracker.send("error", {
2011
+ message: reason,
2012
+ stack,
2013
+ url: window.location.href,
2014
+ type: "unhandled_rejection"
2015
+ });
2016
+ };
2017
+ window.addEventListener("error", errorHandler);
2018
+ window.addEventListener("unhandledrejection", rejectionHandler);
2019
+ this.cleanupFns.push(() => {
2020
+ window.removeEventListener("error", errorHandler);
2021
+ window.removeEventListener("unhandledrejection", rejectionHandler);
2022
+ });
2023
+ }
2024
+ setupRouteTracking() {
2025
+ if (typeof window === "undefined" || typeof window.history === "undefined")
2026
+ return;
2027
+ const originalPushState = history.pushState.bind(history);
2028
+ const originalReplaceState = history.replaceState.bind(history);
2029
+ history.pushState = (...args) => {
2030
+ const prevPath = window.location.pathname;
2031
+ originalPushState(...args);
2032
+ if (!this.isInitializedByReactProvider()) {
2033
+ this.pageView(prevPath);
2034
+ }
2035
+ };
2036
+ history.replaceState = (...args) => {
2037
+ originalReplaceState(...args);
2038
+ };
2039
+ const popStateHandler = () => {
2040
+ if (!this.isInitializedByReactProvider()) {
2041
+ this.pageView();
2042
+ }
2043
+ };
2044
+ window.addEventListener("popstate", popStateHandler);
2045
+ this.cleanupFns.push(() => {
2046
+ history.pushState = originalPushState;
2047
+ history.replaceState = originalReplaceState;
2048
+ window.removeEventListener("popstate", popStateHandler);
2049
+ });
2050
+ }
2051
+ isInitializedByReactProvider() {
2052
+ return !!document.getElementById("__nexus_react_active");
2053
+ }
2054
+ async identify(userId, traits = {}) {
2055
+ return this.sendIdentityRequest("identify", { user_id: userId, traits });
2056
+ }
2057
+ async group(groupId, traits = {}) {
2058
+ return this.sendIdentityRequest("group", { group_id: groupId, traits });
2059
+ }
2060
+ async alias(newId) {
2061
+ return this.sendIdentityRequest("alias", {
2062
+ previous_id: this.tracker.getSession(),
2063
+ user_id: newId
2064
+ });
2065
+ }
2066
+ reset(performGdprScrub = false) {
2067
+ const config = this.tracker.config;
2068
+ this.tracker.stop();
2069
+ localStorage.removeItem("nexus_sid");
2070
+ localStorage.removeItem("nexus_vid");
2071
+ safeRemoveItem2("nexus_anon_id");
2072
+ this.tracker.clearIdentity();
2073
+ if (performGdprScrub) {
2074
+ const endpoint = `${config.analyticsUrl}/api/privacy/scrub`;
2075
+ const userId = this.tracker.getVisitorId();
2076
+ fetch(endpoint, {
2077
+ method: "DELETE",
2078
+ headers: {
2079
+ "Content-Type": "application/json",
2080
+ Authorization: `Bearer ${config.apiKey}`
2081
+ },
2082
+ body: JSON.stringify({
2083
+ project_id: config.projectId,
2084
+ user_id: userId
2085
+ })
2086
+ }).catch((err) => console.error("[NexusHub] Privacy scrub failed:", err));
2087
+ }
2088
+ window.location.reload();
2089
+ }
2090
+ track(eventName, properties = {}) {
2091
+ this.tracker.send(
2092
+ "custom_event",
2093
+ { event_name: eventName, ...properties },
2094
+ eventName
2095
+ );
2096
+ }
2097
+ trackPurchase(orderData) {
2098
+ const ecommerce = {
2099
+ orderId: orderData.orderId,
2100
+ total: orderData.total,
2101
+ revenue: orderData.revenue ?? orderData.total,
2102
+ currency: orderData.currency || "USD",
2103
+ products: orderData.products.map((p) => ({
2104
+ productId: p.id,
2105
+ sku: p.sku,
2106
+ name: p.name,
2107
+ price: p.price,
2108
+ quantity: p.quantity
2109
+ }))
2110
+ };
2111
+ this.tracker.send(
2112
+ "purchase",
2113
+ { order_id: orderData.orderId },
2114
+ "purchase",
2115
+ ecommerce
2116
+ );
2117
+ }
2118
+ trackError(error, context) {
2119
+ this.tracker.send("error", {
2120
+ message: error.message,
2121
+ stack: error.stack,
2122
+ context,
2123
+ url: typeof window !== "undefined" ? window.location.href : "",
2124
+ type: "manual"
2125
+ });
2126
+ }
2127
+ async sendIdentityRequest(type, data) {
2128
+ try {
2129
+ const config = this.tracker.config;
2130
+ const endpoint = `${config.analyticsUrl}/api/${type}`;
2131
+ const payload = {
2132
+ projectId: config.projectId,
2133
+ sessionId: this.tracker.getSession(),
2134
+ visitorId: this.tracker.getVisitorId(),
2135
+ anonymousId: this.tracker.getAnonymousId(),
2136
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2137
+ ...data
2138
+ };
2139
+ const response = await fetch(endpoint, {
2140
+ method: "POST",
2141
+ headers: {
2142
+ "Content-Type": "application/json",
2143
+ Authorization: `Bearer ${config.apiKey}`
2144
+ },
2145
+ body: JSON.stringify(payload)
2146
+ });
2147
+ return response.ok;
2148
+ } catch (error) {
2149
+ console.error(`[NexusHub] ${type} failed:`, error);
2150
+ return false;
2151
+ }
2152
+ }
2153
+ getSessionId() {
2154
+ return this.tracker.getSession();
2155
+ }
2156
+ stop(isFinalShutdown = false) {
2157
+ this.cleanupFns.forEach((fn) => fn());
2158
+ this.cleanupFns = [];
2159
+ if (isFinalShutdown) {
2160
+ this.tracker.send("session_end", {
2161
+ session_id: this.tracker.getSession(),
2162
+ duration: this.tracker.getSessionDuration(),
2163
+ max_scroll_depth: this.maxScrollDepth / 100
2164
+ });
2165
+ }
2166
+ this.tracker.stop();
2167
+ this.isInitialized = false;
2168
+ }
2169
+ };
2170
+ function getSelector(el, depth = 0) {
2171
+ if (!el || depth > 2) return "";
2172
+ const tag = el.tagName.toLowerCase();
2173
+ const id = el.id ? `#${el.id}` : "";
2174
+ const cls = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
2175
+ const self = `${tag}${id}${cls}`;
2176
+ const parent = el.parentElement && depth < 2 ? `${getSelector(el.parentElement, depth + 1)} > ` : "";
2177
+ return `${parent}${self}`;
2178
+ }
2179
+
2180
+ // src/client.ts
2181
+ var NexusClient = class {
2182
+ constructor(config) {
2183
+ const fullConfig = getFullConfig(config);
2184
+ this.config = {
2185
+ debug: config?.debug ?? false,
2186
+ cacheStrategy: config?.cacheStrategy ?? "memory",
2187
+ revalidateTime: config?.revalidateTime ?? 60,
2188
+ timeout: config?.timeout ?? 1e4,
2189
+ retries: config?.retries ?? 3,
2190
+ ...fullConfig
2191
+ };
2192
+ const errors = validateConfig(this.config);
2193
+ if (errors.length > 0) {
2194
+ console.warn("\u26A0\uFE0F NexusHub: Configuration issues:", errors.join(", "));
2195
+ if (!this.config.projectId) {
2196
+ console.warn("\u26A0\uFE0F NexusHub: No Project ID found. Tracking will fail.");
2197
+ }
2198
+ }
2199
+ this.content = new ContentEngine(this.config);
2200
+ if (typeof window !== "undefined") {
2201
+ this.analytics = new AnalyticsEngine(this.config);
2202
+ this.analytics.start();
2203
+ }
2204
+ }
2205
+ /**
2206
+ * Helper alias for cleaner content fetching.
2207
+ */
2208
+ getPage(slug, options) {
2209
+ return this.content.getPage(slug, options);
2210
+ }
2211
+ /**
2212
+ * Returns a readonly snapshot of the current config.
2213
+ */
2214
+ getConfig() {
2215
+ return { ...this.config };
2216
+ }
2217
+ /**
2218
+ * Updates specific config fields at runtime.
2219
+ * Replaces the previous pattern of `(nexus as any).config.projectId = x`
2220
+ * which bypassed TypeScript and mutated internal state unsafely.
2221
+ */
2222
+ updateConfig(updates) {
2223
+ this.config = { ...this.config, ...updates };
2224
+ }
2225
+ };
2226
+ var nexus = new NexusClient();
2227
+
2228
+ // src/auth/context.tsx
2229
+ import {
2230
+ createContext,
2231
+ useContext,
2232
+ useEffect,
2233
+ useState,
2234
+ useCallback
2235
+ } from "react";
2236
+ import { jsx } from "react/jsx-runtime";
2237
+ var AuthContext = createContext(null);
2238
+ var AuthProvider = ({
2239
+ children,
2240
+ config
2241
+ }) => {
2242
+ const [user, setUser] = useState(null);
2243
+ const [isLoading, setIsLoading] = useState(true);
2244
+ const [error, setError] = useState(null);
2245
+ const AUTH_BASE = `${config.apiUrl}/auth/project/${config.projectId}`;
2246
+ const checkSession = useCallback(async () => {
2247
+ try {
2248
+ const res = await fetch(`${AUTH_BASE}/me`, {
2249
+ headers: getHeaders(config)
2250
+ });
2251
+ if (res.ok) {
2252
+ const data = await res.json();
2253
+ setUser(data.user);
2254
+ if (nexus.analytics) {
2255
+ nexus.analytics.identify(data.user.id, {
2256
+ email: data.user.email,
2257
+ role: data.user.role,
2258
+ ...data.user.metadata
2259
+ });
2260
+ }
2261
+ } else {
2262
+ setUser(null);
2263
+ }
2264
+ } catch (err) {
2265
+ console.debug("[NexusHub Auth] Session check failed:", err);
2266
+ setUser(null);
2267
+ } finally {
2268
+ setIsLoading(false);
2269
+ }
2270
+ }, [AUTH_BASE, config]);
2271
+ useEffect(() => {
2272
+ checkSession();
2273
+ }, [checkSession]);
2274
+ const login = async (creds) => {
2275
+ setIsLoading(true);
2276
+ setError(null);
2277
+ try {
2278
+ const res = await fetch(`${AUTH_BASE}/login`, {
2279
+ method: "POST",
2280
+ headers: getHeaders(config),
2281
+ body: JSON.stringify(creds)
2282
+ });
2283
+ if (!res.ok) throw await parseError(res);
2284
+ const data = await res.json();
2285
+ setUser(data.user);
2286
+ if (nexus.analytics) {
2287
+ await nexus.analytics.identify(data.user.id, {
2288
+ email: data.user.email,
2289
+ role: data.user.role,
2290
+ login_method: "email",
2291
+ ...data.user.metadata
2292
+ });
2293
+ }
2294
+ } catch (err) {
2295
+ setError(err);
2296
+ throw err;
2297
+ } finally {
2298
+ setIsLoading(false);
2299
+ }
2300
+ };
2301
+ const register = async (creds) => {
2302
+ setIsLoading(true);
2303
+ setError(null);
2304
+ try {
2305
+ const res = await fetch(`${AUTH_BASE}/register`, {
2306
+ method: "POST",
2307
+ headers: getHeaders(config),
2308
+ body: JSON.stringify(creds)
2309
+ });
2310
+ if (!res.ok) throw await parseError(res);
2311
+ const data = await res.json();
2312
+ setUser(data.user);
2313
+ if (nexus.analytics) {
2314
+ nexus.analytics.track("signup", { method: "email" });
2315
+ nexus.analytics.identify(data.user.id, {
2316
+ email: data.user.email,
2317
+ role: data.user.role,
2318
+ ...data.user.metadata
2319
+ });
2320
+ }
2321
+ } catch (err) {
2322
+ setError(err);
2323
+ throw err;
2324
+ } finally {
2325
+ setIsLoading(false);
2326
+ }
2327
+ };
2328
+ const logout = async () => {
2329
+ setIsLoading(true);
2330
+ try {
2331
+ await fetch(`${AUTH_BASE}/logout`, {
2332
+ method: "POST",
2333
+ headers: getHeaders(config)
2334
+ });
2335
+ } catch (e) {
2336
+ console.warn("Logout network error", e);
2337
+ } finally {
2338
+ setUser(null);
2339
+ setIsLoading(false);
2340
+ if (nexus.analytics) {
2341
+ nexus.analytics.track("logout");
2342
+ }
2343
+ }
2344
+ };
2345
+ const updateProfile = async (updates) => {
2346
+ try {
2347
+ const res = await fetch(`${AUTH_BASE}/profile`, {
2348
+ method: "PATCH",
2349
+ headers: getHeaders(config),
2350
+ body: JSON.stringify(updates)
2351
+ });
2352
+ if (!res.ok) throw await parseError(res);
2353
+ const data = await res.json();
2354
+ setUser(data.user);
2355
+ if (nexus.analytics) {
2356
+ nexus.analytics.identify(data.user.id, updates);
2357
+ }
2358
+ } catch (err) {
2359
+ setError(err);
2360
+ throw err;
2361
+ }
2362
+ };
2363
+ const requestPasswordReset = async (email) => {
2364
+ const res = await fetch(`${AUTH_BASE}/password/reset-request`, {
2365
+ method: "POST",
2366
+ headers: getHeaders(config),
2367
+ body: JSON.stringify({ email })
2368
+ });
2369
+ if (!res.ok) throw await parseError(res);
2370
+ };
2371
+ return /* @__PURE__ */ jsx(
2372
+ AuthContext.Provider,
2373
+ {
2374
+ value: {
2375
+ user,
2376
+ isLoading,
2377
+ error,
2378
+ isAuthenticated: !!user,
2379
+ login,
2380
+ register,
2381
+ logout,
2382
+ updateProfile,
2383
+ requestPasswordReset
2384
+ },
2385
+ children
2386
+ }
2387
+ );
2388
+ };
2389
+ var useNexusAuth = () => {
2390
+ const context = useContext(AuthContext);
2391
+ if (!context) {
2392
+ throw new Error("useNexusAuth must be used within a NexusProvider");
2393
+ }
2394
+ return context;
2395
+ };
2396
+ function getHeaders(config) {
2397
+ return {
2398
+ "Content-Type": "application/json",
2399
+ "x-nexus-project": config.projectId,
2400
+ Authorization: `Bearer ${config.apiKey}`
2401
+ // 👈 CRITICAL: Send the nx_pk_ key
2402
+ // Note: We do NOT send the Master API Key here.
2403
+ // This is client-side. The backend uses Cookies or public tokens.
2404
+ };
2405
+ }
2406
+ async function parseError(res) {
2407
+ try {
2408
+ const json = await res.json();
2409
+ return {
2410
+ status: res.status,
2411
+ code: json.code || "UNKNOWN_ERROR",
2412
+ message: json.message || "An error occurred during authentication"
2413
+ };
2414
+ } catch {
2415
+ return {
2416
+ status: res.status,
2417
+ code: "NETWORK_ERROR",
2418
+ message: res.statusText
2419
+ };
2420
+ }
2421
+ }
2422
+
2423
+ // src/components/NexusProvider.tsx
2424
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
2425
+ var NexusContext = createContext2(nexus);
2426
+ var NexusLiveFeedContext = createContext2({
2427
+ latestEvent: null,
2428
+ isConnected: false
2429
+ });
2430
+ var useNexusLiveFeed = () => React2.useContext(NexusLiveFeedContext);
2431
+ function NexusAnalyticsTracker({
2432
+ disableAnalytics
2433
+ }) {
2434
+ const pathname = usePathname();
2435
+ const searchParams = useSearchParams();
2436
+ const prevPathRef = useRef("");
2437
+ const searchParamsString = searchParams.toString();
2438
+ useEffect2(() => {
2439
+ if (nexus.analytics && !disableAnalytics) {
2440
+ nexus.analytics.pageView(prevPathRef.current || document.referrer);
2441
+ prevPathRef.current = pathname;
2442
+ }
2443
+ }, [pathname, searchParamsString, disableAnalytics]);
2444
+ return /* @__PURE__ */ jsx2("div", { id: "__nexus_react_active", style: { display: "none" } });
2445
+ }
2446
+ var NexusProvider = ({
2447
+ children,
2448
+ projectId,
2449
+ disableAnalytics = false,
2450
+ hasConsent = true,
2451
+ // NEW: default true to preserve existing behaviour
2452
+ enableLiveFeed = false,
2453
+ // NEW
2454
+ onLiveEvent
2455
+ // NEW
2456
+ }) => {
2457
+ const isInitialized = useRef(false);
2458
+ const socketRef = useRef(null);
2459
+ const [latestEvent, setLatestEvent] = useState2(
2460
+ null
2461
+ );
2462
+ const [isConnected, setIsConnected] = useState2(false);
2463
+ const liveFeedValue = useMemo(
2464
+ () => ({ latestEvent, isConnected }),
2465
+ [latestEvent, isConnected]
2466
+ );
2467
+ const config = useMemo(() => {
2468
+ if (projectId && nexus.getConfig().projectId !== projectId) {
2469
+ nexus.updateConfig({ projectId });
2470
+ }
2471
+ return nexus.getConfig();
2472
+ }, [projectId]);
2473
+ useEffect2(() => {
2474
+ if (typeof window === "undefined" || disableAnalytics || !hasConsent)
2475
+ return;
2476
+ if (!isInitialized.current) {
2477
+ if (!nexus.analytics) {
2478
+ nexus.analytics = new AnalyticsEngine(nexus.getConfig());
2479
+ }
2480
+ nexus.analytics.start();
2481
+ isInitialized.current = true;
2482
+ if (nexus.getConfig().debug) {
2483
+ console.log("[NexusHub] \u{1F680} Provider initialized analytics");
2484
+ }
2485
+ }
2486
+ return () => {
2487
+ if (nexus.analytics) {
2488
+ nexus.analytics.stop();
2489
+ isInitialized.current = false;
2490
+ }
2491
+ };
2492
+ }, [disableAnalytics, hasConsent]);
2493
+ const connectLiveFeed = useCallback2(async () => {
2494
+ if (typeof window === "undefined" || !enableLiveFeed) return;
2495
+ try {
2496
+ const { io } = await import("socket.io-client");
2497
+ const cfg = nexus.getConfig();
2498
+ const wsUrl = cfg.apiUrl || "http://localhost:3001";
2499
+ const socket = io(`${wsUrl}/analytics`, {
2500
+ auth: { token: cfg.apiKey },
2501
+ query: { projectId: cfg.projectId },
2502
+ transports: ["websocket"],
2503
+ reconnectionAttempts: 5,
2504
+ reconnectionDelay: 2e3
2505
+ });
2506
+ socket.on("connect", () => {
2507
+ setIsConnected(true);
2508
+ if (cfg.debug) console.log("[NexusHub] \u{1F534} Live feed connected");
2509
+ });
2510
+ socket.on("disconnect", () => {
2511
+ setIsConnected(false);
2512
+ if (cfg.debug) console.log("[NexusHub] Live feed disconnected");
2513
+ });
2514
+ socket.on("live_event", (event) => {
2515
+ setLatestEvent(event);
2516
+ onLiveEvent?.(event);
2517
+ });
2518
+ socketRef.current = socket;
2519
+ } catch (err) {
2520
+ console.error("[NexusHub] Live feed connection failed:", err);
2521
+ }
2522
+ }, [enableLiveFeed, onLiveEvent]);
2523
+ useEffect2(() => {
2524
+ if (enableLiveFeed) {
2525
+ connectLiveFeed();
2526
+ }
2527
+ return () => {
2528
+ if (socketRef.current) {
2529
+ socketRef.current.disconnect();
2530
+ socketRef.current = null;
2531
+ setIsConnected(false);
2532
+ }
2533
+ };
2534
+ }, [enableLiveFeed, connectLiveFeed]);
2535
+ return /* @__PURE__ */ jsx2(NexusContext.Provider, { value: nexus, children: /* @__PURE__ */ jsx2(NexusLiveFeedContext.Provider, { value: liveFeedValue, children: /* @__PURE__ */ jsxs(AuthProvider, { config, children: [
2536
+ /* @__PURE__ */ jsx2(Suspense, { fallback: null, children: /* @__PURE__ */ jsx2(NexusAnalyticsTracker, { disableAnalytics }) }),
2537
+ children
2538
+ ] }) }) });
2539
+ };
2540
+ var useNexus = () => {
2541
+ const context = React2.useContext(NexusContext);
2542
+ if (!context) {
2543
+ throw new Error("useNexus must be used within a NexusProvider");
2544
+ }
2545
+ return context;
2546
+ };
2547
+ var useNexusAnalytics = () => {
2548
+ const client = useNexus();
2549
+ if (!client.analytics) {
2550
+ return {
2551
+ track: () => {
2552
+ },
2553
+ identify: async () => false,
2554
+ group: async () => false,
2555
+ alias: async () => false,
2556
+ trackPurchase: () => {
2557
+ },
2558
+ trackError: () => {
2559
+ },
2560
+ reset: () => {
2561
+ }
2562
+ };
2563
+ }
2564
+ return {
2565
+ track: client.analytics.track.bind(client.analytics),
2566
+ identify: client.analytics.identify.bind(client.analytics),
2567
+ group: client.analytics.group.bind(client.analytics),
2568
+ alias: client.analytics.alias.bind(client.analytics),
2569
+ trackPurchase: client.analytics.trackPurchase.bind(client.analytics),
2570
+ trackError: client.analytics.trackError.bind(client.analytics),
2571
+ reset: client.analytics.reset.bind(client.analytics)
2572
+ };
2573
+ };
2574
+
2575
+ // src/components/NexusRenders.tsx
2576
+ import { useState as useState3 } from "react";
2577
+ import * as LucideIcons from "lucide-react";
2578
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2579
+ function NexusRichText({ value, className = "" }) {
2580
+ if (!value) return null;
2581
+ return /* @__PURE__ */ jsx3(
2582
+ "div",
2583
+ {
2584
+ className: `prose dark:prose-invert max-w-none text-foreground leading-relaxed ${className}`,
2585
+ dangerouslySetInnerHTML: { __html: value }
2586
+ }
2587
+ );
2588
+ }
2589
+ function NexusLongText({ value, className = "" }) {
2590
+ if (!value) return null;
2591
+ return /* @__PURE__ */ jsx3(
2592
+ "p",
2593
+ {
2594
+ className: `text-sm text-muted-foreground whitespace-pre-line leading-relaxed ${className}`,
2595
+ dangerouslySetInnerHTML: { __html: value }
2596
+ }
2597
+ );
2598
+ }
2599
+ function NexusIcon({
2600
+ name,
2601
+ className = "",
2602
+ size = 20,
2603
+ strokeWidth = 2
2604
+ }) {
2605
+ if (!name) return null;
2606
+ const IconComponent = LucideIcons[name];
2607
+ if (!IconComponent) {
2608
+ const Fallback = LucideIcons.HelpCircle;
2609
+ return /* @__PURE__ */ jsx3(Fallback, { className, size, strokeWidth });
2610
+ }
2611
+ return /* @__PURE__ */ jsx3(
2612
+ IconComponent,
2613
+ {
2614
+ className,
2615
+ size,
2616
+ strokeWidth
2617
+ }
2618
+ );
2619
+ }
2620
+ function NexusImage({
2621
+ value,
2622
+ className = "",
2623
+ alt = "",
2624
+ ...props
2625
+ }) {
2626
+ if (!value) return null;
2627
+ const imageUrl = typeof value === "string" ? value : value.url;
2628
+ const imageAlt = typeof value === "string" ? alt : value.alt || alt;
2629
+ if (!imageUrl) return null;
2630
+ return (
2631
+ /* eslint-disable-next-line @next/next/no-img-element */
2632
+ /* @__PURE__ */ jsx3(
2633
+ "img",
2634
+ {
2635
+ src: imageUrl,
2636
+ alt: imageAlt,
2637
+ className: `max-w-full h-auto object-cover rounded-xl ${className}`,
2638
+ ...props
2639
+ }
2640
+ )
2641
+ );
2642
+ }
2643
+ function NexusGallery({
2644
+ value,
2645
+ className = "",
2646
+ imageClassName = ""
2647
+ }) {
2648
+ if (!value || value.length === 0) return null;
2649
+ return /* @__PURE__ */ jsx3(
2650
+ "div",
2651
+ {
2652
+ className: `grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 ${className}`,
2653
+ children: value.map((img, idx) => /* @__PURE__ */ jsx3(
2654
+ "div",
2655
+ {
2656
+ className: "overflow-hidden rounded-xl aspect-square bg-muted/20 border border-border",
2657
+ children: /* @__PURE__ */ jsx3(
2658
+ NexusImage,
2659
+ {
2660
+ value: img,
2661
+ className: `w-full h-full object-cover hover:scale-105 transition-transform duration-500 ${imageClassName}`
2662
+ }
2663
+ )
2664
+ },
2665
+ idx
2666
+ ))
2667
+ }
2668
+ );
2669
+ }
2670
+ function NexusVideo({
2671
+ value,
2672
+ className = "",
2673
+ autoplay = false
2674
+ }) {
2675
+ if (!value) return null;
2676
+ const isYouTube = value.includes("youtube.com") || value.includes("youtu.be");
2677
+ const isVimeo = value.includes("vimeo.com");
2678
+ if (isYouTube) {
2679
+ const videoId = value.includes("youtu.be") ? value.split("/").pop() : value.split("v=")[1]?.split("&")[0];
2680
+ return /* @__PURE__ */ jsx3(
2681
+ "iframe",
2682
+ {
2683
+ src: `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}`,
2684
+ className: `w-full aspect-video rounded-xl border-0 ${className}`,
2685
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
2686
+ allowFullScreen: true,
2687
+ title: "YouTube Video"
2688
+ }
2689
+ );
2690
+ }
2691
+ if (isVimeo) {
2692
+ const videoId = value.split("/").pop();
2693
+ return /* @__PURE__ */ jsx3(
2694
+ "iframe",
2695
+ {
2696
+ src: `https://player.vimeo.com/video/${videoId}?autoplay=${autoplay ? 1 : 0}`,
2697
+ className: `w-full aspect-video rounded-xl border-0 ${className}`,
2698
+ allow: "autoplay; fullscreen; picture-in-picture",
2699
+ allowFullScreen: true,
2700
+ title: "Vimeo Video"
2701
+ }
2702
+ );
2703
+ }
2704
+ return /* @__PURE__ */ jsx3(
2705
+ "video",
2706
+ {
2707
+ src: value,
2708
+ controls: true,
2709
+ autoPlay: autoplay,
2710
+ muted: autoplay,
2711
+ playsInline: true,
2712
+ className: `w-full rounded-xl border border-border object-contain ${className}`
2713
+ }
2714
+ );
2715
+ }
2716
+ function NexusMap({ value, className = "" }) {
2717
+ if (!value || !value.lat || !value.lng) return null;
2718
+ const query = encodeURIComponent(
2719
+ value.address || `${value.lat},${value.lng}`
2720
+ );
2721
+ const embedUrl = `https://maps.google.com/maps?q=${query}&t=&z=13&ie=UTF8&iwloc=&output=embed`;
2722
+ return /* @__PURE__ */ jsx3(
2723
+ "div",
2724
+ {
2725
+ className: `overflow-hidden rounded-xl border border-border aspect-video w-full ${className}`,
2726
+ children: /* @__PURE__ */ jsx3(
2727
+ "iframe",
2728
+ {
2729
+ title: "Embedded Map",
2730
+ width: "100%",
2731
+ height: "100%",
2732
+ src: embedUrl,
2733
+ className: "border-0",
2734
+ allowFullScreen: true,
2735
+ loading: "lazy"
2736
+ }
2737
+ )
2738
+ }
2739
+ );
2740
+ }
2741
+ function NexusColor({
2742
+ value,
2743
+ className = "",
2744
+ showHexLabel = true
2745
+ }) {
2746
+ if (!value) return null;
2747
+ return /* @__PURE__ */ jsxs2("div", { className: `flex items-center gap-2.5 ${className}`, children: [
2748
+ /* @__PURE__ */ jsx3(
2749
+ "div",
2750
+ {
2751
+ className: "h-6 w-6 rounded-full border border-border/80 shadow-xs shrink-0",
2752
+ style: { backgroundColor: value }
2753
+ }
2754
+ ),
2755
+ showHexLabel && /* @__PURE__ */ jsx3("span", { className: "font-mono text-xs font-semibold text-foreground/80 uppercase", children: value })
2756
+ ] });
2757
+ }
2758
+ function NexusGradient({
2759
+ value,
2760
+ children,
2761
+ className = "",
2762
+ asTextMask = false
2763
+ }) {
2764
+ if (!value) return /* @__PURE__ */ jsx3(Fragment, { children });
2765
+ if (asTextMask) {
2766
+ return /* @__PURE__ */ jsx3(
2767
+ "span",
2768
+ {
2769
+ className: `bg-clip-text text-transparent font-bold ${className}`,
2770
+ style: { backgroundImage: value, WebkitBackgroundClip: "text" },
2771
+ children
2772
+ }
2773
+ );
2774
+ }
2775
+ return /* @__PURE__ */ jsx3(
2776
+ "div",
2777
+ {
2778
+ className: `rounded-xl ${className}`,
2779
+ style: { backgroundImage: value },
2780
+ children
2781
+ }
2782
+ );
2783
+ }
2784
+ function NexusAddress({ value, className = "" }) {
2785
+ if (!value) return null;
2786
+ const lines = [
2787
+ value.street,
2788
+ [value.city, value.state, value.postalCode].filter(Boolean).join(", "),
2789
+ value.country
2790
+ ].filter(Boolean);
2791
+ if (lines.length === 0) return null;
2792
+ return /* @__PURE__ */ jsxs2(
2793
+ "div",
2794
+ {
2795
+ className: `flex items-start gap-2.5 p-3 rounded-xl border border-border bg-card/50 ${className}`,
2796
+ children: [
2797
+ /* @__PURE__ */ jsx3(LucideIcons.MapPin, { className: "h-4 w-4 text-muted-foreground shrink-0 mt-0.5" }),
2798
+ /* @__PURE__ */ jsx3("div", { className: "text-xs text-foreground/80 leading-relaxed font-medium", children: lines.map((line, i) => /* @__PURE__ */ jsx3("div", { children: line }, i)) })
2799
+ ]
2800
+ }
2801
+ );
2802
+ }
2803
+ function NexusKeyValue({ value, className = "" }) {
2804
+ if (!value || Object.keys(value).length === 0) return null;
2805
+ return /* @__PURE__ */ jsx3(
2806
+ "div",
2807
+ {
2808
+ className: `border border-border rounded-xl overflow-hidden bg-card/30 divide-y divide-border ${className}`,
2809
+ children: Object.entries(value).map(([k, v]) => /* @__PURE__ */ jsxs2("div", { className: "flex text-xs px-4 py-3 gap-4", children: [
2810
+ /* @__PURE__ */ jsx3("div", { className: "w-1/3 font-bold text-muted-foreground select-none uppercase tracking-wider text-[10px]", children: k }),
2811
+ /* @__PURE__ */ jsx3("div", { className: "flex-1 font-medium text-foreground", children: v })
2812
+ ] }, k))
2813
+ }
2814
+ );
2815
+ }
2816
+ function NexusTags({
2817
+ value,
2818
+ className = "",
2819
+ badgeClassName = ""
2820
+ }) {
2821
+ if (!value || value.length === 0) return null;
2822
+ return /* @__PURE__ */ jsx3("div", { className: `flex flex-wrap gap-1.5 ${className}`, children: value.map((tag) => /* @__PURE__ */ jsx3(
2823
+ "span",
2824
+ {
2825
+ className: `inline-flex items-center rounded-lg border border-border bg-muted/40 px-2.5 py-1 text-xs font-semibold text-foreground/80 ${badgeClassName}`,
2826
+ children: tag
2827
+ },
2828
+ tag
2829
+ )) });
2830
+ }
2831
+ function NexusProgress({
2832
+ value,
2833
+ max = 100,
2834
+ className = "",
2835
+ color = "bg-primary"
2836
+ }) {
2837
+ if (value === null) return null;
2838
+ const percent = Math.min(value / max * 100, 100);
2839
+ return /* @__PURE__ */ jsx3("div", { className: `w-full ${className}`, children: /* @__PURE__ */ jsx3("div", { className: "h-2 w-full rounded-full bg-muted overflow-hidden border border-border/30", children: /* @__PURE__ */ jsx3(
2840
+ "div",
2841
+ {
2842
+ className: `h-full rounded-full transition-all duration-500 ${color}`,
2843
+ style: { width: `${percent}%` }
2844
+ }
2845
+ ) }) });
2846
+ }
2847
+ function NexusBlendContainer({
2848
+ mode,
2849
+ children,
2850
+ className = ""
2851
+ }) {
2852
+ const blendStyle = mode ? { mixBlendMode: mode } : {};
2853
+ return /* @__PURE__ */ jsx3("div", { className, style: blendStyle, children });
2854
+ }
2855
+ function NexusCode({
2856
+ value,
2857
+ language = "json",
2858
+ className = "",
2859
+ showLineNumbers = false
2860
+ }) {
2861
+ const [copied, setCopied] = useState3(false);
2862
+ if (!value) return null;
2863
+ const handleCopy = () => {
2864
+ navigator.clipboard.writeText(value);
2865
+ setCopied(true);
2866
+ setTimeout(() => setCopied(false), 2e3);
2867
+ };
2868
+ const lines = value.split("\n");
2869
+ return /* @__PURE__ */ jsxs2(
2870
+ "div",
2871
+ {
2872
+ className: `relative rounded-xl border border-border bg-[#090A0F] overflow-hidden ${className}`,
2873
+ children: [
2874
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border bg-white/2 select-none", children: [
2875
+ /* @__PURE__ */ jsx3("span", { className: "text-[10px] font-black uppercase text-muted-foreground tracking-wider", children: language }),
2876
+ /* @__PURE__ */ jsx3(
2877
+ "button",
2878
+ {
2879
+ onClick: handleCopy,
2880
+ className: "flex items-center gap-1.5 text-[10px] font-bold text-muted-foreground hover:text-primary transition-colors cursor-pointer",
2881
+ children: copied ? /* @__PURE__ */ jsxs2(Fragment, { children: [
2882
+ /* @__PURE__ */ jsx3(LucideIcons.Check, { className: "h-3 w-3 text-emerald-400" }),
2883
+ /* @__PURE__ */ jsx3("span", { className: "text-emerald-400", children: "Copied!" })
2884
+ ] }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
2885
+ /* @__PURE__ */ jsx3(LucideIcons.Copy, { className: "h-3 w-3" }),
2886
+ /* @__PURE__ */ jsx3("span", { children: "Copy Code" })
2887
+ ] })
2888
+ }
2889
+ )
2890
+ ] }),
2891
+ /* @__PURE__ */ jsxs2("div", { className: "flex overflow-x-auto p-4 font-mono text-[11px] leading-relaxed text-emerald-400 select-text", children: [
2892
+ showLineNumbers && /* @__PURE__ */ jsx3("div", { className: "flex flex-col text-right text-gray-600 select-none pr-3.5 border-r border-border/30 mr-3.5", children: lines.map((_, i) => /* @__PURE__ */ jsx3("span", { children: i + 1 }, i)) }),
2893
+ /* @__PURE__ */ jsx3("pre", { className: "flex-1 whitespace-pre", children: value })
2894
+ ] })
2895
+ ]
2896
+ }
2897
+ );
2898
+ }
2899
+ function NexusBoolean({
2900
+ value,
2901
+ className = "",
2902
+ trueLabel = "Active",
2903
+ falseLabel = "Inactive"
2904
+ }) {
2905
+ if (value === null) return null;
2906
+ return /* @__PURE__ */ jsxs2(
2907
+ "span",
2908
+ {
2909
+ className: `inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-black uppercase tracking-wider border shadow-sm ${value ? "border-emerald-500/20 bg-emerald-500/10 text-emerald-400" : "border-muted-foreground/20 bg-muted-foreground/10 text-muted-foreground"} ${className}`,
2910
+ children: [
2911
+ /* @__PURE__ */ jsx3(
2912
+ "span",
2913
+ {
2914
+ className: `h-1.5 w-1.5 rounded-full ${value ? "bg-emerald-500 animate-pulse" : "bg-muted-foreground"}`
2915
+ }
2916
+ ),
2917
+ value ? trueLabel : falseLabel
2918
+ ]
2919
+ }
2920
+ );
2921
+ }
10
2922
  export {
11
2923
  AuthProvider,
2924
+ NexusAddress,
2925
+ NexusBlendContainer,
2926
+ NexusBoolean,
2927
+ NexusCode,
2928
+ NexusColor,
2929
+ NexusGallery,
2930
+ NexusGradient,
2931
+ NexusIcon,
2932
+ NexusImage,
2933
+ NexusKeyValue,
2934
+ NexusLongText,
2935
+ NexusMap,
2936
+ NexusProgress,
12
2937
  NexusProvider,
2938
+ NexusRichText,
2939
+ NexusTags,
2940
+ NexusVideo,
13
2941
  useNexus,
14
2942
  useNexusAnalytics,
15
2943
  useNexusAuth,