@antelopejs/dms-marketing 0.6.0 → 0.7.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 (181) hide show
  1. package/dist/config.d.ts +3 -0
  2. package/dist/config.js +11 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/data-api/websites.d.ts +10 -4
  5. package/dist/data-api/websites.js +79 -3
  6. package/dist/data-api/websites.js.map +1 -1
  7. package/dist/db/models/page_snapshots.model.d.ts +2 -0
  8. package/dist/db/models/page_snapshots.model.js +6 -0
  9. package/dist/db/models/page_snapshots.model.js.map +1 -1
  10. package/dist/db/models/websites.model.d.ts +1 -0
  11. package/dist/db/models/websites.model.js +9 -0
  12. package/dist/db/models/websites.model.js.map +1 -1
  13. package/dist/db/tables/page_snapshots.table.d.ts +1 -0
  14. package/dist/db/tables/page_snapshots.table.js +4 -0
  15. package/dist/db/tables/page_snapshots.table.js.map +1 -1
  16. package/dist/db/tables/websites.table.d.ts +2 -0
  17. package/dist/db/tables/websites.table.js +4 -0
  18. package/dist/db/tables/websites.table.js.map +1 -1
  19. package/dist/frontend-module.d.ts +6 -0
  20. package/dist/frontend-module.js +46 -0
  21. package/dist/frontend-module.js.map +1 -0
  22. package/dist/index.js +3 -11
  23. package/dist/index.js.map +1 -1
  24. package/dist/pages/acquisition/page.d.ts +1 -0
  25. package/dist/pages/acquisition/page.js +9 -4
  26. package/dist/pages/acquisition/page.js.map +1 -1
  27. package/dist/pages/blocks.d.ts +11 -2
  28. package/dist/pages/blocks.js +43 -5
  29. package/dist/pages/blocks.js.map +1 -1
  30. package/dist/pages/funnel/page.d.ts +1 -0
  31. package/dist/pages/funnel/page.js +1 -0
  32. package/dist/pages/funnel/page.js.map +1 -1
  33. package/dist/pages/funnels/page.d.ts +1 -0
  34. package/dist/pages/funnels/page.js +4 -2
  35. package/dist/pages/funnels/page.js.map +1 -1
  36. package/dist/pages/overview/page.d.ts +1 -0
  37. package/dist/pages/overview/page.js +5 -6
  38. package/dist/pages/overview/page.js.map +1 -1
  39. package/dist/pages/pages/page.d.ts +1 -0
  40. package/dist/pages/pages/page.js +1 -0
  41. package/dist/pages/pages/page.js.map +1 -1
  42. package/dist/pages/settings/form.js +1 -0
  43. package/dist/pages/settings/form.js.map +1 -1
  44. package/dist/pages/websites/page.d.ts +2 -3
  45. package/dist/pages/websites/page.js +63 -5
  46. package/dist/pages/websites/page.js.map +1 -1
  47. package/dist/routes/dashboard-tracking.js +5 -1
  48. package/dist/routes/dashboard-tracking.js.map +1 -1
  49. package/dist/routes/funnels.d.ts +2 -0
  50. package/dist/routes/funnels.js +6 -5
  51. package/dist/routes/funnels.js.map +1 -1
  52. package/dist/routes/public-endpoints.js +1 -1
  53. package/dist/routes/public-endpoints.js.map +1 -1
  54. package/dist/routes/snapshot.d.ts +1 -1
  55. package/dist/routes/snapshot.js +30 -13
  56. package/dist/routes/snapshot.js.map +1 -1
  57. package/dist/routes/stats.d.ts +10 -4
  58. package/dist/routes/stats.js +77 -23
  59. package/dist/routes/stats.js.map +1 -1
  60. package/dist/routes/websites.d.ts +5 -1
  61. package/dist/routes/websites.js +31 -13
  62. package/dist/routes/websites.js.map +1 -1
  63. package/dist/services/acquisition-report.d.ts +1 -0
  64. package/dist/services/acquisition-report.js +1 -0
  65. package/dist/services/acquisition-report.js.map +1 -1
  66. package/dist/services/collect.js +7 -2
  67. package/dist/services/collect.js.map +1 -1
  68. package/dist/services/dashboard-tracking.d.ts +7 -14
  69. package/dist/services/dashboard-tracking.js +42 -26
  70. package/dist/services/dashboard-tracking.js.map +1 -1
  71. package/dist/services/funnels-table.d.ts +3 -2
  72. package/dist/services/funnels-table.js +9 -9
  73. package/dist/services/funnels-table.js.map +1 -1
  74. package/dist/services/heatmap.d.ts +6 -0
  75. package/dist/services/heatmap.js +82 -17
  76. package/dist/services/heatmap.js.map +1 -1
  77. package/dist/services/index.d.ts +1 -0
  78. package/dist/services/index.js +1 -0
  79. package/dist/services/index.js.map +1 -1
  80. package/dist/services/prune.js +6 -1
  81. package/dist/services/prune.js.map +1 -1
  82. package/dist/services/realtime.d.ts +3 -0
  83. package/dist/services/realtime.js +49 -0
  84. package/dist/services/realtime.js.map +1 -0
  85. package/dist/services/request-body.d.ts +1 -0
  86. package/dist/services/request-body.js +18 -0
  87. package/dist/services/request-body.js.map +1 -1
  88. package/dist/services/sessionize.d.ts +1 -0
  89. package/dist/services/sessionize.js +9 -0
  90. package/dist/services/sessionize.js.map +1 -1
  91. package/dist/services/snapshots.d.ts +28 -1
  92. package/dist/services/snapshots.js +147 -26
  93. package/dist/services/snapshots.js.map +1 -1
  94. package/dist/services/tenant-website.d.ts +11 -0
  95. package/dist/services/tenant-website.js +27 -5
  96. package/dist/services/tenant-website.js.map +1 -1
  97. package/dist/services/website-report.d.ts +5 -0
  98. package/dist/services/website-report.js +13 -1
  99. package/dist/services/website-report.js.map +1 -1
  100. package/dist/services/workspace-scope.d.ts +31 -0
  101. package/dist/services/workspace-scope.js +72 -0
  102. package/dist/services/workspace-scope.js.map +1 -0
  103. package/dist/types/constants.d.ts +7 -0
  104. package/dist/types/constants.js +8 -1
  105. package/dist/types/constants.js.map +1 -1
  106. package/dist/types/events.d.ts +25 -25
  107. package/dist/types/snapshots.d.ts +29 -29
  108. package/dist/types/snapshots.js +1 -1
  109. package/dist/types/snapshots.js.map +1 -1
  110. package/dist/utils/funnel-steps-type.d.ts +13 -13
  111. package/frontend-vue/app/components/ChannelsCard.vue +19 -2
  112. package/frontend-vue/app/components/Context.vue +50 -376
  113. package/frontend-vue/app/components/FunnelBuilder.vue +28 -22
  114. package/frontend-vue/app/components/FunnelFigure.vue +15 -2
  115. package/frontend-vue/app/components/FunnelReport.vue +3 -0
  116. package/frontend-vue/app/components/FunnelTemplates.vue +15 -13
  117. package/frontend-vue/app/components/InstallGuide.vue +1 -1
  118. package/frontend-vue/app/components/PagePreview.vue +330 -74
  119. package/frontend-vue/app/components/PagesExplorer.vue +230 -85
  120. package/frontend-vue/app/components/TopListTabs.vue +51 -18
  121. package/frontend-vue/app/components/WebsiteCard.vue +394 -0
  122. package/frontend-vue/app/components/WebsiteCreate.vue +159 -0
  123. package/frontend-vue/app/components/WebsitePicker.vue +210 -0
  124. package/frontend-vue/app/components/WebsiteSettings.vue +539 -0
  125. package/frontend-vue/app/components/WorkspaceScope.vue +119 -0
  126. package/frontend-vue/app/composables/useContextWebsite.ts +105 -0
  127. package/frontend-vue/app/composables/useCopyDraft.ts +47 -0
  128. package/frontend-vue/app/composables/useMarketingApi.ts +92 -4
  129. package/frontend-vue/app/composables/useMarketingReads.ts +32 -23
  130. package/frontend-vue/app/composables/useMarketingRoutes.ts +25 -2
  131. package/frontend-vue/app/composables/useRecordCrumb.ts +17 -0
  132. package/frontend-vue/app/composables/useSnapshotFrame.ts +35 -1
  133. package/frontend-vue/app/utils/anchor-drift.ts +58 -0
  134. package/frontend-vue/app/utils/drift-dismissals.ts +20 -0
  135. package/frontend-vue/app/utils/fill-height.ts +141 -0
  136. package/frontend-vue/app/utils/format.ts +21 -2
  137. package/frontend-vue/app/utils/funnel-draft.ts +22 -0
  138. package/frontend-vue/app/utils/heatmap.ts +17 -0
  139. package/frontend-vue/app/utils/page-snapshot.ts +75 -0
  140. package/frontend-vue/app/utils/snapshot-options.ts +148 -0
  141. package/frontend-vue/app/utils/snapshotScrollRoot.ts +93 -0
  142. package/frontend-vue/app/utils/website-card.ts +93 -0
  143. package/frontend-vue/app/utils/website-purge.ts +95 -0
  144. package/frontend-vue/app/utils/website-settings.ts +121 -0
  145. package/frontend-vue/app/utils/website-state.ts +93 -0
  146. package/frontend-vue/app/utils/workspace-scope.ts +51 -0
  147. package/frontend-vue/i18n/locales/marketing-en-GB.json +112 -36
  148. package/frontend-vue/i18n/locales/marketing-fr-FR.json +112 -36
  149. package/frontend-vue/package.json +1 -1
  150. package/frontend-vue/tests/anchor-drift.test.ts +59 -0
  151. package/frontend-vue/tests/build-flags.test.ts +38 -0
  152. package/frontend-vue/tests/context-bar.test.ts +226 -0
  153. package/frontend-vue/tests/copy-draft.test.ts +64 -0
  154. package/frontend-vue/tests/drift-dismissals.test.ts +12 -0
  155. package/frontend-vue/tests/fill-height.test.ts +110 -0
  156. package/frontend-vue/tests/format.test.ts +14 -0
  157. package/frontend-vue/tests/funnel-draft.test.ts +37 -0
  158. package/frontend-vue/tests/heatmap.test.ts +25 -0
  159. package/frontend-vue/tests/install-guide.test.ts +25 -0
  160. package/frontend-vue/tests/marketing-reads.test.ts +20 -6
  161. package/frontend-vue/tests/page-snapshot.test.ts +62 -0
  162. package/frontend-vue/tests/record-crumb.test.ts +43 -0
  163. package/frontend-vue/tests/registration.test.ts +4 -1
  164. package/frontend-vue/tests/routes.test.ts +21 -0
  165. package/frontend-vue/tests/save-funnel.test.ts +47 -0
  166. package/frontend-vue/tests/server-parity.test.ts +3 -0
  167. package/frontend-vue/tests/snapshot-options.test.ts +148 -0
  168. package/frontend-vue/tests/snapshot-scroll-root.test.ts +84 -0
  169. package/frontend-vue/tests/stubs/frontend-module.ts +5 -1
  170. package/frontend-vue/tests/top-list-tabs.test.ts +158 -0
  171. package/frontend-vue/tests/website-card.test.ts +119 -0
  172. package/frontend-vue/tests/website-purge.test.ts +70 -0
  173. package/frontend-vue/tests/website-settings.test.ts +133 -0
  174. package/frontend-vue/tests/website-state.test.ts +96 -0
  175. package/frontend-vue/tests/workspace-scope-control.test.ts +113 -0
  176. package/frontend-vue/tests/workspace-scope.test.ts +55 -0
  177. package/package.json +5 -4
  178. package/skills/dms-marketing/SKILL.md +47 -6
  179. package/static/snapshot.js +539 -48
  180. package/static/tracker.js +154 -5
  181. package/frontend-vue/app/components/WebsitesGrid.vue +0 -812
