Measuring engagement and creating a report on user events
Author
james
Date Published

Step 1
Add the dependency. This bundles React chart
npm i @nouance/payload-dashboard-analytics

Step 2
Using google analytics and this git repo we add components to dashboard
draw a report on the traffic to the pages, that create the bookings the listing you created.
|_ π package.json
|_ π src
| |_ π AnalyticsUtilties
| | | |_ π ga4.ts
| |_ π app/(payload)/admin
| | | |_ π importMap.js.ts
|_ π componets
| |_ π AnalyticsDashboard
| | |_ π AnalyticsDashboard.tsx
| |_ π GoogleAnalytics.tsx
|_ π payload.config.ts
package.json
1 "@radix-ui/react-select": "^2.0.0",2 "@radix-ui/react-slot": "^1.1.1",3 "@revenuecat/purchases-js": "^0.18.2",4 "chart.js": "^4.4.9",5 "class-variance-authority": "^0.7.0",6 "clsx": "^2.1.1",7 "date-fns": "^4.1.0",8@@ -51,6 +52,7 @@9 "payload-admin-bar": "^1.0.6",10 "prism-react-renderer": "^2.3.1",11 "react": "^19.0.0",12 "react-chartjs-2": "^5.3.0",13 "react-day-picker": "^8.10.1",14 "react-dom": "^19.0.0",15 "react-hook-form": "7.45.4",
taken from google
1import { BetaAnalyticsDataClient } from '@google-analytics/data'23// Enhanced logging for initialization4console.log('=== GA4 Configuration Debug ===')5console.log('Environment:', process.env.NODE_ENV)6console.log('GA4 Property ID:', process.env.GA4_PROPERTY_ID)7console.log('Service Account JSON Length:', process.env.GOOGLE_SERVICE_ACCOUNT_JSON?.length || 0)89try {10 if (process.env.GOOGLE_SERVICE_ACCOUNT_JSON) {11 const serviceAccount = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON)12 console.log('Service Account Details:')13 console.log('- Project ID:', serviceAccount.project_id)14 console.log('- Client Email:', serviceAccount.client_email)15 console.log('- Private Key Length:', serviceAccount.private_key?.length || 0)16 } else {17 console.error('GOOGLE_SERVICE_ACCOUNT_JSON is empty or undefined')18 }19} catch (e) {20 console.error('Error parsing service account JSON:', e)21 console.error('Raw JSON:', process.env.GOOGLE_SERVICE_ACCOUNT_JSON)22}2324let analyticsDataClient: BetaAnalyticsDataClient | null = null2526// Initialize the analytics client27try {28 const serviceAccountJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON29 if (!serviceAccountJson) {30 console.error('GOOGLE_SERVICE_ACCOUNT_JSON environment variable is not set')31 } else {32 try {33 const credentials = JSON.parse(serviceAccountJson)34 analyticsDataClient = new BetaAnalyticsDataClient({35 credentials,36 })37 console.log('Analytics client initialized successfully')38 } catch (parseError) {39 console.error('Error parsing GOOGLE_SERVICE_ACCOUNT_JSON:', parseError)40 console.error('Service account JSON content:', serviceAccountJson)41 }42 }43} catch (e) {44 console.error('Error initializing analytics client:', e)45}4647export const getAnalyticsData = async () => {48 if (!analyticsDataClient) {49 console.error('Analytics client not initialized. Checking environment variables...')50 console.error('GA4_PROPERTY_ID:', process.env.GA4_PROPERTY_ID)51 console.error('GOOGLE_SERVICE_ACCOUNT_JSON exists:', !!process.env.GOOGLE_SERVICE_ACCOUNT_JSON)52 return {53 activeUsersNow: 0,54 total30DayUsers: 0,55 total30DayViews: 0,56 historicalData: [],57 }58 }5960 try {61 // Get realtime data62 console.log('Fetching realtime data...')63 console.log('Using property ID:', process.env.GA4_PROPERTY_ID)64 const realtimeResponse = await analyticsDataClient.runRealtimeReport({65 property: `properties/${process.env.GA4_PROPERTY_ID}`,66 dimensions: [{ name: 'minutesAgo' }],67 metrics: [{ name: 'activeUsers' }],68 })69 console.log('Raw realtime response:', JSON.stringify(realtimeResponse, null, 2))7071 // Get historical data72 console.log('Fetching historical data...')73 const historicalResponse = await analyticsDataClient.runReport({74 property: `properties/${process.env.GA4_PROPERTY_ID}`,75 dateRanges: [76 {77 startDate: '30daysAgo',78 endDate: 'today',79 },80 ],81 dimensions: [{ name: 'date' }],82 metrics: [{ name: 'totalUsers' }, { name: 'screenPageViews' }],83 })84 console.log('Raw historical response:', JSON.stringify(historicalResponse, null, 2))8586 // Process realtime data87 const activeUsersNow = realtimeResponse[0]?.rows?.[0]?.metricValues?.[0]?.value || '0'88 console.log('Processed activeUsersNow:', activeUsersNow)8990 // Process historical data91 const historicalData =92 historicalResponse[0]?.rows?.map((row) => {93 const processedRow = {94 date: row.dimensionValues?.[0]?.value || '',95 users: parseInt(row.metricValues?.[0]?.value || '0', 10),96 views: parseInt(row.metricValues?.[1]?.value || '0', 10),97 }98 console.log('Processing row:', processedRow)99 return processedRow100 }) || []101102 console.log('Processed historical data:', historicalData)103104 // Calculate totals105 const total30DayUsers = historicalData.reduce((sum: number, row) => sum + row.users, 0)106 const total30DayViews = historicalData.reduce((sum: number, row) => sum + row.views, 0)107108 console.log('Calculated totals:', {109 total30DayUsers,110 total30DayViews,111 })112113 return {114 activeUsersNow,115 total30DayUsers,116 total30DayViews,117 historicalData,118 }119 } catch (error) {120 console.error('Error in getAnalyticsData:', error)121 if (error instanceof Error) {122 console.error('Error details:', {123 message: error.message,124 stack: error.stack,125 })126 }127 return {128 activeUsersNow: 0,129 total30DayUsers: 0,130 total30DayViews: 0,131 historicalData: [],132 }133 }134}
1'use client'2import React, { useEffect, useState } from 'react'3import { Line } from 'react-chartjs-2'4import {5 Chart as ChartJS,6 CategoryScale,7 LinearScale,8 PointElement,9 LineElement,10 Title,11 Tooltip,12 Legend,13} from 'chart.js'1415ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend)1617interface AnalyticsData {18 activeUsersNow?: number19 total30DayUsers?: number20 total30DayViews?: number21 historicalData?: Array<{22 date: string23 users: number24 views: number25 }>26}2728const AnalyticsDashboard: React.FC = () => {29 const [data, setData] = useState<AnalyticsData | null>(null)30 const [loading, setLoading] = useState(true)31 const [error, setError] = useState<string | null>(null)3233 // fallback data34 const fallbackData: AnalyticsData = {35 historicalData: [36 { date: '2025-03-15', users: 4, views: 5 },37 { date: '2025-03-16', users: 5, views: 6 },38 { date: '2025-03-17', users: 6, views: 7 },39 { date: '2025-03-18', users: 2, views: 3 },40 { date: '2025-03-19', users: 1, views: 2 },41 { date: '2025-03-20', users: 10, views: 15 },42 { date: '2025-03-21', users: 8, views: 12 },43 { date: '2025-03-22', users: 11, views: 13 },44 { date: '2025-03-23', users: 7, views: 9 },45 { date: '2025-03-24', users: 4, views: 5 },46 { date: '2025-03-25', users: 13, views: 15 },47 { date: '2025-03-26', users: 14, views: 16 },48 { date: '2025-03-27', users: 12, views: 14 },49 { date: '2025-03-28', users: 16, views: 18 },50 { date: '2025-03-29', users: 13, views: 15 },51 { date: '2025-03-30', users: 14, views: 17 },52 { date: '2025-03-31', users: 3, views: 4 },53 { date: '2025-04-01', users: 20, views: 24 },54 ],55 }5657 const fetchAnalytics = async () => {58 try {59 const response = await fetch('/api/analytics', {60 headers: { 'Content-Type': 'application/json' },61 })6263 if (!response.ok) {64 throw new Error(`HTTP error! status: ${response.status}`)65 }6667 return await response.json()68 } catch (err) {69 console.error('Fetch error:', err)70 throw err instanceof Error ? err : new Error('Failed to fetch analytics')71 }72 }7374 useEffect(() => {75 const loadData = async () => {76 try {77 const analyticsData = await fetchAnalytics()7879 // Check if the data is empty or invalid80 const isEmptyData =81 !analyticsData ||82 (analyticsData.activeUsersNow === 0 &&83 analyticsData.total30DayUsers === 0 &&84 (!analyticsData.historicalData || analyticsData.historicalData.length === 0))8586 setData(isEmptyData ? fallbackData : analyticsData)87 setError(null)88 } catch (err) {89 console.error('Error loading analytics:', err)90 setData(fallbackData) // Use fallback data on error91 setError(err instanceof Error ? err.message : 'Failed to fetch analytics')92 } finally {93 setLoading(false)94 }95 }9697 loadData()98 // const interval = setInterval(loadData, 120000)99 const interval = setInterval(loadData, 8000)100 return () => clearInterval(interval)101 }, [])102103 const Card = ({104 title,105 value,106 style = {},107 }: {108 title: string109 value: number110 style?: React.CSSProperties111 }) => {112 const [hovered, setHovered] = useState(false)113114 return (115 <div116 style={{117 padding: '1.5rem',118 backgroundColor: 'var(--theme-elevation-50)',119 borderRadius: '8px',120 boxShadow: hovered ? '0 4px 12px rgba(0, 0, 0, 0.15)' : '0 2px 6px rgba(0,0,0,0.08)',121 border: hovered ? '1px solid var(--theme-elevation-200)' : '1px solid transparent',122 transition: 'all 0.2s ease-in-out',123 cursor: 'pointer',124 display: 'flex',125 flexDirection: 'column',126 justifyContent: 'space-between',127 minHeight: '120px',128 ...style,129 }}130 onMouseEnter={() => setHovered(true)}131 onMouseLeave={() => setHovered(false)}132 >133 <h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 600 }}>{title}</h3>134 <p style={{ fontSize: '2rem', fontWeight: 'bold', margin: '0.5rem 0 0' }}>{value}</p>135 </div>136 )137 }138139 // Determine which data to use (fallback if no data available)140 const displayData = data || fallbackData141 const chartData = displayData.historicalData || []142143 return (144 <div style={{ margin: '2rem 0', padding: '0 1rem' }}>145 <h2 style={{ fontSize: '1.8rem', marginBottom: '1rem' }}>π Google Analytics Overview</h2>146147 {loading && <div>Loading analytics data...</div>}148 {error && (149 <div150 style={{151 color: 'var(--theme-error-500)',152 backgroundColor: 'var(--theme-error-50)',153 padding: '1rem',154 borderRadius: '4px',155 marginBottom: '1rem',156 }}157 >158 {error} (using fallback data)159 </div>160 )}161162 <div163 style={{164 display: 'grid',165 marginTop: '2rem',166 gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))',167 gap: '1.5rem',168 marginBottom: '2rem',169 }}170 >171 <Card title="Active Users Now" value={displayData.activeUsersNow || 0} />172 <Card title="Total Users 30 Days" value={displayData.total30DayUsers || 0} />173 <Card title="Total Views 30 Days" value={displayData.total30DayViews || 0} />174 </div>175176 <div177 style={{178 padding: '2rem 1rem',179 backgroundColor: 'var(--theme-elevation-25)',180 borderRadius: '8px',181 boxShadow: '0 2px 6px rgba(0,0,0,0.08)',182 }}183 >184 <h3 style={{ marginTop: 0, marginBottom: '1rem' }}>π User Activity 30 Days</h3>185186 {chartData.length > 0 ? (187 <div style={{ width: '100%', height: '400px' }}>188 <Line189 data={{190 labels: chartData.map((item) => item.date),191 datasets: [192 {193 label: 'Users',194 data: chartData.map((item) => item.users),195 borderColor: 'rgb(75, 192, 192)',196 backgroundColor: 'rgba(75, 192, 192, 0.2)',197 tension: 0.3,198 fill: true,199 },200 {201 label: 'Views',202 data: chartData.map((item) => item.views),203 borderColor: 'rgb(53, 162, 235)',204 backgroundColor: 'rgba(53, 162, 235, 0.2)',205 tension: 0.3,206 fill: true,207 },208 ],209 }}210 options={{211 responsive: true,212 maintainAspectRatio: false,213 plugins: {214 legend: {215 position: 'top',216 },217 tooltip: {218 mode: 'index',219 intersect: false,220 },221 },222 scales: {223 x: {224 grid: {225 display: false,226 },227 },228 y: {229 beginAtZero: true,230 },231 },232 }}233 />234 </div>235 ) : (236 <p>No data available for the selected period.</p>237 )}238 </div>239 </div>240 )241}242243export default AnalyticsDashboard244
Show the traffic to the listing

|_ π package.json
|_ π src
| |_ π AnalyticsUtilties
| | | |_ π ga4.ts
| |_ π app/(payload)/admin
| | | |_ π importMap.js.ts
|_ π componets
| |_ π AnalyticsDashboard
| | |_ π AnalyticsDashboard.tsx
| |_ π GoogleAnalytics.tsx
|_ π payload.config.ts
12import React from 'react'3import Script from 'next/script'4const GoogleAnalytics = () => {5 console.log('Google Analytics ID:', process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS)6 return (7 <>8 <Script9 strategy="lazyOnload"10 src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS}`}11 />12 <Script id="google-analytics" strategy="lazyOnload">13 {`14 window.dataLayer = window.dataLayer || [];15 function gtag(){dataLayer.push(arguments);}16 gtag('js', new Date());17 gtag('config', '${process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS}', {18 page_path: window.location.pathname,19 });20 `}21 </Script>22 </>23 )24}25export default GoogleAnalytics
register it with the payload config replaing the BeforelLogin

1export default buildConfig({2 admin: {3 components: {4 afterDashboard: ['@/components/AnalyticsDashboardData/AnalyticsDashboard'],5 // The `BeforeLogin` component renders a message that you see while logging into your admin panel.6 // Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.7 beforeLogin: ['@/components/BeforeLogin'],

Coded theme examples. Customise the branding and initiate image storage. Own your own design system

Subscription payment and Sharing the policy using a protected route with a paywall to ensure privacy of users the payment is intended for