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