@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,330 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
- import { useDmsRoute, useI18n } from '#dms/frontend-module'
4
- import { useChildId } from '../composables/useChildId'
5
- import { useLatestRequest } from '../composables/useLatestRequest'
6
- import {
7
- useMarketingApi,
8
- type MarketingCampaignEntry,
9
- type MarketingCampaigns,
10
- type MarketingChannel,
11
- } from '../composables/useMarketingApi'
12
- import {
13
- MARKETING_PERIOD_PRESETS,
14
- ndToPreset,
15
- useMarketingPeriod,
16
- } from '../composables/useMarketingPeriod'
17
- import { useMarketingWebsites } from '../composables/useMarketingWebsites'
18
-
19
- import type { TableColumn } from '@nuxt/ui'
20
-
21
- /**
22
- * Acquisition surface: the five-channel split of sessions (donut) next to the
23
- * UTM campaign table — one row per (source, medium, campaign) triple observed
24
- * on new sessions — with UTM terms and contents as top lists below.
25
- *
26
- * Everything reads the daily rollups through one campaigns fetch; the search
27
- * box filters the table server-side because a day's map caps its entries and
28
- * the merged table can exceed what one response carries.
29
- */
30
- const PERIOD_SCOPE = 'dms-marketing-campaigns'
31
- const SEARCH_DEBOUNCE_MS = 300
32
-
33
- // The DMS page renderer hands every component its identity; keeping it is what
34
- // lets dms-ui primitives key their events and persisted state per child.
35
- const props = withDefaults(
36
- defineProps<{ componentId?: string, pageId?: string }>(),
37
- { componentId: 'marketing-campaigns', pageId: 'marketing' },
38
- )
39
-
40
- const childId = useChildId(() => props.componentId)
41
-
42
- const api = useMarketingApi()
43
- const { t, te } = useI18n()
44
- const route = useDmsRoute()
45
-
46
- const queryParam = (key: string): string => {
47
- const value = route.query[key]
48
- return typeof value === 'string' ? value : ''
49
- }
50
-
51
- // Read once: the selector seeds its preset at init and never reactively, so a
52
- // deep link's window has to be resolved before it renders.
53
- const initialPreset = ndToPreset(queryParam('period'))
54
-
55
- const {
56
- websites,
57
- items: websiteItems,
58
- selectedId,
59
- loading: loadingWebsites,
60
- failed: websitesFailed,
61
- load: loadWebsites,
62
- create: createWebsite,
63
- } = useMarketingWebsites(queryParam('website'))
64
-
65
- const period = useMarketingPeriod(PERIOD_SCOPE)
66
- const search = ref('')
67
-
68
- let searchTimer: ReturnType<typeof setTimeout> | null = null
69
-
70
- const {
71
- data: acquisition,
72
- loading: loadingCampaigns,
73
- failed: campaignsFailed,
74
- settled: campaignsSettled,
75
- run: loadCampaigns,
76
- } = useLatestRequest<MarketingCampaigns>(() =>
77
- // Server-side there is no session to authenticate with: the fetch belongs to
78
- // the client, like every other read here.
79
- selectedId.value && !import.meta.env.SSR
80
- ? api.getCampaigns(selectedId.value, period.value, search.value.trim())
81
- : null,
82
- )
83
-
84
- // The watcher owns every fetch, including the first: seeding it from onMounted
85
- // as well would issue the initial request twice on a bare link, and not at all
86
- // on a link that already names the site the list would have picked — resolving
87
- // it writes back the same id, which is no change to watch.
88
- watch([selectedId, period], () => {
89
- void loadCampaigns()
90
- }, { immediate: true })
91
-
92
- watch(search, () => {
93
- if (searchTimer) {
94
- clearTimeout(searchTimer)
95
- }
96
- searchTimer = setTimeout(() => void loadCampaigns(), SEARCH_DEBOUNCE_MS)
97
- })
98
-
99
- onBeforeUnmount(() => {
100
- if (searchTimer) {
101
- clearTimeout(searchTimer)
102
- }
103
- })
104
-
105
- const loading = computed(
106
- () =>
107
- loadingWebsites.value
108
- || loadingCampaigns.value
109
- || (!!selectedId.value && !campaignsSettled.value),
110
- )
111
-
112
- // Two failures, two flags: retrying the campaigns fetch must not clear the
113
- // one that says the site list itself never loaded.
114
- const failed = computed(() => websitesFailed.value || campaignsFailed.value)
115
-
116
- /** Retry entry point: whichever of the two fetches has nothing to show. */
117
- function reload() {
118
- return websites.value.length === 0 ? loadWebsites() : loadCampaigns()
119
- }
120
-
121
- onMounted(loadWebsites)
122
-
123
- // --- Channels ---------------------------------------------------------------
124
-
125
- /** Display order mirrors the backend's MARKETING_CHANNELS. */
126
- const CHANNEL_ORDER: MarketingChannel[] = ['direct', 'organic', 'social', 'referral', 'paid']
127
-
128
- /** Known channels localize; a key minted by a future classifier still shows. */
129
- function channelLabel(channel: string): string {
130
- const key = `page.marketing.campaigns.channels.${channel}`
131
- return te(key) ? t(key) : channel
132
- }
133
-
134
- const channelRecords = computed(() => {
135
- const channels = acquisition.value?.channels ?? {}
136
- return CHANNEL_ORDER
137
- .map(channel => ({ label: channelLabel(channel), value: channels[channel] ?? 0 }))
138
- .filter(record => record.value > 0)
139
- })
140
-
141
- // --- Campaign table ---------------------------------------------------------
142
-
143
- const campaignRows = computed(() => acquisition.value?.campaigns ?? [])
144
- const truncated = computed(() => acquisition.value?.truncated ?? false)
145
-
146
- const columns = computed<TableColumn<MarketingCampaignEntry>[]>(() => [
147
- { accessorKey: 'source', header: t('page.marketing.campaigns.table.source') },
148
- { accessorKey: 'medium', header: t('page.marketing.campaigns.table.medium') },
149
- { accessorKey: 'campaign', header: t('page.marketing.campaigns.table.campaign') },
150
- {
151
- accessorKey: 'sessions',
152
- header: t('page.marketing.campaigns.table.sessions'),
153
- meta: { class: { th: 'text-right', td: 'text-right' } },
154
- },
155
- ])
156
-
157
- // --- Referrer / term / content top lists ------------------------------------
158
-
159
- const TOP_LIST_LIMIT = 8
160
-
161
- function toTopItems(record: Record<string, number> | undefined) {
162
- return Object.entries(record ?? {})
163
- .sort((a, b) => b[1] - a[1])
164
- .slice(0, TOP_LIST_LIMIT)
165
- .map(([label, value]) => ({ id: label, title: label, value }))
166
- }
167
-
168
- const topLists = computed(() => [
169
- // Referrer domains across all channels — the overview's acquisition card
170
- // links here for this detail.
171
- {
172
- id: 'referrers',
173
- title: t('page.marketing.campaigns.top.referrers'),
174
- items: toTopItems(acquisition.value?.referrers),
175
- },
176
- {
177
- id: 'terms',
178
- title: t('page.marketing.campaigns.top.terms'),
179
- items: toTopItems(acquisition.value?.terms),
180
- },
181
- {
182
- id: 'contents',
183
- title: t('page.marketing.campaigns.top.contents'),
184
- items: toTopItems(acquisition.value?.contents),
185
- },
186
- ])
187
- </script>
188
-
189
- <template>
190
- <div class="space-y-6 p-6">
191
- <div class="flex flex-wrap items-center justify-between gap-4">
192
- <div>
193
- <h1 class="text-xl font-semibold">{{ t('page.marketing.campaigns.title') }}</h1>
194
- <p class="text-sm text-muted">{{ t('page.marketing.campaigns.subtitle') }}</p>
195
- </div>
196
- <div class="flex items-center gap-3">
197
- <USelect
198
- v-if="websites.length > 1"
199
- v-model="selectedId"
200
- :items="websiteItems"
201
- class="w-56"
202
- />
203
- <DmsPeriodSelector
204
- :id="PERIOD_SCOPE"
205
- :component-id="childId('period')"
206
- :page-id="props.pageId"
207
- :default-preset="initialPreset"
208
- :presets="MARKETING_PERIOD_PRESETS"
209
- :comparisons="['none']"
210
- :show-range-label="false"
211
- />
212
- <UButton
213
- icon="i-ph-arrow-clockwise"
214
- color="neutral"
215
- variant="subtle"
216
- size="sm"
217
- :loading="loading"
218
- @click="loadCampaigns"
219
- />
220
- </div>
221
- </div>
222
-
223
- <div v-if="failed" class="flex flex-col items-start gap-3">
224
- <UAlert
225
- color="error"
226
- variant="subtle"
227
- icon="i-ph-warning-circle"
228
- :title="t('page.marketing.campaigns.error.title')"
229
- :description="t('page.marketing.campaigns.error.description')"
230
- />
231
- <UButton
232
- icon="i-ph-arrow-clockwise"
233
- :label="t('page.marketing.campaigns.retry')"
234
- color="neutral"
235
- variant="subtle"
236
- size="sm"
237
- @click="reload"
238
- />
239
- </div>
240
-
241
- <DmsMarketingNoWebsiteState
242
- v-else-if="!loading && websites.length === 0"
243
- icon="i-ph-megaphone"
244
- :title="t('page.marketing.campaigns.empty.title')"
245
- :description="t('page.marketing.campaigns.empty.description')"
246
- @create="createWebsite"
247
- />
248
-
249
- <!-- Only before the first answer: a refetch keeps the tables on screen
250
- and spins the refresh button instead. -->
251
- <USkeleton v-else-if="loading && !acquisition" class="h-96 w-full" />
252
-
253
- <template v-else>
254
- <div class="grid gap-4 lg:grid-cols-3">
255
- <UCard>
256
- <template #header>
257
- <div>
258
- <p class="font-medium">{{ t('page.marketing.campaigns.channels.title') }}</p>
259
- <p class="text-sm text-muted">{{ t('page.marketing.campaigns.channels.subtitle') }}</p>
260
- </div>
261
- </template>
262
- <DmsChart
263
- v-if="channelRecords.length > 0"
264
- :component-id="childId('channels')"
265
- :page-id="props.pageId"
266
- type="donut"
267
- :static-dataset="channelRecords"
268
- height="300px"
269
- :show-total="true"
270
- />
271
- <p v-else class="py-12 text-center text-sm text-muted">
272
- {{ t('page.marketing.campaigns.channels.empty') }}
273
- </p>
274
- </UCard>
275
-
276
- <UCard class="lg:col-span-2">
277
- <template #header>
278
- <div class="flex flex-wrap items-center justify-between gap-3">
279
- <div>
280
- <p class="font-medium">{{ t('page.marketing.campaigns.table.title') }}</p>
281
- <p class="text-sm text-muted">{{ t('page.marketing.campaigns.table.subtitle') }}</p>
282
- </div>
283
- <UInput
284
- v-model="search"
285
- icon="i-ph-magnifying-glass"
286
- :placeholder="t('page.marketing.campaigns.table.search_placeholder')"
287
- class="w-56"
288
- />
289
- </div>
290
- </template>
291
- <UTable :data="campaignRows" :columns="columns">
292
- <template #source-cell="{ row }">
293
- <span v-if="row.original.source">{{ row.original.source }}</span>
294
- <span v-else class="text-muted">—</span>
295
- </template>
296
- <template #medium-cell="{ row }">
297
- <span v-if="row.original.medium">{{ row.original.medium }}</span>
298
- <span v-else class="text-muted">—</span>
299
- </template>
300
- <template #campaign-cell="{ row }">
301
- <span v-if="row.original.campaign">{{ row.original.campaign }}</span>
302
- <span v-else class="text-muted">—</span>
303
- </template>
304
- <template #empty>
305
- <p class="py-8 text-center text-sm text-muted">
306
- {{ t('page.marketing.campaigns.table.empty') }}
307
- </p>
308
- </template>
309
- </UTable>
310
- <p v-if="truncated" class="mt-2 text-sm text-muted">
311
- {{ t('page.marketing.campaigns.table.truncated') }}
312
- </p>
313
- </UCard>
314
- </div>
315
-
316
- <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
317
- <DmsTopListCard
318
- v-for="list in topLists"
319
- :key="list.id"
320
- :component-id="childId(`top-${list.id}`)"
321
- :page-id="props.pageId"
322
- :title="list.title"
323
- :static-items="list.items"
324
- :show-delta="false"
325
- :empty-label="t('page.marketing.campaigns.top.empty')"
326
- />
327
- </div>
328
- </template>
329
- </div>
330
- </template>