Skip to content

Commit 05b881a

Browse files
Merge pull request #165 from StreetSupport/staging
Merge staging into main
2 parents 04dff66 + b2695dc commit 05b881a

File tree

5 files changed

+174
-54
lines changed

5 files changed

+174
-54
lines changed

src/app/api/analytics/download-count/route.ts

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,106 @@
11
import { NextResponse } from 'next/server';
2+
import { BetaAnalyticsDataClient } from '@google-analytics/data';
23

3-
// Note: This is a simplified implementation for demonstration.
4-
// For production, you would need to:
5-
// 1. Install Google Analytics Data API: npm install @google-analytics/data google-auth-library
6-
// 2. Set up a service account with GA4 read permissions
7-
// 3. Add environment variables for authentication
4+
// Note: This implementation uses the Google Analytics Data API (GA4).
5+
// It requires:
6+
// 1. @google-analytics/data and google-auth-library installed
7+
// 2. A service account with GA4 read permissions configured via Application Default Credentials
8+
// 3. GA4_PROPERTY_ID environment variable set to your GA4 property ID
9+
10+
const analyticsDataClient = new BetaAnalyticsDataClient({
11+
credentials: JSON.parse(
12+
process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON || '{}'
13+
),
14+
});
815

916
export async function GET(request: Request) {
1017
const { searchParams } = new URL(request.url);
1118
const fileName = searchParams.get('fileName');
1219
const fileType = searchParams.get('fileType');
1320
const resourceType = searchParams.get('resourceType');
21+
const bannerAnalyticsId = searchParams.get('banner_analytics_id');
1422

1523
if (!fileName) {
1624
return NextResponse.json({ error: 'fileName is required' }, { status: 400 });
1725
}
1826

1927
try {
2028
const measurementId = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
29+
const propertyId = process.env.GA4_PROPERTY_ID;
2130

2231
if (!measurementId) {
2332
console.error('NEXT_PUBLIC_GA_MEASUREMENT_ID not configured');
2433
return NextResponse.json({ error: 'Analytics not configured' }, { status: 500 });
2534
}
2635

27-
// For now, return a mock count that simulates GA data
28-
// In production, this would query the actual Google Analytics Data API
29-
const mockCount = Math.floor(Math.random() * 1000) + 50; // Random count between 50-1050
30-
31-
//console.log(`[Analytics] Fetching download count for: ${fileName} (${fileType}, ${resourceType})`);
32-
36+
if (!propertyId) {
37+
console.error('GA4_PROPERTY_ID not configured');
38+
return NextResponse.json({ error: 'Analytics property not configured' }, { status: 500 });
39+
}
40+
41+
if (!bannerAnalyticsId) {
42+
console.error('banner_analytics_id is required to fetch download count');
43+
return NextResponse.json({ error: 'banner_analytics_id is required' }, { status: 400 });
44+
}
45+
46+
const bannerId = bannerAnalyticsId as string;
47+
48+
const property = propertyId.startsWith('properties/')
49+
? propertyId
50+
: `properties/${propertyId}`;
51+
52+
const [report] = await analyticsDataClient.runReport({
53+
property,
54+
dateRanges: [{ startDate: '2025-11-11', endDate: 'today' }],
55+
dimensions: [
56+
{ name: 'eventName' },
57+
{ name: 'customEvent:banner_analytics_id' },
58+
],
59+
metrics: [{ name: 'eventCount' }],
60+
dimensionFilter: {
61+
andGroup: {
62+
expressions: [
63+
{
64+
filter: {
65+
fieldName: 'eventName',
66+
stringFilter: {
67+
matchType: 'EXACT',
68+
value: 'resource_file_download',
69+
},
70+
},
71+
},
72+
{
73+
filter: {
74+
fieldName: 'customEvent:banner_analytics_id',
75+
stringFilter: {
76+
matchType: 'EXACT',
77+
value: bannerId,
78+
},
79+
},
80+
},
81+
],
82+
},
83+
},
84+
});
85+
86+
let count = 0;
87+
if (report.rows && report.rows.length > 0) {
88+
const metricValue = report.rows[0].metricValues?.[0]?.value;
89+
if (metricValue !== undefined) {
90+
const parsed = parseInt(metricValue || '0', 10);
91+
if (!Number.isNaN(parsed)) {
92+
count = parsed;
93+
}
94+
}
95+
}
96+
3397
return NextResponse.json({
34-
count: mockCount,
98+
count,
3599
fileName,
36100
fileType,
37101
resourceType,
38-
source: 'mock_data' // This indicates it's mock data
102+
bannerAnalyticsId,
103+
source: 'ga4_data_api'
39104
});
40105

41106
} catch (error) {

src/app/api/banners/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ export async function GET(req: NextRequest) {
128128
...baseBanner,
129129
templateType: 'resource-project',
130130
resourceType: banner.ResourceProject?.ResourceFile?.ResourceType,
131-
downloadCount: banner.ResourceProject?.ResourceFile?.DownloadCount,
132131
lastUpdated: banner.ResourceProject?.ResourceFile?.LastUpdated
133132
? new Date(banner.ResourceProject.ResourceFile.LastUpdated).toISOString()
134133
: undefined,

src/components/Banners/ResourceProjectBanner.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface ResourceProjectBannerProps extends ResourceProjectProps {
1818
}
1919

2020
const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
21+
id,
2122
title,
2223
description,
2324
subtitle,
@@ -33,14 +34,13 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
3334
endDate,
3435
badgeText,
3536
resourceType,
36-
downloadCount: initialDownloadCount,
3737
lastUpdated,
3838
fileSize,
3939
fileType,
4040
trackingContext = 'resource-project',
4141
className = ''
4242
}) => {
43-
const [downloadCount, setDownloadCount] = useState<number | undefined>(initialDownloadCount);
43+
const [downloadCount, setDownloadCount] = useState<number | undefined>(0);
4444

4545
// Fetch download count from Google Analytics
4646
useEffect(() => {
@@ -51,6 +51,7 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
5151

5252
if (downloadButton) {
5353
const params = new URLSearchParams({
54+
banner_analytics_id: id,
5455
fileName: title,
5556
...(fileType && { fileType }),
5657
...(resourceType && { resourceType }),
@@ -59,10 +60,8 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
5960
const response = await fetch(`/api/analytics/download-count?${params.toString()}`);
6061

6162
if (response.ok) {
62-
// TODO: Uncomment it when we get value of downloadCount from GA4
63-
// const data = await response.json();
64-
// setDownloadCount(data.count);
65-
setDownloadCount(undefined);
63+
const data = await response.json();
64+
setDownloadCount(data.count);
6665
}
6766
}
6867
} catch (error) {
@@ -71,7 +70,7 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
7170
};
7271

7372
fetchDownloadCount();
74-
}, [title, fileType, resourceType, ctaButtons]);
73+
}, [title, fileType, resourceType, ctaButtons, id]);
7574

7675
const backgroundClasses = generateBackgroundClasses(background);
7776
const backgroundStyles = generateBackgroundStyles(background);
@@ -166,6 +165,19 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
166165
button_position: index + 1,
167166
tracking_context: button.trackingContext || trackingContext
168167
});
168+
169+
// Track downloads specifically: by default we set up file download to the first button
170+
if (index === 0) {
171+
window.gtag('event', 'resource_file_download', {
172+
banner_analytics_id: id,
173+
resource_title: title,
174+
resource_type: resourceType || 'unknown',
175+
file_type: fileType || 'unknown',
176+
cta_position: index + 1,
177+
button_label: button.label,
178+
tracking_context: button.trackingContext || trackingContext
179+
});
180+
}
169181
}
170182

