@adtogether/web-sdk 0.1.0 → 0.1.5

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.
@@ -0,0 +1,328 @@
1
+ import React, { useEffect, useRef, useState, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { AdTogether } from '../core/AdTogether';
4
+ import { AdModel } from '../core/types';
5
+
6
+ export interface AdTogetherInterstitialProps {
7
+ adUnitId: string;
8
+ /** When true, the interstitial is shown (fetches and displays full-screen ad) */
9
+ isOpen: boolean;
10
+ /** Called when the user closes the interstitial */
11
+ onClose: () => void;
12
+ /** Called when the ad is successfully loaded */
13
+ onAdLoaded?: () => void;
14
+ /** Called when the ad fails to load */
15
+ onAdFailedToLoad?: (error: Error) => void;
16
+ /** Pass 'dark' to use dark mode, 'light' for light mode, or 'auto' (default) to respect system preference */
17
+ theme?: 'dark' | 'light' | 'auto';
18
+ /** Delay in seconds before the close button appears. Defaults to 3 */
19
+ closeDelay?: number;
20
+ }
21
+
22
+ export const AdTogetherInterstitial: React.FC<AdTogetherInterstitialProps> = ({
23
+ adUnitId,
24
+ isOpen,
25
+ onClose,
26
+ onAdLoaded,
27
+ onAdFailedToLoad,
28
+ theme = 'auto',
29
+ closeDelay = 3,
30
+ }) => {
31
+ const [adData, setAdData] = useState<AdModel | null>(null);
32
+ const [isLoading, setIsLoading] = useState(false);
33
+ const [hasError, setHasError] = useState(false);
34
+ const [isDarkMode, setIsDarkMode] = useState(theme === 'dark');
35
+ const [canClose, setCanClose] = useState(false);
36
+ const [countdown, setCountdown] = useState(closeDelay);
37
+
38
+ const impressionTrackedRef = useRef(false);
39
+ const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
40
+ const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
41
+
42
+ // Handle system theme changes
43
+ useEffect(() => {
44
+ if (theme === 'auto') {
45
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
46
+ setIsDarkMode(mediaQuery.matches);
47
+
48
+ const handler = (e: MediaQueryListEvent) => setIsDarkMode(e.matches);
49
+ mediaQuery.addEventListener('change', handler);
50
+ return () => mediaQuery.removeEventListener('change', handler);
51
+ } else {
52
+ setIsDarkMode(theme === 'dark');
53
+ }
54
+ }, [theme]);
55
+
56
+ // Fetch ad when opened
57
+ useEffect(() => {
58
+ if (!isOpen) {
59
+ // Reset state when closed
60
+ setAdData(null);
61
+ setIsLoading(false);
62
+ setHasError(false);
63
+ setCanClose(false);
64
+ setCountdown(closeDelay);
65
+ impressionTrackedRef.current = false;
66
+
67
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
68
+ if (countdownRef.current) clearInterval(countdownRef.current);
69
+ return;
70
+ }
71
+
72
+ let isMounted = true;
73
+ setIsLoading(true);
74
+
75
+ AdTogether.fetchAd(adUnitId, 'interstitial')
76
+ .then((ad) => {
77
+ if (isMounted) {
78
+ setAdData(ad);
79
+ setIsLoading(false);
80
+ onAdLoaded?.();
81
+ }
82
+ })
83
+ .catch((err) => {
84
+ if (isMounted) {
85
+ console.error('AdTogether Failed to load interstitial:', err);
86
+ setHasError(true);
87
+ setIsLoading(false);
88
+ onAdFailedToLoad?.(err);
89
+ onClose();
90
+ }
91
+ });
92
+
93
+ return () => {
94
+ isMounted = false;
95
+ };
96
+ }, [isOpen, adUnitId]);
97
+
98
+ // Close delay countdown
99
+ useEffect(() => {
100
+ if (!isOpen || !adData) return;
101
+
102
+ setCountdown(closeDelay);
103
+
104
+ countdownRef.current = setInterval(() => {
105
+ setCountdown((prev) => {
106
+ if (prev <= 1) {
107
+ if (countdownRef.current) clearInterval(countdownRef.current);
108
+ setCanClose(true);
109
+ return 0;
110
+ }
111
+ return prev - 1;
112
+ });
113
+ }, 1000);
114
+
115
+ return () => {
116
+ if (countdownRef.current) clearInterval(countdownRef.current);
117
+ };
118
+ }, [isOpen, adData, closeDelay]);
119
+
120
+ // Impression tracking
121
+ useEffect(() => {
122
+ if (!adData || !isOpen || impressionTrackedRef.current) return;
123
+ impressionTrackedRef.current = true;
124
+ AdTogether.trackImpression(adData.id, adData.token);
125
+ }, [adData, isOpen]);
126
+
127
+ const handleAdClick = useCallback(() => {
128
+ if (!adData) return;
129
+ AdTogether.trackClick(adData.id, adData.token);
130
+ if (adData.clickUrl) {
131
+ window.open(adData.clickUrl, '_blank', 'noopener,noreferrer');
132
+ }
133
+ }, [adData]);
134
+
135
+ const handleClose = useCallback(() => {
136
+ if (canClose) {
137
+ onClose();
138
+ }
139
+ }, [canClose, onClose]);
140
+
141
+ // Block body scroll when open
142
+ useEffect(() => {
143
+ if (isOpen) {
144
+ document.body.style.overflow = 'hidden';
145
+ } else {
146
+ document.body.style.overflow = '';
147
+ }
148
+ return () => {
149
+ document.body.style.overflow = '';
150
+ };
151
+ }, [isOpen]);
152
+
153
+ if (!isOpen) return null;
154
+
155
+ const bgOverlay = 'rgba(0, 0, 0, 0.7)';
156
+ const cardBg = isDarkMode ? '#1F2937' : '#ffffff';
157
+ const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
158
+ const textColor = isDarkMode ? '#F9FAFB' : '#111827';
159
+ const descColor = isDarkMode ? '#9CA3AF' : '#6B7280';
160
+
161
+ const content = (
162
+ <div
163
+ className="adtogether-interstitial-overlay"
164
+ style={{
165
+ position: 'fixed',
166
+ top: 0,
167
+ left: 0,
168
+ width: '100vw',
169
+ height: '100vh',
170
+ backgroundColor: bgOverlay,
171
+ display: 'flex',
172
+ alignItems: 'center',
173
+ justifyContent: 'center',
174
+ zIndex: 100000,
175
+ backdropFilter: 'blur(8px)',
176
+ animation: 'adtogether-fade-in 0.3s ease-out',
177
+ }}
178
+ onClick={(e) => {
179
+ if (e.target === e.currentTarget && canClose) handleClose();
180
+ }}
181
+ >
182
+ <style>{`
183
+ @keyframes adtogether-fade-in {
184
+ from { opacity: 0; }
185
+ to { opacity: 1; }
186
+ }
187
+ @keyframes adtogether-scale-in {
188
+ from { opacity: 0; transform: scale(0.9); }
189
+ to { opacity: 1; transform: scale(1); }
190
+ }
191
+ `}</style>
192
+
193
+ {isLoading ? (
194
+ <div style={{ color: '#fff', fontSize: '18px' }}>Loading Ad...</div>
195
+ ) : adData ? (
196
+ <div
197
+ style={{
198
+ position: 'relative',
199
+ maxWidth: '800px',
200
+ width: '95%',
201
+ backgroundColor: cardBg,
202
+ borderRadius: '24px',
203
+ border: `1px solid ${borderColor}`,
204
+ overflow: 'hidden',
205
+ boxShadow: '0 25px 50px rgba(0, 0, 0, 0.5)',
206
+ animation: 'adtogether-scale-in 0.3s ease-out',
207
+ }}
208
+ >
209
+ {/* Close / Countdown Button */}
210
+ <div style={{ position: 'absolute', top: '12px', right: '12px', zIndex: 10 }}>
211
+ {canClose ? (
212
+ <button
213
+ onClick={handleClose}
214
+ style={{
215
+ width: '36px',
216
+ height: '36px',
217
+ borderRadius: '50%',
218
+ backgroundColor: 'rgba(0,0,0,0.6)',
219
+ border: 'none',
220
+ color: '#fff',
221
+ fontSize: '18px',
222
+ cursor: 'pointer',
223
+ display: 'flex',
224
+ alignItems: 'center',
225
+ justifyContent: 'center',
226
+ backdropFilter: 'blur(4px)',
227
+ }}
228
+ aria-label="Close ad"
229
+ >
230
+
231
+ </button>
232
+ ) : (
233
+ <div
234
+ style={{
235
+ width: '36px',
236
+ height: '36px',
237
+ borderRadius: '50%',
238
+ backgroundColor: 'rgba(0,0,0,0.6)',
239
+ color: '#fff',
240
+ fontSize: '14px',
241
+ fontWeight: 'bold',
242
+ display: 'flex',
243
+ alignItems: 'center',
244
+ justifyContent: 'center',
245
+ backdropFilter: 'blur(4px)',
246
+ }}
247
+ >
248
+ {countdown}
249
+ </div>
250
+ )}
251
+ </div>
252
+
253
+ {/* Ad Image */}
254
+ {adData.imageUrl && (
255
+ <div
256
+ style={{ cursor: 'pointer', position: 'relative' }}
257
+ onClick={handleAdClick}
258
+ >
259
+ <img
260
+ src={adData.imageUrl}
261
+ alt={adData.title}
262
+ style={{
263
+ width: '100%',
264
+ aspectRatio: '16/9',
265
+ objectFit: 'cover',
266
+ display: 'block',
267
+ }}
268
+ />
269
+ </div>
270
+ )}
271
+
272
+ {/* Ad Content */}
273
+ <div
274
+ style={{ padding: '20px', cursor: 'pointer' }}
275
+ onClick={handleAdClick}
276
+ >
277
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
278
+ <span style={{ fontWeight: 'bold', fontSize: '18px', color: textColor }}>
279
+ {adData.title}
280
+ </span>
281
+ <span
282
+ style={{
283
+ backgroundColor: '#FBBF24',
284
+ color: '#000',
285
+ fontSize: '10px',
286
+ fontWeight: 'bold',
287
+ padding: '3px 6px',
288
+ borderRadius: '4px',
289
+ marginLeft: '8px',
290
+ flexShrink: 0,
291
+ }}
292
+ >
293
+ AD
294
+ </span>
295
+ </div>
296
+ <p style={{ fontSize: '14px', color: descColor, margin: 0, lineHeight: 1.5 }}>
297
+ {adData.description}
298
+ </p>
299
+
300
+ {/* CTA Button */}
301
+ <button
302
+ style={{
303
+ marginTop: '16px',
304
+ width: '100%',
305
+ padding: '12px',
306
+ backgroundColor: '#F59E0B',
307
+ color: '#000',
308
+ fontWeight: 'bold',
309
+ fontSize: '14px',
310
+ border: 'none',
311
+ borderRadius: '12px',
312
+ cursor: 'pointer',
313
+ }}
314
+ onClick={(e) => {
315
+ e.stopPropagation();
316
+ handleAdClick();
317
+ }}
318
+ >
319
+ Learn More →
320
+ </button>
321
+ </div>
322
+ </div>
323
+ ) : null}
324
+ </div>
325
+ );
326
+
327
+ return createPortal(content, document.body);
328
+ };
@@ -1 +1,2 @@
1
1
  export * from './AdTogetherBanner';
2
+ export * from './AdTogetherInterstitial';
@@ -1,57 +0,0 @@
1
- // src/core/AdTogether.ts
2
- var AdTogether = class _AdTogether {
3
- constructor() {
4
- this.baseUrl = "https://adtogether.com";
5
- }
6
- static get shared() {
7
- if (!_AdTogether.instance) {
8
- _AdTogether.instance = new _AdTogether();
9
- }
10
- return _AdTogether.instance;
11
- }
12
- static initialize(options) {
13
- const sdk = _AdTogether.shared;
14
- sdk.appId = options.appId;
15
- if (options.baseUrl) {
16
- sdk.baseUrl = options.baseUrl;
17
- }
18
- console.log(`AdTogether SDK Initialized with App ID: ${options.appId}`);
19
- }
20
- assertInitialized() {
21
- if (!this.appId) {
22
- console.error("AdTogether Error: SDK has not been initialized. Please call AdTogether.initialize() before displaying ads.");
23
- return false;
24
- }
25
- return true;
26
- }
27
- static async fetchAd(adUnitId) {
28
- if (!_AdTogether.shared.assertInitialized()) {
29
- throw new Error("AdTogether not initialized");
30
- }
31
- const response = await fetch(`${_AdTogether.shared.baseUrl}/api/ads/serve?country=global&adUnitId=${adUnitId}`);
32
- if (!response.ok) {
33
- throw new Error(`Failed to fetch ad. Status: ${response.status}`);
34
- }
35
- return response.json();
36
- }
37
- static trackImpression(adId) {
38
- this.trackEvent("/api/ads/impression", adId);
39
- }
40
- static trackClick(adId) {
41
- this.trackEvent("/api/ads/click", adId);
42
- }
43
- static trackEvent(endpoint, adId) {
44
- if (!_AdTogether.shared.assertInitialized()) return;
45
- fetch(`${_AdTogether.shared.baseUrl}${endpoint}`, {
46
- method: "POST",
47
- headers: {
48
- "Content-Type": "application/json"
49
- },
50
- body: JSON.stringify({ adId })
51
- }).catch(console.error);
52
- }
53
- };
54
-
55
- export {
56
- AdTogether
57
- };