@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,172 @@
1
+ <script server>
2
+ /**
3
+ * LiveActivityFeed Component
4
+ *
5
+ * Real-time feed of visitor activity.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+
9
+ export type ActivityType = 'pageview' | 'event' | 'conversion' | 'session_start' | 'session_end'
10
+
11
+ export interface Activity {
12
+ id: string
13
+ type: ActivityType
14
+ page?: string
15
+ eventName?: string
16
+ country?: string
17
+ countryCode?: string
18
+ device?: 'desktop' | 'mobile' | 'tablet'
19
+ timestamp: Date
20
+ value?: number
21
+ }
22
+
23
+ interface LiveActivityFeedProps {
24
+ activities: Activity[]
25
+ maxItems?: number
26
+ autoScroll?: boolean
27
+ showTimestamps?: boolean
28
+ paused?: boolean
29
+ }
30
+
31
+ const { activities, maxItems, autoScroll, showTimestamps, paused } = withDefaults(
32
+ defineProps<LiveActivityFeedProps>(),
33
+ {
34
+ maxItems: 20,
35
+ autoScroll: true,
36
+ showTimestamps: true,
37
+ paused: false,
38
+ }
39
+ )
40
+
41
+ const displayedActivities = activities.slice(0, maxItems)
42
+
43
+ const activityConfig: Record<ActivityType, { icon: string, color: string, label: string }> = {
44
+ pageview: {
45
+ icon: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z',
46
+ color: 'bg-blue-100 text-blue-600',
47
+ label: 'Page View',
48
+ },
49
+ event: {
50
+ icon: 'M13 10V3L4 14h7v7l9-11h-7z',
51
+ color: 'bg-purple-100 text-purple-600',
52
+ label: 'Event',
53
+ },
54
+ conversion: {
55
+ icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
56
+ color: 'bg-green-100 text-green-600',
57
+ label: 'Conversion',
58
+ },
59
+ session_start: {
60
+ icon: 'M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1',
61
+ color: 'bg-cyan-100 text-cyan-600',
62
+ label: 'Session Start',
63
+ },
64
+ session_end: {
65
+ icon: 'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
66
+ color: 'bg-gray-100 text-gray-600',
67
+ label: 'Session End',
68
+ },
69
+ }
70
+
71
+ const deviceIcons: Record<string, string> = {
72
+ desktop: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
73
+ mobile: 'M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z',
74
+ tablet: 'M12 18h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z',
75
+ }
76
+
77
+ function getFlag(countryCode?: string): string {
78
+ if (!countryCode || countryCode.length !== 2)
79
+ return '🌍'
80
+ const codePoints = countryCode.toUpperCase().split('').map(char => 127397 + char.charCodeAt(0))
81
+ return String.fromCodePoint(...codePoints)
82
+ }
83
+
84
+ function getActivityText(activity: Activity): string {
85
+ switch (activity.type) {
86
+ case 'pageview':
87
+ return activity.page || 'Unknown page'
88
+ case 'event':
89
+ return activity.eventName || 'Custom event'
90
+ case 'conversion':
91
+ return activity.eventName || 'Goal completed'
92
+ case 'session_start':
93
+ return 'Started browsing'
94
+ case 'session_end':
95
+ return 'Left the site'
96
+ default:
97
+ return 'Activity'
98
+ }
99
+ }
100
+ </script>
101
+
102
+ <div class="live-activity-feed card-hover p-6">
103
+ <div class="flex items-center justify-between mb-4">
104
+ <div class="flex items-center gap-2">
105
+ <h3 class="text-sm font-medium text-gray-900">Live Activity</h3>
106
+ @if (!paused)
107
+ <div class="relative">
108
+ <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
109
+ <div class="absolute inset-0 w-2 h-2 rounded-full bg-green-500 animate-ping"></div>
110
+ </div>
111
+ @else
112
+ <span class="text-xs text-gray-500">(Paused)</span>
113
+ @endif
114
+ </div>
115
+ @if (activities.length)
116
+ <span class="text-xs text-gray-500">
117
+ {{ activities.length }} activities
118
+ </span>
119
+ @endif
120
+ </div>
121
+
122
+ @if (!activities.length)
123
+ <div class="text-center py-8 text-gray-500">
124
+ <Icon name="zap" size="48" class="mx-auto text-gray-300 mb-3" />
125
+ <p>Waiting for activity...</p>
126
+ </div>
127
+ @else
128
+ <div class="space-y-2 max-h-96 overflow-y-auto">
129
+ @foreach (displayedActivities as activity)
130
+ <div class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
131
+ <div class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 {{ activityConfig[activity.type].color }}">
132
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
133
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ activityConfig[activity.type].icon }}" />
134
+ </svg>
135
+ </div>
136
+
137
+ <div class="flex-1 min-w-0">
138
+ <div class="flex items-center gap-2">
139
+ <span class="text-sm text-gray-900 truncate">
140
+ {{ getActivityText(activity) }}
141
+ </span>
142
+ @if (activity.value)
143
+ <span class="text-xs font-medium text-green-600">
144
+ +${{ activity.value }}
145
+ </span>
146
+ @endif
147
+ </div>
148
+ <div class="flex items-center gap-2 text-xs text-gray-500">
149
+ @if (activity.countryCode)
150
+ <span class="flex items-center gap-1">
151
+ {{ getFlag(activity.countryCode) }}
152
+ {{ activity.country }}
153
+ </span>
154
+ @endif
155
+ @if (activity.device)
156
+ <span class="flex items-center gap-1">
157
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
158
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ deviceIcons[activity.device] }}" />
159
+ </svg>
160
+ </span>
161
+ @endif
162
+ </div>
163
+ </div>
164
+
165
+ @if (showTimestamps)
166
+ <span class="text-xs text-gray-400 flex-shrink-0">Just now</span>
167
+ @endif
168
+ </div>
169
+ @endforeach
170
+ </div>
171
+ @endif
172
+ </div>
@@ -0,0 +1,106 @@
1
+ <script server>
2
+ /**
3
+ * MetricComparison Component
4
+ *
5
+ * Displays a metric with visual comparison between current and previous periods.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import { formatChange, formatCompact, formatCurrency, formatDuration, formatPercentage } from '../utils'
9
+
10
+ interface MetricComparisonProps {
11
+ title: string
12
+ current: number
13
+ previous: number
14
+ format?: 'number' | 'percentage' | 'duration' | 'currency'
15
+ loading?: boolean
16
+ invertColors?: boolean
17
+ }
18
+
19
+ const { title, current, previous, format, loading, invertColors } = withDefaults(
20
+ defineProps<MetricComparisonProps>(),
21
+ {
22
+ format: 'number',
23
+ loading: false,
24
+ invertColors: false,
25
+ }
26
+ )
27
+
28
+ const change = previous === 0
29
+ ? (current > 0 ? 100 : 0)
30
+ : ((current - previous) / previous) * 100
31
+
32
+ function formatValue(value: number): string {
33
+ switch (format) {
34
+ case 'percentage':
35
+ return formatPercentage(value)
36
+ case 'duration':
37
+ return formatDuration(value)
38
+ case 'currency':
39
+ return formatCurrency(value)
40
+ default:
41
+ return formatCompact(value)
42
+ }
43
+ }
44
+
45
+ const formattedCurrent = formatValue(current)
46
+ const formattedPrevious = formatValue(previous)
47
+
48
+ const positive = change >= 0
49
+ const isPositive = invertColors ? !positive : positive
50
+
51
+ const max = Math.max(current, previous)
52
+ const barWidthCurrent = max > 0 ? (current / max) * 100 : 0
53
+ const barWidthPrevious = max > 0 ? (previous / max) * 100 : 0
54
+ </script>
55
+
56
+ <div class="metric-comparison card-hover p-6">
57
+ <div class="flex items-center justify-between mb-4">
58
+ <h3 class="text-sm font-medium text-gray-900">{{ title }}</h3>
59
+ @if (!loading)
60
+ <span class="text-sm font-medium {{ isPositive ? 'text-green-600' : 'text-red-600' }}">
61
+ {{ formatChange(change / 100) }}
62
+ </span>
63
+ @endif
64
+ </div>
65
+
66
+ @if (loading)
67
+ <div class="space-y-4 animate-pulse">
68
+ <div>
69
+ <div class="skeleton h-4 w-20 mb-2"></div>
70
+ <div class="skeleton h-3 w-full"></div>
71
+ </div>
72
+ <div>
73
+ <div class="skeleton h-4 w-20 mb-2"></div>
74
+ <div class="skeleton h-3 w-3/4"></div>
75
+ </div>
76
+ </div>
77
+ @else
78
+ <div class="space-y-4">
79
+ <div>
80
+ <div class="flex items-center justify-between text-sm mb-1">
81
+ <span class="text-gray-600">Current period</span>
82
+ <span class="font-semibold text-gray-900">{{ formattedCurrent }}</span>
83
+ </div>
84
+ <div class="h-3 bg-gray-100 rounded-full overflow-hidden">
85
+ <div
86
+ class="h-full rounded-full transition-all duration-500 {{ isPositive ? 'bg-green-500' : 'bg-red-500' }}"
87
+ style="width: {{ barWidthCurrent }}%"
88
+ ></div>
89
+ </div>
90
+ </div>
91
+
92
+ <div>
93
+ <div class="flex items-center justify-between text-sm mb-1">
94
+ <span class="text-gray-600">Previous period</span>
95
+ <span class="font-medium text-gray-500">{{ formattedPrevious }}</span>
96
+ </div>
97
+ <div class="h-3 bg-gray-100 rounded-full overflow-hidden">
98
+ <div
99
+ class="h-full bg-gray-400 rounded-full transition-all duration-500"
100
+ style="width: {{ barWidthPrevious }}%"
101
+ ></div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ @endif
106
+ </div>
@@ -0,0 +1,96 @@
1
+ <script server>
2
+ /**
3
+ * MiniStats Component
4
+ *
5
+ * Compact horizontal row of mini stats, perfect for headers or summaries.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import { formatCompact, formatCurrency, formatDuration, formatPercentage } from '../utils'
9
+
10
+ export interface MiniStat {
11
+ label: string
12
+ value: number | string
13
+ format?: 'number' | 'percentage' | 'duration' | 'currency'
14
+ change?: number
15
+ icon?: string
16
+ }
17
+
18
+ interface MiniStatsProps {
19
+ stats: MiniStat[]
20
+ loading?: boolean
21
+ dividers?: boolean
22
+ }
23
+
24
+ const { stats, loading, dividers } = withDefaults(
25
+ defineProps<MiniStatsProps>(),
26
+ {
27
+ loading: false,
28
+ dividers: true,
29
+ }
30
+ )
31
+
32
+ function formatValue(stat: MiniStat): string {
33
+ if (typeof stat.value === 'string')
34
+ return stat.value
35
+
36
+ switch (stat.format) {
37
+ case 'percentage':
38
+ return formatPercentage(stat.value)
39
+ case 'duration':
40
+ return formatDuration(stat.value)
41
+ case 'currency':
42
+ return formatCurrency(stat.value)
43
+ default:
44
+ return formatCompact(stat.value)
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <div class="mini-stats flex items-center flex-wrap gap-y-2 py-2">
50
+ @if (loading)
51
+ @foreach ([1, 2, 3, 4] as i)
52
+ <div class="flex items-center gap-6 animate-pulse">
53
+ <div class="flex items-center gap-2">
54
+ <div class="skeleton h-4 w-16"></div>
55
+ <div class="skeleton h-5 w-12"></div>
56
+ </div>
57
+ @if (dividers && i < 4)
58
+ <div class="h-4 w-px bg-gray-200"></div>
59
+ @endif
60
+ </div>
61
+ @endforeach
62
+ @else
63
+ @foreach (stats as stat)
64
+ <div class="flex items-center">
65
+ <div class="flex items-center gap-2 px-3 first:pl-0 last:pr-0">
66
+ @if (stat.icon)
67
+ <svg
68
+ class="w-4 h-4 text-gray-400"
69
+ fill="none"
70
+ stroke="currentColor"
71
+ viewBox="0 0 24 24"
72
+ >
73
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ stat.icon }}" />
74
+ </svg>
75
+ @endif
76
+
77
+ <span class="text-sm text-gray-500">{{ stat.label }}</span>
78
+
79
+ <span class="text-sm font-semibold text-gray-900">
80
+ {{ formatValue(stat) }}
81
+ </span>
82
+
83
+ @if (stat.change !== undefined)
84
+ <span class="text-xs font-medium {{ stat.change >= 0 ? 'text-green-600' : 'text-red-600' }}">
85
+ {{ stat.change >= 0 ? '+' : '' }}{{ stat.change.toFixed(1) }}%
86
+ </span>
87
+ @endif
88
+ </div>
89
+
90
+ @if (dividers && !loop.last)
91
+ <div class="h-4 w-px bg-gray-200"></div>
92
+ @endif
93
+ </div>
94
+ @endforeach
95
+ @endif
96
+ </div>
@@ -0,0 +1,124 @@
1
+ <script server>
2
+ /**
3
+ * OSBreakdown Component
4
+ *
5
+ * Displays operating system breakdown with icons.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import type { TopItem } from '../types'
9
+ import { formatPercentage } from '../utils'
10
+
11
+ interface OSBreakdownProps {
12
+ systems: TopItem[]
13
+ loading?: boolean
14
+ }
15
+
16
+ const { systems, loading } = withDefaults(
17
+ defineProps<OSBreakdownProps>(),
18
+ {
19
+ loading: false,
20
+ }
21
+ )
22
+
23
+ const osIcons: Record<string, string> = {
24
+ windows: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"/></svg>`,
25
+ macos: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
26
+ ios: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
27
+ android: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.341c-.5 0-.91-.41-.91-.91s.41-.91.91-.91.91.41.91.91-.41.91-.91.91m-11.046 0c-.5 0-.91-.41-.91-.91s.41-.91.91-.91.91.41.91.91-.41.91-.91.91m11.4-6.02l1.97-3.41c.11-.19.04-.43-.15-.54-.19-.11-.43-.04-.54.15l-2 3.46C15.53 8.31 13.84 7.91 12 7.91s-3.53.4-5.15 1.07l-2-3.46c-.11-.19-.35-.26-.54-.15-.19.11-.26.35-.15.54l1.97 3.41C3.47 11.07 1.59 14.07 1.5 17.5h21c-.09-3.43-1.97-6.43-4.63-8.18z"/></svg>`,
28
+ linux: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12.504 0c-.155 0-.311.001-.465.003-.653.014-1.302.082-1.933.225-1.308.298-2.482.88-3.46 1.71-.476.4-.903.85-1.273 1.347-.383.515-.706 1.076-.958 1.672-.264.628-.456 1.29-.57 1.97-.118.704-.162 1.425-.132 2.143.032.757.136 1.505.31 2.234.159.662.378 1.304.649 1.92.238.54.522 1.058.846 1.546.261.392.549.764.86 1.114.296.333.615.646.95.938.317.276.65.533.995.77.284.197.578.38.879.548.21.118.426.227.644.33.197.093.398.18.6.262.19.078.384.15.58.217.189.065.38.125.573.18.194.057.39.108.587.155.198.048.398.09.6.127.21.04.422.072.635.1.225.03.452.052.679.068.24.018.481.028.723.028.242 0 .483-.01.723-.028.227-.016.454-.038.679-.068.213-.028.425-.06.635-.1.202-.037.402-.079.6-.127.197-.047.393-.098.587-.155.193-.055.384-.115.573-.18.196-.067.39-.139.58-.217.202-.082.403-.169.6-.262.218-.103.434-.212.644-.33.301-.168.595-.351.879-.548.345-.237.678-.494.995-.77.335-.292.654-.605.95-.938.311-.35.599-.722.86-1.114.324-.488.608-1.006.846-1.546.271-.616.49-1.258.649-1.92.174-.729.278-1.477.31-2.234.03-.718-.014-1.439-.132-2.143-.114-.68-.306-1.342-.57-1.97-.252-.596-.575-1.157-.958-1.672-.37-.497-.797-.947-1.273-1.347-.978-.83-2.152-1.412-3.46-1.71-.631-.143-1.28-.211-1.933-.225C12.815.001 12.659 0 12.504 0z"/></svg>`,
29
+ chromeos: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C8.21 0 4.831 1.757 2.632 4.501l3.953 6.848A5.454 5.454 0 0 1 12 6.545h10.691A12 12 0 0 0 12 0zM1.931 5.47A11.943 11.943 0 0 0 0 12c0 6.012 4.42 10.991 10.189 11.864l3.953-6.847a5.45 5.45 0 0 1-6.865-2.29zm13.342 2.166a5.446 5.446 0 0 1 1.45 7.09l.002.001h-.002l-3.952 6.848a12.014 12.014 0 0 0 9.229-9.006zM12 16.364a4.364 4.364 0 1 1 0-8.728 4.364 4.364 0 0 1 0 8.728z"/></svg>`,
30
+ other: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>`,
31
+ }
32
+
33
+ const osColors: Record<string, string> = {
34
+ windows: 'bg-blue-100 text-blue-600',
35
+ macos: 'bg-gray-100 text-gray-700',
36
+ ios: 'bg-gray-100 text-gray-700',
37
+ android: 'bg-green-100 text-green-600',
38
+ linux: 'bg-amber-100 text-amber-600',
39
+ chromeos: 'bg-yellow-100 text-yellow-600',
40
+ other: 'bg-gray-100 text-gray-600',
41
+ }
42
+
43
+ const sortedSystems = [...systems].sort((a, b) => b.value - a.value)
44
+
45
+ function getOSIcon(name: string): string {
46
+ const key = name.toLowerCase().replace(/\s+/g, '')
47
+ if (key.includes('windows'))
48
+ return osIcons.windows
49
+ if (key.includes('mac') || key.includes('osx'))
50
+ return osIcons.macos
51
+ if (key.includes('ios') || key.includes('iphone') || key.includes('ipad'))
52
+ return osIcons.ios
53
+ if (key.includes('android'))
54
+ return osIcons.android
55
+ if (key.includes('linux') || key.includes('ubuntu') || key.includes('debian'))
56
+ return osIcons.linux
57
+ if (key.includes('chrome'))
58
+ return osIcons.chromeos
59
+ return osIcons.other
60
+ }
61
+
62
+ function getOSColor(name: string): string {
63
+ const key = name.toLowerCase().replace(/\s+/g, '')
64
+ if (key.includes('windows'))
65
+ return osColors.windows
66
+ if (key.includes('mac') || key.includes('osx'))
67
+ return osColors.macos
68
+ if (key.includes('ios') || key.includes('iphone') || key.includes('ipad'))
69
+ return osColors.ios
70
+ if (key.includes('android'))
71
+ return osColors.android
72
+ if (key.includes('linux') || key.includes('ubuntu') || key.includes('debian'))
73
+ return osColors.linux
74
+ if (key.includes('chrome'))
75
+ return osColors.chromeos
76
+ return osColors.other
77
+ }
78
+ </script>
79
+
80
+ <div class="os-breakdown card-hover p-6">
81
+ <h3 class="text-sm font-medium text-gray-900 mb-4">
82
+ Operating Systems
83
+ </h3>
84
+
85
+ @if (loading)
86
+ <div class="space-y-4">
87
+ @foreach ([1, 2, 3, 4] as i)
88
+ <div class="animate-pulse flex items-center gap-4">
89
+ <div class="skeleton w-10 h-10 rounded-lg"></div>
90
+ <div class="flex-1">
91
+ <div class="skeleton h-4 w-20 mb-2"></div>
92
+ <div class="skeleton h-2 w-full"></div>
93
+ </div>
94
+ </div>
95
+ @endforeach
96
+ </div>
97
+ @elseif (!systems.length)
98
+ <div class="text-center py-8 text-gray-500">
99
+ No OS data available
100
+ </div>
101
+ @else
102
+ <div class="space-y-4">
103
+ @foreach (sortedSystems as system)
104
+ <div class="flex items-center gap-4">
105
+ <div
106
+ class="w-10 h-10 rounded-lg flex items-center justify-center {{ getOSColor(system.name) }}"
107
+ >{!! getOSIcon(system.name) !!}</div>
108
+ <div class="flex-1">
109
+ <div class="flex items-center justify-between">
110
+ <span class="font-medium text-gray-900">{{ system.name }}</span>
111
+ <span class="text-sm text-gray-600">{{ formatPercentage(system.percentage) }}</span>
112
+ </div>
113
+ <div class="progress-bar mt-1">
114
+ <div
115
+ class="progress-fill bg-gray-400"
116
+ style="width: {{ system.percentage * 100 }}%"
117
+ ></div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ @endforeach
122
+ </div>
123
+ @endif
124
+ </div>
@@ -0,0 +1,102 @@
1
+ <script server>
2
+ /**
3
+ * PageDetailCard Component
4
+ *
5
+ * Displays detailed metrics for a specific page.
6
+ */
7
+ import { defineProps, withDefaults } from 'stx'
8
+ import { formatCompact, formatDuration, formatPercentage } from '../utils'
9
+
10
+ export interface PageDetail {
11
+ path: string
12
+ title?: string
13
+ pageViews: number
14
+ uniquePageViews: number
15
+ avgTimeOnPage: number
16
+ bounceRate: number
17
+ exitRate: number
18
+ entrances: number
19
+ exits: number
20
+ change?: number
21
+ }
22
+
23
+ interface PageDetailCardProps {
24
+ page: PageDetail | null
25
+ loading?: boolean
26
+ showSparkline?: boolean
27
+ }
28
+
29
+ const { page, loading, showSparkline } = withDefaults(
30
+ defineProps<PageDetailCardProps>(),
31
+ {
32
+ loading: false,
33
+ showSparkline: false,
34
+ }
35
+ )
36
+
37
+ const metrics = page ? [
38
+ { label: 'Page Views', value: formatCompact(page.pageViews), icon: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z', isNegative: false },
39
+ { label: 'Unique Views', value: formatCompact(page.uniquePageViews), icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z', isNegative: false },
40
+ { label: 'Avg. Time', value: formatDuration(page.avgTimeOnPage), icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', isNegative: false },
41
+ { label: 'Bounce Rate', value: formatPercentage(page.bounceRate), icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6', isNegative: page.bounceRate > 0.5 },
42
+ { label: 'Exit Rate', value: formatPercentage(page.exitRate), icon: 'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1', isNegative: false },
43
+ { label: 'Entrances', value: formatCompact(page.entrances), icon: 'M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1', isNegative: false },
44
+ ] : []
45
+ </script>
46
+
47
+ <div class="page-detail-card card-hover p-6">
48
+ @if (loading)
49
+ <div class="animate-pulse">
50
+ <div class="skeleton h-6 w-3/4 mb-2"></div>
51
+ <div class="skeleton h-4 w-1/2 mb-6"></div>
52
+ <div class="grid grid-cols-3 gap-4">
53
+ @foreach ([1, 2, 3, 4, 5, 6] as i)
54
+ <div>
55
+ <div class="skeleton h-4 w-16 mb-2"></div>
56
+ <div class="skeleton h-6 w-12"></div>
57
+ </div>
58
+ @endforeach
59
+ </div>
60
+ </div>
61
+ @elseif (!page)
62
+ <div class="text-center py-8 text-gray-500">
63
+ Select a page to view details
64
+ </div>
65
+ @else
66
+ <div class="mb-6">
67
+ <div class="flex items-start justify-between gap-4">
68
+ <div class="min-w-0">
69
+ <h3 class="text-lg font-semibold text-gray-900 truncate" title="{{ page.path }}">
70
+ {{ page.title ? page.title : page.path }}
71
+ </h3>
72
+ <p class="text-sm text-gray-500 truncate mt-0.5" title="{{ page.path }}">
73
+ {{ page.path }}
74
+ </p>
75
+ </div>
76
+ @if (page.change !== undefined)
77
+ <span
78
+ class="flex-shrink-0 px-2 py-1 text-xs font-medium rounded-full {{ page.change >= 0 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' }}"
79
+ >
80
+ {{ page.change >= 0 ? '+' : '' }}{{ page.change.toFixed(1) }}%
81
+ </span>
82
+ @endif
83
+ </div>
84
+ </div>
85
+
86
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
87
+ @foreach (metrics as metric)
88
+ <div class="p-3 bg-gray-50 rounded-lg">
89
+ <div class="flex items-center gap-2 text-xs text-gray-500 mb-1">
90
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ metric.icon }}" />
92
+ </svg>
93
+ {{ metric.label }}
94
+ </div>
95
+ <div class="text-lg font-semibold {{ metric.isNegative ? 'text-red-600' : 'text-gray-900' }}">
96
+ {{ metric.value }}
97
+ </div>
98
+ </div>
99
+ @endforeach
100
+ </div>
101
+ @endif
102
+ </div>