@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.
@@ -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
+ }
@@ -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
+