@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.
Files changed (237) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +361 -0
  4. package/bin/cli.ts +169 -0
  5. package/dist/Analytics.d.ts +558 -0
  6. package/dist/api.d.ts +109 -0
  7. package/dist/batching.d.ts +93 -0
  8. package/dist/chunk-2mx7fq49.js +4 -0
  9. package/dist/chunk-3z29508k.js +204 -0
  10. package/dist/chunk-deephkz6.js +56 -0
  11. package/dist/chunk-j261vgyp.js +305 -0
  12. package/dist/chunk-xga17tz7.js +207 -0
  13. package/dist/config.d.ts +132 -0
  14. package/dist/dashboard/components/index.d.ts +80 -0
  15. package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
  16. package/dist/dashboard/index.d.ts +110 -0
  17. package/dist/dashboard/types/index.d.ts +144 -0
  18. package/dist/dashboard/utils/index.d.ts +81 -0
  19. package/dist/dynamodb.d.ts +86 -0
  20. package/dist/funnels.d.ts +104 -0
  21. package/dist/geolocation.d.ts +96 -0
  22. package/dist/index.d.ts +371 -0
  23. package/dist/index.js +10211 -0
  24. package/dist/infrastructure/cdk.d.ts +60 -0
  25. package/dist/infrastructure/cloudformation.d.ts +44 -0
  26. package/dist/infrastructure/index.d.ts +45 -0
  27. package/dist/infrastructure/setup.d.ts +133 -0
  28. package/dist/integrations/cloudflare.d.ts +76 -0
  29. package/dist/integrations/hono.d.ts +60 -0
  30. package/dist/integrations/index.d.ts +24 -0
  31. package/dist/integrations/nuxt.d.ts +7 -0
  32. package/dist/integrations/nuxt.js +42 -0
  33. package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
  34. package/dist/integrations/runtime/use-ts-analytics.js +17 -0
  35. package/dist/integrations/stx.d.ts +88 -0
  36. package/dist/integrations/stx.js +18 -0
  37. package/dist/lib/crypto-random.d.ts +4 -0
  38. package/dist/lib/salt.d.ts +16 -0
  39. package/dist/local.d.ts +56 -0
  40. package/dist/model-connector.d.ts +145 -0
  41. package/dist/models/AggregatedStats.d.ts +9 -0
  42. package/dist/models/CampaignStats.d.ts +9 -0
  43. package/dist/models/Conversion.d.ts +11 -0
  44. package/dist/models/CustomEvent.d.ts +11 -0
  45. package/dist/models/DeviceStats.d.ts +9 -0
  46. package/dist/models/EventStats.d.ts +9 -0
  47. package/dist/models/GeoStats.d.ts +9 -0
  48. package/dist/models/Goal.d.ts +9 -0
  49. package/dist/models/GoalStats.d.ts +9 -0
  50. package/dist/models/PageStats.d.ts +11 -0
  51. package/dist/models/PageView.d.ts +13 -0
  52. package/dist/models/RealtimeStats.d.ts +9 -0
  53. package/dist/models/ReferrerStats.d.ts +9 -0
  54. package/dist/models/Session.d.ts +11 -0
  55. package/dist/models/Site.d.ts +11 -0
  56. package/dist/models/index.d.ts +28 -0
  57. package/dist/models/types.d.ts +60 -0
  58. package/dist/sqs-buffering.d.ts +243 -0
  59. package/dist/stacks-integration.d.ts +159 -0
  60. package/dist/tracking-script.d.ts +71 -0
  61. package/dist/tracking.d.ts +17 -0
  62. package/dist/tracking.js +26 -0
  63. package/dist/types.d.ts +595 -0
  64. package/dist/utils/geolocation.d.ts +111 -0
  65. package/dist/utils/user-agent.d.ts +17 -0
  66. package/dist/version.d.ts +7 -0
  67. package/package.json +119 -0
  68. package/src/Analytics.ts +3349 -0
  69. package/src/api.ts +1286 -0
  70. package/src/assets/crosswind.css +2220 -0
  71. package/src/batching.ts +452 -0
  72. package/src/components/dashboard/index.ts +18 -0
  73. package/src/config.ts +456 -0
  74. package/src/dashboard/Dashboard.stx +1517 -0
  75. package/src/dashboard/components/AlertCard.stx +177 -0
  76. package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
  77. package/src/dashboard/components/AnimatedNumber.stx +86 -0
  78. package/src/dashboard/components/BarChart.stx +220 -0
  79. package/src/dashboard/components/BrowserBreakdown.stx +98 -0
  80. package/src/dashboard/components/BrowsersTable.stx +125 -0
  81. package/src/dashboard/components/CampaignBreakdown.stx +163 -0
  82. package/src/dashboard/components/CampaignTable.stx +238 -0
  83. package/src/dashboard/components/CountryList.stx +101 -0
  84. package/src/dashboard/components/DataTable.stx +226 -0
  85. package/src/dashboard/components/DateRangePicker.stx +77 -0
  86. package/src/dashboard/components/DeviceBreakdown.stx +94 -0
  87. package/src/dashboard/components/DevicesTable.stx +163 -0
  88. package/src/dashboard/components/DonutChart.stories.ts +55 -0
  89. package/src/dashboard/components/DonutChart.stx +157 -0
  90. package/src/dashboard/components/EmptyState.stx +90 -0
  91. package/src/dashboard/components/EngagementMetrics.stx +183 -0
  92. package/src/dashboard/components/EventsSection.stx +192 -0
  93. package/src/dashboard/components/FilterBar.stx +104 -0
  94. package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
  95. package/src/dashboard/components/FunnelChart.stx +142 -0
  96. package/src/dashboard/components/GeoTable.stx +337 -0
  97. package/src/dashboard/components/GoalsPanel.stx +109 -0
  98. package/src/dashboard/components/Header.stx +39 -0
  99. package/src/dashboard/components/HeatmapChart.stx +140 -0
  100. package/src/dashboard/components/LiveActivityFeed.stx +172 -0
  101. package/src/dashboard/components/MetricComparison.stx +106 -0
  102. package/src/dashboard/components/MiniStats.stx +96 -0
  103. package/src/dashboard/components/OSBreakdown.stx +124 -0
  104. package/src/dashboard/components/PageDetailCard.stx +102 -0
  105. package/src/dashboard/components/PagesTable.stx +127 -0
  106. package/src/dashboard/components/ProgressRing.stx +89 -0
  107. package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
  108. package/src/dashboard/components/RealtimeCounter.stx +46 -0
  109. package/src/dashboard/components/ReferrersTable.stx +180 -0
  110. package/src/dashboard/components/SparklineChart.stx +160 -0
  111. package/src/dashboard/components/StatCard.stories.ts +58 -0
  112. package/src/dashboard/components/StatCard.stx +90 -0
  113. package/src/dashboard/components/SummaryStats.stx +81 -0
  114. package/src/dashboard/components/TabNav.stx +66 -0
  115. package/src/dashboard/components/ThemeSwitcher.stx +124 -0
  116. package/src/dashboard/components/TimeSeriesChart.stx +106 -0
  117. package/src/dashboard/components/TopList.stories.ts +58 -0
  118. package/src/dashboard/components/TopList.stx +74 -0
  119. package/src/dashboard/components/TrendIndicator.stx +84 -0
  120. package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
  121. package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
  122. package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
  123. package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
  124. package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
  125. package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
  126. package/src/dashboard/components/heatmap/index.ts +12 -0
  127. package/src/dashboard/components/index.ts +86 -0
  128. package/src/dashboard/composables/useAnalytics.ts +465 -0
  129. package/src/dashboard/demo/DemoApp.stx +370 -0
  130. package/src/dashboard/demo/index.ts +8 -0
  131. package/src/dashboard/demo/mockData.ts +234 -0
  132. package/src/dashboard/index.ts +117 -0
  133. package/src/dashboard/stx-shim.d.ts +4 -0
  134. package/src/dashboard/types/index.ts +203 -0
  135. package/src/dashboard/utils/index.ts +426 -0
  136. package/src/dynamodb.ts +344 -0
  137. package/src/funnels.ts +534 -0
  138. package/src/geolocation.ts +515 -0
  139. package/src/handlers/alerts.ts +328 -0
  140. package/src/handlers/annotations.ts +128 -0
  141. package/src/handlers/api-keys.ts +240 -0
  142. package/src/handlers/auth.ts +1020 -0
  143. package/src/handlers/authz.ts +137 -0
  144. package/src/handlers/collect.ts +724 -0
  145. package/src/handlers/data.ts +625 -0
  146. package/src/handlers/experiments.ts +138 -0
  147. package/src/handlers/funnels.ts +216 -0
  148. package/src/handlers/goals.ts +218 -0
  149. package/src/handlers/heatmaps.ts +272 -0
  150. package/src/handlers/index.ts +56 -0
  151. package/src/handlers/lib/read-cache.ts +63 -0
  152. package/src/handlers/misc.ts +486 -0
  153. package/src/handlers/oauth.ts +233 -0
  154. package/src/handlers/performance.ts +388 -0
  155. package/src/handlers/sessions.ts +413 -0
  156. package/src/handlers/sharing.ts +198 -0
  157. package/src/handlers/stats.ts +1368 -0
  158. package/src/handlers/team.ts +161 -0
  159. package/src/handlers/uptime.ts +283 -0
  160. package/src/handlers/views.ts +390 -0
  161. package/src/handlers/webhooks.ts +226 -0
  162. package/src/heatmap/index.ts +32 -0
  163. package/src/heatmap/tracking-script.ts +452 -0
  164. package/src/heatmap/types.ts +79 -0
  165. package/src/index.ts +387 -0
  166. package/src/infrastructure/cdk.ts +496 -0
  167. package/src/infrastructure/cloudformation.ts +595 -0
  168. package/src/infrastructure/index.ts +48 -0
  169. package/src/infrastructure/setup.ts +611 -0
  170. package/src/integrations/cloudflare.ts +732 -0
  171. package/src/integrations/hono.ts +589 -0
  172. package/src/integrations/index.ts +27 -0
  173. package/src/integrations/nuxt.ts +78 -0
  174. package/src/integrations/runtime/use-ts-analytics.ts +32 -0
  175. package/src/integrations/stx.ts +138 -0
  176. package/src/jobs/index.ts +127 -0
  177. package/src/lib/crypto-random.ts +41 -0
  178. package/src/lib/ddb-errors.ts +20 -0
  179. package/src/lib/dynamodb.ts +216 -0
  180. package/src/lib/email.ts +38 -0
  181. package/src/lib/ga-import.ts +471 -0
  182. package/src/lib/ga4-api.ts +244 -0
  183. package/src/lib/goals.ts +205 -0
  184. package/src/lib/index.ts +6 -0
  185. package/src/lib/ingest-counters.ts +123 -0
  186. package/src/lib/log.ts +36 -0
  187. package/src/lib/membership.ts +98 -0
  188. package/src/lib/plans.ts +90 -0
  189. package/src/lib/quota.ts +46 -0
  190. package/src/lib/rate-limit.ts +27 -0
  191. package/src/lib/rollups.ts +472 -0
  192. package/src/lib/salt.ts +97 -0
  193. package/src/lib/scheduler.ts +97 -0
  194. package/src/lib/significance.ts +66 -0
  195. package/src/lib/site-retention.ts +56 -0
  196. package/src/local.ts +360 -0
  197. package/src/model-connector.ts +616 -0
  198. package/src/models/AggregatedStats.ts +162 -0
  199. package/src/models/CampaignStats.ts +141 -0
  200. package/src/models/Conversion.ts +135 -0
  201. package/src/models/CustomEvent.ts +123 -0
  202. package/src/models/DeviceStats.ts +105 -0
  203. package/src/models/EventStats.ts +108 -0
  204. package/src/models/GeoStats.ts +112 -0
  205. package/src/models/Goal.ts +105 -0
  206. package/src/models/GoalStats.ts +106 -0
  207. package/src/models/PageStats.ts +152 -0
  208. package/src/models/PageView.ts +277 -0
  209. package/src/models/RealtimeStats.ts +96 -0
  210. package/src/models/ReferrerStats.ts +115 -0
  211. package/src/models/Session.ts +235 -0
  212. package/src/models/Site.ts +115 -0
  213. package/src/models/index.ts +50 -0
  214. package/src/models/orm/index.ts +2043 -0
  215. package/src/models/types.ts +71 -0
  216. package/src/router.ts +521 -0
  217. package/src/sqs-buffering.ts +806 -0
  218. package/src/stacks-integration.ts +527 -0
  219. package/src/tracking-script.ts +944 -0
  220. package/src/tracking.ts +27 -0
  221. package/src/types/analytics.ts +219 -0
  222. package/src/types/api.ts +75 -0
  223. package/src/types/bun-router.d.ts +15 -0
  224. package/src/types/dashboard.ts +62 -0
  225. package/src/types/index.ts +66 -0
  226. package/src/types/stx.d.ts +27 -0
  227. package/src/types/window.d.ts +101 -0
  228. package/src/types.ts +911 -0
  229. package/src/utils/cache.ts +148 -0
  230. package/src/utils/date.ts +118 -0
  231. package/src/utils/filters.ts +71 -0
  232. package/src/utils/geolocation.ts +323 -0
  233. package/src/utils/index.ts +9 -0
  234. package/src/utils/response.ts +180 -0
  235. package/src/utils/timezone-country.ts +242 -0
  236. package/src/utils/user-agent.ts +110 -0
  237. package/src/version.ts +7 -0
