@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,455 @@
1
+ <script server>
2
+ /**
3
+ * FullAnalyticsDashboard Component
4
+ *
5
+ * A comprehensive analytics dashboard with tabbed navigation
6
+ * for Overview, Audience, Behavior, and Conversions views.
7
+ */
8
+ import { defineProps, withDefaults } from 'stx'
9
+ import type {
10
+ AnalyticsApiConfig,
11
+ DashboardSummary,
12
+ GoalConversion,
13
+ TimeSeriesDataPoint,
14
+ TopItem,
15
+ } from '../types'
16
+
17
+ type TabId = 'overview' | 'audience' | 'behavior' | 'conversions'
18
+
19
+ interface Tab {
20
+ id: TabId
21
+ label: string
22
+ icon: string
23
+ }
24
+
25
+ interface FullAnalyticsDashboardProps {
26
+ config: AnalyticsApiConfig
27
+ refreshInterval?: number
28
+ realtimeInterval?: number
29
+ defaultTab?: TabId
30
+ stats?: DashboardSummary | null
31
+ timeSeries?: TimeSeriesDataPoint[]
32
+ topPages?: TopItem[]
33
+ topReferrers?: TopItem[]
34
+ devices?: TopItem[]
35
+ browsers?: TopItem[]
36
+ countries?: TopItem[]
37
+ goals?: GoalConversion[]
38
+ realtimeVisitors?: number
39
+ realtimePageViews?: number
40
+ loading?: boolean
41
+ error?: string | null
42
+ }
43
+
44
+ const {
45
+ config,
46
+ refreshInterval,
47
+ realtimeInterval,
48
+ defaultTab,
49
+ stats,
50
+ timeSeries,
51
+ topPages,
52
+ topReferrers,
53
+ devices,
54
+ browsers,
55
+ countries,
56
+ goals,
57
+ realtimeVisitors,
58
+ realtimePageViews,
59
+ loading,
60
+ error,
61
+ } = withDefaults(
62
+ defineProps<FullAnalyticsDashboardProps>(),
63
+ {
64
+ refreshInterval: 60000,
65
+ realtimeInterval: 5000,
66
+ defaultTab: 'overview',
67
+ stats: null,
68
+ timeSeries: () => [],
69
+ topPages: () => [],
70
+ topReferrers: () => [],
71
+ devices: () => [],
72
+ browsers: () => [],
73
+ countries: () => [],
74
+ goals: () => [],
75
+ realtimeVisitors: 0,
76
+ realtimePageViews: 0,
77
+ loading: false,
78
+ error: null,
79
+ }
80
+ )
81
+
82
+ const tabs: Tab[] = [
83
+ { id: 'overview', label: 'Overview', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
84
+ { id: 'audience', label: 'Audience', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z' },
85
+ { id: 'behavior', label: 'Behavior', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
86
+ { id: 'conversions', label: 'Conversions', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
87
+ ]
88
+
89
+ const totalConversions = goals.reduce((sum, g) => sum + g.conversions, 0)
90
+ const avgConversionRate = goals.length ? goals.reduce((sum, g) => sum + g.conversionRate, 0) / goals.length : 0
91
+ const totalRevenue = goals.reduce((sum, g) => sum + g.revenue, 0)
92
+ </script>
93
+
94
+ <script client>
95
+ const activeTab = state('{{ defaultTab }}')
96
+ const isLoading = state({{ loading ? 'true' : 'false' }})
97
+
98
+ function refresh() {
99
+ isLoading.set(true)
100
+ document.dispatchEvent(new CustomEvent('refresh'))
101
+ }
102
+ </script>
103
+ <div class="full-analytics-dashboard min-h-screen bg-gray-50">
104
+ <header class="bg-white border-b border-gray-200 sticky top-0 z-20">
105
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
106
+ <div class="flex items-center justify-between h-16">
107
+ <div>
108
+ <h1 class="text-xl font-semibold text-gray-900">Analytics</h1>
109
+ </div>
110
+
111
+ <div class="flex items-center gap-4">
112
+ @if (realtimeVisitors > 0)
113
+ <div class="flex items-center gap-2 px-3 py-1.5 bg-green-50 rounded-full">
114
+ <div class="relative">
115
+ <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
116
+ <div class="absolute inset-0 w-2 h-2 rounded-full bg-green-500 animate-ping"></div>
117
+ </div>
118
+ <span class="text-sm font-medium text-green-700">
119
+ {{ realtimeVisitors }} online
120
+ </span>
121
+ </div>
122
+ @endif
123
+
124
+ <select
125
+ class="bg-white border border-gray-200 text-gray-900 px-4 py-2 rounded-lg text-sm cursor-pointer hover:border-indigo-500"
126
+ @change="document.dispatchEvent(new CustomEvent('date-change', { detail: { range: event.target.value } }))"
127
+ >
128
+ <option value="7d">Last 7 days</option>
129
+ <option value="30d">Last 30 days</option>
130
+ <option value="90d">Last 90 days</option>
131
+ </select>
132
+
133
+ <button
134
+ type="button"
135
+ class="btn-icon p-2 rounded-lg hover:bg-gray-100"
136
+ title="Refresh"
137
+ x-disabled="isLoading"
138
+ @click="refresh()"
139
+ >
140
+ <Icon name="refresh-cw" size="20" x-class="{ 'animate-spin': isLoading() }" />
141
+ </button>
142
+ </div>
143
+ </div>
144
+
145
+ <nav class="flex gap-6 -mb-px">
146
+ @foreach (tabs as tab)
147
+ <button
148
+ type="button"
149
+ class="flex items-center gap-2 px-1 py-4 text-sm font-medium border-b-2 transition-colors"
150
+ x-class="activeTab() === '{{ tab.id }}'
151
+ ? 'border-indigo-500 text-indigo-600'
152
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
153
+ @click="activeTab.set('{{ tab.id }}')"
154
+ >
155
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ tab.icon }}" />
157
+ </svg>
158
+ {{ tab.label }}
159
+ </button>
160
+ @endforeach
161
+ </nav>
162
+ </div>
163
+ </header>
164
+
165
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
166
+ @if (error)
167
+ <div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
168
+ <p class="font-medium">Error loading analytics</p>
169
+ <p class="text-sm mt-1">{{ error }}</p>
170
+ <button type="button" class="mt-2 text-sm text-red-600 hover:text-red-800 underline" @click="refresh()">
171
+ Try again
172
+ </button>
173
+ </div>
174
+ @endif
175
+
176
+ <!-- Overview Tab -->
177
+ <div :show="activeTab() === 'overview'" class="space-y-6">
178
+ <section class="dashboard-grid">
179
+ <div class="stat-card card-hover p-6">
180
+ <h3 class="text-sm font-medium text-gray-500">Page Views</h3>
181
+ @if (loading)
182
+ <div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
183
+ @else
184
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ stats?.pageViews ?? 0 }}</p>
185
+ @endif
186
+ </div>
187
+ <div class="stat-card card-hover p-6">
188
+ <h3 class="text-sm font-medium text-gray-500">Unique Visitors</h3>
189
+ @if (loading)
190
+ <div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
191
+ @else
192
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ stats?.uniqueVisitors ?? 0 }}</p>
193
+ @endif
194
+ </div>
195
+ <div class="stat-card card-hover p-6">
196
+ <h3 class="text-sm font-medium text-gray-500">Bounce Rate</h3>
197
+ @if (loading)
198
+ <div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
199
+ @else
200
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ ((stats?.bounceRate ?? 0) * 100).toFixed(1) }}%</p>
201
+ @endif
202
+ </div>
203
+ <div class="stat-card card-hover p-6">
204
+ <h3 class="text-sm font-medium text-gray-500">Avg. Session</h3>
205
+ @if (loading)
206
+ <div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
207
+ @else
208
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ Math.floor((stats?.avgSessionDuration ?? 0) / 60) }}m</p>
209
+ @endif
210
+ </div>
211
+ </section>
212
+
213
+ <section class="card-hover p-6">
214
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Traffic Overview</h3>
215
+ @if (loading)
216
+ <div class="animate-pulse"><div class="skeleton h-80 w-full"></div></div>
217
+ @elseif (timeSeries.length === 0)
218
+ <div class="flex items-center justify-center h-80 text-gray-400">No data available</div>
219
+ @else
220
+ <script client>
221
+ onMount(() => {
222
+ const canvas = document.getElementById('overviewChart')
223
+ const ctx = canvas?.getContext('2d')
224
+ if (ctx) {
225
+ const chartData = {{ JSON.stringify(timeSeries) }}
226
+ new Chart(ctx, {
227
+ type: 'line',
228
+ data: {
229
+ labels: chartData.map(d => new Date(d.date).toLocaleDateString()),
230
+ datasets: [{
231
+ label: 'Page Views',
232
+ data: chartData.map(d => d.pageViews || 0),
233
+ borderColor: '#6366f1',
234
+ backgroundColor: '#6366f120',
235
+ fill: true,
236
+ tension: 0.3,
237
+ }]
238
+ },
239
+ options: {
240
+ responsive: true,
241
+ maintainAspectRatio: false,
242
+ plugins: { legend: { display: false } },
243
+ scales: { y: { beginAtZero: true } }
244
+ }
245
+ })
246
+ }
247
+ })
248
+ </script>
249
+ <canvas
250
+ id="overviewChart"
251
+ style="height: 320px"
252
+ ></canvas>
253
+ @endif
254
+ </section>
255
+
256
+ <section class="dashboard-grid-3">
257
+ <div class="card-hover p-6">
258
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Top Pages</h3>
259
+ @if (topPages.length === 0)
260
+ <p class="text-gray-500 text-center py-8">No data</p>
261
+ @else
262
+ <div class="space-y-3">
263
+ @foreach (topPages.slice(0, 5) as page)
264
+ <div class="flex justify-between text-sm">
265
+ <span class="text-gray-900 truncate">{{ page.name }}</span>
266
+ <span class="text-gray-500">{{ page.value }}</span>
267
+ </div>
268
+ @endforeach
269
+ </div>
270
+ @endif
271
+ </div>
272
+ <div class="card-hover p-6">
273
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Top Referrers</h3>
274
+ @if (topReferrers.length === 0)
275
+ <p class="text-gray-500 text-center py-8">No data</p>
276
+ @else
277
+ <div class="space-y-3">
278
+ @foreach (topReferrers.slice(0, 5) as referrer)
279
+ <div class="flex justify-between text-sm">
280
+ <span class="text-gray-900 truncate">{{ referrer.name }}</span>
281
+ <span class="text-gray-500">{{ referrer.value }}</span>
282
+ </div>
283
+ @endforeach
284
+ </div>
285
+ @endif
286
+ </div>
287
+ <div class="card-hover p-6">
288
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Devices</h3>
289
+ @if (devices.length === 0)
290
+ <p class="text-gray-500 text-center py-8">No data</p>
291
+ @else
292
+ <div class="space-y-3">
293
+ @foreach (devices as device)
294
+ <div class="flex justify-between text-sm">
295
+ <span class="text-gray-900">{{ device.name }}</span>
296
+ <span class="text-gray-500">{{ (device.percentage * 100).toFixed(1) }}%</span>
297
+ </div>
298
+ @endforeach
299
+ </div>
300
+ @endif
301
+ </div>
302
+ </section>
303
+ </div>
304
+
305
+ <!-- Audience Tab -->
306
+ <div :show="activeTab() === 'audience'" class="space-y-6">
307
+ <section class="card-hover p-6">
308
+ <div class="flex items-center gap-3">
309
+ <div class="relative">
310
+ <div class="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div>
311
+ </div>
312
+ <div>
313
+ <span class="text-4xl font-bold text-gray-900">{{ realtimeVisitors }}</span>
314
+ <span class="ml-2 text-gray-500 text-sm">visitors right now</span>
315
+ </div>
316
+ </div>
317
+ @if (realtimePageViews > 0)
318
+ <p class="mt-2 text-sm text-gray-500">{{ realtimePageViews }} page views in the last hour</p>
319
+ @endif
320
+ </section>
321
+
322
+ <section class="grid grid-cols-1 lg:grid-cols-2 gap-6">
323
+ <div class="card-hover p-6">
324
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Countries</h3>
325
+ @if (countries.length === 0)
326
+ <p class="text-gray-500 text-center py-8">No country data</p>
327
+ @else
328
+ <div class="space-y-3">
329
+ @foreach (countries as country)
330
+ <div class="flex justify-between text-sm">
331
+ <span class="text-gray-900">{{ country.name }}</span>
332
+ <span class="text-gray-500">{{ country.value }}</span>
333
+ </div>
334
+ @endforeach
335
+ </div>
336
+ @endif
337
+ </div>
338
+ <div class="card-hover p-6">
339
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Browsers</h3>
340
+ @if (browsers.length === 0)
341
+ <p class="text-gray-500 text-center py-8">No browser data</p>
342
+ @else
343
+ <div class="space-y-3">
344
+ @foreach (browsers as browser)
345
+ <div class="flex justify-between text-sm">
346
+ <span class="text-gray-900">{{ browser.name }}</span>
347
+ <span class="text-gray-500">{{ (browser.percentage * 100).toFixed(1) }}%</span>
348
+ </div>
349
+ @endforeach
350
+ </div>
351
+ @endif
352
+ </div>
353
+ </section>
354
+ </div>
355
+
356
+ <!-- Behavior Tab -->
357
+ <div :show="activeTab() === 'behavior'" class="space-y-6">
358
+ <section class="grid grid-cols-1 md:grid-cols-3 gap-4">
359
+ <div class="stat-card card-hover p-6">
360
+ <h3 class="text-sm font-medium text-gray-500">Pages / Session</h3>
361
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ (stats?.avgPagesPerSession ?? 0).toFixed(1) }}</p>
362
+ </div>
363
+ <div class="stat-card card-hover p-6">
364
+ <h3 class="text-sm font-medium text-gray-500">Avg. Session Duration</h3>
365
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ Math.floor((stats?.avgSessionDuration ?? 0) / 60) }}m {{ Math.floor((stats?.avgSessionDuration ?? 0) % 60) }}s</p>
366
+ </div>
367
+ <div class="stat-card card-hover p-6">
368
+ <h3 class="text-sm font-medium text-gray-500">Bounce Rate</h3>
369
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ ((stats?.bounceRate ?? 0) * 100).toFixed(1) }}%</p>
370
+ </div>
371
+ </section>
372
+
373
+ <section class="grid grid-cols-1 lg:grid-cols-2 gap-6">
374
+ <div class="card-hover p-6">
375
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Most Viewed Pages</h3>
376
+ @if (topPages.length === 0)
377
+ <p class="text-gray-500 text-center py-8">No page views yet</p>
378
+ @else
379
+ <div class="space-y-3">
380
+ @foreach (topPages as page)
381
+ <div class="flex justify-between text-sm">
382
+ <span class="text-gray-900 truncate">{{ page.name }}</span>
383
+ <span class="text-gray-500">{{ page.value }}</span>
384
+ </div>
385
+ @endforeach
386
+ </div>
387
+ @endif
388
+ </div>
389
+ <div class="card-hover p-6">
390
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Traffic Sources</h3>
391
+ @if (topReferrers.length === 0)
392
+ <p class="text-gray-500 text-center py-8">No referrer data</p>
393
+ @else
394
+ <div class="space-y-3">
395
+ @foreach (topReferrers as referrer)
396
+ <div class="flex justify-between text-sm">
397
+ <span class="text-gray-900 truncate">{{ referrer.name }}</span>
398
+ <span class="text-gray-500">{{ referrer.value }}</span>
399
+ </div>
400
+ @endforeach
401
+ </div>
402
+ @endif
403
+ </div>
404
+ </section>
405
+ </div>
406
+
407
+ <!-- Conversions Tab -->
408
+ <div :show="activeTab() === 'conversions'" class="space-y-6">
409
+ <section class="grid grid-cols-1 md:grid-cols-3 gap-4">
410
+ <div class="stat-card card-hover p-6">
411
+ <h3 class="text-sm font-medium text-gray-500">Total Conversions</h3>
412
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ totalConversions }}</p>
413
+ </div>
414
+ <div class="stat-card card-hover p-6">
415
+ <h3 class="text-sm font-medium text-gray-500">Conversion Rate</h3>
416
+ <p class="text-3xl font-bold text-gray-900 mt-2">{{ (avgConversionRate * 100).toFixed(2) }}%</p>
417
+ </div>
418
+ <div class="stat-card card-hover p-6">
419
+ <h3 class="text-sm font-medium text-gray-500">Total Revenue</h3>
420
+ <p class="text-3xl font-bold text-gray-900 mt-2">${{ totalRevenue.toLocaleString() }}</p>
421
+ </div>
422
+ </section>
423
+
424
+ <section class="card-hover p-6">
425
+ <h3 class="text-sm font-medium text-gray-900 mb-4">Conversion Goals</h3>
426
+ @if (goals.length === 0)
427
+ <div class="text-center py-8 text-gray-500">
428
+ <p>No goals configured</p>
429
+ <p class="text-sm text-gray-400 mt-1">Set up conversion goals to track performance</p>
430
+ </div>
431
+ @else
432
+ <div class="space-y-4">
433
+ @foreach (goals as goal)
434
+ <div class="p-4 bg-gray-50 rounded-lg">
435
+ <div class="flex items-center justify-between mb-2">
436
+ <span class="font-medium text-gray-900">{{ goal.goalName }}</span>
437
+ <span class="text-lg font-semibold text-gray-900">{{ goal.conversions }}</span>
438
+ </div>
439
+ <div class="progress-bar mb-2">
440
+ <div class="progress-fill bg-green-500" style="width: {{ Math.min(goal.conversionRate * 100, 100) }}%"></div>
441
+ </div>
442
+ <div class="flex items-center justify-between text-sm">
443
+ <span class="text-gray-500">{{ (goal.conversionRate * 100).toFixed(2) }}% conversion rate</span>
444
+ @if (goal.revenue > 0)
445
+ <span class="text-green-600 font-medium">${{ goal.revenue.toLocaleString() }}</span>
446
+ @endif
447
+ </div>
448
+ </div>
449
+ @endforeach
450
+ </div>
451
+ @endif
452
+ </section>
453
+ </div>
454
+ </main>
455
+ </div>
@@ -0,0 +1,142 @@
1
+ <script server>
2
+ /**
3
+ * FunnelChart Component
4
+ *
5
+ * Displays conversion funnel stages with drop-off visualization.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import { formatCompact, formatPercentage } from '../utils'
9
+
10
+ export interface FunnelStage {
11
+ name: string
12
+ value: number
13
+ color?: string
14
+ }
15
+
16
+ interface FunnelChartProps {
17
+ stages: FunnelStage[]
18
+ title?: string
19
+ loading?: boolean
20
+ showDropoff?: boolean
21
+ }
22
+
23
+ const { stages, title, loading, showDropoff } = withDefaults(
24
+ defineProps<FunnelChartProps>(),
25
+ {
26
+ loading: false,
27
+ showDropoff: true,
28
+ }
29
+ )
30
+
31
+ const defaultColors = [
32
+ '#6366f1', // indigo
33
+ '#8b5cf6', // purple
34
+ '#a855f7', // violet
35
+ '#d946ef', // fuchsia
36
+ '#ec4899', // pink
37
+ ]
38
+
39
+ const firstValue = stages[0]?.value ?? 1
40
+
41
+ const stagesWithData = stages.map((stage, index) => {
42
+ const prevValue = index > 0 ? stages[index - 1].value : stage.value
43
+ const dropoff = prevValue > 0 ? ((prevValue - stage.value) / prevValue) * 100 : 0
44
+ const conversionFromStart = firstValue > 0 ? (stage.value / firstValue) * 100 : 0
45
+ const widthPercent = firstValue > 0 ? (stage.value / firstValue) * 100 : 0
46
+
47
+ return {
48
+ ...stage,
49
+ color: stage.color || defaultColors[index % defaultColors.length],
50
+ dropoff,
51
+ conversionFromStart,
52
+ widthPercent: Math.max(widthPercent, 20),
53
+ actualWidthPercent: widthPercent,
54
+ }
55
+ })
56
+
57
+ const overallConversion = stages.length > 0 && stages[0].value > 0
58
+ ? (stages[stages.length - 1].value / stages[0].value) * 100
59
+ : 0
60
+ </script>
61
+
62
+ <div class="funnel-chart card-hover p-6">
63
+ @if (title)
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 && stages.length)
67
+ <span class="text-sm text-gray-500">
68
+ {{ formatPercentage(overallConversion / 100) }} overall conversion
69
+ </span>
70
+ @endif
71
+ </div>
72
+ @endif
73
+
74
+ @if (loading)
75
+ <div class="space-y-4 animate-pulse">
76
+ @foreach ([1, 2, 3, 4] as i)
77
+ <div class="flex items-center gap-4">
78
+ <div class="skeleton h-12 flex-1" style="max-width: {{ 100 - i * 15 }}%"></div>
79
+ <div class="skeleton h-4 w-20"></div>
80
+ </div>
81
+ @endforeach
82
+ </div>
83
+ @elseif (!stages.length)
84
+ <div class="text-center py-12 text-gray-500">
85
+ No funnel data available
86
+ </div>
87
+ @else
88
+ <div class="space-y-2">
89
+ @foreach (stagesWithData as stage)
90
+ <div class="relative">
91
+ <div class="flex items-center gap-4">
92
+ <div
93
+ class="h-12 rounded-lg flex items-center justify-center transition-all duration-500"
94
+ style="width: {{ stage.widthPercent }}%; background-color: {{ stage.color }}; min-width: 120px"
95
+ >
96
+ <span class="text-white font-medium text-sm px-3 truncate">
97
+ {{ stage.name }}
98
+ </span>
99
+ </div>
100
+
101
+ <div class="flex items-center gap-3 text-sm">
102
+ <span class="font-semibold text-gray-900">
103
+ {{ formatCompact(stage.value) }}
104
+ </span>
105
+ <span class="text-gray-500">
106
+ ({{ formatPercentage(stage.conversionFromStart / 100) }})
107
+ </span>
108
+ </div>
109
+ </div>
110
+
111
+ @if (showDropoff && stage.dropoff > 0)
112
+ <div class="absolute -top-1 flex items-center gap-1 text-xs text-red-500" style="right: {{ 100 - stage.widthPercent }}%">
113
+ <Icon name="arrow-down" size="12" />
114
+ <span>-{{ stage.dropoff.toFixed(1) }}%</span>
115
+ </div>
116
+ @endif
117
+ </div>
118
+ @endforeach
119
+ </div>
120
+
121
+ @if (stages.length > 1)
122
+ <div class="mt-6 pt-4 border-t border-gray-200">
123
+ <div class="grid grid-cols-3 gap-4 text-center">
124
+ <div>
125
+ <p class="text-2xl font-bold text-gray-900">{{ formatCompact(stages[0].value) }}</p>
126
+ <p class="text-xs text-gray-500">Started</p>
127
+ </div>
128
+ <div>
129
+ <p class="text-2xl font-bold text-gray-900">{{ formatCompact(stages[stages.length - 1].value) }}</p>
130
+ <p class="text-xs text-gray-500">Completed</p>
131
+ </div>
132
+ <div>
133
+ <p class="text-2xl font-bold {{ overallConversion >= 10 ? 'text-green-600' : 'text-amber-600' }}">
134
+ {{ formatPercentage(overallConversion / 100) }}
135
+ </p>
136
+ <p class="text-xs text-gray-500">Conversion</p>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ @endif
141
+ @endif
142
+ </div>