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