@antelopejs/dms-marketing 0.3.2 → 0.4.0

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 (287) 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 -0
  36. package/dist/hooks/index.js.map +1 -1
  37. package/dist/index.js +18 -2
  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 +3 -0
  259. package/frontend-vue/tests/drafts.test.ts +3 -3
  260. package/frontend-vue/tests/format.test.ts +73 -0
  261. package/frontend-vue/tests/funnel-draft.test.ts +194 -0
  262. package/frontend-vue/tests/heatmap.test.ts +32 -0
  263. package/frontend-vue/tests/i18n.test.ts +87 -4
  264. package/frontend-vue/tests/latest-request.test.ts +6 -3
  265. package/frontend-vue/tests/locale-typography.test.ts +115 -0
  266. package/frontend-vue/tests/registration.test.ts +21 -11
  267. package/frontend-vue/tests/routes.test.ts +67 -7
  268. package/frontend-vue/tests/server-parity.test.ts +77 -0
  269. package/frontend-vue/tests/stubs/frontend-module.ts +18 -3
  270. package/package.json +9 -7
  271. package/static/snapshot.js +46 -16
  272. package/static/tracker.js +139 -48
  273. package/dist/pages/campaigns/page.js.map +0 -1
  274. package/frontend-vue/app/components/CampaignsView.vue +0 -330
  275. package/frontend-vue/app/components/FunnelDetail.vue +0 -1026
  276. package/frontend-vue/app/components/FunnelsView.vue +0 -493
  277. package/frontend-vue/app/components/NoWebsiteState.vue +0 -18
  278. package/frontend-vue/app/components/OverviewView.vue +0 -493
  279. package/frontend-vue/app/components/PagesView.vue +0 -391
  280. package/frontend-vue/app/components/TextKpiCard.vue +0 -28
  281. package/frontend-vue/app/components/TopListTabsCard.vue +0 -162
  282. package/frontend-vue/app/composables/useChildId.ts +0 -13
  283. package/frontend-vue/app/composables/useMarketingPeriod.ts +0 -65
  284. package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +0 -69
  285. package/frontend-vue/app/composables/useMarketingWebsites.ts +0 -80
  286. package/frontend-vue/app/utils/dmsComponent.ts +0 -18
  287. package/frontend-vue/tests/period.test.ts +0 -35
