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.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 = true;
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 canvas = document.createElement('canvas');
23
- const ctx = canvas.getContext('2d');
24
- ctx?.fillText('SovAds fingerprint', 10, 10);
25
- const fingerprint = [
26
- navigator.userAgent,
27
- navigator.language,
28
- screen.width + 'x' + screen.height,
29
- new Date().getTimezoneOffset(),
30
- canvas.toDataURL()
31
- ].join('|');
32
- return btoa(fingerprint).substring(0, 16);
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 parsed = new URL(url);
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(consumerId) {
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, consumerId },
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
- mediaType: rawAd.mediaType === 'video' ? 'video' : 'image',
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 webhookUrl = `${this.config.apiUrl}/api/webhook/beacon`;
380
- const response = await fetch(webhookUrl, {
381
- method: 'POST',
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
- else {
394
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
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
- // Use sendBeacon for reliable delivery (better for tracking impressions)
437
- // Beacon API ensures events are sent even if user navigates away
438
- if (navigator.sendBeacon) {
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.warn(`SovAds: Beacon failed for ${type}, falling back to fetch`);
589
+ console.log(`SovAds: Tracked ${type} event via signed beacon`, {
590
+ payload: { ...payload, fingerprint: payload.fingerprint.substring(0, 8) + '...' }
591
+ });
449
592
  }
450
- await this.trackEventWithRetry(type, adId, campaignId, renderInfo, 1);
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
- else {
460
- // Fallback to fetch for older browsers
461
- await this.trackEventWithRetry(type, adId, campaignId, renderInfo, 1);
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(consumerId);
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
- // Add click handler
683
- adElement.addEventListener('click', () => {
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.isShowing = true;
730
- this.currentAd = await this.sovads.loadAd(consumerId);
731
- if (!this.currentAd) {
732
- console.log('No popup ad available');
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
- // Show popup after delay
737
- setTimeout(() => {
738
- this.renderPopup();
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
- }, delay);
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
- // Don't track impressions for dummy ads
747
- if (!this.currentAd.isDummy) {
748
- // Track impression immediately when popup is rendered (not waiting for image load)
749
- // This ensures impression is tracked even if user closes popup quickly
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: true,
1093
+ rendered,
752
1094
  viewportVisible: true,
753
- renderTime: 0 // Will be updated when image loads
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: 0 },
1101
+ metadata: { renderTime, rendered },
761
1102
  });
762
- }
763
- // Create overlay
764
- const overlay = document.createElement('div');
765
- overlay.className = 'sovads-popup-overlay';
766
- overlay.style.cssText = `
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
- top: 0;
769
- left: 0;
770
- width: 100%;
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: 20px;
784
- max-width: 400px;
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
- overlay.appendChild(this.popupElement);
835
- document.body.appendChild(overlay);
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
- this.sovads._trackEvent('IMPRESSION', this.currentAd.id, this.currentAd.campaignId, {
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
- mediaElement.style.cursor = 'pointer';
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
- overlay.appendChild(this.popupElement);
907
- document.body.appendChild(overlay);
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
- overlay.remove();
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(consumerId);
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
- // Add click handler
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
- adElement.appendChild(mediaElement);
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 };