@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
@@ -0,0 +1,317 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useI18n } from '#dms/frontend-module'
4
+ import { useContextLink } from '../composables/useContextLink'
5
+ import { displayName, formatNumber, percentChangeTrend } from '../utils/format'
6
+ import { fractionDigits } from '../constants'
7
+
8
+ /**
9
+ * One overview card holding sibling top lists behind tabs (top pages, entry,
10
+ * exit…), read in one request that follows the period scope. Each row shows
11
+ * its rank, a bar of its share, the count, the share and, with a comparison,
12
+ * its change; rows lead to the surface that owns them, in the page's website
13
+ * and period.
14
+ */
15
+ type LabelKind = 'raw' | 'channel' | 'country' | 'language' | 'device'
16
+
17
+ interface TopRow {
18
+ id: string
19
+ title: string
20
+ value: number
21
+ share: number
22
+ /** Percent change; null without a value to compare, absent without a comparison. */
23
+ delta?: number | null
24
+ to?: string
25
+ tag?: string
26
+ }
27
+
28
+ interface TopTab {
29
+ id: string
30
+ label: string
31
+ labelKind: LabelKind
32
+ total: number
33
+ items: TopRow[]
34
+ }
35
+
36
+ interface TopTabsResponse {
37
+ tabs: TopTab[]
38
+ }
39
+
40
+ const props = withDefaults(
41
+ defineProps<{
42
+ fetchUrl: string
43
+ periodScope?: string
44
+ eyebrow: string
45
+ valueLabel: string
46
+ note?: string
47
+ footerLabel?: string
48
+ footerTo?: string
49
+ componentId?: string
50
+ pageId?: string
51
+ }>(),
52
+ {
53
+ periodScope: undefined,
54
+ note: undefined,
55
+ footerLabel: undefined,
56
+ footerTo: undefined,
57
+ componentId: undefined,
58
+ pageId: undefined,
59
+ },
60
+ )
61
+
62
+ const HIGHLIGHTED_RANKS = 3
63
+ const SKELETON_ROWS = 6
64
+ const RANK_DIGITS = 2
65
+
66
+ const { t, te, locale } = useI18n()
67
+ const { processI18n } = useTranslation()
68
+ const inContext = useContextLink()
69
+
70
+ const { data, isLoading, error, refresh } = useChartFetch<TopTabsResponse>({
71
+ fetchUrl: props.fetchUrl,
72
+ periodScope: props.periodScope,
73
+ })
74
+
75
+ const tabs = computed(() => data.value?.tabs ?? [])
76
+ const activeId = ref<string>('')
77
+
78
+ watch(
79
+ tabs,
80
+ (list) => {
81
+ if (!list.some((tab) => tab.id === activeId.value)) {
82
+ activeId.value = list[0]?.id ?? ''
83
+ }
84
+ },
85
+ { immediate: true },
86
+ )
87
+
88
+ const active = computed(
89
+ () => tabs.value.find((tab) => tab.id === activeId.value) ?? null,
90
+ )
91
+ const maxShare = computed(() =>
92
+ Math.max(1, ...(active.value?.items.map((item) => item.share) ?? [])),
93
+ )
94
+ /** The period is compared with another: each row says how it moved. */
95
+ const compared = computed(
96
+ () => active.value?.items.some((item) => item.delta !== undefined) ?? false,
97
+ )
98
+
99
+ const LABELERS: Record<LabelKind, (key: string) => string> = {
100
+ raw: (key) => key,
101
+ channel: (key) => {
102
+ const i18nKey = `page.marketing.channels.${key}`
103
+ return te(i18nKey) ? t(i18nKey) : key
104
+ },
105
+ country: (key) => displayName(key, 'region', locale.value),
106
+ language: (key) => displayName(key, 'language', locale.value),
107
+ device: (key) => t(`page.marketing.devices.${key}`),
108
+ }
109
+
110
+ const MONO_KINDS = new Set<LabelKind>(['raw'])
111
+
112
+ const DELTA_SIGNS = { up: '+', down: '−', flat: '±' } as const
113
+ const DELTA_TONES = {
114
+ up: 'text-success',
115
+ down: 'text-error',
116
+ flat: 'text-dimmed',
117
+ } as const
118
+
119
+ function rank(index: number): string {
120
+ return String(index + 1).padStart(RANK_DIGITS, '0')
121
+ }
122
+
123
+ function deltaLabel(delta: number | null | undefined): string {
124
+ if (delta === null || delta === undefined) {
125
+ return '—'
126
+ }
127
+ return `${DELTA_SIGNS[percentChangeTrend(delta)]}${formatPercentage(Math.abs(delta) / 100, locale.value, fractionDigits(0))}`
128
+ }
129
+
130
+ function deltaClass(delta: number | null | undefined): string {
131
+ return delta === null || delta === undefined
132
+ ? 'text-dimmed'
133
+ : DELTA_TONES[percentChangeTrend(delta)]
134
+ }
135
+
136
+ // --- Tabs --------------------------------------------------------------------
137
+
138
+ const tabItems = computed(() =>
139
+ tabs.value.map((tab) => ({ label: processI18n(tab.label), value: tab.id })),
140
+ )
141
+
142
+ /**
143
+ * The card's own look over the stock tabs: no list border (the header draws
144
+ * it), and a short bar under the active tab instead of the sliding indicator.
145
+ */
146
+ const TABS_UI = {
147
+ root: 'gap-0',
148
+ list: 'gap-1 overflow-x-auto border-b-0 p-0 mb-0 [scrollbar-width:none]',
149
+ indicator: 'hidden',
150
+ trigger:
151
+ 'shrink-0 rounded-sm px-2 py-3 text-[13px] font-normal whitespace-nowrap data-[state=active]:font-semibold data-[state=active]:text-highlighted hover:data-[state=inactive]:not-disabled:text-highlighted focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary data-[state=active]:after:absolute data-[state=active]:after:inset-x-2 data-[state=active]:after:bottom-0 data-[state=active]:after:h-0.5 data-[state=active]:after:rounded-full data-[state=active]:after:bg-primary',
152
+ }
153
+ </script>
154
+
155
+ <template>
156
+ <section class="dms-card flex min-w-0 flex-col overflow-hidden p-0">
157
+ <header
158
+ class="flex flex-wrap items-center gap-x-4 border-b border-default px-4"
159
+ >
160
+ <DmsEyebrow :label="processI18n(eyebrow)" class="shrink-0 py-3" />
161
+ <!-- UTabs cannot name its tablist: the group around it carries the name. -->
162
+ <div
163
+ v-if="tabs.length > 1"
164
+ role="group"
165
+ :aria-label="processI18n(eyebrow)"
166
+ class="order-last -mx-1 min-w-0 basis-full sm:order-none sm:mx-0 sm:flex-1 sm:basis-auto"
167
+ >
168
+ <UTabs
169
+ v-model="activeId"
170
+ :items="tabItems"
171
+ :content="false"
172
+ variant="link"
173
+ :ui="TABS_UI"
174
+ class="w-full"
175
+ />
176
+ </div>
177
+ <DmsEyebrow
178
+ :label="processI18n(valueLabel)"
179
+ class="ms-auto shrink-0 py-3"
180
+ />
181
+ </header>
182
+
183
+ <div
184
+ class="flex-1"
185
+ :role="tabs.length > 1 ? 'tabpanel' : undefined"
186
+ :aria-label="
187
+ tabs.length > 1 && active ? processI18n(active.label) : undefined
188
+ "
189
+ >
190
+ <DmsEmptyState
191
+ v-if="error && !data"
192
+ variant="error"
193
+ size="sm"
194
+ :title="t('page.marketing.common.load_error')"
195
+ >
196
+ <template #actions>
197
+ <UButton
198
+ size="xs"
199
+ color="neutral"
200
+ variant="outline"
201
+ icon="i-ph-arrow-clockwise"
202
+ :label="t('page.marketing.common.retry')"
203
+ @click="refresh"
204
+ />
205
+ </template>
206
+ </DmsEmptyState>
207
+ <ul
208
+ v-else-if="isLoading && !data"
209
+ class="divide-y divide-default"
210
+ aria-hidden="true"
211
+ >
212
+ <li
213
+ v-for="index in SKELETON_ROWS"
214
+ :key="index"
215
+ class="flex items-center gap-3 px-4 py-3"
216
+ >
217
+ <USkeleton class="h-3 w-5" />
218
+ <USkeleton class="h-3 flex-1" />
219
+ <USkeleton class="h-3 w-12" />
220
+ </li>
221
+ </ul>
222
+ <p
223
+ v-else-if="!active || active.items.length === 0"
224
+ class="px-4 py-10 text-center text-sm text-muted"
225
+ >
226
+ {{ t('page.marketing.overview.groups.empty') }}
227
+ </p>
228
+ <ol
229
+ v-else
230
+ class="divide-y divide-default transition-opacity"
231
+ :class="isLoading ? 'opacity-60' : ''"
232
+ >
233
+ <li v-for="(item, index) in active.items" :key="item.id">
234
+ <component
235
+ :is="item.to ? 'DmsAutoLink' : 'div'"
236
+ :to="item.to ? inContext(item.to) : undefined"
237
+ class="grid items-center gap-x-3 px-4 py-2"
238
+ :class="[
239
+ compared
240
+ ? 'grid-cols-[1.5rem_minmax(0,1fr)_auto_3rem_3rem]'
241
+ : 'grid-cols-[1.5rem_minmax(0,1fr)_auto_3rem]',
242
+ item.to ? 'transition-colors hover:bg-elevated/60' : '',
243
+ ]"
244
+ >
245
+ <span
246
+ class="font-mono text-[11px]"
247
+ :class="
248
+ index < HIGHLIGHTED_RANKS
249
+ ? 'font-semibold text-primary'
250
+ : 'text-dimmed'
251
+ "
252
+ >
253
+ {{ rank(index) }}
254
+ </span>
255
+ <span class="min-w-0">
256
+ <span class="flex min-w-0 items-center gap-2">
257
+ <span
258
+ class="truncate text-[13px] text-highlighted"
259
+ :class="MONO_KINDS.has(active.labelKind) ? 'font-mono' : ''"
260
+ :title="LABELERS[active.labelKind](item.title)"
261
+ >
262
+ {{ LABELERS[active.labelKind](item.title) }}
263
+ </span>
264
+ <DmsStatusPill
265
+ v-if="item.tag"
266
+ tone="primary"
267
+ size="sm"
268
+ :mono="false"
269
+ icon="i-ph-flag-checkered"
270
+ :label="processI18n(item.tag)"
271
+ />
272
+ </span>
273
+ <span
274
+ class="mt-1 block h-0.5 rounded-full bg-primary/70"
275
+ :style="{ width: `${(item.share / maxShare) * 100}%` }"
276
+ />
277
+ </span>
278
+ <span class="font-mono text-[13px] text-highlighted tabular-nums">
279
+ {{ formatNumber(item.value, locale) }}
280
+ </span>
281
+ <span
282
+ class="text-right font-mono text-[11px] text-dimmed tabular-nums"
283
+ >
284
+ {{
285
+ formatPercentage(item.share / 100, locale, fractionDigits(1))
286
+ }}
287
+ </span>
288
+ <span
289
+ v-if="compared"
290
+ class="text-right font-mono text-[11px] tabular-nums"
291
+ :class="deltaClass(item.delta)"
292
+ >
293
+ {{ deltaLabel(item.delta) }}
294
+ </span>
295
+ </component>
296
+ </li>
297
+ </ol>
298
+ </div>
299
+
300
+ <footer
301
+ v-if="footerTo || note"
302
+ class="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 border-t border-default px-4 py-2.5 text-xs"
303
+ >
304
+ <DmsAutoLink
305
+ v-if="footerTo && footerLabel"
306
+ :to="inContext(footerTo)"
307
+ class="inline-flex items-center gap-1 font-medium whitespace-nowrap text-primary hover:underline"
308
+ >
309
+ {{ processI18n(footerLabel) }}
310
+ <UIcon name="i-ph-arrow-right" class="size-3.5" />
311
+ </DmsAutoLink>
312
+ <span v-if="note" class="min-w-0 text-dimmed">
313
+ {{ processI18n(note) }}
314
+ </span>
315
+ </footer>
316
+ </section>
317
+ </template>
@@ -1,31 +1,65 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
2
+ import { computed, onMounted, ref } from 'vue'
3
3
  import { useDmsRuntimeConfig, useI18n } from '#dms/frontend-module'