@@ -0,0 +1,220 @@
1
+ <script server>
2
+ /**
3
+ * BarChart Component
4
+ *
5
+ * Horizontal or vertical bar chart for comparing values.
6
+ * Uses Canvas for rendering with Alpine.js for initialization.
7
+ */
8
+ import { defineProps, withDefaults } from 'stx'
9
+ import type { DashboardTheme } from '../types'
10
+ import { defaultTheme } from '../types'
11
+ import { formatCompact } from '../utils'
12
+
13
+ export interface BarChartItem {
14
+ label: string
15
+ value: number
16
+ color?: string
17
+ previousValue?: number
18
+ }
19
+
20
+ interface BarChartProps {
21
+ data: BarChartItem[]
22
+ title?: string
23
+ height?: number
24
+ orientation?: 'horizontal' | 'vertical'
25
+ showValues?: boolean
26
+ showComparison?: boolean
27
+ loading?: boolean
28
+ theme?: DashboardTheme
29
+ }
30
+
31
+ const { data, title, height, orientation, showValues, showComparison, loading, theme } = withDefaults(
32
+ defineProps<BarChartProps>(),
33
+ {
34
+ height: 300,
35
+ orientation: 'horizontal',
36
+ showValues: true,
37
+ showComparison: false,
38
+ loading: false,
39
+ theme: () => defaultTheme,
40
+ }
41
+ )
42
+
43
+ const defaultColors = [
44
+ '#6366f1',
45
+ '#22c55e',
46
+ '#f59e0b',
47
+ '#ef4444',
48
+ '#8b5cf6',
49
+ '#06b6d4',
50
+ ]
51
+
52
+ const maxValue = Math.max(...data.flatMap(d => [d.value, d.previousValue ?? 0]), 1)
53
+
54
+ const chartData = JSON.stringify(data)
55
+ const chartTheme = JSON.stringify(theme)
56
+ </script>
57
+
58
+ <div class="bar-chart card-hover p-6">
59
+ @if (title)
60
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
61
+ {{ title }}
62
+ </h3>
63
+ @endif
64
+
65
+ @if (loading)
66
+ <div class="animate-pulse" style="height: {{ height }}px">
67
+ <div class="skeleton h-full"></div>
68
+ </div>
69
+ @elseif (!data.length)
70
+ <div
71
+ class="flex items-center justify-center text-gray-500"
72
+ style="height: {{ height }}px"
73
+ >
74
+ No data available
75
+ </div>
76
+ @else
77
+ <script client>
78
+ const barChartData = {{ chartData }}
79
+ const barChartTheme = {{ chartTheme }}
80
+ const barChartMaxValue = {{ maxValue }}
81
+ const barChartOrientation = '{{ orientation }}'
82
+ const barChartShowValues = {{ showValues ? 'true' : 'false' }}
83
+ const barChartShowComparison = {{ showComparison ? 'true' : 'false' }}
84
+ const barChartDefaultColors = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
85
+
86
+ function formatCompactClient(val) {
87
+ if (val >= 1e6) return (val / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
88
+ if (val >= 1e3) return (val / 1e3).toFixed(1).replace(/\.0$/, '') + 'k'
89
+ return val.toLocaleString()
90
+ }
91
+
92
+ function drawHorizontalBars(ctx, width, height) {
93
+ const padding = { left: 100, right: 60, top: 20, bottom: 20 }
94
+ const chartWidth = width - padding.left - padding.right
95
+ const barH = barChartShowComparison ? 16 : 24
96
+ const barGap = barChartShowComparison ? 4 : 0
97
+ const groupHeight = barH * (barChartShowComparison ? 2 : 1) + barGap
98
+ const groupGap = 16
99
+
100
+ barChartData.forEach((item, index) => {
101
+ const y = padding.top + index * (groupHeight + groupGap)
102
+ const barWidth = (item.value / barChartMaxValue) * chartWidth
103
+ const color = item.color || barChartDefaultColors[index % barChartDefaultColors.length]
104
+
105
+ ctx.fillStyle = barChartTheme.text
106
+ ctx.font = '13px system-ui, sans-serif'
107
+ ctx.textAlign = 'right'
108
+ ctx.textBaseline = 'middle'
109
+ ctx.fillText(item.label, padding.left - 12, y + groupHeight / 2)
110
+
111
+ ctx.fillStyle = color
112
+ ctx.beginPath()
113
+ ctx.roundRect(padding.left, y, barWidth, barH, 4)
114
+ ctx.fill()
115
+
116
+ if (barChartShowComparison && item.previousValue !== undefined) {
117
+ const prevBarWidth = (item.previousValue / barChartMaxValue) * chartWidth
118
+ ctx.fillStyle = color + '60'
119
+ ctx.beginPath()
120
+ ctx.roundRect(padding.left, y + barH + barGap, prevBarWidth, barH, 4)
121
+ ctx.fill()
122
+ }
123
+
124
+ if (barChartShowValues) {
125
+ ctx.fillStyle = barChartTheme.textSecondary
126
+ ctx.font = '12px system-ui, sans-serif'
127
+ ctx.textAlign = 'left'
128
+ ctx.fillText(formatCompactClient(item.value), padding.left + barWidth + 8, y + barH / 2)
129
+ }
130
+ })
131
+ }
132
+
133
+ function drawVerticalBars(ctx, width, height) {
134
+ const padding = { left: 50, right: 20, top: 20, bottom: 60 }
135
+ const chartWidth = width - padding.left - padding.right
136
+ const chartHeight = height - padding.top - padding.bottom
137
+ const barW = barChartShowComparison ? 20 : 32
138
+ const barGap = barChartShowComparison ? 4 : 0
139
+ const groupWidth = barW * (barChartShowComparison ? 2 : 1) + barGap
140
+ const groupGap = (chartWidth - groupWidth * barChartData.length) / (barChartData.length + 1)
141
+
142
+ ctx.strokeStyle = barChartTheme.border
143
+ ctx.lineWidth = 1
144
+ ctx.beginPath()
145
+ ctx.moveTo(padding.left, padding.top)
146
+ ctx.lineTo(padding.left, height - padding.bottom)
147
+ ctx.stroke()
148
+
149
+ barChartData.forEach((item, index) => {
150
+ const x = padding.left + groupGap + index * (groupWidth + groupGap)
151
+ const barHeight = (item.value / barChartMaxValue) * chartHeight
152
+ const color = item.color || barChartDefaultColors[index % barChartDefaultColors.length]
153
+
154
+ ctx.fillStyle = color
155
+ ctx.beginPath()
156
+ ctx.roundRect(x, height - padding.bottom - barHeight, barW, barHeight, [4, 4, 0, 0])
157
+ ctx.fill()
158
+
159
+ ctx.fillStyle = barChartTheme.textSecondary
160
+ ctx.font = '12px system-ui, sans-serif'
161
+ ctx.textAlign = 'center'
162
+ ctx.fillText(item.label, x + groupWidth / 2, height - padding.bottom + 16)
163
+
164
+ if (barChartShowValues) {
165
+ ctx.fillStyle = barChartTheme.text
166
+ ctx.fillText(formatCompactClient(item.value), x + barW / 2, height - padding.bottom - barHeight - 8)
167
+ }
168
+ })
169
+ }
170
+
171
+ function drawBarChart() {
172
+ const canvas = document.getElementById('barChartCanvas')
173
+ const ctx = canvas?.getContext('2d')
174
+ if (!ctx) return
175
+
176
+ const dpr = window.devicePixelRatio || 1
177
+ const rect = canvas.getBoundingClientRect()
178
+
179
+ canvas.width = rect.width * dpr
180
+ canvas.height = rect.height * dpr
181
+ ctx.scale(dpr, dpr)
182
+
183
+ const width = rect.width
184
+ const height = rect.height
185
+ ctx.clearRect(0, 0, width, height)
186
+
187
+ if (barChartOrientation === 'horizontal') {
188
+ drawHorizontalBars(ctx, width, height)
189
+ } else {
190
+ drawVerticalBars(ctx, width, height)
191
+ }
192
+ }
193
+
194
+ onMount(() => {
195
+ drawBarChart()
196
+ const canvas = document.getElementById('barChartCanvas')
197
+ if (canvas?.parentElement) {
198
+ new ResizeObserver(() => drawBarChart()).observe(canvas.parentElement)
199
+ }
200
+ })
201
+ </script>
202
+ <canvas
203
+ id="barChartCanvas"
204
+ style="width: 100%; height: {{ height }}px"
205
+ ></canvas>
206
+
207
+ @if (showComparison && !loading && data.length)
208
+ <div class="flex items-center justify-end gap-4 mt-4 text-sm">
209
+ <div class="flex items-center gap-2">
210
+ <div class="w-3 h-3 rounded bg-primary-500"></div>
211
+ <span class="text-gray-600">Current period</span>
212
+ </div>
213
+ <div class="flex items-center gap-2">
214
+ <div class="w-3 h-3 rounded bg-primary-500/40"></div>
215
+ <span class="text-gray-600">Previous period</span>
216
+ </div>
217
+ </div>
218
+ @endif
219
+ @endif
220
+ </div>
@@ -0,0 +1,98 @@
1
+ <script server>
2
+ /**
3
+ * BrowserBreakdown Component
4
+ *
5
+ * Displays browser usage breakdown with icons.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { TopItem } from '../types'
9
+ import { formatPercentage } from '../utils'
10
+
11
+ interface BrowserBreakdownProps {
12
+ browsers: TopItem[]
13
+ loading?: boolean
14
+ }
15
+
16
+ const { browsers, loading } = withDefaults(
17
+ defineProps<BrowserBreakdownProps>(),
18
+ {
19
+ loading: false,
20
+ }
21
+ )
22
+
23
+ const browserIcons: Record<string, string> = {
24
+ chrome: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C8.21 0 4.831 1.757 2.632 4.501l3.953 6.848A5.454 5.454 0 0 1 12 6.545h10.691A12 12 0 0 0 12 0zM1.931 5.47A11.943 11.943 0 0 0 0 12c0 6.012 4.42 10.991 10.189 11.864l3.953-6.847a5.45 5.45 0 0 1-6.865-2.29zm13.342 2.166a5.446 5.446 0 0 1 1.45 7.09l.002.001h-.002l-3.952 6.848a12.014 12.014 0 0 0 9.229-9.006zM12 16.364a4.364 4.364 0 1 1 0-8.728 4.364 4.364 0 0 1 0 8.728z"/></svg>`,
25
+ firefox: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M8.824 7.287c.008 0 .004 0 0 0zm-2.8-1.4c.006 0 .003 0 0 0zm16.754 2.161c-.505-1.215-1.53-2.528-2.333-2.943.654 1.283 1.033 2.57 1.177 3.53l.002.02c-1.314-3.278-3.544-4.6-5.366-7.477-.091-.147-.184-.292-.273-.446a3.545 3.545 0 0 1-.13-.24 2.118 2.118 0 0 1-.172-.46.03.03 0 0 0-.027-.03.038.038 0 0 0-.021 0l-.006.001-.006.001-.008.001c-2.044 1.193-2.734 3.393-2.8 4.494a3.946 3.946 0 0 0-2.18.825 2.364 2.364 0 0 0-.203-.148 3.687 3.687 0 0 1-.07-1.959 5.706 5.706 0 0 0-1.89 1.458 3.95 3.95 0 0 0-.67-.063c-.014 0-.027-.001-.04-.001l-.062-.02a3.26 3.26 0 0 1-.07-.672c0-.453.057-.888.164-1.296l-.005-.006c-.396.224-.75.504-1.048.828-.327-.29-.617-.633-.864-1.025a6.485 6.485 0 0 0-.578-.265c-.081-.033-.166-.066-.254-.1-.1-.04-.201-.078-.306-.117-.1-.038-.204-.076-.309-.113l-.103-.037c-.101-.037-.204-.074-.309-.11C3.22 23.007.213 17.37 1.635 11.17c.336-1.504.855-2.726 1.616-3.744.247.392.537.735.864 1.025a5.02 5.02 0 0 1-.123 1.403 6.2 6.2 0 0 1 1.636-1.48c-.127.662-.046 1.351.233 1.96.08.175.175.341.284.496a2.384 2.384 0 0 1 .66-.617 2.11 2.11 0 0 1-.142-.686c-.007-.153.003-.306.028-.458.015-.1.037-.197.065-.293.055-.186.137-.362.243-.523.098-.148.212-.286.34-.41a5.44 5.44 0 0 1 .775-.612c.297-.196.623-.35.969-.46a3.949 3.949 0 0 1 1.14-.17h.031a3.94 3.94 0 0 1 1.14.169c.346.11.672.264.969.46.263.173.513.377.775.612.128.124.242.262.34.41.106.161.188.337.243.523.028.096.05.193.065.293.025.152.035.305.028.458a2.11 2.11 0 0 1-.142.686 2.384 2.384 0 0 1 .66.617c.109-.155.204-.321.284-.496.279-.609.36-1.298.233-1.96a6.2 6.2 0 0 1 1.636 1.48 5.02 5.02 0 0 1-.123-1.403c.327-.29.617-.633.864-1.025z"/></svg>`,
26
+ safari: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 24C5.373 24 0 18.627 0 12S5.373 0 12 0s12 5.373 12 12-5.373 12-12 12zm0-2c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm-1.276-4.768l-.246-.612 5.852-9.606.474.114-5.852 9.606-.228.498z"/></svg>`,
27
+ edge: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M21.86 17.86q.14 0 .25.12.1.13.1.25t-.11.33l-.32.46-.43.53-.44.5q-.21.25-.38.42l-.22.23q-.58.53-1.34 1.04-.76.51-1.6.91-.86.4-1.74.64t-1.67.24q-.9 0-1.69-.28-.8-.28-1.48-.78-.68-.5-1.22-1.17-.53-.66-.92-1.44-.38-.77-.6-1.63-.2-.85-.2-1.73 0-1.17.27-2.26.28-1.1.8-2.08.51-.98 1.23-1.82.71-.83 1.61-1.48.9-.65 1.94-1.06 1.05-.41 2.2-.54l.14-.02q.14 0 .27.07t.2.2q.08.13.08.28 0 .15-.08.28-.14.22-.34.42-.2.2-.42.39-.22.18-.44.35l-.42.3q-.2.14-.37.25-.17.12-.28.19l-.14.09q-.12.07-.27.13-.14.06-.3.1-.16.05-.34.07-.18.02-.37.02-1.02 0-1.87.44-.84.43-1.45 1.15-.6.72-.95 1.66-.34.94-.34 1.99 0 1.24.4 2.26.4 1.02 1.07 1.75.68.73 1.55 1.13.88.4 1.82.4 1.15 0 2.14-.38 1-.39 1.83-.99.84-.6 1.5-1.32t1.13-1.41q.09-.13.21-.18.12-.05.25-.05z"/></svg>`,
28
+ opera: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M8.51 7.04C9.44 5.57 10.67 4.62 12 4.62s2.56.95 3.49 2.42c.66.97 1.16 2.15 1.48 3.48.27 1.15.42 2.42.42 3.76s-.15 2.61-.42 3.76c-.32 1.33-.82 2.51-1.48 3.48-.93 1.47-2.16 2.42-3.49 2.42s-2.56-.95-3.49-2.42c-.66-.97-1.16-2.15-1.48-3.48-.27-1.15-.42-2.42-.42-3.76s.15-2.61.42-3.76c.32-1.33.82-2.51 1.48-3.48zM12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0z"/></svg>`,
29
+ other: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"/></svg>`,
30
+ }
31
+
32
+ const browserColors: Record<string, string> = {
33
+ chrome: 'bg-yellow-100 text-yellow-600',
34
+ firefox: 'bg-orange-100 text-orange-600',
35
+ safari: 'bg-blue-100 text-blue-600',
36
+ edge: 'bg-cyan-100 text-cyan-600',
37
+ opera: 'bg-red-100 text-red-600',
38
+ other: 'bg-gray-100 text-gray-600',
39
+ }
40
+
41
+ const sortedBrowsers = [...browsers].sort((a, b) => b.value - a.value)
42
+
43
+ function getBrowserIcon(name: string): string {
44
+ const key = name.toLowerCase()
45
+ return browserIcons[key] || browserIcons.other
46
+ }
47
+
48
+ function getBrowserColor(name: string): string {
49
+ const key = name.toLowerCase()
50
+ return browserColors[key] || browserColors.other
51
+ }
52
+ </script>
53
+
54
+ <div class="browser-breakdown card-hover p-6">
55
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
56
+ Browsers
57
+ </h3>
58
+
59
+ @if (loading)
60
+ <div class="space-y-4">
61
+ @foreach ([1, 2, 3, 4] as i)
62
+ <div class="animate-pulse flex items-center gap-4">
63
+ <div class="skeleton w-10 h-10 rounded-lg"></div>
64
+ <div class="flex-1">
65
+ <div class="skeleton h-4 w-20 mb-2"></div>
66
+ <div class="skeleton h-2 w-full"></div>
67
+ </div>
68
+ </div>
69
+ @endforeach
70
+ </div>
71
+ @elseif (!browsers.length)
72
+ <div class="text-center py-8 text-gray-500">
73
+ No browser data available
74
+ </div>
75
+ @else
76
+ <div class="space-y-4">
77
+ @foreach (sortedBrowsers as browser)
78
+ <div class="flex items-center gap-4">
79
+ <div class="w-10 h-10 rounded-lg flex items-center justify-center {{ getBrowserColor(browser.name) }}">
80
+ {!! getBrowserIcon(browser.name) !!}
81
+ </div>
82
+ <div class="flex-1">
83
+ <div class="flex items-center justify-between">
84
+ <span class="font-medium text-gray-900 capitalize">{{ browser.name }}</span>
85
+ <span class="text-sm text-gray-600">{{ formatPercentage(browser.percentage) }}</span>
86
+ </div>
87
+ <div class="progress-bar mt-1">
88
+ <div
89
+ class="progress-fill bg-gray-400"
90
+ style="width: {{ browser.percentage * 100 }}%"
91
+ ></div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ @endforeach
96
+ </div>
97
+ @endif
98
+ </div>
@@ -0,0 +1,125 @@
1
+ {{-- BrowsersTable Component --}}
2
+ {{-- Props: browsers (array of {name, visitors}) --}}
3
+ <div class="data-panel">
4
+ <div class="panel-header">
5
+ <span class="panel-title">Browsers</span>
6
+ </div>
7
+ <table class="data-table">
8
+ <thead>
9
+ <tr>
10
+ <th>Browser</th>
11
+ <th class="text-right">Visitors</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ @forelse(browsers as browser)
16
+ <tr>
17
+ <td class="name">
18
+ <span class="browser-icon" data-browser="{{ browser.name.toLowerCase() }}"></span>
19
+ {{ browser.name }}
20
+ </td>
21
+ <td class="value">{{ browser.visitors | fmt }}</td>
22
+ </tr>
23
+ @empty
24
+ <tr>
25
+ <td colspan="2" class="empty-cell">No browser data</td>
26
+ </tr>
27
+ @endforelse
28
+ </tbody>
29
+ </table>
30
+ </div>
31
+
32
+
33
+ <style>
34
+ .data-panel {
35
+ background: var(--bg-secondary);
36
+ border-radius: 8px;
37
+ padding: 1rem;
38
+ }
39
+
40
+ .panel-header {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 0.5rem;
44
+ margin-bottom: 1rem;
45
+ }
46
+
47
+ .panel-title {
48
+ font-size: 0.875rem;
49
+ font-weight: 500;
50
+ color: var(--text-primary);
51
+ }
52
+
53
+ .data-table {
54
+ width: 100%;
55
+ font-size: 0.8125rem;
56
+ border-collapse: collapse;
57
+ }
58
+
59
+ .data-table th {
60
+ text-align: left;
61
+ color: var(--text-muted);
62
+ font-weight: 500;
63
+ padding: 0.5rem 0;
64
+ border-bottom: 1px solid var(--border);
65
+ font-size: 0.6875rem;
66
+ text-transform: uppercase;
67
+ letter-spacing: 0.05em;
68
+ }
69
+
70
+ .data-table th.text-right {
71
+ text-align: right;
72
+ }
73
+
74
+ .data-table td {
75
+ padding: 0.625rem 0;
76
+ border-bottom: 1px solid var(--bg-tertiary);
77
+ }
78
+
79
+ .data-table tr:last-child td {
80
+ border-bottom: none;
81
+ }
82
+
83
+ .data-table .name {
84
+ color: var(--text-primary);
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 0.5rem;
88
+ }
89
+
90
+ .browser-icon {
91
+ width: 16px;
92
+ height: 16px;
93
+ background-size: contain;
94
+ background-repeat: no-repeat;
95
+ background-position: center;
96
+ border-radius: 2px;
97
+ }
98
+
99
+ .browser-icon[data-browser="chrome"] {
100
+ background-color: #4285F4;
101
+ }
102
+
103
+ .browser-icon[data-browser="firefox"] {
104
+ background-color: #FF7139;
105
+ }
106
+
107
+ .browser-icon[data-browser="safari"] {
108
+ background-color: #006CFF;
109
+ }
110
+
111
+ .browser-icon[data-browser="edge"] {
112
+ background-color: #0078D7;
113
+ }
114
+
115
+ .data-table .value {
116
+ color: var(--text-secondary);
117
+ text-align: right;
118
+ }
119
+
120
+ .empty-cell {
121
+ text-align: center;
122
+ color: var(--text-muted);
123
+ padding: 1.5rem 0;
124
+ }
125
+ </style>
@@ -0,0 +1,163 @@
1
+ <script server>
2
+ /**
3
+ * CampaignBreakdown Component
4
+ *
5
+ * Displays UTM campaign data with source, medium, and campaign breakdown.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import { formatCompact, formatPercentage } from '../utils'
9
+
10
+ export interface CampaignData {
11
+ name: string
12
+ source?: string
13
+ medium?: string
14
+ visitors: number
15
+ conversions: number
16
+ conversionRate: number
17
+ revenue?: number
18
+ }
19
+
20
+ interface CampaignBreakdownProps {
21
+ campaigns: CampaignData[]
22
+ loading?: boolean
23
+ title?: string
24
+ maxItems?: number
25
+ }
26
+
27
+ const { campaigns, loading, title, maxItems } = withDefaults(
28
+ defineProps<CampaignBreakdownProps>(),
29
+ {
30
+ loading: false,
31
+ title: 'Campaigns',
32
+ maxItems: 10,
33
+ }
34
+ )
35
+
36
+ const sortedCampaigns = [...campaigns]
37
+ .sort((a, b) => b.visitors - a.visitors)
38
+ .slice(0, maxItems)
39
+
40
+ const totals = {
41
+ visitors: campaigns.reduce((sum, c) => sum + c.visitors, 0),
42
+ conversions: campaigns.reduce((sum, c) => sum + c.conversions, 0),
43
+ revenue: campaigns.reduce((sum, c) => sum + (c.revenue ?? 0), 0),
44
+ }
45
+
46
+ const sourceIcons: Record<string, string> = {
47
+ google: 'M12.545 10.239v3.821h5.445c-.712 2.315-2.647 3.972-5.445 3.972a6.033 6.033 0 110-12.064c1.498 0 2.866.549 3.921 1.453l2.814-2.814A9.969 9.969 0 0012.545 2C7.021 2 2.543 6.477 2.543 12s4.478 10 10.002 10c8.396 0 10.249-7.85 9.426-11.748l-9.426-.013z',
48
+ facebook: 'M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z',
49
+ twitter: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
50
+ linkedin: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
51
+ email: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
52
+ direct: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1',
53
+ }
54
+
55
+ function getSourceIcon(source?: string): string {
56
+ if (!source)
57
+ return sourceIcons.direct
58
+ const key = source.toLowerCase()
59
+ return sourceIcons[key] || sourceIcons.direct
60
+ }
61
+ </script>
62
+
63
+ <div class="campaign-breakdown card-hover p-6">
64
+ <div class="flex items-center justify-between mb-4">
65
+ <h3 class="text-sm font-medium text-gray-900">{{ title }}</h3>
66
+ @if (!loading && campaigns.length)
67
+ <span class="text-xs text-gray-500">
68
+ {{ campaigns.length }} campaigns
69
+ </span>
70
+ @endif
71
+ </div>
72
+
73
+ @if (loading)
74
+ <div class="space-y-3 animate-pulse">
75
+ <div class="skeleton h-10 rounded"></div>
76
+ @foreach ([1, 2, 3, 4, 5] as i)
77
+ <div class="skeleton h-14 rounded"></div>
78
+ @endforeach
79
+ </div>
80
+ @elseif (!campaigns.length)
81
+ <div class="text-center py-8 text-gray-500">
82
+ <Icon name="pie-chart" size="48" class="mx-auto text-gray-300 mb-3" />
83
+ <p>No campaign data available</p>
84
+ </div>
85
+ @else
86
+ <script client>
87
+ const sortBy = state('visitors')
88
+ const sortOrder = state('desc')
89
+ </script>
90
+ <div class="overflow-x-auto">
91
+ <table class="w-full">
92
+ <thead>
93
+ <tr class="border-b border-gray-200">
94
+ <th class="text-left text-xs font-medium text-gray-500 uppercase py-2">Campaign</th>
95
+ <th
96
+ class="text-right text-xs font-medium text-gray-500 uppercase py-2 cursor-pointer hover:text-gray-700"
97
+ @click="sortBy.set('visitors'); sortOrder.set(sortOrder() === 'desc' ? 'asc' : 'desc')"
98
+ >
99
+ <span class="inline-flex items-center gap-1">
100
+ Visitors
101
+ <svg :show="sortBy() === 'visitors'" class="w-3 h-3" x-class="{ 'rotate-180': sortOrder() === 'asc' }" fill="currentColor" viewBox="0 0 20 20">
102
+ <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
103
+ </svg>
104
+ </span>
105
+ </th>
106
+ <th class="text-right text-xs font-medium text-gray-500 uppercase py-2 cursor-pointer hover:text-gray-700">
107
+ Conv.
108
+ </th>
109
+ <th class="text-right text-xs font-medium text-gray-500 uppercase py-2 cursor-pointer hover:text-gray-700">
110
+ Rate
111
+ </th>
112
+ </tr>
113
+ </thead>
114
+ <tbody>
115
+ @foreach (sortedCampaigns as campaign)
116
+ <tr class="border-b border-gray-100 hover:bg-gray-50">
117
+ <td class="py-3">
118
+ <div class="flex items-center gap-3">
119
+ <div class="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500">
120
+ <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
121
+ <path d="{{ getSourceIcon(campaign.source) }}" />
122
+ </svg>
123
+ </div>
124
+ <div>
125
+ <div class="text-sm font-medium text-gray-900 truncate max-w-48">
126
+ {{ campaign.name }}
127
+ </div>
128
+ @if (campaign.source || campaign.medium)
129
+ <div class="text-xs text-gray-500">
130
+ {{ [campaign.source, campaign.medium].filter(Boolean).join(' / ') }}
131
+ </div>
132
+ @endif
133
+ </div>
134
+ </div>
135
+ </td>
136
+ <td class="text-right text-sm tabular-nums py-3">
137
+ {{ formatCompact(campaign.visitors) }}
138
+ </td>
139
+ <td class="text-right text-sm tabular-nums py-3">
140
+ {{ formatCompact(campaign.conversions) }}
141
+ </td>
142
+ <td class="text-right text-sm tabular-nums py-3">
143
+ <span class="{{ campaign.conversionRate >= 0.03 ? 'text-green-600' : 'text-gray-600' }}">
144
+ {{ formatPercentage(campaign.conversionRate) }}
145
+ </span>
146
+ </td>
147
+ </tr>
148
+ @endforeach
149
+ </tbody>
150
+ <tfoot>
151
+ <tr class="border-t border-gray-200 font-medium">
152
+ <td class="py-3 text-sm text-gray-900">Total</td>
153
+ <td class="text-right text-sm tabular-nums py-3">{{ formatCompact(totals.visitors) }}</td>
154
+ <td class="text-right text-sm tabular-nums py-3">{{ formatCompact(totals.conversions) }}</td>
155
+ <td class="text-right text-sm tabular-nums py-3">
156
+ {{ formatPercentage(totals.visitors > 0 ? totals.conversions / totals.visitors : 0) }}
157
+ </td>
158
+ </tr>
159
+ </tfoot>
160
+ </table>
161
+ </div>
162
+ @endif
163
+ </div>