update viewers page
This commit is contained in:
+144
-63
@@ -2011,6 +2011,11 @@ const shortDateFormat = new Intl.DateTimeFormat('en-GB', {
|
||||
month: 'short',
|
||||
})
|
||||
|
||||
const shortTimeFormat = new Intl.DateTimeFormat('en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
const countryNames = {
|
||||
AR: 'Argentina',
|
||||
AU: 'Australia',
|
||||
@@ -2054,7 +2059,35 @@ function filledLast30Days(rows) {
|
||||
})
|
||||
}
|
||||
|
||||
function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke = '#e82517' }) {
|
||||
function filledLast24Hours(rows) {
|
||||
const byDate = new Map(rows.map((row) => [row.date, row]))
|
||||
return Array.from({ length: 24 }, (_, index) => {
|
||||
const date = new Date()
|
||||
date.setMinutes(0, 0, 0)
|
||||
date.setHours(date.getHours() - (23 - index))
|
||||
const key = date.toISOString().slice(0, 13) + ':00:00.000Z'
|
||||
return byDate.get(key) || {
|
||||
date: key,
|
||||
events: 0,
|
||||
visitors: 0,
|
||||
page_views: 0,
|
||||
clients: 0,
|
||||
locations: 0,
|
||||
client_labels: [],
|
||||
location_labels: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function MiniLineChart({
|
||||
accent = 'text-fury-cyan',
|
||||
bucketLabel = 'Daily count',
|
||||
data,
|
||||
label,
|
||||
metric,
|
||||
pointFormat = shortDateFormat,
|
||||
stroke = '#e82517',
|
||||
}) {
|
||||
const [hoveredPoint, setHoveredPoint] = useState(null)
|
||||
const width = 320
|
||||
const height = 112
|
||||
@@ -2076,7 +2109,7 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">{label}</p>
|
||||
<p className="mt-1 text-xs text-text-soft">Daily count</p>
|
||||
<p className="mt-1 text-xs text-text-soft">{bucketLabel}</p>
|
||||
</div>
|
||||
<p className={`text-sm font-semibold ${accent}`}>{formatNumber(latest)}</p>
|
||||
</div>
|
||||
@@ -2118,7 +2151,7 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke
|
||||
transform: 'translate(-50%, -115%)',
|
||||
}}
|
||||
>
|
||||
{shortDateFormat.format(new Date(hoveredPoint.date))}: {formatNumber(hoveredPoint.value)} {label.toLowerCase()}
|
||||
{pointFormat.format(new Date(hoveredPoint.date))}: {formatNumber(hoveredPoint.value)} {label.toLowerCase()}
|
||||
<span className="block font-normal text-bg/80">
|
||||
{formatNumber(hoveredPoint.visitors || 0)} visitors
|
||||
</span>
|
||||
@@ -2144,6 +2177,87 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke
|
||||
)
|
||||
}
|
||||
|
||||
function AnalyticsPeriodSection({
|
||||
activity,
|
||||
bucketLabel,
|
||||
description,
|
||||
pointFormat,
|
||||
title,
|
||||
totals,
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-fury-white p-5 shadow-sm">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
<p className="mt-1 text-sm text-text-soft">{description}</p>
|
||||
</div>
|
||||
<div className="grid gap-3 text-sm sm:grid-cols-4">
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.visitors)} visitors
|
||||
</span>
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.sessions)} sessions
|
||||
</span>
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.pageViews)} page views
|
||||
</span>
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.events)} events
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-2 xl:grid-cols-5">
|
||||
<MiniLineChart
|
||||
bucketLabel={bucketLabel}
|
||||
data={activity}
|
||||
label="Events"
|
||||
metric="events"
|
||||
pointFormat={pointFormat}
|
||||
stroke="#e82517"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-fury-violet"
|
||||
bucketLabel={bucketLabel}
|
||||
data={activity}
|
||||
label="Visitors"
|
||||
metric="visitors"
|
||||
pointFormat={pointFormat}
|
||||
stroke="#fb7b04"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-fury-cyan"
|
||||
bucketLabel={bucketLabel}
|
||||
data={activity}
|
||||
label="Page views"
|
||||
metric="page_views"
|
||||
pointFormat={pointFormat}
|
||||
stroke="#ed5145"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-text"
|
||||
bucketLabel={bucketLabel}
|
||||
data={activity}
|
||||
label="Clients"
|
||||
metric="clients"
|
||||
pointFormat={pointFormat}
|
||||
stroke="#000000"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-fury-cyan"
|
||||
bucketLabel={bucketLabel}
|
||||
data={activity}
|
||||
label="Locations"
|
||||
metric="locations"
|
||||
pointFormat={pointFormat}
|
||||
stroke="#009ccc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ViewersPage({ viewers }) {
|
||||
const data = viewers.data || {}
|
||||
const active = data.active || []
|
||||
@@ -2151,6 +2265,7 @@ function ViewersPage({ viewers }) {
|
||||
const topPages = data.top_pages || []
|
||||
const clients = data.clients || []
|
||||
const clients30d = data.clients_30d || []
|
||||
const activity24h = filledLast24Hours(data.activity_24h || [])
|
||||
const activity30d = filledLast30Days(data.activity_30d || [])
|
||||
const countries = data.countries || []
|
||||
const locations = data.locations || []
|
||||
@@ -2184,67 +2299,33 @@ function ViewersPage({ viewers }) {
|
||||
<Stat label="Events 24h" value={formatNumber(totals.events_24h)} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border bg-fury-white p-5 shadow-sm">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">Last 30 days</h2>
|
||||
<p className="mt-1 text-sm text-text-soft">
|
||||
Retained consented analytics over the current privacy window
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3 text-sm sm:grid-cols-4">
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.visitors_30d)} visitors
|
||||
</span>
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.sessions_30d)} sessions
|
||||
</span>
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.page_views_30d)} page views
|
||||
</span>
|
||||
<span className="rounded-md bg-surface px-3 py-2 font-semibold">
|
||||
{formatNumber(totals.events_30d)} events
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<AnalyticsPeriodSection
|
||||
activity={activity24h}
|
||||
bucketLabel="Hourly count"
|
||||
description="Consented analytics grouped by hour"
|
||||
pointFormat={shortTimeFormat}
|
||||
title="Last 24 hours"
|
||||
totals={{
|
||||
events: totals.events_24h,
|
||||
visitors: totals.visitors_24h,
|
||||
sessions: totals.sessions_24h,
|
||||
pageViews: totals.page_views_24h,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-2 xl:grid-cols-5">
|
||||
<MiniLineChart
|
||||
data={activity30d}
|
||||
label="Events"
|
||||
metric="events"
|
||||
stroke="#e82517"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-fury-violet"
|
||||
data={activity30d}
|
||||
label="Visitors"
|
||||
metric="visitors"
|
||||
stroke="#fb7b04"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-fury-cyan"
|
||||
data={activity30d}
|
||||
label="Page views"
|
||||
metric="page_views"
|
||||
stroke="#ed5145"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-text"
|
||||
data={activity30d}
|
||||
label="Clients"
|
||||
metric="clients"
|
||||
stroke="#000000"
|
||||
/>
|
||||
<MiniLineChart
|
||||
accent="text-fury-cyan"
|
||||
data={activity30d}
|
||||
label="Locations"
|
||||
metric="locations"
|
||||
stroke="#009ccc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AnalyticsPeriodSection
|
||||
activity={activity30d}
|
||||
bucketLabel="Daily count"
|
||||
description="Retained consented analytics over the current privacy window"
|
||||
pointFormat={shortDateFormat}
|
||||
title="Last 30 days"
|
||||
totals={{
|
||||
events: totals.events_30d,
|
||||
visitors: totals.visitors_30d,
|
||||
sessions: totals.sessions_30d,
|
||||
pageViews: totals.page_views_30d,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">
|
||||
<div className="border-b border-surface px-5 py-4">
|
||||
|
||||
Reference in New Issue
Block a user