4
- import type { MarketingWebsite } from '../composables/useMarketingApi'
4
+ import { trackerSnippet, type SnippetFlavor } from '../utils/snippet'
5
+
6
+ /**
7
+ * The tracker tag of one website in its three shapes (plain HTML, Nuxt,
8
+ * Google Tag Manager), with a copy button.
9
+ */
10
+ const props = defineProps<{ websiteId: string }>()
11
+
12
+ const FLAVORS: SnippetFlavor[] = ['html', 'nuxt', 'gtm']
13
+ const FILE_LABELS: Record<SnippetFlavor, string> = {
14
+ html: 'index.html',
15
+ nuxt: 'nuxt.config.ts',
16
+ gtm: 'Tag Manager',
17
+ }
5
18
 
6
- const props = defineProps<{ website: MarketingWebsite }>()
7
19
  const { t } = useI18n()
8
20
  const config = useDmsRuntimeConfig()
9
21
 
10
- const backendOrigin = computed(() =>
11
- String((config.public as { dms?: { baseURL?: string } }).dms?.baseURL ?? ''),
12
- )
22
+ const flavor = ref<SnippetFlavor>('html')
13
23
 
14
- const CLOSING_TAG = '</' + 'script>'
24
+ // Read once mounted: the server renders with its internal backend URL, the
25
+ // browser with the public one — the tag must only ever show the latter.
26
+ const apiOrigin = ref('')
27
+ onMounted(() => {
28
+ apiOrigin.value = String(
29
+ (config.public as { dms?: { baseURL?: string } }).dms?.baseURL ?? '',
30
+ )
31
+ })
32
+
33
+ const code = computed(() =>
34
+ trackerSnippet(flavor.value, apiOrigin.value, props.websiteId),
35
+ )
15
36
 
