@thead-vantage/react 2.1.0 → 2.1.2
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/README.md +429 -84
- package/package.json +71 -49
- package/src/app/api/ads/route.ts +174 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +26 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +103 -0
- package/src/components/AdBanner.tsx +119 -0
- package/src/components/AdDisplay.tsx +98 -0
- package/src/index.ts +33 -0
- package/src/lib/ads.ts +275 -0
- package/src/lib/thead-vantage-config.ts +108 -0
- package/dist/components/AdBanner.d.ts +0 -12
- package/dist/index.d.ts +0 -1
- package/dist/index.esm.js +0 -2
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { getApiBaseUrl, isTheadVantageDevMode, isDevelopmentMode } from '@/lib/thead-vantage-config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* API route that proxies ads requests to TheAd Vantage Platform
|
|
6
|
+
* Supports three modes:
|
|
7
|
+
* 1. Production Mode (default) - https://thead-vantage.com/api/ads
|
|
8
|
+
* 2. Platform Developer Dev Mode - custom URL via NEXT_PUBLIC_THEAD_VANTAGE_API_URL
|
|
9
|
+
* 3. TheAd Vantage Dev Mode - localhost:3001 (NEXT_PUBLIC_THEAD_VANTAGE_DEV_MODE=true)
|
|
10
|
+
*/
|
|
11
|
+
export async function GET(request: NextRequest) {
|
|
12
|
+
try {
|
|
13
|
+
const isDevelopment = isDevelopmentMode();
|
|
14
|
+
const isTheadVantageDev = isTheadVantageDevMode();
|
|
15
|
+
|
|
16
|
+
// Get explicit API URL from query params if provided
|
|
17
|
+
const explicitApiUrl = request.nextUrl.searchParams.get('apiUrl') || undefined;
|
|
18
|
+
|
|
19
|
+
// Determine the base API URL using the three-mode system
|
|
20
|
+
const apiBaseUrl = getApiBaseUrl(explicitApiUrl);
|
|
21
|
+
|
|
22
|
+
// Get query parameters from the request (excluding apiUrl which we already used)
|
|
23
|
+
const searchParams = request.nextUrl.searchParams;
|
|
24
|
+
const params = new URLSearchParams();
|
|
25
|
+
|
|
26
|
+
// Copy all params except apiUrl
|
|
27
|
+
searchParams.forEach((value, key) => {
|
|
28
|
+
if (key !== 'apiUrl') {
|
|
29
|
+
params.append(key, value);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// In TheAd Vantage dev mode or general development, add flags to prevent tracking
|
|
34
|
+
if (isTheadVantageDev || isDevelopment) {
|
|
35
|
+
params.set('dev_mode', 'true');
|
|
36
|
+
params.set('no_track', 'true'); // Prevent impression tracking
|
|
37
|
+
params.set('no_click_track', 'true'); // Prevent click tracking
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Build the URL for TheAd Vantage platform
|
|
41
|
+
// If apiBaseUrl already includes /api/ads, use it directly, otherwise append
|
|
42
|
+
const advantageUrl = apiBaseUrl.includes('/api/ads')
|
|
43
|
+
? `${apiBaseUrl}?${params.toString()}`
|
|
44
|
+
: `${apiBaseUrl}/api/ads?${params.toString()}`;
|
|
45
|
+
|
|
46
|
+
// Fetch from advantage platform
|
|
47
|
+
const response = await fetch(advantageUrl, {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
// Forward any custom headers if needed
|
|
52
|
+
...(request.headers.get('user-agent') && {
|
|
53
|
+
'User-Agent': request.headers.get('user-agent') || '',
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
// Add cache control for development
|
|
57
|
+
cache: isDevelopment ? 'no-store' : 'default',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
// In TheAd Vantage dev mode, return a mock response instead of failing
|
|
62
|
+
if (isTheadVantageDev) {
|
|
63
|
+
return NextResponse.json({
|
|
64
|
+
success: true,
|
|
65
|
+
dev_mode: true,
|
|
66
|
+
ad: {
|
|
67
|
+
id: 'dev-ad-1',
|
|
68
|
+
imageUrl: '/placeholder-ad.png',
|
|
69
|
+
linkUrl: '#',
|
|
70
|
+
alt: 'Development Ad - No Tracking',
|
|
71
|
+
width: 300,
|
|
72
|
+
height: 250,
|
|
73
|
+
},
|
|
74
|
+
message: 'TheAd Vantage dev mode: Using mock ad (no tracking)',
|
|
75
|
+
}, { status: 200 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return NextResponse.json(
|
|
79
|
+
{ error: 'Failed to fetch ads from TheAd Vantage platform' },
|
|
80
|
+
{ status: response.status }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
|
|
86
|
+
// Add development mode indicator to response
|
|
87
|
+
if (isTheadVantageDev || isDevelopment) {
|
|
88
|
+
return NextResponse.json({
|
|
89
|
+
...data,
|
|
90
|
+
dev_mode: true,
|
|
91
|
+
_dev_note: 'Development mode: Impressions and clicks are not being tracked',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return NextResponse.json(data);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// In TheAd Vantage dev mode, return mock data instead of error
|
|
98
|
+
if (isTheadVantageDevMode()) {
|
|
99
|
+
console.warn('TheAd Vantage platform connection failed, using mock ad:', error);
|
|
100
|
+
return NextResponse.json({
|
|
101
|
+
success: true,
|
|
102
|
+
dev_mode: true,
|
|
103
|
+
ad: {
|
|
104
|
+
id: 'dev-ad-1',
|
|
105
|
+
imageUrl: '/placeholder-ad.png',
|
|
106
|
+
linkUrl: '#',
|
|
107
|
+
alt: 'Development Ad - No Tracking',
|
|
108
|
+
width: 300,
|
|
109
|
+
height: 250,
|
|
110
|
+
},
|
|
111
|
+
message: 'TheAd Vantage dev mode: Using mock ad (platform unavailable)',
|
|
112
|
+
}, { status: 200 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.error('Error fetching ads:', error);
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{ error: 'Failed to fetch ads' },
|
|
118
|
+
{ status: 500 }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle impression tracking (called when ad is viewed)
|
|
125
|
+
* In TheAd Vantage dev mode, this is a no-op
|
|
126
|
+
*/
|
|
127
|
+
export async function POST(request: NextRequest) {
|
|
128
|
+
try {
|
|
129
|
+
const isTheadVantageDev = isTheadVantageDevMode();
|
|
130
|
+
const isDevelopment = isDevelopmentMode();
|
|
131
|
+
const body = await request.json();
|
|
132
|
+
const { action, adId } = body;
|
|
133
|
+
|
|
134
|
+
// In TheAd Vantage dev mode or general development, don't track impressions or clicks
|
|
135
|
+
if (isTheadVantageDev || isDevelopment) {
|
|
136
|
+
return NextResponse.json({
|
|
137
|
+
success: true,
|
|
138
|
+
dev_mode: true,
|
|
139
|
+
message: `Development mode: ${action} tracking skipped for ad ${adId}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Get the API base URL to determine where to send tracking
|
|
144
|
+
const apiBaseUrl = getApiBaseUrl();
|
|
145
|
+
const trackingUrl = apiBaseUrl.includes('/api/ads')
|
|
146
|
+
? apiBaseUrl.replace('/api/ads', '/api/ads/track')
|
|
147
|
+
: `${apiBaseUrl}/api/ads/track`;
|
|
148
|
+
|
|
149
|
+
const response = await fetch(trackingUrl, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({ action, adId }),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
return NextResponse.json(
|
|
159
|
+
{ error: 'Failed to track ad event' },
|
|
160
|
+
{ status: response.status }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const data = await response.json();
|
|
165
|
+
return NextResponse.json(data);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Error tracking ad:', error);
|
|
168
|
+
return NextResponse.json(
|
|
169
|
+
{ error: 'Failed to track ad' },
|
|
170
|
+
{ status: 500 }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: #ffffff;
|
|
5
|
+
--foreground: #171717;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--background);
|
|
10
|
+
--color-foreground: var(--foreground);
|
|
11
|
+
--font-sans: var(--font-geist-sans);
|
|
12
|
+
--font-mono: var(--font-geist-mono);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@media (prefers-color-scheme: dark) {
|
|
16
|
+
:root {
|
|
17
|
+
--background: #0a0a0a;
|
|
18
|
+
--foreground: #ededed;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
background: var(--background);
|
|
24
|
+
color: var(--foreground);
|
|
25
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const geistSans = Geist({
|
|
6
|
+
variable: "--font-geist-sans",
|
|
7
|
+
subsets: ["latin"],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const geistMono = Geist_Mono({
|
|
11
|
+
variable: "--font-geist-mono",
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
title: "Create Next App",
|
|
17
|
+
description: "Generated by create next app",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({
|
|
21
|
+
children,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<body
|
|
28
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
);
|
|
34
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
|
|
3
|
+
export default function Home() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
|
6
|
+
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
|
7
|
+
<Image
|
|
8
|
+
className="dark:invert"
|
|
9
|
+
src="/next.svg"
|
|
10
|
+
alt="Next.js logo"
|
|
11
|
+
width={180}
|
|
12
|
+
height={38}
|
|
13
|
+
priority
|
|
14
|
+
/>
|
|
15
|
+
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
|
16
|
+
<li className="mb-2 tracking-[-.01em]">
|
|
17
|
+
Get started by editing{" "}
|
|
18
|
+
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
|
19
|
+
src/app/page.tsx
|
|
20
|
+
</code>
|
|
21
|
+
.
|
|
22
|
+
</li>
|
|
23
|
+
<li className="tracking-[-.01em]">
|
|
24
|
+
Save and see your changes instantly.
|
|
25
|
+
</li>
|
|
26
|
+
</ol>
|
|
27
|
+
|
|
28
|
+
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
|
29
|
+
<a
|
|
30
|
+
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
|
31
|
+
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
32
|
+
target="_blank"
|
|
33
|
+
rel="noopener noreferrer"
|
|
34
|
+
>
|
|
35
|
+
<Image
|
|
36
|
+
className="dark:invert"
|
|
37
|
+
src="/vercel.svg"
|
|
38
|
+
alt="Vercel logomark"
|
|
39
|
+
width={20}
|
|
40
|
+
height={20}
|
|
41
|
+
/>
|
|
42
|
+
Deploy now
|
|
43
|
+
</a>
|
|
44
|
+
<a
|
|
45
|
+
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
|
46
|
+
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
47
|
+
target="_blank"
|
|
48
|
+
rel="noopener noreferrer"
|
|
49
|
+
>
|
|
50
|
+
Read our docs
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
</main>
|
|
54
|
+
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
|
55
|
+
<a
|
|
56
|
+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
57
|
+
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
>
|
|
61
|
+
<Image
|
|
62
|
+
aria-hidden
|
|
63
|
+
src="/file.svg"
|
|
64
|
+
alt="File icon"
|
|
65
|
+
width={16}
|
|
66
|
+
height={16}
|
|
67
|
+
/>
|
|
68
|
+
Learn
|
|
69
|
+
</a>
|
|
70
|
+
<a
|
|
71
|
+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
72
|
+
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
73
|
+
target="_blank"
|
|
74
|
+
rel="noopener noreferrer"
|
|
75
|
+
>
|
|
76
|
+
<Image
|
|
77
|
+
aria-hidden
|
|
78
|
+
src="/window.svg"
|
|
79
|
+
alt="Window icon"
|
|
80
|
+
width={16}
|
|
81
|
+
height={16}
|
|
82
|
+
/>
|
|
83
|
+
Examples
|
|
84
|
+
</a>
|
|
85
|
+
<a
|
|
86
|
+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
87
|
+
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
88
|
+
target="_blank"
|
|
89
|
+
rel="noopener noreferrer"
|
|
90
|
+
>
|
|
91
|
+
<Image
|
|
92
|
+
aria-hidden
|
|
93
|
+
src="/globe.svg"
|
|
94
|
+
alt="Globe icon"
|
|
95
|
+
width={16}
|
|
96
|
+
height={16}
|
|
97
|
+
/>
|
|
98
|
+
Go to nextjs.org →
|
|
99
|
+
</a>
|
|
100
|
+
</footer>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { fetchAdBanner, trackImpression, trackClick, type Ad } from '@/lib/ads';
|
|
6
|
+
|
|
7
|
+
export interface AdBannerProps {
|
|
8
|
+
platformId: string;
|
|
9
|
+
apiKey: string;
|
|
10
|
+
size?: 'leaderboard' | 'medium-rectangle' | 'wide-skyscraper' | 'banner';
|
|
11
|
+
apiUrl?: string; // Optional explicit API URL override
|
|
12
|
+
userId?: string | null;
|
|
13
|
+
userSegment?: string | null;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AdBanner({
|
|
18
|
+
platformId,
|
|
19
|
+
apiKey,
|
|
20
|
+
size = 'banner',
|
|
21
|
+
apiUrl,
|
|
22
|
+
userId = null,
|
|
23
|
+
userSegment = null,
|
|
24
|
+
className = '',
|
|
25
|
+
}: AdBannerProps) {
|
|
26
|
+
const [ad, setAd] = useState<Ad | null>(null);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
const [error, setError] = useState<string | null>(null);
|
|
29
|
+
const [devMode, setDevMode] = useState(false);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const loadAd = async () => {
|
|
33
|
+
try {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
|
|
37
|
+
const response = await fetchAdBanner({
|
|
38
|
+
platformId,
|
|
39
|
+
apiKey,
|
|
40
|
+
size,
|
|
41
|
+
apiUrl,
|
|
42
|
+
userId,
|
|
43
|
+
userSegment,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (response.success && response.ad) {
|
|
47
|
+
setAd(response.ad);
|
|
48
|
+
setDevMode(response.dev_mode || false);
|
|
49
|
+
|
|
50
|
+
// Track impression (will be skipped in dev mode)
|
|
51
|
+
trackImpression(response.ad.id);
|
|
52
|
+
} else {
|
|
53
|
+
setError('No ads available');
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('[AdBanner] Error fetching ad:', err);
|
|
57
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch ad');
|
|
58
|
+
} finally {
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
loadAd();
|
|
64
|
+
}, [platformId, apiKey, size, apiUrl, userId, userSegment]);
|
|
65
|
+
|
|
66
|
+
const handleClick = () => {
|
|
67
|
+
if (ad) {
|
|
68
|
+
trackClick(ad.id);
|
|
69
|
+
// The link will handle navigation
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (loading) {
|
|
74
|
+
return (
|
|
75
|
+
<div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
|
|
76
|
+
<span className="text-sm text-gray-500">Loading ad...</span>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error || !ad) {
|
|
82
|
+
return (
|
|
83
|
+
<div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
|
|
84
|
+
<span className="text-sm text-gray-500">{error || 'Ad unavailable'}</span>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className={className}>
|
|
91
|
+
<a
|
|
92
|
+
href={ad.targetUrl}
|
|
93
|
+
onClick={handleClick}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noopener noreferrer"
|
|
96
|
+
className="block"
|
|
97
|
+
>
|
|
98
|
+
{ad.type === 'image' ? (
|
|
99
|
+
<Image
|
|
100
|
+
src={ad.contentUrl}
|
|
101
|
+
alt={ad.name}
|
|
102
|
+
width={ad.width || 300}
|
|
103
|
+
height={ad.height || 250}
|
|
104
|
+
className="rounded"
|
|
105
|
+
unoptimized
|
|
106
|
+
/>
|
|
107
|
+
) : (
|
|
108
|
+
<div className="flex items-center justify-center bg-gray-200 dark:bg-gray-700 rounded" style={{ width: ad.width || 300, height: ad.height || 250 }}>
|
|
109
|
+
<span className="text-sm text-gray-500">Ad content</span>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</a>
|
|
113
|
+
{devMode && (
|
|
114
|
+
<p className="text-xs text-gray-500 mt-1">[DEV] No tracking active</p>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { fetchAds, trackImpression, trackClick, type AdData } from '@/lib/ads';
|
|
6
|
+
|
|
7
|
+
interface AdDisplayProps {
|
|
8
|
+
position?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function AdDisplay({ position, className = '' }: AdDisplayProps) {
|
|
13
|
+
const [ad, setAd] = useState<AdData | null>(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
const [devMode, setDevMode] = useState(false);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
async function loadAd() {
|
|
20
|
+
try {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
|
|
24
|
+
const params: Record<string, string> = {};
|
|
25
|
+
if (position) {
|
|
26
|
+
params.position = position;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const response = await fetchAds(params);
|
|
30
|
+
|
|
31
|
+
if (response.success && response.ad) {
|
|
32
|
+
setAd(response.ad);
|
|
33
|
+
setDevMode(response.dev_mode || false);
|
|
34
|
+
|
|
35
|
+
// Track impression (will be skipped in dev mode)
|
|
36
|
+
trackImpression(response.ad.id);
|
|
37
|
+
} else {
|
|
38
|
+
setError('No ad available');
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('Error loading ad:', err);
|
|
42
|
+
setError('Failed to load ad');
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
loadAd();
|
|
49
|
+
}, [position]);
|
|
50
|
+
|
|
51
|
+
const handleClick = () => {
|
|
52
|
+
if (ad) {
|
|
53
|
+
trackClick(ad.id);
|
|
54
|
+
// The link will handle navigation
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (loading) {
|
|
59
|
+
return (
|
|
60
|
+
<div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
|
|
61
|
+
<span className="text-sm text-gray-500">Loading ad...</span>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (error || !ad) {
|
|
67
|
+
return (
|
|
68
|
+
<div className={`flex items-center justify-center bg-gray-100 dark:bg-gray-800 rounded ${className}`}>
|
|
69
|
+
<span className="text-sm text-gray-500">{error || 'Ad unavailable'}</span>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className={className}>
|
|
76
|
+
<a
|
|
77
|
+
href={ad.linkUrl}
|
|
78
|
+
onClick={handleClick}
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noopener noreferrer"
|
|
81
|
+
className="block"
|
|
82
|
+
>
|
|
83
|
+
<Image
|
|
84
|
+
src={ad.imageUrl}
|
|
85
|
+
alt={ad.alt}
|
|
86
|
+
width={ad.width || 300}
|
|
87
|
+
height={ad.height || 250}
|
|
88
|
+
className="rounded"
|
|
89
|
+
unoptimized
|
|
90
|
+
/>
|
|
91
|
+
</a>
|
|
92
|
+
{devMode && (
|
|
93
|
+
<p className="text-xs text-gray-500 mt-1">[DEV] No tracking active</p>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @thead-vantage/react
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for TheAd Vantage React library
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Export components
|
|
8
|
+
export { AdBanner, type AdBannerProps } from './components/AdBanner';
|
|
9
|
+
export { default as AdDisplay } from './components/AdDisplay';
|
|
10
|
+
|
|
11
|
+
// Export utilities
|
|
12
|
+
export {
|
|
13
|
+
fetchAds,
|
|
14
|
+
fetchAdBanner,
|
|
15
|
+
trackImpression,
|
|
16
|
+
trackClick,
|
|
17
|
+
type Ad,
|
|
18
|
+
type AdData,
|
|
19
|
+
type AdsResponse,
|
|
20
|
+
type AdBannerResponse,
|
|
21
|
+
type FetchAdBannerParams,
|
|
22
|
+
} from './lib/ads';
|
|
23
|
+
|
|
24
|
+
// Export configuration utilities
|
|
25
|
+
export {
|
|
26
|
+
getApiBaseUrl,
|
|
27
|
+
isTheadVantageDevMode,
|
|
28
|
+
isDevelopmentMode,
|
|
29
|
+
setTheadVantageConfig,
|
|
30
|
+
getTheadVantageConfig,
|
|
31
|
+
type TheadVantageConfig,
|
|
32
|
+
} from './lib/thead-vantage-config';
|
|
33
|
+
|