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