16
- const snippet = computed(() =>
17
- `<script defer src="${backendOrigin.value}/api/marketing/tracker.js" data-website-id="${props.website._id}">${CLOSING_TAG}`,
37
+ const items = computed(() =>
38
+ FLAVORS.map((value) => ({
39
+ value,
40
+ label: t(`page.marketing.install.flavors.${value}`),
41
+ })),
18
42
  )
19
43
  </script>
20
44
 
21
45
  <template>
22
- <div class="space-y-4">
23
- <p class="text-sm text-muted">
24
- {{ t('page.marketing.websites.snippet.description', { name: website.name }) }}
25
- </p>
26
- <div class="flex items-start gap-2 rounded-lg bg-elevated p-3">
27
- <pre class="min-w-0 flex-1 overflow-x-auto text-xs"><code>{{ snippet }}</code></pre>
28
- <DmsCopyButton :value="snippet" />
46
+ <div class="flex flex-col gap-3">
47
+ <DmsSegmented
48
+ v-model="flavor"
49
+ :items="items"
50
+ size="sm"
51
+ class="self-start"
52
+ />
53
+ <div class="overflow-hidden rounded-lg border border-default">
54
+ <div
55
+ class="flex items-center justify-between border-b border-default px-3 py-1.5"
56
+ >
57
+ <DmsEyebrow :label="FILE_LABELS[flavor]" />
58
+ <DmsCopyButton :value="code" />
59
+ </div>
60
+ <pre
61
+ class="overflow-x-auto px-3 py-2.5 font-mono text-xs leading-relaxed text-toned"
62
+ >{{ code }}</pre>
29
63
  </div>
30
64
  </div>
31
65
  </template>
@@ -1,10 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, ref, watch } from 'vue'
3
3
  import { useI18n } from '#dms/frontend-module'
