@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,264 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* ClickHeatmap Component
|
|
4
|
+
* Canvas-based click density overlay for page heatmap visualization.
|
|
5
|
+
* Uses STX signals for reactive state management.
|
|
6
|
+
*/
|
|
7
|
+
const clicks = state($props.clicks || [])
|
|
8
|
+
const width = $props.width || 800
|
|
9
|
+
const height = $props.height || 600
|
|
10
|
+
const radius = $props.radius || 25
|
|
11
|
+
const blur = $props.blur || 15
|
|
12
|
+
const maxOpacity = $props.maxOpacity || 0.6
|
|
13
|
+
const loading = state($props.loading || false)
|
|
14
|
+
|
|
15
|
+
// Reactive tooltip state
|
|
16
|
+
const hoveredPoint = state(null)
|
|
17
|
+
const tooltipX = state(0)
|
|
18
|
+
const tooltipY = state(0)
|
|
19
|
+
|
|
20
|
+
// Computed: max click count for normalization
|
|
21
|
+
const maxCount = derived(() => {
|
|
22
|
+
const c = clicks()
|
|
23
|
+
return c.length > 0 ? Math.max(...c.map(p => p.count)) : 1
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Initialize canvas on mount
|
|
27
|
+
function initCanvas() {
|
|
28
|
+
const canvas = document.getElementById('heatmap-canvas')
|
|
29
|
+
if (!canvas) return
|
|
30
|
+
|
|
31
|
+
const ctx = canvas.getContext('2d')
|
|
32
|
+
if (!ctx) return
|
|
33
|
+
|
|
34
|
+
// Clear canvas
|
|
35
|
+
ctx.clearRect(0, 0, width, height)
|
|
36
|
+
|
|
37
|
+
// Draw all points with intensity based on count
|
|
38
|
+
const max = maxCount()
|
|
39
|
+
for (const point of clicks()) {
|
|
40
|
+
const intensity = (point.count / max) * maxOpacity
|
|
41
|
+
drawPoint(ctx, point.x, point.y, intensity)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Apply blur
|
|
45
|
+
if (blur > 0) {
|
|
46
|
+
ctx.filter = `blur(${blur}px)`
|
|
47
|
+
ctx.drawImage(canvas, 0, 0)
|
|
48
|
+
ctx.filter = 'none'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Colorize using gradient
|
|
52
|
+
colorizeHeatmap(ctx)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function drawPoint(ctx, x, y, intensity) {
|
|
56
|
+
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius)
|
|
57
|
+
gradient.addColorStop(0, `rgba(0, 0, 0, ${intensity})`)
|
|
58
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)')
|
|
59
|
+
ctx.fillStyle = gradient
|
|
60
|
+
ctx.beginPath()
|
|
61
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2)
|
|
62
|
+
ctx.fill()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function colorizeHeatmap(ctx) {
|
|
66
|
+
const palette = createGradientPalette()
|
|
67
|
+
const imageData = ctx.getImageData(0, 0, width, height)
|
|
68
|
+
const data = imageData.data
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
71
|
+
const alpha = data[i + 3]
|
|
72
|
+
if (alpha > 0) {
|
|
73
|
+
const paletteIndex = Math.min(255, Math.floor((alpha / 255) * 255)) * 4
|
|
74
|
+
data[i] = palette[paletteIndex]
|
|
75
|
+
data[i + 1] = palette[paletteIndex + 1]
|
|
76
|
+
data[i + 2] = palette[paletteIndex + 2]
|
|
77
|
+
data[i + 3] = alpha * 2
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ctx.putImageData(imageData, 0, 0)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createGradientPalette() {
|
|
85
|
+
const canvas = document.createElement('canvas')
|
|
86
|
+
canvas.width = 256
|
|
87
|
+
canvas.height = 1
|
|
88
|
+
const ctx = canvas.getContext('2d')
|
|
89
|
+
const gradient = ctx.createLinearGradient(0, 0, 256, 0)
|
|
90
|
+
|
|
91
|
+
gradient.addColorStop(0.0, 'rgba(0, 0, 255, 0)')
|
|
92
|
+
gradient.addColorStop(0.25, 'rgba(0, 0, 255, 0.5)')
|
|
93
|
+
gradient.addColorStop(0.5, 'rgba(0, 255, 0, 0.5)')
|
|
94
|
+
gradient.addColorStop(0.75, 'rgba(255, 255, 0, 0.75)')
|
|
95
|
+
gradient.addColorStop(1.0, 'rgba(255, 0, 0, 1)')
|
|
96
|
+
|
|
97
|
+
ctx.fillStyle = gradient
|
|
98
|
+
ctx.fillRect(0, 0, 256, 1)
|
|
99
|
+
return ctx.getImageData(0, 0, 256, 1).data
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function handleMouseMove(event) {
|
|
103
|
+
const canvas = event.target
|
|
104
|
+
const rect = canvas.getBoundingClientRect()
|
|
105
|
+
const x = event.clientX - rect.left
|
|
106
|
+
const y = event.clientY - rect.top
|
|
107
|
+
|
|
108
|
+
// Find nearest point within threshold
|
|
109
|
+
const threshold = radius * 2
|
|
110
|
+
let nearest = null
|
|
111
|
+
let minDistance = threshold
|
|
112
|
+
|
|
113
|
+
for (const point of clicks()) {
|
|
114
|
+
const distance = Math.sqrt((point.x - x) ** 2 + (point.y - y) ** 2)
|
|
115
|
+
if (distance < minDistance) {
|
|
116
|
+
minDistance = distance
|
|
117
|
+
nearest = point
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
hoveredPoint.set(nearest)
|
|
122
|
+
if (nearest) {
|
|
123
|
+
tooltipX.set(nearest.x + 15)
|
|
124
|
+
tooltipY.set(nearest.y - 10)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Call optional callback
|
|
128
|
+
if ($props.onHover) $props.onHover(nearest)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleMouseLeave() {
|
|
132
|
+
hoveredPoint.set(null)
|
|
133
|
+
if ($props.onHover) $props.onHover(null)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handleClick() {
|
|
137
|
+
const point = hoveredPoint()
|
|
138
|
+
if (point && $props.onClick) {
|
|
139
|
+
$props.onClick(point)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
onMount(() => {
|
|
144
|
+
initCanvas()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Re-render when clicks change
|
|
148
|
+
effect(() => {
|
|
149
|
+
clicks()
|
|
150
|
+
initCanvas()
|
|
151
|
+
})
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<div class="click-heatmap" @style="{ width: width + 'px', height: height + 'px', position: 'relative', overflow: 'hidden' }">
|
|
155
|
+
<!-- Loading overlay -->
|
|
156
|
+
<div x-if="loading()" class="heatmap-loading-overlay">
|
|
157
|
+
<div class="heatmap-spinner"></div>
|
|
158
|
+
<span>Loading heatmap data...</span>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- Main heatmap canvas -->
|
|
162
|
+
<canvas
|
|
163
|
+
id="heatmap-canvas"
|
|
164
|
+
x-width="width"
|
|
165
|
+
x-height="height"
|
|
166
|
+
class="heatmap-canvas"
|
|
167
|
+
></canvas>
|
|
168
|
+
|
|
169
|
+
<!-- Interaction overlay -->
|
|
170
|
+
<canvas
|
|
171
|
+
id="heatmap-interaction"
|
|
172
|
+
x-width="width"
|
|
173
|
+
x-height="height"
|
|
174
|
+
class="heatmap-interaction"
|
|
175
|
+
@mousemove="handleMouseMove"
|
|
176
|
+
@mouseleave="handleMouseLeave"
|
|
177
|
+
@click="handleClick"
|
|
178
|
+
></canvas>
|
|
179
|
+
|
|
180
|
+
<!-- Tooltip -->
|
|
181
|
+
<div
|
|
182
|
+
x-if="hoveredPoint()"
|
|
183
|
+
class="heatmap-tooltip"
|
|
184
|
+
@style="{ left: tooltipX() + 'px', top: tooltipY() + 'px' }"
|
|
185
|
+
>
|
|
186
|
+
<div class="heatmap-tooltip-content">
|
|
187
|
+
<strong class="heatmap-tooltip-count">{{ hoveredPoint().count }}</strong> clicks
|
|
188
|
+
<div x-if="hoveredPoint().topElement" class="heatmap-tooltip-selector">
|
|
189
|
+
{{ hoveredPoint().topElement.selector }}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<style>
|
|
196
|
+
.click-heatmap {
|
|
197
|
+
position: relative;
|
|
198
|
+
overflow: hidden;
|
|
199
|
+
}
|
|
200
|
+
.heatmap-loading-overlay {
|
|
201
|
+
position: absolute;
|
|
202
|
+
inset: 0;
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-direction: column;
|
|
205
|
+
align-items: center;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
background: rgba(255, 255, 255, 0.9);
|
|
208
|
+
z-index: 20;
|
|
209
|
+
gap: 0.75rem;
|
|
210
|
+
color: #6b7280;
|
|
211
|
+
font-size: 0.875rem;
|
|
212
|
+
}
|
|
213
|
+
.heatmap-spinner {
|
|
214
|
+
width: 32px;
|
|
215
|
+
height: 32px;
|
|
216
|
+
border: 2px solid #e5e7eb;
|
|
217
|
+
border-top-color: #6366f1;
|
|
218
|
+
border-radius: 50%;
|
|
219
|
+
animation: spin 1s linear infinite;
|
|
220
|
+
}
|
|
221
|
+
@keyframes spin {
|
|
222
|
+
to { transform: rotate(-360deg); }
|
|
223
|
+
}
|
|
224
|
+
.heatmap-canvas {
|
|
225
|
+
position: absolute;
|
|
226
|
+
top: 0;
|
|
227
|
+
left: 0;
|
|
228
|
+
pointer-events: none;
|
|
229
|
+
}
|
|
230
|
+
.heatmap-interaction {
|
|
231
|
+
position: absolute;
|
|
232
|
+
top: 0;
|
|
233
|
+
left: 0;
|
|
234
|
+
cursor: crosshair;
|
|
235
|
+
background: transparent;
|
|
236
|
+
}
|
|
237
|
+
.heatmap-tooltip {
|
|
238
|
+
position: absolute;
|
|
239
|
+
z-index: 30;
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
transform: translateY(-100%);
|
|
242
|
+
}
|
|
243
|
+
.heatmap-tooltip-content {
|
|
244
|
+
background: #1f2937;
|
|
245
|
+
color: white;
|
|
246
|
+
padding: 0.5rem 0.75rem;
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
font-size: 0.75rem;
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
251
|
+
}
|
|
252
|
+
.heatmap-tooltip-count {
|
|
253
|
+
color: #fbbf24;
|
|
254
|
+
}
|
|
255
|
+
.heatmap-tooltip-selector {
|
|
256
|
+
font-size: 0.625rem;
|
|
257
|
+
color: #9ca3af;
|
|
258
|
+
font-family: monospace;
|
|
259
|
+
margin-top: 0.25rem;
|
|
260
|
+
overflow: hidden;
|
|
261
|
+
text-overflow: ellipsis;
|
|
262
|
+
max-width: 200px;
|
|
263
|
+
}
|
|
264
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* ElementClickList Component
|
|
4
|
+
*
|
|
5
|
+
* Displays a table of top clicked elements with their selectors and counts.
|
|
6
|
+
*/
|
|
7
|
+
export const elements = $props.elements || []
|
|
8
|
+
export const loading = $props.loading || false
|
|
9
|
+
export const maxItems = $props.maxItems || 10
|
|
10
|
+
export const onHighlight = $props.onHighlight
|
|
11
|
+
export const onClick = $props.onClick
|
|
12
|
+
|
|
13
|
+
const displayElements = elements.slice(0, maxItems)
|
|
14
|
+
const totalClicks = elements.reduce((sum, el) => sum + el.count, 0)
|
|
15
|
+
|
|
16
|
+
export function formatSelector(selector) {
|
|
17
|
+
if (selector.length > 50) {
|
|
18
|
+
return selector.slice(0, 47) + '...'
|
|
19
|
+
}
|
|
20
|
+
return selector
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getPercentage(count) {
|
|
24
|
+
if (totalClicks === 0) return 0
|
|
25
|
+
return Math.round((count / totalClicks) * 100)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatCount(count) {
|
|
29
|
+
if (count >= 1000) {
|
|
30
|
+
return `${(count / 1000).toFixed(1)}k`
|
|
31
|
+
}
|
|
32
|
+
return count.toString()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function handleMouseEnter(selector) {
|
|
36
|
+
onHighlight?.(selector)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function handleMouseLeave() {
|
|
40
|
+
onHighlight?.(null)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function handleClick(element) {
|
|
44
|
+
onClick?.(element)
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div class="element-click-list bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
49
|
+
<div class="flex justify-between items-center p-4 border-b border-gray-200">
|
|
50
|
+
<h3 class="text-sm font-semibold text-gray-900">Top Clicked Elements</h3>
|
|
51
|
+
@if(!loading)
|
|
52
|
+
<span class="text-xs text-gray-500">{{ formatCount(totalClicks) }} total clicks</span>
|
|
53
|
+
@endif
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Loading state -->
|
|
57
|
+
@if(loading)
|
|
58
|
+
<div class="p-4">
|
|
59
|
+
@each(i in [1, 2, 3, 4, 5])
|
|
60
|
+
<div class="flex justify-between py-3">
|
|
61
|
+
<div class="h-4 w-3/5 bg-gray-200 rounded animate-pulse" />
|
|
62
|
+
<div class="h-4 w-12 bg-gray-200 rounded animate-pulse" />
|
|
63
|
+
</div>
|
|
64
|
+
@endeach
|
|
65
|
+
</div>
|
|
66
|
+
@elseif(elements.length === 0)
|
|
67
|
+
<!-- Empty state -->
|
|
68
|
+
<div class="flex flex-col items-center py-8 text-gray-500">
|
|
69
|
+
<Icon name="mouse-pointer" size="40" class="mb-3" />
|
|
70
|
+
<p>No click data available</p>
|
|
71
|
+
</div>
|
|
72
|
+
@else
|
|
73
|
+
<!-- Element list -->
|
|
74
|
+
<div class="text-sm">
|
|
75
|
+
<div class="flex justify-between px-4 py-2 text-xs font-medium text-gray-500 uppercase tracking-wider bg-gray-50 border-b border-gray-200">
|
|
76
|
+
<span>Element</span>
|
|
77
|
+
<span>Clicks</span>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
@each(element, index in displayElements)
|
|
81
|
+
<div
|
|
82
|
+
class="flex justify-between items-center px-4 py-3 cursor-pointer hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0"
|
|
83
|
+
@mouseenter="handleMouseEnter('{{ element.selector }}')"
|
|
84
|
+
@mouseleave="handleMouseLeave"
|
|
85
|
+
@click="handleClick(element)"
|
|
86
|
+
>
|
|
87
|
+
<div class="flex items-start gap-3 flex-1 min-w-0">
|
|
88
|
+
<span class="flex-shrink-0 w-6 h-6 flex items-center justify-center text-xs font-semibold text-gray-500 bg-gray-100 rounded">
|
|
89
|
+
{{ index + 1 }}
|
|
90
|
+
</span>
|
|
91
|
+
<div class="flex flex-col gap-0.5 min-w-0">
|
|
92
|
+
<span class="text-[10px] font-semibold text-indigo-600 uppercase tracking-wider">
|
|
93
|
+
{{ element.tag }}
|
|
94
|
+
</span>
|
|
95
|
+
<span class="text-xs text-gray-700 font-mono truncate" title="{{ element.selector }}">
|
|
96
|
+
{{ formatSelector(element.selector) }}
|
|
97
|
+
</span>
|
|
98
|
+
@if(element.text)
|
|
99
|
+
<span class="text-xs text-gray-400 truncate max-w-[200px]">
|
|
100
|
+
"{{ element.text }}"
|
|
101
|
+
</span>
|
|
102
|
+
@endif
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="flex items-center gap-2 flex-shrink-0 w-28">
|
|
106
|
+
<span class="font-semibold text-gray-900 w-10 text-right">{{ formatCount(element.count) }}</span>
|
|
107
|
+
<div class="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
|
108
|
+
<div
|
|
109
|
+
class="h-full bg-indigo-600 rounded-full transition-all duration-300"
|
|
110
|
+
style="width: {{ getPercentage(element.count) }}%"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
<span class="text-xs text-gray-500 w-8 text-right">{{ getPercentage(element.count) }}%</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
@endeach
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
@if(elements.length > maxItems)
|
|
120
|
+
<div class="py-3 text-center text-xs text-gray-500 bg-gray-50 border-t border-gray-200">
|
|
121
|
+
{{ elements.length - maxItems }} more elements
|
|
122
|
+
</div>
|
|
123
|
+
@endif
|
|
124
|
+
@endif
|
|
125
|
+
</div>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* HeatmapControls Component
|
|
4
|
+
*
|
|
5
|
+
* Filter controls for heatmap visualization: date range, device type, view mode.
|
|
6
|
+
*/
|
|
7
|
+
export const viewMode = $props.viewMode || 'click'
|
|
8
|
+
export const deviceFilter = $props.deviceFilter || 'all'
|
|
9
|
+
export const startDate = $props.startDate || ''
|
|
10
|
+
export const endDate = $props.endDate || ''
|
|
11
|
+
export const loading = $props.loading || false
|
|
12
|
+
export const onViewModeChange = $props.onViewModeChange
|
|
13
|
+
export const onDeviceChange = $props.onDeviceChange
|
|
14
|
+
export const onDateRangeChange = $props.onDateRangeChange
|
|
15
|
+
export const onRefresh = $props.onRefresh
|
|
16
|
+
|
|
17
|
+
const viewModes = [
|
|
18
|
+
{ value: 'click', label: 'Clicks' },
|
|
19
|
+
{ value: 'scroll', label: 'Scroll' },
|
|
20
|
+
{ value: 'movement', label: 'Movement' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const deviceFilters = [
|
|
24
|
+
{ value: 'all', label: 'All Devices' },
|
|
25
|
+
{ value: 'desktop', label: 'Desktop' },
|
|
26
|
+
{ value: 'mobile', label: 'Mobile' },
|
|
27
|
+
{ value: 'tablet', label: 'Tablet' },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export function handleViewMode(mode) {
|
|
31
|
+
onViewModeChange?.(mode)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function handleDevice(e) {
|
|
35
|
+
onDeviceChange?.(e.target.value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function handleDateChange(type, e) {
|
|
39
|
+
const value = e.target.value
|
|
40
|
+
if (type === 'start') {
|
|
41
|
+
onDateRangeChange?.({ start: new Date(value), end: new Date(endDate) })
|
|
42
|
+
} else {
|
|
43
|
+
onDateRangeChange?.({ start: new Date(startDate), end: new Date(value) })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function handleRefresh() {
|
|
48
|
+
onRefresh?.()
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<div class="heatmap-controls flex flex-wrap items-end gap-4 p-4 bg-white rounded-lg border border-gray-200">
|
|
53
|
+
<!-- View Mode Toggle -->
|
|
54
|
+
<div class="flex flex-col gap-1">
|
|
55
|
+
<label class="text-xs font-medium text-gray-500 uppercase tracking-wider">View</label>
|
|
56
|
+
<div class="flex gap-1 bg-gray-100 p-1 rounded-lg">
|
|
57
|
+
@each(mode in viewModes)
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
class="flex items-center gap-1.5 px-3 py-2 text-sm rounded-md transition-all {{ viewMode === mode.value ? 'text-indigo-600 bg-white shadow-sm' : 'text-gray-500 hover:text-gray-700' }}"
|
|
61
|
+
@if(loading)disabled@endif
|
|
62
|
+
@click="handleViewMode('{{ mode.value }}')"
|
|
63
|
+
>
|
|
64
|
+
@if(mode.value === 'click')
|
|
65
|
+
<Icon name="mouse-pointer" size="16" />
|
|
66
|
+
@elseif(mode.value === 'scroll')
|
|
67
|
+
<Icon name="arrow-up-down" size="16" />
|
|
68
|
+
@else
|
|
69
|
+
<Icon name="trending-up" size="16" />
|
|
70
|
+
@endif
|
|
71
|
+
<span>{{ mode.label }}</span>
|
|
72
|
+
</button>
|
|
73
|
+
@endeach
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Device Filter -->
|
|
78
|
+
<div class="flex flex-col gap-1">
|
|
79
|
+
<label class="text-xs font-medium text-gray-500 uppercase tracking-wider" for="device-filter">Device</label>
|
|
80
|
+
<select
|
|
81
|
+
id="device-filter"
|
|
82
|
+
class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
83
|
+
@if(loading)disabled@endif
|
|
84
|
+
@change="handleDevice"
|
|
85
|
+
>
|
|
86
|
+
@each(device in deviceFilters)
|
|
87
|
+
<option value="{{ device.value }}" @if(deviceFilter === device.value)selected@endif>
|
|
88
|
+
{{ device.label }}
|
|
89
|
+
</option>
|
|
90
|
+
@endeach
|
|
91
|
+
</select>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Date Range -->
|
|
95
|
+
<div class="flex flex-col gap-1">
|
|
96
|
+
<label class="text-xs font-medium text-gray-500 uppercase tracking-wider">Date Range</label>
|
|
97
|
+
<div class="flex items-center gap-2">
|
|
98
|
+
<input
|
|
99
|
+
type="date"
|
|
100
|
+
value="{{ startDate }}"
|
|
101
|
+
class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
102
|
+
@if(loading)disabled@endif
|
|
103
|
+
@change="handleDateChange('start', event)"
|
|
104
|
+
/>
|
|
105
|
+
<span class="text-sm text-gray-400">to</span>
|
|
106
|
+
<input
|
|
107
|
+
type="date"
|
|
108
|
+
value="{{ endDate }}"
|
|
109
|
+
class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
110
|
+
@if(loading)disabled@endif
|
|
111
|
+
@change="handleDateChange('end', event)"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Refresh Button -->
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
class="flex items-center justify-center w-10 h-10 text-gray-500 bg-gray-100 rounded-lg hover:bg-gray-200 hover:text-gray-700 transition-all"
|
|
120
|
+
@if(loading)disabled@endif
|
|
121
|
+
@click="handleRefresh"
|
|
122
|
+
>
|
|
123
|
+
<Icon name="refresh-cw" size="16" class="{{ loading ? 'animate-spin' : '' }}" />
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* HeatmapLegend Component
|
|
4
|
+
*
|
|
5
|
+
* Displays a color scale legend for heatmap visualization.
|
|
6
|
+
*/
|
|
7
|
+
export const colorStart = $props.colorStart || 'rgba(79, 70, 229, 0.1)'
|
|
8
|
+
export const colorEnd = $props.colorEnd || 'rgba(79, 70, 229, 1)'
|
|
9
|
+
export const minLabel = $props.minLabel || 'Less'
|
|
10
|
+
export const maxLabel = $props.maxLabel || 'More'
|
|
11
|
+
export const showValues = $props.showValues || false
|
|
12
|
+
export const minValue = $props.minValue || 0
|
|
13
|
+
export const maxValue = $props.maxValue || 100
|
|
14
|
+
|
|
15
|
+
// Generate gradient stops
|
|
16
|
+
const gradientStops = [0, 0.25, 0.5, 0.75, 1]
|
|
17
|
+
|
|
18
|
+
function interpolateColor(ratio) {
|
|
19
|
+
const startMatch = colorStart.match(/[\d.]+/g)
|
|
20
|
+
const endMatch = colorEnd.match(/[\d.]+/g)
|
|
21
|
+
|
|
22
|
+
if (!startMatch || !endMatch) return colorStart
|
|
23
|
+
|
|
24
|
+
const [sr, sg, sb, sa] = startMatch.map(Number)
|
|
25
|
+
const [er, eg, eb, ea] = endMatch.map(Number)
|
|
26
|
+
|
|
27
|
+
const r = Math.round(sr + (er - sr) * ratio)
|
|
28
|
+
const g = Math.round(sg + (eg - sg) * ratio)
|
|
29
|
+
const b = Math.round(sb + (eb - sb) * ratio)
|
|
30
|
+
const a = (sa + (ea - sa) * ratio).toFixed(2)
|
|
31
|
+
|
|
32
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatValue(ratio) {
|
|
36
|
+
const value = minValue + (maxValue - minValue) * ratio
|
|
37
|
+
if (value >= 1000) {
|
|
38
|
+
return `${(value / 1000).toFixed(1)}k`
|
|
39
|
+
}
|
|
40
|
+
return Math.round(value).toString()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const stops = gradientStops.map(stop => ({
|
|
44
|
+
ratio: stop,
|
|
45
|
+
color: interpolateColor(stop),
|
|
46
|
+
value: formatValue(stop)
|
|
47
|
+
}))
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class="heatmap-legend flex items-center gap-2 text-xs">
|
|
51
|
+
<span class="text-gray-500">{{ minLabel }}</span>
|
|
52
|
+
<div class="flex gap-0.5">
|
|
53
|
+
@each(stop in stops)
|
|
54
|
+
<div
|
|
55
|
+
class="w-4 h-4 rounded-sm"
|
|
56
|
+
style="background-color: {{ stop.color }}"
|
|
57
|
+
@if(showValues)title="{{ stop.value }}"@endif
|
|
58
|
+
/>
|
|
59
|
+
@endeach
|
|
60
|
+
</div>
|
|
61
|
+
<span class="text-gray-500">{{ maxLabel }}</span>
|
|
62
|
+
|
|
63
|
+
@if(showValues)
|
|
64
|
+
<div class="flex justify-between mt-1 text-[10px] text-gray-400">
|
|
65
|
+
<span>{{ formatValue(0) }}</span>
|
|
66
|
+
<span>{{ formatValue(1) }}</span>
|
|
67
|
+
</div>
|
|
68
|
+
@endif
|
|
69
|
+
</div>
|