@antelopejs/dms-marketing 0.3.3 → 0.4.1

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.
Files changed (289) hide show
  1. package/README.md +18 -15
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.js +4 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/data-api/funnels.d.ts +2 -0
  6. package/dist/data-api/funnels.js +8 -1
  7. package/dist/data-api/funnels.js.map +1 -1
  8. package/dist/db/models/funnels.model.d.ts +2 -0
  9. package/dist/db/models/funnels.model.js +9 -0
  10. package/dist/db/models/funnels.model.js.map +1 -1
  11. package/dist/db/models/index.d.ts +1 -0
  12. package/dist/db/models/index.js +1 -0
  13. package/dist/db/models/index.js.map +1 -1
  14. package/dist/db/models/marketing_events.model.d.ts +22 -0
  15. package/dist/db/models/marketing_events.model.js +70 -5
  16. package/dist/db/models/marketing_events.model.js.map +1 -1
  17. package/dist/db/models/marketing_preferences.model.d.ts +25 -0
  18. package/dist/db/models/marketing_preferences.model.js +29 -0
  19. package/dist/db/models/marketing_preferences.model.js.map +1 -0
  20. package/dist/db/models/marketing_sessions.model.d.ts +2 -1
  21. package/dist/db/models/marketing_sessions.model.js +11 -1
  22. package/dist/db/models/marketing_sessions.model.js.map +1 -1
  23. package/dist/db/models/page_snapshots.model.d.ts +2 -1
  24. package/dist/db/models/page_snapshots.model.js +7 -7
  25. package/dist/db/models/page_snapshots.model.js.map +1 -1
  26. package/dist/db/models/website_statistics.model.d.ts +4 -1
  27. package/dist/db/models/website_statistics.model.js +15 -1
  28. package/dist/db/models/website_statistics.model.js.map +1 -1
  29. package/dist/db/tables/index.d.ts +1 -0
  30. package/dist/db/tables/index.js +1 -0
  31. package/dist/db/tables/index.js.map +1 -1
  32. package/dist/db/tables/marketing_preferences.table.d.ts +6 -0
  33. package/dist/db/tables/marketing_preferences.table.js +31 -0
  34. package/dist/db/tables/marketing_preferences.table.js.map +1 -0
  35. package/dist/hooks/index.js +8 -4
  36. package/dist/hooks/index.js.map +1 -1
  37. package/dist/index.js +18 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/pages/acquisition/page.d.ts +9 -0
  40. package/dist/pages/acquisition/page.js +181 -0
  41. package/dist/pages/acquisition/page.js.map +1 -0
  42. package/dist/pages/all.d.ts +5 -1
  43. package/dist/pages/all.js +5 -1
  44. package/dist/pages/all.js.map +1 -1
  45. package/dist/pages/blocks.d.ts +18 -0
  46. package/dist/pages/blocks.js +56 -0
  47. package/dist/pages/blocks.js.map +1 -0
  48. package/dist/pages/funnel/page.d.ts +9 -0
  49. package/dist/pages/funnel/page.js +36 -0
  50. package/dist/pages/funnel/page.js.map +1 -0
  51. package/dist/pages/funnel-builder/page.d.ts +9 -0
  52. package/dist/pages/funnel-builder/page.js +29 -0
  53. package/dist/pages/funnel-builder/page.js.map +1 -0
  54. package/dist/pages/funnels/page.d.ts +1 -1
  55. package/dist/pages/funnels/page.js +200 -3
  56. package/dist/pages/funnels/page.js.map +1 -1
  57. package/dist/pages/index.d.ts +1 -0
  58. package/dist/pages/index.js +1 -0
  59. package/dist/pages/index.js.map +1 -1
  60. package/dist/pages/{campaigns → install}/page.d.ts +3 -3
  61. package/dist/pages/{campaigns → install}/page.js +14 -12
  62. package/dist/pages/install/page.js.map +1 -0
  63. package/dist/pages/module.d.ts +3 -0
  64. package/dist/pages/module.js +30 -3
  65. package/dist/pages/module.js.map +1 -1
  66. package/dist/pages/overview/page.d.ts +1 -1
  67. package/dist/pages/overview/page.js +122 -3
  68. package/dist/pages/overview/page.js.map +1 -1
  69. package/dist/pages/pages/page.d.ts +1 -1
  70. package/dist/pages/pages/page.js +9 -4
  71. package/dist/pages/pages/page.js.map +1 -1
  72. package/dist/pages/quick-actions.d.ts +1 -0
  73. package/dist/pages/quick-actions.js +25 -0
  74. package/dist/pages/quick-actions.js.map +1 -0
  75. package/dist/pages/settings/form.d.ts +2 -1
  76. package/dist/pages/settings/form.js +62 -20
  77. package/dist/pages/settings/form.js.map +1 -1
  78. package/dist/pages/settings/page.d.ts +4 -1
  79. package/dist/pages/settings/page.js +50 -4
  80. package/dist/pages/settings/page.js.map +1 -1
  81. package/dist/pages/websites/page.d.ts +11 -0
  82. package/dist/pages/websites/page.js +62 -0
  83. package/dist/pages/websites/page.js.map +1 -0
  84. package/dist/routes/blocks.d.ts +50 -0
  85. package/dist/routes/blocks.js +269 -0
  86. package/dist/routes/blocks.js.map +1 -0
  87. package/dist/routes/collect.d.ts +1 -2
  88. package/dist/routes/collect.js +19 -12
  89. package/dist/routes/collect.js.map +1 -1
  90. package/dist/routes/context.d.ts +8 -0
  91. package/dist/routes/context.js +54 -0
  92. package/dist/routes/context.js.map +1 -0
  93. package/dist/routes/experiments.js +2 -1
  94. package/dist/routes/experiments.js.map +1 -1
  95. package/dist/routes/funnels.d.ts +21 -3
  96. package/dist/routes/funnels.js +150 -7
  97. package/dist/routes/funnels.js.map +1 -1
  98. package/dist/routes/index.d.ts +2 -0
  99. package/dist/routes/index.js +2 -0
  100. package/dist/routes/index.js.map +1 -1
  101. package/dist/routes/settings.d.ts +6 -1
  102. package/dist/routes/settings.js +65 -4
  103. package/dist/routes/settings.js.map +1 -1
  104. package/dist/routes/snapshot.d.ts +1 -1
  105. package/dist/routes/snapshot.js +24 -25
  106. package/dist/routes/snapshot.js.map +1 -1
  107. package/dist/routes/stats.d.ts +14 -23
  108. package/dist/routes/stats.js +73 -97
  109. package/dist/routes/stats.js.map +1 -1
  110. package/dist/routes/tracker.js +6 -4
  111. package/dist/routes/tracker.js.map +1 -1
  112. package/dist/routes/websites.d.ts +4 -0
  113. package/dist/routes/websites.js +111 -12
  114. package/dist/routes/websites.js.map +1 -1
  115. package/dist/services/acquisition-report.d.ts +62 -0
  116. package/dist/services/acquisition-report.js +181 -0
  117. package/dist/services/acquisition-report.js.map +1 -0
  118. package/dist/services/acquisition.d.ts +9 -3
  119. package/dist/services/acquisition.js +35 -5
  120. package/dist/services/acquisition.js.map +1 -1
  121. package/dist/services/catalog.d.ts +3 -0
  122. package/dist/services/catalog.js +17 -0
  123. package/dist/services/catalog.js.map +1 -0
  124. package/dist/services/collect.d.ts +1 -2
  125. package/dist/services/collect.js +49 -24
  126. package/dist/services/collect.js.map +1 -1
  127. package/dist/services/composed.d.ts +9 -0
  128. package/dist/services/composed.js +62 -0
  129. package/dist/services/composed.js.map +1 -0
  130. package/dist/services/context.d.ts +20 -0
  131. package/dist/services/context.js +76 -0
  132. package/dist/services/context.js.map +1 -0
  133. package/dist/services/dashboard.d.ts +74 -0
  134. package/dist/services/dashboard.js +343 -0
  135. package/dist/services/dashboard.js.map +1 -0
  136. package/dist/services/experiments/results.d.ts +12 -1
  137. package/dist/services/experiments/results.js +19 -2
  138. package/dist/services/experiments/results.js.map +1 -1
  139. package/dist/services/experiments/stats.d.ts +7 -1
  140. package/dist/services/experiments/stats.js +43 -5
  141. package/dist/services/experiments/stats.js.map +1 -1
  142. package/dist/services/funnel-report.d.ts +64 -0
  143. package/dist/services/funnel-report.js +280 -0
  144. package/dist/services/funnel-report.js.map +1 -0
  145. package/dist/services/funnels/compute.d.ts +2 -0
  146. package/dist/services/funnels/compute.js +25 -13
  147. package/dist/services/funnels/compute.js.map +1 -1
  148. package/dist/services/funnels/results.js +4 -1
  149. package/dist/services/funnels/results.js.map +1 -1
  150. package/dist/services/funnels/step-path.d.ts +4 -0
  151. package/dist/services/funnels/step-path.js +47 -0
  152. package/dist/services/funnels/step-path.js.map +1 -0
  153. package/dist/services/funnels-table.d.ts +31 -0
  154. package/dist/services/funnels-table.js +230 -0
  155. package/dist/services/funnels-table.js.map +1 -0
  156. package/dist/services/heatmap.d.ts +15 -3
  157. package/dist/services/heatmap.js +54 -11
  158. package/dist/services/heatmap.js.map +1 -1
  159. package/dist/services/hourly-traffic.d.ts +4 -0
  160. package/dist/services/hourly-traffic.js +35 -0
  161. package/dist/services/hourly-traffic.js.map +1 -0
  162. package/dist/services/origin-guard.d.ts +9 -1
  163. package/dist/services/origin-guard.js +46 -1
  164. package/dist/services/origin-guard.js.map +1 -1
  165. package/dist/services/period.d.ts +26 -1
  166. package/dist/services/period.js +99 -9
  167. package/dist/services/period.js.map +1 -1
  168. package/dist/services/prune.d.ts +6 -0
  169. package/dist/services/prune.js +35 -0
  170. package/dist/services/prune.js.map +1 -1
  171. package/dist/services/request-body.d.ts +1 -2
  172. package/dist/services/request-body.js +10 -36
  173. package/dist/services/request-body.js.map +1 -1
  174. package/dist/services/request-meta.d.ts +0 -1
  175. package/dist/services/request-meta.js +0 -11
  176. package/dist/services/request-meta.js.map +1 -1
  177. package/dist/services/rollups.d.ts +25 -0
  178. package/dist/services/rollups.js +102 -0
  179. package/dist/services/rollups.js.map +1 -0
  180. package/dist/services/sessionize.d.ts +5 -1
  181. package/dist/services/sessionize.js +52 -10
  182. package/dist/services/sessionize.js.map +1 -1
  183. package/dist/services/settings.d.ts +41 -2
  184. package/dist/services/settings.js +219 -6
  185. package/dist/services/settings.js.map +1 -1
  186. package/dist/services/snapshots.d.ts +3 -2
  187. package/dist/services/snapshots.js +110 -9
  188. package/dist/services/snapshots.js.map +1 -1
  189. package/dist/services/tenant-website.js +1 -0
  190. package/dist/services/tenant-website.js.map +1 -1
  191. package/dist/services/url.js +12 -4
  192. package/dist/services/url.js.map +1 -1
  193. package/dist/services/website-hosts.d.ts +15 -0
  194. package/dist/services/website-hosts.js +67 -0
  195. package/dist/services/website-hosts.js.map +1 -0
  196. package/dist/services/website-report.d.ts +40 -0
  197. package/dist/services/website-report.js +84 -0
  198. package/dist/services/website-report.js.map +1 -0
  199. package/dist/types/constants.d.ts +14 -2
  200. package/dist/types/constants.js +17 -6
  201. package/dist/types/constants.js.map +1 -1
  202. package/dist/types/events.d.ts +221 -68
  203. package/dist/types/events.js +60 -11
  204. package/dist/types/events.js.map +1 -1
  205. package/dist/types/snapshots.d.ts +6 -0
  206. package/dist/types/snapshots.js +2 -0
  207. package/dist/types/snapshots.js.map +1 -1
  208. package/dist/types/statistics.d.ts +2 -1
  209. package/dist/types/statistics.js +9 -2
  210. package/dist/types/statistics.js.map +1 -1
  211. package/dist/utils/experiment-type.js +1 -1
  212. package/dist/utils/experiment-type.js.map +1 -1
  213. package/dist/utils/funnel-steps-type.d.ts +21 -3
  214. package/dist/utils/funnel-steps-type.js +7 -2
  215. package/dist/utils/funnel-steps-type.js.map +1 -1
  216. package/frontend-vue/app/components/ChannelsCard.vue +265 -0
  217. package/frontend-vue/app/components/Context.vue +479 -0
  218. package/frontend-vue/app/components/ExperimentInput.vue +3 -1
  219. package/frontend-vue/app/components/ExperimentReport.vue +736 -0
  220. package/frontend-vue/app/components/FirstRun.vue +60 -0
  221. package/frontend-vue/app/components/FunnelBuilder.vue +1595 -0
  222. package/frontend-vue/app/components/FunnelFigure.vue +640 -0
  223. package/frontend-vue/app/components/FunnelReport.vue +532 -0
  224. package/frontend-vue/app/components/FunnelStepsInput.vue +3 -1
  225. package/frontend-vue/app/components/FunnelTemplates.vue +161 -0
  226. package/frontend-vue/app/components/HeatmapCanvas.vue +11 -4
  227. package/frontend-vue/app/components/InstallGuide.vue +753 -0
  228. package/frontend-vue/app/components/PagePreview.vue +889 -416
  229. package/frontend-vue/app/components/PagesExplorer.vue +552 -0
  230. package/frontend-vue/app/components/ScrollDepthOverlay.vue +48 -20
  231. package/frontend-vue/app/components/TopListTabs.vue +317 -0
  232. package/frontend-vue/app/components/TrackerSnippet.vue +50 -16
  233. package/frontend-vue/app/components/VariationsInput.vue +15 -5
  234. package/frontend-vue/app/components/WebsitesGrid.vue +799 -0
  235. package/frontend-vue/app/composables/useContextLink.ts +16 -0
  236. package/frontend-vue/app/composables/useExperiment.ts +1 -2
  237. package/frontend-vue/app/composables/useFunnelSteps.ts +1 -2
  238. package/frontend-vue/app/composables/useLatestRequest.ts +2 -4
  239. package/frontend-vue/app/composables/useMarketingApi.ts +298 -133
  240. package/frontend-vue/app/composables/useMarketingContext.ts +55 -0
  241. package/frontend-vue/app/composables/useMarketingRoutes.ts +80 -27
  242. package/frontend-vue/app/composables/useSnapshotFrame.ts +26 -4
  243. package/frontend-vue/app/constants.ts +22 -0
  244. package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +2 -1
  245. package/frontend-vue/app/utils/channels.ts +37 -0
  246. package/frontend-vue/app/utils/format.ts +132 -0
  247. package/frontend-vue/app/utils/funnel-draft.ts +264 -0
  248. package/frontend-vue/app/utils/heatmap.ts +82 -0
  249. package/frontend-vue/app/utils/heatmapPalette.ts +10 -0
  250. package/frontend-vue/app/utils/host-conflict.ts +48 -0
  251. package/frontend-vue/app/utils/sample-size.ts +17 -0
  252. package/frontend-vue/app/utils/snapshotViewportUnits.ts +15 -5
  253. package/frontend-vue/app/utils/snippet.ts +53 -0
  254. package/frontend-vue/app/utils/step-path.ts +32 -0
  255. package/frontend-vue/dms.frontend.ts +4 -3
  256. package/frontend-vue/i18n/locales/marketing-en-GB.json +1083 -316
  257. package/frontend-vue/i18n/locales/marketing-fr-FR.json +1087 -320
  258. package/frontend-vue/package.json +1 -2
  259. package/frontend-vue/pnpm-lock.yaml +51 -19
  260. package/frontend-vue/tests/drafts.test.ts +3 -3
  261. package/frontend-vue/tests/format.test.ts +73 -0
  262. package/frontend-vue/tests/funnel-draft.test.ts +194 -0
  263. package/frontend-vue/tests/heatmap.test.ts +32 -0
  264. package/frontend-vue/tests/i18n.test.ts +87 -4
  265. package/frontend-vue/tests/latest-request.test.ts +6 -3
  266. package/frontend-vue/tests/locale-typography.test.ts +115 -0
  267. package/frontend-vue/tests/registration.test.ts +17 -11
  268. package/frontend-vue/tests/routes.test.ts +67 -7
  269. package/frontend-vue/tests/server-parity.test.ts +77 -0
  270. package/frontend-vue/tests/stubs/frontend-module.ts +18 -3
  271. package/frontend-vue/vitest.config.ts +3 -1
  272. package/package.json +10 -8
  273. package/static/snapshot.js +46 -16
  274. package/static/tracker.js +139 -48
  275. package/dist/pages/campaigns/page.js.map +0 -1
  276. package/frontend-vue/app/components/CampaignsView.vue +0 -330
  277. package/frontend-vue/app/components/FunnelDetail.vue +0 -1026
  278. package/frontend-vue/app/components/FunnelsView.vue +0 -493
  279. package/frontend-vue/app/components/NoWebsiteState.vue +0 -18
  280. package/frontend-vue/app/components/OverviewView.vue +0 -493
  281. package/frontend-vue/app/components/PagesView.vue +0 -391
  282. package/frontend-vue/app/components/TextKpiCard.vue +0 -28
  283. package/frontend-vue/app/components/TopListTabsCard.vue +0 -162
  284. package/frontend-vue/app/composables/useChildId.ts +0 -13
  285. package/frontend-vue/app/composables/useMarketingPeriod.ts +0 -65
  286. package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +0 -69
  287. package/frontend-vue/app/composables/useMarketingWebsites.ts +0 -80
  288. package/frontend-vue/app/utils/dmsComponent.ts +0 -18
  289. package/frontend-vue/tests/period.test.ts +0 -35
@@ -1,493 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, onMounted, watch } from 'vue'
3
- import { useI18n } from '#dms/frontend-module'
4
- import { MS_PER_DAY } from '../constants'
5
- import { useChildId } from '../composables/useChildId'
6
- import { useLatestRequest } from '../composables/useLatestRequest'
7
- import {
8
- useMarketingApi,
9
- type MarketingOverview,
10
- } from '../composables/useMarketingApi'
11
- import {
12
- DEFAULT_PERIOD_PRESET,
13
- MARKETING_PERIOD_PRESETS,
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>