sovads-sdk 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +57 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +689 -133
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -5,31 +5,58 @@ class SovAds {
|
|
|
5
5
|
this.components = new Map();
|
|
6
6
|
this.siteId = null;
|
|
7
7
|
this.renderObservers = new Map();
|
|
8
|
-
this.debugLoggingEnabled =
|
|
8
|
+
this.debugLoggingEnabled = false;
|
|
9
|
+
this.adTrackingTokens = new Map();
|
|
9
10
|
this.config = {
|
|
10
11
|
apiUrl: typeof window !== 'undefined' && window.location.hostname === 'localhost'
|
|
11
12
|
? 'http://localhost:3000'
|
|
12
13
|
: 'https://ads.sovseas.xyz',
|
|
13
14
|
debug: false,
|
|
15
|
+
refreshInterval: 0, // No auto-refresh by default
|
|
16
|
+
lazyLoad: true,
|
|
17
|
+
rotationEnabled: true,
|
|
18
|
+
popupMinIntervalMinutes: 30,
|
|
19
|
+
popupSessionMax: 1,
|
|
14
20
|
...config
|
|
15
21
|
};
|
|
22
|
+
this.debugLoggingEnabled = Boolean(this.config.debug);
|
|
16
23
|
this.fingerprint = this.generateFingerprint();
|
|
17
24
|
if (this.config.debug) {
|
|
18
25
|
console.log('SovAds SDK initialized:', this.config);
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
generateFingerprint() {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const storageKey = 'sovads_fingerprint_v1';
|
|
30
|
+
try {
|
|
31
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
32
|
+
const existing = window.localStorage.getItem(storageKey);
|
|
33
|
+
if (existing) {
|
|
34
|
+
return existing;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Ignore storage access errors and fall back to generated value.
|
|
40
|
+
}
|
|
41
|
+
const browserParts = [
|
|
42
|
+
typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown-ua',
|
|
43
|
+
typeof navigator !== 'undefined' ? navigator.language : 'unknown-lang',
|
|
44
|
+
typeof screen !== 'undefined' ? `${screen.width}x${screen.height}` : 'unknown-screen',
|
|
45
|
+
String(new Date().getTimezoneOffset()),
|
|
46
|
+
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
47
|
+
? crypto.randomUUID()
|
|
48
|
+
: `${Date.now()}-${Math.random()}`,
|
|
49
|
+
];
|
|
50
|
+
const value = btoa(browserParts.join('|')).replace(/=+$/g, '');
|
|
51
|
+
try {
|
|
52
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
53
|
+
window.localStorage.setItem(storageKey, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore storage write failures.
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
33
60
|
}
|
|
34
61
|
async detectSiteId() {
|
|
35
62
|
if (this.siteId) {
|
|
@@ -47,6 +74,7 @@ class SovAds {
|
|
|
47
74
|
const domain = window.location.hostname;
|
|
48
75
|
const payload = {
|
|
49
76
|
domain,
|
|
77
|
+
pathname: window.location.pathname,
|
|
50
78
|
fingerprint: this.fingerprint,
|
|
51
79
|
userAgent: navigator.userAgent,
|
|
52
80
|
pageUrl: window.location.href,
|
|
@@ -212,18 +240,39 @@ class SovAds {
|
|
|
212
240
|
timestamp: Date.now()
|
|
213
241
|
};
|
|
214
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Normalize URL - add protocol if missing for localhost
|
|
245
|
+
*/
|
|
246
|
+
normalizeUrl(url) {
|
|
247
|
+
const trimmed = url.trim();
|
|
248
|
+
if (!trimmed.includes('://')) {
|
|
249
|
+
// Allow localhost URLs without protocol for debugging
|
|
250
|
+
if (trimmed.startsWith('localhost') || trimmed.startsWith('127.0.0.1')) {
|
|
251
|
+
return `http://${trimmed}`;
|
|
252
|
+
}
|
|
253
|
+
// Treat bare domains as https by default.
|
|
254
|
+
return `https://${trimmed}`;
|
|
255
|
+
}
|
|
256
|
+
return trimmed;
|
|
257
|
+
}
|
|
215
258
|
/**
|
|
216
259
|
* Validate URL format
|
|
217
260
|
*/
|
|
218
261
|
isValidUrl(url) {
|
|
219
262
|
try {
|
|
220
|
-
const
|
|
263
|
+
const normalized = this.normalizeUrl(url);
|
|
264
|
+
const parsed = new URL(normalized);
|
|
221
265
|
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
222
266
|
}
|
|
223
267
|
catch {
|
|
224
268
|
return false;
|
|
225
269
|
}
|
|
226
270
|
}
|
|
271
|
+
inferMediaTypeFromUrl(url) {
|
|
272
|
+
const value = (url || '').toLowerCase();
|
|
273
|
+
const videoExts = ['.mp4', '.webm', '.ogg', '.ogv', '.mov', '.m3u8'];
|
|
274
|
+
return videoExts.some((ext) => value.includes(ext)) ? 'video' : 'image';
|
|
275
|
+
}
|
|
227
276
|
/**
|
|
228
277
|
* Fetch with retry logic
|
|
229
278
|
*/
|
|
@@ -250,13 +299,15 @@ class SovAds {
|
|
|
250
299
|
}
|
|
251
300
|
throw lastError || new Error('Fetch failed after retries');
|
|
252
301
|
}
|
|
253
|
-
async loadAd(
|
|
302
|
+
async loadAd(options = {}) {
|
|
254
303
|
const startTime = Date.now();
|
|
255
304
|
try {
|
|
256
305
|
const siteId = await this.detectSiteId();
|
|
257
306
|
const params = new URLSearchParams({
|
|
258
307
|
siteId,
|
|
259
|
-
...(consumerId && { consumerId })
|
|
308
|
+
...(options.consumerId && { consumerId: options.consumerId }),
|
|
309
|
+
...(options.placement && { placement: options.placement }),
|
|
310
|
+
...(options.size && { size: options.size }),
|
|
260
311
|
});
|
|
261
312
|
const endpoint = `${this.config.apiUrl}/api/ads?${params}`;
|
|
262
313
|
const response = await this.fetchWithRetry(endpoint);
|
|
@@ -271,7 +322,7 @@ class SovAds {
|
|
|
271
322
|
pageUrl: window.location.href,
|
|
272
323
|
userAgent: navigator.userAgent,
|
|
273
324
|
fingerprint: this.fingerprint,
|
|
274
|
-
requestBody: { siteId,
|
|
325
|
+
requestBody: { siteId, ...options },
|
|
275
326
|
responseStatus: response.status,
|
|
276
327
|
duration,
|
|
277
328
|
});
|
|
@@ -320,8 +371,15 @@ class SovAds {
|
|
|
320
371
|
}
|
|
321
372
|
const normalizedAd = {
|
|
322
373
|
...rawAd,
|
|
323
|
-
|
|
374
|
+
bannerUrl: this.normalizeUrl(rawAd.bannerUrl),
|
|
375
|
+
targetUrl: this.normalizeUrl(rawAd.targetUrl),
|
|
376
|
+
mediaType: rawAd.mediaType === 'video'
|
|
377
|
+
? 'video'
|
|
378
|
+
: this.inferMediaTypeFromUrl(this.normalizeUrl(rawAd.bannerUrl)),
|
|
324
379
|
};
|
|
380
|
+
if (normalizedAd.trackingToken) {
|
|
381
|
+
this.adTrackingTokens.set(normalizedAd.id, normalizedAd.trackingToken);
|
|
382
|
+
}
|
|
325
383
|
if (this.config.debug) {
|
|
326
384
|
console.log('Ad loaded:', normalizedAd);
|
|
327
385
|
}
|
|
@@ -355,6 +413,105 @@ class SovAds {
|
|
|
355
413
|
return null;
|
|
356
414
|
}
|
|
357
415
|
}
|
|
416
|
+
toBase64(bytes) {
|
|
417
|
+
let binary = '';
|
|
418
|
+
for (const b of bytes) {
|
|
419
|
+
binary += String.fromCharCode(b);
|
|
420
|
+
}
|
|
421
|
+
return btoa(binary);
|
|
422
|
+
}
|
|
423
|
+
async signTrackingPayload(payload, timestamp) {
|
|
424
|
+
if (!this.config.apiSecret || typeof crypto === 'undefined' || !crypto.subtle) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const encoder = new TextEncoder();
|
|
429
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(this.config.apiSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
430
|
+
const message = `${timestamp}:${payload}`;
|
|
431
|
+
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
|
|
432
|
+
return this.toBase64(new Uint8Array(signature));
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
if (this.config.debug) {
|
|
436
|
+
console.error('Failed to sign tracking payload:', error);
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async sendTrackingEnvelope(eventPayload, useBeacon) {
|
|
442
|
+
if (eventPayload.trackingToken) {
|
|
443
|
+
const tokenBody = JSON.stringify({
|
|
444
|
+
trackingToken: eventPayload.trackingToken,
|
|
445
|
+
payload: eventPayload,
|
|
446
|
+
});
|
|
447
|
+
const tokenWebhookUrl = `${this.config.apiUrl}/api/webhook/track`;
|
|
448
|
+
try {
|
|
449
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
450
|
+
return navigator.sendBeacon(tokenWebhookUrl, new Blob([tokenBody], { type: 'application/json' }));
|
|
451
|
+
}
|
|
452
|
+
const response = await fetch(tokenWebhookUrl, {
|
|
453
|
+
method: 'POST',
|
|
454
|
+
headers: { 'Content-Type': 'application/json' },
|
|
455
|
+
body: tokenBody,
|
|
456
|
+
keepalive: true,
|
|
457
|
+
});
|
|
458
|
+
return response.ok;
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (!this.config.apiKey || !this.config.apiSecret) {
|
|
465
|
+
if (this.config.debug) {
|
|
466
|
+
const devWebhookUrl = `${this.config.apiUrl}/api/webhook/beacon`;
|
|
467
|
+
const body = JSON.stringify(eventPayload);
|
|
468
|
+
try {
|
|
469
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
470
|
+
return navigator.sendBeacon(devWebhookUrl, new Blob([body], { type: 'application/json' }));
|
|
471
|
+
}
|
|
472
|
+
const response = await fetch(devWebhookUrl, {
|
|
473
|
+
method: 'POST',
|
|
474
|
+
headers: { 'Content-Type': 'application/json' },
|
|
475
|
+
body,
|
|
476
|
+
keepalive: true,
|
|
477
|
+
});
|
|
478
|
+
return response.ok;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
console.warn('SovAds: Missing apiKey/apiSecret, skipping signed tracking event');
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const timestamp = Date.now();
|
|
490
|
+
const payload = JSON.stringify(eventPayload);
|
|
491
|
+
const signature = await this.signTrackingPayload(payload, timestamp);
|
|
492
|
+
if (!signature) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
const envelope = JSON.stringify({
|
|
496
|
+
apiKey: this.config.apiKey,
|
|
497
|
+
siteId: eventPayload.siteId,
|
|
498
|
+
payload,
|
|
499
|
+
signature,
|
|
500
|
+
timestamp,
|
|
501
|
+
});
|
|
502
|
+
const webhookUrl = `${this.config.apiUrl}/api/webhook/track`;
|
|
503
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
504
|
+
const blob = new Blob([envelope], { type: 'application/json' });
|
|
505
|
+
return navigator.sendBeacon(webhookUrl, blob);
|
|
506
|
+
}
|
|
507
|
+
const response = await fetch(webhookUrl, {
|
|
508
|
+
method: 'POST',
|
|
509
|
+
headers: { 'Content-Type': 'application/json' },
|
|
510
|
+
body: envelope,
|
|
511
|
+
keepalive: true,
|
|
512
|
+
});
|
|
513
|
+
return response.ok;
|
|
514
|
+
}
|
|
358
515
|
/**
|
|
359
516
|
* Track event with retry logic (internal helper)
|
|
360
517
|
*/
|
|
@@ -374,24 +531,15 @@ class SovAds {
|
|
|
374
531
|
renderTime: renderInfo?.renderTime ?? Date.now(),
|
|
375
532
|
timestamp: metadata.timestamp,
|
|
376
533
|
pageUrl: metadata.pageUrl,
|
|
377
|
-
userAgent: metadata.userAgent
|
|
534
|
+
userAgent: metadata.userAgent,
|
|
535
|
+
trackingToken: this.adTrackingTokens.get(adId),
|
|
378
536
|
};
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
headers: {
|
|
383
|
-
'Content-Type': 'application/json'
|
|
384
|
-
},
|
|
385
|
-
body: JSON.stringify(payload),
|
|
386
|
-
keepalive: true // Similar behavior to beacon
|
|
387
|
-
});
|
|
388
|
-
if (response.ok) {
|
|
389
|
-
if (this.config.debug) {
|
|
390
|
-
console.log(`SovAds: Tracked ${type} event via fetch (attempt ${attempt})`, payload);
|
|
391
|
-
}
|
|
537
|
+
const ok = await this.sendTrackingEnvelope(payload, false);
|
|
538
|
+
if (!ok) {
|
|
539
|
+
throw new Error('Tracking endpoint rejected event');
|
|
392
540
|
}
|
|
393
|
-
|
|
394
|
-
|
|
541
|
+
if (this.config.debug) {
|
|
542
|
+
console.log(`SovAds: Tracked ${type} event via signed fetch (attempt ${attempt})`, payload);
|
|
395
543
|
}
|
|
396
544
|
}
|
|
397
545
|
catch (error) {
|
|
@@ -431,35 +579,25 @@ class SovAds {
|
|
|
431
579
|
renderTime: renderInfo?.renderTime ?? Date.now(),
|
|
432
580
|
timestamp: metadata.timestamp,
|
|
433
581
|
pageUrl: metadata.pageUrl,
|
|
434
|
-
userAgent: metadata.userAgent
|
|
582
|
+
userAgent: metadata.userAgent,
|
|
583
|
+
trackingToken: this.adTrackingTokens.get(adId),
|
|
435
584
|
};
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const blob = new Blob([JSON.stringify(payload)], {
|
|
440
|
-
type: 'application/json'
|
|
441
|
-
});
|
|
442
|
-
// Send to dedicated webhook endpoint for beamer interactions
|
|
443
|
-
const webhookUrl = `${this.config.apiUrl}/api/webhook/beacon`;
|
|
444
|
-
const sent = navigator.sendBeacon(webhookUrl, blob);
|
|
445
|
-
if (!sent) {
|
|
446
|
-
// Beacon failed, fallback to fetch with retry
|
|
585
|
+
if (typeof navigator.sendBeacon === 'function') {
|
|
586
|
+
const sent = await this.sendTrackingEnvelope(payload, true);
|
|
587
|
+
if (sent) {
|
|
447
588
|
if (this.config.debug) {
|
|
448
|
-
console.
|
|
589
|
+
console.log(`SovAds: Tracked ${type} event via signed beacon`, {
|
|
590
|
+
payload: { ...payload, fingerprint: payload.fingerprint.substring(0, 8) + '...' }
|
|
591
|
+
});
|
|
449
592
|
}
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
else if (this.config.debug) {
|
|
453
|
-
console.log(`SovAds: Tracked ${type} event via beacon`, {
|
|
454
|
-
sent,
|
|
455
|
-
payload: { ...payload, fingerprint: payload.fingerprint.substring(0, 8) + '...' }
|
|
456
|
-
});
|
|
593
|
+
return;
|
|
457
594
|
}
|
|
458
595
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
596
|
+
// Fallback to signed fetch for older browsers and beacon failures
|
|
597
|
+
if (this.config.debug) {
|
|
598
|
+
console.warn(`SovAds: Beacon unavailable/failed for ${type}, falling back to signed fetch`);
|
|
462
599
|
}
|
|
600
|
+
await this.trackEventWithRetry(type, adId, campaignId, renderInfo, 1);
|
|
463
601
|
}
|
|
464
602
|
catch (error) {
|
|
465
603
|
if (this.config.debug) {
|
|
@@ -540,17 +678,22 @@ class SovAds {
|
|
|
540
678
|
}
|
|
541
679
|
// Banner Component
|
|
542
680
|
export class Banner {
|
|
543
|
-
constructor(sovads, containerId) {
|
|
681
|
+
constructor(sovads, containerId, slotConfig = {}) {
|
|
544
682
|
this.currentAd = null;
|
|
545
683
|
this.renderStartTime = 0;
|
|
546
684
|
this.hasTrackedImpression = false;
|
|
547
685
|
this.isRendering = false;
|
|
686
|
+
this.refreshTimer = null;
|
|
687
|
+
this.lastAdId = null;
|
|
688
|
+
this.retryCount = 0;
|
|
689
|
+
this.maxRetries = 3;
|
|
548
690
|
this.sovads = sovads;
|
|
549
691
|
this.containerId = containerId;
|
|
692
|
+
this.slotConfig = slotConfig;
|
|
550
693
|
}
|
|
551
|
-
async render(consumerId) {
|
|
694
|
+
async render(consumerId, forceRefresh = false) {
|
|
552
695
|
// Prevent concurrent renders
|
|
553
|
-
if (this.isRendering) {
|
|
696
|
+
if (this.isRendering && !forceRefresh) {
|
|
554
697
|
if (this.sovads.getConfig().debug) {
|
|
555
698
|
console.warn(`Banner render already in progress for ${this.containerId}`);
|
|
556
699
|
}
|
|
@@ -564,8 +707,33 @@ export class Banner {
|
|
|
564
707
|
this.isRendering = false;
|
|
565
708
|
return;
|
|
566
709
|
}
|
|
710
|
+
// Lazy loading: wait for container to be in viewport
|
|
711
|
+
if (this.sovads.getConfig().lazyLoad && !forceRefresh) {
|
|
712
|
+
const isInViewport = await this.checkViewport(container);
|
|
713
|
+
if (!isInViewport) {
|
|
714
|
+
// Set up intersection observer for lazy loading
|
|
715
|
+
this.setupLazyLoadObserver(container, consumerId);
|
|
716
|
+
this.isRendering = false;
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
567
720
|
this.renderStartTime = Date.now();
|
|
568
|
-
this.currentAd = await this.sovads.loadAd(
|
|
721
|
+
this.currentAd = await this.sovads.loadAd({
|
|
722
|
+
consumerId,
|
|
723
|
+
placement: this.slotConfig.placementId || 'banner',
|
|
724
|
+
size: this.slotConfig.size,
|
|
725
|
+
});
|
|
726
|
+
this.hasTrackedImpression = false;
|
|
727
|
+
// Skip if same ad (rotation disabled or same ad returned)
|
|
728
|
+
if (!forceRefresh && this.lastAdId === this.currentAd?.id && this.sovads.getConfig().rotationEnabled) {
|
|
729
|
+
if (this.sovads.getConfig().debug) {
|
|
730
|
+
console.log('Same ad returned, skipping render');
|
|
731
|
+
}
|
|
732
|
+
this.isRendering = false;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
this.lastAdId = this.currentAd?.id || null;
|
|
736
|
+
this.retryCount = 0; // Reset retry count on success
|
|
569
737
|
if (!this.currentAd) {
|
|
570
738
|
container.innerHTML = '<div class="sovads-no-ad">No ads available</div>';
|
|
571
739
|
this.isRendering = false;
|
|
@@ -573,6 +741,7 @@ export class Banner {
|
|
|
573
741
|
}
|
|
574
742
|
// Handle dummy ads for unregistered sites
|
|
575
743
|
if (this.currentAd.isDummy) {
|
|
744
|
+
container.innerHTML = '';
|
|
576
745
|
const dummyElement = document.createElement('div');
|
|
577
746
|
dummyElement.className = 'sovads-banner-dummy';
|
|
578
747
|
dummyElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
@@ -603,7 +772,7 @@ export class Banner {
|
|
|
603
772
|
dummyElement.appendChild(img);
|
|
604
773
|
dummyElement.appendChild(message);
|
|
605
774
|
dummyElement.addEventListener('click', () => {
|
|
606
|
-
window.open(this.currentAd.targetUrl, '_blank', 'noopener,noreferrer');
|
|
775
|
+
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
607
776
|
});
|
|
608
777
|
dummyElement.addEventListener('mouseenter', () => {
|
|
609
778
|
dummyElement.style.transform = 'scale(1.02)';
|
|
@@ -618,16 +787,21 @@ export class Banner {
|
|
|
618
787
|
return;
|
|
619
788
|
}
|
|
620
789
|
const adElement = document.createElement('div');
|
|
790
|
+
container.innerHTML = '';
|
|
621
791
|
adElement.className = 'sovads-banner';
|
|
622
792
|
adElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
793
|
+
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
623
794
|
adElement.style.cssText = `
|
|
624
795
|
border: 1px solid #333;
|
|
625
796
|
border-radius: 8px;
|
|
626
797
|
overflow: hidden;
|
|
627
|
-
cursor: pointer;
|
|
798
|
+
cursor: ${mediaType === 'video' ? 'default' : 'pointer'};
|
|
628
799
|
transition: transform 0.2s ease;
|
|
800
|
+
max-width: 100%;
|
|
801
|
+
width: 100%;
|
|
802
|
+
box-sizing: border-box;
|
|
803
|
+
opacity: 0;
|
|
629
804
|
`;
|
|
630
|
-
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
631
805
|
const handleVisibilityTracking = (renderInfo) => {
|
|
632
806
|
this.sovads.setupRenderObserver(adElement, this.currentAd.id, (isVisible) => {
|
|
633
807
|
renderInfo.viewportVisible = isVisible;
|
|
@@ -638,6 +812,7 @@ export class Banner {
|
|
|
638
812
|
});
|
|
639
813
|
};
|
|
640
814
|
const handleRenderSuccess = () => {
|
|
815
|
+
adElement.style.opacity = '1';
|
|
641
816
|
const renderTime = Date.now() - this.renderStartTime;
|
|
642
817
|
handleVisibilityTracking({
|
|
643
818
|
rendered: true,
|
|
@@ -646,6 +821,7 @@ export class Banner {
|
|
|
646
821
|
});
|
|
647
822
|
};
|
|
648
823
|
const handleRenderError = () => {
|
|
824
|
+
adElement.style.opacity = '1';
|
|
649
825
|
if (this.sovads.getConfig().debug) {
|
|
650
826
|
console.warn(`Failed to load ad media: ${this.currentAd.bannerUrl}`);
|
|
651
827
|
}
|
|
@@ -673,28 +849,50 @@ export class Banner {
|
|
|
673
849
|
const img = document.createElement('img');
|
|
674
850
|
img.src = this.currentAd.bannerUrl;
|
|
675
851
|
img.alt = this.currentAd.description;
|
|
676
|
-
img.style.cssText = 'width: 100%; height: auto; display: block;';
|
|
852
|
+
img.style.cssText = 'width: 100%; height: auto; display: block; max-width: 100%; object-fit: contain;';
|
|
677
853
|
img.addEventListener('load', handleRenderSuccess, { once: true });
|
|
678
854
|
img.addEventListener('error', handleRenderError, { once: true });
|
|
679
855
|
mediaElement = img;
|
|
680
856
|
}
|
|
681
|
-
mediaElement.style.cursor = 'pointer';
|
|
682
|
-
|
|
683
|
-
|
|
857
|
+
mediaElement.style.cursor = mediaType === 'video' ? 'default' : 'pointer';
|
|
858
|
+
mediaElement.style.maxWidth = '100%';
|
|
859
|
+
const handleClickThrough = () => {
|
|
684
860
|
this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
|
|
685
861
|
rendered: true,
|
|
686
862
|
viewportVisible: true,
|
|
687
863
|
renderTime: Date.now() - this.renderStartTime
|
|
688
864
|
});
|
|
689
|
-
// Log interaction
|
|
690
865
|
this.sovads.logInteraction('CLICK', {
|
|
691
866
|
adId: this.currentAd.id,
|
|
692
867
|
campaignId: this.currentAd.campaignId,
|
|
693
868
|
elementType: 'BANNER',
|
|
694
869
|
metadata: { renderTime: Date.now() - this.renderStartTime },
|
|
695
870
|
});
|
|
696
|
-
window.open(this.currentAd.targetUrl, '_blank', 'noopener,noreferrer');
|
|
697
|
-
}
|
|
871
|
+
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
872
|
+
};
|
|
873
|
+
if (mediaType === 'video') {
|
|
874
|
+
const ctaButton = document.createElement('button');
|
|
875
|
+
ctaButton.type = 'button';
|
|
876
|
+
ctaButton.textContent = 'Learn more';
|
|
877
|
+
ctaButton.style.cssText = `
|
|
878
|
+
width: 100%;
|
|
879
|
+
border: none;
|
|
880
|
+
border-top: 1px solid #333;
|
|
881
|
+
background: #111;
|
|
882
|
+
color: #fff;
|
|
883
|
+
font-size: 12px;
|
|
884
|
+
font-weight: 600;
|
|
885
|
+
padding: 8px 12px;
|
|
886
|
+
cursor: pointer;
|
|
887
|
+
`;
|
|
888
|
+
ctaButton.addEventListener('click', handleClickThrough);
|
|
889
|
+
adElement.appendChild(mediaElement);
|
|
890
|
+
adElement.appendChild(ctaButton);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
adElement.addEventListener('click', handleClickThrough);
|
|
894
|
+
adElement.appendChild(mediaElement);
|
|
895
|
+
}
|
|
698
896
|
// Add hover effect
|
|
699
897
|
adElement.addEventListener('mouseenter', () => {
|
|
700
898
|
adElement.style.transform = 'scale(1.02)';
|
|
@@ -702,13 +900,94 @@ export class Banner {
|
|
|
702
900
|
adElement.addEventListener('mouseleave', () => {
|
|
703
901
|
adElement.style.transform = 'scale(1)';
|
|
704
902
|
});
|
|
705
|
-
adElement.appendChild(mediaElement);
|
|
706
903
|
container.appendChild(adElement);
|
|
904
|
+
// Set up auto-refresh if enabled
|
|
905
|
+
this.setupAutoRefresh(consumerId);
|
|
906
|
+
}
|
|
907
|
+
catch (error) {
|
|
908
|
+
// Retry logic on error
|
|
909
|
+
if (this.retryCount < this.maxRetries) {
|
|
910
|
+
this.retryCount++;
|
|
911
|
+
if (this.sovads.getConfig().debug) {
|
|
912
|
+
console.warn(`Banner render failed, retrying (${this.retryCount}/${this.maxRetries})...`);
|
|
913
|
+
}
|
|
914
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount)); // Exponential backoff
|
|
915
|
+
this.isRendering = false;
|
|
916
|
+
return this.render(consumerId, true);
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
const container = document.getElementById(this.containerId);
|
|
920
|
+
if (container) {
|
|
921
|
+
container.innerHTML = '<div class="sovads-error" style="padding: 10px; text-align: center; color: #666; font-size: 12px;">Ad temporarily unavailable</div>';
|
|
922
|
+
}
|
|
923
|
+
if (this.sovads.getConfig().debug) {
|
|
924
|
+
console.error('Banner render failed after retries:', error);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
707
927
|
}
|
|
708
928
|
finally {
|
|
709
929
|
this.isRendering = false;
|
|
710
930
|
}
|
|
711
931
|
}
|
|
932
|
+
async checkViewport(element) {
|
|
933
|
+
return new Promise((resolve) => {
|
|
934
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
935
|
+
resolve(true); // Fallback: load immediately if IntersectionObserver not supported
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const observer = new IntersectionObserver((entries) => {
|
|
939
|
+
entries.forEach((entry) => {
|
|
940
|
+
if (entry.isIntersecting) {
|
|
941
|
+
observer.disconnect();
|
|
942
|
+
resolve(true);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
}, { rootMargin: '50px' } // Start loading 50px before entering viewport
|
|
946
|
+
);
|
|
947
|
+
observer.observe(element);
|
|
948
|
+
// Timeout after 5 seconds - load anyway
|
|
949
|
+
setTimeout(() => {
|
|
950
|
+
observer.disconnect();
|
|
951
|
+
resolve(true);
|
|
952
|
+
}, 5000);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
setupLazyLoadObserver(container, consumerId) {
|
|
956
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
957
|
+
// Fallback: load immediately
|
|
958
|
+
this.render(consumerId);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const observer = new IntersectionObserver((entries) => {
|
|
962
|
+
entries.forEach((entry) => {
|
|
963
|
+
if (entry.isIntersecting && !this.isRendering) {
|
|
964
|
+
observer.disconnect();
|
|
965
|
+
this.render(consumerId);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
}, { rootMargin: '50px' });
|
|
969
|
+
observer.observe(container);
|
|
970
|
+
}
|
|
971
|
+
setupAutoRefresh(consumerId) {
|
|
972
|
+
// Clear existing timer
|
|
973
|
+
if (this.refreshTimer) {
|
|
974
|
+
clearInterval(this.refreshTimer);
|
|
975
|
+
}
|
|
976
|
+
const refreshInterval = this.sovads.getConfig().refreshInterval || 0;
|
|
977
|
+
if (refreshInterval > 0) {
|
|
978
|
+
this.refreshTimer = window.setInterval(() => {
|
|
979
|
+
if (!this.isRendering) {
|
|
980
|
+
this.render(consumerId, true);
|
|
981
|
+
}
|
|
982
|
+
}, refreshInterval * 1000);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
destroy() {
|
|
986
|
+
if (this.refreshTimer) {
|
|
987
|
+
clearInterval(this.refreshTimer);
|
|
988
|
+
this.refreshTimer = null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
712
991
|
}
|
|
713
992
|
// Popup Component
|
|
714
993
|
export class Popup {
|
|
@@ -716,8 +995,40 @@ export class Popup {
|
|
|
716
995
|
this.currentAd = null;
|
|
717
996
|
this.popupElement = null;
|
|
718
997
|
this.isShowing = false;
|
|
998
|
+
this.retryCount = 0;
|
|
999
|
+
this.maxRetries = 3;
|
|
1000
|
+
this.storageKeyLastShown = 'sovads_popup_last_shown';
|
|
1001
|
+
this.storageKeySessionCount = 'sovads_popup_session_count';
|
|
719
1002
|
this.sovads = sovads;
|
|
720
1003
|
}
|
|
1004
|
+
canShowByFrequencyCap() {
|
|
1005
|
+
try {
|
|
1006
|
+
const minIntervalMs = (this.sovads.getConfig().popupMinIntervalMinutes || 30) * 60 * 1000;
|
|
1007
|
+
const sessionMax = this.sovads.getConfig().popupSessionMax || 1;
|
|
1008
|
+
const now = Date.now();
|
|
1009
|
+
const lastShown = Number(localStorage.getItem(this.storageKeyLastShown) || 0);
|
|
1010
|
+
const sessionCount = Number(sessionStorage.getItem(this.storageKeySessionCount) || 0);
|
|
1011
|
+
if (sessionCount >= sessionMax)
|
|
1012
|
+
return false;
|
|
1013
|
+
if (lastShown > 0 && now - lastShown < minIntervalMs)
|
|
1014
|
+
return false;
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
markShown() {
|
|
1022
|
+
try {
|
|
1023
|
+
const now = Date.now();
|
|
1024
|
+
const currentSessionCount = Number(sessionStorage.getItem(this.storageKeySessionCount) || 0);
|
|
1025
|
+
localStorage.setItem(this.storageKeyLastShown, String(now));
|
|
1026
|
+
sessionStorage.setItem(this.storageKeySessionCount, String(currentSessionCount + 1));
|
|
1027
|
+
}
|
|
1028
|
+
catch {
|
|
1029
|
+
// Ignore storage access issues.
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
721
1032
|
async show(consumerId, delay = 3000) {
|
|
722
1033
|
// Prevent concurrent shows
|
|
723
1034
|
if (this.isShowing) {
|
|
@@ -726,65 +1037,113 @@ export class Popup {
|
|
|
726
1037
|
}
|
|
727
1038
|
return;
|
|
728
1039
|
}
|
|
729
|
-
this.
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
this.isShowing = false;
|
|
1040
|
+
if (!this.canShowByFrequencyCap()) {
|
|
1041
|
+
if (this.sovads.getConfig().debug) {
|
|
1042
|
+
console.log('Popup skipped due to frequency cap');
|
|
1043
|
+
}
|
|
734
1044
|
return;
|
|
735
1045
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
this.
|
|
1046
|
+
this.isShowing = true;
|
|
1047
|
+
try {
|
|
1048
|
+
this.currentAd = await this.sovads.loadAd({
|
|
1049
|
+
consumerId,
|
|
1050
|
+
placement: 'popup',
|
|
1051
|
+
size: window.innerWidth < 640 ? '320x100' : '360x120',
|
|
1052
|
+
});
|
|
1053
|
+
if (!this.currentAd) {
|
|
1054
|
+
if (this.retryCount < this.maxRetries) {
|
|
1055
|
+
this.retryCount++;
|
|
1056
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
|
|
1057
|
+
this.isShowing = false;
|
|
1058
|
+
return this.show(consumerId, delay);
|
|
1059
|
+
}
|
|
1060
|
+
if (this.sovads.getConfig().debug) {
|
|
1061
|
+
console.log('No popup ad available after retries');
|
|
1062
|
+
}
|
|
1063
|
+
this.isShowing = false;
|
|
1064
|
+
this.retryCount = 0;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
this.retryCount = 0; // Reset on success
|
|
1068
|
+
// Show popup after delay
|
|
1069
|
+
setTimeout(() => {
|
|
1070
|
+
this.renderPopup();
|
|
1071
|
+
this.markShown();
|
|
1072
|
+
this.isShowing = false;
|
|
1073
|
+
}, delay);
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
if (this.sovads.getConfig().debug) {
|
|
1077
|
+
console.error('Error loading popup ad:', error);
|
|
1078
|
+
}
|
|
739
1079
|
this.isShowing = false;
|
|
740
|
-
|
|
1080
|
+
this.retryCount = 0;
|
|
1081
|
+
}
|
|
741
1082
|
}
|
|
742
1083
|
renderPopup() {
|
|
743
1084
|
if (!this.currentAd)
|
|
744
1085
|
return;
|
|
745
1086
|
const renderStartTime = Date.now();
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1087
|
+
let impressionTracked = false;
|
|
1088
|
+
const trackPopupImpression = (rendered, renderTime) => {
|
|
1089
|
+
if (impressionTracked || !this.currentAd || this.currentAd.isDummy)
|
|
1090
|
+
return;
|
|
1091
|
+
impressionTracked = true;
|
|
750
1092
|
this.sovads._trackEvent('IMPRESSION', this.currentAd.id, this.currentAd.campaignId, {
|
|
751
|
-
rendered
|
|
1093
|
+
rendered,
|
|
752
1094
|
viewportVisible: true,
|
|
753
|
-
renderTime
|
|
1095
|
+
renderTime,
|
|
754
1096
|
});
|
|
755
|
-
// Log interaction
|
|
756
1097
|
this.sovads.logInteraction('IMPRESSION', {
|
|
757
1098
|
adId: this.currentAd.id,
|
|
758
1099
|
campaignId: this.currentAd.campaignId,
|
|
759
1100
|
elementType: 'POPUP',
|
|
760
|
-
metadata: { renderTime
|
|
1101
|
+
metadata: { renderTime, rendered },
|
|
761
1102
|
});
|
|
762
|
-
}
|
|
763
|
-
// Create
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
1103
|
+
};
|
|
1104
|
+
// Create non-blocking sticky container
|
|
1105
|
+
const wrapper = document.createElement('div');
|
|
1106
|
+
wrapper.className = 'sovads-popup-overlay';
|
|
1107
|
+
wrapper.style.cssText = `
|
|
767
1108
|
position: fixed;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
width:
|
|
771
|
-
height: 100%;
|
|
772
|
-
background: rgba(0, 0, 0, 0.5);
|
|
1109
|
+
right: 16px;
|
|
1110
|
+
bottom: 16px;
|
|
1111
|
+
width: min(360px, calc(100vw - 24px));
|
|
773
1112
|
z-index: 10000;
|
|
774
|
-
display: flex;
|
|
775
|
-
align-items: center;
|
|
776
|
-
justify-content: center;
|
|
777
1113
|
`;
|
|
778
1114
|
// Create popup
|
|
779
1115
|
this.popupElement = document.createElement('div');
|
|
780
1116
|
this.popupElement.style.cssText = `
|
|
781
1117
|
background: white;
|
|
782
1118
|
border-radius: 12px;
|
|
783
|
-
padding:
|
|
784
|
-
max-width:
|
|
1119
|
+
padding: 14px;
|
|
1120
|
+
max-width: 360px;
|
|
785
1121
|
position: relative;
|
|
786
1122
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
|
1123
|
+
opacity: 0;
|
|
1124
|
+
transition: opacity 0.2s ease;
|
|
1125
|
+
`;
|
|
1126
|
+
// SovAds logo badge in small left corner
|
|
1127
|
+
const logoBadge = document.createElement('div');
|
|
1128
|
+
logoBadge.style.cssText = `
|
|
1129
|
+
position: absolute;
|
|
1130
|
+
top: 8px;
|
|
1131
|
+
left: 12px;
|
|
1132
|
+
width: 24px;
|
|
1133
|
+
height: 24px;
|
|
1134
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1135
|
+
border-radius: 4px;
|
|
1136
|
+
display: flex;
|
|
1137
|
+
align-items: center;
|
|
1138
|
+
justify-content: center;
|
|
1139
|
+
font-size: 10px;
|
|
1140
|
+
font-weight: bold;
|
|
1141
|
+
color: white;
|
|
1142
|
+
z-index: 1;
|
|
1143
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
787
1144
|
`;
|
|
1145
|
+
logoBadge.textContent = 'SA';
|
|
1146
|
+
logoBadge.title = 'SovAds';
|
|
788
1147
|
// Close button
|
|
789
1148
|
const closeBtn = document.createElement('button');
|
|
790
1149
|
closeBtn.innerHTML = '×';
|
|
@@ -797,10 +1156,24 @@ export class Popup {
|
|
|
797
1156
|
font-size: 24px;
|
|
798
1157
|
cursor: pointer;
|
|
799
1158
|
color: #666;
|
|
1159
|
+
z-index: 2;
|
|
800
1160
|
`;
|
|
801
1161
|
closeBtn.addEventListener('click', () => {
|
|
802
1162
|
this.hide();
|
|
803
1163
|
});
|
|
1164
|
+
// Add "Ad" message text below logo
|
|
1165
|
+
const adLabel = document.createElement('div');
|
|
1166
|
+
adLabel.style.cssText = `
|
|
1167
|
+
position: absolute;
|
|
1168
|
+
top: 36px;
|
|
1169
|
+
left: 12px;
|
|
1170
|
+
font-size: 9px;
|
|
1171
|
+
color: #999;
|
|
1172
|
+
font-weight: 500;
|
|
1173
|
+
text-transform: uppercase;
|
|
1174
|
+
letter-spacing: 0.5px;
|
|
1175
|
+
`;
|
|
1176
|
+
adLabel.textContent = 'Ad';
|
|
804
1177
|
// Handle dummy ads
|
|
805
1178
|
if (this.currentAd.isDummy) {
|
|
806
1179
|
const dummyContent = document.createElement('div');
|
|
@@ -821,7 +1194,7 @@ export class Popup {
|
|
|
821
1194
|
message.textContent = 'Register your site to get ads';
|
|
822
1195
|
message.style.cssText = 'color: #333; font-size: 16px; font-weight: 500; margin-bottom: 16px;';
|
|
823
1196
|
const link = document.createElement('a');
|
|
824
|
-
link.href = this.currentAd.targetUrl;
|
|
1197
|
+
link.href = this.sovads.normalizeUrl(this.currentAd.targetUrl);
|
|
825
1198
|
link.target = '_blank';
|
|
826
1199
|
link.rel = 'noopener noreferrer';
|
|
827
1200
|
link.textContent = 'Register Now';
|
|
@@ -829,10 +1202,12 @@ export class Popup {
|
|
|
829
1202
|
dummyContent.appendChild(img);
|
|
830
1203
|
dummyContent.appendChild(message);
|
|
831
1204
|
dummyContent.appendChild(link);
|
|
1205
|
+
this.popupElement.appendChild(logoBadge);
|
|
1206
|
+
this.popupElement.appendChild(adLabel);
|
|
832
1207
|
this.popupElement.appendChild(closeBtn);
|
|
833
1208
|
this.popupElement.appendChild(dummyContent);
|
|
834
|
-
|
|
835
|
-
document.body.appendChild(
|
|
1209
|
+
wrapper.appendChild(this.popupElement);
|
|
1210
|
+
document.body.appendChild(wrapper);
|
|
836
1211
|
// Auto close after 10 seconds
|
|
837
1212
|
setTimeout(() => {
|
|
838
1213
|
this.hide();
|
|
@@ -841,15 +1216,14 @@ export class Popup {
|
|
|
841
1216
|
}
|
|
842
1217
|
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
843
1218
|
const handleMediaError = () => {
|
|
1219
|
+
if (this.popupElement) {
|
|
1220
|
+
this.popupElement.style.opacity = '1';
|
|
1221
|
+
}
|
|
844
1222
|
if (this.sovads.getConfig().debug) {
|
|
845
1223
|
console.warn(`Failed to load popup ad media: ${this.currentAd.bannerUrl}`);
|
|
846
1224
|
}
|
|
847
1225
|
const renderTime = Date.now() - renderStartTime;
|
|
848
|
-
|
|
849
|
-
rendered: false,
|
|
850
|
-
viewportVisible: true,
|
|
851
|
-
renderTime
|
|
852
|
-
});
|
|
1226
|
+
trackPopupImpression(false, renderTime);
|
|
853
1227
|
};
|
|
854
1228
|
let mediaElement;
|
|
855
1229
|
if (mediaType === 'video') {
|
|
@@ -862,7 +1236,11 @@ export class Popup {
|
|
|
862
1236
|
video.controls = true;
|
|
863
1237
|
video.style.cssText = 'width: 100%; height: auto; border-radius: 8px; cursor: pointer;';
|
|
864
1238
|
video.addEventListener('loadeddata', () => {
|
|
1239
|
+
if (this.popupElement) {
|
|
1240
|
+
this.popupElement.style.opacity = '1';
|
|
1241
|
+
}
|
|
865
1242
|
const renderTime = Date.now() - renderStartTime;
|
|
1243
|
+
trackPopupImpression(true, renderTime);
|
|
866
1244
|
if (this.sovads.getConfig().debug) {
|
|
867
1245
|
console.log(`Popup ad video loaded in ${renderTime}ms`);
|
|
868
1246
|
}
|
|
@@ -876,7 +1254,11 @@ export class Popup {
|
|
|
876
1254
|
img.alt = this.currentAd.description;
|
|
877
1255
|
img.style.cssText = 'width: 100%; height: auto; border-radius: 8px; cursor: pointer;';
|
|
878
1256
|
img.addEventListener('load', () => {
|
|
1257
|
+
if (this.popupElement) {
|
|
1258
|
+
this.popupElement.style.opacity = '1';
|
|
1259
|
+
}
|
|
879
1260
|
const renderTime = Date.now() - renderStartTime;
|
|
1261
|
+
trackPopupImpression(true, renderTime);
|
|
880
1262
|
if (this.sovads.getConfig().debug) {
|
|
881
1263
|
console.log(`Popup ad image loaded in ${renderTime}ms`);
|
|
882
1264
|
}
|
|
@@ -884,27 +1266,53 @@ export class Popup {
|
|
|
884
1266
|
img.addEventListener('error', handleMediaError);
|
|
885
1267
|
mediaElement = img;
|
|
886
1268
|
}
|
|
887
|
-
|
|
888
|
-
mediaElement.addEventListener('click', () => {
|
|
1269
|
+
const handleClickThrough = () => {
|
|
889
1270
|
this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
|
|
890
1271
|
rendered: true,
|
|
891
1272
|
viewportVisible: true,
|
|
892
1273
|
renderTime: Date.now() - renderStartTime
|
|
893
1274
|
});
|
|
894
|
-
// Log interaction
|
|
895
1275
|
this.sovads.logInteraction('CLICK', {
|
|
896
1276
|
adId: this.currentAd.id,
|
|
897
1277
|
campaignId: this.currentAd.campaignId,
|
|
898
1278
|
elementType: 'POPUP',
|
|
899
1279
|
metadata: { renderTime: Date.now() - renderStartTime },
|
|
900
1280
|
});
|
|
901
|
-
window.open(this.currentAd.targetUrl, '_blank', 'noopener,noreferrer');
|
|
1281
|
+
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
902
1282
|
this.hide();
|
|
903
|
-
}
|
|
1283
|
+
};
|
|
1284
|
+
if (mediaType === 'video') {
|
|
1285
|
+
mediaElement.style.cursor = 'default';
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
mediaElement.style.cursor = 'pointer';
|
|
1289
|
+
mediaElement.addEventListener('click', handleClickThrough);
|
|
1290
|
+
}
|
|
1291
|
+
this.popupElement.appendChild(logoBadge);
|
|
1292
|
+
this.popupElement.appendChild(adLabel);
|
|
904
1293
|
this.popupElement.appendChild(closeBtn);
|
|
905
1294
|
this.popupElement.appendChild(mediaElement);
|
|
906
|
-
|
|
907
|
-
|
|
1295
|
+
if (mediaType === 'video') {
|
|
1296
|
+
const ctaButton = document.createElement('button');
|
|
1297
|
+
ctaButton.type = 'button';
|
|
1298
|
+
ctaButton.textContent = 'Learn more';
|
|
1299
|
+
ctaButton.style.cssText = `
|
|
1300
|
+
width: 100%;
|
|
1301
|
+
margin-top: 10px;
|
|
1302
|
+
border: none;
|
|
1303
|
+
border-radius: 6px;
|
|
1304
|
+
background: #111;
|
|
1305
|
+
color: #fff;
|
|
1306
|
+
font-size: 12px;
|
|
1307
|
+
font-weight: 600;
|
|
1308
|
+
padding: 10px 12px;
|
|
1309
|
+
cursor: pointer;
|
|
1310
|
+
`;
|
|
1311
|
+
ctaButton.addEventListener('click', handleClickThrough);
|
|
1312
|
+
this.popupElement.appendChild(ctaButton);
|
|
1313
|
+
}
|
|
1314
|
+
wrapper.appendChild(this.popupElement);
|
|
1315
|
+
document.body.appendChild(wrapper);
|
|
908
1316
|
// Auto close after 10 seconds
|
|
909
1317
|
setTimeout(() => {
|
|
910
1318
|
this.hide();
|
|
@@ -913,23 +1321,43 @@ export class Popup {
|
|
|
913
1321
|
hide() {
|
|
914
1322
|
const overlay = document.querySelector('.sovads-popup-overlay');
|
|
915
1323
|
if (overlay) {
|
|
916
|
-
|
|
1324
|
+
try {
|
|
1325
|
+
// Check if element is still connected to DOM before removing
|
|
1326
|
+
if (overlay.isConnected) {
|
|
1327
|
+
// Use remove() method which is safer and doesn't require parentNode
|
|
1328
|
+
overlay.remove();
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
catch (error) {
|
|
1332
|
+
// Element may have already been removed by React or another process
|
|
1333
|
+
// Silently fail - this is expected in some cases
|
|
1334
|
+
if (this.sovads.getConfig().debug) {
|
|
1335
|
+
console.warn('Could not remove popup overlay:', error);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
917
1338
|
}
|
|
1339
|
+
this.popupElement = null;
|
|
1340
|
+
this.currentAd = null;
|
|
918
1341
|
}
|
|
919
1342
|
}
|
|
920
1343
|
// Sidebar Component
|
|
921
1344
|
export class Sidebar {
|
|
922
|
-
constructor(sovads, containerId) {
|
|
1345
|
+
constructor(sovads, containerId, slotConfig = {}) {
|
|
923
1346
|
this.currentAd = null;
|
|
924
1347
|
this.renderStartTime = 0;
|
|
925
1348
|
this.hasTrackedImpression = false;
|
|
926
1349
|
this.isRendering = false;
|
|
1350
|
+
this.refreshTimer = null;
|
|
1351
|
+
this.lastAdId = null;
|
|
1352
|
+
this.retryCount = 0;
|
|
1353
|
+
this.maxRetries = 3;
|
|
927
1354
|
this.sovads = sovads;
|
|
928
1355
|
this.containerId = containerId;
|
|
1356
|
+
this.slotConfig = slotConfig;
|
|
929
1357
|
}
|
|
930
|
-
async render(consumerId) {
|
|
1358
|
+
async render(consumerId, forceRefresh = false) {
|
|
931
1359
|
// Prevent concurrent renders
|
|
932
|
-
if (this.isRendering) {
|
|
1360
|
+
if (this.isRendering && !forceRefresh) {
|
|
933
1361
|
if (this.sovads.getConfig().debug) {
|
|
934
1362
|
console.warn(`Sidebar render already in progress for ${this.containerId}`);
|
|
935
1363
|
}
|
|
@@ -943,8 +1371,32 @@ export class Sidebar {
|
|
|
943
1371
|
this.isRendering = false;
|
|
944
1372
|
return;
|
|
945
1373
|
}
|
|
1374
|
+
// Lazy loading: wait for container to be in viewport
|
|
1375
|
+
if (this.sovads.getConfig().lazyLoad && !forceRefresh) {
|
|
1376
|
+
const isInViewport = await this.checkViewport(container);
|
|
1377
|
+
if (!isInViewport) {
|
|
1378
|
+
this.setupLazyLoadObserver(container, consumerId);
|
|
1379
|
+
this.isRendering = false;
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
946
1383
|
this.renderStartTime = Date.now();
|
|
947
|
-
this.currentAd = await this.sovads.loadAd(
|
|
1384
|
+
this.currentAd = await this.sovads.loadAd({
|
|
1385
|
+
consumerId,
|
|
1386
|
+
placement: this.slotConfig.placementId || 'sidebar',
|
|
1387
|
+
size: this.slotConfig.size,
|
|
1388
|
+
});
|
|
1389
|
+
this.hasTrackedImpression = false;
|
|
1390
|
+
// Skip if same ad (rotation disabled or same ad returned)
|
|
1391
|
+
if (!forceRefresh && this.lastAdId === this.currentAd?.id && this.sovads.getConfig().rotationEnabled) {
|
|
1392
|
+
if (this.sovads.getConfig().debug) {
|
|
1393
|
+
console.log('Same ad returned, skipping render');
|
|
1394
|
+
}
|
|
1395
|
+
this.isRendering = false;
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
this.lastAdId = this.currentAd?.id || null;
|
|
1399
|
+
this.retryCount = 0;
|
|
948
1400
|
if (!this.currentAd) {
|
|
949
1401
|
container.innerHTML = '<div class="sovads-no-ad">No ads available</div>';
|
|
950
1402
|
this.isRendering = false;
|
|
@@ -952,6 +1404,7 @@ export class Sidebar {
|
|
|
952
1404
|
}
|
|
953
1405
|
// Handle dummy ads for unregistered sites
|
|
954
1406
|
if (this.currentAd.isDummy) {
|
|
1407
|
+
container.innerHTML = '';
|
|
955
1408
|
const dummyElement = document.createElement('div');
|
|
956
1409
|
dummyElement.className = 'sovads-sidebar-dummy';
|
|
957
1410
|
dummyElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
@@ -983,7 +1436,7 @@ export class Sidebar {
|
|
|
983
1436
|
dummyElement.appendChild(img);
|
|
984
1437
|
dummyElement.appendChild(message);
|
|
985
1438
|
dummyElement.addEventListener('click', () => {
|
|
986
|
-
window.open(this.currentAd.targetUrl, '_blank', 'noopener,noreferrer');
|
|
1439
|
+
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
987
1440
|
});
|
|
988
1441
|
dummyElement.addEventListener('mouseenter', () => {
|
|
989
1442
|
dummyElement.style.background = '#f0f0f0';
|
|
@@ -998,18 +1451,20 @@ export class Sidebar {
|
|
|
998
1451
|
return;
|
|
999
1452
|
}
|
|
1000
1453
|
const adElement = document.createElement('div');
|
|
1454
|
+
container.innerHTML = '';
|
|
1001
1455
|
adElement.className = 'sovads-sidebar';
|
|
1002
1456
|
adElement.setAttribute('data-ad-id', this.currentAd.id);
|
|
1457
|
+
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
1003
1458
|
adElement.style.cssText = `
|
|
1004
1459
|
background: #f8f9fa;
|
|
1005
1460
|
border: 1px solid #e9ecef;
|
|
1006
1461
|
border-radius: 8px;
|
|
1007
1462
|
padding: 15px;
|
|
1008
1463
|
margin-bottom: 15px;
|
|
1009
|
-
cursor: pointer;
|
|
1464
|
+
cursor: ${mediaType === 'video' ? 'default' : 'pointer'};
|
|
1010
1465
|
transition: all 0.2s ease;
|
|
1466
|
+
opacity: 0;
|
|
1011
1467
|
`;
|
|
1012
|
-
const mediaType = this.currentAd.mediaType === 'video' ? 'video' : 'image';
|
|
1013
1468
|
const handleVisibilityTracking = (renderInfo) => {
|
|
1014
1469
|
this.sovads.setupRenderObserver(adElement, this.currentAd.id, (isVisible) => {
|
|
1015
1470
|
renderInfo.viewportVisible = isVisible;
|
|
@@ -1020,6 +1475,7 @@ export class Sidebar {
|
|
|
1020
1475
|
});
|
|
1021
1476
|
};
|
|
1022
1477
|
const handleRenderSuccess = () => {
|
|
1478
|
+
adElement.style.opacity = '1';
|
|
1023
1479
|
const renderTime = Date.now() - this.renderStartTime;
|
|
1024
1480
|
handleVisibilityTracking({
|
|
1025
1481
|
rendered: true,
|
|
@@ -1028,6 +1484,7 @@ export class Sidebar {
|
|
|
1028
1484
|
});
|
|
1029
1485
|
};
|
|
1030
1486
|
const handleRenderError = () => {
|
|
1487
|
+
adElement.style.opacity = '1';
|
|
1031
1488
|
if (this.sovads.getConfig().debug) {
|
|
1032
1489
|
console.warn(`Failed to load sidebar ad media: ${this.currentAd.bannerUrl}`);
|
|
1033
1490
|
}
|
|
@@ -1060,22 +1517,20 @@ export class Sidebar {
|
|
|
1060
1517
|
img.addEventListener('error', handleRenderError, { once: true });
|
|
1061
1518
|
mediaElement = img;
|
|
1062
1519
|
}
|
|
1063
|
-
|
|
1064
|
-
adElement.addEventListener('click', () => {
|
|
1520
|
+
const handleClickThrough = () => {
|
|
1065
1521
|
this.sovads._trackEvent('CLICK', this.currentAd.id, this.currentAd.campaignId, {
|
|
1066
1522
|
rendered: true,
|
|
1067
1523
|
viewportVisible: true,
|
|
1068
1524
|
renderTime: Date.now() - this.renderStartTime
|
|
1069
1525
|
});
|
|
1070
|
-
// Log interaction
|
|
1071
1526
|
this.sovads.logInteraction('CLICK', {
|
|
1072
1527
|
adId: this.currentAd.id,
|
|
1073
1528
|
campaignId: this.currentAd.campaignId,
|
|
1074
1529
|
elementType: 'SIDEBAR',
|
|
1075
1530
|
metadata: { renderTime: Date.now() - this.renderStartTime },
|
|
1076
1531
|
});
|
|
1077
|
-
window.open(this.currentAd.targetUrl, '_blank', 'noopener,noreferrer');
|
|
1078
|
-
}
|
|
1532
|
+
window.open(this.sovads.normalizeUrl(this.currentAd.targetUrl), '_blank', 'noopener,noreferrer');
|
|
1533
|
+
};
|
|
1079
1534
|
// Add hover effect
|
|
1080
1535
|
adElement.addEventListener('mouseenter', () => {
|
|
1081
1536
|
adElement.style.background = '#e9ecef';
|
|
@@ -1085,14 +1540,115 @@ export class Sidebar {
|
|
|
1085
1540
|
adElement.style.background = '#f8f9fa';
|
|
1086
1541
|
adElement.style.transform = 'translateY(0)';
|
|
1087
1542
|
});
|
|
1088
|
-
mediaElement.style.cursor = 'pointer';
|
|
1089
|
-
|
|
1543
|
+
mediaElement.style.cursor = mediaType === 'video' ? 'default' : 'pointer';
|
|
1544
|
+
if (mediaType === 'video') {
|
|
1545
|
+
const ctaButton = document.createElement('button');
|
|
1546
|
+
ctaButton.type = 'button';
|
|
1547
|
+
ctaButton.textContent = 'Learn more';
|
|
1548
|
+
ctaButton.style.cssText = `
|
|
1549
|
+
width: 100%;
|
|
1550
|
+
border: none;
|
|
1551
|
+
margin-top: 8px;
|
|
1552
|
+
background: #111;
|
|
1553
|
+
color: #fff;
|
|
1554
|
+
font-size: 12px;
|
|
1555
|
+
font-weight: 600;
|
|
1556
|
+
padding: 8px 12px;
|
|
1557
|
+
border-radius: 6px;
|
|
1558
|
+
cursor: pointer;
|
|
1559
|
+
`;
|
|
1560
|
+
ctaButton.addEventListener('click', handleClickThrough);
|
|
1561
|
+
adElement.appendChild(mediaElement);
|
|
1562
|
+
adElement.appendChild(ctaButton);
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
adElement.addEventListener('click', handleClickThrough);
|
|
1566
|
+
adElement.appendChild(mediaElement);
|
|
1567
|
+
}
|
|
1090
1568
|
container.appendChild(adElement);
|
|
1569
|
+
// Set up auto-refresh if enabled
|
|
1570
|
+
this.setupAutoRefresh(consumerId);
|
|
1571
|
+
}
|
|
1572
|
+
catch (error) {
|
|
1573
|
+
// Retry logic on error
|
|
1574
|
+
if (this.retryCount < this.maxRetries) {
|
|
1575
|
+
this.retryCount++;
|
|
1576
|
+
if (this.sovads.getConfig().debug) {
|
|
1577
|
+
console.warn(`Sidebar render failed, retrying (${this.retryCount}/${this.maxRetries})...`);
|
|
1578
|
+
}
|
|
1579
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * this.retryCount));
|
|
1580
|
+
this.isRendering = false;
|
|
1581
|
+
return this.render(consumerId, true);
|
|
1582
|
+
}
|
|
1583
|
+
else {
|
|
1584
|
+
const container = document.getElementById(this.containerId);
|
|
1585
|
+
if (container) {
|
|
1586
|
+
container.innerHTML = '<div class="sovads-error" style="padding: 10px; text-align: center; color: #666; font-size: 12px;">Ad temporarily unavailable</div>';
|
|
1587
|
+
}
|
|
1588
|
+
if (this.sovads.getConfig().debug) {
|
|
1589
|
+
console.error('Sidebar render failed after retries:', error);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1091
1592
|
}
|
|
1092
1593
|
finally {
|
|
1093
1594
|
this.isRendering = false;
|
|
1094
1595
|
}
|
|
1095
1596
|
}
|
|
1597
|
+
async checkViewport(element) {
|
|
1598
|
+
return new Promise((resolve) => {
|
|
1599
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
1600
|
+
resolve(true);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1604
|
+
entries.forEach((entry) => {
|
|
1605
|
+
if (entry.isIntersecting) {
|
|
1606
|
+
observer.disconnect();
|
|
1607
|
+
resolve(true);
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
}, { rootMargin: '50px' });
|
|
1611
|
+
observer.observe(element);
|
|
1612
|
+
setTimeout(() => {
|
|
1613
|
+
observer.disconnect();
|
|
1614
|
+
resolve(true);
|
|
1615
|
+
}, 5000);
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
setupLazyLoadObserver(container, consumerId) {
|
|
1619
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
1620
|
+
this.render(consumerId);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1624
|
+
entries.forEach((entry) => {
|
|
1625
|
+
if (entry.isIntersecting && !this.isRendering) {
|
|
1626
|
+
observer.disconnect();
|
|
1627
|
+
this.render(consumerId);
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
1630
|
+
}, { rootMargin: '50px' });
|
|
1631
|
+
observer.observe(container);
|
|
1632
|
+
}
|
|
1633
|
+
setupAutoRefresh(consumerId) {
|
|
1634
|
+
if (this.refreshTimer) {
|
|
1635
|
+
clearInterval(this.refreshTimer);
|
|
1636
|
+
}
|
|
1637
|
+
const refreshInterval = this.sovads.getConfig().refreshInterval || 0;
|
|
1638
|
+
if (refreshInterval > 0) {
|
|
1639
|
+
this.refreshTimer = window.setInterval(() => {
|
|
1640
|
+
if (!this.isRendering) {
|
|
1641
|
+
this.render(consumerId, true);
|
|
1642
|
+
}
|
|
1643
|
+
}, refreshInterval * 1000);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
destroy() {
|
|
1647
|
+
if (this.refreshTimer) {
|
|
1648
|
+
clearInterval(this.refreshTimer);
|
|
1649
|
+
this.refreshTimer = null;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1096
1652
|
}
|
|
1097
1653
|
// Export main SovAds class
|
|
1098
1654
|
export { SovAds };
|