@antelopejs/dms-marketing 0.2.2
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/LICENSE +190 -0
- package/README.md +143 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +35 -0
- package/dist/config.js.map +1 -0
- package/dist/data-api/body.d.ts +1 -0
- package/dist/data-api/body.js +15 -0
- package/dist/data-api/body.js.map +1 -0
- package/dist/data-api/funnels.d.ts +32 -0
- package/dist/data-api/funnels.js +302 -0
- package/dist/data-api/funnels.js.map +1 -0
- package/dist/data-api/hidden-filter.d.ts +1 -0
- package/dist/data-api/hidden-filter.js +11 -0
- package/dist/data-api/hidden-filter.js.map +1 -0
- package/dist/data-api/index.d.ts +4 -0
- package/dist/data-api/index.js +21 -0
- package/dist/data-api/index.js.map +1 -0
- package/dist/data-api/tenant-scope.d.ts +15 -0
- package/dist/data-api/tenant-scope.js +43 -0
- package/dist/data-api/tenant-scope.js.map +1 -0
- package/dist/data-api/websites.d.ts +25 -0
- package/dist/data-api/websites.js +73 -0
- package/dist/data-api/websites.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/funnels.model.d.ts +25 -0
- package/dist/db/models/funnels.model.js +39 -0
- package/dist/db/models/funnels.model.js.map +1 -0
- package/dist/db/models/index.d.ts +7 -0
- package/dist/db/models/index.js +24 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/marketing_events.model.d.ts +49 -0
- package/dist/db/models/marketing_events.model.js +85 -0
- package/dist/db/models/marketing_events.model.js.map +1 -0
- package/dist/db/models/marketing_sessions.model.d.ts +33 -0
- package/dist/db/models/marketing_sessions.model.js +50 -0
- package/dist/db/models/marketing_sessions.model.js.map +1 -0
- package/dist/db/models/marketing_settings.model.d.ts +23 -0
- package/dist/db/models/marketing_settings.model.js +13 -0
- package/dist/db/models/marketing_settings.model.js.map +1 -0
- package/dist/db/models/page_snapshots.model.d.ts +27 -0
- package/dist/db/models/page_snapshots.model.js +32 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -0
- package/dist/db/models/retention.d.ts +6 -0
- package/dist/db/models/retention.js +26 -0
- package/dist/db/models/retention.js.map +1 -0
- package/dist/db/models/website_statistics.model.d.ts +31 -0
- package/dist/db/models/website_statistics.model.js +63 -0
- package/dist/db/models/website_statistics.model.js.map +1 -0
- package/dist/db/models/websites.model.d.ts +26 -0
- package/dist/db/models/websites.model.js +27 -0
- package/dist/db/models/websites.model.js.map +1 -0
- package/dist/db/tables/funnels.table.d.ts +31 -0
- package/dist/db/tables/funnels.table.js +54 -0
- package/dist/db/tables/funnels.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +7 -0
- package/dist/db/tables/index.js +24 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/marketing_events.table.d.ts +15 -0
- package/dist/db/tables/marketing_events.table.js +66 -0
- package/dist/db/tables/marketing_events.table.js.map +1 -0
- package/dist/db/tables/marketing_sessions.table.d.ts +26 -0
- package/dist/db/tables/marketing_sessions.table.js +115 -0
- package/dist/db/tables/marketing_sessions.table.js.map +1 -0
- package/dist/db/tables/marketing_settings.table.d.ts +11 -0
- package/dist/db/tables/marketing_settings.table.js +51 -0
- package/dist/db/tables/marketing_settings.table.js.map +1 -0
- package/dist/db/tables/page_snapshots.table.d.ts +17 -0
- package/dist/db/tables/page_snapshots.table.js +72 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -0
- package/dist/db/tables/website_statistics.table.d.ts +14 -0
- package/dist/db/tables/website_statistics.table.js +60 -0
- package/dist/db/tables/website_statistics.table.js.map +1 -0
- package/dist/db/tables/websites.table.d.ts +13 -0
- package/dist/db/tables/websites.table.js +60 -0
- package/dist/db/tables/websites.table.js.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +39 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/implementations/dms-marketing/index.d.ts +2 -0
- package/dist/implementations/dms-marketing/index.js +8 -0
- package/dist/implementations/dms-marketing/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/all.d.ts +5 -0
- package/dist/pages/all.js +22 -0
- package/dist/pages/all.js.map +1 -0
- package/dist/pages/campaigns/page.d.ts +9 -0
- package/dist/pages/campaigns/page.js +27 -0
- package/dist/pages/campaigns/page.js.map +1 -0
- package/dist/pages/funnels/page.d.ts +9 -0
- package/dist/pages/funnels/page.js +27 -0
- package/dist/pages/funnels/page.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/module.d.ts +1 -0
- package/dist/pages/module.js +13 -0
- package/dist/pages/module.js.map +1 -0
- package/dist/pages/overview/page.d.ts +9 -0
- package/dist/pages/overview/page.js +27 -0
- package/dist/pages/overview/page.js.map +1 -0
- package/dist/pages/pages/page.d.ts +9 -0
- package/dist/pages/pages/page.js +27 -0
- package/dist/pages/pages/page.js.map +1 -0
- package/dist/pages/settings/form.d.ts +3 -0
- package/dist/pages/settings/form.js +35 -0
- package/dist/pages/settings/form.js.map +1 -0
- package/dist/pages/settings/page.d.ts +9 -0
- package/dist/pages/settings/page.js +27 -0
- package/dist/pages/settings/page.js.map +1 -0
- package/dist/routes/collect.d.ts +7 -0
- package/dist/routes/collect.js +62 -0
- package/dist/routes/collect.js.map +1 -0
- package/dist/routes/experiments.d.ts +6 -0
- package/dist/routes/experiments.js +63 -0
- package/dist/routes/experiments.js.map +1 -0
- package/dist/routes/funnels.d.ts +16 -0
- package/dist/routes/funnels.js +44 -0
- package/dist/routes/funnels.js.map +1 -0
- package/dist/routes/index.d.ts +8 -0
- package/dist/routes/index.js +25 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/settings.d.ts +8 -0
- package/dist/routes/settings.js +50 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/snapshot.d.ts +10 -0
- package/dist/routes/snapshot.js +147 -0
- package/dist/routes/snapshot.js.map +1 -0
- package/dist/routes/stats.d.ts +44 -0
- package/dist/routes/stats.js +278 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/tracker.d.ts +6 -0
- package/dist/routes/tracker.js +57 -0
- package/dist/routes/tracker.js.map +1 -0
- package/dist/routes/websites.d.ts +13 -0
- package/dist/routes/websites.js +144 -0
- package/dist/routes/websites.js.map +1 -0
- package/dist/services/acquisition.d.ts +15 -0
- package/dist/services/acquisition.js +153 -0
- package/dist/services/acquisition.js.map +1 -0
- package/dist/services/bounded-cache.d.ts +14 -0
- package/dist/services/bounded-cache.js +45 -0
- package/dist/services/bounded-cache.js.map +1 -0
- package/dist/services/collect.d.ts +11 -0
- package/dist/services/collect.js +193 -0
- package/dist/services/collect.js.map +1 -0
- package/dist/services/experiment-definitions.d.ts +4 -0
- package/dist/services/experiment-definitions.js +36 -0
- package/dist/services/experiment-definitions.js.map +1 -0
- package/dist/services/experiments/assign.d.ts +5 -0
- package/dist/services/experiments/assign.js +52 -0
- package/dist/services/experiments/assign.js.map +1 -0
- package/dist/services/experiments/index.d.ts +3 -0
- package/dist/services/experiments/index.js +20 -0
- package/dist/services/experiments/index.js.map +1 -0
- package/dist/services/experiments/results.d.ts +20 -0
- package/dist/services/experiments/results.js +91 -0
- package/dist/services/experiments/results.js.map +1 -0
- package/dist/services/experiments/stats.d.ts +12 -0
- package/dist/services/experiments/stats.js +62 -0
- package/dist/services/experiments/stats.js.map +1 -0
- package/dist/services/funnels/compute.d.ts +15 -0
- package/dist/services/funnels/compute.js +68 -0
- package/dist/services/funnels/compute.js.map +1 -0
- package/dist/services/funnels/index.d.ts +2 -0
- package/dist/services/funnels/index.js +19 -0
- package/dist/services/funnels/index.js.map +1 -0
- package/dist/services/funnels/results.d.ts +14 -0
- package/dist/services/funnels/results.js +72 -0
- package/dist/services/funnels/results.js.map +1 -0
- package/dist/services/geoip.d.ts +3 -0
- package/dist/services/geoip.js +38 -0
- package/dist/services/geoip.js.map +1 -0
- package/dist/services/heatmap.d.ts +10 -0
- package/dist/services/heatmap.js +180 -0
- package/dist/services/heatmap.js.map +1 -0
- package/dist/services/index.d.ts +26 -0
- package/dist/services/index.js +43 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/ingest-guard.d.ts +11 -0
- package/dist/services/ingest-guard.js +44 -0
- package/dist/services/ingest-guard.js.map +1 -0
- package/dist/services/keyed-queue.d.ts +4 -0
- package/dist/services/keyed-queue.js +19 -0
- package/dist/services/keyed-queue.js.map +1 -0
- package/dist/services/origin-guard.d.ts +5 -0
- package/dist/services/origin-guard.js +43 -0
- package/dist/services/origin-guard.js.map +1 -0
- package/dist/services/period.d.ts +2 -0
- package/dist/services/period.js +19 -0
- package/dist/services/period.js.map +1 -0
- package/dist/services/prune.d.ts +7 -0
- package/dist/services/prune.js +49 -0
- package/dist/services/prune.js.map +1 -0
- package/dist/services/query-param.d.ts +1 -0
- package/dist/services/query-param.js +11 -0
- package/dist/services/query-param.js.map +1 -0
- package/dist/services/request-body.d.ts +2 -0
- package/dist/services/request-body.js +41 -0
- package/dist/services/request-body.js.map +1 -0
- package/dist/services/request-meta.d.ts +3 -0
- package/dist/services/request-meta.js +19 -0
- package/dist/services/request-meta.js.map +1 -0
- package/dist/services/rollup-write.d.ts +4 -0
- package/dist/services/rollup-write.js +84 -0
- package/dist/services/rollup-write.js.map +1 -0
- package/dist/services/sessionize.d.ts +23 -0
- package/dist/services/sessionize.js +78 -0
- package/dist/services/sessionize.js.map +1 -0
- package/dist/services/settings.d.ts +6 -0
- package/dist/services/settings.js +113 -0
- package/dist/services/settings.js.map +1 -0
- package/dist/services/snapshots.d.ts +24 -0
- package/dist/services/snapshots.js +139 -0
- package/dist/services/snapshots.js.map +1 -0
- package/dist/services/tenant-funnel.d.ts +3 -0
- package/dist/services/tenant-funnel.js +17 -0
- package/dist/services/tenant-funnel.js.map +1 -0
- package/dist/services/tenant-website.d.ts +5 -0
- package/dist/services/tenant-website.js +34 -0
- package/dist/services/tenant-website.js.map +1 -0
- package/dist/services/url.d.ts +10 -0
- package/dist/services/url.js +81 -0
- package/dist/services/url.js.map +1 -0
- package/dist/services/user-agent.d.ts +2 -0
- package/dist/services/user-agent.js +17 -0
- package/dist/services/user-agent.js.map +1 -0
- package/dist/services/visitor-id.d.ts +1 -0
- package/dist/services/visitor-id.js +19 -0
- package/dist/services/visitor-id.js.map +1 -0
- package/dist/services/website-cache.d.ts +3 -0
- package/dist/services/website-cache.js +22 -0
- package/dist/services/website-cache.js.map +1 -0
- package/dist/types/constants.d.ts +74 -0
- package/dist/types/constants.js +79 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/events.d.ts +114 -0
- package/dist/types/events.js +29 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/settings.d.ts +67 -0
- package/dist/types/settings.js +58 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/snapshots.d.ts +95 -0
- package/dist/types/snapshots.js +41 -0
- package/dist/types/snapshots.js.map +1 -0
- package/dist/types/statistics.d.ts +38 -0
- package/dist/types/statistics.js +33 -0
- package/dist/types/statistics.js.map +1 -0
- package/dist/utils/experiment-type.d.ts +52 -0
- package/dist/utils/experiment-type.js +94 -0
- package/dist/utils/experiment-type.js.map +1 -0
- package/dist/utils/funnel-steps-type.d.ts +42 -0
- package/dist/utils/funnel-steps-type.js +62 -0
- package/dist/utils/funnel-steps-type.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +7 -0
- package/frontend-vue/app/components/CampaignsView.vue +330 -0
- package/frontend-vue/app/components/ExperimentInput.vue +128 -0
- package/frontend-vue/app/components/FunnelDetail.vue +1026 -0
- package/frontend-vue/app/components/FunnelStepsInput.vue +154 -0
- package/frontend-vue/app/components/FunnelsView.vue +493 -0
- package/frontend-vue/app/components/HeatmapCanvas.vue +116 -0
- package/frontend-vue/app/components/NoWebsiteState.vue +18 -0
- package/frontend-vue/app/components/OverviewView.vue +493 -0
- package/frontend-vue/app/components/PagePreview.vue +915 -0
- package/frontend-vue/app/components/PagesView.vue +378 -0
- package/frontend-vue/app/components/ScrollDepthOverlay.vue +158 -0
- package/frontend-vue/app/components/TextKpiCard.vue +28 -0
- package/frontend-vue/app/components/TopListTabsCard.vue +162 -0
- package/frontend-vue/app/components/TrackerSnippet.vue +31 -0
- package/frontend-vue/app/components/VariationsInput.vue +164 -0
- package/frontend-vue/app/composables/useChildId.ts +13 -0
- package/frontend-vue/app/composables/useExperiment.ts +59 -0
- package/frontend-vue/app/composables/useFunnelSteps.ts +25 -0
- package/frontend-vue/app/composables/useLatestRequest.ts +90 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +420 -0
- package/frontend-vue/app/composables/useMarketingPeriod.ts +66 -0
- package/frontend-vue/app/composables/useMarketingRoutes.ts +51 -0
- package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +69 -0
- package/frontend-vue/app/composables/useMarketingWebsites.ts +80 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +183 -0
- package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +46 -0
- package/frontend-vue/app/utils/dmsComponent.ts +18 -0
- package/frontend-vue/app/utils/heatmapPalette.ts +41 -0
- package/frontend-vue/app/utils/snapshotViewportUnits.ts +63 -0
- package/frontend-vue/dms.frontend.ts +35 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +382 -0
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +382 -0
- package/frontend-vue/package.json +30 -0
- package/frontend-vue/pnpm-lock.yaml +5226 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/drafts.test.ts +54 -0
- package/frontend-vue/tests/i18n.test.ts +86 -0
- package/frontend-vue/tests/period.test.ts +35 -0
- package/frontend-vue/tests/registration.test.ts +63 -0
- package/frontend-vue/tests/routes.test.ts +31 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +56 -0
- package/frontend-vue/vitest.config.ts +20 -0
- package/package.json +91 -0
- package/static/snapshot.js +497 -0
- package/static/tracker.js +503 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref, watch } from 'vue'
|
|
3
|
+
import type { MarketingHeatmapPoint } from '../composables/useMarketingApi'
|
|
4
|
+
import { HEAT_STOPS, heatRgba } from '../utils/heatmapPalette'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dependency-free heatmap renderer: points are drawn as radial alpha
|
|
8
|
+
* gradients on an offscreen grayscale pass, then the alpha channel is mapped
|
|
9
|
+
* through a color palette (classic heatmap.js technique, ~none of its bytes).
|
|
10
|
+
* Coordinates are 0-1, relative to the drawn surface.
|
|
11
|
+
*
|
|
12
|
+
* `width`/`height` are CSS pixels as much as they are the backing store: the
|
|
13
|
+
* element sizes itself from them. Letting the parent's box decide instead
|
|
14
|
+
* would silently rescale x and y — a document wider than its container would
|
|
15
|
+
* have every click squeezed leftwards, and the round blobs would render as
|
|
16
|
+
* ellipses. The caller positions this; it does not stretch it.
|
|
17
|
+
*/
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
points: MarketingHeatmapPoint[]
|
|
20
|
+
maxWeight: number
|
|
21
|
+
width: number
|
|
22
|
+
height: number
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const POINT_RADIUS = 28
|
|
26
|
+
const MAX_RENDER_ALPHA = 210
|
|
27
|
+
/** Floor on a point's intensity so single clicks stay visible. */
|
|
28
|
+
const MIN_POINT_ALPHA = 0.08
|
|
29
|
+
/** Flat opacity boost applied when colorising, capped at MAX_RENDER_ALPHA. */
|
|
30
|
+
const ALPHA_BOOST = 40
|
|
31
|
+
const PALETTE_SIZE = 256
|
|
32
|
+
|
|
33
|
+
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
|
34
|
+
|
|
35
|
+
function buildPalette(): Uint8ClampedArray | null {
|
|
36
|
+
const strip = document.createElement('canvas')
|
|
37
|
+
strip.width = PALETTE_SIZE
|
|
38
|
+
strip.height = 1
|
|
39
|
+
const ctx = strip.getContext('2d')
|
|
40
|
+
if (!ctx) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
const gradient = ctx.createLinearGradient(0, 0, PALETTE_SIZE, 0)
|
|
44
|
+
for (const [stop, rgb] of HEAT_STOPS) {
|
|
45
|
+
gradient.addColorStop(stop, heatRgba(rgb, 1))
|
|
46
|
+
}
|
|
47
|
+
ctx.fillStyle = gradient
|
|
48
|
+
ctx.fillRect(0, 0, PALETTE_SIZE, 1)
|
|
49
|
+
return ctx.getImageData(0, 0, PALETTE_SIZE, 1).data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function drawIntensity(ctx: CanvasRenderingContext2D): void {
|
|
53
|
+
const reference = Math.max(props.maxWeight, 1)
|
|
54
|
+
for (const point of props.points) {
|
|
55
|
+
const px = point.x * props.width
|
|
56
|
+
const py = point.y * props.height
|
|
57
|
+
const alpha = Math.max(point.weight / reference, MIN_POINT_ALPHA)
|
|
58
|
+
const gradient = ctx.createRadialGradient(px, py, 0, px, py, POINT_RADIUS)
|
|
59
|
+
gradient.addColorStop(0, `rgba(0, 0, 0, ${alpha})`)
|
|
60
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)')
|
|
61
|
+
ctx.fillStyle = gradient
|
|
62
|
+
ctx.fillRect(px - POINT_RADIUS, py - POINT_RADIUS, POINT_RADIUS * 2, POINT_RADIUS * 2)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function colorize(ctx: CanvasRenderingContext2D): void {
|
|
67
|
+
const palette = buildPalette()
|
|
68
|
+
if (!palette) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const image = ctx.getImageData(0, 0, props.width, props.height)
|
|
72
|
+
const data = image.data
|
|
73
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
74
|
+
const alpha = data[i + 3]
|
|
75
|
+
if (alpha === 0) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const offset = Math.min(alpha, PALETTE_SIZE - 1) * 4
|
|
79
|
+
data[i] = palette[offset]
|
|
80
|
+
data[i + 1] = palette[offset + 1]
|
|
81
|
+
data[i + 2] = palette[offset + 2]
|
|
82
|
+
data[i + 3] = Math.min(alpha + ALPHA_BOOST, MAX_RENDER_ALPHA)
|
|
83
|
+
}
|
|
84
|
+
ctx.putImageData(image, 0, 0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function render(): void {
|
|
88
|
+
const canvas = canvasRef.value
|
|
89
|
+
if (!canvas || props.width <= 0 || props.height <= 0) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
canvas.width = props.width
|
|
93
|
+
canvas.height = props.height
|
|
94
|
+
const ctx = canvas.getContext('2d')
|
|
95
|
+
if (!ctx) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
ctx.clearRect(0, 0, props.width, props.height)
|
|
99
|
+
if (props.points.length === 0) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
drawIntensity(ctx)
|
|
103
|
+
colorize(ctx)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
watch(() => [props.points, props.width, props.height], render, { deep: true })
|
|
107
|
+
onMounted(render)
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<canvas
|
|
112
|
+
ref="canvasRef"
|
|
113
|
+
class="pointer-events-none absolute left-0 top-0"
|
|
114
|
+
:style="{ width: `${props.width}px`, height: `${props.height}px` }"
|
|
115
|
+
/>
|
|
116
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useI18n } from '#dms/frontend-module'
|
|
3
|
+
|
|
4
|
+
defineProps<{ icon: string, title: string, description: string }>()
|
|
5
|
+
const emit = defineEmits<{ create: [] }>()
|
|
6
|
+
const { t } = useI18n()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div class="flex flex-col items-start gap-3">
|
|
11
|
+
<UAlert :icon="icon" :title="title" :description="description" />
|
|
12
|
+
<UButton
|
|
13
|
+
icon="i-ph-plus"
|
|
14
|
+
:label="t('page.marketing.websites.create')"
|
|
15
|
+
@click="emit('create')"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, watch } from 'vue'
|
|
3
|
+
import { useI18n } from '#dms/frontend-module'
|
|
4
|
+
import { useChildId } from '../composables/useChildId'
|
|
5
|
+
import { useLatestRequest } from '../composables/useLatestRequest'
|
|
6
|
+
import {
|
|
7
|
+
useMarketingApi,
|
|
8
|
+
type MarketingOverview,
|
|
9
|
+
} from '../composables/useMarketingApi'
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_PERIOD_PRESET,
|
|
12
|
+
MARKETING_PERIOD_PRESETS,
|
|
13
|
+
MS_PER_DAY,
|
|
14
|
+
periodDays,
|
|
15
|
+
useMarketingPeriod,
|
|
16
|
+
} from '../composables/useMarketingPeriod'
|
|
17
|
+
import {
|
|
18
|
+
campaignsLink,
|
|
19
|
+
funnelsLink,
|
|
20
|
+
pagesLink,
|
|
21
|
+
} from '../composables/useMarketingRoutes'
|
|
22
|
+
import { useMarketingWebsites } from '../composables/useMarketingWebsites'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Overview dashboard: period-scoped traffic area chart, device donut and
|
|
26
|
+
* the top dimensions grouped into four cards — content, acquisition,
|
|
27
|
+
* audience, events — each a tabbed stack of top lists linking out to the
|
|
28
|
+
* surface that owns the detail (pages, campaigns, funnels). All of it is
|
|
29
|
+
* fed by one rollup-backed overview fetch. Chart/KPI primitives come from
|
|
30
|
+
* the dms-ui layer so the look is inherited, not ported; UTM sources have
|
|
31
|
+
* no card here on purpose — the campaigns table is their surface.
|
|
32
|
+
*/
|
|
33
|
+
const PERIOD_SCOPE = 'dms-marketing-overview'
|
|
34
|
+
const TOP_LIST_LIMIT = 8
|
|
35
|
+
|
|
36
|
+
// The DMS page renderer hands every component its identity; keeping it is what
|
|
37
|
+
// lets dms-ui primitives key their events and persisted state per child.
|
|
38
|
+
const props = withDefaults(
|
|
39
|
+
defineProps<{ componentId?: string, pageId?: string }>(),
|
|
40
|
+
{ componentId: 'marketing-overview', pageId: 'marketing' },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const childId = useChildId(() => props.componentId)
|
|
44
|
+
|
|
45
|
+
const api = useMarketingApi()
|
|
46
|
+
const { t, te, locale } = useI18n()
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
websites,
|
|
50
|
+
items: websiteItems,
|
|
51
|
+
selectedId,
|
|
52
|
+
loading: loadingWebsites,
|
|
53
|
+
failed: websitesFailed,
|
|
54
|
+
load: loadWebsites,
|
|
55
|
+
create: createWebsite,
|
|
56
|
+
showSnippet,
|
|
57
|
+
} = useMarketingWebsites()
|
|
58
|
+
|
|
59
|
+
// Period comes from the DMS-standard <DmsPeriodSelector>, read back through
|
|
60
|
+
// its scope registry and mapped onto the `Nd` windows the backend accepts.
|
|
61
|
+
const period = useMarketingPeriod(PERIOD_SCOPE)
|
|
62
|
+
|
|
63
|
+
const windowLength = computed(() => periodDays(period.value))
|
|
64
|
+
|
|
65
|
+
const {
|
|
66
|
+
data: overview,
|
|
67
|
+
loading: loadingOverview,
|
|
68
|
+
failed: overviewFailed,
|
|
69
|
+
settled: overviewSettled,
|
|
70
|
+
run: loadOverview,
|
|
71
|
+
} = useLatestRequest<MarketingOverview>(() =>
|
|
72
|
+
// Server-side there is no session to authenticate with: the fetch belongs to
|
|
73
|
+
// the client, like every other read here.
|
|
74
|
+
selectedId.value && !import.meta.env.SSR
|
|
75
|
+
? api.getOverview(selectedId.value, period.value)
|
|
76
|
+
: null,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// The watcher owns every overview fetch, including the first: seeding it from
|
|
80
|
+
// onMounted as well would issue the initial request twice. The endpoint lists
|
|
81
|
+
// websites by last activity, so the default selection is the one that has
|
|
82
|
+
// something to show. Immediate costs nothing while the selection starts empty
|
|
83
|
+
// — the fetcher just answers null — and is what keeps this correct the day a
|
|
84
|
+
// deep link seeds it: resolving the site list would then write the same id
|
|
85
|
+
// back, which is no change to watch, and a non-immediate watcher would never
|
|
86
|
+
// fetch at all.
|
|
87
|
+
watch([selectedId, period], () => {
|
|
88
|
+
void loadOverview()
|
|
89
|
+
}, { immediate: true })
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The window between a site being selected and the watcher firing on the next
|
|
93
|
+
* tick counts as loading: dropping the flag there would flash an empty
|
|
94
|
+
* dashboard between the two fetches.
|
|
95
|
+
*/
|
|
96
|
+
const loading = computed(
|
|
97
|
+
() =>
|
|
98
|
+
loadingWebsites.value
|
|
99
|
+
|| loadingOverview.value
|
|
100
|
+
|| (!!selectedId.value && !overviewSettled.value),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Two failures, two flags: retrying the overview must not clear the one that
|
|
104
|
+
// says the site list itself never loaded.
|
|
105
|
+
const failed = computed(() => websitesFailed.value || overviewFailed.value)
|
|
106
|
+
|
|
107
|
+
/** Retry entry point: whichever of the two fetches has nothing to show. */
|
|
108
|
+
function reload() {
|
|
109
|
+
return websites.value.length === 0 ? loadWebsites() : loadOverview()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted(loadWebsites)
|
|
113
|
+
|
|
114
|
+
// --- KPIs -------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/** "1m 32s" the way Plausible writes it — locale-neutral abbreviations. */
|
|
117
|
+
function formatDuration(ms: number): string {
|
|
118
|
+
const totalSeconds = Math.round(ms / 1000)
|
|
119
|
+
const hours = Math.floor(totalSeconds / 3600)
|
|
120
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
|
121
|
+
if (hours > 0) {
|
|
122
|
+
return `${hours}h ${minutes}m`
|
|
123
|
+
}
|
|
124
|
+
const seconds = totalSeconds % 60
|
|
125
|
+
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface OverviewKpi {
|
|
129
|
+
id: string
|
|
130
|
+
label: string
|
|
131
|
+
icon: string
|
|
132
|
+
/** Rendered by DmsKpiCard when numeric… */
|
|
133
|
+
value?: number
|
|
134
|
+
format?: 'number' | 'percent'
|
|
135
|
+
/** …and by the local text card when already composed (duration). */
|
|
136
|
+
display?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const kpis = computed<OverviewKpi[]>(() => {
|
|
140
|
+
const totals = overview.value?.totals
|
|
141
|
+
const sessions = totals?.sessions ?? 0
|
|
142
|
+
// Transition-counted bounces can sum below zero on windows cutting a
|
|
143
|
+
// session in two; a negative rate is never worth showing.
|
|
144
|
+
const bounceRate = sessions > 0
|
|
145
|
+
? (Math.max(0, totals?.bouncedSessions ?? 0) / sessions) * 100
|
|
146
|
+
: 0
|
|
147
|
+
const pagesPerSession = sessions > 0
|
|
148
|
+
? Math.round(((totals?.pageviews ?? 0) / sessions) * 10) / 10
|
|
149
|
+
: 0
|
|
150
|
+
const avgDuration = sessions > 0 ? (totals?.sessionDurationMs ?? 0) / sessions : 0
|
|
151
|
+
return [
|
|
152
|
+
{ id: 'pageviews', label: t('page.marketing.overview.kpis.pageviews'), value: totals?.pageviews ?? 0, format: 'number', icon: 'i-ph-eye' },
|
|
153
|
+
{ id: 'sessions', label: t('page.marketing.overview.kpis.sessions'), value: totals?.sessions ?? 0, format: 'number', icon: 'i-ph-users' },
|
|
154
|
+
{ id: 'new-visitors', label: t('page.marketing.overview.kpis.new_visitors'), value: totals?.newVisitors ?? 0, format: 'number', icon: 'i-ph-user-plus' },
|
|
155
|
+
{ id: 'custom-events', label: t('page.marketing.overview.kpis.custom_events'), value: totals?.customEvents ?? 0, format: 'number', icon: 'i-ph-cursor-click' },
|
|
156
|
+
{ id: 'bounce-rate', label: t('page.marketing.overview.kpis.bounce_rate'), value: bounceRate, format: 'percent', icon: 'i-ph-arrow-u-up-left' },
|
|
157
|
+
{ id: 'avg-session-duration', label: t('page.marketing.overview.kpis.avg_session_duration'), display: formatDuration(avgDuration), icon: 'i-ph-timer' },
|
|
158
|
+
{ id: 'pages-per-session', label: t('page.marketing.overview.kpis.pages_per_session'), value: pagesPerSession, format: 'number', icon: 'i-ph-stack' },
|
|
159
|
+
]
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// --- Traffic chart ----------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
function utcMidnightToday(): number {
|
|
165
|
+
const now = new Date()
|
|
166
|
+
return Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Every day of the window, so days without traffic plot at zero. */
|
|
170
|
+
const windowDays = computed(() => {
|
|
171
|
+
const today = utcMidnightToday()
|
|
172
|
+
const count = windowLength.value
|
|
173
|
+
return Array.from({ length: count }, (_, i) => today - (count - 1 - i) * MS_PER_DAY)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const trafficSeries = computed(() => {
|
|
177
|
+
const days = overview.value?.days ?? []
|
|
178
|
+
const byDay = new Map(days.map(d => [d.day, d]))
|
|
179
|
+
const point = (day: number, pick: (v: { pageviews: number, sessions: number }) => number) => ({
|
|
180
|
+
x: new Date(day),
|
|
181
|
+
y: pick(byDay.get(day) ?? { pageviews: 0, sessions: 0 }),
|
|
182
|
+
})
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
name: t('page.marketing.overview.traffic.pageviews'),
|
|
186
|
+
data: windowDays.value.map(day => point(day, v => v.pageviews)),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: t('page.marketing.overview.traffic.sessions'),
|
|
190
|
+
data: windowDays.value.map(day => point(day, v => v.sessions)),
|
|
191
|
+
},
|
|
192
|
+
]
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const hasTraffic = computed(() => (overview.value?.totals.pageviews ?? 0) > 0)
|
|
196
|
+
|
|
197
|
+
// --- Devices donut ----------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
const deviceRecords = computed(() =>
|
|
200
|
+
Object.entries(overview.value?.topDevices ?? {})
|
|
201
|
+
.sort((a, b) => b[1] - a[1])
|
|
202
|
+
.map(([label, value]) => ({ label, value })),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// --- Top lists --------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `linkTo` is only passed for pages: a path has a surface of its own (its
|
|
209
|
+
* heatmap), while a referrer or a browser has nowhere to lead.
|
|
210
|
+
*/
|
|
211
|
+
function toTopItems(
|
|
212
|
+
record: Record<string, number> | undefined,
|
|
213
|
+
linkTo?: (key: string) => ReturnType<typeof pagesLink>,
|
|
214
|
+
) {
|
|
215
|
+
return Object.entries(record ?? {})
|
|
216
|
+
.sort((a, b) => b[1] - a[1])
|
|
217
|
+
.slice(0, TOP_LIST_LIMIT)
|
|
218
|
+
.map(([label, value]) => ({
|
|
219
|
+
id: label,
|
|
220
|
+
title: label,
|
|
221
|
+
value,
|
|
222
|
+
...(linkTo ? { to: linkTo(label) } : {}),
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const regionNames = computed(() => new Intl.DisplayNames([locale.value], { type: 'region' }))
|
|
227
|
+
|
|
228
|
+
/** Rollup keys are ISO codes but historical map entries can be anything, and
|
|
229
|
+
* `DisplayNames.of` throws on a malformed region code. */
|
|
230
|
+
function countryLabel(code: string): string {
|
|
231
|
+
try {
|
|
232
|
+
return regionNames.value.of(code) ?? code
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return code
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const languageNames = computed(() => new Intl.DisplayNames([locale.value], { type: 'language' }))
|
|
240
|
+
|
|
241
|
+
/** Same guard as countries: keys are whatever `navigator.language` reported. */
|
|
242
|
+
function languageLabel(tag: string): string {
|
|
243
|
+
try {
|
|
244
|
+
return languageNames.value.of(tag) ?? tag
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return tag
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Channel keys localize on the campaigns page's labels; a key minted outside
|
|
252
|
+
* the known five still shows raw. */
|
|
253
|
+
function channelLabel(channel: string): string {
|
|
254
|
+
const key = `page.marketing.campaigns.channels.${channel}`
|
|
255
|
+
return te(key) ? t(key) : channel
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const groupCards = computed(() => {
|
|
259
|
+
const toPage = (path: string) =>
|
|
260
|
+
pagesLink({ website: selectedId.value, path, period: period.value })
|
|
261
|
+
const label = (key: string) => t(`page.marketing.overview.groups.${key}`)
|
|
262
|
+
return [
|
|
263
|
+
// Entry and exit are paths too, so every row leads to the page surface.
|
|
264
|
+
{
|
|
265
|
+
id: 'content',
|
|
266
|
+
title: label('content.title'),
|
|
267
|
+
stateKey: 'content_tab',
|
|
268
|
+
footerLabel: label('content.view_all'),
|
|
269
|
+
footerTo: pagesLink({ website: selectedId.value, period: period.value }),
|
|
270
|
+
tabs: [
|
|
271
|
+
{ id: 'pages', label: label('content.tabs.pages'), items: toTopItems(overview.value?.topPages, toPage) },
|
|
272
|
+
{ id: 'entry', label: label('content.tabs.entry'), items: toTopItems(overview.value?.topEntryPages, toPage) },
|
|
273
|
+
{ id: 'exit', label: label('content.tabs.exit'), items: toTopItems(overview.value?.topExitPages, toPage) },
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
// No UTM tab: the campaigns table owns that detail, the footer leads there.
|
|
277
|
+
{
|
|
278
|
+
id: 'acquisition',
|
|
279
|
+
title: label('acquisition.title'),
|
|
280
|
+
stateKey: 'acquisition_tab',
|
|
281
|
+
footerLabel: label('acquisition.view_all'),
|
|
282
|
+
footerTo: campaignsLink({ website: selectedId.value, period: period.value }),
|
|
283
|
+
tabs: [
|
|
284
|
+
{
|
|
285
|
+
id: 'channels',
|
|
286
|
+
label: label('acquisition.tabs.channels'),
|
|
287
|
+
items: toTopItems(overview.value?.topChannels).map(item =>
|
|
288
|
+
({ ...item, title: channelLabel(item.title) })),
|
|
289
|
+
},
|
|
290
|
+
{ id: 'referrers', label: label('acquisition.tabs.referrers'), items: toTopItems(overview.value?.topReferrers) },
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'audience',
|
|
295
|
+
title: label('audience.title'),
|
|
296
|
+
stateKey: 'audience_tab',
|
|
297
|
+
tabs: [
|
|
298
|
+
{ id: 'browsers', label: label('audience.tabs.browsers'), items: toTopItems(overview.value?.topBrowsers) },
|
|
299
|
+
{
|
|
300
|
+
id: 'languages',
|
|
301
|
+
label: label('audience.tabs.languages'),
|
|
302
|
+
items: toTopItems(overview.value?.topLanguages).map(item =>
|
|
303
|
+
({ ...item, title: languageLabel(item.title) })),
|
|
304
|
+
},
|
|
305
|
+
// Without a GeoIP database the map can only ever be empty — no tab
|
|
306
|
+
// at all beats a permanently empty one.
|
|
307
|
+
...(overview.value?.geoipEnabled
|
|
308
|
+
? [{
|
|
309
|
+
id: 'countries',
|
|
310
|
+
label: label('audience.tabs.countries'),
|
|
311
|
+
items: toTopItems(overview.value?.topCountries).map(item =>
|
|
312
|
+
({ ...item, title: countryLabel(item.title) })),
|
|
313
|
+
}]
|
|
314
|
+
: []),
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
// The names behind the custom-events KPI: which CTA gets clicked.
|
|
318
|
+
{
|
|
319
|
+
id: 'events',
|
|
320
|
+
title: label('events.title'),
|
|
321
|
+
footerLabel: label('events.view_all'),
|
|
322
|
+
footerTo: funnelsLink({ website: selectedId.value, period: period.value }),
|
|
323
|
+
tabs: [
|
|
324
|
+
{ id: 'events', label: label('events.title'), items: toTopItems(overview.value?.topEvents) },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
]
|
|
328
|
+
})
|
|
329
|
+
</script>
|
|
330
|
+
|
|
331
|
+
<template>
|
|
332
|
+
<div class="space-y-6 p-6">
|
|
333
|
+
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
334
|
+
<div>
|
|
335
|
+
<h1 class="text-xl font-semibold">{{ t('page.marketing.overview.title') }}</h1>
|
|
336
|
+
<p class="text-sm text-muted">{{ t('page.marketing.overview.subtitle') }}</p>
|
|
337
|
+
</div>
|
|
338
|
+
<div class="flex items-center gap-3">
|
|
339
|
+
<USelect
|
|
340
|
+
v-if="websites.length > 1"
|
|
341
|
+
v-model="selectedId"
|
|
342
|
+
:items="websiteItems"
|
|
343
|
+
class="w-56"
|
|
344
|
+
/>
|
|
345
|
+
<UButton
|
|
346
|
+
v-if="selectedId"
|
|
347
|
+
icon="i-ph-code"
|
|
348
|
+
color="neutral"
|
|
349
|
+
variant="ghost"
|
|
350
|
+
size="sm"
|
|
351
|
+
:label="t('page.marketing.websites.snippet.button')"
|
|
352
|
+
@click="showSnippet"
|
|
353
|
+
/>
|
|
354
|
+
<UButton
|
|
355
|
+
v-if="websites.length > 0"
|
|
356
|
+
icon="i-ph-plus"
|
|
357
|
+
color="neutral"
|
|
358
|
+
variant="ghost"
|
|
359
|
+
size="sm"
|
|
360
|
+
:label="t('page.marketing.websites.create')"
|
|
361
|
+
@click="createWebsite"
|
|
362
|
+
/>
|
|
363
|
+
<DmsPeriodSelector
|
|
364
|
+
:id="PERIOD_SCOPE"
|
|
365
|
+
:component-id="childId('period')"
|
|
366
|
+
:page-id="props.pageId"
|
|
367
|
+
:default-preset="DEFAULT_PERIOD_PRESET"
|
|
368
|
+
:presets="MARKETING_PERIOD_PRESETS"
|
|
369
|
+
:comparisons="['none']"
|
|
370
|
+
:show-range-label="false"
|
|
371
|
+
/>
|
|
372
|
+
<UButton
|
|
373
|
+
icon="i-ph-arrow-clockwise"
|
|
374
|
+
color="neutral"
|
|
375
|
+
variant="subtle"
|
|
376
|
+
size="sm"
|
|
377
|
+
:loading="loading"
|
|
378
|
+
@click="loadOverview"
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div v-if="failed" class="flex flex-col items-start gap-3">
|
|
384
|
+
<UAlert
|
|
385
|
+
color="error"
|
|
386
|
+
variant="subtle"
|
|
387
|
+
icon="i-ph-warning-circle"
|
|
388
|
+
:title="t('page.marketing.overview.error.title')"
|
|
389
|
+
:description="t('page.marketing.overview.error.description')"
|
|
390
|
+
/>
|
|
391
|
+
<UButton
|
|
392
|
+
icon="i-ph-arrow-clockwise"
|
|
393
|
+
:label="t('page.marketing.overview.retry')"
|
|
394
|
+
color="neutral"
|
|
395
|
+
variant="subtle"
|
|
396
|
+
size="sm"
|
|
397
|
+
@click="reload"
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<DmsMarketingNoWebsiteState
|
|
402
|
+
v-else-if="!loading && websites.length === 0"
|
|
403
|
+
icon="i-ph-chart-line-up"
|
|
404
|
+
:title="t('page.marketing.overview.empty.title')"
|
|
405
|
+
:description="t('page.marketing.overview.empty.description')"
|
|
406
|
+
@create="createWebsite"
|
|
407
|
+
/>
|
|
408
|
+
|
|
409
|
+
<!-- Only before the first answer: a refetch keeps the dashboard on screen
|
|
410
|
+
and spins the refresh button instead. -->
|
|
411
|
+
<USkeleton v-else-if="loading && !overview" class="h-96 w-full" />
|
|
412
|
+
|
|
413
|
+
<template v-else>
|
|
414
|
+
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
415
|
+
<template v-for="kpi in kpis" :key="kpi.id">
|
|
416
|
+
<DmsMarketingTextKpiCard
|
|
417
|
+
v-if="kpi.display !== undefined"
|
|
418
|
+
:title="kpi.label"
|
|
419
|
+
:value="kpi.display"
|
|
420
|
+
:icon="kpi.icon"
|
|
421
|
+
/>
|
|
422
|
+
<DmsKpiCard
|
|
423
|
+
v-else
|
|
424
|
+
:component-id="childId(`kpi-${kpi.id}`)"
|
|
425
|
+
:page-id="props.pageId"
|
|
426
|
+
variant="stat"
|
|
427
|
+
:title="kpi.label"
|
|
428
|
+
:icon="kpi.icon"
|
|
429
|
+
:static-value="kpi.value"
|
|
430
|
+
:show-delta="false"
|
|
431
|
+
:value-format="kpi.format"
|
|
432
|
+
/>
|
|
433
|
+
</template>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<div class="grid gap-4 lg:grid-cols-3">
|
|
437
|
+
<UCard class="lg:col-span-2">
|
|
438
|
+
<template #header>
|
|
439
|
+
<div>
|
|
440
|
+
<p class="font-medium">{{ t('page.marketing.overview.traffic.title') }}</p>
|
|
441
|
+
<p class="text-sm text-muted">{{ t('page.marketing.overview.traffic.subtitle') }}</p>
|
|
442
|
+
</div>
|
|
443
|
+
</template>
|
|
444
|
+
<DmsChart
|
|
445
|
+
v-if="hasTraffic"
|
|
446
|
+
:component-id="childId('traffic')"
|
|
447
|
+
:page-id="props.pageId"
|
|
448
|
+
type="area"
|
|
449
|
+
:static-dataset="trafficSeries"
|
|
450
|
+
:smooth="true"
|
|
451
|
+
:show-legend="true"
|
|
452
|
+
xaxis-type="datetime"
|
|
453
|
+
height="300px"
|
|
454
|
+
/>
|
|
455
|
+
<p v-else class="py-12 text-center text-sm text-muted">
|
|
456
|
+
{{ t('page.marketing.overview.traffic.empty') }}
|
|
457
|
+
</p>
|
|
458
|
+
</UCard>
|
|
459
|
+
|
|
460
|
+
<UCard>
|
|
461
|
+
<template #header>
|
|
462
|
+
<p class="font-medium">{{ t('page.marketing.overview.devices.title') }}</p>
|
|
463
|
+
</template>
|
|
464
|
+
<DmsChart
|
|
465
|
+
v-if="deviceRecords.length > 0"
|
|
466
|
+
:component-id="childId('devices')"
|
|
467
|
+
:page-id="props.pageId"
|
|
468
|
+
type="donut"
|
|
469
|
+
:static-dataset="deviceRecords"
|
|
470
|
+
height="300px"
|
|
471
|
+
:show-total="true"
|
|
472
|
+
/>
|
|
473
|
+
<p v-else class="py-12 text-center text-sm text-muted">
|
|
474
|
+
{{ t('page.marketing.overview.devices.empty') }}
|
|
475
|
+
</p>
|
|
476
|
+
</UCard>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
480
|
+
<DmsMarketingTopListTabsCard
|
|
481
|
+
v-for="card in groupCards"
|
|
482
|
+
:key="card.id"
|
|
483
|
+
:title="card.title"
|
|
484
|
+
:tabs="card.tabs"
|
|
485
|
+
:state-key="card.stateKey"
|
|
486
|
+
:empty-label="t('page.marketing.overview.groups.empty')"
|
|
487
|
+
:footer-label="card.footerLabel"
|
|
488
|
+
:footer-to="card.footerTo"
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
</template>
|
|
492
|
+
</div>
|
|
493
|
+
</template>
|