171183
// Track downloads specifically: by default we set up file download to the first button
@@ -277,8 +289,8 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
277289

278290
{/* Resource Stats */}
279291
{/* Update to lg:grid-cols-3 when we configure downloadCount */}
280-
<div className="mb-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-4">
281-
{downloadCount !== undefined && downloadCount !== 0 && (
292+
<div className="mb-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
293+
{downloadCount !== undefined && (
282294
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
283295
<div className="flex items-center gap-2 mb-1">
284296
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
@@ -337,7 +349,7 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
337349
}
338350
: { href: button.url };
339351

340-
const isDownload = button.label.toLowerCase().includes('download');
352+
const isDownload = button.label.toLowerCase().includes('download') || index === 0;
341353

342354
return (
343355
<Component

src/types/banners.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export interface BannerProps {
7272
badgeText?: string;
7373

7474
// CMS metadata
75-
id?: string;
75+
id: string;
7676
templateType: BannerTemplateType;
7777
isActive?: boolean;
7878
locationSlug?: string;
@@ -105,7 +105,6 @@ export interface PartnershipCharterProps extends BannerProps {
105105
export interface ResourceProjectProps extends BannerProps {
106106
templateType: 'resource-project';
107107
resourceType?: 'guide' | 'toolkit' | 'research' | 'training' | 'event';
108-
downloadCount?: number;
109108
lastUpdated?: string;
110109
fileSize?: string;
111110
fileType?: string;

0 commit comments

Comments
 (0)