@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,337 @@
1
+ {{-- GeoTable Component --}}
2
+ {{-- Props: countries, regions, cities, activeTab, onFetchRegions, onFetchCities --}}
3
+ <div class="data-panel">
4
+ <div class="tabs">
5
+ <button class="tab-btn @if(activeTab === 'countries') active @endif" data-tab="countries" type="button" @click="handleTabChange('countries')">
6
+ Countries
7
+ </button>
8
+ <button class="tab-btn @if(activeTab === 'regions') active @endif" data-tab="regions" type="button" @click="handleTabChange('regions')">
9
+ Regions
10
+ @if(selectedCountry)
11
+ <span class="filter-badge">{{ selectedCountry }}</span>
12
+ @endif
13
+ </button>
14
+ <button class="tab-btn @if(activeTab === 'cities') active @endif" data-tab="cities" type="button" @click="handleTabChange('cities')">
15
+ Cities
16
+ @if(selectedRegion)
17
+ <span class="filter-badge">{{ selectedRegion }}</span>
18
+ @endif
19
+ </button>
20
+ </div>
21
+
22
+ {{-- Breadcrumb navigation for drill-down --}}
23
+ @if(selectedCountry || selectedRegion)
24
+ <div class="breadcrumb">
25
+ <button class="breadcrumb-btn" type="button" @click="clearFilters">All</button>
26
+ @if(selectedCountry)
27
+ <span class="breadcrumb-sep">/</span>
28
+ <button class="breadcrumb-btn @if(!selectedRegion) active @endif" type="button" @click="clearRegionFilter">{{ selectedCountry }}</button>
29
+ @endif
30
+ @if(selectedRegion)
31
+ <span class="breadcrumb-sep">/</span>
32
+ <span class="breadcrumb-current">{{ selectedRegion }}</span>
33
+ @endif
34
+ </div>
35
+ @endif
36
+
37
+ <div class="tab-content" data-content="countries" @if(activeTab !== 'countries') style="display:none" @endif>
38
+ <table class="data-table">
39
+ <thead>
40
+ <tr>
41
+ <th>Country</th>
42
+ <th class="text-right">Visitors</th>
43
+ </tr>
44
+ </thead>
45
+ <tbody>
46
+ @forelse(countries as country)
47
+ <tr class="clickable-row" @click="drillDownToRegions(country)">
48
+ <td class="name">
49
+ <span class="flag" data-country="{{ country.code }}">{{ country.flag }}</span>
50
+ {{ country.name }}
51
+ <span class="drill-icon">&#8250;</span>
52
+ </td>
53
+ <td class="value">{{ country.visitors | fmt }}</td>
54
+ </tr>
55
+ @empty
56
+ <tr>
57
+ <td colspan="2" class="empty-cell">No country data</td>
58
+ </tr>
59
+ @endforelse
60
+ </tbody>
61
+ </table>
62
+ </div>
63
+
64
+ <div class="tab-content" data-content="regions" @if(activeTab !== 'regions') style="display:none" @endif>
65
+ <table class="data-table">
66
+ <thead>
67
+ <tr>
68
+ <th>Region</th>
69
+ <th class="text-right">Visitors</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ @forelse(regions as region)
74
+ <tr class="clickable-row" @click="drillDownToCities(region)">
75
+ <td class="name">
76
+ {{ region.name }}
77
+ <span class="drill-icon">&#8250;</span>
78
+ </td>
79
+ <td class="value">{{ region.visitors | fmt }}</td>
80
+ </tr>
81
+ @empty
82
+ <tr>
83
+ <td colspan="2" class="empty-cell">
84
+ @if(selectedCountry)
85
+ No region data for {{ selectedCountry }}
86
+ @else
87
+ Select a country to view regions
88
+ @endif
89
+ </td>
90
+ </tr>
91
+ @endforelse
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+
96
+ <div class="tab-content" data-content="cities" @if(activeTab !== 'cities') style="display:none" @endif>
97
+ <table class="data-table">
98
+ <thead>
99
+ <tr>
100
+ <th>City</th>
101
+ <th class="text-right">Visitors</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ @forelse(cities as city)
106
+ <tr>
107
+ <td class="name">{{ city.name }}</td>
108
+ <td class="value">{{ city.visitors | fmt }}</td>
109
+ </tr>
110
+ @empty
111
+ <tr>
112
+ <td colspan="2" class="empty-cell">
113
+ @if(selectedRegion)
114
+ No city data for {{ selectedRegion }}
115
+ @elseif(selectedCountry)
116
+ Select a region to view cities
117
+ @else
118
+ Select a country and region to view cities
119
+ @endif
120
+ </td>
121
+ </tr>
122
+ @endforelse
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ </div>
127
+
128
+ <script server>
129
+ import { defineProps } from 'stx'
130
+
131
+ interface GeoTableProps {
132
+ countries: Array<{ name: string, code: string, flag?: string, visitors: number }>
133
+ regions: Array<{ name: string, country: string, visitors: number }>
134
+ cities: Array<{ name: string, region: string, country: string, visitors: number }>
135
+ activeTab: 'countries' | 'regions' | 'cities'
136
+ onFetchRegions?: (country: string) => void
137
+ onFetchCities?: (country: string, region: string) => void
138
+ onTabChange?: (tab: 'countries' | 'regions' | 'cities') => void
139
+ }
140
+
141
+ const props = defineProps<GeoTableProps>()
142
+
143
+ let selectedCountry: string | null = null
144
+ let selectedRegion: string | null = null
145
+
146
+ function handleTabChange(tab: 'countries' | 'regions' | 'cities') {
147
+ props.onTabChange?.(tab)
148
+ }
149
+
150
+ function drillDownToRegions(country: { name: string, code: string }) {
151
+ selectedCountry = country.name
152
+ selectedRegion = null
153
+ props.onFetchRegions?.(country.code)
154
+ props.onTabChange?.('regions')
155
+ }
156
+
157
+ function drillDownToCities(region: { name: string, country: string }) {
158
+ selectedRegion = region.name
159
+ // Use the country code from the selected country context
160
+ const countryCode = props.countries.find(c => c.name === selectedCountry)?.code || selectedCountry || ''
161
+ props.onFetchCities?.(countryCode, region.name)
162
+ props.onTabChange?.('cities')
163
+ }
164
+
165
+ function clearFilters() {
166
+ selectedCountry = null
167
+ selectedRegion = null
168
+ props.onTabChange?.('countries')
169
+ }
170
+
171
+ function clearRegionFilter() {
172
+ selectedRegion = null
173
+ if (selectedCountry) {
174
+ const countryCode = props.countries.find(c => c.name === selectedCountry)?.code || selectedCountry
175
+ props.onFetchRegions?.(countryCode)
176
+ }
177
+ props.onTabChange?.('regions')
178
+ }
179
+ </script>
180
+
181
+ <style>
182
+ .data-panel {
183
+ background: var(--bg-secondary);
184
+ border-radius: 8px;
185
+ padding: 1rem;
186
+ }
187
+
188
+ .tabs {
189
+ display: flex;
190
+ gap: 0.25rem;
191
+ border-bottom: 1px solid var(--border);
192
+ margin-bottom: 1rem;
193
+ }
194
+
195
+ .tab-btn {
196
+ background: transparent;
197
+ border: none;
198
+ color: var(--text-secondary);
199
+ padding: 0.5rem 0.75rem;
200
+ font-size: 0.75rem;
201
+ font-weight: 500;
202
+ cursor: pointer;
203
+ border-bottom: 2px solid transparent;
204
+ transition: all 0.15s;
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 0.375rem;
208
+ }
209
+
210
+ .tab-btn:hover {
211
+ color: var(--text-primary);
212
+ }
213
+
214
+ .tab-btn.active {
215
+ color: var(--accent);
216
+ border-bottom-color: var(--accent);
217
+ }
218
+
219
+ .filter-badge {
220
+ background: var(--accent);
221
+ color: white;
222
+ font-size: 0.625rem;
223
+ padding: 0.125rem 0.375rem;
224
+ border-radius: 9999px;
225
+ font-weight: 600;
226
+ }
227
+
228
+ .breadcrumb {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 0.25rem;
232
+ padding: 0.5rem 0;
233
+ margin-bottom: 0.5rem;
234
+ font-size: 0.75rem;
235
+ }
236
+
237
+ .breadcrumb-btn {
238
+ background: transparent;
239
+ border: none;
240
+ color: var(--accent);
241
+ cursor: pointer;
242
+ padding: 0.125rem 0.25rem;
243
+ border-radius: 4px;
244
+ transition: background 0.15s;
245
+ }
246
+
247
+ .breadcrumb-btn:hover {
248
+ background: var(--bg-tertiary);
249
+ }
250
+
251
+ .breadcrumb-btn.active {
252
+ color: var(--text-primary);
253
+ font-weight: 500;
254
+ }
255
+
256
+ .breadcrumb-sep {
257
+ color: var(--text-muted);
258
+ }
259
+
260
+ .breadcrumb-current {
261
+ color: var(--text-primary);
262
+ font-weight: 500;
263
+ }
264
+
265
+ .data-table {
266
+ width: 100%;
267
+ font-size: 0.8125rem;
268
+ border-collapse: collapse;
269
+ }
270
+
271
+ .data-table th {
272
+ text-align: left;
273
+ color: var(--text-muted);
274
+ font-weight: 500;
275
+ padding: 0.5rem 0;
276
+ border-bottom: 1px solid var(--border);
277
+ font-size: 0.6875rem;
278
+ text-transform: uppercase;
279
+ letter-spacing: 0.05em;
280
+ }
281
+
282
+ .data-table th.text-right {
283
+ text-align: right;
284
+ }
285
+
286
+ .data-table td {
287
+ padding: 0.625rem 0;
288
+ border-bottom: 1px solid var(--bg-tertiary);
289
+ }
290
+
291
+ .data-table tr:last-child td {
292
+ border-bottom: none;
293
+ }
294
+
295
+ .data-table .name {
296
+ color: var(--text-primary);
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 0.5rem;
300
+ }
301
+
302
+ .clickable-row {
303
+ cursor: pointer;
304
+ transition: background 0.15s;
305
+ }
306
+
307
+ .clickable-row:hover {
308
+ background: var(--bg-tertiary);
309
+ }
310
+
311
+ .clickable-row:hover .drill-icon {
312
+ opacity: 1;
313
+ }
314
+
315
+ .drill-icon {
316
+ margin-left: auto;
317
+ color: var(--text-muted);
318
+ opacity: 0;
319
+ transition: opacity 0.15s;
320
+ font-size: 1rem;
321
+ }
322
+
323
+ .flag {
324
+ font-size: 1rem;
325
+ }
326
+
327
+ .data-table .value {
328
+ color: var(--text-secondary);
329
+ text-align: right;
330
+ }
331
+
332
+ .empty-cell {
333
+ text-align: center;
334
+ color: var(--text-muted);
335
+ padding: 1.5rem 0;
336
+ }
337
+ </style>
@@ -0,0 +1,109 @@
1
+ <script server>
2
+ /**
3
+ * GoalsPanel Component
4
+ *
5
+ * Displays conversion goals with progress and revenue.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { GoalConversion } from '../types'
9
+ import { formatCompact, formatCurrency, formatPercentage } from '../utils'
10
+
11
+ interface GoalsPanelProps {
12
+ goals: GoalConversion[]
13
+ loading?: boolean
14
+ title?: string
15
+ }
16
+
17
+ const { goals, loading, title } = withDefaults(
18
+ defineProps<GoalsPanelProps>(),
19
+ {
20
+ loading: false,
21
+ title: 'Conversion Goals',
22
+ }
23
+ )
24
+
25
+ const sortedGoals = [...goals].sort((a, b) => b.conversions - a.conversions)
26
+
27
+ const totalConversions = goals.reduce((sum, g) => sum + g.conversions, 0)
28
+
29
+ const totalRevenue = goals.reduce((sum, g) => sum + g.revenue, 0)
30
+ </script>
31
+
32
+ <div class="goals-panel card-hover p-6">
33
+ <div class="flex items-center justify-between mb-4">
34
+ <h3 class="text-sm font-medium text-gray-900">
35
+ {{ title }}
36
+ </h3>
37
+ @if (!loading && goals.length)
38
+ <div class="text-sm text-gray-500">
39
+ {{ formatCompact(totalConversions) }} total
40
+ </div>
41
+ @endif
42
+ </div>
43
+
44
+ @if (loading)
45
+ <div class="space-y-4">
46
+ @foreach ([1, 2, 3] as i)
47
+ <div class="animate-pulse">
48
+ <div class="flex items-center justify-between mb-2">
49
+ <div class="skeleton h-5 w-32"></div>
50
+ <div class="skeleton h-5 w-20"></div>
51
+ </div>
52
+ <div class="skeleton h-2"></div>
53
+ <div class="flex items-center justify-between mt-2">
54
+ <div class="skeleton h-4 w-24"></div>
55
+ <div class="skeleton h-4 w-16"></div>
56
+ </div>
57
+ </div>
58
+ @endforeach
59
+ </div>
60
+ @elseif (!goals.length)
61
+ <div class="text-center py-8 text-gray-500">
62
+ <Icon name="bar-chart-2" size="48" class="mx-auto text-gray-300 mb-3" />
63
+ <p>No goals configured</p>
64
+ <p class="text-sm text-gray-400 mt-1">Set up conversion goals to track performance</p>
65
+ </div>
66
+ @else
67
+ <div class="space-y-4">
68
+ @foreach (sortedGoals as goal)
69
+ <div class="p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
70
+ <div class="flex items-center justify-between mb-2">
71
+ <span class="font-medium text-gray-900">{{ goal.goalName }}</span>
72
+ <span class="text-lg font-semibold text-gray-900">
73
+ {{ formatCompact(goal.conversions) }}
74
+ </span>
75
+ </div>
76
+
77
+ <div class="progress-bar mb-2">
78
+ <div
79
+ class="progress-fill bg-green-500"
80
+ style="width: {{ Math.min(goal.conversionRate * 100, 100) }}%"
81
+ ></div>
82
+ </div>
83
+
84
+ <div class="flex items-center justify-between text-sm">
85
+ <span class="text-gray-500">
86
+ {{ formatPercentage(goal.conversionRate) }} conversion rate
87
+ </span>
88
+ @if (goal.revenue > 0)
89
+ <span class="text-green-600 font-medium">
90
+ {{ formatCurrency(goal.revenue) }}
91
+ </span>
92
+ @endif
93
+ </div>
94
+ </div>
95
+ @endforeach
96
+ </div>
97
+
98
+ @if (totalRevenue > 0)
99
+ <div class="mt-4 pt-4 border-t border-gray-200">
100
+ <div class="flex items-center justify-between">
101
+ <span class="text-sm text-gray-500">Total Revenue</span>
102
+ <span class="text-lg font-semibold text-green-600">
103
+ {{ formatCurrency(totalRevenue) }}
104
+ </span>
105
+ </div>
106
+ </div>
107
+ @endif
108
+ @endif
109
+ </div>
@@ -0,0 +1,39 @@
1
+ {{-- Header Component --}}
2
+ {{-- Props: siteName, sites, realtimeCount --}}
3
+ <header class="flex justify-between items-center py-4 border-b border-border mb-4">
4
+ <div>
5
+ <select id="site-select" class="filter-select bg-bg-2 border border-border text-text px-4 py-2 rounded-md text-sm cursor-pointer">
6
+ @foreach(sites as site)
7
+ <option value="{{ site.id }}" @if(site.id === siteId) selected @endif>
8
+ {{ site.name }}
9
+ </option>
10
+ @endforeach
11
+ </select>
12
+ </div>
13
+ <div class="flex items-center gap-4">
14
+ <div class="badge flex items-center gap-2 bg-bg-2 px-4 py-2 rounded-full text-sm">
15
+ <span class="pulse-dot w-2 h-2 bg-success rounded-full"></span>
16
+ <span>{{ realtimeCount }} current visitors</span>
17
+ </div>
18
+ <button class="icon-btn" title="Notifications">
19
+ <Icon name="bell" size="20" />
20
+ </button>
21
+ <button class="icon-btn" title="Options">
22
+ <Icon name="more-vertical" size="20" />
23
+ </button>
24
+ <button class="icon-btn" title="Settings">
25
+ <Icon name="settings" size="20" />
26
+ </button>
27
+ </div>
28
+ </header>
29
+
30
+ <style>
31
+ .pulse-dot {
32
+ animation: pulse 2s infinite;
33
+ }
34
+
35
+ @keyframes pulse {
36
+ 0%, 100% { opacity: 1; }
37
+ 50% { opacity: 0.5; }
38
+ }
39
+ </style>
@@ -0,0 +1,140 @@
1
+ <script server>
2
+ /**
3
+ * HeatmapChart Component
4
+ *
5
+ * Displays activity data as a heatmap grid (e.g., hours x days).
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import { formatCompact } from '../utils'
9
+
10
+ export interface HeatmapDataPoint {
11
+ x: number // Column index (e.g., hour 0-23)
12
+ y: number // Row index (e.g., day 0-6)
13
+ value: number
14
+ }
15
+
16
+ interface HeatmapChartProps {
17
+ data: HeatmapDataPoint[]
18
+ xLabels?: string[]
19
+ yLabels?: string[]
20
+ title?: string
21
+ colorStart?: string
22
+ colorEnd?: string
23
+ loading?: boolean
24
+ }
25
+
26
+ const { data, xLabels, yLabels, title, colorStart, colorEnd, loading } = withDefaults(
27
+ defineProps<HeatmapChartProps>(),
28
+ {
29
+ xLabels: () => ['12a', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12p', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'],
30
+ yLabels: () => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
31
+ colorStart: '#e0e7ff',
32
+ colorEnd: '#4f46e5',
33
+ loading: false,
34
+ }
35
+ )
36
+
37
+ const maxValue = data.length ? Math.max(...data.map(d => d.value), 1) : 1
38
+ const minValue = data.length ? Math.min(...data.map(d => d.value)) : 0
39
+
40
+ const dataMap = new Map<string, number>()
41
+ for (const point of data) {
42
+ dataMap.set(`${point.x}-${point.y}`, point.value)
43
+ }
44
+
45
+ function getValue(x: number, y: number): number {
46
+ return dataMap.get(`${x}-${y}`) ?? 0
47
+ }
48
+
49
+ function hexToRgb(hex: string): { r: number, g: number, b: number } | null {
50
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
51
+ return result
52
+ ? {
53
+ r: Number.parseInt(result[1], 16),
54
+ g: Number.parseInt(result[2], 16),
55
+ b: Number.parseInt(result[3], 16),
56
+ }
57
+ : null
58
+ }
59
+
60
+ function getColor(value: number): string {
61
+ if (maxValue === minValue)
62
+ return colorStart
63
+
64
+ const ratio = (value - minValue) / (maxValue - minValue)
65
+
66
+ const startRgb = hexToRgb(colorStart)
67
+ const endRgb = hexToRgb(colorEnd)
68
+
69
+ if (!startRgb || !endRgb)
70
+ return colorStart
71
+
72
+ const r = Math.round(startRgb.r + (endRgb.r - startRgb.r) * ratio)
73
+ const g = Math.round(startRgb.g + (endRgb.g - startRgb.g) * ratio)
74
+ const b = Math.round(startRgb.b + (endRgb.b - startRgb.b) * ratio)
75
+
76
+ return `rgb(${r}, ${g}, ${b})`
77
+ }
78
+
79
+ const legendColors = [0, 1, 2, 3, 4].map(i => getColor(minValue + ((maxValue - minValue) * i) / 4))
80
+ </script>
81
+
82
+ <div class="heatmap-chart card-hover p-6">
83
+ @if (title)
84
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
85
+ {{ title }}
86
+ </h3>
87
+ @endif
88
+
89
+ @if (loading)
90
+ <div class="animate-pulse">
91
+ <div class="skeleton h-48 w-full"></div>
92
+ </div>
93
+ @elseif (!data.length)
94
+ <div class="text-center py-12 text-gray-500">
95
+ No activity data available
96
+ </div>
97
+ @else
98
+ <div class="overflow-x-auto">
99
+ <div class="min-w-max">
100
+ <div class="flex ml-12 mb-1">
101
+ @foreach (xLabels as label)
102
+ <div class="flex-1 text-center text-xs text-gray-500" style="min-width: 24px">
103
+ {{ loop.index % 3 === 0 ? label : '' }}
104
+ </div>
105
+ @endforeach
106
+ </div>
107
+
108
+ <div class="space-y-1">
109
+ @foreach (yLabels as yLabel)
110
+ <div class="flex items-center gap-2">
111
+ <div class="w-10 text-xs text-gray-500 text-right">
112
+ {{ yLabel }}
113
+ </div>
114
+
115
+ <div class="flex-1 flex gap-0.5">
116
+ @foreach (xLabels as xLabel)
117
+ <div
118
+ class="flex-1 aspect-square rounded-sm cursor-pointer transition-transform hover:scale-110 hover:z-10"
119
+ style="min-width: 20px; min-height: 20px; background-color: {{ getColor(getValue(loop.index, loop.parent.index)) }}"
120
+ title="{{ yLabel }} {{ xLabel }}: {{ formatCompact(getValue(loop.index, loop.parent.index)) }}"
121
+ ></div>
122
+ @endforeach
123
+ </div>
124
+ </div>
125
+ @endforeach
126
+ </div>
127
+
128
+ <div class="flex items-center justify-end mt-4 gap-2">
129
+ <span class="text-xs text-gray-500">Less</span>
130
+ <div class="flex gap-0.5">
131
+ @foreach (legendColors as color)
132
+ <div class="w-4 h-4 rounded-sm" style="background-color: {{ color }}"></div>
133
+ @endforeach
134
+ </div>
135
+ <span class="text-xs text-gray-500">More</span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ @endif
140
+ </div>