Payload Logo
Components, Β User interaction

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'
2
3// Enhanced logging for initialization
4console.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)
8
9try {
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}
23
24let analyticsDataClient: BetaAnalyticsDataClient | null = null
25
26// Initialize the analytics client
27try {
28 const serviceAccountJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON
29 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}
46
47export 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 }
59
60 try {
61 // Get realtime data
62 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))
70
71 // Get historical data
72 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))
85
86 // Process realtime data
87 const activeUsersNow = realtimeResponse[0]?.rows?.[0]?.metricValues?.[0]?.value || '0'
88 console.log('Processed activeUsersNow:', activeUsersNow)
89
90 // Process historical data
91 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 processedRow
100 }) || []
101
102 console.log('Processed historical data:', historicalData)
103
104 // Calculate totals
105 const total30DayUsers = historicalData.reduce((sum: number, row) => sum + row.users, 0)
106 const total30DayViews = historicalData.reduce((sum: number, row) => sum + row.views, 0)
107
108 console.log('Calculated totals:', {
109 total30DayUsers,
110 total30DayViews,
111 })
112
113 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'
14
15ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend)
16
17interface AnalyticsData {
18 activeUsersNow?: number
19 total30DayUsers?: number
20 total30DayViews?: number
21 historicalData?: Array<{
22 date: string
23 users: number
24 views: number
25 }>
26}
27
28const 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)
32
33 // fallback data
34 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 }
56
57 const fetchAnalytics = async () => {
58 try {
59 const response = await fetch('/api/analytics', {
60 headers: { 'Content-Type': 'application/json' },
61 })
62
63 if (!response.ok) {
64 throw new Error(`HTTP error! status: ${response.status}`)
65 }
66
67 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 }
73
74 useEffect(() => {
75 const loadData = async () => {
76 try {
77 const analyticsData = await fetchAnalytics()
78
79 // Check if the data is empty or invalid
80 const isEmptyData =
81 !analyticsData ||
82 (analyticsData.activeUsersNow === 0 &&
83 analyticsData.total30DayUsers === 0 &&
84 (!analyticsData.historicalData || analyticsData.historicalData.length === 0))
85
86 setData(isEmptyData ? fallbackData : analyticsData)
87 setError(null)
88 } catch (err) {
89 console.error('Error loading analytics:', err)
90 setData(fallbackData) // Use fallback data on error
91 setError(err instanceof Error ? err.message : 'Failed to fetch analytics')
92 } finally {
93 setLoading(false)
94 }
95 }
96
97 loadData()
98 // const interval = setInterval(loadData, 120000)
99 const interval = setInterval(loadData, 8000)
100 return () => clearInterval(interval)
101 }, [])
102
103 const Card = ({
104 title,
105 value,
106 style = {},
107 }: {
108 title: string
109 value: number
110 style?: React.CSSProperties
111 }) => {
112 const [hovered, setHovered] = useState(false)
113
114 return (
115 <div
116 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 }
138
139 // Determine which data to use (fallback if no data available)
140 const displayData = data || fallbackData
141 const chartData = displayData.historicalData || []
142
143 return (
144 <div style={{ margin: '2rem 0', padding: '0 1rem' }}>
145 <h2 style={{ fontSize: '1.8rem', marginBottom: '1rem' }}>πŸ“Š Google Analytics Overview</h2>
146
147 {loading && <div>Loading analytics data...</div>}
148 {error && (
149 <div
150 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 )}
161
162 <div
163 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>
175
176 <div
177 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>
185
186 {chartData.length > 0 ? (
187 <div style={{ width: '100%', height: '400px' }}>
188 <Line
189 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}
242
243export default AnalyticsDashboard
244

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

1
2import 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 <Script
9 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'],
Insurance, Β CX, Β Service design

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