@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.
- package/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- package/src/version.ts +7 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* AlertCard Component
|
|
4
|
+
*
|
|
5
|
+
* Displays alerts for threshold breaches or important notifications.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import { formatCompact } from '../utils'
|
|
9
|
+
|
|
10
|
+
export type AlertSeverity = 'info' | 'success' | 'warning' | 'danger'
|
|
11
|
+
|
|
12
|
+
export interface Alert {
|
|
13
|
+
id: string
|
|
14
|
+
title: string
|
|
15
|
+
message: string
|
|
16
|
+
severity: AlertSeverity
|
|
17
|
+
metric?: string
|
|
18
|
+
value?: number
|
|
19
|
+
threshold?: number
|
|
20
|
+
timestamp?: Date
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AlertCardProps {
|
|
24
|
+
alerts: Alert[]
|
|
25
|
+
title?: string
|
|
26
|
+
loading?: boolean
|
|
27
|
+
maxAlerts?: number
|
|
28
|
+
dismissible?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { alerts, title, loading, maxAlerts, dismissible } = withDefaults(
|
|
32
|
+
defineProps<AlertCardProps>(),
|
|
33
|
+
{
|
|
34
|
+
title: 'Alerts',
|
|
35
|
+
loading: false,
|
|
36
|
+
maxAlerts: 5,
|
|
37
|
+
dismissible: true,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const displayedAlerts = alerts.slice(0, maxAlerts)
|
|
42
|
+
|
|
43
|
+
const severityConfig: Record<AlertSeverity, { bg: string, border: string, icon: string, text: string }> = {
|
|
44
|
+
info: {
|
|
45
|
+
bg: 'bg-blue-50',
|
|
46
|
+
border: 'border-blue-200',
|
|
47
|
+
icon: 'text-blue-500',
|
|
48
|
+
text: 'text-blue-800',
|
|
49
|
+
},
|
|
50
|
+
success: {
|
|
51
|
+
bg: 'bg-green-50',
|
|
52
|
+
border: 'border-green-200',
|
|
53
|
+
icon: 'text-green-500',
|
|
54
|
+
text: 'text-green-800',
|
|
55
|
+
},
|
|
56
|
+
warning: {
|
|
57
|
+
bg: 'bg-amber-50',
|
|
58
|
+
border: 'border-amber-200',
|
|
59
|
+
icon: 'text-amber-500',
|
|
60
|
+
text: 'text-amber-800',
|
|
61
|
+
},
|
|
62
|
+
danger: {
|
|
63
|
+
bg: 'bg-red-50',
|
|
64
|
+
border: 'border-red-200',
|
|
65
|
+
icon: 'text-red-500',
|
|
66
|
+
text: 'text-red-800',
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const severityIcons: Record<AlertSeverity, string> = {
|
|
71
|
+
info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
72
|
+
success: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
73
|
+
warning: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
|
|
74
|
+
danger: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatTimestamp(date?: Date): string {
|
|
78
|
+
if (!date)
|
|
79
|
+
return ''
|
|
80
|
+
const now = new Date()
|
|
81
|
+
const diff = now.getTime() - date.getTime()
|
|
82
|
+
const minutes = Math.floor(diff / 60000)
|
|
83
|
+
|
|
84
|
+
if (minutes < 1)
|
|
85
|
+
return 'Just now'
|
|
86
|
+
if (minutes < 60)
|
|
87
|
+
return `${minutes}m ago`
|
|
88
|
+
const hours = Math.floor(minutes / 60)
|
|
89
|
+
if (hours < 24)
|
|
90
|
+
return `${hours}h ago`
|
|
91
|
+
const days = Math.floor(hours / 24)
|
|
92
|
+
return `${days}d ago`
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<div class="alert-card card-hover p-6">
|
|
97
|
+
<div class="flex items-center justify-between mb-4">
|
|
98
|
+
<h3 class="text-sm font-medium text-gray-900">{{ title }}</h3>
|
|
99
|
+
@if (alerts.length > maxAlerts)
|
|
100
|
+
<span class="text-xs text-gray-500">
|
|
101
|
+
+{{ alerts.length - maxAlerts }} more
|
|
102
|
+
</span>
|
|
103
|
+
@endif
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
@if (loading)
|
|
107
|
+
<div class="space-y-3 animate-pulse">
|
|
108
|
+
@foreach ([1, 2, 3] as i)
|
|
109
|
+
<div class="skeleton h-16 rounded-lg"></div>
|
|
110
|
+
@endforeach
|
|
111
|
+
</div>
|
|
112
|
+
@elseif (!alerts.length)
|
|
113
|
+
<div class="text-center py-8">
|
|
114
|
+
<Icon name="check-circle" size="48" class="mx-auto text-green-300 mb-3" />
|
|
115
|
+
<p class="text-gray-500">All systems normal</p>
|
|
116
|
+
<p class="text-sm text-gray-400 mt-1">No alerts at this time</p>
|
|
117
|
+
</div>
|
|
118
|
+
@else
|
|
119
|
+
<script client>
|
|
120
|
+
const dismissedAlerts = state([])
|
|
121
|
+
</script>
|
|
122
|
+
<div class="space-y-3">
|
|
123
|
+
@foreach (displayedAlerts as alert)
|
|
124
|
+
<div
|
|
125
|
+
class="p-3 rounded-lg border cursor-pointer transition-all hover:shadow-sm {{ severityConfig[alert.severity].bg }} {{ severityConfig[alert.severity].border }}"
|
|
126
|
+
:show="!dismissedAlerts().includes('{{ alert.id }}')"
|
|
127
|
+
>
|
|
128
|
+
<div class="flex items-start gap-3">
|
|
129
|
+
<svg
|
|
130
|
+
class="w-5 h-5 flex-shrink-0 mt-0.5 {{ severityConfig[alert.severity].icon }}"
|
|
131
|
+
fill="none"
|
|
132
|
+
stroke="currentColor"
|
|
133
|
+
viewBox="0 0 24 24"
|
|
134
|
+
>
|
|
135
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ severityIcons[alert.severity] }}" />
|
|
136
|
+
</svg>
|
|
137
|
+
|
|
138
|
+
<div class="flex-1 min-w-0">
|
|
139
|
+
<div class="flex items-start justify-between gap-2">
|
|
140
|
+
<h4 class="text-sm font-medium {{ severityConfig[alert.severity].text }}">
|
|
141
|
+
{{ alert.title }}
|
|
142
|
+
</h4>
|
|
143
|
+
@if (dismissible)
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
class="flex-shrink-0 p-1 rounded hover:bg-black/5 transition-colors {{ severityConfig[alert.severity].text }}"
|
|
147
|
+
@click.stop="dismissedAlerts.set([...dismissedAlerts(), '{{ alert.id }}'])"
|
|
148
|
+
>
|
|
149
|
+
<Icon name="x" size="16" />
|
|
150
|
+
</button>
|
|
151
|
+
@endif
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<p class="text-sm mt-0.5 {{ severityConfig[alert.severity].text }}" style="opacity: 0.8">
|
|
155
|
+
{{ alert.message }}
|
|
156
|
+
</p>
|
|
157
|
+
|
|
158
|
+
@if (alert.value !== undefined)
|
|
159
|
+
<div class="flex items-center gap-4 mt-2 text-xs {{ severityConfig[alert.severity].text }}" style="opacity: 0.7">
|
|
160
|
+
@if (alert.metric)
|
|
161
|
+
<span>{{ alert.metric }}: {{ formatCompact(alert.value) }}</span>
|
|
162
|
+
@endif
|
|
163
|
+
@if (alert.threshold)
|
|
164
|
+
<span>Threshold: {{ formatCompact(alert.threshold) }}</span>
|
|
165
|
+
@endif
|
|
166
|
+
@if (alert.timestamp)
|
|
167
|
+
<span>{{ formatTimestamp(alert.timestamp) }}</span>
|
|
168
|
+
@endif
|
|
169
|
+
</div>
|
|
170
|
+
@endif
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
@endforeach
|
|
175
|
+
</div>
|
|
176
|
+
@endif
|
|
177
|
+
</div>
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* AnalyticsDashboard Component
|
|
4
|
+
*
|
|
5
|
+
* Main dashboard layout combining all analytics components.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import type {
|
|
9
|
+
AnalyticsApiConfig,
|
|
10
|
+
DashboardSummary,
|
|
11
|
+
TimeSeriesDataPoint,
|
|
12
|
+
TopItem,
|
|
13
|
+
} from '../types'
|
|
14
|
+
|
|
15
|
+
interface AnalyticsDashboardProps {
|
|
16
|
+
config: AnalyticsApiConfig
|
|
17
|
+
refreshInterval?: number
|
|
18
|
+
realtimeInterval?: number
|
|
19
|
+
stats?: DashboardSummary | null
|
|
20
|
+
timeSeries?: TimeSeriesDataPoint[]
|
|
21
|
+
topPages?: TopItem[]
|
|
22
|
+
topReferrers?: TopItem[]
|
|
23
|
+
devices?: TopItem[]
|
|
24
|
+
realtimeVisitors?: number
|
|
25
|
+
realtimePageViews?: number
|
|
26
|
+
loading?: boolean
|
|
27
|
+
error?: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
config,
|
|
32
|
+
refreshInterval,
|
|
33
|
+
realtimeInterval,
|
|
34
|
+
stats,
|
|
35
|
+
timeSeries,
|
|
36
|
+
topPages,
|
|
37
|
+
topReferrers,
|
|
38
|
+
devices,
|
|
39
|
+
realtimeVisitors,
|
|
40
|
+
realtimePageViews,
|
|
41
|
+
loading,
|
|
42
|
+
error,
|
|
43
|
+
} = withDefaults(
|
|
44
|
+
defineProps<AnalyticsDashboardProps>(),
|
|
45
|
+
{
|
|
46
|
+
refreshInterval: 60000,
|
|
47
|
+
realtimeInterval: 5000,
|
|
48
|
+
stats: null,
|
|
49
|
+
timeSeries: () => [],
|
|
50
|
+
topPages: () => [],
|
|
51
|
+
topReferrers: () => [],
|
|
52
|
+
devices: () => [],
|
|
53
|
+
realtimeVisitors: 0,
|
|
54
|
+
realtimePageViews: 0,
|
|
55
|
+
loading: false,
|
|
56
|
+
error: null,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<script client>
|
|
62
|
+
const isLoading = state({{ loading ? 'true' : 'false' }})
|
|
63
|
+
const dateRange = state('7d')
|
|
64
|
+
|
|
65
|
+
function refresh() {
|
|
66
|
+
isLoading.set(true)
|
|
67
|
+
document.dispatchEvent(new CustomEvent('refresh'))
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
<div
|
|
71
|
+
class="analytics-dashboard min-h-screen bg-gray-50 p-6"
|
|
72
|
+
style="max-width: 1400px; margin: 0 auto"
|
|
73
|
+
>
|
|
74
|
+
<header class="flex items-center justify-between mb-6">
|
|
75
|
+
<div>
|
|
76
|
+
<h1 class="text-2xl font-bold text-gray-900">
|
|
77
|
+
Analytics Dashboard
|
|
78
|
+
</h1>
|
|
79
|
+
<p class="text-gray-500 text-sm mt-1">
|
|
80
|
+
Track your website performance
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="flex items-center gap-4">
|
|
85
|
+
<select
|
|
86
|
+
class="bg-white border border-gray-200 text-gray-900 px-4 py-2 rounded-lg text-sm cursor-pointer hover:border-indigo-500"
|
|
87
|
+
:model="dateRange"
|
|
88
|
+
@change="document.dispatchEvent(new CustomEvent('date-change', { detail: { range: dateRange() } }))"
|
|
89
|
+
>
|
|
90
|
+
<option value="today">Today</option>
|
|
91
|
+
<option value="yesterday">Yesterday</option>
|
|
92
|
+
<option value="7d">Last 7 days</option>
|
|
93
|
+
<option value="30d">Last 30 days</option>
|
|
94
|
+
<option value="90d">Last 90 days</option>
|
|
95
|
+
</select>
|
|
96
|
+
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
class="btn-icon p-2 rounded-lg hover:bg-gray-100"
|
|
100
|
+
title="Refresh"
|
|
101
|
+
x-disabled="isLoading"
|
|
102
|
+
@click="refresh()"
|
|
103
|
+
>
|
|
104
|
+
<Icon name="refresh-cw" size="20" x-class="{ 'animate-spin': isLoading() }" />
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
</header>
|
|
108
|
+
|
|
109
|
+
@if (error)
|
|
110
|
+
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
|
|
111
|
+
<p class="font-medium">Error loading analytics</p>
|
|
112
|
+
<p class="text-sm mt-1">{{ error }}</p>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
class="mt-2 text-sm text-red-600 hover:text-red-800 underline"
|
|
116
|
+
@click="refresh()"
|
|
117
|
+
>
|
|
118
|
+
Try again
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
@endif
|
|
122
|
+
|
|
123
|
+
<section class="mb-6">
|
|
124
|
+
<div class="realtime-counter card-hover p-6">
|
|
125
|
+
<div class="flex items-center gap-3">
|
|
126
|
+
<div class="relative">
|
|
127
|
+
<div class="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div>
|
|
128
|
+
<div class="absolute inset-0 w-3 h-3 rounded-full bg-green-500 animate-ping"></div>
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<span class="text-4xl font-bold text-gray-900">{{ realtimeVisitors }}</span>
|
|
132
|
+
<span class="ml-2 text-gray-500 text-sm">visitors right now</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
@if (realtimePageViews > 0)
|
|
136
|
+
<p class="mt-2 text-sm text-gray-500">
|
|
137
|
+
{{ realtimePageViews }} page views in the last hour
|
|
138
|
+
</p>
|
|
139
|
+
@endif
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<section class="dashboard-grid mb-6">
|
|
144
|
+
<div class="stat-card card-hover p-6">
|
|
145
|
+
<h3 class="text-sm font-medium text-gray-500">Page Views</h3>
|
|
146
|
+
@if (loading)
|
|
147
|
+
<div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
|
|
148
|
+
@else
|
|
149
|
+
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats?.pageViews ?? 0 }}</p>
|
|
150
|
+
@if (stats?.change?.pageViews !== undefined)
|
|
151
|
+
<p class="text-sm mt-1 {{ stats.change.pageViews >= 0 ? 'text-green-600' : 'text-red-600' }}">
|
|
152
|
+
{{ stats.change.pageViews >= 0 ? '+' : '' }}{{ stats.change.pageViews.toFixed(1) }}% vs previous period
|
|
153
|
+
</p>
|
|
154
|
+
@endif
|
|
155
|
+
@endif
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="stat-card card-hover p-6">
|
|
159
|
+
<h3 class="text-sm font-medium text-gray-500">Unique Visitors</h3>
|
|
160
|
+
@if (loading)
|
|
161
|
+
<div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
|
|
162
|
+
@else
|
|
163
|
+
<p class="text-3xl font-bold text-gray-900 mt-2">{{ stats?.uniqueVisitors ?? 0 }}</p>
|
|
164
|
+
@if (stats?.change?.uniqueVisitors !== undefined)
|
|
165
|
+
<p class="text-sm mt-1 {{ stats.change.uniqueVisitors >= 0 ? 'text-green-600' : 'text-red-600' }}">
|
|
166
|
+
{{ stats.change.uniqueVisitors >= 0 ? '+' : '' }}{{ stats.change.uniqueVisitors.toFixed(1) }}% vs previous period
|
|
167
|
+
</p>
|
|
168
|
+
@endif
|
|
169
|
+
@endif
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="stat-card card-hover p-6">
|
|
173
|
+
<h3 class="text-sm font-medium text-gray-500">Bounce Rate</h3>
|
|
174
|
+
@if (loading)
|
|
175
|
+
<div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
|
|
176
|
+
@else
|
|
177
|
+
<p class="text-3xl font-bold text-gray-900 mt-2">{{ ((stats?.bounceRate ?? 0) * 100).toFixed(1) }}%</p>
|
|
178
|
+
@if (stats?.change?.bounceRate !== undefined)
|
|
179
|
+
<p class="text-sm mt-1 {{ stats.change.bounceRate <= 0 ? 'text-green-600' : 'text-red-600' }}">
|
|
180
|
+
{{ stats.change.bounceRate >= 0 ? '+' : '' }}{{ stats.change.bounceRate.toFixed(1) }}% vs previous period
|
|
181
|
+
</p>
|
|
182
|
+
@endif
|
|
183
|
+
@endif
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="stat-card card-hover p-6">
|
|
187
|
+
<h3 class="text-sm font-medium text-gray-500">Avg. Session Duration</h3>
|
|
188
|
+
@if (loading)
|
|
189
|
+
<div class="animate-pulse"><div class="skeleton h-8 w-24 mt-2"></div></div>
|
|
190
|
+
@else
|
|
191
|
+
<p class="text-3xl font-bold text-gray-900 mt-2">{{ Math.floor((stats?.avgSessionDuration ?? 0) / 60) }}m {{ Math.floor((stats?.avgSessionDuration ?? 0) % 60) }}s</p>
|
|
192
|
+
@if (stats?.change?.avgSessionDuration !== undefined)
|
|
193
|
+
<p class="text-sm mt-1 {{ stats.change.avgSessionDuration >= 0 ? 'text-green-600' : 'text-red-600' }}">
|
|
194
|
+
{{ stats.change.avgSessionDuration >= 0 ? '+' : '' }}{{ stats.change.avgSessionDuration.toFixed(1) }}% vs previous period
|
|
195
|
+
</p>
|
|
196
|
+
@endif
|
|
197
|
+
@endif
|
|
198
|
+
</div>
|
|
199
|
+
</section>
|
|
200
|
+
|
|
201
|
+
<section class="mb-6">
|
|
202
|
+
<div class="card-hover p-6">
|
|
203
|
+
<h3 class="text-sm font-medium text-gray-900 mb-4">Traffic Overview</h3>
|
|
204
|
+
@if (loading)
|
|
205
|
+
<div class="animate-pulse"><div class="skeleton h-80 w-full"></div></div>
|
|
206
|
+
@elseif (timeSeries.length === 0)
|
|
207
|
+
<div class="flex items-center justify-center h-80 text-gray-400">
|
|
208
|
+
<p>No data available for selected period</p>
|
|
209
|
+
</div>
|
|
210
|
+
@else
|
|
211
|
+
<script client>
|
|
212
|
+
onMount(() => {
|
|
213
|
+
const canvas = document.getElementById('trafficChart')
|
|
214
|
+
const ctx = canvas?.getContext('2d')
|
|
215
|
+
if (!ctx) return
|
|
216
|
+
|
|
217
|
+
const chartData = {{ JSON.stringify(timeSeries) }}
|
|
218
|
+
const labels = chartData.map(d => new Date(d.date).toLocaleDateString())
|
|
219
|
+
|
|
220
|
+
new Chart(ctx, {
|
|
221
|
+
type: 'line',
|
|
222
|
+
data: {
|
|
223
|
+
labels,
|
|
224
|
+
datasets: [
|
|
225
|
+
{
|
|
226
|
+
label: 'Page Views',
|
|
227
|
+
data: chartData.map(d => d.pageViews || d.views || 0),
|
|
228
|
+
borderColor: '#6366f1',
|
|
229
|
+
backgroundColor: '#6366f120',
|
|
230
|
+
fill: true,
|
|
231
|
+
tension: 0.3,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
label: 'Unique Visitors',
|
|
235
|
+
data: chartData.map(d => d.uniqueVisitors || d.visitors || 0),
|
|
236
|
+
borderColor: '#22c55e',
|
|
237
|
+
backgroundColor: '#22c55e20',
|
|
238
|
+
fill: true,
|
|
239
|
+
tension: 0.3,
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
options: {
|
|
244
|
+
responsive: true,
|
|
245
|
+
maintainAspectRatio: false,
|
|
246
|
+
plugins: { legend: { display: true, position: 'top' } },
|
|
247
|
+
scales: {
|
|
248
|
+
y: { beginAtZero: true, grid: { color: '#e5e7eb' } },
|
|
249
|
+
x: { grid: { display: false } }
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
</script>
|
|
255
|
+
<canvas
|
|
256
|
+
id="trafficChart"
|
|
257
|
+
style="height: 320px"
|
|
258
|
+
></canvas>
|
|
259
|
+
@endif
|
|
260
|
+
</div>
|
|
261
|
+
</section>
|
|
262
|
+
|
|
263
|
+
<section class="dashboard-grid-3">
|
|
264
|
+
<div class="card-hover p-6">
|
|
265
|
+
<h3 class="text-sm font-medium text-gray-900 mb-4">Top Pages</h3>
|
|
266
|
+
@if (loading)
|
|
267
|
+
<div class="space-y-3">
|
|
268
|
+
@foreach ([1, 2, 3, 4, 5] as i)
|
|
269
|
+
<div class="animate-pulse">
|
|
270
|
+
<div class="skeleton h-4 w-full mb-1"></div>
|
|
271
|
+
<div class="skeleton h-2 w-3/4"></div>
|
|
272
|
+
</div>
|
|
273
|
+
@endforeach
|
|
274
|
+
</div>
|
|
275
|
+
@elseif (topPages.length === 0)
|
|
276
|
+
<p class="text-gray-500 text-center py-8">No page views yet</p>
|
|
277
|
+
@else
|
|
278
|
+
<div class="space-y-3">
|
|
279
|
+
@foreach (topPages as page)
|
|
280
|
+
<div>
|
|
281
|
+
<div class="flex justify-between text-sm mb-1">
|
|
282
|
+
<span class="text-gray-900 truncate" title="{{ page.name }}">{{ page.name }}</span>
|
|
283
|
+
<span class="text-gray-500">{{ page.value }}</span>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="progress-bar">
|
|
286
|
+
<div class="progress-fill bg-indigo-500" style="width: {{ page.percentage * 100 }}%"></div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
@endforeach
|
|
290
|
+
</div>
|
|
291
|
+
@endif
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div class="card-hover p-6">
|
|
295
|
+
<h3 class="text-sm font-medium text-gray-900 mb-4">Top Referrers</h3>
|
|
296
|
+
@if (loading)
|
|
297
|
+
<div class="space-y-3">
|
|
298
|
+
@foreach ([1, 2, 3, 4, 5] as i)
|
|
299
|
+
<div class="animate-pulse">
|
|
300
|
+
<div class="skeleton h-4 w-full mb-1"></div>
|
|
301
|
+
<div class="skeleton h-2 w-3/4"></div>
|
|
302
|
+
</div>
|
|
303
|
+
@endforeach
|
|
304
|
+
</div>
|
|
305
|
+
@elseif (topReferrers.length === 0)
|
|
306
|
+
<p class="text-gray-500 text-center py-8">No referrer data</p>
|
|
307
|
+
@else
|
|
308
|
+
<div class="space-y-3">
|
|
309
|
+
@foreach (topReferrers as referrer)
|
|
310
|
+
<div>
|
|
311
|
+
<div class="flex justify-between text-sm mb-1">
|
|
312
|
+
<span class="text-gray-900 truncate" title="{{ referrer.name }}">{{ referrer.name }}</span>
|
|
313
|
+
<span class="text-gray-500">{{ referrer.value }}</span>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="progress-bar">
|
|
316
|
+
<div class="progress-fill bg-indigo-500" style="width: {{ referrer.percentage * 100 }}%"></div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
@endforeach
|
|
320
|
+
</div>
|
|
321
|
+
@endif
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div class="card-hover p-6">
|
|
325
|
+
<h3 class="text-sm font-medium text-gray-900 mb-4">Devices</h3>
|
|
326
|
+
@if (loading)
|
|
327
|
+
<div class="space-y-3">
|
|
328
|
+
@foreach ([1, 2, 3] as i)
|
|
329
|
+
<div class="animate-pulse">
|
|
330
|
+
<div class="skeleton h-4 w-full mb-1"></div>
|
|
331
|
+
<div class="skeleton h-2 w-3/4"></div>
|
|
332
|
+
</div>
|
|
333
|
+
@endforeach
|
|
334
|
+
</div>
|
|
335
|
+
@elseif (devices.length === 0)
|
|
336
|
+
<p class="text-gray-500 text-center py-8">No device data</p>
|
|
337
|
+
@else
|
|
338
|
+
<div class="space-y-3">
|
|
339
|
+
@foreach (devices as device)
|
|
340
|
+
<div>
|
|
341
|
+
<div class="flex justify-between text-sm mb-1">
|
|
342
|
+
<span class="text-gray-900">{{ device.name }}</span>
|
|
343
|
+
<span class="text-gray-500">{{ (device.percentage * 100).toFixed(1) }}%</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="progress-bar">
|
|
346
|
+
<div class="progress-fill bg-indigo-500" style="width: {{ device.percentage * 100 }}%"></div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
@endforeach
|
|
350
|
+
</div>
|
|
351
|
+
@endif
|
|
352
|
+
</div>
|
|
353
|
+
</section>
|
|
354
|
+
</div>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* AnimatedNumber Component
|
|
4
|
+
*
|
|
5
|
+
* Animated counting number with formatting options.
|
|
6
|
+
* Uses Alpine.js for client-side animation.
|
|
7
|
+
*/
|
|
8
|
+
import { defineProps, withDefaults } from 'stx'
|
|
9
|
+
import { formatCompact, formatCurrency, formatPercentage } from '../utils'
|
|
10
|
+
|
|
11
|
+
interface AnimatedNumberProps {
|
|
12
|
+
value: number
|
|
13
|
+
format?: 'number' | 'compact' | 'percentage' | 'currency'
|
|
14
|
+
duration?: number
|
|
15
|
+
decimals?: number
|
|
16
|
+
prefix?: string
|
|
17
|
+
suffix?: string
|
|
18
|
+
animate?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { value, format, duration, decimals, prefix, suffix, animate } = withDefaults(
|
|
22
|
+
defineProps<AnimatedNumberProps>(),
|
|
23
|
+
{
|
|
24
|
+
format: 'compact',
|
|
25
|
+
duration: 1000,
|
|
26
|
+
decimals: 0,
|
|
27
|
+
prefix: '',
|
|
28
|
+
suffix: '',
|
|
29
|
+
animate: true,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
function formatNumber(num: number): string {
|
|
34
|
+
switch (format) {
|
|
35
|
+
case 'percentage':
|
|
36
|
+
return formatPercentage(num)
|
|
37
|
+
case 'currency':
|
|
38
|
+
return formatCurrency(num)
|
|
39
|
+
case 'compact':
|
|
40
|
+
return formatCompact(num)
|
|
41
|
+
default:
|
|
42
|
+
return num.toLocaleString(undefined, {
|
|
43
|
+
minimumFractionDigits: decimals,
|
|
44
|
+
maximumFractionDigits: decimals,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const formattedValue = `${prefix}${formatNumber(value)}${suffix}`
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
@if (animate)
|
|
53
|
+
<script client>
|
|
54
|
+
const current = state(0)
|
|
55
|
+
const isAnimating = state(true)
|
|
56
|
+
|
|
57
|
+
function formatNum(n) {
|
|
58
|
+
return '{{ prefix }}' + n.toLocaleString() + '{{ suffix }}'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onMount(() => {
|
|
62
|
+
const target = {{ value }}
|
|
63
|
+
const animDuration = {{ duration }}
|
|
64
|
+
const start = performance.now()
|
|
65
|
+
const step = (now) => {
|
|
66
|
+
const elapsed = now - start
|
|
67
|
+
const progress = Math.min(elapsed / animDuration, 1)
|
|
68
|
+
const eased = 1 - Math.pow(1 - progress, 3)
|
|
69
|
+
current.set(Math.round(eased * target))
|
|
70
|
+
if (progress < 1) {
|
|
71
|
+
requestAnimationFrame(step)
|
|
72
|
+
} else {
|
|
73
|
+
isAnimating.set(false)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
requestAnimationFrame(step)
|
|
77
|
+
})
|
|
78
|
+
</script>
|
|
79
|
+
<span
|
|
80
|
+
class="animated-number tabular-nums"
|
|
81
|
+
:text="formatNum(current())"
|
|
82
|
+
x-class="{ 'opacity-80': isAnimating() }"
|
|
83
|
+
>{{ formattedValue }}</span>
|
|
84
|
+
@else
|
|
85
|
+
<span class="animated-number tabular-nums">{{ formattedValue }}</span>
|
|
86
|
+
@endif
|