update viewers page
This commit is contained in:
+60
-1
@@ -777,6 +777,65 @@ function viewerDashboard() {
|
|||||||
order by date asc
|
order by date asc
|
||||||
`).all(thirtyDaysSince)
|
`).all(thirtyDaysSince)
|
||||||
|
|
||||||
|
const activityLocationRows = db.prepare(`
|
||||||
|
select
|
||||||
|
date(occurred_at) as date,
|
||||||
|
country,
|
||||||
|
city,
|
||||||
|
region,
|
||||||
|
timezone,
|
||||||
|
count(distinct visitor_id) as visitors
|
||||||
|
from viewer_events
|
||||||
|
where occurred_at >= ?
|
||||||
|
and (
|
||||||
|
country != ''
|
||||||
|
or city != ''
|
||||||
|
or region != ''
|
||||||
|
or (timezone != '' and timezone != 'Not shared')
|
||||||
|
)
|
||||||
|
group by date(occurred_at), country, city, region, timezone
|
||||||
|
order by date asc, visitors desc
|
||||||
|
`).all(thirtyDaysSince)
|
||||||
|
|
||||||
|
const locationsByDate = new Map()
|
||||||
|
for (const row of activityLocationRows) {
|
||||||
|
const label = [row.city, row.region, row.country || row.timezone]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ')
|
||||||
|
if (!label) continue
|
||||||
|
const current = locationsByDate.get(row.date) || []
|
||||||
|
if (current.length < 4) current.push({ label, visitors: row.visitors || 0 })
|
||||||
|
locationsByDate.set(row.date, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityClientRows = db.prepare(`
|
||||||
|
select
|
||||||
|
date(occurred_at) as date,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
count(distinct visitor_id) as visitors,
|
||||||
|
count(*) as events
|
||||||
|
from viewer_events
|
||||||
|
where occurred_at >= ?
|
||||||
|
group by date(occurred_at), browser, os, device
|
||||||
|
order by date asc, visitors desc, events desc
|
||||||
|
`).all(thirtyDaysSince)
|
||||||
|
|
||||||
|
const clientsByDate = new Map()
|
||||||
|
for (const row of activityClientRows) {
|
||||||
|
const label = `${row.browser} on ${row.os}${row.device ? ` (${row.device})` : ''}`
|
||||||
|
const current = clientsByDate.get(row.date) || []
|
||||||
|
if (current.length < 4) current.push({ label, visitors: row.visitors || 0, events: row.events || 0 })
|
||||||
|
clientsByDate.set(row.date, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
const activityWithLocations = activity30d.map((row) => ({
|
||||||
|
...row,
|
||||||
|
client_labels: clientsByDate.get(row.date) || [],
|
||||||
|
location_labels: locationsByDate.get(row.date) || [],
|
||||||
|
}))
|
||||||
|
|
||||||
const countries = db.prepare(`
|
const countries = db.prepare(`
|
||||||
select
|
select
|
||||||
country,
|
country,
|
||||||
@@ -841,7 +900,7 @@ function viewerDashboard() {
|
|||||||
top_pages: topPages,
|
top_pages: topPages,
|
||||||
clients,
|
clients,
|
||||||
clients_30d: clients30d,
|
clients_30d: clients30d,
|
||||||
activity_30d: activity30d,
|
activity_30d: activityWithLocations,
|
||||||
countries,
|
countries,
|
||||||
locations,
|
locations,
|
||||||
totals: {
|
totals: {
|
||||||
|
|||||||
+25
-1
@@ -2060,6 +2060,7 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke
|
|||||||
const height = 112
|
const height = 112
|
||||||
const padding = 12
|
const padding = 12
|
||||||
const maxValue = Math.max(1, ...data.map((item) => Number(item[metric] || 0)))
|
const maxValue = Math.max(1, ...data.map((item) => Number(item[metric] || 0)))
|
||||||
|
const midValue = Math.round(maxValue / 2)
|
||||||
const points = data.map((item, index) => {
|
const points = data.map((item, index) => {
|
||||||
const x = padding + (index / Math.max(1, data.length - 1)) * (width - padding * 2)
|
const x = padding + (index / Math.max(1, data.length - 1)) * (width - padding * 2)
|
||||||
const y = height - padding - (Number(item[metric] || 0) / maxValue) * (height - padding * 2)
|
const y = height - padding - (Number(item[metric] || 0) / maxValue) * (height - padding * 2)
|
||||||
@@ -2080,8 +2081,16 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke
|
|||||||
<p className={`text-sm font-semibold ${accent}`}>{formatNumber(latest)}</p>
|
<p className={`text-sm font-semibold ${accent}`}>{formatNumber(latest)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg className="mt-3 h-28 w-full overflow-visible" viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${label} line chart`}>
|
<div className="relative mt-3">
|
||||||
|
<div className="absolute left-0 top-0 flex h-28 w-8 flex-col justify-between text-[10px] font-semibold text-text-muted">
|
||||||
|
<span>{formatNumber(maxValue)}</span>
|
||||||
|
<span>{formatNumber(midValue)}</span>
|
||||||
|
<span>0</span>
|
||||||
|
</div>
|
||||||
|
<svg className="ml-8 h-28 w-[calc(100%-2rem)] overflow-visible" viewBox={`0 0 ${width} ${height}`} role="img" aria-label={`${label} line chart`}>
|
||||||
<path d={`M ${padding} ${height - padding} H ${width - padding}`} fill="none" stroke="#fee5cd" strokeWidth="1" />
|
<path d={`M ${padding} ${height - padding} H ${width - padding}`} fill="none" stroke="#fee5cd" strokeWidth="1" />
|
||||||
|
<path d={`M ${padding} ${padding} H ${width - padding}`} fill="none" stroke="#fee5cd" strokeDasharray="4 5" strokeWidth="1" />
|
||||||
|
<path d={`M ${padding} ${height / 2} H ${width - padding}`} fill="none" stroke="#fee5cd" strokeDasharray="4 5" strokeWidth="1" />
|
||||||
<path d={pathData} fill="none" stroke={stroke} strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" />
|
<path d={pathData} fill="none" stroke={stroke} strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" />
|
||||||
{points.map((point) => (
|
{points.map((point) => (
|
||||||
<circle
|
<circle
|
||||||
@@ -2098,6 +2107,7 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
{hoveredPoint ? (
|
{hoveredPoint ? (
|
||||||
<div
|
<div
|
||||||
@@ -2112,6 +2122,20 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke
|
|||||||
<span className="block font-normal text-bg/80">
|
<span className="block font-normal text-bg/80">
|
||||||
{formatNumber(hoveredPoint.visitors || 0)} visitors
|
{formatNumber(hoveredPoint.visitors || 0)} visitors
|
||||||
</span>
|
</span>
|
||||||
|
{metric === 'clients' && hoveredPoint.client_labels?.length ? (
|
||||||
|
<span className="mt-1 block max-w-56 whitespace-normal font-normal text-bg/80">
|
||||||
|
{hoveredPoint.client_labels
|
||||||
|
.map((client) => `${client.label}: ${formatNumber(client.visitors)} visitors`)
|
||||||
|
.join(', ')}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{metric === 'locations' && hoveredPoint.location_labels?.length ? (
|
||||||
|
<span className="mt-1 block max-w-48 whitespace-normal font-normal text-bg/80">
|
||||||
|
{hoveredPoint.location_labels
|
||||||
|
.map((location) => `${location.label} (${formatNumber(location.visitors)})`)
|
||||||
|
.join(', ')}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user