@@ -1,812 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, ref } from 'vue'
3
- import { useDmsRouter, useI18n } from '#dms/frontend-module'
4
- import {
5
- useMarketingApi,
6
- type MarketingWebsiteCard,
7
- } from '../composables/useMarketingApi'
8
- import { installLink, overviewLink } from '../composables/useMarketingRoutes'
9
- import {
10
- useWebsiteCards,
11
- useWebsiteDeletionImpactRead,
12
- } from '../composables/useMarketingReads'
13
- import { useMarketingNow } from '../composables/useMarketingNow'
14
- import {
15
- formatNumber,
16
- formatRelativeTime,
17
- formatShortDate,
18
- initials,
19
- } from '../utils/format'
20
- import { allowHostConfirmation, hostConflictOf } from '../utils/host-conflict'
21
- import {
22
- MAX_WEBSITE_DOMAIN_LENGTH,
23
- MAX_WEBSITE_NAME_LENGTH,
24
- fractionDigits,
25
- } from '../constants'
26
-
27
- /**
28
- * The Websites page: one card per site with its state, volumes, accepted
29
- * hosts and capture options; a site still waiting for its first pageview
30
- * leads to the install guide. Hosts the guard refused show on any card, with
31
- * the fix. The pencil opens a drawer with the site's General, Capture and
32
- * Danger zone tabs.
33
- */
34
- const props = withDefaults(
35
- defineProps<{ fetchUrl?: string; componentId?: string; pageId?: string }>(),
36
- { fetchUrl: undefined, componentId: undefined, pageId: undefined },
37
- )
38
-
39
- type DrawerTab = 'general' | 'capture' | 'danger'
40
-
41
- const CLOCK_TICK_MS = 15_000
42
-
43
- const { t, locale } = useI18n()
44
- const router = useDmsRouter()
45
- const api = useMarketingApi()
46
- const toast = useToast()
47
- // A field-tied error from the save shows under its field, the rest as a toast.
48
- const fieldErrors = useFieldErrors({
49
- fields: {
50
- name: 'website-name',
51
- domain: 'website-domain',
52
- extraDomains: 'website-hosts',
53
- },
54
- })
55
- const { confirm } = useConfirm()
56
-
57
- void props
58
-
59
- const cardsRead = useWebsiteCards()
60
- const cards = cardsRead.data
61
- /** The cards could not be read; shown once a retry is over. */
62
- const failed = computed(
63
- () => !!cardsRead.error.value && cardsRead.status.value !== 'pending',
64
- )
65
-
66
- /** The cards again: a retry after a failed read. */
67
- const load = (): Promise<void> => cardsRead.refresh()
68
-
69
- /** The website a deletion is asked about. */
70
- const deletionTarget = ref('')
71
- const readDeletionImpact = useWebsiteDeletionImpactRead(deletionTarget)
72
-
73
- const now = useMarketingNow(CLOCK_TICK_MS)
74
- /** The browser's zone, which a server render writes the dates in. */
75
- const readerZone = readerTimeZone()
76
-
77
- function stateLine(card: MarketingWebsiteCard): string {
78
- if (
79
- (card.state === 'live' || card.state === 'imported') &&
80
- card.lastActivityAt
81
- ) {
82
- return t(`page.marketing.context.${card.state}`, {
83
- ago: formatRelativeTime(card.lastActivityAt, now.value, locale.value),
84
- })
85
- }
86
- return t(`page.marketing.context.state.${card.state}`)
87
- }
88
-
89
- const STATE_DOTS = {
90
- live: 'bg-success',
91
- imported: 'bg-info',
92
- waiting: 'bg-warning',
93
- paused: 'bg-(--ui-text-dimmed)',
94
- } as const
95
-
96
- async function openOverview(card: MarketingWebsiteCard): Promise<void> {
97
- await api.selectContextWebsite(card.id)
98
- await router.push(overviewLink(card.id))
99
- }
100
-
101
- // --- Card actions ----------------------------------------------------------------
102
-
103
- /** The card action running, as `<site id>:<what>`, for its spinner. */
104
- const busy = ref<string | null>(null)
105
-
106
- async function runCardAction(
107
- key: string,
108
- action: () => Promise<unknown>,
109
- successTitle: string,
110
- errorTitle: string,
111
- ): Promise<void> {
112
- busy.value = key
113
- try {
114
- await action()
115
- toast.add({ color: 'success', title: successTitle })
116
- // Their state and counts are the backend's: the page reads them again,
117
- // the card's spinner on until it shows them.
118
- await refreshPageBlocks()
119
- } catch (error) {
120
- const conflict = hostConflictOf(error)
121
- if (conflict) {
122
- toast.add({
123
- color: 'error',
124
- title: t(errorTitle),
125
- description: t('page.marketing.errors.duplicate_host_named', {
126
- ...conflict,
127
- }),
128
- })
129
- } else {
130
- fieldErrors.toastError(error, { toastTitle: errorTitle })
131
- }
132
- } finally {
133
- busy.value = null
134
- }
135
- }
136
-
137
- async function allowHost(card: MarketingWebsiteCard, hostname: string) {
138
- if (!(await confirm(allowHostConfirmation(t, hostname)))) {
139
- return
140
- }
141
- await runCardAction(
142
- `${card.id}:${hostname}`,
143
- () => api.allowHost(card.id, hostname),
144
- t('page.marketing.install.allowed', { host: hostname }),
145
- 'page.marketing.install.allow_error',
146
- )
147
- }
148
-
149
- function resume(card: MarketingWebsiteCard) {
150
- return runCardAction(
151
- `${card.id}:resume`,
152
- () => api.updateWebsite(card.id, { trackingEnabled: true }),
153
- t('page.marketing.websites.resumed'),
154
- 'page.marketing.websites.save_error',
155
- )
156
- }
157
-
158
- // --- Drawer --------------------------------------------------------------------
159
-
160
- const editing = ref<MarketingWebsiteCard | null>(null)
161
- const drawerOpen = ref(false)
162
- const tab = ref<DrawerTab>('general')
163
- const draftName = ref('')
164
- const draftDomain = ref('')
165
- const draftHosts = ref<string[]>([])
166
- const draftTracking = ref(true)
167
- const draftSnapshots = ref(false)
168
- const draftMask = ref(false)
169
- const saving = ref(false)
170
-
171
- function edit(
172
- card: MarketingWebsiteCard,
173
- initialTab: DrawerTab = 'general',
174
- ): void {
175
- fieldErrors.clear()
176
- editing.value = card
177
- tab.value = initialTab
178
- draftName.value = card.name
179
- draftDomain.value = card.domain
180
- draftHosts.value = [...card.extraDomains]
181
- draftTracking.value = card.state !== 'paused'
182
- draftSnapshots.value = card.snapshotsEnabled
183
- draftMask.value = card.snapshotMaskText
184
- drawerOpen.value = true
185
- }
186
-
187
- // The content stays while the drawer slides out; it goes once it is gone.
188
- function onDrawerClosed(): void {
189
- editing.value = null
190
- }
191
-
192
- const tabItems = computed(() => [
193
- { value: 'general', label: t('page.marketing.websites.drawer.general') },
194
- { value: 'capture', label: t('page.marketing.websites.drawer.capture') },
195
- { value: 'danger', label: t('page.marketing.websites.drawer.danger') },
196
- ])
197
-
198
- /** Turning snapshots off, or changing the mask, discards the captures. */
199
- const discardsSnapshots = computed(
200
- () =>
201
- editing.value !== null &&
202
- editing.value.snapshots > 0 &&
203
- (draftSnapshots.value !== editing.value.snapshotsEnabled ||
204
- draftMask.value !== editing.value.snapshotMaskText),
205
- )
206
-
207
- /** Empty required fields say so under themselves, on the General tab. */
208
- async function requireFields(): Promise<boolean> {
209
- const blank = (['domain', 'name'] as const).filter(
210
- (field) => !{ name: draftName, domain: draftDomain }[field].value.trim(),
211
- )
212
- if (blank.length > 0) {
213
- tab.value = 'general'
214
- }
215
- for (const field of blank) {
216
- await fieldErrors.setError(field, t('dms.field_errors.required'))
217
- }
218
- return blank.length === 0
219
- }
220
-
221
- async function save(): Promise<void> {
222
- const card = editing.value
223
- if (!card || !(await requireFields())) {
224
- return
225
- }
226
- saving.value = true
227
- try {
228
- await api.updateWebsite(card.id, {
229
- name: draftName.value.trim(),
230
- domain: draftDomain.value.trim(),
231
- extraDomains: draftHosts.value.map((host) => host.trim()).filter(Boolean),
232
- trackingEnabled: draftTracking.value,
233
- snapshotsEnabled: draftSnapshots.value,
234
- snapshotMaskText: draftMask.value,
235
- })
236
- toast.add({ color: 'success', title: t('page.marketing.websites.saved') })
237
- drawerOpen.value = false
238
- refreshPageBlocks()
239
- } catch (error) {
240
- // A refused field lives on the General tab: show it there.
241
- const conflict = hostConflictOf(error)
242
- if (conflict) {
243
- tab.value = 'general'
244
- await fieldErrors.setError(
245
- conflict.field === 'extraDomains' ? 'extraDomains' : 'domain',
246
- t('page.marketing.errors.duplicate_host_named', { ...conflict }),
247
- )
248
- return
249
- }
250
- if (await fieldErrors.applyApiError(error)) {
251
- tab.value = 'general'
252
- return
253
- }
254
- fieldErrors.toastError(error, {
255
- toastTitle: 'page.marketing.websites.save_error',
256
- })
257
- } finally {
258
- saving.value = false
259
- }
260
- }
261
-
262
- async function remove(card: MarketingWebsiteCard): Promise<void> {
263
- let impact
264
- try {
265
- deletionTarget.value = card.id
266
- impact = await readDeletionImpact()
267
- } catch (error) {
268
- fieldErrors.toastError(error, {
269
- toastTitle: 'page.marketing.websites.delete.impact_error',
270
- })
271
- return
272
- }
273
- // Every stored day goes, so the counts read "since the oldest one".
274
- const since =
275
- impact.since === null ? null : formatShortDate(impact.since, locale.value)
276
- const sinceLabel = (key: 'sessions' | 'pageviews') =>
277
- since === null
278
- ? t(`page.marketing.websites.delete.${key}`)
279
- : t(`page.marketing.websites.delete.${key}_since`, { date: since })
280
- await confirm({
281
- title: t('page.marketing.websites.delete.title', { name: card.name }),
282
- description: t('page.marketing.websites.delete.description'),
283
- color: 'error',
284
- icon: 'i-ph-trash',
285
- confirmLabel: t('page.marketing.websites.delete.confirm'),
286
- // A hostname is not case-sensitive: neither is the typed check.
287
- confirmText: card.domain,
288
- confirmTextCaseInsensitive: true,
289
- impact: [
290
- {
291
- icon: 'i-ph-users',
292
- label: sinceLabel('sessions'),
293
- count: formatNumber(impact.sessions, locale.value),
294
- },
295
- {
296
- icon: 'i-ph-eye',
297
- label: sinceLabel('pageviews'),
298
- count: formatNumber(impact.pageviews, locale.value),
299
- },
300
- {
301
- icon: 'i-ph-funnel',
302
- label: t('page.marketing.websites.delete.funnels'),
303
- count: impact.funnels,
304
- },
305
- {
306
- icon: 'i-ph-camera',
307
- label: t('page.marketing.websites.delete.snapshots'),
308
- count: impact.snapshots,
309
- },
310
- ],
311
- onConfirm: async () => {
312
- await api.deleteWebsite(card.id)
313
- toast.add({
314
- color: 'success',
315
- title: t('page.marketing.websites.delete.done', { name: card.name }),
316
- })
317
- drawerOpen.value = false
318
- // Its card goes; the others are unchanged, so nothing is read again.
319
- cards.value = cards.value?.filter((other) => other.id !== card.id) ?? null
320
- },
321
- })
322
- }
323
- </script>
324
-
325
- <template>
326
- <div class="flex flex-col gap-4">
327
- <DmsEmptyState
328
- v-if="failed"
329
- variant="error"
330
- class="dms-card"
331
- :title="t('page.marketing.websites.error')"
332
- >
333
- <template #actions>
334
- <UButton
335
- size="sm"
336
- color="neutral"
337
- variant="outline"
338
- icon="i-ph-arrow-clockwise"
339
- :label="t('page.marketing.common.retry')"
340
- @click="load"
341
- />
342
- </template>
343
- </DmsEmptyState>
344
-
345
- <div v-else-if="!cards" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
346
- <USkeleton v-for="index in 3" :key="index" class="h-72 rounded-xl" />
347
- </div>
348
-
349
- <DmsMarketingFirstRun v-else-if="cards.length === 0" />
350
-
351
- <template v-else>
352
- <!-- The page's "collection paused" Banner block, when the module is paused. -->
353
- <slot />
354
-
355
- <div class="grid items-start gap-4 md:grid-cols-2 xl:grid-cols-3">
356
- <article
357
- v-for="card in cards"
358
- :key="card.id"
359
- class="flex flex-col overflow-hidden rounded-xl border bg-(--ui-bg)"
360
- :class="
361
- card.state === 'waiting'
362
- ? 'border-dashed border-warning/60'
363
- : 'dms-card p-0'
364
- "
365
- >
366
- <header class="flex items-start gap-3 px-4 pt-4">
367
- <span
368
- class="grid size-10 shrink-0 place-items-center rounded-lg bg-primary/15 font-mono text-sm font-semibold text-(--ui-color-primary-800) dark:text-primary"
369
- >
370
- {{ initials(card.name) }}
371
- </span>
372
- <span class="min-w-0 flex-1">
373
- <span class="block truncate font-semibold text-highlighted">
374
- {{ card.name }}
375
- </span>
376
- <span class="block truncate font-mono text-xs text-dimmed">
377
- {{ card.domain }}
378
- </span>
379
- </span>
380
- <UButton
381
- icon="i-ph-pencil-simple"
382
- size="xs"
383
- color="neutral"
384
- variant="ghost"
385
- :aria-label="
386
- t('page.marketing.websites.edit_named', { name: card.name })
387
- "
388
- @click="edit(card)"
389
- />
390
- </header>
391
- <p
392
- class="flex items-center gap-2 px-4 pt-3 pb-3 font-mono text-xs text-muted"
393
- >
394
- <span class="size-2 rounded-full" :class="STATE_DOTS[card.state]" />
395
- {{ stateLine(card) }}
396
- </p>
397
-
398
- <div
399
- v-if="card.state === 'paused' || card.rejected.length > 0"
400
- class="flex flex-col gap-2 px-4 pb-3"
401
- >
402
- <DmsBanner
403
- v-if="card.state === 'paused'"
404
- tone="info"
405
- size="sm"
406
- icon="i-ph-pause-circle"
407
- :title="t('page.marketing.websites.paused_description')"
408
- >
409
- <template #actions>
410
- <UButton
411
- size="xs"
412
- color="neutral"
413
- variant="outline"
414
- icon="i-ph-play"
415
- :loading="busy === `${card.id}:resume`"
416
- :label="t('page.marketing.websites.resume')"
417
- @click="resume(card)"
418
- />
419
- </template>
420
- </DmsBanner>
421
- <DmsBanner
422
- v-for="refused in card.rejected"
423
- :key="refused.hostname"
424
- tone="warning"
425
- size="sm"
426
- icon="i-ph-prohibit"
427
- :title="
428
- t(
429
- 'page.marketing.websites.refused',
430
- { count: refused.count, host: refused.hostname },
431
- refused.count,
432
- )
433
- "
434
- >
435
- <template #actions>
436
- <UButton
437
- size="xs"
438
- color="neutral"
439
- variant="outline"
440
- :loading="busy === `${card.id}:${refused.hostname}`"
441
- :label="t('page.marketing.install.allow')"
442
- @click="allowHost(card, refused.hostname)"
443
- />
444
- </template>
445
- </DmsBanner>
446
- </div>
447
-
448
- <template v-if="card.state !== 'waiting'">
449
- <div class="border-y border-default">
450
- <!-- The window the three counts cover, whatever period the
451
- other pages show. -->
452
- <p
453
- class="px-4 pt-1.5 text-right font-mono text-[10px] text-dimmed"
454
- >
455
- {{ t('page.marketing.websites.stats.window') }}
456
- </p>
457
- <dl class="grid grid-cols-3">
458
- <div
459
- v-for="stat in ['sessions', 'pages', 'funnels'] as const"
460
- :key="stat"
461
- class="border-default px-4 pt-0.5 pb-3 not-first:border-l"
462
- >
463
- <dt>
464
- <DmsEyebrow
465
- :label="t(`page.marketing.websites.stats.${stat}`)"
466
- />
467
- </dt>
468
- <dd
469
- class="mt-1 font-mono text-lg font-semibold tabular-nums text-highlighted"
470
- >
471
- {{ formatNumber(card[stat], locale) }}
472
- </dd>
473
- </div>
474
- </dl>
475
- </div>
476
- <dl class="flex flex-col gap-2 px-4 py-3 text-sm">
477
- <div class="flex items-start justify-between gap-3">
478
- <dt
479
- class="flex shrink-0 items-center gap-2 whitespace-nowrap text-muted"
480
- >
481
- <UIcon name="i-ph-link" class="size-3.5" />
482
- {{ t('page.marketing.websites.also_accepts') }}
483
- </dt>
484
- <dd class="flex min-w-0 flex-wrap justify-end gap-1">
485
- <span
486
- v-for="host in card.extraDomains"
487
- :key="host"
488
- class="max-w-full truncate rounded bg-elevated px-1.5 py-0.5 font-mono text-[11px] text-toned"
489
- >
490
- {{ host }}
491
- </span>
492
- <span
493
- v-if="card.extraDomains.length === 0"
494
- class="text-xs text-dimmed"
495
- >
496
- {{ t('page.marketing.websites.subdomains_only') }}
497
- </span>
498
- </dd>
499
- </div>
500
- <div class="flex items-center justify-between gap-3">
501
- <dt
502
- class="flex shrink-0 items-center gap-2 whitespace-nowrap text-muted"
503
- >
504
- <UIcon name="i-ph-camera" class="size-3.5" />
505
- {{ t('page.marketing.websites.snapshots') }}
506
- </dt>
507
- <dd>
508
- <DmsStatusPill
509
- size="sm"
510
- :mono="false"
511
- :tone="card.snapshotsEnabled ? 'success' : 'neutral'"
512
- :label="
513
- card.snapshotsEnabled
514
- ? t(
515
- card.snapshotMaskText
516
- ? 'page.marketing.websites.snapshots_masked'
517
- : 'page.marketing.websites.snapshots_on',
518
- )
519
- : t('page.marketing.websites.snapshots_off')
520
- "
521
- />
522
- </dd>
523
- </div>
524
- <div class="flex items-center justify-between gap-3">
525
- <dt
526
- class="flex shrink-0 items-center gap-2 whitespace-nowrap text-muted"
527
- >
528
- <UIcon name="i-ph-cursor-click" class="size-3.5" />
529
- {{ t('page.marketing.websites.sampling') }}
530
- </dt>
531
- <dd class="text-right font-mono text-xs text-highlighted">
532
- {{
533
- formatPercentage(card.sampleRate, locale, fractionDigits(0))
534
- }}
535
- ·
536
- {{ t('page.marketing.websites.sampling_default') }}
537
- </dd>
538
- </div>
539
- </dl>
540
- <footer
541
- class="mt-auto flex items-center justify-between gap-2 border-t border-default px-4 py-3"
542
- >
543
- <UButton
544
- icon="i-ph-code"
545
- size="sm"
546
- color="neutral"
547
- variant="outline"
548
- :label="t('page.marketing.websites.snippet_button')"
549
- :aria-label="
550
- t('page.marketing.websites.snippet_named', {
551
- name: card.name,
552
- })
553
- "
554
- :to="installLink(card.id)"
555
- />
556
- <UButton
557
- trailing-icon="i-ph-arrow-right"
558
- size="sm"
559
- color="neutral"
560
- variant="ghost"
561
- :label="t('page.marketing.websites.open_overview')"
562
- :aria-label="
563
- t('page.marketing.websites.open_overview_named', {
564
- name: card.name,
565
- })
566
- "
567
- @click="openOverview(card)"
568
- />
569
- </footer>
570
- </template>
571
-
572
- <template v-else>
573
- <p class="px-4 text-sm text-muted">
574
- {{
575
- t(
576
- card.rejected.length > 0
577
- ? 'page.marketing.websites.waiting_refused'
578
- : 'page.marketing.websites.waiting_description',
579
- {
580
- date: formatShortDate(card.createdAt, locale, readerZone),
581
- },
582
- )
583
- }}
584
- </p>
585
- <footer class="mt-auto px-4 py-4">
586
- <UButton
587
- icon="i-ph-pulse"
588
- size="sm"
589
- :label="t('page.marketing.websites.finish_install')"
590
- :aria-label="
591
- t('page.marketing.websites.finish_install_named', {
592
- name: card.name,
593
- })
594
- "
595
- :to="installLink(card.id)"
596
- />
597
- </footer>
598
- </template>
599
- </article>
600
- </div>
601
- </template>
602
-
603
- <p
604
- v-if="cards && cards.length > 0"
605
- class="flex items-center gap-2 text-xs text-dimmed"
606
- >
607
- <UIcon name="i-ph-shield-check" class="size-4" />
608
- {{ t('page.marketing.websites.privacy') }}
609
- </p>
610
-
611
- <USlideover
612
- v-model:open="drawerOpen"
613
- :ui="{ content: 'max-w-lg', title: 'min-w-0' }"
614
- @after:leave="onDrawerClosed"
615
- >
616
- <template #title>
617
- <span v-if="editing" class="flex min-w-0 flex-col gap-2">
618
- <DmsEyebrow :label="t('page.marketing.websites.drawer.eyebrow')" />
619
- <span class="flex min-w-0 items-center gap-3">
620
- <span
621
- class="grid size-9 shrink-0 place-items-center rounded-lg bg-primary/15 font-mono text-sm font-semibold text-(--ui-color-primary-800) dark:text-primary"
622
- >
623
- {{ initials(editing.name) }}
624
- </span>
625
- <span class="truncate text-xl font-semibold text-highlighted">
626
- {{ editing.name }}
627
- </span>
628
- </span>
629
- </span>
630
- </template>
631
- <template #description>
632
- <span v-if="editing" class="font-mono text-xs break-all text-dimmed">
633
- {{ editing.id }} ·
634
- {{
635
- t('page.marketing.websites.drawer.added', {
636
- date: formatShortDate(editing.createdAt, locale),
637
- })
638
- }}
639
- </span>
640
- </template>
641
- <template #body>
642
- <div v-if="editing" class="flex flex-col gap-5">
643
- <DmsSegmented
644
- v-model="tab"
645
- :items="tabItems"
646
- size="sm"
647
- class="self-start"
648
- />
649
- <template v-if="tab === 'general'">
650
- <DmsFieldRow
651
- layout="stack"
652
- spacing="list"
653
- :inset="false"
654
- label="$page.marketing.websites.fields.name"
655
- label-for="website-name"
656
- required
657
- >
658
- <UInput
659
- id="website-name"
660
- v-model="draftName"
661
- class="w-full"
662
- :maxlength="MAX_WEBSITE_NAME_LENGTH"
663
- v-bind="fieldErrors.aria('name')"
664
- @update:model-value="fieldErrors.clear('name')"
665
- />
666
- <DmsFieldError
667
- :id="fieldErrors.errorId('name')"
668
- :message="fieldErrors.errors.name"
669
- />
670
- </DmsFieldRow>
671
- <DmsFieldRow
672
- layout="stack"
673
- spacing="list"
674
- :inset="false"
675
- label="$page.marketing.websites.fields.domain"
676
- description="$page.marketing.websites.fields.domain_hint"
677
- label-for="website-domain"
678
- required
679
- >
680
- <UInput
681
- id="website-domain"
682
- v-model="draftDomain"
683
- class="w-full font-mono"
684
- :maxlength="MAX_WEBSITE_DOMAIN_LENGTH"
685
- v-bind="fieldErrors.aria('domain')"
686
- @update:model-value="fieldErrors.clear('domain')"
687
- />
688
- <DmsFieldError
689
- :id="fieldErrors.errorId('domain')"
690
- :message="fieldErrors.errors.domain"
691
- />
692
- </DmsFieldRow>
693
- <DmsFieldRow
694
- layout="stack"
695
- spacing="list"
696
- :inset="false"
697
- label="$page.marketing.websites.fields.hosts"
698
- description="$page.marketing.websites.fields.hosts_hint"
699
- label-for="website-hosts"
700
- >
701
- <UInputTags
702
- id="website-hosts"
703
- v-model="draftHosts"
704
- class="w-full font-mono"
705
- :placeholder="'staging.example.com'"
706
- v-bind="fieldErrors.aria('extraDomains')"
707
- @update:model-value="fieldErrors.clear('extraDomains')"
708
- />
709
- <DmsFieldError
710
- :id="fieldErrors.errorId('extraDomains')"
711
- :message="fieldErrors.errors.extraDomains"
712
- />
713
- </DmsFieldRow>
714
- <DmsFieldRow
715
- spacing="list"
716
- :inset="false"
717
- label="$page.marketing.websites.fields.tracking"
718
- description="$page.marketing.websites.fields.tracking_hint"
719
- label-for="website-tracking"
720
- >
721
- <USwitch id="website-tracking" v-model="draftTracking" />
722
- </DmsFieldRow>
723
- <DmsFormRequiredLegend />
724
- </template>
725
- <template v-else-if="tab === 'capture'">
726
- <DmsFieldRow
727
- spacing="list"
728
- :inset="false"
729
- label="$page.marketing.websites.fields.snapshots"
730
- description="$page.marketing.websites.fields.snapshots_hint"
731
- label-for="website-snapshots"
732
- >
733
- <USwitch id="website-snapshots" v-model="draftSnapshots" />
734
- </DmsFieldRow>
735
- <DmsFieldRow
736
- spacing="list"
737
- :inset="false"
738
- label="$page.marketing.websites.fields.mask"
739
- description="$page.marketing.websites.fields.mask_hint"
740
- label-for="website-mask"
741
- :disabled="!draftSnapshots"
742
- >
743
- <USwitch
744
- id="website-mask"
745
- v-model="draftMask"
746
- :disabled="!draftSnapshots"
747
- />
748
- </DmsFieldRow>
749
- <DmsBanner
750
- v-if="discardsSnapshots"
751
- tone="warning"
752
- size="sm"
753
- :title="
754
- t(
755
- 'page.marketing.websites.fields.discard',
756
- { count: editing.snapshots },
757
- editing.snapshots,
758
- )
759
- "
760
- />
761
- </template>
762
- <template v-else>
763
- <div
764
- class="flex flex-col gap-3 rounded-lg border border-error/40 p-4"
765
- >
766
- <span class="text-sm font-semibold text-highlighted">
767
- {{ t('page.marketing.websites.delete.section') }}
768
- </span>
769
- <span class="text-sm text-muted">
770
- {{ t('page.marketing.websites.delete.section_description') }}
771
- </span>
772
- <UButton
773
- class="self-start"
774
- color="error"
775
- variant="outline"
776
- icon="i-ph-trash"
777
- :label="t('page.marketing.websites.delete.action')"
778
- @click="remove(editing)"
779
- />
780
- </div>
781
- </template>
782
- </div>
783
- </template>
784
- <template #footer>
785
- <div
786
- v-if="tab !== 'danger'"
787
- class="flex w-full items-center justify-between"
788
- >
789
- <UButton
790
- color="neutral"
791
- variant="ghost"
792
- :label="t('page.marketing.common.cancel')"
793
- @click="drawerOpen = false"
794
- />
795
- <UButton
796
- :loading="saving"
797
- :label="t('page.marketing.common.save_changes')"
798
- @click="save"
799
- />
800
- </div>
801
- <UButton
802
- v-else
803
- class="ml-auto"
804
- color="neutral"
805
- variant="outline"
806
- :label="t('page.marketing.websites.drawer.close')"
807
- @click="drawerOpen = false"
808
- />
809
- </template>
810
- </USlideover>
811
- </div>
812
- </template>