@stacksjs/ts-analytics 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,1517 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{ siteName }} - Analytics</title>
|
|
7
|
+
<script>
|
|
8
|
+
// Configuration - auto-detect from current URL or use overrides
|
|
9
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
10
|
+
const API_ENDPOINT = window.ANALYTICS_API_ENDPOINT || urlParams.get('api') || window.location.origin
|
|
11
|
+
const SITE_ID = urlParams.get('siteId') || window.ANALYTICS_SITE_ID || ''
|
|
12
|
+
const USE_STEALTH = urlParams.get('stealth') !== 'false'
|
|
13
|
+
|
|
14
|
+
// Stealth API path mapper - bypass content blockers
|
|
15
|
+
const STEALTH_MAP = {
|
|
16
|
+
sites: 'projects', stats: 'summary', realtime: 'pulse', pages: 'content',
|
|
17
|
+
referrers: 'sources', devices: 'clients', browsers: 'agents', countries: 'geo',
|
|
18
|
+
timeseries: 'series', events: 'actions', campaigns: 'promo', comparison: 'diff',
|
|
19
|
+
sessions: 'visits', flow: 'journey', live: 'now', heatmap: 'touch', errors: 'issues',
|
|
20
|
+
vitals: 'metrics', goals: 'targets', funnels: 'pipelines', annotations: 'notes'
|
|
21
|
+
}
|
|
22
|
+
function apiPath(path) {
|
|
23
|
+
if (!USE_STEALTH) return path
|
|
24
|
+
let result = path.replace('/api/sites/', '/api/p/').replace('/api/sites', '/api/projects')
|
|
25
|
+
Object.keys(STEALTH_MAP).sort((a, b) => b.length - a.length).forEach(k => {
|
|
26
|
+
result = result.replace(new RegExp(`/${k}(?=/|\\?|$)`, 'g'), `/${STEALTH_MAP[k]}`)
|
|
27
|
+
})
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
window.apiPath = apiPath
|
|
31
|
+
|
|
32
|
+
// Sites list
|
|
33
|
+
let availableSites = []
|
|
34
|
+
let sitesLoading = true
|
|
35
|
+
let sitesError = null
|
|
36
|
+
|
|
37
|
+
// Dashboard state
|
|
38
|
+
let siteName = "Analytics Dashboard"
|
|
39
|
+
let siteId = SITE_ID
|
|
40
|
+
let dateRange = "Last 30 days"
|
|
41
|
+
let activeView = 'dashboard' // 'dashboard' or 'heatmap'
|
|
42
|
+
|
|
43
|
+
// Heatmap state
|
|
44
|
+
let heatmapPages = []
|
|
45
|
+
let selectedHeatmapPage = ''
|
|
46
|
+
let heatmapViewMode = 'click' // 'click' or 'scroll'
|
|
47
|
+
let heatmapClicks = []
|
|
48
|
+
let heatmapTopElements = []
|
|
49
|
+
let heatmapTotalClicks = 0
|
|
50
|
+
let heatmapScrollData = []
|
|
51
|
+
let heatmapAvgScrollDepth = 0
|
|
52
|
+
let heatmapTotalSessions = 0
|
|
53
|
+
let heatmapLoading = false
|
|
54
|
+
|
|
55
|
+
// Summary stats
|
|
56
|
+
let stats = {
|
|
57
|
+
realtime: 0,
|
|
58
|
+
people: 0,
|
|
59
|
+
views: 0,
|
|
60
|
+
avgTime: "00:00",
|
|
61
|
+
bounceRate: 0,
|
|
62
|
+
events: 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Table data
|
|
66
|
+
let pages = []
|
|
67
|
+
let referrers = []
|
|
68
|
+
let deviceTypes = []
|
|
69
|
+
let operatingSystems = []
|
|
70
|
+
let browsers = []
|
|
71
|
+
let countries = []
|
|
72
|
+
let campaigns = []
|
|
73
|
+
let events = []
|
|
74
|
+
let timeSeriesData = []
|
|
75
|
+
|
|
76
|
+
// Fetch available sites
|
|
77
|
+
async function fetchAvailableSites() {
|
|
78
|
+
sitesLoading = true
|
|
79
|
+
sitesError = null
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(apiPath(`${API_ENDPOINT}/api/sites`))
|
|
82
|
+
if (!res.ok) throw new Error('Failed to fetch sites')
|
|
83
|
+
const data = await res.json()
|
|
84
|
+
availableSites = data.sites || []
|
|
85
|
+
sitesLoading = false
|
|
86
|
+
renderSiteSelector()
|
|
87
|
+
} catch (err) {
|
|
88
|
+
sitesError = err.message || 'Failed to load sites'
|
|
89
|
+
sitesLoading = false
|
|
90
|
+
renderSiteSelector()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderSiteSelector() {
|
|
95
|
+
const container = document.getElementById('site-selector-container')
|
|
96
|
+
if (!container) return
|
|
97
|
+
|
|
98
|
+
if (sitesLoading) {
|
|
99
|
+
container.innerHTML = `
|
|
100
|
+
<div class="site-selector-loading">
|
|
101
|
+
<div class="spinner"></div>
|
|
102
|
+
<p>Loading sites...</p>
|
|
103
|
+
</div>
|
|
104
|
+
`
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (sitesError) {
|
|
109
|
+
container.innerHTML = `
|
|
110
|
+
<div class="site-selector-error">
|
|
111
|
+
<svg width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
112
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
113
|
+
</svg>
|
|
114
|
+
<p>${sitesError}</p>
|
|
115
|
+
<button onclick="fetchAvailableSites()">Try Again</button>
|
|
116
|
+
</div>
|
|
117
|
+
`
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (availableSites.length === 0) {
|
|
122
|
+
container.innerHTML = `
|
|
123
|
+
<div class="site-selector-empty">
|
|
124
|
+
<svg width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
125
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
|
126
|
+
</svg>
|
|
127
|
+
<h2>No Sites Found</h2>
|
|
128
|
+
<p>Create a site to start tracking analytics.</p>
|
|
129
|
+
</div>
|
|
130
|
+
`
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
container.innerHTML = `
|
|
135
|
+
<div class="site-selector-grid">
|
|
136
|
+
${availableSites.map(site => `
|
|
137
|
+
<button class="site-card" onclick="selectSite('${site.id}', '${site.name.replace(/'/g, "\\'")}')">
|
|
138
|
+
<div class="site-card-icon">
|
|
139
|
+
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
140
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
|
|
141
|
+
</svg>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="site-card-info">
|
|
144
|
+
<span class="site-card-name">${site.name}</span>
|
|
145
|
+
<span class="site-card-domain">${site.domains?.[0] || site.id}</span>
|
|
146
|
+
</div>
|
|
147
|
+
<svg class="site-card-arrow" width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
148
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
|
149
|
+
</svg>
|
|
150
|
+
</button>
|
|
151
|
+
`).join('')}
|
|
152
|
+
</div>
|
|
153
|
+
`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function selectSite(id, name) {
|
|
157
|
+
siteId = id
|
|
158
|
+
siteName = name
|
|
159
|
+
document.getElementById('site-selector-screen').style.display = 'none'
|
|
160
|
+
document.getElementById('dashboard-content').style.display = 'block'
|
|
161
|
+
// Update URL without reload
|
|
162
|
+
const url = new URL(window.location.href)
|
|
163
|
+
url.searchParams.set('siteId', id)
|
|
164
|
+
window.history.pushState({}, '', url)
|
|
165
|
+
// Update site name in header
|
|
166
|
+
document.querySelector('.site-selector select').innerHTML = `<option>${name}</option>`
|
|
167
|
+
// Fetch dashboard data
|
|
168
|
+
fetchDashboardData()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Fetch data from API
|
|
172
|
+
async function fetchDashboardData() {
|
|
173
|
+
if (!siteId) {
|
|
174
|
+
console.error('No siteId configured. Add ?siteId=YOUR_SITE_ID to the URL.')
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const baseUrl = `${API_ENDPOINT}/api/sites/${siteId}`
|
|
179
|
+
const now = new Date()
|
|
180
|
+
const thirtyDaysAgo = new Date(now)
|
|
181
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
|
182
|
+
const params = `?startDate=${thirtyDaysAgo.toISOString()}&endDate=${now.toISOString()}`
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// Fetch all data in parallel - using stealth API paths
|
|
186
|
+
const [statsRes, realtimeRes, pagesRes, referrersRes, devicesRes, browsersRes, countriesRes, timeseriesRes, eventsRes, campaignsRes] = await Promise.all([
|
|
187
|
+
fetch(apiPath(`${baseUrl}/stats${params}`)).then(r => r.json()).catch(() => ({})),
|
|
188
|
+
fetch(apiPath(`${baseUrl}/realtime`)).then(r => r.json()).catch(() => ({ currentVisitors: 0 })),
|
|
189
|
+
fetch(apiPath(`${baseUrl}/pages${params}`)).then(r => r.json()).catch(() => ({ pages: [] })),
|
|
190
|
+
fetch(apiPath(`${baseUrl}/referrers${params}`)).then(r => r.json()).catch(() => ({ referrers: [] })),
|
|
191
|
+
fetch(apiPath(`${baseUrl}/devices${params}`)).then(r => r.json()).catch(() => ({ deviceTypes: [], operatingSystems: [] })),
|
|
192
|
+
fetch(apiPath(`${baseUrl}/browsers${params}`)).then(r => r.json()).catch(() => ({ browsers: [] })),
|
|
193
|
+
fetch(apiPath(`${baseUrl}/countries${params}`)).then(r => r.json()).catch(() => ({ countries: [] })),
|
|
194
|
+
fetch(apiPath(`${baseUrl}/timeseries${params}`)).then(r => r.json()).catch(() => ({ timeSeries: [] })),
|
|
195
|
+
fetch(apiPath(`${baseUrl}/events${params}`)).then(r => r.json()).catch(() => ({ events: [] })),
|
|
196
|
+
fetch(apiPath(`${baseUrl}/campaigns${params}`)).then(r => r.json()).catch(() => ({ campaigns: [] })),
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
// Update stats
|
|
200
|
+
stats = {
|
|
201
|
+
realtime: realtimeRes.currentVisitors || 0,
|
|
202
|
+
people: statsRes.people || 0,
|
|
203
|
+
views: statsRes.views || 0,
|
|
204
|
+
avgTime: statsRes.avgTime || "00:00",
|
|
205
|
+
bounceRate: statsRes.bounceRate || 0,
|
|
206
|
+
events: statsRes.events || 0
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Update tables
|
|
210
|
+
pages = pagesRes.pages || []
|
|
211
|
+
referrers = referrersRes.referrers || []
|
|
212
|
+
deviceTypes = devicesRes.deviceTypes || []
|
|
213
|
+
operatingSystems = devicesRes.operatingSystems || []
|
|
214
|
+
browsers = browsersRes.browsers || []
|
|
215
|
+
countries = countriesRes.countries || []
|
|
216
|
+
campaigns = campaignsRes.campaigns || []
|
|
217
|
+
events = eventsRes.events || []
|
|
218
|
+
timeSeriesData = timeseriesRes.timeSeries || []
|
|
219
|
+
|
|
220
|
+
// Re-render the dashboard
|
|
221
|
+
renderDashboard()
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('Failed to fetch dashboard data:', error)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatNumber(num) {
|
|
228
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
|
|
229
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'k'
|
|
230
|
+
return String(num)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Heatmap functions
|
|
234
|
+
async function fetchHeatmapPages() {
|
|
235
|
+
if (!siteId) return
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(apiPath(`${API_ENDPOINT}/api/sites/${siteId}/heatmap/pages`))
|
|
238
|
+
const data = await res.json()
|
|
239
|
+
heatmapPages = data.pages || []
|
|
240
|
+
renderHeatmapPageSelector()
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error('Failed to fetch heatmap pages:', err)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function fetchHeatmapData() {
|
|
247
|
+
if (!siteId || !selectedHeatmapPage) return
|
|
248
|
+
heatmapLoading = true
|
|
249
|
+
renderHeatmapLoading()
|
|
250
|
+
|
|
251
|
+
const now = new Date()
|
|
252
|
+
const thirtyDaysAgo = new Date(now)
|
|
253
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
|
254
|
+
const params = `path=${encodeURIComponent(selectedHeatmapPage)}&startDate=${thirtyDaysAgo.toISOString()}&endDate=${now.toISOString()}`
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
if (heatmapViewMode === 'click') {
|
|
258
|
+
const res = await fetch(apiPath(`${API_ENDPOINT}/api/sites/${siteId}/heatmap/clicks?${params}`))
|
|
259
|
+
const data = await res.json()
|
|
260
|
+
heatmapClicks = data.clicks || []
|
|
261
|
+
heatmapTopElements = data.topElements || []
|
|
262
|
+
heatmapTotalClicks = data.totalClicks || 0
|
|
263
|
+
} else {
|
|
264
|
+
const res = await fetch(apiPath(`${API_ENDPOINT}/api/sites/${siteId}/heatmap/scroll?${params}`))
|
|
265
|
+
const data = await res.json()
|
|
266
|
+
heatmapScrollData = data.scrollDepths || []
|
|
267
|
+
heatmapAvgScrollDepth = data.avgMaxDepth || 0
|
|
268
|
+
heatmapTotalSessions = data.totalSessions || 0
|
|
269
|
+
}
|
|
270
|
+
renderHeatmap()
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error('Failed to fetch heatmap data:', err)
|
|
273
|
+
} finally {
|
|
274
|
+
heatmapLoading = false
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function renderHeatmapPageSelector() {
|
|
279
|
+
const selector = document.getElementById('heatmap-page-select')
|
|
280
|
+
if (!selector) return
|
|
281
|
+
selector.innerHTML = '<option value="">Select a page...</option>' +
|
|
282
|
+
heatmapPages.map(p => `<option value="${p.path}">${p.path} (${p.clickCount || 0} clicks)</option>`).join('')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function renderHeatmapLoading() {
|
|
286
|
+
const container = document.getElementById('heatmap-viz')
|
|
287
|
+
if (!container) return
|
|
288
|
+
container.innerHTML = `
|
|
289
|
+
<div class="heatmap-loading">
|
|
290
|
+
<div class="spinner"></div>
|
|
291
|
+
<span>Loading heatmap data...</span>
|
|
292
|
+
</div>
|
|
293
|
+
`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function renderHeatmap() {
|
|
297
|
+
if (heatmapViewMode === 'click') {
|
|
298
|
+
renderClickHeatmap()
|
|
299
|
+
} else {
|
|
300
|
+
renderScrollHeatmap()
|
|
301
|
+
}
|
|
302
|
+
renderHeatmapSidebar()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderClickHeatmap() {
|
|
306
|
+
const container = document.getElementById('heatmap-viz')
|
|
307
|
+
if (!container) return
|
|
308
|
+
|
|
309
|
+
if (heatmapClicks.length === 0) {
|
|
310
|
+
container.innerHTML = `
|
|
311
|
+
<div class="heatmap-empty">
|
|
312
|
+
<svg width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
313
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"/>
|
|
314
|
+
</svg>
|
|
315
|
+
<p>No click data for this page yet</p>
|
|
316
|
+
</div>
|
|
317
|
+
`
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Create canvas-based heatmap
|
|
322
|
+
const width = container.offsetWidth
|
|
323
|
+
const height = 500
|
|
324
|
+
const maxCount = Math.max(...heatmapClicks.map(c => c.count), 1)
|
|
325
|
+
|
|
326
|
+
container.innerHTML = `
|
|
327
|
+
<div class="heatmap-canvas-container" style="position:relative;width:100%;height:${height}px;background:#1a1f2e;border-radius:8px;overflow:hidden;">
|
|
328
|
+
<canvas id="heatmap-canvas" width="${width}" height="${height}"></canvas>
|
|
329
|
+
<div class="heatmap-stats">
|
|
330
|
+
<span>${heatmapTotalClicks.toLocaleString()} total clicks</span>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
`
|
|
334
|
+
|
|
335
|
+
const canvas = document.getElementById('heatmap-canvas')
|
|
336
|
+
const ctx = canvas.getContext('2d')
|
|
337
|
+
|
|
338
|
+
// Draw click points as heat
|
|
339
|
+
for (const click of heatmapClicks) {
|
|
340
|
+
const intensity = click.count / maxCount
|
|
341
|
+
const x = (click.x / 100) * width
|
|
342
|
+
const y = (click.y / 100) * height
|
|
343
|
+
drawHeatPoint(ctx, x, y, intensity, 30)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Colorize
|
|
347
|
+
colorizeHeatmap(ctx, width, height)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function drawHeatPoint(ctx, x, y, intensity, radius) {
|
|
351
|
+
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius)
|
|
352
|
+
gradient.addColorStop(0, `rgba(255, 255, 255, ${intensity * 0.6})`)
|
|
353
|
+
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)')
|
|
354
|
+
ctx.fillStyle = gradient
|
|
355
|
+
ctx.beginPath()
|
|
356
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2)
|
|
357
|
+
ctx.fill()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function colorizeHeatmap(ctx, width, height) {
|
|
361
|
+
const imageData = ctx.getImageData(0, 0, width, height)
|
|
362
|
+
const data = imageData.data
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
365
|
+
const alpha = data[i + 3]
|
|
366
|
+
if (alpha > 0) {
|
|
367
|
+
const ratio = alpha / 255
|
|
368
|
+
// Blue -> Green -> Yellow -> Red gradient
|
|
369
|
+
if (ratio < 0.25) {
|
|
370
|
+
data[i] = 0
|
|
371
|
+
data[i + 1] = Math.floor(ratio * 4 * 255)
|
|
372
|
+
data[i + 2] = 255
|
|
373
|
+
} else if (ratio < 0.5) {
|
|
374
|
+
data[i] = 0
|
|
375
|
+
data[i + 1] = 255
|
|
376
|
+
data[i + 2] = Math.floor((1 - (ratio - 0.25) * 4) * 255)
|
|
377
|
+
} else if (ratio < 0.75) {
|
|
378
|
+
data[i] = Math.floor((ratio - 0.5) * 4 * 255)
|
|
379
|
+
data[i + 1] = 255
|
|
380
|
+
data[i + 2] = 0
|
|
381
|
+
} else {
|
|
382
|
+
data[i] = 255
|
|
383
|
+
data[i + 1] = Math.floor((1 - (ratio - 0.75) * 4) * 255)
|
|
384
|
+
data[i + 2] = 0
|
|
385
|
+
}
|
|
386
|
+
data[i + 3] = Math.min(255, alpha * 2)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
ctx.putImageData(imageData, 0, 0)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function renderScrollHeatmap() {
|
|
394
|
+
const container = document.getElementById('heatmap-viz')
|
|
395
|
+
if (!container) return
|
|
396
|
+
|
|
397
|
+
if (heatmapScrollData.length === 0) {
|
|
398
|
+
container.innerHTML = `
|
|
399
|
+
<div class="heatmap-empty">
|
|
400
|
+
<svg width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
401
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
|
|
402
|
+
</svg>
|
|
403
|
+
<p>No scroll data for this page yet</p>
|
|
404
|
+
</div>
|
|
405
|
+
`
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const getColor = (pct) => {
|
|
410
|
+
if (pct >= 80) return '#10b981'
|
|
411
|
+
if (pct >= 60) return '#34d399'
|
|
412
|
+
if (pct >= 40) return '#fbbf24'
|
|
413
|
+
if (pct >= 20) return '#f97316'
|
|
414
|
+
return '#ef4444'
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Create depth buckets 0-100 in 10% increments
|
|
418
|
+
const buckets = []
|
|
419
|
+
for (let d = 0; d <= 90; d += 10) {
|
|
420
|
+
const dataPoint = heatmapScrollData.find(s => s.depth === d)
|
|
421
|
+
buckets.push({
|
|
422
|
+
depth: d,
|
|
423
|
+
reachPercentage: dataPoint?.reachPercentage ?? (d === 0 ? 100 : 0),
|
|
424
|
+
avgTimeMs: dataPoint?.avgTimeMs ?? 0
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
container.innerHTML = `
|
|
429
|
+
<div class="scroll-heatmap">
|
|
430
|
+
<div class="scroll-stats">
|
|
431
|
+
<div class="scroll-stat">
|
|
432
|
+
<span class="scroll-stat-value">${Math.round(heatmapAvgScrollDepth)}%</span>
|
|
433
|
+
<span class="scroll-stat-label">Avg. Scroll Depth</span>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="scroll-stat">
|
|
436
|
+
<span class="scroll-stat-value">${heatmapTotalSessions.toLocaleString()}</span>
|
|
437
|
+
<span class="scroll-stat-label">Total Sessions</span>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="scroll-bars">
|
|
441
|
+
${buckets.map(b => `
|
|
442
|
+
<div class="scroll-bar-row">
|
|
443
|
+
<span class="scroll-depth-label">${b.depth}%</span>
|
|
444
|
+
<div class="scroll-bar-track">
|
|
445
|
+
<div class="scroll-bar-fill" style="width:${b.reachPercentage}%;background:${getColor(b.reachPercentage)}"></div>
|
|
446
|
+
</div>
|
|
447
|
+
<span class="scroll-reach-label">${b.reachPercentage}%</span>
|
|
448
|
+
</div>
|
|
449
|
+
`).join('')}
|
|
450
|
+
</div>
|
|
451
|
+
<div class="scroll-legend">
|
|
452
|
+
<span><span class="legend-dot" style="background:#10b981"></span>80%+ reach</span>
|
|
453
|
+
<span><span class="legend-dot" style="background:#fbbf24"></span>40-60% reach</span>
|
|
454
|
+
<span><span class="legend-dot" style="background:#ef4444"></span><20% reach</span>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
`
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function renderHeatmapSidebar() {
|
|
461
|
+
const sidebar = document.getElementById('heatmap-sidebar')
|
|
462
|
+
if (!sidebar) return
|
|
463
|
+
|
|
464
|
+
if (heatmapViewMode === 'click' && heatmapTopElements.length > 0) {
|
|
465
|
+
const maxClicks = heatmapTopElements[0]?.clicks || 1
|
|
466
|
+
sidebar.innerHTML = `
|
|
467
|
+
<div class="heatmap-panel">
|
|
468
|
+
<h3>Top Clicked Elements</h3>
|
|
469
|
+
<div class="element-list">
|
|
470
|
+
${heatmapTopElements.slice(0, 15).map(el => `
|
|
471
|
+
<div class="element-item">
|
|
472
|
+
<div class="element-info">
|
|
473
|
+
<span class="element-tag"><${el.tag}></span>
|
|
474
|
+
<span class="element-selector" title="${el.selector}">${el.selector}</span>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="element-stats">
|
|
477
|
+
<div class="element-bar-track">
|
|
478
|
+
<div class="element-bar-fill" style="width:${(el.clicks / maxClicks) * 100}%"></div>
|
|
479
|
+
</div>
|
|
480
|
+
<span class="element-clicks">${el.clicks}</span>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
`).join('')}
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
`
|
|
487
|
+
} else if (heatmapViewMode === 'scroll') {
|
|
488
|
+
sidebar.innerHTML = `
|
|
489
|
+
<div class="heatmap-panel">
|
|
490
|
+
<h3>Scroll Insights</h3>
|
|
491
|
+
<div class="insight-list">
|
|
492
|
+
<div class="insight-item">
|
|
493
|
+
<span class="insight-label">Average scroll depth</span>
|
|
494
|
+
<span class="insight-value">${Math.round(heatmapAvgScrollDepth)}%</span>
|
|
495
|
+
</div>
|
|
496
|
+
<div class="insight-item">
|
|
497
|
+
<span class="insight-label">Sessions analyzed</span>
|
|
498
|
+
<span class="insight-value">${heatmapTotalSessions.toLocaleString()}</span>
|
|
499
|
+
</div>
|
|
500
|
+
<div class="insight-item">
|
|
501
|
+
<span class="insight-label">Reached 50%</span>
|
|
502
|
+
<span class="insight-value">${heatmapScrollData.find(s => s.depth === 50)?.reachPercentage || 0}%</span>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="insight-item">
|
|
505
|
+
<span class="insight-label">Reached bottom</span>
|
|
506
|
+
<span class="insight-value">${heatmapScrollData.find(s => s.depth === 90)?.reachPercentage || 0}%</span>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
`
|
|
511
|
+
} else {
|
|
512
|
+
sidebar.innerHTML = `
|
|
513
|
+
<div class="heatmap-panel">
|
|
514
|
+
<h3>No Data</h3>
|
|
515
|
+
<p class="text-muted">Select a page to view heatmap data</p>
|
|
516
|
+
</div>
|
|
517
|
+
`
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function switchView(view) {
|
|
522
|
+
activeView = view
|
|
523
|
+
document.getElementById('dashboard-view').style.display = view === 'dashboard' ? 'block' : 'none'
|
|
524
|
+
document.getElementById('heatmap-view').style.display = view === 'heatmap' ? 'block' : 'none'
|
|
525
|
+
document.querySelectorAll('.view-tab').forEach(t => t.classList.remove('active'))
|
|
526
|
+
document.querySelector(`.view-tab[data-view="${view}"]`)?.classList.add('active')
|
|
527
|
+
|
|
528
|
+
if (view === 'heatmap' && heatmapPages.length === 0) {
|
|
529
|
+
fetchHeatmapPages()
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function switchHeatmapMode(mode) {
|
|
534
|
+
heatmapViewMode = mode
|
|
535
|
+
document.querySelectorAll('.heatmap-mode-btn').forEach(b => b.classList.remove('active'))
|
|
536
|
+
document.querySelector(`.heatmap-mode-btn[data-mode="${mode}"]`)?.classList.add('active')
|
|
537
|
+
if (selectedHeatmapPage) {
|
|
538
|
+
fetchHeatmapData()
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function selectHeatmapPage(path) {
|
|
543
|
+
selectedHeatmapPage = path
|
|
544
|
+
if (path) {
|
|
545
|
+
fetchHeatmapData()
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderDashboard() {
|
|
550
|
+
// Update stats
|
|
551
|
+
document.getElementById('stat-realtime').textContent = formatNumber(stats.realtime)
|
|
552
|
+
document.getElementById('stat-people').textContent = formatNumber(stats.people)
|
|
553
|
+
document.getElementById('stat-views').textContent = formatNumber(stats.views)
|
|
554
|
+
document.getElementById('stat-avgtime').textContent = stats.avgTime
|
|
555
|
+
document.getElementById('stat-bounce').textContent = stats.bounceRate + '%'
|
|
556
|
+
document.getElementById('stat-events').textContent = formatNumber(stats.events)
|
|
557
|
+
document.getElementById('realtime-count').textContent = stats.realtime + ' current visitors'
|
|
558
|
+
|
|
559
|
+
// Update pages table
|
|
560
|
+
const pagesBody = document.getElementById('pages-body')
|
|
561
|
+
pagesBody.innerHTML = pages.length ? pages.map(p => `
|
|
562
|
+
<tr>
|
|
563
|
+
<td class="name" title="${p.path}">${p.path}</td>
|
|
564
|
+
<td class="value">${formatNumber(p.entries || 0)}</td>
|
|
565
|
+
<td class="value">${formatNumber(p.visitors || 0)}</td>
|
|
566
|
+
<td class="value">${formatNumber(p.views || 0)}</td>
|
|
567
|
+
</tr>
|
|
568
|
+
`).join('') : '<tr><td colspan="4" style="text-align:center;color:var(--text-muted);padding:1rem">No data</td></tr>'
|
|
569
|
+
|
|
570
|
+
// Update referrers table
|
|
571
|
+
const referrersBody = document.getElementById('referrers-body')
|
|
572
|
+
referrersBody.innerHTML = referrers.length ? referrers.map(r => `
|
|
573
|
+
<tr>
|
|
574
|
+
<td class="name" title="${r.source}">${r.source}</td>
|
|
575
|
+
<td class="value">${formatNumber(r.visitors || 0)}</td>
|
|
576
|
+
<td class="value">${formatNumber(r.views || 0)}</td>
|
|
577
|
+
</tr>
|
|
578
|
+
`).join('') : '<tr><td colspan="3" style="text-align:center;color:var(--text-muted);padding:1rem">No data</td></tr>'
|
|
579
|
+
|
|
580
|
+
// Update devices table
|
|
581
|
+
const devicesBody = document.getElementById('devices-body')
|
|
582
|
+
devicesBody.innerHTML = deviceTypes.length ? deviceTypes.map(d => `
|
|
583
|
+
<tr>
|
|
584
|
+
<td class="name">${d.type}</td>
|
|
585
|
+
<td class="value">${formatNumber(d.visitors || 0)}</td>
|
|
586
|
+
</tr>
|
|
587
|
+
`).join('') : '<tr><td colspan="2" style="text-align:center;color:var(--text-muted);padding:1rem">No data</td></tr>'
|
|
588
|
+
|
|
589
|
+
// Update browsers table
|
|
590
|
+
const browsersBody = document.getElementById('browsers-body')
|
|
591
|
+
browsersBody.innerHTML = browsers.length ? browsers.map(b => `
|
|
592
|
+
<tr>
|
|
593
|
+
<td class="name">${b.name}</td>
|
|
594
|
+
<td class="value">${formatNumber(b.visitors || 0)}</td>
|
|
595
|
+
</tr>
|
|
596
|
+
`).join('') : '<tr><td colspan="2" style="text-align:center;color:var(--text-muted);padding:1rem">No data</td></tr>'
|
|
597
|
+
|
|
598
|
+
// Update countries table
|
|
599
|
+
const countriesBody = document.getElementById('countries-body')
|
|
600
|
+
countriesBody.innerHTML = countries.length ? countries.map(c => `
|
|
601
|
+
<tr>
|
|
602
|
+
<td class="name">${c.name}</td>
|
|
603
|
+
<td class="value">${formatNumber(c.visitors || 0)}</td>
|
|
604
|
+
</tr>
|
|
605
|
+
`).join('') : '<tr><td colspan="2" style="text-align:center;color:var(--text-muted);padding:1rem">No data</td></tr>'
|
|
606
|
+
|
|
607
|
+
// Update campaigns table
|
|
608
|
+
const campaignsBody = document.getElementById('campaigns-body')
|
|
609
|
+
campaignsBody.innerHTML = campaigns.length ? campaigns.map(c => `
|
|
610
|
+
<tr>
|
|
611
|
+
<td class="name" title="${c.name}">${c.name}</td>
|
|
612
|
+
<td class="value">${formatNumber(c.visitors || 0)}</td>
|
|
613
|
+
<td class="value">${formatNumber(c.views || 0)}</td>
|
|
614
|
+
</tr>
|
|
615
|
+
`).join('') : '<tr><td colspan="3" style="text-align:center;color:var(--text-muted);padding:1rem">No data</td></tr>'
|
|
616
|
+
|
|
617
|
+
// Update events section
|
|
618
|
+
const eventsContainer = document.getElementById('events-container')
|
|
619
|
+
eventsContainer.innerHTML = events.length ? `
|
|
620
|
+
<table class="data-table">
|
|
621
|
+
<thead>
|
|
622
|
+
<tr>
|
|
623
|
+
<th>Event Name</th>
|
|
624
|
+
<th style="text-align:right">Count</th>
|
|
625
|
+
<th style="text-align:right">Unique</th>
|
|
626
|
+
<th style="text-align:right">Value</th>
|
|
627
|
+
</tr>
|
|
628
|
+
</thead>
|
|
629
|
+
<tbody>
|
|
630
|
+
${events.map(e => `
|
|
631
|
+
<tr>
|
|
632
|
+
<td class="name">${e.name}</td>
|
|
633
|
+
<td class="value">${formatNumber(e.count || 0)}</td>
|
|
634
|
+
<td class="value">${formatNumber(e.unique || 0)}</td>
|
|
635
|
+
<td class="value">${e.totalValue ? formatNumber(e.totalValue) : '-'}</td>
|
|
636
|
+
</tr>
|
|
637
|
+
`).join('')}
|
|
638
|
+
</tbody>
|
|
639
|
+
</table>
|
|
640
|
+
` : `
|
|
641
|
+
<div class="empty-state">
|
|
642
|
+
<p>You haven't created any events yet.</p>
|
|
643
|
+
<a href="#">Learn how to use events</a>
|
|
644
|
+
</div>
|
|
645
|
+
`
|
|
646
|
+
|
|
647
|
+
// Render time series chart if data available
|
|
648
|
+
renderChart()
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function renderChart() {
|
|
652
|
+
const canvas = document.getElementById('timeSeriesChart')
|
|
653
|
+
const tooltip = document.getElementById('chartTooltip')
|
|
654
|
+
if (!canvas || !timeSeriesData.length) return
|
|
655
|
+
|
|
656
|
+
const ctx = canvas.getContext('2d')
|
|
657
|
+
const rect = canvas.parentElement.getBoundingClientRect()
|
|
658
|
+
canvas.width = rect.width - 48
|
|
659
|
+
canvas.height = 220
|
|
660
|
+
|
|
661
|
+
const padding = { top: 20, right: 20, bottom: 50, left: 50 }
|
|
662
|
+
const width = canvas.width - padding.left - padding.right
|
|
663
|
+
const height = canvas.height - padding.top - padding.bottom
|
|
664
|
+
|
|
665
|
+
const maxViews = Math.max(...timeSeriesData.map(d => d.views), 1)
|
|
666
|
+
const xScale = width / (timeSeriesData.length - 1 || 1)
|
|
667
|
+
const yScale = height / maxViews
|
|
668
|
+
|
|
669
|
+
// Store point positions for hover detection
|
|
670
|
+
const points = timeSeriesData.map((d, i) => ({
|
|
671
|
+
x: padding.left + i * xScale,
|
|
672
|
+
y: padding.top + height - d.views * yScale,
|
|
673
|
+
data: d
|
|
674
|
+
}))
|
|
675
|
+
|
|
676
|
+
function draw(hoverIndex = -1) {
|
|
677
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
678
|
+
|
|
679
|
+
// Draw grid lines
|
|
680
|
+
ctx.strokeStyle = '#374151'
|
|
681
|
+
ctx.lineWidth = 1
|
|
682
|
+
for (let i = 0; i <= 4; i++) {
|
|
683
|
+
const y = padding.top + (height / 4) * i
|
|
684
|
+
ctx.beginPath()
|
|
685
|
+
ctx.moveTo(padding.left, y)
|
|
686
|
+
ctx.lineTo(padding.left + width, y)
|
|
687
|
+
ctx.stroke()
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Draw area fill
|
|
691
|
+
ctx.beginPath()
|
|
692
|
+
ctx.fillStyle = 'rgba(129, 140, 248, 0.1)'
|
|
693
|
+
points.forEach((p, i) => {
|
|
694
|
+
if (i === 0) {
|
|
695
|
+
ctx.moveTo(p.x, padding.top + height)
|
|
696
|
+
ctx.lineTo(p.x, p.y)
|
|
697
|
+
} else {
|
|
698
|
+
ctx.lineTo(p.x, p.y)
|
|
699
|
+
}
|
|
700
|
+
})
|
|
701
|
+
ctx.lineTo(points[points.length - 1].x, padding.top + height)
|
|
702
|
+
ctx.closePath()
|
|
703
|
+
ctx.fill()
|
|
704
|
+
|
|
705
|
+
// Draw line
|
|
706
|
+
ctx.beginPath()
|
|
707
|
+
ctx.strokeStyle = '#818cf8'
|
|
708
|
+
ctx.lineWidth = 2
|
|
709
|
+
points.forEach((p, i) => {
|
|
710
|
+
if (i === 0) ctx.moveTo(p.x, p.y)
|
|
711
|
+
else ctx.lineTo(p.x, p.y)
|
|
712
|
+
})
|
|
713
|
+
ctx.stroke()
|
|
714
|
+
|
|
715
|
+
// Draw data points
|
|
716
|
+
points.forEach((p, i) => {
|
|
717
|
+
ctx.beginPath()
|
|
718
|
+
ctx.fillStyle = i === hoverIndex ? '#ffffff' : '#818cf8'
|
|
719
|
+
ctx.arc(p.x, p.y, i === hoverIndex ? 6 : 3, 0, Math.PI * 2)
|
|
720
|
+
ctx.fill()
|
|
721
|
+
if (i === hoverIndex) {
|
|
722
|
+
ctx.strokeStyle = '#818cf8'
|
|
723
|
+
ctx.lineWidth = 2
|
|
724
|
+
ctx.stroke()
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// Draw Y axis labels
|
|
729
|
+
ctx.fillStyle = '#6b7280'
|
|
730
|
+
ctx.font = '11px -apple-system, sans-serif'
|
|
731
|
+
ctx.textAlign = 'right'
|
|
732
|
+
for (let i = 0; i <= 4; i++) {
|
|
733
|
+
const value = Math.round(maxViews - (maxViews / 4) * i)
|
|
734
|
+
const y = padding.top + (height / 4) * i
|
|
735
|
+
ctx.fillText(formatNumber(value), padding.left - 10, y + 4)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Draw X axis labels
|
|
739
|
+
ctx.textAlign = 'center'
|
|
740
|
+
ctx.fillStyle = '#6b7280'
|
|
741
|
+
const labelCount = Math.min(timeSeriesData.length, 7)
|
|
742
|
+
const step = Math.max(1, Math.floor((timeSeriesData.length - 1) / (labelCount - 1)))
|
|
743
|
+
for (let i = 0; i < timeSeriesData.length; i += step) {
|
|
744
|
+
const d = timeSeriesData[i]
|
|
745
|
+
const x = padding.left + i * xScale
|
|
746
|
+
const dateStr = formatChartDate(d.date)
|
|
747
|
+
ctx.fillText(dateStr, x, canvas.height - 10)
|
|
748
|
+
}
|
|
749
|
+
// Always show last label if not already shown
|
|
750
|
+
if ((timeSeriesData.length - 1) % step !== 0) {
|
|
751
|
+
const d = timeSeriesData[timeSeriesData.length - 1]
|
|
752
|
+
const x = padding.left + (timeSeriesData.length - 1) * xScale
|
|
753
|
+
ctx.fillText(formatChartDate(d.date), x, canvas.height - 10)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Draw vertical hover line
|
|
757
|
+
if (hoverIndex >= 0) {
|
|
758
|
+
ctx.beginPath()
|
|
759
|
+
ctx.strokeStyle = 'rgba(129, 140, 248, 0.5)'
|
|
760
|
+
ctx.lineWidth = 1
|
|
761
|
+
ctx.setLineDash([4, 4])
|
|
762
|
+
ctx.moveTo(points[hoverIndex].x, padding.top)
|
|
763
|
+
ctx.lineTo(points[hoverIndex].x, padding.top + height)
|
|
764
|
+
ctx.stroke()
|
|
765
|
+
ctx.setLineDash([])
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function formatChartDate(dateStr) {
|
|
770
|
+
const date = new Date(dateStr)
|
|
771
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
772
|
+
return months[date.getMonth()] + ' ' + date.getDate()
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Initial draw
|
|
776
|
+
draw()
|
|
777
|
+
|
|
778
|
+
// Hover interaction
|
|
779
|
+
canvas.onmousemove = function(e) {
|
|
780
|
+
const canvasRect = canvas.getBoundingClientRect()
|
|
781
|
+
const mouseX = e.clientX - canvasRect.left
|
|
782
|
+
const mouseY = e.clientY - canvasRect.top
|
|
783
|
+
|
|
784
|
+
// Find closest point
|
|
785
|
+
let closestIdx = -1
|
|
786
|
+
let closestDist = 30 // Max distance to trigger hover
|
|
787
|
+
points.forEach((p, i) => {
|
|
788
|
+
const dist = Math.abs(mouseX - p.x)
|
|
789
|
+
if (dist < closestDist) {
|
|
790
|
+
closestDist = dist
|
|
791
|
+
closestIdx = i
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
if (closestIdx >= 0) {
|
|
796
|
+
const p = points[closestIdx]
|
|
797
|
+
const d = p.data
|
|
798
|
+
tooltip.innerHTML = `
|
|
799
|
+
<div class="tooltip-date">${formatChartDate(d.date)}</div>
|
|
800
|
+
<div class="tooltip-row"><span class="tooltip-dot views"></span>Views: <strong>${formatNumber(d.views)}</strong></div>
|
|
801
|
+
<div class="tooltip-row"><span class="tooltip-dot visitors"></span>Visitors: <strong>${formatNumber(d.visitors || 0)}</strong></div>
|
|
802
|
+
`
|
|
803
|
+
tooltip.style.display = 'block'
|
|
804
|
+
// Position tooltip
|
|
805
|
+
let left = p.x + 10
|
|
806
|
+
if (left + 150 > canvas.width) left = p.x - 160
|
|
807
|
+
tooltip.style.left = left + 'px'
|
|
808
|
+
tooltip.style.top = (p.y - 30) + 'px'
|
|
809
|
+
draw(closestIdx)
|
|
810
|
+
canvas.style.cursor = 'pointer'
|
|
811
|
+
} else {
|
|
812
|
+
tooltip.style.display = 'none'
|
|
813
|
+
draw()
|
|
814
|
+
canvas.style.cursor = 'default'
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
canvas.onmouseleave = function() {
|
|
819
|
+
tooltip.style.display = 'none'
|
|
820
|
+
draw()
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Initial load
|
|
825
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
826
|
+
if (siteId) {
|
|
827
|
+
// Site ID provided - show dashboard directly
|
|
828
|
+
document.getElementById('site-selector-screen').style.display = 'none'
|
|
829
|
+
document.getElementById('dashboard-content').style.display = 'block'
|
|
830
|
+
fetchDashboardData()
|
|
831
|
+
// Refresh realtime every 30 seconds
|
|
832
|
+
setInterval(fetchDashboardData, 30000)
|
|
833
|
+
} else {
|
|
834
|
+
// No site ID - show site selector
|
|
835
|
+
document.getElementById('site-selector-screen').style.display = 'flex'
|
|
836
|
+
document.getElementById('dashboard-content').style.display = 'none'
|
|
837
|
+
fetchAvailableSites()
|
|
838
|
+
}
|
|
839
|
+
})
|
|
840
|
+
</script>
|
|
841
|
+
<style>
|
|
842
|
+
:root {
|
|
843
|
+
--bg-primary: #1a1f2e;
|
|
844
|
+
--bg-secondary: #242938;
|
|
845
|
+
--bg-tertiary: #2d3344;
|
|
846
|
+
--text-primary: #ffffff;
|
|
847
|
+
--text-secondary: #9ca3af;
|
|
848
|
+
--text-muted: #6b7280;
|
|
849
|
+
--accent: #818cf8;
|
|
850
|
+
--accent-hover: #6366f1;
|
|
851
|
+
--success: #10b981;
|
|
852
|
+
--border: #374151;
|
|
853
|
+
--chart-line: #818cf8;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
857
|
+
|
|
858
|
+
/* ===== Classes referenced by innerHTML (JS-rendered) ===== */
|
|
859
|
+
|
|
860
|
+
/* Site selector states */
|
|
861
|
+
.site-selector-loading,
|
|
862
|
+
.site-selector-error,
|
|
863
|
+
.site-selector-empty {
|
|
864
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 3rem; text-align: center; gap: 1rem;
|
|
865
|
+
color: var(--text-muted);
|
|
866
|
+
}
|
|
867
|
+
.site-selector-error button,
|
|
868
|
+
.site-selector-empty button {
|
|
869
|
+
border: none; padding: 0.5rem 1.5rem; border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem; margin-top: 0.5rem;
|
|
870
|
+
background: var(--accent);
|
|
871
|
+
color: white;
|
|
872
|
+
}
|
|
873
|
+
.site-selector-empty h2 {
|
|
874
|
+
font-size: 1.125rem; line-height: 1.75rem; margin: 0;
|
|
875
|
+
color: var(--text-primary);
|
|
876
|
+
}
|
|
877
|
+
.site-selector-grid {
|
|
878
|
+
display: flex; flex-direction: column; gap: 0.75rem;
|
|
879
|
+
}
|
|
880
|
+
.site-card {
|
|
881
|
+
display: flex; align-items: center; gap: 1rem; padding: 1rem 1.25rem; border-radius: 0.5rem; cursor: pointer; transition: all 150ms; text-align: left; width: 100%;
|
|
882
|
+
background: var(--bg-secondary);
|
|
883
|
+
border: 1px solid var(--border);
|
|
884
|
+
}
|
|
885
|
+
.site-card:hover {
|
|
886
|
+
background: var(--bg-tertiary);
|
|
887
|
+
border-color: var(--accent);
|
|
888
|
+
}
|
|
889
|
+
.site-card-icon {
|
|
890
|
+
width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; border-radius: 0.5rem;
|
|
891
|
+
background: var(--bg-tertiary);
|
|
892
|
+
color: var(--accent);
|
|
893
|
+
}
|
|
894
|
+
.site-card-info {
|
|
895
|
+
flex: 1 1 0%; display: flex; flex-direction: column; gap: 0.25rem;
|
|
896
|
+
}
|
|
897
|
+
.site-card-name {
|
|
898
|
+
font-weight: 500;
|
|
899
|
+
color: var(--text-primary);
|
|
900
|
+
}
|
|
901
|
+
.site-card-domain {
|
|
902
|
+
font-size: 0.75rem; line-height: 1rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
903
|
+
color: var(--text-muted);
|
|
904
|
+
}
|
|
905
|
+
.site-card-arrow {
|
|
906
|
+
color: var(--text-muted);
|
|
907
|
+
}
|
|
908
|
+
.site-card:hover .site-card-arrow {
|
|
909
|
+
color: var(--accent);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* Data table (used in innerHTML) */
|
|
913
|
+
.data-table {
|
|
914
|
+
width: 100%;
|
|
915
|
+
font-size: 0.8125rem;
|
|
916
|
+
}
|
|
917
|
+
.data-table th {
|
|
918
|
+
text-align: left; font-weight: 500; padding: 0.5rem 0; text-transform: uppercase; letter-spacing: 0.05em;
|
|
919
|
+
color: var(--text-muted);
|
|
920
|
+
padding-bottom: 0.5rem;
|
|
921
|
+
border-bottom: 1px solid var(--border);
|
|
922
|
+
font-size: 0.6875rem;
|
|
923
|
+
}
|
|
924
|
+
.data-table td {
|
|
925
|
+
border-bottom: 1px solid var(--bg-tertiary);
|
|
926
|
+
padding: 0.625rem 0;
|
|
927
|
+
}
|
|
928
|
+
.data-table tr:last-child td {
|
|
929
|
+
border-bottom: none;
|
|
930
|
+
}
|
|
931
|
+
.data-table .name {
|
|
932
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
933
|
+
color: var(--text-primary);
|
|
934
|
+
max-width: 180px;
|
|
935
|
+
}
|
|
936
|
+
.data-table .value {
|
|
937
|
+
text-align: right;
|
|
938
|
+
color: var(--text-secondary);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/* Empty state (used in innerHTML) */
|
|
942
|
+
.empty-state {
|
|
943
|
+
text-align: center; padding: 2rem;
|
|
944
|
+
color: var(--text-muted);
|
|
945
|
+
}
|
|
946
|
+
.empty-state p {
|
|
947
|
+
margin-bottom: 1rem;
|
|
948
|
+
}
|
|
949
|
+
.empty-state a {
|
|
950
|
+
text-decoration: none;
|
|
951
|
+
color: var(--accent);
|
|
952
|
+
}
|
|
953
|
+
.empty-state a:hover {
|
|
954
|
+
text-decoration: underline;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/* Chart tooltip (used in innerHTML) */
|
|
958
|
+
.chart-tooltip {
|
|
959
|
+
display: none; position: absolute; border-radius: 0.5rem; pointer-events: none; z-index: 100;
|
|
960
|
+
background: var(--bg-tertiary);
|
|
961
|
+
border: 1px solid var(--border);
|
|
962
|
+
padding: 0.75rem 1rem;
|
|
963
|
+
font-size: 0.8125rem;
|
|
964
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
965
|
+
min-width: 140px;
|
|
966
|
+
}
|
|
967
|
+
.tooltip-date {
|
|
968
|
+
font-size: 0.75rem; line-height: 1rem; margin-bottom: 0.5rem; font-weight: 500;
|
|
969
|
+
color: var(--text-secondary);
|
|
970
|
+
}
|
|
971
|
+
.tooltip-row {
|
|
972
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
973
|
+
color: var(--text-secondary);
|
|
974
|
+
margin: 0.25rem 0;
|
|
975
|
+
}
|
|
976
|
+
.tooltip-row strong {
|
|
977
|
+
margin-left: auto;
|
|
978
|
+
color: var(--text-primary);
|
|
979
|
+
}
|
|
980
|
+
.tooltip-dot {
|
|
981
|
+
width: 0.5rem; height: 0.5rem; border-radius: 0.125rem;
|
|
982
|
+
}
|
|
983
|
+
.tooltip-dot.views {
|
|
984
|
+
background: var(--accent);
|
|
985
|
+
}
|
|
986
|
+
.tooltip-dot.visitors {
|
|
987
|
+
background: var(--success);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/* Heatmap states (used in innerHTML) */
|
|
991
|
+
.heatmap-loading, .heatmap-empty {
|
|
992
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem;
|
|
993
|
+
height: 500px;
|
|
994
|
+
color: var(--text-muted);
|
|
995
|
+
}
|
|
996
|
+
.heatmap-stats {
|
|
997
|
+
position: absolute; bottom: 1rem; right: 1rem; padding: 0.5rem 1rem; border-radius: 0.25rem; font-size: 0.75rem; color: white;
|
|
998
|
+
background: rgba(0,0,0,0.7);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/* Heatmap panel (used in innerHTML) */
|
|
1002
|
+
.heatmap-panel {
|
|
1003
|
+
border-radius: 0.5rem; padding: 1rem;
|
|
1004
|
+
background: var(--bg-secondary);
|
|
1005
|
+
}
|
|
1006
|
+
.heatmap-panel h3 {
|
|
1007
|
+
font-size: 0.875rem; font-weight: 500; margin-bottom: 1rem;
|
|
1008
|
+
color: var(--text-primary);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/* Scroll heatmap (used in innerHTML) */
|
|
1012
|
+
.scroll-heatmap {
|
|
1013
|
+
padding: 1rem;
|
|
1014
|
+
}
|
|
1015
|
+
.scroll-stats {
|
|
1016
|
+
display: flex; gap: 2rem; margin-bottom: 1.5rem;
|
|
1017
|
+
}
|
|
1018
|
+
.scroll-stat {
|
|
1019
|
+
display: flex; flex-direction: column;
|
|
1020
|
+
}
|
|
1021
|
+
.scroll-stat-value {
|
|
1022
|
+
font-size: 1.875rem; line-height: 2.25rem; font-weight: 600;
|
|
1023
|
+
color: var(--text-primary);
|
|
1024
|
+
}
|
|
1025
|
+
.scroll-stat-label {
|
|
1026
|
+
font-size: 0.75rem; line-height: 1rem;
|
|
1027
|
+
color: var(--text-muted);
|
|
1028
|
+
}
|
|
1029
|
+
.scroll-bars {
|
|
1030
|
+
display: flex; flex-direction: column; gap: 0.5rem;
|
|
1031
|
+
}
|
|
1032
|
+
.scroll-bar-row {
|
|
1033
|
+
display: flex; align-items: center; gap: 0.75rem;
|
|
1034
|
+
}
|
|
1035
|
+
.scroll-depth-label {
|
|
1036
|
+
font-size: 0.75rem; text-align: right;
|
|
1037
|
+
width: 40px;
|
|
1038
|
+
color: var(--text-muted);
|
|
1039
|
+
}
|
|
1040
|
+
.scroll-bar-track {
|
|
1041
|
+
flex: 1 1 0%; border-radius: 0.25rem; overflow: hidden;
|
|
1042
|
+
height: 24px;
|
|
1043
|
+
background: var(--bg-tertiary);
|
|
1044
|
+
}
|
|
1045
|
+
.scroll-bar-fill {
|
|
1046
|
+
height: 100%; border-radius: 0.25rem; transition: width 300ms;
|
|
1047
|
+
}
|
|
1048
|
+
.scroll-reach-label {
|
|
1049
|
+
font-size: 0.8125rem; font-weight: 500;
|
|
1050
|
+
width: 50px;
|
|
1051
|
+
color: var(--text-primary);
|
|
1052
|
+
}
|
|
1053
|
+
.scroll-legend {
|
|
1054
|
+
display: flex; justify-content: center; gap: 1.5rem; margin-top: 1.5rem; padding-top: 1rem; font-size: 0.75rem;
|
|
1055
|
+
border-top: 1px solid var(--border);
|
|
1056
|
+
color: var(--text-muted);
|
|
1057
|
+
}
|
|
1058
|
+
.legend-dot {
|
|
1059
|
+
display: inline-block; width: 0.625rem; height: 0.625rem; border-radius: 0.125rem; margin-right: 0.375rem;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Element list (used in innerHTML) */
|
|
1063
|
+
.element-list {
|
|
1064
|
+
display: flex; flex-direction: column; gap: 0.75rem;
|
|
1065
|
+
}
|
|
1066
|
+
.element-item {
|
|
1067
|
+
display: flex; flex-direction: column; gap: 0.375rem;
|
|
1068
|
+
}
|
|
1069
|
+
.element-info {
|
|
1070
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
1071
|
+
}
|
|
1072
|
+
.element-tag {
|
|
1073
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; border-radius: 0.25rem;
|
|
1074
|
+
font-size: 0.6875rem;
|
|
1075
|
+
color: var(--accent);
|
|
1076
|
+
background: var(--bg-tertiary);
|
|
1077
|
+
padding: 0.125rem 0.375rem;
|
|
1078
|
+
}
|
|
1079
|
+
.element-selector {
|
|
1080
|
+
font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1081
|
+
color: var(--text-secondary);
|
|
1082
|
+
max-width: 200px;
|
|
1083
|
+
}
|
|
1084
|
+
.element-stats {
|
|
1085
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
1086
|
+
}
|
|
1087
|
+
.element-bar-track {
|
|
1088
|
+
flex: 1 1 0%; border-radius: 0.125rem; overflow: hidden;
|
|
1089
|
+
height: 6px;
|
|
1090
|
+
background: var(--bg-tertiary);
|
|
1091
|
+
}
|
|
1092
|
+
.element-bar-fill {
|
|
1093
|
+
height: 100%; border-radius: 0.125rem;
|
|
1094
|
+
background: var(--accent);
|
|
1095
|
+
}
|
|
1096
|
+
.element-clicks {
|
|
1097
|
+
font-size: 0.75rem; font-weight: 500; text-align: right;
|
|
1098
|
+
color: var(--text-primary);
|
|
1099
|
+
min-width: 40px;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/* Insight list (used in innerHTML) */
|
|
1103
|
+
.insight-list {
|
|
1104
|
+
display: flex; flex-direction: column; gap: 0.75rem;
|
|
1105
|
+
}
|
|
1106
|
+
.insight-item {
|
|
1107
|
+
display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0;
|
|
1108
|
+
border-bottom: 1px solid var(--bg-tertiary);
|
|
1109
|
+
}
|
|
1110
|
+
.insight-item:last-child {
|
|
1111
|
+
border-bottom: none;
|
|
1112
|
+
}
|
|
1113
|
+
.insight-label {
|
|
1114
|
+
font-size: 0.8125rem;
|
|
1115
|
+
color: var(--text-secondary);
|
|
1116
|
+
}
|
|
1117
|
+
.insight-value {
|
|
1118
|
+
font-size: 0.875rem; font-weight: 500;
|
|
1119
|
+
color: var(--text-primary);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/* text-muted helper (used in innerHTML) */
|
|
1123
|
+
.text-muted {
|
|
1124
|
+
font-size: 0.8125rem;
|
|
1125
|
+
color: var(--text-muted);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/* View tab active state (toggled by JS classList) */
|
|
1129
|
+
.view-tab.active {
|
|
1130
|
+
background: var(--accent);
|
|
1131
|
+
border-color: var(--accent);
|
|
1132
|
+
color: white;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/* Heatmap mode button active state (toggled by JS classList) */
|
|
1136
|
+
.heatmap-mode-btn.active {
|
|
1137
|
+
background: var(--accent);
|
|
1138
|
+
color: white;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/* Tab button active state (toggled by JS classList) */
|
|
1142
|
+
.tab-btn.active {
|
|
1143
|
+
color: var(--accent);
|
|
1144
|
+
border-bottom-color: var(--accent);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/* Pulse animation */
|
|
1148
|
+
@keyframes pulse {
|
|
1149
|
+
0%, 100% { opacity: 1; }
|
|
1150
|
+
50% { opacity: 0.5; }
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/* Responsive */
|
|
1154
|
+
@media (max-width: 1024px) {
|
|
1155
|
+
.summary-stats {
|
|
1156
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1157
|
+
}
|
|
1158
|
+
.data-grid {
|
|
1159
|
+
grid-template-columns: 1fr;
|
|
1160
|
+
}
|
|
1161
|
+
.heatmap-container {
|
|
1162
|
+
grid-template-columns: 1fr;
|
|
1163
|
+
}
|
|
1164
|
+
.heatmap-sidebar {
|
|
1165
|
+
order: -1;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
@media (max-width: 640px) {
|
|
1169
|
+
.summary-stats {
|
|
1170
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1171
|
+
}
|
|
1172
|
+
.header {
|
|
1173
|
+
flex-direction: column;
|
|
1174
|
+
gap: 1rem;
|
|
1175
|
+
}
|
|
1176
|
+
.heatmap-controls {
|
|
1177
|
+
flex-direction: column;
|
|
1178
|
+
align-items: stretch;
|
|
1179
|
+
}
|
|
1180
|
+
.heatmap-controls select {
|
|
1181
|
+
max-width: none;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
</style>
|
|
1185
|
+
</head>
|
|
1186
|
+
<body class="font-sans leading-normal min-h-screen" style="background:var(--bg-primary);color:var(--text-primary);">
|
|
1187
|
+
<!-- Site Selector Screen (shown when no siteId) -->
|
|
1188
|
+
<div id="site-selector-screen" class="min-h-screen flex flex-col items-center justify-center p-8" style="display:none">
|
|
1189
|
+
<div class="text-center mb-8">
|
|
1190
|
+
<h1 class="text-2xl font-semibold mb-2" style="color:var(--text-primary);">Analytics Dashboard</h1>
|
|
1191
|
+
<p class="text-sm" style="color:var(--text-secondary);">Select a site to view analytics</p>
|
|
1192
|
+
</div>
|
|
1193
|
+
<div id="site-selector-container" class="w-full max-w-[600px]">
|
|
1194
|
+
<div class="site-selector-loading">
|
|
1195
|
+
<div class="spinner"></div>
|
|
1196
|
+
<p>Loading sites...</p>
|
|
1197
|
+
</div>
|
|
1198
|
+
</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
|
|
1201
|
+
<!-- Dashboard Content -->
|
|
1202
|
+
<div id="dashboard-content" class="max-w-[1400px] mx-auto p-4" style="display:none">
|
|
1203
|
+
<!-- Header -->
|
|
1204
|
+
<header class="header flex justify-between items-center py-4 border-b border-border mb-4">
|
|
1205
|
+
<div class="site-selector flex items-center gap-2">
|
|
1206
|
+
<select class="py-2 px-4 rounded-md text-sm cursor-pointer" style="background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary);">
|
|
1207
|
+
<option>{{ siteName }}</option>
|
|
1208
|
+
</select>
|
|
1209
|
+
</div>
|
|
1210
|
+
<div class="flex items-center gap-4">
|
|
1211
|
+
<div class="flex items-center gap-2 py-2 px-4 rounded-full text-sm" style="background:var(--bg-secondary);">
|
|
1212
|
+
<span class="w-2 h-2 rounded-full" style="background:var(--success);animation:pulse 2s infinite;"></span>
|
|
1213
|
+
<span id="realtime-count">0 current visitors</span>
|
|
1214
|
+
</div>
|
|
1215
|
+
<button class="icon-btn" title="Notifications">
|
|
1216
|
+
<Icon name="bell" size="20" />
|
|
1217
|
+
</button>
|
|
1218
|
+
<button class="icon-btn" title="Settings">
|
|
1219
|
+
<Icon name="settings" size="20" />
|
|
1220
|
+
</button>
|
|
1221
|
+
</div>
|
|
1222
|
+
</header>
|
|
1223
|
+
|
|
1224
|
+
<!-- View Tabs -->
|
|
1225
|
+
<div class="flex gap-2 mb-4">
|
|
1226
|
+
<button class="view-tab py-2 px-4 rounded-md text-sm cursor-pointer transition-all duration-150 border active" style="background:var(--bg-secondary);border-color:var(--border);color:var(--text-secondary);" data-view="dashboard" onclick="switchView('dashboard')">
|
|
1227
|
+
<Icon name="bar-chart-3" size="16" class="inline align-middle mr-1.5" />
|
|
1228
|
+
Dashboard
|
|
1229
|
+
</button>
|
|
1230
|
+
<button class="view-tab py-2 px-4 rounded-md text-sm cursor-pointer transition-all duration-150 border" style="background:var(--bg-secondary);border-color:var(--border);color:var(--text-secondary);" data-view="heatmap" onclick="switchView('heatmap')">
|
|
1231
|
+
<Icon name="flame" size="16" class="inline align-middle mr-1.5" />
|
|
1232
|
+
Heatmaps
|
|
1233
|
+
</button>
|
|
1234
|
+
</div>
|
|
1235
|
+
|
|
1236
|
+
<!-- Dashboard View -->
|
|
1237
|
+
<div id="dashboard-view">
|
|
1238
|
+
|
|
1239
|
+
<!-- Date Range Bar -->
|
|
1240
|
+
<div class="flex items-center gap-4 py-3 mb-4">
|
|
1241
|
+
<select class="py-2 px-4 rounded-md text-sm cursor-pointer" style="background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary);">
|
|
1242
|
+
<option>Last 30 days</option>
|
|
1243
|
+
<option>Last 7 days</option>
|
|
1244
|
+
<option>Today</option>
|
|
1245
|
+
<option>Yesterday</option>
|
|
1246
|
+
<option>Last 12 months</option>
|
|
1247
|
+
<option>Custom range</option>
|
|
1248
|
+
</select>
|
|
1249
|
+
<span class="text-sm" style="color:var(--text-secondary);">compared to</span>
|
|
1250
|
+
<select class="py-2 px-4 rounded-md text-sm cursor-pointer" style="background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary);">
|
|
1251
|
+
<option>No comparison</option>
|
|
1252
|
+
<option>Previous period</option>
|
|
1253
|
+
<option>Same period last year</option>
|
|
1254
|
+
</select>
|
|
1255
|
+
<select class="py-2 px-4 rounded-md text-sm cursor-pointer" style="background:var(--bg-secondary);border:1px solid var(--border);color:var(--text-primary);">
|
|
1256
|
+
<option>Auto</option>
|
|
1257
|
+
<option>Hourly</option>
|
|
1258
|
+
<option>Daily</option>
|
|
1259
|
+
<option>Monthly</option>
|
|
1260
|
+
</select>
|
|
1261
|
+
</div>
|
|
1262
|
+
|
|
1263
|
+
<!-- Summary Stats -->
|
|
1264
|
+
<div class="summary-stats grid grid-cols-6 gap-4 mb-6">
|
|
1265
|
+
<div class="stat-card text-center" style="background:var(--bg-secondary);padding:1.25rem;border-radius:8px;">
|
|
1266
|
+
<div class="text-3xl font-semibold mb-1" style="color:var(--text-primary);" id="stat-realtime">0</div>
|
|
1267
|
+
<div class="text-xs uppercase tracking-wider" style="color:var(--text-secondary);">Realtime</div>
|
|
1268
|
+
</div>
|
|
1269
|
+
<div class="stat-card text-center" style="background:var(--bg-secondary);padding:1.25rem;border-radius:8px;">
|
|
1270
|
+
<div class="text-3xl font-semibold mb-1" style="color:var(--text-primary);" id="stat-people">0</div>
|
|
1271
|
+
<div class="text-xs uppercase tracking-wider" style="color:var(--text-secondary);">People</div>
|
|
1272
|
+
</div>
|
|
1273
|
+
<div class="stat-card text-center" style="background:var(--bg-secondary);padding:1.25rem;border-radius:8px;">
|
|
1274
|
+
<div class="text-3xl font-semibold mb-1" style="color:var(--text-primary);" id="stat-views">0</div>
|
|
1275
|
+
<div class="text-xs uppercase tracking-wider" style="color:var(--text-secondary);">Views</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
<div class="stat-card text-center" style="background:var(--bg-secondary);padding:1.25rem;border-radius:8px;">
|
|
1278
|
+
<div class="text-3xl font-semibold mb-1" style="color:var(--text-primary);" id="stat-avgtime">00:00</div>
|
|
1279
|
+
<div class="text-xs uppercase tracking-wider" style="color:var(--text-secondary);">Avg time</div>
|
|
1280
|
+
</div>
|
|
1281
|
+
<div class="stat-card text-center" style="background:var(--bg-secondary);padding:1.25rem;border-radius:8px;">
|
|
1282
|
+
<div class="text-3xl font-semibold mb-1" style="color:var(--text-primary);" id="stat-bounce">0%</div>
|
|
1283
|
+
<div class="text-xs uppercase tracking-wider" style="color:var(--text-secondary);">Bounce</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
<div class="stat-card text-center" style="background:var(--bg-secondary);padding:1.25rem;border-radius:8px;">
|
|
1286
|
+
<div class="text-3xl font-semibold mb-1" style="color:var(--text-primary);" id="stat-events">0</div>
|
|
1287
|
+
<div class="text-xs uppercase tracking-wider" style="color:var(--text-secondary);">Events</div>
|
|
1288
|
+
</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
|
|
1291
|
+
<!-- Time Series Chart -->
|
|
1292
|
+
<div class="rounded-lg p-6 mb-6 min-h-[250px] relative" style="background:var(--bg-secondary);">
|
|
1293
|
+
<canvas id="timeSeriesChart"></canvas>
|
|
1294
|
+
<div id="chartTooltip" class="chart-tooltip"></div>
|
|
1295
|
+
</div>
|
|
1296
|
+
|
|
1297
|
+
<!-- Data Grid - Row 1 -->
|
|
1298
|
+
<div class="data-grid grid grid-cols-2 gap-4 mb-6">
|
|
1299
|
+
<!-- Pages -->
|
|
1300
|
+
<div class="rounded-lg p-4" style="background:var(--bg-secondary);">
|
|
1301
|
+
<div class="panel-header flex items-center gap-2 mb-4">
|
|
1302
|
+
<span class="panel-title text-sm font-medium" style="color:var(--text-primary);">Pages</span>
|
|
1303
|
+
</div>
|
|
1304
|
+
<table class="data-table">
|
|
1305
|
+
<thead>
|
|
1306
|
+
<tr>
|
|
1307
|
+
<th>Path</th>
|
|
1308
|
+
<th style="text-align:right">Entries</th>
|
|
1309
|
+
<th style="text-align:right">Visitors</th>
|
|
1310
|
+
<th style="text-align:right">Views</th>
|
|
1311
|
+
</tr>
|
|
1312
|
+
</thead>
|
|
1313
|
+
<tbody id="pages-body">
|
|
1314
|
+
<tr>
|
|
1315
|
+
<td colspan="4" style="text-align:center;color:var(--text-muted);padding:1rem">Loading...</td>
|
|
1316
|
+
</tr>
|
|
1317
|
+
</tbody>
|
|
1318
|
+
</table>
|
|
1319
|
+
</div>
|
|
1320
|
+
|
|
1321
|
+
<!-- Referrers / Sources -->
|
|
1322
|
+
<div class="rounded-lg p-4" style="background:var(--bg-secondary);">
|
|
1323
|
+
<div class="tabs flex gap-1 border-b border-border mb-4">
|
|
1324
|
+
<button class="tab-btn active">Referrers</button>
|
|
1325
|
+
<button class="tab-btn" data-tab="sources">Sources</button>
|
|
1326
|
+
</div>
|
|
1327
|
+
<table class="data-table">
|
|
1328
|
+
<thead>
|
|
1329
|
+
<tr>
|
|
1330
|
+
<th>Source</th>
|
|
1331
|
+
<th style="text-align:right">Visitors</th>
|
|
1332
|
+
<th style="text-align:right">Views</th>
|
|
1333
|
+
</tr>
|
|
1334
|
+
</thead>
|
|
1335
|
+
<tbody id="referrers-body">
|
|
1336
|
+
<tr>
|
|
1337
|
+
<td colspan="3" style="text-align:center;color:var(--text-muted);padding:1rem">Loading...</td>
|
|
1338
|
+
</tr>
|
|
1339
|
+
</tbody>
|
|
1340
|
+
</table>
|
|
1341
|
+
</div>
|
|
1342
|
+
</div>
|
|
1343
|
+
|
|
1344
|
+
<!-- Data Grid - Row 2 -->
|
|
1345
|
+
<div class="data-grid grid grid-cols-2 gap-4 mb-6">
|
|
1346
|
+
<!-- Device Types / OS -->
|
|
1347
|
+
<div class="rounded-lg p-4" style="background:var(--bg-secondary);">
|
|
1348
|
+
<div class="tabs flex gap-1 border-b border-border mb-4">
|
|
1349
|
+
<button class="tab-btn active" data-tab="types">Device Types</button>
|
|
1350
|
+
<button class="tab-btn" data-tab="os">OS</button>
|
|
1351
|
+
</div>
|
|
1352
|
+
<table class="data-table">
|
|
1353
|
+
<thead>
|
|
1354
|
+
<tr>
|
|
1355
|
+
<th>Type</th>
|
|
1356
|
+
<th style="text-align:right">Visitors</th>
|
|
1357
|
+
</tr>
|
|
1358
|
+
</thead>
|
|
1359
|
+
<tbody id="devices-body">
|
|
1360
|
+
<tr>
|
|
1361
|
+
<td colspan="2" style="text-align:center;color:var(--text-muted);padding:1rem">Loading...</td>
|
|
1362
|
+
</tr>
|
|
1363
|
+
</tbody>
|
|
1364
|
+
</table>
|
|
1365
|
+
</div>
|
|
1366
|
+
|
|
1367
|
+
<!-- Browsers -->
|
|
1368
|
+
<div class="rounded-lg p-4" style="background:var(--bg-secondary);">
|
|
1369
|
+
<div class="panel-header flex items-center gap-2 mb-4">
|
|
1370
|
+
<span class="panel-title text-sm font-medium" style="color:var(--text-primary);">Browsers</span>
|
|
1371
|
+
</div>
|
|
1372
|
+
<table class="data-table">
|
|
1373
|
+
<thead>
|
|
1374
|
+
<tr>
|
|
1375
|
+
<th>Browser</th>
|
|
1376
|
+
<th style="text-align:right">Visitors</th>
|
|
1377
|
+
</tr>
|
|
1378
|
+
</thead>
|
|
1379
|
+
<tbody id="browsers-body">
|
|
1380
|
+
<tr>
|
|
1381
|
+
<td colspan="2" style="text-align:center;color:var(--text-muted);padding:1rem">Loading...</td>
|
|
1382
|
+
</tr>
|
|
1383
|
+
</tbody>
|
|
1384
|
+
</table>
|
|
1385
|
+
</div>
|
|
1386
|
+
</div>
|
|
1387
|
+
|
|
1388
|
+
<!-- Data Grid - Row 3 -->
|
|
1389
|
+
<div class="data-grid grid grid-cols-2 gap-4 mb-6">
|
|
1390
|
+
<!-- Countries / Regions / Cities -->
|
|
1391
|
+
<div class="rounded-lg p-4" style="background:var(--bg-secondary);">
|
|
1392
|
+
<div class="tabs flex gap-1 border-b border-border mb-4">
|
|
1393
|
+
<button class="tab-btn active" data-tab="countries">Countries</button>
|
|
1394
|
+
<button class="tab-btn" data-tab="regions">Regions</button>
|
|
1395
|
+
<button class="tab-btn" data-tab="cities">Cities</button>
|
|
1396
|
+
</div>
|
|
1397
|
+
<table class="data-table">
|
|
1398
|
+
<thead>
|
|
1399
|
+
<tr>
|
|
1400
|
+
<th>Country</th>
|
|
1401
|
+
<th style="text-align:right">Visitors</th>
|
|
1402
|
+
</tr>
|
|
1403
|
+
</thead>
|
|
1404
|
+
<tbody id="countries-body">
|
|
1405
|
+
<tr>
|
|
1406
|
+
<td colspan="2" style="text-align:center;color:var(--text-muted);padding:1rem">Loading...</td>
|
|
1407
|
+
</tr>
|
|
1408
|
+
</tbody>
|
|
1409
|
+
</table>
|
|
1410
|
+
</div>
|
|
1411
|
+
|
|
1412
|
+
<!-- Campaigns -->
|
|
1413
|
+
<div class="rounded-lg p-4" style="background:var(--bg-secondary);">
|
|
1414
|
+
<div class="tabs flex gap-1 border-b border-border mb-4">
|
|
1415
|
+
<button class="tab-btn active" data-tab="campaign">Campaign</button>
|
|
1416
|
+
<button class="tab-btn" data-tab="source">Source</button>
|
|
1417
|
+
<button class="tab-btn" data-tab="medium">Medium</button>
|
|
1418
|
+
<button class="tab-btn" data-tab="content">Content</button>
|
|
1419
|
+
<button class="tab-btn" data-tab="term">Term</button>
|
|
1420
|
+
</div>
|
|
1421
|
+
<table class="data-table">
|
|
1422
|
+
<thead>
|
|
1423
|
+
<tr>
|
|
1424
|
+
<th>Campaign</th>
|
|
1425
|
+
<th style="text-align:right">Visitors</th>
|
|
1426
|
+
<th style="text-align:right">Views</th>
|
|
1427
|
+
</tr>
|
|
1428
|
+
</thead>
|
|
1429
|
+
<tbody id="campaigns-body">
|
|
1430
|
+
<tr>
|
|
1431
|
+
<td colspan="3" style="text-align:center;color:var(--text-muted);padding:1rem">Loading...</td>
|
|
1432
|
+
</tr>
|
|
1433
|
+
</tbody>
|
|
1434
|
+
</table>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
|
|
1438
|
+
<!-- Events Section -->
|
|
1439
|
+
<div class="rounded-lg p-6" style="background:var(--bg-secondary);">
|
|
1440
|
+
<div class="flex justify-between items-center mb-4">
|
|
1441
|
+
<span class="text-sm font-medium">Events</span>
|
|
1442
|
+
</div>
|
|
1443
|
+
<div id="events-container">
|
|
1444
|
+
<div class="empty-state">
|
|
1445
|
+
<p>Loading events...</p>
|
|
1446
|
+
</div>
|
|
1447
|
+
</div>
|
|
1448
|
+
</div>
|
|
1449
|
+
|
|
1450
|
+
</div><!-- End Dashboard View -->
|
|
1451
|
+
|
|
1452
|
+
<!-- Heatmap View -->
|
|
1453
|
+
<div id="heatmap-view" style="display:none">
|
|
1454
|
+
<div class="heatmap-container grid gap-4" style="grid-template-columns:1fr 320px;">
|
|
1455
|
+
<div class="heatmap-main flex flex-col gap-4">
|
|
1456
|
+
<!-- Controls -->
|
|
1457
|
+
<div class="heatmap-controls flex items-center gap-4 p-4 rounded-lg" style="background:var(--bg-secondary);">
|
|
1458
|
+
<select id="heatmap-page-select" class="flex-1 max-w-[400px] py-2 px-4 rounded-md text-sm" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-primary);" onchange="selectHeatmapPage(this.value)">
|
|
1459
|
+
<option value="">Select a page...</option>
|
|
1460
|
+
</select>
|
|
1461
|
+
<div class="flex gap-1 p-1 rounded-md" style="background:var(--bg-tertiary);">
|
|
1462
|
+
<button class="heatmap-mode-btn py-2 px-4 rounded text-[0.8125rem] cursor-pointer transition-all duration-150 border-none active" style="background:transparent;color:var(--text-secondary);" data-mode="click" onclick="switchHeatmapMode('click')">
|
|
1463
|
+
<Icon name="mouse-pointer-click" size="14" class="inline align-middle mr-1" />
|
|
1464
|
+
Clicks
|
|
1465
|
+
</button>
|
|
1466
|
+
<button class="heatmap-mode-btn py-2 px-4 rounded text-[0.8125rem] cursor-pointer transition-all duration-150 border-none" style="background:transparent;color:var(--text-secondary);" data-mode="scroll" onclick="switchHeatmapMode('scroll')">
|
|
1467
|
+
<Icon name="arrow-down" size="14" class="inline align-middle mr-1" />
|
|
1468
|
+
Scroll
|
|
1469
|
+
</button>
|
|
1470
|
+
</div>
|
|
1471
|
+
<button class="icon-btn" title="Refresh" onclick="fetchHeatmapData()">
|
|
1472
|
+
<Icon name="refresh-cw" size="18" />
|
|
1473
|
+
</button>
|
|
1474
|
+
</div>
|
|
1475
|
+
|
|
1476
|
+
<!-- Visualization -->
|
|
1477
|
+
<div class="rounded-lg p-4 min-h-[540px]" style="background:var(--bg-secondary);">
|
|
1478
|
+
<div id="heatmap-viz">
|
|
1479
|
+
<div class="heatmap-empty">
|
|
1480
|
+
<Icon name="flame" size="48" />
|
|
1481
|
+
<p>Select a page to view its heatmap</p>
|
|
1482
|
+
</div>
|
|
1483
|
+
</div>
|
|
1484
|
+
</div>
|
|
1485
|
+
</div>
|
|
1486
|
+
|
|
1487
|
+
<!-- Sidebar -->
|
|
1488
|
+
<div class="heatmap-sidebar flex flex-col gap-4" id="heatmap-sidebar">
|
|
1489
|
+
<div class="heatmap-panel">
|
|
1490
|
+
<h3>Getting Started</h3>
|
|
1491
|
+
<p class="text-muted">Select a page from the dropdown to view click or scroll heatmaps. Heatmap data is collected from user interactions on your site.</p>
|
|
1492
|
+
</div>
|
|
1493
|
+
</div>
|
|
1494
|
+
</div>
|
|
1495
|
+
</div>
|
|
1496
|
+
|
|
1497
|
+
</div><!-- End dashboard-content -->
|
|
1498
|
+
|
|
1499
|
+
<script>
|
|
1500
|
+
// Tab switching logic
|
|
1501
|
+
document.querySelectorAll('.tabs').forEach(tabContainer => {
|
|
1502
|
+
const buttons = tabContainer.querySelectorAll('.tab-btn')
|
|
1503
|
+
buttons.forEach(btn => {
|
|
1504
|
+
btn.addEventListener('click', () => {
|
|
1505
|
+
buttons.forEach(b => b.classList.remove('active'))
|
|
1506
|
+
btn.classList.add('active')
|
|
1507
|
+
})
|
|
1508
|
+
})
|
|
1509
|
+
})
|
|
1510
|
+
|
|
1511
|
+
// Handle window resize for chart
|
|
1512
|
+
window.addEventListener('resize', () => {
|
|
1513
|
+
if (timeSeriesData.length) renderChart()
|
|
1514
|
+
})
|
|
1515
|
+
</script>
|
|
1516
|
+
</body>
|
|
1517
|
+
</html>
|