@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,753 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
+ import {
4
+ useDmsRoute,
5
+ useDmsRouter,
6
+ useDmsRuntimeConfig,
7
+ useI18n,
8
+ } from '#dms/frontend-module'
9
+ import {
10
+ useMarketingApi,
11
+ type MarketingConnectionStatus,
12
+ } from '../composables/useMarketingApi'
13
+ import {
14
+ installLink,
15
+ overviewLink,
16
+ websitesLink,
17
+ } from '../composables/useMarketingRoutes'
18
+ import {
19
+ allowHostConfirmation,
20
+ hostConflictOf,
21
+ } from '../utils/host-conflict'
22
+ import { trackerSnippet } from '../utils/snippet'
23
+
24
+ /**
25
+ * The guided install (`?website=` once the site exists): add the website,
26
+ * paste the tag, then a connection check that polls until a pageview arrives
27
+ * — or names a host the guard refused, with the fix. "Check again" waits for
28
+ * a pageview sent after it, to re-verify a site already live.
29
+ */
30
+ const props = withDefaults(
31
+ defineProps<{ componentId?: string; pageId?: string }>(),
32
+ {
33
+ componentId: undefined,
34
+ pageId: undefined,
35
+ },
36
+ )
37
+
38
+ const POLL_MS = 4000
39
+ const HTTP_NOT_FOUND = 404
40
+ const MAX_NAME_LENGTH = 100
41
+ const MAX_DOMAIN_LENGTH = 253
42
+
43
+ void props
44
+
45
+ const { t, locale } = useI18n()
46
+ const route = useDmsRoute()
47
+ const router = useDmsRouter()
48
+ const config = useDmsRuntimeConfig()
49
+ const api = useMarketingApi()
50
+ const toast = useToast()
51
+ const { confirm } = useConfirm()
52
+ // A field-tied error from the creation shows under its field, the rest as a toast.
53
+ const fieldErrors = useFieldErrors({
54
+ fields: { name: 'install-name', domain: 'install-domain' },
55
+ })
56
+
57
+ const websiteId = ref(
58
+ typeof route.query.website === 'string' ? route.query.website : '',
59
+ )
60
+ const status = ref<MarketingConnectionStatus | null>(null)
61
+ const failed = ref(false)
62
+ /** The `?website=` names no site of the workspace (deleted, mistyped). */
63
+ const missing = ref(false)
64
+ /** Server time "Check again" waits from; null: since the site's creation. */
65
+ const recheckSince = ref<number | null>(null)
66
+
67
+ // --- Step 1 ----------------------------------------------------------------------
68
+
69
+ const name = ref('')
70
+ const domain = ref('')
71
+ const creating = ref(false)
72
+
73
+ /** Empty required fields say so under themselves, the first one focused. */
74
+ async function requireFields(): Promise<boolean> {
75
+ const blank = (['domain', 'name'] as const).filter(
76
+ (field) => !{ name, domain }[field].value.trim(),
77
+ )
78
+ for (const field of blank) {
79
+ await fieldErrors.setError(field, t('dms.field_errors.required'))
80
+ }
81
+ return blank.length === 0
82
+ }
83
+
84
+ async function createWebsite(): Promise<void> {
85
+ fieldErrors.clear()
86
+ if (!(await requireFields())) {
87
+ return
88
+ }
89
+ creating.value = true
90
+ try {
91
+ const created = await api.createWebsite({
92
+ name: name.value.trim(),
93
+ domain: domain.value.trim(),
94
+ })
95
+ await api.selectContextWebsite(created._id)
96
+ websiteId.value = created._id
97
+ await router.replace(installLink(created._id))
98
+ await poll()
99
+ } catch (error) {
100
+ const conflict = hostConflictOf(error)
101
+ if (conflict) {
102
+ await fieldErrors.setError(
103
+ 'domain',
104
+ t('page.marketing.errors.duplicate_host_named', { ...conflict }),
105
+ )
106
+ return
107
+ }
108
+ await fieldErrors.handleApiError(error, {
109
+ toastTitle: 'page.marketing.install.create_error',
110
+ })
111
+ } finally {
112
+ creating.value = false
113
+ }
114
+ }
115
+
116
+ // --- Step 3: connection check ---------------------------------------------------
117
+
118
+ let timer: ReturnType<typeof setTimeout> | null = null
119
+
120
+ function stopPolling(): void {
121
+ if (timer) {
122
+ clearTimeout(timer)
123
+ timer = null
124
+ }
125
+ }
126
+
127
+ async function poll(): Promise<void> {
128
+ stopPolling()
129
+ if (!websiteId.value) {
130
+ return
131
+ }
132
+ try {
133
+ status.value = await api.getConnection(
134
+ websiteId.value,
135
+ recheckSince.value ?? undefined,
136
+ )
137
+ failed.value = false
138
+ } catch (error) {
139
+ if (apiErrorStatus(error) === HTTP_NOT_FOUND) {
140
+ missing.value = true
141
+ status.value = null
142
+ return
143
+ }
144
+ failed.value = true
145
+ }
146
+ if (!status.value?.visit) {
147
+ timer = setTimeout(() => void poll(), POLL_MS)
148
+ }
149
+ }
150
+
151
+ /** Waits for a pageview sent from now on, to re-verify an installed tag. */
152
+ async function checkAgain(): Promise<void> {
153
+ if (!status.value) {
154
+ return
155
+ }
156
+ recheckSince.value = status.value.checkedAt
157
+ status.value = { ...status.value, visit: null }
158
+ await poll()
159
+ }
160
+
161
+ const apiOrigin = ref('')
162
+ onMounted(() => {
163
+ // Read once mounted: the server renders with its internal backend URL.
164
+ apiOrigin.value = String(
165
+ (config.public as { dms?: { baseURL?: string } }).dms?.baseURL ?? '',
166
+ )
167
+ void poll()
168
+ })
169
+ onBeforeUnmount(stopPolling)
170
+
171
+ watch(
172
+ () => route.query.website,
173
+ (value) => {
174
+ const next = typeof value === 'string' ? value : ''
175
+ if (next !== websiteId.value) {
176
+ websiteId.value = next
177
+ status.value = null
178
+ missing.value = false
179
+ recheckSince.value = null
180
+ void poll()
181
+ }
182
+ },
183
+ )
184
+
185
+ const website = computed(() => status.value?.website ?? null)
186
+ /** The site named in the URL is still being loaded. */
187
+ const loading = computed(
188
+ () => !!websiteId.value && !website.value && !missing.value,
189
+ )
190
+ const visit = computed(() => status.value?.visit ?? null)
191
+ const allowing = ref<string | null>(null)
192
+
193
+ async function allow(hostname: string): Promise<void> {
194
+ if (!(await confirm(allowHostConfirmation(t, hostname)))) {
195
+ return
196
+ }
197
+ allowing.value = hostname
198
+ try {
199
+ await api.allowHost(websiteId.value, hostname)
200
+ toast.add({
201
+ color: 'success',
202
+ title: t('page.marketing.install.allowed', { host: hostname }),
203
+ })
204
+ await poll()
205
+ } catch (error) {
206
+ const conflict = hostConflictOf(error)
207
+ if (conflict) {
208
+ toast.add({
209
+ color: 'error',
210
+ title: t('page.marketing.install.allow_error'),
211
+ description: t('page.marketing.errors.duplicate_host_named', {
212
+ ...conflict,
213
+ }),
214
+ })
215
+ } else {
216
+ fieldErrors.toastError(error, {
217
+ toastTitle: 'page.marketing.install.allow_error',
218
+ })
219
+ }
220
+ } finally {
221
+ allowing.value = null
222
+ }
223
+ }
224
+
225
+ const resuming = ref(false)
226
+
227
+ async function resumeTracking(): Promise<void> {
228
+ resuming.value = true
229
+ try {
230
+ await api.updateWebsite(websiteId.value, { trackingEnabled: true })
231
+ toast.add({
232
+ color: 'success',
233
+ title: t('page.marketing.websites.resumed'),
234
+ })
235
+ await poll()
236
+ } catch (error) {
237
+ fieldErrors.toastError(error, {
238
+ toastTitle: 'page.marketing.websites.save_error',
239
+ })
240
+ } finally {
241
+ resuming.value = false
242
+ }
243
+ }
244
+
245
+ async function openOverview(): Promise<void> {
246
+ await api.selectContextWebsite(websiteId.value)
247
+ await router.push(overviewLink(websiteId.value))
248
+ }
249
+
250
+ /** "11 Oct, 14:02": the DMS date and time, without its year and seconds. */
251
+ const EVENT_TIME: Intl.DateTimeFormatOptions = {
252
+ year: undefined,
253
+ month: 'short',
254
+ day: 'numeric',
255
+ second: undefined,
256
+ }
257
+
258
+ function when(at: number): string {
259
+ return formatDateTime(at, locale.value, EVENT_TIME) ?? ''
260
+ }
261
+
262
+ const siteUrl = computed(() =>
263
+ website.value ? `https://${website.value.domain}` : '',
264
+ )
265
+
266
+ const developerNote = computed(() =>
267
+ website.value
268
+ ? t('page.marketing.install.developer_note', {
269
+ name: website.value.name,
270
+ domain: website.value.domain,
271
+ snippet: trackerSnippet('html', apiOrigin.value, website.value.id),
272
+ })
273
+ : '',
274
+ )
275
+
276
+ const COPIED_FOR_MS = 1600
277
+ const copied = ref(false)
278
+
279
+ async function copy(text: string): Promise<void> {
280
+ await navigator.clipboard?.writeText(text)
281
+ copied.value = true
282
+ setTimeout(() => (copied.value = false), COPIED_FOR_MS)
283
+ }
284
+
285
+ const RECORDS = ['pageviews', 'clicks', 'snapshots', 'privacy'] as const
286
+ const RECORD_ICONS: Record<(typeof RECORDS)[number], string> = {
287
+ pageviews: 'i-ph-eye',
288
+ clicks: 'i-ph-cursor-click',
289
+ snapshots: 'i-ph-camera',
290
+ privacy: 'i-ph-shield-check',
291
+ }
292
+
293
+ const stepTwoState = computed(() =>
294
+ visit.value ? 'done' : website.value ? 'current' : 'next',
295
+ )
296
+ </script>
297
+
298
+ <template>
299
+ <div class="flex flex-col gap-4">
300
+ <header class="flex flex-wrap items-start gap-4">
301
+ <UButton
302
+ icon="i-ph-arrow-left"
303
+ color="neutral"
304
+ variant="ghost"
305
+ :to="websitesLink()"
306
+ :aria-label="t('page.marketing.install.back')"
307
+ />
308
+ <div class="min-w-0 flex-1 basis-60">
309
+ <h1 class="text-2xl font-semibold break-words text-highlighted">
310
+ {{
311
+ website
312
+ ? t('page.marketing.install.title_for', { name: website.name })
313
+ : t('page.marketing.install.title')
314
+ }}
315
+ </h1>
316
+ <p class="mt-1 text-sm text-muted">
317
+ {{ t('page.marketing.install.subtitle') }}
318
+ </p>
319
+ </div>
320
+ <UButton
321
+ v-if="website"
322
+ :icon="copied ? 'i-ph-check' : 'i-ph-paper-plane-tilt'"
323
+ color="neutral"
324
+ variant="outline"
325
+ :label="t('page.marketing.install.copy_instructions')"
326
+ @click="copy(developerNote)"
327
+ />
328
+ </header>
329
+
330
+ <DmsEmptyState
331
+ v-if="missing"
332
+ class="dms-card"
333
+ icon="i-ph-globe-x"
334
+ :title="t('page.marketing.install.missing.title')"
335
+ :description="t('page.marketing.install.missing.description')"
336
+ >
337
+ <template #actions>
338
+ <UButton
339
+ size="sm"
340
+ color="neutral"
341
+ variant="outline"
342
+ icon="i-ph-arrow-left"
343
+ :label="t('page.marketing.install.back')"
344
+ :to="websitesLink()"
345
+ />
346
+ <UButton
347
+ size="sm"
348
+ icon="i-ph-plus"
349
+ :label="t('page.marketing.websites.create')"
350
+ :to="installLink()"
351
+ />
352
+ </template>
353
+ </DmsEmptyState>
354
+
355
+ <div
356
+ v-else
357
+ class="grid items-start gap-4 lg:grid-cols-[minmax(0,1fr)_22rem]"
358
+ >
359
+ <ol class="dms-card flex flex-col gap-0 p-5">
360
+ <li class="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-3 pb-6">
361
+ <span class="flex flex-col items-center">
362
+ <span
363
+ class="grid size-8 place-items-center rounded-full border font-mono text-xs"
364
+ :class="
365
+ website
366
+ ? 'border-primary/40 text-primary'
367
+ : 'border-primary bg-primary text-inverted'
368
+ "
369
+ >
370
+ <UIcon v-if="website" name="i-ph-check" class="size-4" />
371
+ <template v-else>1</template>
372
+ </span>
373
+ <span class="mt-2 w-px flex-1 bg-(--ui-border)" />
374
+ </span>
375
+ <div class="flex min-w-0 flex-col gap-2">
376
+ <h2 class="flex items-center gap-2 font-semibold text-highlighted">
377
+ {{ t('page.marketing.install.step1') }}
378
+ <DmsStatusPill
379
+ v-if="website"
380
+ tone="success"
381
+ size="sm"
382
+ :mono="false"
383
+ :label="t('page.marketing.install.done')"
384
+ />
385
+ </h2>
386
+ <USkeleton v-if="loading" class="h-5 w-64" />
387
+ <p v-else-if="website" class="text-sm break-words text-muted">
388
+ <span class="font-semibold text-highlighted">
389
+ {{ website.name }}
390
+ </span>
391
+ ·
392
+ <span class="font-mono">{{ website.domain }}</span>
393
+ {{ t('page.marketing.install.and_subdomains') }}
394
+ <DmsAutoLink
395
+ :to="websitesLink()"
396
+ class="text-primary hover:underline"
397
+ >
398
+ {{ t('page.marketing.common.edit') }}
399
+ </DmsAutoLink>
400
+ </p>
401
+ <form
402
+ v-else
403
+ class="grid gap-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] md:items-start"
404
+ novalidate
405
+ @submit.prevent="createWebsite"
406
+ >
407
+ <DmsFieldRow
408
+ layout="stack"
409
+ spacing="list"
410
+ :inset="false"
411
+ label="$page.marketing.websites.fields.name"
412
+ label-for="install-name"
413
+ required
414
+ >
415
+ <UInput
416
+ id="install-name"
417
+ v-model="name"
418
+ class="w-full"
419
+ placeholder="Acme shop"
420
+ :maxlength="MAX_NAME_LENGTH"
421
+ v-bind="fieldErrors.aria('name')"
422
+ @update:model-value="fieldErrors.clear('name')"
423
+ />
424
+ <DmsFieldError
425
+ :id="fieldErrors.errorId('name')"
426
+ :message="fieldErrors.errors.name"
427
+ />
428
+ </DmsFieldRow>
429
+ <DmsFieldRow
430
+ layout="stack"
431
+ spacing="list"
432
+ :inset="false"
433
+ label="$page.marketing.websites.fields.domain"
434
+ label-for="install-domain"
435
+ required
436
+ >
437
+ <UInput
438
+ id="install-domain"
439
+ v-model="domain"
440
+ class="w-full font-mono"
441
+ placeholder="shop.example.com"
442
+ :maxlength="MAX_DOMAIN_LENGTH"
443
+ v-bind="fieldErrors.aria('domain')"
444
+ @update:model-value="fieldErrors.clear('domain')"
445
+ />
446
+ <DmsFieldError
447
+ :id="fieldErrors.errorId('domain')"
448
+ :message="fieldErrors.errors.domain"
449
+ />
450
+ <!-- Under the control, so both inputs of the row line up. -->
451
+ <p v-if="!fieldErrors.errors.domain" class="text-xs text-muted">
452
+ {{ t('page.marketing.websites.fields.domain_hint') }}
453
+ </p>
454
+ </DmsFieldRow>
455
+ <UButton
456
+ type="submit"
457
+ icon="i-ph-plus"
458
+ class="md:mt-6"
459
+ :loading="creating"
460
+ :label="t('page.marketing.install.add')"
461
+ />
462
+ <DmsFormRequiredLegend class="md:col-span-3" />
463
+ </form>
464
+ </div>
465
+ </li>
466
+
467
+ <li class="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-3 pb-6">
468
+ <span class="flex flex-col items-center">
469
+ <span
470
+ class="grid size-8 place-items-center rounded-full border font-mono text-xs"
471
+ :class="
472
+ stepTwoState === 'done'
473
+ ? 'border-primary/40 text-primary'
474
+ : stepTwoState === 'current'
475
+ ? 'border-primary bg-primary text-inverted ring-4 ring-primary/20'
476
+ : 'border-default text-dimmed'
477
+ "
478
+ >
479
+ <UIcon
480
+ v-if="stepTwoState === 'done'"
481
+ name="i-ph-check"
482
+ class="size-4"
483
+ />
484
+ <template v-else>2</template>
485
+ </span>
486
+ <span class="mt-2 w-px flex-1 bg-(--ui-border)" />
487
+ </span>
488
+ <div class="flex min-w-0 flex-col gap-3">
489
+ <h2
490
+ class="font-semibold"
491
+ :class="website ? 'text-highlighted' : 'text-muted'"
492
+ >
493
+ {{ t('page.marketing.install.step2') }}
494
+ </h2>
495
+ <p class="text-sm text-muted">
496
+ {{ t('page.marketing.install.step2_description') }}
497
+ </p>
498
+ <DmsMarketingTrackerSnippet
499
+ v-if="website"
500
+ :website-id="website.id"
501
+ />
502
+ <UCollapsible
503
+ v-if="website"
504
+ class="rounded-lg border border-default"
505
+ >
506
+ <UButton
507
+ color="neutral"
508
+ variant="ghost"
509
+ block
510
+ class="justify-start"
511
+ icon="i-ph-sliders-horizontal"
512
+ trailing-icon="i-ph-caret-down"
513
+ >
514
+ <span class="font-medium">
515
+ {{ t('page.marketing.install.options') }}
516
+ </span>
517
+ <span class="text-xs text-dimmed">
518
+ {{ t('page.marketing.install.options_hint') }}
519
+ </span>
520
+ </UButton>
521
+ <template #content>
522
+ <dl
523
+ class="grid grid-cols-[12rem_minmax(0,1fr)] gap-x-4 gap-y-2 px-4 pb-4 text-sm"
524
+ >
525
+ <dt class="font-mono text-xs text-highlighted">
526
+ data-heatmap-sample
527
+ </dt>
528
+ <dd class="text-muted">
529
+ {{ t('page.marketing.install.option_sample') }}
530
+ </dd>
531
+ <dt class="font-mono text-xs text-highlighted">
532
+ data-do-not-track
533
+ </dt>
534
+ <dd class="text-muted">
535
+ {{ t('page.marketing.install.option_dnt') }}
536
+ </dd>
537
+ <dt class="font-mono text-xs text-highlighted">
538
+ data-host-url
539
+ </dt>
540
+ <dd class="text-muted">
541
+ {{ t('page.marketing.install.option_host') }}
542
+ </dd>
543
+ </dl>
544
+ </template>
545
+ </UCollapsible>
546
+ </div>
547
+ </li>
548
+
549
+ <li class="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-3">
550
+ <span class="flex flex-col items-center">
551
+ <span
552
+ class="grid size-8 place-items-center rounded-full border font-mono text-xs"
553
+ :class="
554
+ visit
555
+ ? 'border-success bg-success text-inverted'
556
+ : 'border-default text-dimmed'
557
+ "
558
+ >
559
+ <UIcon v-if="visit" name="i-ph-check" class="size-4" />
560
+ <template v-else>3</template>
561
+ </span>
562
+ </span>
563
+ <div class="flex min-w-0 flex-col gap-3">
564
+ <h2
565
+ class="font-semibold"
566
+ :class="website ? 'text-highlighted' : 'text-muted'"
567
+ >
568
+ {{ t('page.marketing.install.step3') }}
569
+ </h2>
570
+ <p class="text-sm text-muted">
571
+ {{ t('page.marketing.install.step3_description') }}
572
+ </p>
573
+ <template v-if="website && status">
574
+ <!-- The page's "collection paused" Banner block, when the module is paused. -->
575
+ <slot />
576
+ <DmsBanner
577
+ v-if="website.state === 'paused'"
578
+ tone="info"
579
+ icon="i-ph-pause-circle"
580
+ :title="t('page.marketing.context.state.paused')"
581
+ :description="t('page.marketing.websites.paused_description')"
582
+ >
583
+ <template #actions>
584
+ <UButton
585
+ size="sm"
586
+ color="neutral"
587
+ variant="outline"
588
+ icon="i-ph-play"
589
+ :loading="resuming"
590
+ :label="t('page.marketing.websites.resume')"
591
+ @click="resumeTracking"
592
+ />
593
+ </template>
594
+ </DmsBanner>
595
+ <div
596
+ v-if="visit"
597
+ class="flex flex-wrap items-center gap-4 rounded-lg border border-success/40 bg-success/10 p-4"
598
+ >
599
+ <DmsIconWell icon="i-ph-check" tone="success" />
600
+ <span class="min-w-0 flex-1">
601
+ <span class="block font-semibold text-highlighted">
602
+ {{
603
+ t('page.marketing.install.connected', {
604
+ when: when(visit.at),
605
+ })
606
+ }}
607
+ </span>
608
+ <span class="block font-mono text-xs break-all text-muted">
609
+ {{ visit.host ?? '' }}{{ visit.url }} ·
610
+ {{ visit.browser ?? '—' }} · {{ visit.deviceType }}
611
+ </span>
612
+ </span>
613
+ <span class="flex flex-wrap items-center gap-2">
614
+ <UButton
615
+ icon="i-ph-arrow-clockwise"
616
+ color="neutral"
617
+ variant="outline"
618
+ :label="t('page.marketing.install.check_again')"
619
+ @click="checkAgain"
620
+ />
621
+ <UButton
622
+ trailing-icon="i-ph-arrow-right"
623
+ :label="t('page.marketing.install.open_overview')"
624
+ @click="openOverview"
625
+ />
626
+ </span>
627
+ </div>
628
+ <div
629
+ v-else
630
+ class="flex flex-wrap items-center gap-4 rounded-lg border border-dashed border-primary/50 bg-primary/5 p-4"
631
+ >
632
+ <span
633
+ class="relative grid size-10 place-items-center rounded-full border border-primary/40 text-primary"
634
+ >
635
+ <UIcon name="i-ph-broadcast" class="size-5" />
636
+ <span
637
+ class="absolute inset-0 animate-ping rounded-full border border-primary/30"
638
+ />
639
+ </span>
640
+ <span class="min-w-0 flex-1">
641
+ <span class="block font-semibold break-words text-highlighted">
642
+ {{
643
+ t(
644
+ recheckSince === null
645
+ ? 'page.marketing.install.listening'
646
+ : 'page.marketing.install.listening_again',
647
+ { domain: website.domain },
648
+ )
649
+ }}
650
+ </span>
651
+ <span class="block text-xs text-muted">
652
+ {{
653
+ recheckSince === null
654
+ ? t('page.marketing.install.listening_since', {
655
+ when: when(status.createdAt),
656
+ })
657
+ : t('page.marketing.install.listening_again_since', {
658
+ when: when(recheckSince),
659
+ })
660
+ }}
661
+ </span>
662
+ </span>
663
+ <UButton
664
+ v-if="siteUrl"
665
+ icon="i-ph-arrow-square-out"
666
+ color="neutral"
667
+ variant="outline"
668
+ :to="siteUrl"
669
+ target="_blank"
670
+ :label="t('page.marketing.install.open_site')"
671
+ />
672
+ </div>
673
+ <div
674
+ v-for="refused in status.rejected"
675
+ :key="refused.hostname"
676
+ class="flex flex-wrap items-center gap-4 rounded-lg border border-error/40 bg-error/10 p-4"
677
+ >
678
+ <DmsIconWell icon="i-ph-prohibit" tone="error" />
679
+ <span class="min-w-0 flex-1">
680
+ <span class="block font-semibold break-words text-highlighted">
681
+ {{
682
+ t(
683
+ 'page.marketing.install.refused',
684
+ { count: refused.count, host: refused.hostname },
685
+ refused.count,
686
+ )
687
+ }}
688
+ </span>
689
+ <span class="block text-xs text-muted">
690
+ {{ t('page.marketing.install.refused_hint') }}
691
+ </span>
692
+ </span>
693
+ <UButton
694
+ color="neutral"
695
+ variant="outline"
696
+ :loading="allowing === refused.hostname"
697
+ :label="t('page.marketing.install.allow')"
698
+ @click="allow(refused.hostname)"
699
+ />
700
+ </div>
701
+ </template>
702
+ <p v-if="failed" class="text-xs text-error">
703
+ {{ t('page.marketing.install.poll_error') }}
704
+ </p>
705
+ </div>
706
+ </li>
707
+ </ol>
708
+
709
+ <aside class="flex flex-col gap-4">
710
+ <section class="dms-card p-0">
711
+ <header class="border-b border-default px-4 py-3">
712
+ <DmsEyebrow :label="t('page.marketing.install.records_title')" />
713
+ </header>
714
+ <ul class="divide-y divide-default">
715
+ <li
716
+ v-for="record in RECORDS"
717
+ :key="record"
718
+ class="flex gap-3 px-4 py-3"
719
+ >
720
+ <UIcon
721
+ :name="RECORD_ICONS[record]"
722
+ class="mt-0.5 size-4 shrink-0 text-primary"
723
+ />
724
+ <span>
725
+ <span class="block text-sm font-medium text-highlighted">
726
+ {{ t(`page.marketing.install.records.${record}`) }}
727
+ </span>
728
+ <span class="block text-xs text-muted">
729
+ {{ t(`page.marketing.install.records.${record}_hint`) }}
730
+ </span>
731
+ </span>
732
+ </li>
733
+ </ul>
734
+ </section>
735
+ <section class="dms-card p-0">
736
+ <header class="border-b border-default px-4 py-3">
737
+ <DmsEyebrow :label="t('page.marketing.install.live_title')" />
738
+ </header>
739
+ <div class="flex flex-col gap-2 p-4">
740
+ <p class="text-sm text-muted">
741
+ {{ t('page.marketing.install.live_description') }}
742
+ </p>
743
+ <code
744
+ class="rounded-lg border border-default px-3 py-2 font-mono text-xs text-toned"
745
+ >
746
+ dmsMarketing?.track("quote_requested")
747
+ </code>
748
+ </div>
749
+ </section>
750
+ </aside>
751
+ </div>
752
+ </div>
753
+ </template>