@@ -1,391 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
- import {
4
- useDmsRoute,
5
- useDmsRouter,
6
- useI18n,
7
- } from '#dms/frontend-module'
8
- import { useChildId } from '../composables/useChildId'
9
- import { useLatestRequest } from '../composables/useLatestRequest'
10
- import {
11
- useMarketingApi,
12
- type MarketingPages,
13
- } from '../composables/useMarketingApi'
14
- import {
15
- MARKETING_PERIOD_PRESETS,
16
- ndToPreset,
17
- useMarketingPeriod,
18
- } from '../composables/useMarketingPeriod'
19
- import { funnelsLink, pagesLink } from '../composables/useMarketingRoutes'
20
- import { useMarketingWebsites } from '../composables/useMarketingWebsites'
21
-
22
- /**
23
- * Tracked pages of a site: the inventory on the left, the clicks and scroll
24
- * depth of the selected page on the right.
25
- *
26
- * The surface is deliberately keyed on nothing but a website id and a bare
27
- * pathname the tracker itself reported — no route registry, no sitemap, no
28
- * build manifest, no framework convention. That is what makes it hold for any
29
- * tracked site, whatever it is built with.
30
- *
31
- * The search box is also a path field, and that is not a fallback: a day keeps
32
- * at most MAX_ENTRIES_PER_TOP_MAP paths and older days keep counters only, so
33
- * a page can be missing from the inventory while its clicks are perfectly
34
- * queryable — the backend matches the path exactly either way.
35
- */
36
- const PERIOD_SCOPE = 'dms-marketing-pages'
37
- const SEARCH_DEBOUNCE_MS = 300
38
-
39
- // Declared, not inherited as attributes: the DMS page renderer hands every
40
- // component its identity, and an undeclared prop lands on the root element.
41
- const props = withDefaults(
42
- defineProps<{ componentId?: string, pageId?: string }>(),
43
- { componentId: 'marketing-pages', pageId: 'marketing' },
44
- )
45
-
46
- const childId = useChildId(() => props.componentId)
47
-
48
- const api = useMarketingApi()
49
- const { t } = useI18n()
50
- const route = useDmsRoute()
51
- const router = useDmsRouter()
52
-
53
- const queryParam = (key: string): string => {
54
- const value = route.query[key]
55
- return typeof value === 'string' ? value : ''
56
- }
57
-
58
- // Read once: the selector seeds its preset at init and never reactively, so a
59
- // deep link's window has to be resolved before it renders.
60
- const initialPreset = ndToPreset(queryParam('period'))
61
-
62
- const {
63
- websites,
64
- items: websiteItems,
65
- selectedId: selectedWebsiteId,
66
- selected: selectedWebsite,
67
- loading: loadingWebsites,
68
- failed: websitesFailed,
69
- load: loadWebsites,
70
- create: createWebsite,
71
- } = useMarketingWebsites(queryParam('website'))
72
-
73
- // `undefined`, not null: this is what DmsMasterDetail's model is typed on.
74
- const selectedPath = ref<string | undefined>(queryParam('path') || undefined)
75
- const search = ref('')
76
-
77
- const period = useMarketingPeriod(PERIOD_SCOPE)
78
-
79
- let searchTimer: ReturnType<typeof setTimeout> | null = null
80
-
81
- const {
82
- data: inventory,
83
- loading: loadingPages,
84
- settled: pagesSettled,
85
- failed: pagesFailed,
86
- run: loadPages,
87
- } = useLatestRequest<MarketingPages>(() =>
88
- // Server-side there is no session to authenticate with: the fetch belongs to
89
- // the client, like every other read here.
90
- selectedWebsiteId.value && !import.meta.env.SSR
91
- ? api.listPages(selectedWebsiteId.value, period.value, search.value.trim())
92
- : null,
93
- )
94
-
95
- const pages = computed(() => inventory.value?.pages ?? [])
96
- const truncated = computed(() => inventory.value?.truncated ?? false)
97
- const sampleRate = computed(() => inventory.value?.heatmapSampleRate ?? 0)
98
- const trackerEnabled = computed(() => inventory.value?.trackerEnabled ?? true)
99
-
100
- // Two failures, two flags: the inventory fetch clears its own error on every
101
- // attempt, and must not clear the one that says the site list never loaded.
102
- const failed = computed(() => websitesFailed.value || pagesFailed.value)
103
-
104
- // A selected site means an inventory request is expected. Keep the surface
105
- // visibly loading through the first answer instead of rendering an empty
106
- // master/detail shell while websites, pages and the preview requests settle.
107
- const loading = computed(
108
- () =>
109
- loadingWebsites.value
110
- || loadingPages.value
111
- || (!!selectedWebsiteId.value && !pagesSettled.value),
112
- )
113
-
114
- // Land on something rather than on an empty pane, but never override a path
115
- // that was linked to or typed. Watching the answer rather than writing from
116
- // the fetch keeps superseded ones out: only the newest ever reaches here.
117
- watch(inventory, (data) => {
118
- if (!selectedPath.value) {
119
- selectedPath.value = data?.pages[0]?.path
120
- }
121
- })
122
-
123
- /**
124
- * Retry entry point. Both fetches, in order, because either can be the one
125
- * that failed: on a deep link the inventory runs even when the site list did
126
- * not, and reloading the list alone leaves the selection unchanged, so the
127
- * watcher never fires and the inventory is never retried.
128
- */
129
- async function reload() {
130
- if (websites.value.length === 0) {
131
- await loadWebsites()
132
- }
133
- await loadPages()
134
- }
135
-
136
- onMounted(loadWebsites)
137
-
138
- // The watcher owns every inventory fetch, including the first: seeding it from
139
- // onMounted as well would issue the initial request twice on a bare link, and
140
- // not at all on a link that already names the site the list would have picked.
141
- watch([selectedWebsiteId, period], ([websiteId], previous) => {
142
- // Switching site invalidates the path — the same URL on another site is
143
- // another page, and usually does not exist there. Only a real site-to-site
144
- // change may clear: the immediate run hands multi-source watchers an EMPTY
145
- // previous array (truthy — `previous &&` does not guard it), and the site
146
- // list resolving undefined → id is not a switch either. Both carry a linked
147
- // path that must survive.
148
- const previousWebsiteId = previous?.[0]
149
- if (previousWebsiteId !== undefined && websiteId !== previousWebsiteId) {
150
- selectedPath.value = undefined
151
- }
152
- void loadPages()
153
- }, { immediate: true })
154
-
155
- watch(search, () => {
156
- if (searchTimer) {
157
- clearTimeout(searchTimer)
158
- }
159
- searchTimer = setTimeout(() => void loadPages(), SEARCH_DEBOUNCE_MS)
160
- })
161
-
162
- onBeforeUnmount(() => {
163
- if (searchTimer) {
164
- clearTimeout(searchTimer)
165
- }
166
- })
167
-
168
- /** Enter inspects what was typed, whether or not the inventory lists it. */
169
- function inspectTyped(): void {
170
- const typed = search.value.trim()
171
- if (!typed.startsWith('/')) {
172
- return
173
- }
174
- selectedPath.value = typed
175
- }
176
-
177
- // State lives in the URL so a heatmap can be linked to. Query-only navigation
178
- // does not remount the page, so this is free.
179
- watch([selectedWebsiteId, selectedPath, period], () => {
180
- void router.replace(
181
- pagesLink({
182
- website: selectedWebsiteId.value,
183
- path: selectedPath.value,
184
- period: period.value,
185
- }),
186
- )
187
- })
188
-
189
- const pageviewsBySelected = computed(
190
- () => pages.value.find(page => page.path === selectedPath.value)?.pageviews,
191
- )
192
-
193
- const listItems = computed(() =>
194
- pages.value.map((page) => {
195
- // Sampled PAGE LOADS, not clicks: the tracker draws once per page load,
196
- // so this says whether a heatmap can exist at all — a click estimate here
197
- // would contradict the real count shown beside it in the detail pane.
198
- const sampled = Math.round(page.pageviews * sampleRate.value)
199
- // Two counts, two plural choices: one message could only ever inflect on
200
- // one of them, and the two rarely agree.
201
- return {
202
- value: page.path,
203
- label: page.path,
204
- sublabel: [
205
- t('page.marketing.pages.row_hint.views', { count: page.pageviews }, page.pageviews),
206
- t('page.marketing.pages.row_hint.sampled', { count: sampled }, sampled),
207
- ].join(' · '),
208
- }
209
- }),
210
- )
211
-
212
- // --- Site-wide reset --------------------------------------------------------
213
-
214
- const { confirm } = useConfirm()
215
- const toast = useToast()
216
- const resettingHeatmaps = ref(false)
217
-
218
- /**
219
- * The detail pane refetches on this bump: a site-wide reset changes none of
220
- * the keys it watches (site, path, period), so without it the pane would keep
221
- * showing clicks that no longer exist. The inventory needs no reload — its
222
- * pageview counts come from the rollup, which a click purge never touches.
223
- */
224
- const heatmapRefreshToken = ref(0)
225
-
226
- async function resetSiteHeatmaps() {
227
- const site = selectedWebsite.value
228
- if (!site) {
229
- return
230
- }
231
- const confirmed = await confirm({
232
- title: t('page.marketing.pages.reset.site_title'),
233
- description: t('page.marketing.pages.reset.site_description', { name: site.name }),
234
- confirmLabel: t('page.marketing.pages.reset.confirm'),
235
- confirmColor: 'error',
236
- })
237
- if (!confirmed) {
238
- return
239
- }
240
- resettingHeatmaps.value = true
241
- try {
242
- const { deleted } = await api.resetHeatmap(site._id)
243
- toast.add({
244
- color: 'success',
245
- title: t('page.marketing.pages.reset.done', { count: deleted }, deleted),
246
- })
247
- heatmapRefreshToken.value++
248
- }
249
- catch {
250
- toast.add({ color: 'error', title: t('page.marketing.pages.reset.error') })
251
- }
252
- finally {
253
- resettingHeatmaps.value = false
254
- }
255
- }
256
- </script>
257
-
258
- <template>
259
- <div class="space-y-6 p-6">
260
- <div class="flex flex-wrap items-center justify-between gap-4">
261
- <div>
262
- <h1 class="text-xl font-semibold">{{ t('page.marketing.pages.title') }}</h1>
263
- <p class="text-sm text-muted">{{ t('page.marketing.pages.subtitle') }}</p>
264
- </div>
265
- <div class="flex flex-wrap items-center gap-3">
266
- <USelect
267
- v-if="websites.length > 1"
268
- v-model="selectedWebsiteId"
269
- :items="websiteItems"
270
- class="w-56"
271
- />
272
- <UInput
273
- v-model="search"
274
- icon="i-ph-magnifying-glass"
275
- class="w-64"
276
- :placeholder="t('page.marketing.pages.search_placeholder')"
277
- @keyup.enter="inspectTyped"
278
- />
279
- <DmsPeriodSelector
280
- :id="PERIOD_SCOPE"
281
- :component-id="childId('period')"
282
- :page-id="props.pageId"
283
- :default-preset="initialPreset"
284
- :presets="MARKETING_PERIOD_PRESETS"
285
- :comparisons="['none']"
286
- :show-range-label="false"
287
- />
288
- <UButton
289
- v-if="selectedWebsiteId"
290
- icon="i-ph-funnel"
291
- color="neutral"
292
- variant="ghost"
293
- size="sm"
294
- :to="funnelsLink({ website: selectedWebsiteId })"
295
- :label="t('page.marketing.pages.to_funnels')"
296
- />
297
- <UButton
298
- v-if="selectedWebsiteId"
299
- icon="i-ph-eraser"
300
- color="neutral"
301
- variant="ghost"
302
- size="sm"
303
- :loading="resettingHeatmaps"
304
- :label="t('page.marketing.pages.reset.site_button')"
305
- @click="resetSiteHeatmaps"
306
- />
307
- </div>
308
- </div>
309
-
310
- <UAlert
311
- v-if="!trackerEnabled"
312
- icon="i-ph-plugs"
313
- color="warning"
314
- variant="subtle"
315
- :title="t('page.marketing.pages.tracking_off.title')"
316
- :description="t('page.marketing.pages.tracking_off.description')"
317
- />
318
-
319
- <div v-if="failed" class="flex flex-col items-start gap-3">
320
- <UAlert
321
- color="error"
322
- variant="subtle"
323
- icon="i-ph-warning-circle"
324
- :title="t('page.marketing.pages.error')"
325
- :description="t('page.marketing.pages.error_hint')"
326
- />
327
- <UButton
328
- icon="i-ph-arrow-clockwise"
329
- :label="t('page.marketing.pages.retry')"
330
- color="neutral"
331
- variant="subtle"
332
- size="sm"
333
- @click="reload"
334
- />
335
- </div>
336
-
337
- <USkeleton v-else-if="loadingWebsites" class="h-96 w-full" />
338
-
339
- <DmsMarketingNoWebsiteState
340
- v-else-if="websites.length === 0"
341
- icon="i-ph-globe"
342
- :title="t('page.marketing.pages.no_website.title')"
343
- :description="t('page.marketing.pages.no_website.description')"
344
- @create="createWebsite"
345
- />
346
-
347
- <template v-else>
348
- <USkeleton v-if="loading" class="h-96 w-full" />
349
-
350
- <UAlert
351
- v-else-if="truncated"
352
- icon="i-ph-list"
353
- color="neutral"
354
- variant="subtle"
355
- :title="t('page.marketing.pages.list_truncated')"
356
- :description="t('page.marketing.pages.list_truncated_hint')"
357
- />
358
-
359
- <!-- An empty inventory is not an empty surface: a typed path is
360
- inspectable whether or not the rollup ever listed it. -->
361
- <UAlert
362
- v-if="!loadingPages && pages.length === 0 && !selectedPath"
363
- icon="i-ph-cursor-click"
364
- :title="t('page.marketing.pages.no_pages.title')"
365
- :description="t('page.marketing.pages.no_pages.description')"
366
- />
367
-
368
- <DmsMasterDetail
369
- v-else-if="!loading"
370
- v-model="selectedPath"
371
- :items="listItems"
372
- :list-label="t('page.marketing.pages.inventory')"
373
- >
374
- <DmsMarketingPagePreview
375
- v-if="selectedPath"
376
- :website="selectedWebsite"
377
- :path="selectedPath"
378
- :period="period"
379
- :sample-rate="sampleRate"
380
- :tracker-enabled="trackerEnabled"
381
- :pageviews="pageviewsBySelected"
382
- :refresh-token="heatmapRefreshToken"
383
- @website-updated="loadWebsites"
384
- />
385
- <div v-else class="p-8 text-center text-sm text-muted">
386
- {{ t('page.marketing.pages.select_hint') }}
387
- </div>
388
- </DmsMasterDetail>
389
- </template>
390
- </div>
391
- </template>
@@ -1,28 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * The stat-variant DmsKpiCard look for a value that is not a plain number:
4
- * dms-ui's card only formats numeric values (number / currency / percent),
5
- * and a session duration reads as "1m 32s", which is none of them. Classes
6
- * are copied from KpiCard's stat variant so the card sits in the same row
7
- * without a seam.
8
- */
9
- defineProps<{ title: string, value: string, icon: string }>()
10
- </script>
11
-
12
- <template>
13
- <DmsCard :padded="false" class="p-4 sm:p-5">
14
- <div class="flex items-start justify-between gap-3">
15
- <div class="min-w-0 flex-1">
16
- <p class="text-dimmed truncate font-mono text-[10px] font-medium uppercase tracking-widest">
17
- {{ title }}
18
- </p>
19
- <p class="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
20
- {{ value }}
21
- </p>
22
- </div>
23
- <div class="text-dimmed flex shrink-0 items-center justify-center">
24
- <UIcon :name="icon" class="size-4" aria-hidden="true" />
25
- </div>
26
- </div>
27
- </DmsCard>
28
- </template>
@@ -1,162 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, ref, watch } from 'vue'
3
- import {
4
- useDmsRoute,
5
- useDmsRouter,
6
- useI18n,
7
- } from '#dms/frontend-module'
8
-
9
- interface TopListTabItem {
10
- id: string
11
- title: string
12
- value: number
13
- to?: { path: string, query?: Record<string, string> }
14
- }
15
-
16
- interface TopListTab {
17
- id: string
18
- label: string
19
- items: TopListTabItem[]
20
- }
21
-
22
- /**
23
- * One overview card holding sibling top lists behind pill tabs. The rows
24
- * mirror dms-ui's DmsTopListCard rendering, re-done locally because that
25
- * card carries its own DmsCard chrome and cannot nest inside another card.
26
- * A single tab renders as a plain list: no pill bar.
27
- */
28
- const props = defineProps<{
29
- title: string
30
- tabs: TopListTab[]
31
- emptyLabel: string
32
- /** URL query key persisting the active tab; omit for free state. */
33
- stateKey?: string
34
- footerLabel?: string
35
- footerTo?: { path: string, query?: Record<string, string> }
36
- }>()
37
-
38
- const route = useDmsRoute()
39
- const router = useDmsRouter()
40
- const { locale } = useI18n()
41
-
42
- const HIGHLIGHTED_RANKS = 3
43
-
44
- const tabItems = computed(() =>
45
- props.tabs.map(tab => ({ label: tab.label, value: tab.id })))
46
-
47
- function initialTab(): string {
48
- const wanted = props.stateKey ? route.query[props.stateKey] : undefined
49
- return typeof wanted === 'string' && props.tabs.some(tab => tab.id === wanted)
50
- ? wanted
51
- : (props.tabs[0]?.id ?? '')
52
- }
53
-
54
- const activeId = ref(initialTab())
55
-
56
- // Conditional tabs (countries follows geoipEnabled, false until the fetch
57
- // settles) can be missing at mount and appear later: adopt the tab the URL
58
- // still asks for once it exists. The activeId watcher keeps the query in
59
- // step with every manual pick, so re-reading it can never override one.
60
- // The fallback covers the reverse: the active tab disappearing.
61
- watch(() => props.tabs, (tabs) => {
62
- const wanted = props.stateKey ? route.query[props.stateKey] : undefined
63
- if (typeof wanted === 'string' && tabs.some(tab => tab.id === wanted)) {
64
- activeId.value = wanted
65
- return
66
- }
67
- if (!tabs.some(tab => tab.id === activeId.value)) {
68
- activeId.value = tabs[0]?.id ?? ''
69
- }
70
- })
71
-
72
- // Replace, not push: paging through tabs must not stack history entries.
73
- // The default tab keeps the URL bare so a copied link stays minimal.
74
- watch(activeId, (id) => {
75
- const key = props.stateKey
76
- if (!key) {
77
- return
78
- }
79
- const query = Object.fromEntries(
80
- Object.entries(route.query).filter(([name]) => name !== key),
81
- )
82
- if (id !== props.tabs[0]?.id) {
83
- query[key] = id
84
- }
85
- void router.replace({ query })
86
- })
87
-
88
- const activeTab = computed(() =>
89
- props.tabs.find(tab => tab.id === activeId.value) ?? props.tabs[0])
90
-
91
- function rankClass(index: number): string {
92
- return index < HIGHLIGHTED_RANKS ? 'font-semibold text-primary' : 'text-muted'
93
- }
94
-
95
- function formatCount(value: number): string {
96
- return value.toLocaleString(locale.value)
97
- }
98
- </script>
99
-
100
- <template>
101
- <!-- The grid stretches the cards of a row; the growing body soaks up the
102
- slack so every footer anchors to the bottom edge. -->
103
- <UCard class="flex flex-col" :ui="{ body: 'grow' }">
104
- <template #header>
105
- <div class="flex flex-wrap items-center justify-between gap-3">
106
- <p class="font-medium">{{ title }}</p>
107
- <UTabs
108
- v-if="tabs.length > 1"
109
- v-model="activeId"
110
- :items="tabItems"
111
- :content="false"
112
- size="xs"
113
- />
114
- </div>
115
- </template>
116
-
117
- <!-- min-h holds the height of a full list — 8 rows of 2.5rem plus 7
118
- divide-y pixels — so switching to a shorter tab never jumps. -->
119
- <ul
120
- v-if="activeTab && activeTab.items.length > 0"
121
- class="grid min-h-[calc(20rem+7px)] content-start gap-x-3 divide-y divide-default [grid-template-columns:1.75rem_minmax(0,1fr)_auto]"
122
- >
123
- <li
124
- v-for="(item, index) in activeTab.items"
125
- :key="item.id"
126
- class="relative col-[1/-1] grid grid-cols-subgrid items-center py-2.5 transition-colors hover:bg-elevated/60"
127
- :class="item.to ? 'cursor-pointer' : ''"
128
- >
129
- <DmsLink
130
- v-if="item.to"
131
- :to="item.to"
132
- class="absolute inset-0 z-10"
133
- :aria-label="item.title"
134
- />
135
- <span
136
- class="text-center font-mono text-xs tabular-nums"
137
- :class="rankClass(index)"
138
- >
139
- {{ String(index + 1).padStart(2, '0') }}
140
- </span>
141
- <p class="min-w-0 truncate text-sm font-medium">{{ item.title }}</p>
142
- <span class="justify-self-end whitespace-nowrap text-xs tabular-nums text-muted">
143
- {{ formatCount(item.value) }}
144
- </span>
145
- </li>
146
- </ul>
147
-
148
- <div v-else class="flex min-h-[calc(20rem+7px)] items-center justify-center">
149
- <p class="text-sm text-muted">{{ emptyLabel }}</p>
150
- </div>
151
-
152
- <template v-if="footerLabel && footerTo" #footer>
153
- <DmsLink
154
- :to="footerTo"
155
- class="inline-flex items-center gap-1 text-sm text-muted transition-colors hover:text-highlighted"
156
- >
157
- {{ footerLabel }}
158
- <UIcon name="i-ph-arrow-right" class="size-4" />
159
- </DmsLink>
160
- </template>
161
- </UCard>
162
- </template>
@@ -1,13 +0,0 @@
1
- import { toValue, type MaybeRefOrGetter } from 'vue'
2
-
3
- /**
4
- * Derives one stable id per child from the id the DMS page renderer hands the
5
- * component. dms-ui primitives key their events and their persisted state on
6
- * it, so two children sharing an id share that state; the `${parent}-child`
7
- * shape is the one DmsRecursiveComponent uses.
8
- */
9
- export function useChildId(
10
- componentId: MaybeRefOrGetter<string | undefined>,
11
- ): (suffix: string) => string {
12
- return (suffix: string) => `${toValue(componentId)}-${suffix}`
13
- }
@@ -1,65 +0,0 @@
1
- import { computed, type ComputedRef } from 'vue'
2
- import { MS_PER_DAY } from '../constants'
3
-
4
- /**
5
- * Bridge between the DMS period selector and the marketing endpoints.
6
- *
7
- * Every marketing read takes an `Nd` window anchored on now, while the
8
- * selector publishes a from/to range into a scope registry. The conversion
9
- * belongs in one place, not least because the deep links between those views
10
- * carry the window as a query parameter and have to map it back onto a preset.
11
- */
12
-
13
- export const DEFAULT_PERIOD_DAYS = 7
14
- export const DEFAULT_PERIOD = `${DEFAULT_PERIOD_DAYS}d`
15
-
16
- /** Presets every marketing surface offers, in selector order. */
17
- export const MARKETING_PERIOD_PRESETS = [
18
- 'today',
19
- 'last-7-days',
20
- 'last-30-days',
21
- 'last-90-days',
22
- ]
23
-
24
- const PRESET_BY_DAYS: Record<number, string> = {
25
- 1: 'today',
26
- 7: 'last-7-days',
27
- 30: 'last-30-days',
28
- 90: 'last-90-days',
29
- }
30
-
31
- export const DEFAULT_PERIOD_PRESET = PRESET_BY_DAYS[DEFAULT_PERIOD_DAYS]
32
-
33
- /** Day count of an `Nd` string, or the default for anything unparseable. */
34
- export function periodDays(period: string | undefined): number {
35
- const days = Number.parseInt(period ?? '', 10)
36
- return Number.isFinite(days) && days > 0 ? days : DEFAULT_PERIOD_DAYS
37
- }
38
-
39
- /**
40
- * Preset matching an `Nd` window. Only exact matches map back — an arbitrary
41
- * window (a custom range someone linked from) falls back to the default rather
42
- * than silently rounding to a neighbouring preset.
43
- */
44
- export function ndToPreset(period: string | undefined): string {
45
- if (!period) {
46
- return DEFAULT_PERIOD_PRESET
47
- }
48
- return PRESET_BY_DAYS[periodDays(period)] ?? DEFAULT_PERIOD_PRESET
49
- }
50
-
51
- /** The `Nd` window currently published by a period-selector scope. */
52
- export function useMarketingPeriod(scope: string): ComputedRef<string> {
53
- const periodScope = usePeriodScope(scope)
54
-
55
- return computed(() => {
56
- const range = periodScope.value?.range
57
- if (!range) {
58
- return DEFAULT_PERIOD
59
- }
60
- const days = Math.round(
61
- (range.to.getTime() - range.from.getTime()) / MS_PER_DAY,
62
- )
63
- return `${Math.max(1, days)}d`
64
- })
65
- }