@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,238 @@
1
+ {{-- CampaignTable Component --}}
2
+ {{-- Props: campaigns (by campaign name), sources (utm_source), mediums (utm_medium), contents (utm_content), terms (utm_term), activeTab --}}
3
+ <div class="data-panel">
4
+ <div class="tabs">
5
+ <button class="tab-btn @if(activeTab === 'campaign') active @endif" data-tab="campaign" type="button">
6
+ Campaign
7
+ </button>
8
+ <button class="tab-btn @if(activeTab === 'source') active @endif" data-tab="source" type="button">
9
+ Source
10
+ </button>
11
+ <button class="tab-btn @if(activeTab === 'medium') active @endif" data-tab="medium" type="button">
12
+ Medium
13
+ </button>
14
+ <button class="tab-btn @if(activeTab === 'content') active @endif" data-tab="content" type="button">
15
+ Content
16
+ </button>
17
+ <button class="tab-btn @if(activeTab === 'term') active @endif" data-tab="term" type="button">
18
+ Term
19
+ </button>
20
+ </div>
21
+
22
+ <!-- Campaign Tab -->
23
+ <div class="tab-content" data-content="campaign" @if(activeTab !== 'campaign') style="display:none" @endif>
24
+ <table class="data-table">
25
+ <thead>
26
+ <tr>
27
+ <th>Campaign</th>
28
+ <th class="text-right">Visitors</th>
29
+ <th class="text-right">Views</th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ @forelse(campaigns as item)
34
+ <tr>
35
+ <td class="name" title="{{ item.name }}">{{ item.name }}</td>
36
+ <td class="value">{{ item.visitors | fmt }}</td>
37
+ <td class="value">{{ item.views | fmt }}</td>
38
+ </tr>
39
+ @empty
40
+ <tr>
41
+ <td colspan="3" class="empty-cell">No campaign data</td>
42
+ </tr>
43
+ @endforelse
44
+ </tbody>
45
+ </table>
46
+ </div>
47
+
48
+ <!-- Source Tab -->
49
+ <div class="tab-content" data-content="source" @if(activeTab !== 'source') style="display:none" @endif>
50
+ <table class="data-table">
51
+ <thead>
52
+ <tr>
53
+ <th>Source</th>
54
+ <th class="text-right">Visitors</th>
55
+ <th class="text-right">Views</th>
56
+ </tr>
57
+ </thead>
58
+ <tbody>
59
+ @forelse(sources as item)
60
+ <tr>
61
+ <td class="name" title="{{ item.name }}">{{ item.name }}</td>
62
+ <td class="value">{{ item.visitors | fmt }}</td>
63
+ <td class="value">{{ item.views | fmt }}</td>
64
+ </tr>
65
+ @empty
66
+ <tr>
67
+ <td colspan="3" class="empty-cell">No source data</td>
68
+ </tr>
69
+ @endforelse
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+
74
+ <!-- Medium Tab -->
75
+ <div class="tab-content" data-content="medium" @if(activeTab !== 'medium') style="display:none" @endif>
76
+ <table class="data-table">
77
+ <thead>
78
+ <tr>
79
+ <th>Medium</th>
80
+ <th class="text-right">Visitors</th>
81
+ <th class="text-right">Views</th>
82
+ </tr>
83
+ </thead>
84
+ <tbody>
85
+ @forelse(mediums as item)
86
+ <tr>
87
+ <td class="name" title="{{ item.name }}">{{ item.name }}</td>
88
+ <td class="value">{{ item.visitors | fmt }}</td>
89
+ <td class="value">{{ item.views | fmt }}</td>
90
+ </tr>
91
+ @empty
92
+ <tr>
93
+ <td colspan="3" class="empty-cell">No medium data</td>
94
+ </tr>
95
+ @endforelse
96
+ </tbody>
97
+ </table>
98
+ </div>
99
+
100
+ <!-- Content Tab -->
101
+ <div class="tab-content" data-content="content" @if(activeTab !== 'content') style="display:none" @endif>
102
+ <table class="data-table">
103
+ <thead>
104
+ <tr>
105
+ <th>Content</th>
106
+ <th class="text-right">Visitors</th>
107
+ <th class="text-right">Views</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ @forelse(contents as item)
112
+ <tr>
113
+ <td class="name" title="{{ item.name }}">{{ item.name }}</td>
114
+ <td class="value">{{ item.visitors | fmt }}</td>
115
+ <td class="value">{{ item.views | fmt }}</td>
116
+ </tr>
117
+ @empty
118
+ <tr>
119
+ <td colspan="3" class="empty-cell">No content data</td>
120
+ </tr>
121
+ @endforelse
122
+ </tbody>
123
+ </table>
124
+ </div>
125
+
126
+ <!-- Term Tab -->
127
+ <div class="tab-content" data-content="term" @if(activeTab !== 'term') style="display:none" @endif>
128
+ <table class="data-table">
129
+ <thead>
130
+ <tr>
131
+ <th>Term</th>
132
+ <th class="text-right">Visitors</th>
133
+ <th class="text-right">Views</th>
134
+ </tr>
135
+ </thead>
136
+ <tbody>
137
+ @forelse(terms as item)
138
+ <tr>
139
+ <td class="name" title="{{ item.name }}">{{ item.name }}</td>
140
+ <td class="value">{{ item.visitors | fmt }}</td>
141
+ <td class="value">{{ item.views | fmt }}</td>
142
+ </tr>
143
+ @empty
144
+ <tr>
145
+ <td colspan="3" class="empty-cell">No term data</td>
146
+ </tr>
147
+ @endforelse
148
+ </tbody>
149
+ </table>
150
+ </div>
151
+ </div>
152
+
153
+
154
+ <style>
155
+ .data-panel {
156
+ background: var(--bg-secondary);
157
+ border-radius: 8px;
158
+ padding: 1rem;
159
+ }
160
+
161
+ .tabs {
162
+ display: flex;
163
+ gap: 0.25rem;
164
+ border-bottom: 1px solid var(--border);
165
+ margin-bottom: 1rem;
166
+ flex-wrap: wrap;
167
+ }
168
+
169
+ .tab-btn {
170
+ background: transparent;
171
+ border: none;
172
+ color: var(--text-secondary);
173
+ padding: 0.5rem 0.75rem;
174
+ font-size: 0.75rem;
175
+ font-weight: 500;
176
+ cursor: pointer;
177
+ border-bottom: 2px solid transparent;
178
+ transition: all 0.15s;
179
+ }
180
+
181
+ .tab-btn:hover {
182
+ color: var(--text-primary);
183
+ }
184
+
185
+ .tab-btn.active {
186
+ color: var(--accent);
187
+ border-bottom-color: var(--accent);
188
+ }
189
+
190
+ .data-table {
191
+ width: 100%;
192
+ font-size: 0.8125rem;
193
+ border-collapse: collapse;
194
+ }
195
+
196
+ .data-table th {
197
+ text-align: left;
198
+ color: var(--text-muted);
199
+ font-weight: 500;
200
+ padding: 0.5rem 0;
201
+ border-bottom: 1px solid var(--border);
202
+ font-size: 0.6875rem;
203
+ text-transform: uppercase;
204
+ letter-spacing: 0.05em;
205
+ }
206
+
207
+ .data-table th.text-right {
208
+ text-align: right;
209
+ }
210
+
211
+ .data-table td {
212
+ padding: 0.625rem 0;
213
+ border-bottom: 1px solid var(--bg-tertiary);
214
+ }
215
+
216
+ .data-table tr:last-child td {
217
+ border-bottom: none;
218
+ }
219
+
220
+ .data-table .name {
221
+ color: var(--text-primary);
222
+ max-width: 150px;
223
+ overflow: hidden;
224
+ text-overflow: ellipsis;
225
+ white-space: nowrap;
226
+ }
227
+
228
+ .data-table .value {
229
+ color: var(--text-secondary);
230
+ text-align: right;
231
+ }
232
+
233
+ .empty-cell {
234
+ text-align: center;
235
+ color: var(--text-muted);
236
+ padding: 1.5rem 0;
237
+ }
238
+ </style>
@@ -0,0 +1,101 @@
1
+ <script server>
2
+ /**
3
+ * CountryList Component
4
+ *
5
+ * Displays country breakdown with flags using emoji flags.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { TopItem } from '../types'
9
+ import { formatCompact, formatPercentage } from '../utils'
10
+
11
+ export interface CountryItem extends TopItem {
12
+ countryCode?: string
13
+ }
14
+
15
+ interface CountryListProps {
16
+ countries: CountryItem[]
17
+ loading?: boolean
18
+ maxItems?: number
19
+ title?: string
20
+ }
21
+
22
+ const { countries, loading, maxItems, title } = withDefaults(
23
+ defineProps<CountryListProps>(),
24
+ {
25
+ loading: false,
26
+ maxItems: 10,
27
+ title: 'Top Countries',
28
+ }
29
+ )
30
+
31
+ const sortedCountries = [...countries]
32
+ .sort((a, b) => b.value - a.value)
33
+ .slice(0, maxItems)
34
+
35
+ const maxValue = Math.max(...countries.map(c => c.value), 1)
36
+
37
+ function getFlag(countryCode?: string): string {
38
+ if (!countryCode || countryCode.length !== 2)
39
+ return '🌍'
40
+
41
+ const codePoints = countryCode
42
+ .toUpperCase()
43
+ .split('')
44
+ .map(char => 127397 + char.charCodeAt(0))
45
+
46
+ return String.fromCodePoint(...codePoints)
47
+ }
48
+ </script>
49
+
50
+ <div class="country-list card-hover p-6">
51
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
52
+ {{ title }}
53
+ </h3>
54
+
55
+ @if (loading)
56
+ <div class="space-y-3">
57
+ @foreach ([1, 2, 3, 4, 5] as i)
58
+ <div class="animate-pulse">
59
+ <div class="flex items-center justify-between mb-1">
60
+ <div class="flex items-center gap-2">
61
+ <div class="skeleton w-6 h-6 rounded"></div>
62
+ <div class="skeleton h-4 w-24"></div>
63
+ </div>
64
+ <div class="skeleton h-4 w-16"></div>
65
+ </div>
66
+ <div class="skeleton h-2"></div>
67
+ </div>
68
+ @endforeach
69
+ </div>
70
+ @elseif (!countries.length)
71
+ <div class="text-center py-8 text-gray-500">
72
+ No geographic data available
73
+ </div>
74
+ @else
75
+ <div class="space-y-3">
76
+ @foreach (sortedCountries as country)
77
+ <div class="group">
78
+ <div class="list-item mb-1">
79
+ <div class="flex items-center gap-2 min-w-0">
80
+ <span class="text-lg flex-shrink-0">{{ getFlag(country.countryCode) }}</span>
81
+ <span class="list-label" title="{{ country.name }}">
82
+ {{ country.name }}
83
+ </span>
84
+ </div>
85
+ <span class="list-value">
86
+ {{ formatCompact(country.value) }}
87
+ <span class="text-gray-400 ml-1">({{ formatPercentage(country.percentage) }})</span>
88
+ </span>
89
+ </div>
90
+
91
+ <div class="progress-bar">
92
+ <div
93
+ class="progress-fill bg-primary-500"
94
+ style="width: {{ (country.value / maxValue) * 100 }}%"
95
+ ></div>
96
+ </div>
97
+ </div>
98
+ @endforeach
99
+ </div>
100
+ @endif
101
+ </div>
@@ -0,0 +1,226 @@
1
+ <script server>
2
+ /**
3
+ * DataTable Component
4
+ *
5
+ * A flexible table component for displaying analytics data.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ export interface TableColumn {
10
+ key: string
11
+ label: string
12
+ align?: 'left' | 'center' | 'right'
13
+ format?: 'number' | 'percentage' | 'currency' | 'duration' | 'date'
14
+ sortable?: boolean
15
+ width?: string
16
+ }
17
+
18
+ export interface TableRow {
19
+ [key: string]: string | number | boolean | null | undefined
20
+ }
21
+
22
+ interface DataTableProps {
23
+ columns: TableColumn[]
24
+ rows: TableRow[]
25
+ loading?: boolean
26
+ title?: string
27
+ emptyMessage?: string
28
+ sortable?: boolean
29
+ pageSize?: number
30
+ }
31
+
32
+ const { columns, rows, loading, title, emptyMessage, sortable, pageSize } = withDefaults(
33
+ defineProps<DataTableProps>(),
34
+ {
35
+ loading: false,
36
+ emptyMessage: 'No data available',
37
+ sortable: true,
38
+ pageSize: 10,
39
+ }
40
+ )
41
+
42
+ const totalPages = Math.ceil(rows.length / pageSize)
43
+
44
+ function formatValue(value: unknown, format?: string): string {
45
+ if (value === null || value === undefined)
46
+ return '-'
47
+
48
+ switch (format) {
49
+ case 'number':
50
+ return typeof value === 'number' ? value.toLocaleString() : String(value)
51
+ case 'percentage':
52
+ return typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : String(value)
53
+ case 'currency':
54
+ return typeof value === 'number' ? `$${value.toLocaleString()}` : String(value)
55
+ case 'duration': {
56
+ if (typeof value !== 'number')
57
+ return String(value)
58
+ const mins = Math.floor(value / 60)
59
+ const secs = Math.floor(value % 60)
60
+ return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`
61
+ }
62
+ case 'date':
63
+ return value instanceof Date ? value.toLocaleDateString() : String(value)
64
+ default:
65
+ return String(value)
66
+ }
67
+ }
68
+
69
+ const rowsJson = JSON.stringify(rows)
70
+ const columnsJson = JSON.stringify(columns)
71
+ </script>
72
+
73
+ <div class="data-table card-hover p-6">
74
+ @if (title)
75
+ <div class="flex items-center justify-between mb-4">
76
+ <h3 class="text-sm font-medium text-gray-900">{{ title }}</h3>
77
+ @if (rows.length)
78
+ <span class="text-sm text-gray-500">
79
+ {{ rows.length }} {{ rows.length === 1 ? 'row' : 'rows' }}
80
+ </span>
81
+ @endif
82
+ </div>
83
+ @endif
84
+
85
+ @if (loading)
86
+ <div class="animate-pulse">
87
+ <div class="skeleton h-10 mb-2"></div>
88
+ @foreach ([1, 2, 3, 4, 5] as i)
89
+ <div class="skeleton h-12 mb-1"></div>
90
+ @endforeach
91
+ </div>
92
+ @elseif (!rows.length)
93
+ <div class="text-center py-12 text-gray-500">
94
+ {{ emptyMessage }}
95
+ </div>
96
+ @else
97
+ <script client>
98
+ const tableRows = {{ rowsJson }}
99
+ const tableColumns = {{ columnsJson }}
100
+ const sortKey = state(null)
101
+ const sortOrder = state('desc')
102
+ const currentPage = state(1)
103
+ const tablePgSize = {{ pageSize }}
104
+
105
+ const sortedRows = derived(() => {
106
+ if (!sortKey()) return tableRows
107
+ return [...tableRows].sort((a, b) => {
108
+ const aVal = a[sortKey()]
109
+ const bVal = b[sortKey()]
110
+ if (aVal === bVal) return 0
111
+ if (aVal === null || aVal === undefined) return 1
112
+ if (bVal === null || bVal === undefined) return -1
113
+ const comparison = aVal < bVal ? -1 : 1
114
+ return sortOrder() === 'asc' ? comparison : -comparison
115
+ })
116
+ })
117
+
118
+ const paginatedRows = derived(() => {
119
+ const start = (currentPage() - 1) * tablePgSize
120
+ return sortedRows().slice(start, start + tablePgSize)
121
+ })
122
+
123
+ const totalPagesComputed = derived(() => Math.ceil(sortedRows().length / tablePgSize))
124
+
125
+ function toggleSort(key) {
126
+ if (sortKey() === key) {
127
+ sortOrder.set(sortOrder() === 'asc' ? 'desc' : 'asc')
128
+ } else {
129
+ sortKey.set(key)
130
+ sortOrder.set('desc')
131
+ }
132
+ }
133
+
134
+ function formatValueClient(value, format) {
135
+ if (value === null || value === undefined) return '-'
136
+ switch (format) {
137
+ case 'number': return typeof value === 'number' ? value.toLocaleString() : String(value)
138
+ case 'percentage': return typeof value === 'number' ? (value * 100).toFixed(1) + '%' : String(value)
139
+ case 'currency': return typeof value === 'number' ? '$' + value.toLocaleString() : String(value)
140
+ case 'duration':
141
+ if (typeof value !== 'number') return String(value)
142
+ const mins = Math.floor(value / 60)
143
+ const secs = Math.floor(value % 60)
144
+ return mins >= 1 ? mins + 'm ' + secs + 's' : secs + 's'
145
+ default: return String(value)
146
+ }
147
+ }
148
+ </script>
149
+ <div class="overflow-x-auto">
150
+ <table class="w-full">
151
+ <thead>
152
+ <tr class="border-b border-gray-200">
153
+ <div :for="column in tableColumns" :key="column.key">
154
+ <th
155
+ class="px-4 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700"
156
+ x-class="{
157
+ 'text-right': column.align === 'right',
158
+ 'text-center': column.align === 'center',
159
+ 'text-left': column.align !== 'right' && column.align !== 'center'
160
+ }"
161
+ @click="toggleSort(column.key)"
162
+ >
163
+ <div class="flex items-center gap-1" x-class="{ 'justify-end': column.align === 'right' }">
164
+ <span>{{ column.label }}</span>
165
+ <svg
166
+ :show="sortKey() === column.key"
167
+ class="w-4 h-4"
168
+ x-class="{ 'rotate-180': sortOrder() === 'asc' }"
169
+ fill="none"
170
+ stroke="currentColor"
171
+ viewBox="0 0 24 24"
172
+ >
173
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
174
+ </svg>
175
+ </div>
176
+ </th>
177
+ </div>
178
+ </tr>
179
+ </thead>
180
+ <tbody>
181
+ <div :for="(row, index) in paginatedRows()" :key="index">
182
+ <tr class="border-b border-gray-100 hover:bg-gray-50">
183
+ <div :for="column in tableColumns" :key="column.key">
184
+ <td
185
+ class="px-4 py-3 text-sm"
186
+ x-class="{
187
+ 'text-right': column.align === 'right',
188
+ 'text-center': column.align === 'center',
189
+ 'tabular-nums': column.format === 'number' || column.format === 'currency'
190
+ }"
191
+ >{{ formatValueClient(row[column.key], column.format) }}</td>
192
+ </div>
193
+ </tr>
194
+ </div>
195
+ </tbody>
196
+ </table>
197
+
198
+ <div :show="totalPagesComputed() >= 2" class="flex items-center justify-between mt-4 pt-4 border-t border-gray-200">
199
+ <div class="text-sm text-gray-500">
200
+ Showing <span>{{ (currentPage() - 1) * tablePgSize + 1 }}</span> to <span>{{ Math.min(currentPage() * tablePgSize, tableRows.length) }}</span> of <span>{{ tableRows.length }}</span>
201
+ </div>
202
+ <div class="flex items-center gap-2">
203
+ <button
204
+ type="button"
205
+ class="btn-icon"
206
+ x-disabled="currentPage() === 1"
207
+ @click="currentPage.set(currentPage() - 1)"
208
+ >
209
+ <Icon name="chevron-left" size="20" />
210
+ </button>
211
+ <span class="text-sm text-gray-600">
212
+ Page <span>{{ currentPage() }}</span> of <span>{{ totalPagesComputed() }}</span>
213
+ </span>
214
+ <button
215
+ type="button"
216
+ class="btn-icon"
217
+ x-disabled="currentPage() === totalPagesComputed()"
218
+ @click="currentPage.set(currentPage() + 1)"
219
+ >
220
+ <Icon name="chevron-right" size="20" />
221
+ </button>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ @endif
226
+ </div>
@@ -0,0 +1,77 @@
1
+ <script server>
2
+ /**
3
+ * DateRangePicker Component
4
+ *
5
+ * Date range selector with presets and comparison options.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ interface DateRangePickerProps {
10
+ dateRange?: string
11
+ comparison?: string
12
+ granularity?: string
13
+ }
14
+
15
+ const { dateRange, comparison, granularity } = withDefaults(
16
+ defineProps<DateRangePickerProps>(),
17
+ {
18
+ dateRange: '7d',
19
+ comparison: 'none',
20
+ granularity: 'auto',
21
+ }
22
+ )
23
+ </script>
24
+
25
+ <script client>
26
+ const dateRangeVal = state('{{ dateRange }}')
27
+ const comparisonVal = state('{{ comparison }}')
28
+ const granularityVal = state('{{ granularity }}')
29
+
30
+ function onDateChange() {
31
+ document.dispatchEvent(new CustomEvent('date-change', {
32
+ detail: {
33
+ dateRange: dateRangeVal(),
34
+ comparison: comparisonVal(),
35
+ granularity: granularityVal()
36
+ }
37
+ }))
38
+ }
39
+ </script>
40
+ <div class="date-bar flex items-center gap-4 py-3 mb-4 flex-wrap">
41
+ <select
42
+ class="date-select bg-white border border-gray-200 text-gray-900 px-4 py-2 rounded-lg text-sm cursor-pointer hover:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
43
+ :model="dateRangeVal"
44
+ @change="onDateChange()"
45
+ >
46
+ <option value="today">Today</option>
47
+ <option value="yesterday">Yesterday</option>
48
+ <option value="7d">Last 7 days</option>
49
+ <option value="30d">Last 30 days</option>
50
+ <option value="90d">Last 90 days</option>
51
+ <option value="12m">Last 12 months</option>
52
+ <option value="custom">Custom range</option>
53
+ </select>
54
+
55
+ <span class="text-gray-500 text-sm">compared to</span>
56
+
57
+ <select
58
+ class="date-select bg-white border border-gray-200 text-gray-900 px-4 py-2 rounded-lg text-sm cursor-pointer hover:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
59
+ :model="comparisonVal"
60
+ @change="onDateChange()"
61
+ >
62
+ <option value="none">No comparison</option>
63
+ <option value="previous">Previous period</option>
64
+ <option value="year">Same period last year</option>
65
+ </select>
66
+
67
+ <select
68
+ class="date-select bg-white border border-gray-200 text-gray-900 px-4 py-2 rounded-lg text-sm cursor-pointer hover:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
69
+ :model="granularityVal"
70
+ @change="onDateChange()"
71
+ >
72
+ <option value="auto">Auto</option>
73
+ <option value="hour">Hourly</option>
74
+ <option value="day">Daily</option>
75
+ <option value="month">Monthly</option>
76
+ </select>
77
+ </div>