Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 78 additions & 13 deletions src/app/api/analytics/download-count/route.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,106 @@
import { NextResponse } from 'next/server';
import { BetaAnalyticsDataClient } from '@google-analytics/data';

// Note: This is a simplified implementation for demonstration.
// For production, you would need to:
// 1. Install Google Analytics Data API: npm install @google-analytics/data google-auth-library
// 2. Set up a service account with GA4 read permissions
// 3. Add environment variables for authentication
// Note: This implementation uses the Google Analytics Data API (GA4).
// It requires:
// 1. @google-analytics/data and google-auth-library installed
// 2. A service account with GA4 read permissions configured via Application Default Credentials
// 3. GA4_PROPERTY_ID environment variable set to your GA4 property ID

const analyticsDataClient = new BetaAnalyticsDataClient({
credentials: JSON.parse(
process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON || '{}'
),
});

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const fileName = searchParams.get('fileName');
const fileType = searchParams.get('fileType');
const resourceType = searchParams.get('resourceType');
const bannerAnalyticsId = searchParams.get('banner_analytics_id');

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

try {
const measurementId = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID;
const propertyId = process.env.GA4_PROPERTY_ID;

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

// For now, return a mock count that simulates GA data
// In production, this would query the actual Google Analytics Data API
const mockCount = Math.floor(Math.random() * 1000) + 50; // Random count between 50-1050

//console.log(`[Analytics] Fetching download count for: ${fileName} (${fileType}, ${resourceType})`);

if (!propertyId) {
console.error('GA4_PROPERTY_ID not configured');
return NextResponse.json({ error: 'Analytics property not configured' }, { status: 500 });
}

if (!bannerAnalyticsId) {
console.error('banner_analytics_id is required to fetch download count');
return NextResponse.json({ error: 'banner_analytics_id is required' }, { status: 400 });
}

const bannerId = bannerAnalyticsId as string;

const property = propertyId.startsWith('properties/')
? propertyId
: `properties/${propertyId}`;

const [report] = await analyticsDataClient.runReport({
property,
dateRanges: [{ startDate: '2025-11-11', endDate: 'today' }],
dimensions: [
{ name: 'eventName' },
{ name: 'customEvent:banner_analytics_id' },
],
metrics: [{ name: 'eventCount' }],
dimensionFilter: {
andGroup: {
expressions: [
{
filter: {
fieldName: 'eventName',
stringFilter: {
matchType: 'EXACT',
value: 'resource_file_download',
},
},
},
{
filter: {
fieldName: 'customEvent:banner_analytics_id',
stringFilter: {
matchType: 'EXACT',
value: bannerId,
},
},
},
],
},
},
});

let count = 0;
if (report.rows && report.rows.length > 0) {
const metricValue = report.rows[0].metricValues?.[0]?.value;
if (metricValue !== undefined) {
const parsed = parseInt(metricValue || '0', 10);
if (!Number.isNaN(parsed)) {
count = parsed;
}
}
}

return NextResponse.json({
count: mockCount,
count,
fileName,
fileType,
resourceType,
source: 'mock_data' // This indicates it's mock data
bannerAnalyticsId,
source: 'ga4_data_api'
});

} catch (error) {
Expand Down
1 change: 0 additions & 1 deletion src/app/api/banners/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ export async function GET(req: NextRequest) {
...baseBanner,
templateType: 'resource-project',
resourceType: banner.ResourceProject?.ResourceFile?.ResourceType,
downloadCount: banner.ResourceProject?.ResourceFile?.DownloadCount,
lastUpdated: banner.ResourceProject?.ResourceFile?.LastUpdated
? new Date(banner.ResourceProject.ResourceFile.LastUpdated).toISOString()
: undefined,
Expand Down
32 changes: 22 additions & 10 deletions src/components/Banners/ResourceProjectBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface ResourceProjectBannerProps extends ResourceProjectProps {
}

const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
id,
title,
description,
subtitle,
Expand All @@ -33,14 +34,13 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
endDate,
badgeText,
resourceType,
downloadCount: initialDownloadCount,
lastUpdated,
fileSize,
fileType,
trackingContext = 'resource-project',
className = ''
}) => {
const [downloadCount, setDownloadCount] = useState<number | undefined>(initialDownloadCount);
const [downloadCount, setDownloadCount] = useState<number | undefined>(0);

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

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

if (response.ok) {
// TODO: Uncomment it when we get value of downloadCount from GA4
// const data = await response.json();
// setDownloadCount(data.count);
setDownloadCount(undefined);
const data = await response.json();
setDownloadCount(data.count);
}
}
} catch (error) {
Expand All @@ -71,7 +70,7 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
};

fetchDownloadCount();
}, [title, fileType, resourceType, ctaButtons]);
}, [title, fileType, resourceType, ctaButtons, id]);

const backgroundClasses = generateBackgroundClasses(background);
const backgroundStyles = generateBackgroundStyles(background);
Expand Down Expand Up @@ -166,6 +165,19 @@ const ResourceProjectBanner: React.FC<ResourceProjectBannerProps> = ({
button_position: index + 1,
tracking_context: button.trackingContext || trackingContext
});

// Track downloads specifically: by default we set up file download to the first button
if (index === 0) {
window.gtag('event', 'resource_file_download', {
banner_analytics_id: id,
resource_title: title,
resource_type: resourceType || 'unknown',
file_type: fileType || 'unknown',
cta_position: index + 1,
button_label: button.label,
tracking_context: button.trackingContext || trackingContext
});
}
}

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

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

const isDownload = button.label.toLowerCase().includes('download');
const isDownload = button.label.toLowerCase().includes('download') || index === 0;

return (
<Component
Expand Down
3 changes: 1 addition & 2 deletions src/types/banners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export interface BannerProps {
badgeText?: string;

// CMS metadata
id?: string;
id: string;
templateType: BannerTemplateType;
isActive?: boolean;
locationSlug?: string;
Expand Down Expand Up @@ -105,7 +105,6 @@ export interface PartnershipCharterProps extends BannerProps {
export interface ResourceProjectProps extends BannerProps {
templateType: 'resource-project';
resourceType?: 'guide' | 'toolkit' | 'research' | 'training' | 'event';
downloadCount?: number;
lastUpdated?: string;
fileSize?: string;
fileType?: string;
Expand Down
Loading
Loading