@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,172 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* LiveActivityFeed Component
|
|
4
|
+
*
|
|
5
|
+
* Real-time feed of visitor activity.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
|
|
9
|
+
export type ActivityType = 'pageview' | 'event' | 'conversion' | 'session_start' | 'session_end'
|
|
10
|
+
|
|
11
|
+
export interface Activity {
|
|
12
|
+
id: string
|
|
13
|
+
type: ActivityType
|
|
14
|
+
page?: string
|
|
15
|
+
eventName?: string
|
|
16
|
+
country?: string
|
|
17
|
+
countryCode?: string
|
|
18
|
+
device?: 'desktop' | 'mobile' | 'tablet'
|
|
19
|
+
timestamp: Date
|
|
20
|
+
value?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface LiveActivityFeedProps {
|
|
24
|
+
activities: Activity[]
|
|
25
|
+
maxItems?: number
|
|
26
|
+
autoScroll?: boolean
|
|
27
|
+
showTimestamps?: boolean
|
|
28
|
+
paused?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { activities, maxItems, autoScroll, showTimestamps, paused } = withDefaults(
|
|
32
|
+
defineProps<LiveActivityFeedProps>(),
|
|
33
|
+
{
|
|
34
|
+
maxItems: 20,
|
|
35
|
+
autoScroll: true,
|
|
36
|
+
showTimestamps: true,
|
|
37
|
+
paused: false,
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const displayedActivities = activities.slice(0, maxItems)
|
|
42
|
+
|
|
43
|
+
const activityConfig: Record<ActivityType, { icon: string, color: string, label: string }> = {
|
|
44
|
+
pageview: {
|
|
45
|
+
icon: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z',
|
|
46
|
+
color: 'bg-blue-100 text-blue-600',
|
|
47
|
+
label: 'Page View',
|
|
48
|
+
},
|
|
49
|
+
event: {
|
|
50
|
+
icon: 'M13 10V3L4 14h7v7l9-11h-7z',
|
|
51
|
+
color: 'bg-purple-100 text-purple-600',
|
|
52
|
+
label: 'Event',
|
|
53
|
+
},
|
|
54
|
+
conversion: {
|
|
55
|
+
icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
56
|
+
color: 'bg-green-100 text-green-600',
|
|
57
|
+
label: 'Conversion',
|
|
58
|
+
},
|
|
59
|
+
session_start: {
|
|
60
|
+
icon: 'M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1',
|
|
61
|
+
color: 'bg-cyan-100 text-cyan-600',
|
|
62
|
+
label: 'Session Start',
|
|
63
|
+
},
|
|
64
|
+
session_end: {
|
|
65
|
+
icon: 'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
|
|
66
|
+
color: 'bg-gray-100 text-gray-600',
|
|
67
|
+
label: 'Session End',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const deviceIcons: Record<string, string> = {
|
|
72
|
+
desktop: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
|
73
|
+
mobile: 'M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z',
|
|
74
|
+
tablet: 'M12 18h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getFlag(countryCode?: string): string {
|
|
78
|
+
if (!countryCode || countryCode.length !== 2)
|
|
79
|
+
return '🌍'
|
|
80
|
+
const codePoints = countryCode.toUpperCase().split('').map(char => 127397 + char.charCodeAt(0))
|
|
81
|
+
return String.fromCodePoint(...codePoints)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getActivityText(activity: Activity): string {
|
|
85
|
+
switch (activity.type) {
|
|
86
|
+
case 'pageview':
|
|
87
|
+
return activity.page || 'Unknown page'
|
|
88
|
+
case 'event':
|
|
89
|
+
return activity.eventName || 'Custom event'
|
|
90
|
+
case 'conversion':
|
|
91
|
+
return activity.eventName || 'Goal completed'
|
|
92
|
+
case 'session_start':
|
|
93
|
+
return 'Started browsing'
|
|
94
|
+
case 'session_end':
|
|
95
|
+
return 'Left the site'
|
|
96
|
+
default:
|
|
97
|
+
return 'Activity'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<div class="live-activity-feed card-hover p-6">
|
|
103
|
+
<div class="flex items-center justify-between mb-4">
|
|
104
|
+
<div class="flex items-center gap-2">
|
|
105
|
+
<h3 class="text-sm font-medium text-gray-900">Live Activity</h3>
|
|
106
|
+
@if (!paused)
|
|
107
|
+
<div class="relative">
|
|
108
|
+
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
|
109
|
+
<div class="absolute inset-0 w-2 h-2 rounded-full bg-green-500 animate-ping"></div>
|
|
110
|
+
</div>
|
|
111
|
+
@else
|
|
112
|
+
<span class="text-xs text-gray-500">(Paused)</span>
|
|
113
|
+
@endif
|
|
114
|
+
</div>
|
|
115
|
+
@if (activities.length)
|
|
116
|
+
<span class="text-xs text-gray-500">
|
|
117
|
+
{{ activities.length }} activities
|
|
118
|
+
</span>
|
|
119
|
+
@endif
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
@if (!activities.length)
|
|
123
|
+
<div class="text-center py-8 text-gray-500">
|
|
124
|
+
<Icon name="zap" size="48" class="mx-auto text-gray-300 mb-3" />
|
|
125
|
+
<p>Waiting for activity...</p>
|
|
126
|
+
</div>
|
|
127
|
+
@else
|
|
128
|
+
<div class="space-y-2 max-h-96 overflow-y-auto">
|
|
129
|
+
@foreach (displayedActivities as activity)
|
|
130
|
+
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors">
|
|
131
|
+
<div class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 {{ activityConfig[activity.type].color }}">
|
|
132
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
133
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ activityConfig[activity.type].icon }}" />
|
|
134
|
+
</svg>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="flex-1 min-w-0">
|
|
138
|
+
<div class="flex items-center gap-2">
|
|
139
|
+
<span class="text-sm text-gray-900 truncate">
|
|
140
|
+
{{ getActivityText(activity) }}
|
|
141
|
+
</span>
|
|
142
|
+
@if (activity.value)
|
|
143
|
+
<span class="text-xs font-medium text-green-600">
|
|
144
|
+
+${{ activity.value }}
|
|
145
|
+
</span>
|
|
146
|
+
@endif
|
|
147
|
+
</div>
|
|
148
|
+
<div class="flex items-center gap-2 text-xs text-gray-500">
|
|
149
|
+
@if (activity.countryCode)
|
|
150
|
+
<span class="flex items-center gap-1">
|
|
151
|
+
{{ getFlag(activity.countryCode) }}
|
|
152
|
+
{{ activity.country }}
|
|
153
|
+
</span>
|
|
154
|
+
@endif
|
|
155
|
+
@if (activity.device)
|
|
156
|
+
<span class="flex items-center gap-1">
|
|
157
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
158
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ deviceIcons[activity.device] }}" />
|
|
159
|
+
</svg>
|
|
160
|
+
</span>
|
|
161
|
+
@endif
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
@if (showTimestamps)
|
|
166
|
+
<span class="text-xs text-gray-400 flex-shrink-0">Just now</span>
|
|
167
|
+
@endif
|
|
168
|
+
</div>
|
|
169
|
+
@endforeach
|
|
170
|
+
</div>
|
|
171
|
+
@endif
|
|
172
|
+
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* MetricComparison Component
|
|
4
|
+
*
|
|
5
|
+
* Displays a metric with visual comparison between current and previous periods.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import { formatChange, formatCompact, formatCurrency, formatDuration, formatPercentage } from '../utils'
|
|
9
|
+
|
|
10
|
+
interface MetricComparisonProps {
|
|
11
|
+
title: string
|
|
12
|
+
current: number
|
|
13
|
+
previous: number
|
|
14
|
+
format?: 'number' | 'percentage' | 'duration' | 'currency'
|
|
15
|
+
loading?: boolean
|
|
16
|
+
invertColors?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { title, current, previous, format, loading, invertColors } = withDefaults(
|
|
20
|
+
defineProps<MetricComparisonProps>(),
|
|
21
|
+
{
|
|
22
|
+
format: 'number',
|
|
23
|
+
loading: false,
|
|
24
|
+
invertColors: false,
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const change = previous === 0
|
|
29
|
+
? (current > 0 ? 100 : 0)
|
|
30
|
+
: ((current - previous) / previous) * 100
|
|
31
|
+
|
|
32
|
+
function formatValue(value: number): string {
|
|
33
|
+
switch (format) {
|
|
34
|
+
case 'percentage':
|
|
35
|
+
return formatPercentage(value)
|
|
36
|
+
case 'duration':
|
|
37
|
+
return formatDuration(value)
|
|
38
|
+
case 'currency':
|
|
39
|
+
return formatCurrency(value)
|
|
40
|
+
default:
|
|
41
|
+
return formatCompact(value)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const formattedCurrent = formatValue(current)
|
|
46
|
+
const formattedPrevious = formatValue(previous)
|
|
47
|
+
|
|
48
|
+
const positive = change >= 0
|
|
49
|
+
const isPositive = invertColors ? !positive : positive
|
|
50
|
+
|
|
51
|
+
const max = Math.max(current, previous)
|
|
52
|
+
const barWidthCurrent = max > 0 ? (current / max) * 100 : 0
|
|
53
|
+
const barWidthPrevious = max > 0 ? (previous / max) * 100 : 0
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div class="metric-comparison card-hover p-6">
|
|
57
|
+
<div class="flex items-center justify-between mb-4">
|
|
58
|
+
<h3 class="text-sm font-medium text-gray-900">{{ title }}</h3>
|
|
59
|
+
@if (!loading)
|
|
60
|
+
<span class="text-sm font-medium {{ isPositive ? 'text-green-600' : 'text-red-600' }}">
|
|
61
|
+
{{ formatChange(change / 100) }}
|
|
62
|
+
</span>
|
|
63
|
+
@endif
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
@if (loading)
|
|
67
|
+
<div class="space-y-4 animate-pulse">
|
|
68
|
+
<div>
|
|
69
|
+
<div class="skeleton h-4 w-20 mb-2"></div>
|
|
70
|
+
<div class="skeleton h-3 w-full"></div>
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<div class="skeleton h-4 w-20 mb-2"></div>
|
|
74
|
+
<div class="skeleton h-3 w-3/4"></div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
@else
|
|
78
|
+
<div class="space-y-4">
|
|
79
|
+
<div>
|
|
80
|
+
<div class="flex items-center justify-between text-sm mb-1">
|
|
81
|
+
<span class="text-gray-600">Current period</span>
|
|
82
|
+
<span class="font-semibold text-gray-900">{{ formattedCurrent }}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="h-3 bg-gray-100 rounded-full overflow-hidden">
|
|
85
|
+
<div
|
|
86
|
+
class="h-full rounded-full transition-all duration-500 {{ isPositive ? 'bg-green-500' : 'bg-red-500' }}"
|
|
87
|
+
style="width: {{ barWidthCurrent }}%"
|
|
88
|
+
></div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div>
|
|
93
|
+
<div class="flex items-center justify-between text-sm mb-1">
|
|
94
|
+
<span class="text-gray-600">Previous period</span>
|
|
95
|
+
<span class="font-medium text-gray-500">{{ formattedPrevious }}</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="h-3 bg-gray-100 rounded-full overflow-hidden">
|
|
98
|
+
<div
|
|
99
|
+
class="h-full bg-gray-400 rounded-full transition-all duration-500"
|
|
100
|
+
style="width: {{ barWidthPrevious }}%"
|
|
101
|
+
></div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
@endif
|
|
106
|
+
</div>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* MiniStats Component
|
|
4
|
+
*
|
|
5
|
+
* Compact horizontal row of mini stats, perfect for headers or summaries.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import { formatCompact, formatCurrency, formatDuration, formatPercentage } from '../utils'
|
|
9
|
+
|
|
10
|
+
export interface MiniStat {
|
|
11
|
+
label: string
|
|
12
|
+
value: number | string
|
|
13
|
+
format?: 'number' | 'percentage' | 'duration' | 'currency'
|
|
14
|
+
change?: number
|
|
15
|
+
icon?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface MiniStatsProps {
|
|
19
|
+
stats: MiniStat[]
|
|
20
|
+
loading?: boolean
|
|
21
|
+
dividers?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { stats, loading, dividers } = withDefaults(
|
|
25
|
+
defineProps<MiniStatsProps>(),
|
|
26
|
+
{
|
|
27
|
+
loading: false,
|
|
28
|
+
dividers: true,
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
function formatValue(stat: MiniStat): string {
|
|
33
|
+
if (typeof stat.value === 'string')
|
|
34
|
+
return stat.value
|
|
35
|
+
|
|
36
|
+
switch (stat.format) {
|
|
37
|
+
case 'percentage':
|
|
38
|
+
return formatPercentage(stat.value)
|
|
39
|
+
case 'duration':
|
|
40
|
+
return formatDuration(stat.value)
|
|
41
|
+
case 'currency':
|
|
42
|
+
return formatCurrency(stat.value)
|
|
43
|
+
default:
|
|
44
|
+
return formatCompact(stat.value)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="mini-stats flex items-center flex-wrap gap-y-2 py-2">
|
|
50
|
+
@if (loading)
|
|
51
|
+
@foreach ([1, 2, 3, 4] as i)
|
|
52
|
+
<div class="flex items-center gap-6 animate-pulse">
|
|
53
|
+
<div class="flex items-center gap-2">
|
|
54
|
+
<div class="skeleton h-4 w-16"></div>
|
|
55
|
+
<div class="skeleton h-5 w-12"></div>
|
|
56
|
+
</div>
|
|
57
|
+
@if (dividers && i < 4)
|
|
58
|
+
<div class="h-4 w-px bg-gray-200"></div>
|
|
59
|
+
@endif
|
|
60
|
+
</div>
|
|
61
|
+
@endforeach
|
|
62
|
+
@else
|
|
63
|
+
@foreach (stats as stat)
|
|
64
|
+
<div class="flex items-center">
|
|
65
|
+
<div class="flex items-center gap-2 px-3 first:pl-0 last:pr-0">
|
|
66
|
+
@if (stat.icon)
|
|
67
|
+
<svg
|
|
68
|
+
class="w-4 h-4 text-gray-400"
|
|
69
|
+
fill="none"
|
|
70
|
+
stroke="currentColor"
|
|
71
|
+
viewBox="0 0 24 24"
|
|
72
|
+
>
|
|
73
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ stat.icon }}" />
|
|
74
|
+
</svg>
|
|
75
|
+
@endif
|
|
76
|
+
|
|
77
|
+
<span class="text-sm text-gray-500">{{ stat.label }}</span>
|
|
78
|
+
|
|
79
|
+
<span class="text-sm font-semibold text-gray-900">
|
|
80
|
+
{{ formatValue(stat) }}
|
|
81
|
+
</span>
|
|
82
|
+
|
|
83
|
+
@if (stat.change !== undefined)
|
|
84
|
+
<span class="text-xs font-medium {{ stat.change >= 0 ? 'text-green-600' : 'text-red-600' }}">
|
|
85
|
+
{{ stat.change >= 0 ? '+' : '' }}{{ stat.change.toFixed(1) }}%
|
|
86
|
+
</span>
|
|
87
|
+
@endif
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
@if (dividers && !loop.last)
|
|
91
|
+
<div class="h-4 w-px bg-gray-200"></div>
|
|
92
|
+
@endif
|
|
93
|
+
</div>
|
|
94
|
+
@endforeach
|
|
95
|
+
@endif
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* OSBreakdown Component
|
|
4
|
+
*
|
|
5
|
+
* Displays operating system breakdown with icons.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import type { TopItem } from '../types'
|
|
9
|
+
import { formatPercentage } from '../utils'
|
|
10
|
+
|
|
11
|
+
interface OSBreakdownProps {
|
|
12
|
+
systems: TopItem[]
|
|
13
|
+
loading?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { systems, loading } = withDefaults(
|
|
17
|
+
defineProps<OSBreakdownProps>(),
|
|
18
|
+
{
|
|
19
|
+
loading: false,
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const osIcons: Record<string, string> = {
|
|
24
|
+
windows: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"/></svg>`,
|
|
25
|
+
macos: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
|
|
26
|
+
ios: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>`,
|
|
27
|
+
android: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.341c-.5 0-.91-.41-.91-.91s.41-.91.91-.91.91.41.91.91-.41.91-.91.91m-11.046 0c-.5 0-.91-.41-.91-.91s.41-.91.91-.91.91.41.91.91-.41.91-.91.91m11.4-6.02l1.97-3.41c.11-.19.04-.43-.15-.54-.19-.11-.43-.04-.54.15l-2 3.46C15.53 8.31 13.84 7.91 12 7.91s-3.53.4-5.15 1.07l-2-3.46c-.11-.19-.35-.26-.54-.15-.19.11-.26.35-.15.54l1.97 3.41C3.47 11.07 1.59 14.07 1.5 17.5h21c-.09-3.43-1.97-6.43-4.63-8.18z"/></svg>`,
|
|
28
|
+
linux: `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M12.504 0c-.155 0-.311.001-.465.003-.653.014-1.302.082-1.933.225-1.308.298-2.482.88-3.46 1.71-.476.4-.903.85-1.273 1.347-.383.515-.706 1.076-.958 1.672-.264.628-.456 1.29-.57 1.97-.118.704-.162 1.425-.132 2.143.032.757.136 1.505.31 2.234.159.662.378 1.304.649 1.92.238.54.522 1.058.846 1.546.261.392.549.764.86 1.114.296.333.615.646.95.938.317.276.65.533.995.77.284.197.578.38.879.548.21.118.426.227.644.33.197.093.398.18.6.262.19.078.384.15.58.217.189.065.38.125.573.18.194.057.39.108.587.155.198.048.398.09.6.127.21.04.422.072.635.1.225.03.452.052.679.068.24.018.481.028.723.028.242 0 .483-.01.723-.028.227-.016.454-.038.679-.068.213-.028.425-.06.635-.1.202-.037.402-.079.6-.127.197-.047.393-.098.587-.155.193-.055.384-.115.573-.18.196-.067.39-.139.58-.217.202-.082.403-.169.6-.262.218-.103.434-.212.644-.33.301-.168.595-.351.879-.548.345-.237.678-.494.995-.77.335-.292.654-.605.95-.938.311-.35.599-.722.86-1.114.324-.488.608-1.006.846-1.546.271-.616.49-1.258.649-1.92.174-.729.278-1.477.31-2.234.03-.718-.014-1.439-.132-2.143-.114-.68-.306-1.342-.57-1.97-.252-.596-.575-1.157-.958-1.672-.37-.497-.797-.947-1.273-1.347-.978-.83-2.152-1.412-3.46-1.71-.631-.143-1.28-.211-1.933-.225C12.815.001 12.659 0 12.504 0z"/></svg>`,
|
|
29
|
+
chromeos: `<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>`,
|
|
30
|
+
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="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>`,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const osColors: Record<string, string> = {
|
|
34
|
+
windows: 'bg-blue-100 text-blue-600',
|
|
35
|
+
macos: 'bg-gray-100 text-gray-700',
|
|
36
|
+
ios: 'bg-gray-100 text-gray-700',
|
|
37
|
+
android: 'bg-green-100 text-green-600',
|
|
38
|
+
linux: 'bg-amber-100 text-amber-600',
|
|
39
|
+
chromeos: 'bg-yellow-100 text-yellow-600',
|
|
40
|
+
other: 'bg-gray-100 text-gray-600',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sortedSystems = [...systems].sort((a, b) => b.value - a.value)
|
|
44
|
+
|
|
45
|
+
function getOSIcon(name: string): string {
|
|
46
|
+
const key = name.toLowerCase().replace(/\s+/g, '')
|
|
47
|
+
if (key.includes('windows'))
|
|
48
|
+
return osIcons.windows
|
|
49
|
+
if (key.includes('mac') || key.includes('osx'))
|
|
50
|
+
return osIcons.macos
|
|
51
|
+
if (key.includes('ios') || key.includes('iphone') || key.includes('ipad'))
|
|
52
|
+
return osIcons.ios
|
|
53
|
+
if (key.includes('android'))
|
|
54
|
+
return osIcons.android
|
|
55
|
+
if (key.includes('linux') || key.includes('ubuntu') || key.includes('debian'))
|
|
56
|
+
return osIcons.linux
|
|
57
|
+
if (key.includes('chrome'))
|
|
58
|
+
return osIcons.chromeos
|
|
59
|
+
return osIcons.other
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getOSColor(name: string): string {
|
|
63
|
+
const key = name.toLowerCase().replace(/\s+/g, '')
|
|
64
|
+
if (key.includes('windows'))
|
|
65
|
+
return osColors.windows
|
|
66
|
+
if (key.includes('mac') || key.includes('osx'))
|
|
67
|
+
return osColors.macos
|
|
68
|
+
if (key.includes('ios') || key.includes('iphone') || key.includes('ipad'))
|
|
69
|
+
return osColors.ios
|
|
70
|
+
if (key.includes('android'))
|
|
71
|
+
return osColors.android
|
|
72
|
+
if (key.includes('linux') || key.includes('ubuntu') || key.includes('debian'))
|
|
73
|
+
return osColors.linux
|
|
74
|
+
if (key.includes('chrome'))
|
|
75
|
+
return osColors.chromeos
|
|
76
|
+
return osColors.other
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<div class="os-breakdown card-hover p-6">
|
|
81
|
+
<h3 class="text-sm font-medium text-gray-900 mb-4">
|
|
82
|
+
Operating Systems
|
|
83
|
+
</h3>
|
|
84
|
+
|
|
85
|
+
@if (loading)
|
|
86
|
+
<div class="space-y-4">
|
|
87
|
+
@foreach ([1, 2, 3, 4] as i)
|
|
88
|
+
<div class="animate-pulse flex items-center gap-4">
|
|
89
|
+
<div class="skeleton w-10 h-10 rounded-lg"></div>
|
|
90
|
+
<div class="flex-1">
|
|
91
|
+
<div class="skeleton h-4 w-20 mb-2"></div>
|
|
92
|
+
<div class="skeleton h-2 w-full"></div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
@endforeach
|
|
96
|
+
</div>
|
|
97
|
+
@elseif (!systems.length)
|
|
98
|
+
<div class="text-center py-8 text-gray-500">
|
|
99
|
+
No OS data available
|
|
100
|
+
</div>
|
|
101
|
+
@else
|
|
102
|
+
<div class="space-y-4">
|
|
103
|
+
@foreach (sortedSystems as system)
|
|
104
|
+
<div class="flex items-center gap-4">
|
|
105
|
+
<div
|
|
106
|
+
class="w-10 h-10 rounded-lg flex items-center justify-center {{ getOSColor(system.name) }}"
|
|
107
|
+
>{!! getOSIcon(system.name) !!}</div>
|
|
108
|
+
<div class="flex-1">
|
|
109
|
+
<div class="flex items-center justify-between">
|
|
110
|
+
<span class="font-medium text-gray-900">{{ system.name }}</span>
|
|
111
|
+
<span class="text-sm text-gray-600">{{ formatPercentage(system.percentage) }}</span>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="progress-bar mt-1">
|
|
114
|
+
<div
|
|
115
|
+
class="progress-fill bg-gray-400"
|
|
116
|
+
style="width: {{ system.percentage * 100 }}%"
|
|
117
|
+
></div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
@endforeach
|
|
122
|
+
</div>
|
|
123
|
+
@endif
|
|
124
|
+
</div>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
/**
|
|
3
|
+
* PageDetailCard Component
|
|
4
|
+
*
|
|
5
|
+
* Displays detailed metrics for a specific page.
|
|
6
|
+
*/
|
|
7
|
+
import { defineProps, withDefaults } from 'stx'
|
|
8
|
+
import { formatCompact, formatDuration, formatPercentage } from '../utils'
|
|
9
|
+
|
|
10
|
+
export interface PageDetail {
|
|
11
|
+
path: string
|
|
12
|
+
title?: string
|
|
13
|
+
pageViews: number
|
|
14
|
+
uniquePageViews: number
|
|
15
|
+
avgTimeOnPage: number
|
|
16
|
+
bounceRate: number
|
|
17
|
+
exitRate: number
|
|
18
|
+
entrances: number
|
|
19
|
+
exits: number
|
|
20
|
+
change?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PageDetailCardProps {
|
|
24
|
+
page: PageDetail | null
|
|
25
|
+
loading?: boolean
|
|
26
|
+
showSparkline?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { page, loading, showSparkline } = withDefaults(
|
|
30
|
+
defineProps<PageDetailCardProps>(),
|
|
31
|
+
{
|
|
32
|
+
loading: false,
|
|
33
|
+
showSparkline: false,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const metrics = page ? [
|
|
38
|
+
{ label: 'Page Views', value: formatCompact(page.pageViews), icon: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z', isNegative: false },
|
|
39
|
+
{ label: 'Unique Views', value: formatCompact(page.uniquePageViews), icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z', isNegative: false },
|
|
40
|
+
{ label: 'Avg. Time', value: formatDuration(page.avgTimeOnPage), icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', isNegative: false },
|
|
41
|
+
{ label: 'Bounce Rate', value: formatPercentage(page.bounceRate), icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6', isNegative: page.bounceRate > 0.5 },
|
|
42
|
+
{ label: 'Exit Rate', value: formatPercentage(page.exitRate), icon: 'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1', isNegative: false },
|
|
43
|
+
{ label: 'Entrances', value: formatCompact(page.entrances), icon: 'M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1', isNegative: false },
|
|
44
|
+
] : []
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div class="page-detail-card card-hover p-6">
|
|
48
|
+
@if (loading)
|
|
49
|
+
<div class="animate-pulse">
|
|
50
|
+
<div class="skeleton h-6 w-3/4 mb-2"></div>
|
|
51
|
+
<div class="skeleton h-4 w-1/2 mb-6"></div>
|
|
52
|
+
<div class="grid grid-cols-3 gap-4">
|
|
53
|
+
@foreach ([1, 2, 3, 4, 5, 6] as i)
|
|
54
|
+
<div>
|
|
55
|
+
<div class="skeleton h-4 w-16 mb-2"></div>
|
|
56
|
+
<div class="skeleton h-6 w-12"></div>
|
|
57
|
+
</div>
|
|
58
|
+
@endforeach
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
@elseif (!page)
|
|
62
|
+
<div class="text-center py-8 text-gray-500">
|
|
63
|
+
Select a page to view details
|
|
64
|
+
</div>
|
|
65
|
+
@else
|
|
66
|
+
<div class="mb-6">
|
|
67
|
+
<div class="flex items-start justify-between gap-4">
|
|
68
|
+
<div class="min-w-0">
|
|
69
|
+
<h3 class="text-lg font-semibold text-gray-900 truncate" title="{{ page.path }}">
|
|
70
|
+
{{ page.title ? page.title : page.path }}
|
|
71
|
+
</h3>
|
|
72
|
+
<p class="text-sm text-gray-500 truncate mt-0.5" title="{{ page.path }}">
|
|
73
|
+
{{ page.path }}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
@if (page.change !== undefined)
|
|
77
|
+
<span
|
|
78
|
+
class="flex-shrink-0 px-2 py-1 text-xs font-medium rounded-full {{ page.change >= 0 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' }}"
|
|
79
|
+
>
|
|
80
|
+
{{ page.change >= 0 ? '+' : '' }}{{ page.change.toFixed(1) }}%
|
|
81
|
+
</span>
|
|
82
|
+
@endif
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
|
87
|
+
@foreach (metrics as metric)
|
|
88
|
+
<div class="p-3 bg-gray-50 rounded-lg">
|
|
89
|
+
<div class="flex items-center gap-2 text-xs text-gray-500 mb-1">
|
|
90
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
91
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ metric.icon }}" />
|
|
92
|
+
</svg>
|
|
93
|
+
{{ metric.label }}
|
|
94
|
+
</div>
|
|
95
|
+
<div class="text-lg font-semibold {{ metric.isNegative ? 'text-red-600' : 'text-gray-900' }}">
|
|
96
|
+
{{ metric.value }}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
@endforeach
|
|
100
|
+
</div>
|
|
101
|
+
@endif
|
|
102
|
+
</div>
|