@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.
- package/CHANGELOG.md +15 -0
- package/README.md +147 -0
- package/dist/chunk-7IJIBJD4.mjs +85 -0
- package/dist/index.d.mts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +41 -13
- package/dist/index.mjs +1 -1
- package/dist/react/index.d.mts +18 -1
- package/dist/react/index.d.ts +18 -1
- package/dist/react/index.js +330 -17
- package/dist/react/index.mjs +288 -4
- package/doc/Banner_Example.png +0 -0
- package/doc/Interstitial_Example.png +0 -0
- package/package.json +3 -3
- package/src/core/AdTogether.ts +52 -14
- package/src/core/types.ts +8 -1
- package/src/react/AdTogetherBanner.tsx +2 -2
- package/src/react/AdTogetherInterstitial.tsx +328 -0
- package/src/react/index.ts +1 -0
- package/dist/chunk-IDJUJZAL.mjs +0 -57
|
@@ -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
|
+
};
|
package/src/react/index.ts
CHANGED
package/dist/chunk-IDJUJZAL.mjs
DELETED
|
@@ -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
|
-
};
|