@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,220 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* BarChart Component
|
|
4
|
+
*
|
|
5
|
+
* Horizontal or vertical bar chart for comparing values.
|
|
6
|
+
* Uses Canvas for rendering with Alpine.js for initialization.
|
|
7
|
+
*/
|
|
8
|
+
import { defineProps, withDefaults } from 'stx'
|
|
9
|
+
import type { DashboardTheme } from '../types'
|
|
10
|
+
import { defaultTheme } from '../types'
|
|
11
|
+
import { formatCompact } from '../utils'
|
|
12
|
+
|
|
13
|
+
export interface BarChartItem {
|
|
14
|
+
label: string
|
|
15
|
+
value: number
|
|
16
|
+
color?: string
|
|
17
|
+
previousValue?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BarChartProps {
|
|
21
|
+
data: BarChartItem[]
|
|
22
|
+
title?: string
|
|
23
|
+
height?: number
|
|
24
|
+
orientation?: 'horizontal' | 'vertical'
|
|
25
|
+
showValues?: boolean
|
|
26
|
+
showComparison?: boolean
|
|
27
|
+
loading?: boolean
|
|
28
|
+
theme?: DashboardTheme
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { data, title, height, orientation, showValues, showComparison, loading, theme } = withDefaults(
|
|
32
|
+
defineProps<BarChartProps>(),
|
|
33
|
+
{
|
|
34
|
+
height: 300,
|
|
35
|
+
orientation: 'horizontal',
|
|
36
|
+
showValues: true,
|
|
37
|
+
showComparison: false,
|
|
38
|
+
loading: false,
|
|
39
|
+
theme: () => defaultTheme,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const defaultColors = [
|
|
44
|
+
'#6366f1',
|
|
45
|
+
'#22c55e',
|
|
46
|
+
'#f59e0b',
|
|
47
|
+
'#ef4444',
|
|
48
|
+
'#8b5cf6',
|
|
49
|
+
'#06b6d4',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const maxValue = Math.max(...data.flatMap(d => [d.value, d.previousValue ?? 0]), 1)
|
|
53
|
+
|
|
54
|
+
const chartData = JSON.stringify(data)
|
|
55
|
+
const chartTheme = JSON.stringify(theme)
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class="bar-chart card-hover p-6">
|
|
59
|
+
@if (title)
|
|
60
|
+
<h3 class="text-sm font-medium text-gray-900 mb-4">
|
|
61
|
+
{{ title }}
|
|
62
|
+
</h3>
|
|
63
|
+
@endif
|
|
64
|
+
|
|
65
|
+
@if (loading)
|
|
66
|
+
<div class="animate-pulse" style="height: {{ height }}px">
|
|
67
|
+
<div class="skeleton h-full"></div>
|
|
68
|
+
</div>
|
|
69
|
+
@elseif (!data.length)
|
|
70
|
+
<div
|
|
71
|
+
class="flex items-center justify-center text-gray-500"
|
|
72
|
+
style="height: {{ height }}px"
|
|
73
|
+
>
|
|
74
|
+
No data available
|
|
75
|
+
</div>
|
|
76
|
+
@else
|
|
77
|
+
<script client>
|
|
78
|
+
const barChartData = {{ chartData }}
|
|
79
|
+
const barChartTheme = {{ chartTheme }}
|
|
80
|
+
const barChartMaxValue = {{ maxValue }}
|
|
81
|
+
const barChartOrientation = '{{ orientation }}'
|
|
82
|
+
const barChartShowValues = {{ showValues ? 'true' : 'false' }}
|
|
83
|
+
const barChartShowComparison = {{ showComparison ? 'true' : 'false' }}
|
|
84
|
+
const barChartDefaultColors = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
|
|
85
|
+
|
|
86
|
+
function formatCompactClient(val) {
|
|
87
|
+
if (val >= 1e6) return (val / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
|
|
88
|
+
if (val >= 1e3) return (val / 1e3).toFixed(1).replace(/\.0$/, '') + 'k'
|
|
89
|
+
return val.toLocaleString()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function drawHorizontalBars(ctx, width, height) {
|
|
93
|
+
const padding = { left: 100, right: 60, top: 20, bottom: 20 }
|
|
94
|
+
const chartWidth = width - padding.left - padding.right
|
|
95
|
+
const barH = barChartShowComparison ? 16 : 24
|
|
96
|
+
const barGap = barChartShowComparison ? 4 : 0
|
|
97
|
+
const groupHeight = barH * (barChartShowComparison ? 2 : 1) + barGap
|
|
98
|
+
const groupGap = 16
|
|
99
|
+
|
|
100
|
+
barChartData.forEach((item, index) => {
|
|
101
|
+
const y = padding.top + index * (groupHeight + groupGap)
|
|
102
|
+
const barWidth = (item.value / barChartMaxValue) * chartWidth
|
|
103
|
+
const color = item.color || barChartDefaultColors[index % barChartDefaultColors.length]
|
|
104
|
+
|
|
105
|
+
ctx.fillStyle = barChartTheme.text
|
|
106
|
+
ctx.font = '13px system-ui, sans-serif'
|
|
107
|
+
ctx.textAlign = 'right'
|
|
108
|
+
ctx.textBaseline = 'middle'
|
|
109
|
+
ctx.fillText(item.label, padding.left - 12, y + groupHeight / 2)
|
|
110
|
+
|
|
111
|
+
ctx.fillStyle = color
|
|
112
|
+
ctx.beginPath()
|
|
113
|
+
ctx.roundRect(padding.left, y, barWidth, barH, 4)
|
|
114
|
+
ctx.fill()
|
|
115
|
+
|
|
116
|
+
if (barChartShowComparison && item.previousValue !== undefined) {
|
|
117
|
+
const prevBarWidth = (item.previousValue / barChartMaxValue) * chartWidth
|
|
118
|
+
ctx.fillStyle = color + '60'
|
|
119
|
+
ctx.beginPath()
|
|
120
|
+
ctx.roundRect(padding.left, y + barH + barGap, prevBarWidth, barH, 4)
|
|
121
|
+
ctx.fill()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (barChartShowValues) {
|
|
125
|
+
ctx.fillStyle = barChartTheme.textSecondary
|
|
126
|
+
ctx.font = '12px system-ui, sans-serif'
|
|
127
|
+
ctx.textAlign = 'left'
|
|
128
|
+
ctx.fillText(formatCompactClient(item.value), padding.left + barWidth + 8, y + barH / 2)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function drawVerticalBars(ctx, width, height) {
|
|
134
|
+
const padding = { left: 50, right: 20, top: 20, bottom: 60 }
|
|
135
|
+
const chartWidth = width - padding.left - padding.right
|
|
136
|
+
const chartHeight = height - padding.top - padding.bottom
|
|
137
|
+
const barW = barChartShowComparison ? 20 : 32
|
|
138
|
+
const barGap = barChartShowComparison ? 4 : 0
|
|
139
|
+
const groupWidth = barW * (barChartShowComparison ? 2 : 1) + barGap
|
|
140
|
+
const groupGap = (chartWidth - groupWidth * barChartData.length) / (barChartData.length + 1)
|
|
141
|
+
|
|
142
|
+
ctx.strokeStyle = barChartTheme.border
|
|
143
|
+
ctx.lineWidth = 1
|
|
144
|
+
ctx.beginPath()
|
|
145
|
+
ctx.moveTo(padding.left, padding.top)
|
|
146
|
+
ctx.lineTo(padding.left, height - padding.bottom)
|
|
147
|
+
ctx.stroke()
|
|
148
|
+
|
|
149
|
+
barChartData.forEach((item, index) => {
|
|
150
|
+
const x = padding.left + groupGap + index * (groupWidth + groupGap)
|
|
151
|
+
const barHeight = (item.value / barChartMaxValue) * chartHeight
|
|
152
|
+
const color = item.color || barChartDefaultColors[index % barChartDefaultColors.length]
|
|
153
|
+
|
|
154
|
+
ctx.fillStyle = color
|
|
155
|
+
ctx.beginPath()
|
|
156
|
+
ctx.roundRect(x, height - padding.bottom - barHeight, barW, barHeight, [4, 4, 0, 0])
|
|
157
|
+
ctx.fill()
|
|
158
|
+
|
|
159
|
+
ctx.fillStyle = barChartTheme.textSecondary
|
|
160
|
+
ctx.font = '12px system-ui, sans-serif'
|
|
161
|
+
ctx.textAlign = 'center'
|
|
162
|
+
ctx.fillText(item.label, x + groupWidth / 2, height - padding.bottom + 16)
|
|
163
|
+
|
|
164
|
+
if (barChartShowValues) {
|
|
165
|
+
ctx.fillStyle = barChartTheme.text
|
|
166
|
+
ctx.fillText(formatCompactClient(item.value), x + barW / 2, height - padding.bottom - barHeight - 8)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function drawBarChart() {
|
|
172
|
+
const canvas = document.getElementById('barChartCanvas')
|
|
173
|
+
const ctx = canvas?.getContext('2d')
|
|
174
|
+
if (!ctx) return
|
|
175
|
+
|
|
176
|
+
const dpr = window.devicePixelRatio || 1
|
|
177
|
+
const rect = canvas.getBoundingClientRect()
|
|
178
|
+
|
|
179
|
+
canvas.width = rect.width * dpr
|
|
180
|
+
canvas.height = rect.height * dpr
|
|
181
|
+
ctx.scale(dpr, dpr)
|
|
182
|
+
|
|
183
|
+
const width = rect.width
|
|
184
|
+
const height = rect.height
|
|
185
|
+
ctx.clearRect(0, 0, width, height)
|
|
186
|
+
|
|
187
|
+
if (barChartOrientation === 'horizontal') {
|
|
188
|
+
drawHorizontalBars(ctx, width, height)
|
|
189
|
+
} else {
|
|
190
|
+
drawVerticalBars(ctx, width, height)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
onMount(() => {
|
|
195
|
+
drawBarChart()
|
|
196
|
+
const canvas = document.getElementById('barChartCanvas')
|
|
197
|
+
if (canvas?.parentElement) {
|
|
198
|
+
new ResizeObserver(() => drawBarChart()).observe(canvas.parentElement)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
</script>
|
|
202
|
+
<canvas
|
|
203
|
+
id="barChartCanvas"
|
|
204
|
+
style="width: 100%; height: {{ height }}px"
|
|
205
|
+
></canvas>
|
|
206
|
+
|
|
207
|
+
@if (showComparison && !loading && data.length)
|
|
208
|
+
<div class="flex items-center justify-end gap-4 mt-4 text-sm">
|
|
209
|
+
<div class="flex items-center gap-2">
|
|
210
|
+
<div class="w-3 h-3 rounded bg-primary-500"></div>
|
|
211
|
+
<span class="text-gray-600">Current period</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="flex items-center gap-2">
|
|
214
|
+
<div class="w-3 h-3 rounded bg-primary-500/40"></div>
|
|
215
|
+
<span class="text-gray-600">Previous period</span>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
@endif
|
|
219
|
+
@endif
|
|
220
|
+
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* BrowserBreakdown Component
|
|
4
|
+
*
|
|
5
|
+
* Displays browser usage breakdown with icons.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import type { TopItem } from '../types'
|
|
9
|
+
import { formatPercentage } from '../utils'
|
|
10
|
+
|
|
11
|
+
interface BrowserBreakdownProps {
|
|
12
|
+
browsers: TopItem[]
|
|
13
|
+
loading?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { browsers, loading } = withDefaults(
|
|
17
|
+
defineProps<BrowserBreakdownProps>(),
|
|
18
|
+
{
|
|
19
|
+
loading: false,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const browserIcons: Record<string, string> = {
|
|
24
|
+
chrome: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C8.21 0 4.831 1.757 2.632 4.501l3.953 6.848A5.454 5.454 0 0 1 12 6.545h10.691A12 12 0 0 0 12 0zM1.931 5.47A11.943 11.943 0 0 0 0 12c0 6.012 4.42 10.991 10.189 11.864l3.953-6.847a5.45 5.45 0 0 1-6.865-2.29zm13.342 2.166a5.446 5.446 0 0 1 1.45 7.09l.002.001h-.002l-3.952 6.848a12.014 12.014 0 0 0 9.229-9.006zM12 16.364a4.364 4.364 0 1 1 0-8.728 4.364 4.364 0 0 1 0 8.728z"/></svg>`,
|
|
25
|
+
firefox: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M8.824 7.287c.008 0 .004 0 0 0zm-2.8-1.4c.006 0 .003 0 0 0zm16.754 2.161c-.505-1.215-1.53-2.528-2.333-2.943.654 1.283 1.033 2.57 1.177 3.53l.002.02c-1.314-3.278-3.544-4.6-5.366-7.477-.091-.147-.184-.292-.273-.446a3.545 3.545 0 0 1-.13-.24 2.118 2.118 0 0 1-.172-.46.03.03 0 0 0-.027-.03.038.038 0 0 0-.021 0l-.006.001-.006.001-.008.001c-2.044 1.193-2.734 3.393-2.8 4.494a3.946 3.946 0 0 0-2.18.825 2.364 2.364 0 0 0-.203-.148 3.687 3.687 0 0 1-.07-1.959 5.706 5.706 0 0 0-1.89 1.458 3.95 3.95 0 0 0-.67-.063c-.014 0-.027-.001-.04-.001l-.062-.02a3.26 3.26 0 0 1-.07-.672c0-.453.057-.888.164-1.296l-.005-.006c-.396.224-.75.504-1.048.828-.327-.29-.617-.633-.864-1.025a6.485 6.485 0 0 0-.578-.265c-.081-.033-.166-.066-.254-.1-.1-.04-.201-.078-.306-.117-.1-.038-.204-.076-.309-.113l-.103-.037c-.101-.037-.204-.074-.309-.11C3.22 23.007.213 17.37 1.635 11.17c.336-1.504.855-2.726 1.616-3.744.247.392.537.735.864 1.025a5.02 5.02 0 0 1-.123 1.403 6.2 6.2 0 0 1 1.636-1.48c-.127.662-.046 1.351.233 1.96.08.175.175.341.284.496a2.384 2.384 0 0 1 .66-.617 2.11 2.11 0 0 1-.142-.686c-.007-.153.003-.306.028-.458.015-.1.037-.197.065-.293.055-.186.137-.362.243-.523.098-.148.212-.286.34-.41a5.44 5.44 0 0 1 .775-.612c.297-.196.623-.35.969-.46a3.949 3.949 0 0 1 1.14-.17h.031a3.94 3.94 0 0 1 1.14.169c.346.11.672.264.969.46.263.173.513.377.775.612.128.124.242.262.34.41.106.161.188.337.243.523.028.096.05.193.065.293.025.152.035.305.028.458a2.11 2.11 0 0 1-.142.686 2.384 2.384 0 0 1 .66.617c.109-.155.204-.321.284-.496.279-.609.36-1.298.233-1.96a6.2 6.2 0 0 1 1.636 1.48 5.02 5.02 0 0 1-.123-1.403c.327-.29.617-.633.864-1.025z"/></svg>`,
|
|
26
|
+
safari: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 24C5.373 24 0 18.627 0 12S5.373 0 12 0s12 5.373 12 12-5.373 12-12 12zm0-2c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm-1.276-4.768l-.246-.612 5.852-9.606.474.114-5.852 9.606-.228.498z"/></svg>`,
|
|
27
|
+
edge: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M21.86 17.86q.14 0 .25.12.1.13.1.25t-.11.33l-.32.46-.43.53-.44.5q-.21.25-.38.42l-.22.23q-.58.53-1.34 1.04-.76.51-1.6.91-.86.4-1.74.64t-1.67.24q-.9 0-1.69-.28-.8-.28-1.48-.78-.68-.5-1.22-1.17-.53-.66-.92-1.44-.38-.77-.6-1.63-.2-.85-.2-1.73 0-1.17.27-2.26.28-1.1.8-2.08.51-.98 1.23-1.82.71-.83 1.61-1.48.9-.65 1.94-1.06 1.05-.41 2.2-.54l.14-.02q.14 0 .27.07t.2.2q.08.13.08.28 0 .15-.08.28-.14.22-.34.42-.2.2-.42.39-.22.18-.44.35l-.42.3q-.2.14-.37.25-.17.12-.28.19l-.14.09q-.12.07-.27.13-.14.06-.3.1-.16.05-.34.07-.18.02-.37.02-1.02 0-1.87.44-.84.43-1.45 1.15-.6.72-.95 1.66-.34.94-.34 1.99 0 1.24.4 2.26.4 1.02 1.07 1.75.68.73 1.55 1.13.88.4 1.82.4 1.15 0 2.14-.38 1-.39 1.83-.99.84-.6 1.5-1.32t1.13-1.41q.09-.13.21-.18.12-.05.25-.05z"/></svg>`,
|
|
28
|
+
opera: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M8.51 7.04C9.44 5.57 10.67 4.62 12 4.62s2.56.95 3.49 2.42c.66.97 1.16 2.15 1.48 3.48.27 1.15.42 2.42.42 3.76s-.15 2.61-.42 3.76c-.32 1.33-.82 2.51-1.48 3.48-.93 1.47-2.16 2.42-3.49 2.42s-2.56-.95-3.49-2.42c-.66-.97-1.16-2.15-1.48-3.48-.27-1.15-.42-2.42-.42-3.76s.15-2.61.42-3.76c.32-1.33.82-2.51 1.48-3.48zM12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0z"/></svg>`,
|
|
29
|
+
other: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>`,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const browserColors: Record<string, string> = {
|
|
33
|
+
chrome: 'bg-yellow-100 text-yellow-600',
|
|
34
|
+
firefox: 'bg-orange-100 text-orange-600',
|
|
35
|
+
safari: 'bg-blue-100 text-blue-600',
|
|
36
|
+
edge: 'bg-cyan-100 text-cyan-600',
|
|
37
|
+
opera: 'bg-red-100 text-red-600',
|
|
38
|
+
other: 'bg-gray-100 text-gray-600',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sortedBrowsers = [...browsers].sort((a, b) => b.value - a.value)
|
|
42
|
+
|
|
43
|
+
function getBrowserIcon(name: string): string {
|
|
44
|
+
const key = name.toLowerCase()
|
|
45
|
+
return browserIcons[key] || browserIcons.other
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getBrowserColor(name: string): string {
|
|
49
|
+
const key = name.toLowerCase()
|
|
50
|
+
return browserColors[key] || browserColors.other
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class="browser-breakdown card-hover p-6">
|
|
55
|
+
<h3 class="text-sm font-medium text-gray-900 mb-4">
|
|
56
|
+
Browsers
|
|
57
|
+
</h3>
|
|
58
|
+
|
|
59
|
+
@if (loading)
|
|
60
|
+
<div class="space-y-4">
|
|
61
|
+
@foreach ([1, 2, 3, 4] as i)
|
|
62
|
+
<div class="animate-pulse flex items-center gap-4">
|
|
63
|
+
<div class="skeleton w-10 h-10 rounded-lg"></div>
|
|
64
|
+
<div class="flex-1">
|
|
65
|
+
<div class="skeleton h-4 w-20 mb-2"></div>
|
|
66
|
+
<div class="skeleton h-2 w-full"></div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
@endforeach
|
|
70
|
+
</div>
|
|
71
|
+
@elseif (!browsers.length)
|
|
72
|
+
<div class="text-center py-8 text-gray-500">
|
|
73
|
+
No browser data available
|
|
74
|
+
</div>
|
|
75
|
+
@else
|
|
76
|
+
<div class="space-y-4">
|
|
77
|
+
@foreach (sortedBrowsers as browser)
|
|
78
|
+
<div class="flex items-center gap-4">
|
|
79
|
+
<div class="w-10 h-10 rounded-lg flex items-center justify-center {{ getBrowserColor(browser.name) }}">
|
|
80
|
+
{!! getBrowserIcon(browser.name) !!}
|
|
81
|
+
</div>
|
|
82
|
+
<div class="flex-1">
|
|
83
|
+
<div class="flex items-center justify-between">
|
|
84
|
+
<span class="font-medium text-gray-900 capitalize">{{ browser.name }}</span>
|
|
85
|
+
<span class="text-sm text-gray-600">{{ formatPercentage(browser.percentage) }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="progress-bar mt-1">
|
|
88
|
+
<div
|
|
89
|
+
class="progress-fill bg-gray-400"
|
|
90
|
+
style="width: {{ browser.percentage * 100 }}%"
|
|
91
|
+
></div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
@endforeach
|
|
96
|
+
</div>
|
|
97
|
+
@endif
|
|
98
|
+
</div>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{{-- BrowsersTable Component --}}
|
|
2
|
+
{{-- Props: browsers (array of {name, visitors}) --}}
|
|
3
|
+
<div class="data-panel">
|
|
4
|
+
<div class="panel-header">
|
|
5
|
+
<span class="panel-title">Browsers</span>
|
|
6
|
+
</div>
|
|
7
|
+
<table class="data-table">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>Browser</th>
|
|
11
|
+
<th class="text-right">Visitors</th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
@forelse(browsers as browser)
|
|
16
|
+
<tr>
|
|
17
|
+
<td class="name">
|
|
18
|
+
<span class="browser-icon" data-browser="{{ browser.name.toLowerCase() }}"></span>
|
|
19
|
+
{{ browser.name }}
|
|
20
|
+
</td>
|
|
21
|
+
<td class="value">{{ browser.visitors | fmt }}</td>
|
|
22
|
+
</tr>
|
|
23
|
+
@empty
|
|
24
|
+
<tr>
|
|
25
|
+
<td colspan="2" class="empty-cell">No browser data</td>
|
|
26
|
+
</tr>
|
|
27
|
+
@endforelse
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.data-panel {
|
|
35
|
+
background: var(--bg-secondary);
|
|
36
|
+
border-radius: 8px;
|
|
37
|
+
padding: 1rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.panel-header {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.5rem;
|
|
44
|
+
margin-bottom: 1rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.panel-title {
|
|
48
|
+
font-size: 0.875rem;
|
|
49
|
+
font-weight: 500;
|
|
50
|
+
color: var(--text-primary);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.data-table {
|
|
54
|
+
width: 100%;
|
|
55
|
+
font-size: 0.8125rem;
|
|
56
|
+
border-collapse: collapse;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.data-table th {
|
|
60
|
+
text-align: left;
|
|
61
|
+
color: var(--text-muted);
|
|
62
|
+
font-weight: 500;
|
|
63
|
+
padding: 0.5rem 0;
|
|
64
|
+
border-bottom: 1px solid var(--border);
|
|
65
|
+
font-size: 0.6875rem;
|
|
66
|
+
text-transform: uppercase;
|
|
67
|
+
letter-spacing: 0.05em;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.data-table th.text-right {
|
|
71
|
+
text-align: right;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.data-table td {
|
|
75
|
+
padding: 0.625rem 0;
|
|
76
|
+
border-bottom: 1px solid var(--bg-tertiary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.data-table tr:last-child td {
|
|
80
|
+
border-bottom: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.data-table .name {
|
|
84
|
+
color: var(--text-primary);
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0.5rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.browser-icon {
|
|
91
|
+
width: 16px;
|
|
92
|
+
height: 16px;
|
|
93
|
+
background-size: contain;
|
|
94
|
+
background-repeat: no-repeat;
|
|
95
|
+
background-position: center;
|
|
96
|
+
border-radius: 2px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.browser-icon[data-browser="chrome"] {
|
|
100
|
+
background-color: #4285F4;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.browser-icon[data-browser="firefox"] {
|
|
104
|
+
background-color: #FF7139;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.browser-icon[data-browser="safari"] {
|
|
108
|
+
background-color: #006CFF;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.browser-icon[data-browser="edge"] {
|
|
112
|
+
background-color: #0078D7;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.data-table .value {
|
|
116
|
+
color: var(--text-secondary);
|
|
117
|
+
text-align: right;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.empty-cell {
|
|
121
|
+
text-align: center;
|
|
122
|
+
color: var(--text-muted);
|
|
123
|
+
padding: 1.5rem 0;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* CampaignBreakdown Component
|
|
4
|
+
*
|
|
5
|
+
* Displays UTM campaign data with source, medium, and campaign breakdown.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import { formatCompact, formatPercentage } from '../utils'
|
|
9
|
+
|
|
10
|
+
export interface CampaignData {
|
|
11
|
+
name: string
|
|
12
|
+
source?: string
|
|
13
|
+
medium?: string
|
|
14
|
+
visitors: number
|
|
15
|
+
conversions: number
|
|
16
|
+
conversionRate: number
|
|
17
|
+
revenue?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CampaignBreakdownProps {
|
|
21
|
+
campaigns: CampaignData[]
|
|
22
|
+
loading?: boolean
|
|
23
|
+
title?: string
|
|
24
|
+
maxItems?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { campaigns, loading, title, maxItems } = withDefaults(
|
|
28
|
+
defineProps<CampaignBreakdownProps>(),
|
|
29
|
+
{
|
|
30
|
+
loading: false,
|
|
31
|
+
title: 'Campaigns',
|
|
32
|
+
maxItems: 10,
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const sortedCampaigns = [...campaigns]
|
|
37
|
+
.sort((a, b) => b.visitors - a.visitors)
|
|
38
|
+
.slice(0, maxItems)
|
|
39
|
+
|
|
40
|
+
const totals = {
|
|
41
|
+
visitors: campaigns.reduce((sum, c) => sum + c.visitors, 0),
|
|
42
|
+
conversions: campaigns.reduce((sum, c) => sum + c.conversions, 0),
|
|
43
|
+
revenue: campaigns.reduce((sum, c) => sum + (c.revenue ?? 0), 0),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sourceIcons: Record<string, string> = {
|
|
47
|
+
google: 'M12.545 10.239v3.821h5.445c-.712 2.315-2.647 3.972-5.445 3.972a6.033 6.033 0 110-12.064c1.498 0 2.866.549 3.921 1.453l2.814-2.814A9.969 9.969 0 0012.545 2C7.021 2 2.543 6.477 2.543 12s4.478 10 10.002 10c8.396 0 10.249-7.85 9.426-11.748l-9.426-.013z',
|
|
48
|
+
facebook: 'M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z',
|
|
49
|
+
twitter: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
|
|
50
|
+
linkedin: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
|
|
51
|
+
email: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
|
52
|
+
direct: 'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getSourceIcon(source?: string): string {
|
|
56
|
+
if (!source)
|
|
57
|
+
return sourceIcons.direct
|
|
58
|
+
const key = source.toLowerCase()
|
|
59
|
+
return sourceIcons[key] || sourceIcons.direct
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div class="campaign-breakdown card-hover p-6">
|
|
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 && campaigns.length)
|
|
67
|
+
<span class="text-xs text-gray-500">
|
|
68
|
+
{{ campaigns.length }} campaigns
|
|
69
|
+
</span>
|
|
70
|
+
@endif
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
@if (loading)
|
|
74
|
+
<div class="space-y-3 animate-pulse">
|
|
75
|
+
<div class="skeleton h-10 rounded"></div>
|
|
76
|
+
@foreach ([1, 2, 3, 4, 5] as i)
|
|
77
|
+
<div class="skeleton h-14 rounded"></div>
|
|
78
|
+
@endforeach
|
|
79
|
+
</div>
|
|
80
|
+
@elseif (!campaigns.length)
|
|
81
|
+
<div class="text-center py-8 text-gray-500">
|
|
82
|
+
<Icon name="pie-chart" size="48" class="mx-auto text-gray-300 mb-3" />
|
|
83
|
+
<p>No campaign data available</p>
|
|
84
|
+
</div>
|
|
85
|
+
@else
|
|
86
|
+
<script client>
|
|
87
|
+
const sortBy = state('visitors')
|
|
88
|
+
const sortOrder = state('desc')
|
|
89
|
+
</script>
|
|
90
|
+
<div class="overflow-x-auto">
|
|
91
|
+
<table class="w-full">
|
|
92
|
+
<thead>
|
|
93
|
+
<tr class="border-b border-gray-200">
|
|
94
|
+
<th class="text-left text-xs font-medium text-gray-500 uppercase py-2">Campaign</th>
|
|
95
|
+
<th
|
|
96
|
+
class="text-right text-xs font-medium text-gray-500 uppercase py-2 cursor-pointer hover:text-gray-700"
|
|
97
|
+
@click="sortBy.set('visitors'); sortOrder.set(sortOrder() === 'desc' ? 'asc' : 'desc')"
|
|
98
|
+
>
|
|
99
|
+
<span class="inline-flex items-center gap-1">
|
|
100
|
+
Visitors
|
|
101
|
+
<svg :show="sortBy() === 'visitors'" class="w-3 h-3" x-class="{ 'rotate-180': sortOrder() === 'asc' }" fill="currentColor" viewBox="0 0 20 20">
|
|
102
|
+
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
|
|
103
|
+
</svg>
|
|
104
|
+
</span>
|
|
105
|
+
</th>
|
|
106
|
+
<th class="text-right text-xs font-medium text-gray-500 uppercase py-2 cursor-pointer hover:text-gray-700">
|
|
107
|
+
Conv.
|
|
108
|
+
</th>
|
|
109
|
+
<th class="text-right text-xs font-medium text-gray-500 uppercase py-2 cursor-pointer hover:text-gray-700">
|
|
110
|
+
Rate
|
|
111
|
+
</th>
|
|
112
|
+
</tr>
|
|
113
|
+
</thead>
|
|
114
|
+
<tbody>
|
|
115
|
+
@foreach (sortedCampaigns as campaign)
|
|
116
|
+
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
|
117
|
+
<td class="py-3">
|
|
118
|
+
<div class="flex items-center gap-3">
|
|
119
|
+
<div class="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-gray-500">
|
|
120
|
+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
121
|
+
<path d="{{ getSourceIcon(campaign.source) }}" />
|
|
122
|
+
</svg>
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<div class="text-sm font-medium text-gray-900 truncate max-w-48">
|
|
126
|
+
{{ campaign.name }}
|
|
127
|
+
</div>
|
|
128
|
+
@if (campaign.source || campaign.medium)
|
|
129
|
+
<div class="text-xs text-gray-500">
|
|
130
|
+
{{ [campaign.source, campaign.medium].filter(Boolean).join(' / ') }}
|
|
131
|
+
</div>
|
|
132
|
+
@endif
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</td>
|
|
136
|
+
<td class="text-right text-sm tabular-nums py-3">
|
|
137
|
+
{{ formatCompact(campaign.visitors) }}
|
|
138
|
+
</td>
|
|
139
|
+
<td class="text-right text-sm tabular-nums py-3">
|
|
140
|
+
{{ formatCompact(campaign.conversions) }}
|
|
141
|
+
</td>
|
|
142
|
+
<td class="text-right text-sm tabular-nums py-3">
|
|
143
|
+
<span class="{{ campaign.conversionRate >= 0.03 ? 'text-green-600' : 'text-gray-600' }}">
|
|
144
|
+
{{ formatPercentage(campaign.conversionRate) }}
|
|
145
|
+
</span>
|
|
146
|
+
</td>
|
|
147
|
+
</tr>
|
|
148
|
+
@endforeach
|
|
149
|
+
</tbody>
|
|
150
|
+
<tfoot>
|
|
151
|
+
<tr class="border-t border-gray-200 font-medium">
|
|
152
|
+
<td class="py-3 text-sm text-gray-900">Total</td>
|
|
153
|
+
<td class="text-right text-sm tabular-nums py-3">{{ formatCompact(totals.visitors) }}</td>
|
|
154
|
+
<td class="text-right text-sm tabular-nums py-3">{{ formatCompact(totals.conversions) }}</td>
|
|
155
|
+
<td class="text-right text-sm tabular-nums py-3">
|
|
156
|
+
{{ formatPercentage(totals.visitors > 0 ? totals.conversions / totals.visitors : 0) }}
|
|
157
|
+
</td>
|
|
158
|
+
</tr>
|
|
159
|
+
</tfoot>
|
|
160
|
+
</table>
|
|
161
|
+
</div>
|
|
162
|
+
@endif
|
|
163
|
+
</div>
|