4
+ import { MAX_EXPERIMENT_KEY_LENGTH } from '../utils/funnel-draft'
4
5
  import {
5
6
  parseExperimentVariationsValue,
6
7
  type ExperimentVariationDraft,
7
8
  } from '../composables/useExperiment'
9
+ import { fractionDigits } from '../constants'
8
10
 
9
11
  /**
10
12
  * The arms of an experiment, inside DmsMarketingExperimentInput: a list of
@@ -25,7 +27,7 @@ const emit = defineEmits<{
25
27
  'update:modelValue': [value: ExperimentVariationDraft[]]
26
28
  }>()
27
29
 
28
- const { t } = useI18n()
30
+ const { t, locale } = useI18n()
29
31
 
30
32
  function emptyVariation(): ExperimentVariationDraft {
31
33
  return { key: '', weight: DEFAULT_WEIGHT }
@@ -36,7 +38,7 @@ function parseValue(
36
38
  ): ExperimentVariationDraft[] {
37
39
  // Copies, not references: the drafts are mutated by the inputs below and
38
40
  // must never write through to the prop value.
39
- const variations = parseExperimentVariationsValue(raw).map(variation => ({
41
+ const variations = parseExperimentVariationsValue(raw).map((variation) => ({
40
42
  ...variation,
41
43
  }))
42
44
  while (variations.length < MIN_VARIATIONS) {
@@ -57,8 +59,9 @@ const totalWeight = computed(() =>
57
59
  )
58
60
 
59
61
  function sharePercent(variation: ExperimentVariationDraft): number {
62
+ // No weight above 0 serves nobody: the save refuses it, the bars say so.
60
63
  if (totalWeight.value <= 0) {
61
- return Math.round(100 / variations.value.length)
64
+ return 0
62
65
  }
63
66
  return Math.round(
64
67
  (Math.max(variation.weight || 0, 0) / totalWeight.value) * 100,
@@ -70,7 +73,7 @@ function sharePercent(variation: ExperimentVariationDraft): number {
70
73
  function onEdit(): void {
71
74
  emit(
72
75
  'update:modelValue',
73
- variations.value.map(variation => ({
76
+ variations.value.map((variation) => ({
74
77
  key: variation.key,
75
78
  weight: variation.weight,
76
79
  })),
@@ -123,6 +126,7 @@ watch(
123
126
  <UInput
124
127
  v-model="variation.key"
125
128
  :placeholder="t('page.marketing.funnels.form.variation_key')"
129
+ :maxlength="MAX_EXPERIMENT_KEY_LENGTH"
126
130
  :disabled="props.disabled"
127
131
  class="flex-1 font-mono"
128
132
  size="sm"
@@ -138,7 +142,13 @@ watch(
138
142
  @update:model-value="onEdit"
139
143
  />
140
144
  <span class="w-10 shrink-0 text-right text-xs text-muted">
141
- {{ sharePercent(variation) }}%
145
+ {{
146
+ formatPercentage(
147
+ sharePercent(variation) / 100,
148
+ locale,
149
+ fractionDigits(0),
150
+ )
151
+ }}
142
152
  </span>
143
153
  <UButton
144
154
  v-if="!props.disabled"