@antelopejs/dms-marketing 0.2.2
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.
- package/LICENSE +190 -0
- package/README.md +143 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +35 -0
- package/dist/config.js.map +1 -0
- package/dist/data-api/body.d.ts +1 -0
- package/dist/data-api/body.js +15 -0
- package/dist/data-api/body.js.map +1 -0
- package/dist/data-api/funnels.d.ts +32 -0
- package/dist/data-api/funnels.js +302 -0
- package/dist/data-api/funnels.js.map +1 -0
- package/dist/data-api/hidden-filter.d.ts +1 -0
- package/dist/data-api/hidden-filter.js +11 -0
- package/dist/data-api/hidden-filter.js.map +1 -0
- package/dist/data-api/index.d.ts +4 -0
- package/dist/data-api/index.js +21 -0
- package/dist/data-api/index.js.map +1 -0
- package/dist/data-api/tenant-scope.d.ts +15 -0
- package/dist/data-api/tenant-scope.js +43 -0
- package/dist/data-api/tenant-scope.js.map +1 -0
- package/dist/data-api/websites.d.ts +25 -0
- package/dist/data-api/websites.js +73 -0
- package/dist/data-api/websites.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/funnels.model.d.ts +25 -0
- package/dist/db/models/funnels.model.js +39 -0
- package/dist/db/models/funnels.model.js.map +1 -0
- package/dist/db/models/index.d.ts +7 -0
- package/dist/db/models/index.js +24 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/marketing_events.model.d.ts +49 -0
- package/dist/db/models/marketing_events.model.js +85 -0
- package/dist/db/models/marketing_events.model.js.map +1 -0
- package/dist/db/models/marketing_sessions.model.d.ts +33 -0
- package/dist/db/models/marketing_sessions.model.js +50 -0
- package/dist/db/models/marketing_sessions.model.js.map +1 -0
- package/dist/db/models/marketing_settings.model.d.ts +23 -0
- package/dist/db/models/marketing_settings.model.js +13 -0
- package/dist/db/models/marketing_settings.model.js.map +1 -0
- package/dist/db/models/page_snapshots.model.d.ts +27 -0
- package/dist/db/models/page_snapshots.model.js +32 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -0
- package/dist/db/models/retention.d.ts +6 -0
- package/dist/db/models/retention.js +26 -0
- package/dist/db/models/retention.js.map +1 -0
- package/dist/db/models/website_statistics.model.d.ts +31 -0
- package/dist/db/models/website_statistics.model.js +63 -0
- package/dist/db/models/website_statistics.model.js.map +1 -0
- package/dist/db/models/websites.model.d.ts +26 -0
- package/dist/db/models/websites.model.js +27 -0
- package/dist/db/models/websites.model.js.map +1 -0
- package/dist/db/tables/funnels.table.d.ts +31 -0
- package/dist/db/tables/funnels.table.js +54 -0
- package/dist/db/tables/funnels.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +7 -0
- package/dist/db/tables/index.js +24 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/marketing_events.table.d.ts +15 -0
- package/dist/db/tables/marketing_events.table.js +66 -0
- package/dist/db/tables/marketing_events.table.js.map +1 -0
- package/dist/db/tables/marketing_sessions.table.d.ts +26 -0
- package/dist/db/tables/marketing_sessions.table.js +115 -0
- package/dist/db/tables/marketing_sessions.table.js.map +1 -0
- package/dist/db/tables/marketing_settings.table.d.ts +11 -0
- package/dist/db/tables/marketing_settings.table.js +51 -0
- package/dist/db/tables/marketing_settings.table.js.map +1 -0
- package/dist/db/tables/page_snapshots.table.d.ts +17 -0
- package/dist/db/tables/page_snapshots.table.js +72 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -0
- package/dist/db/tables/website_statistics.table.d.ts +14 -0
- package/dist/db/tables/website_statistics.table.js +60 -0
- package/dist/db/tables/website_statistics.table.js.map +1 -0
- package/dist/db/tables/websites.table.d.ts +13 -0
- package/dist/db/tables/websites.table.js +60 -0
- package/dist/db/tables/websites.table.js.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +39 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/implementations/dms-marketing/index.d.ts +2 -0
- package/dist/implementations/dms-marketing/index.js +8 -0
- package/dist/implementations/dms-marketing/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/all.d.ts +5 -0
- package/dist/pages/all.js +22 -0
- package/dist/pages/all.js.map +1 -0
- package/dist/pages/campaigns/page.d.ts +9 -0
- package/dist/pages/campaigns/page.js +27 -0
- package/dist/pages/campaigns/page.js.map +1 -0
- package/dist/pages/funnels/page.d.ts +9 -0
- package/dist/pages/funnels/page.js +27 -0
- package/dist/pages/funnels/page.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/module.d.ts +1 -0
- package/dist/pages/module.js +13 -0
- package/dist/pages/module.js.map +1 -0
- package/dist/pages/overview/page.d.ts +9 -0
- package/dist/pages/overview/page.js +27 -0
- package/dist/pages/overview/page.js.map +1 -0
- package/dist/pages/pages/page.d.ts +9 -0
- package/dist/pages/pages/page.js +27 -0
- package/dist/pages/pages/page.js.map +1 -0
- package/dist/pages/settings/form.d.ts +3 -0
- package/dist/pages/settings/form.js +35 -0
- package/dist/pages/settings/form.js.map +1 -0
- package/dist/pages/settings/page.d.ts +9 -0
- package/dist/pages/settings/page.js +27 -0
- package/dist/pages/settings/page.js.map +1 -0
- package/dist/routes/collect.d.ts +7 -0
- package/dist/routes/collect.js +62 -0
- package/dist/routes/collect.js.map +1 -0
- package/dist/routes/experiments.d.ts +6 -0
- package/dist/routes/experiments.js +63 -0
- package/dist/routes/experiments.js.map +1 -0
- package/dist/routes/funnels.d.ts +16 -0
- package/dist/routes/funnels.js +44 -0
- package/dist/routes/funnels.js.map +1 -0
- package/dist/routes/index.d.ts +8 -0
- package/dist/routes/index.js +25 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/settings.d.ts +8 -0
- package/dist/routes/settings.js +50 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/snapshot.d.ts +10 -0
- package/dist/routes/snapshot.js +147 -0
- package/dist/routes/snapshot.js.map +1 -0
- package/dist/routes/stats.d.ts +44 -0
- package/dist/routes/stats.js +278 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/tracker.d.ts +6 -0
- package/dist/routes/tracker.js +57 -0
- package/dist/routes/tracker.js.map +1 -0
- package/dist/routes/websites.d.ts +13 -0
- package/dist/routes/websites.js +144 -0
- package/dist/routes/websites.js.map +1 -0
- package/dist/services/acquisition.d.ts +15 -0
- package/dist/services/acquisition.js +153 -0
- package/dist/services/acquisition.js.map +1 -0
- package/dist/services/bounded-cache.d.ts +14 -0
- package/dist/services/bounded-cache.js +45 -0
- package/dist/services/bounded-cache.js.map +1 -0
- package/dist/services/collect.d.ts +11 -0
- package/dist/services/collect.js +193 -0
- package/dist/services/collect.js.map +1 -0
- package/dist/services/experiment-definitions.d.ts +4 -0
- package/dist/services/experiment-definitions.js +36 -0
- package/dist/services/experiment-definitions.js.map +1 -0
- package/dist/services/experiments/assign.d.ts +5 -0
- package/dist/services/experiments/assign.js +52 -0
- package/dist/services/experiments/assign.js.map +1 -0
- package/dist/services/experiments/index.d.ts +3 -0
- package/dist/services/experiments/index.js +20 -0
- package/dist/services/experiments/index.js.map +1 -0
- package/dist/services/experiments/results.d.ts +20 -0
- package/dist/services/experiments/results.js +91 -0
- package/dist/services/experiments/results.js.map +1 -0
- package/dist/services/experiments/stats.d.ts +12 -0
- package/dist/services/experiments/stats.js +62 -0
- package/dist/services/experiments/stats.js.map +1 -0
- package/dist/services/funnels/compute.d.ts +15 -0
- package/dist/services/funnels/compute.js +68 -0
- package/dist/services/funnels/compute.js.map +1 -0
- package/dist/services/funnels/index.d.ts +2 -0
- package/dist/services/funnels/index.js +19 -0
- package/dist/services/funnels/index.js.map +1 -0
- package/dist/services/funnels/results.d.ts +14 -0
- package/dist/services/funnels/results.js +72 -0
- package/dist/services/funnels/results.js.map +1 -0
- package/dist/services/geoip.d.ts +3 -0
- package/dist/services/geoip.js +38 -0
- package/dist/services/geoip.js.map +1 -0
- package/dist/services/heatmap.d.ts +10 -0
- package/dist/services/heatmap.js +180 -0
- package/dist/services/heatmap.js.map +1 -0
- package/dist/services/index.d.ts +26 -0
- package/dist/services/index.js +43 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/ingest-guard.d.ts +11 -0
- package/dist/services/ingest-guard.js +44 -0
- package/dist/services/ingest-guard.js.map +1 -0
- package/dist/services/keyed-queue.d.ts +4 -0
- package/dist/services/keyed-queue.js +19 -0
- package/dist/services/keyed-queue.js.map +1 -0
- package/dist/services/origin-guard.d.ts +5 -0
- package/dist/services/origin-guard.js +43 -0
- package/dist/services/origin-guard.js.map +1 -0
- package/dist/services/period.d.ts +2 -0
- package/dist/services/period.js +19 -0
- package/dist/services/period.js.map +1 -0
- package/dist/services/prune.d.ts +7 -0
- package/dist/services/prune.js +49 -0
- package/dist/services/prune.js.map +1 -0
- package/dist/services/query-param.d.ts +1 -0
- package/dist/services/query-param.js +11 -0
- package/dist/services/query-param.js.map +1 -0
- package/dist/services/request-body.d.ts +2 -0
- package/dist/services/request-body.js +41 -0
- package/dist/services/request-body.js.map +1 -0
- package/dist/services/request-meta.d.ts +3 -0
- package/dist/services/request-meta.js +19 -0
- package/dist/services/request-meta.js.map +1 -0
- package/dist/services/rollup-write.d.ts +4 -0
- package/dist/services/rollup-write.js +84 -0
- package/dist/services/rollup-write.js.map +1 -0
- package/dist/services/sessionize.d.ts +23 -0
- package/dist/services/sessionize.js +78 -0
- package/dist/services/sessionize.js.map +1 -0
- package/dist/services/settings.d.ts +6 -0
- package/dist/services/settings.js +113 -0
- package/dist/services/settings.js.map +1 -0
- package/dist/services/snapshots.d.ts +24 -0
- package/dist/services/snapshots.js +139 -0
- package/dist/services/snapshots.js.map +1 -0
- package/dist/services/tenant-funnel.d.ts +3 -0
- package/dist/services/tenant-funnel.js +17 -0
- package/dist/services/tenant-funnel.js.map +1 -0
- package/dist/services/tenant-website.d.ts +5 -0
- package/dist/services/tenant-website.js +34 -0
- package/dist/services/tenant-website.js.map +1 -0
- package/dist/services/url.d.ts +10 -0
- package/dist/services/url.js +81 -0
- package/dist/services/url.js.map +1 -0
- package/dist/services/user-agent.d.ts +2 -0
- package/dist/services/user-agent.js +17 -0
- package/dist/services/user-agent.js.map +1 -0
- package/dist/services/visitor-id.d.ts +1 -0
- package/dist/services/visitor-id.js +19 -0
- package/dist/services/visitor-id.js.map +1 -0
- package/dist/services/website-cache.d.ts +3 -0
- package/dist/services/website-cache.js +22 -0
- package/dist/services/website-cache.js.map +1 -0
- package/dist/types/constants.d.ts +74 -0
- package/dist/types/constants.js +79 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/events.d.ts +114 -0
- package/dist/types/events.js +29 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/settings.d.ts +67 -0
- package/dist/types/settings.js +58 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/snapshots.d.ts +95 -0
- package/dist/types/snapshots.js +41 -0
- package/dist/types/snapshots.js.map +1 -0
- package/dist/types/statistics.d.ts +38 -0
- package/dist/types/statistics.js +33 -0
- package/dist/types/statistics.js.map +1 -0
- package/dist/utils/experiment-type.d.ts +52 -0
- package/dist/utils/experiment-type.js +94 -0
- package/dist/utils/experiment-type.js.map +1 -0
- package/dist/utils/funnel-steps-type.d.ts +42 -0
- package/dist/utils/funnel-steps-type.js +62 -0
- package/dist/utils/funnel-steps-type.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +7 -0
- package/frontend-vue/app/components/CampaignsView.vue +330 -0
- package/frontend-vue/app/components/ExperimentInput.vue +128 -0
- package/frontend-vue/app/components/FunnelDetail.vue +1026 -0
- package/frontend-vue/app/components/FunnelStepsInput.vue +154 -0
- package/frontend-vue/app/components/FunnelsView.vue +493 -0
- package/frontend-vue/app/components/HeatmapCanvas.vue +116 -0
- package/frontend-vue/app/components/NoWebsiteState.vue +18 -0
- package/frontend-vue/app/components/OverviewView.vue +493 -0
- package/frontend-vue/app/components/PagePreview.vue +915 -0
- package/frontend-vue/app/components/PagesView.vue +378 -0
- package/frontend-vue/app/components/ScrollDepthOverlay.vue +158 -0
- package/frontend-vue/app/components/TextKpiCard.vue +28 -0
- package/frontend-vue/app/components/TopListTabsCard.vue +162 -0
- package/frontend-vue/app/components/TrackerSnippet.vue +31 -0
- package/frontend-vue/app/components/VariationsInput.vue +164 -0
- package/frontend-vue/app/composables/useChildId.ts +13 -0
- package/frontend-vue/app/composables/useExperiment.ts +59 -0
- package/frontend-vue/app/composables/useFunnelSteps.ts +25 -0
- package/frontend-vue/app/composables/useLatestRequest.ts +90 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +420 -0
- package/frontend-vue/app/composables/useMarketingPeriod.ts +66 -0
- package/frontend-vue/app/composables/useMarketingRoutes.ts +51 -0
- package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +69 -0
- package/frontend-vue/app/composables/useMarketingWebsites.ts +80 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +183 -0
- package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +46 -0
- package/frontend-vue/app/utils/dmsComponent.ts +18 -0
- package/frontend-vue/app/utils/heatmapPalette.ts +41 -0
- package/frontend-vue/app/utils/snapshotViewportUnits.ts +63 -0
- package/frontend-vue/dms.frontend.ts +35 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +382 -0
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +382 -0
- package/frontend-vue/package.json +30 -0
- package/frontend-vue/pnpm-lock.yaml +5226 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/drafts.test.ts +54 -0
- package/frontend-vue/tests/i18n.test.ts +86 -0
- package/frontend-vue/tests/period.test.ts +35 -0
- package/frontend-vue/tests/registration.test.ts +63 -0
- package/frontend-vue/tests/routes.test.ts +31 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +56 -0
- package/frontend-vue/vitest.config.ts +20 -0
- package/package.json +91 -0
- package/static/snapshot.js +497 -0
- package/static/tracker.js +503 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
useDmsRoute,
|
|
5
|
+
useDmsRouter,
|
|
6
|
+
useI18n,
|
|
7
|
+
} from '#dms/frontend-module'
|
|
8
|
+
|
|
9
|
+
interface TopListTabItem {
|
|
10
|
+
id: string
|
|
11
|
+
title: string
|
|
12
|
+
value: number
|
|
13
|
+
to?: { path: string, query?: Record<string, string> }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TopListTab {
|
|
17
|
+
id: string
|
|
18
|
+
label: string
|
|
19
|
+
items: TopListTabItem[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One overview card holding sibling top lists behind pill tabs. The rows
|
|
24
|
+
* mirror dms-ui's DmsTopListCard rendering, re-done locally because that
|
|
25
|
+
* card carries its own DmsCard chrome and cannot nest inside another card.
|
|
26
|
+
* A single tab renders as a plain list: no pill bar.
|
|
27
|
+
*/
|
|
28
|
+
const props = defineProps<{
|
|
29
|
+
title: string
|
|
30
|
+
tabs: TopListTab[]
|
|
31
|
+
emptyLabel: string
|
|
32
|
+
/** URL query key persisting the active tab; omit for free state. */
|
|
33
|
+
stateKey?: string
|
|
34
|
+
footerLabel?: string
|
|
35
|
+
footerTo?: { path: string, query?: Record<string, string> }
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
const route = useDmsRoute()
|
|
39
|
+
const router = useDmsRouter()
|
|
40
|
+
const { locale } = useI18n()
|
|
41
|
+
|
|
42
|
+
const HIGHLIGHTED_RANKS = 3
|
|
43
|
+
|
|
44
|
+
const tabItems = computed(() =>
|
|
45
|
+
props.tabs.map(tab => ({ label: tab.label, value: tab.id })))
|
|
46
|
+
|
|
47
|
+
function initialTab(): string {
|
|
48
|
+
const wanted = props.stateKey ? route.query[props.stateKey] : undefined
|
|
49
|
+
return typeof wanted === 'string' && props.tabs.some(tab => tab.id === wanted)
|
|
50
|
+
? wanted
|
|
51
|
+
: (props.tabs[0]?.id ?? '')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const activeId = ref(initialTab())
|
|
55
|
+
|
|
56
|
+
// Conditional tabs (countries follows geoipEnabled, false until the fetch
|
|
57
|
+
// settles) can be missing at mount and appear later: adopt the tab the URL
|
|
58
|
+
// still asks for once it exists. The activeId watcher keeps the query in
|
|
59
|
+
// step with every manual pick, so re-reading it can never override one.
|
|
60
|
+
// The fallback covers the reverse: the active tab disappearing.
|
|
61
|
+
watch(() => props.tabs, (tabs) => {
|
|
62
|
+
const wanted = props.stateKey ? route.query[props.stateKey] : undefined
|
|
63
|
+
if (typeof wanted === 'string' && tabs.some(tab => tab.id === wanted)) {
|
|
64
|
+
activeId.value = wanted
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (!tabs.some(tab => tab.id === activeId.value)) {
|
|
68
|
+
activeId.value = tabs[0]?.id ?? ''
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Replace, not push: paging through tabs must not stack history entries.
|
|
73
|
+
// The default tab keeps the URL bare so a copied link stays minimal.
|
|
74
|
+
watch(activeId, (id) => {
|
|
75
|
+
const key = props.stateKey
|
|
76
|
+
if (!key) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const query = Object.fromEntries(
|
|
80
|
+
Object.entries(route.query).filter(([name]) => name !== key),
|
|
81
|
+
)
|
|
82
|
+
if (id !== props.tabs[0]?.id) {
|
|
83
|
+
query[key] = id
|
|
84
|
+
}
|
|
85
|
+
void router.replace({ query })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const activeTab = computed(() =>
|
|
89
|
+
props.tabs.find(tab => tab.id === activeId.value) ?? props.tabs[0])
|
|
90
|
+
|
|
91
|
+
function rankClass(index: number): string {
|
|
92
|
+
return index < HIGHLIGHTED_RANKS ? 'font-semibold text-primary' : 'text-muted'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatCount(value: number): string {
|
|
96
|
+
return value.toLocaleString(locale.value)
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<template>
|
|
101
|
+
<!-- The grid stretches the cards of a row; the growing body soaks up the
|
|
102
|
+
slack so every footer anchors to the bottom edge. -->
|
|
103
|
+
<UCard class="flex flex-col" :ui="{ body: 'grow' }">
|
|
104
|
+
<template #header>
|
|
105
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
106
|
+
<p class="font-medium">{{ title }}</p>
|
|
107
|
+
<UTabs
|
|
108
|
+
v-if="tabs.length > 1"
|
|
109
|
+
v-model="activeId"
|
|
110
|
+
:items="tabItems"
|
|
111
|
+
:content="false"
|
|
112
|
+
size="xs"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<!-- min-h holds the height of a full list — 8 rows of 2.5rem plus 7
|
|
118
|
+
divide-y pixels — so switching to a shorter tab never jumps. -->
|
|
119
|
+
<ul
|
|
120
|
+
v-if="activeTab && activeTab.items.length > 0"
|
|
121
|
+
class="grid min-h-[calc(20rem+7px)] content-start gap-x-3 divide-y divide-default [grid-template-columns:1.75rem_minmax(0,1fr)_auto]"
|
|
122
|
+
>
|
|
123
|
+
<li
|
|
124
|
+
v-for="(item, index) in activeTab.items"
|
|
125
|
+
:key="item.id"
|
|
126
|
+
class="relative col-[1/-1] grid grid-cols-subgrid items-center py-2.5 transition-colors hover:bg-elevated/60"
|
|
127
|
+
:class="item.to ? 'cursor-pointer' : ''"
|
|
128
|
+
>
|
|
129
|
+
<DmsLink
|
|
130
|
+
v-if="item.to"
|
|
131
|
+
:to="item.to"
|
|
132
|
+
class="absolute inset-0 z-10"
|
|
133
|
+
:aria-label="item.title"
|
|
134
|
+
/>
|
|
135
|
+
<span
|
|
136
|
+
class="text-center font-mono text-xs tabular-nums"
|
|
137
|
+
:class="rankClass(index)"
|
|
138
|
+
>
|
|
139
|
+
{{ String(index + 1).padStart(2, '0') }}
|
|
140
|
+
</span>
|
|
141
|
+
<p class="min-w-0 truncate text-sm font-medium">{{ item.title }}</p>
|
|
142
|
+
<span class="justify-self-end whitespace-nowrap text-xs tabular-nums text-muted">
|
|
143
|
+
{{ formatCount(item.value) }}
|
|
144
|
+
</span>
|
|
145
|
+
</li>
|
|
146
|
+
</ul>
|
|
147
|
+
|
|
148
|
+
<div v-else class="flex min-h-[calc(20rem+7px)] items-center justify-center">
|
|
149
|
+
<p class="text-sm text-muted">{{ emptyLabel }}</p>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<template v-if="footerLabel && footerTo" #footer>
|
|
153
|
+
<DmsLink
|
|
154
|
+
:to="footerTo"
|
|
155
|
+
class="inline-flex items-center gap-1 text-sm text-muted transition-colors hover:text-highlighted"
|
|
156
|
+
>
|
|
157
|
+
{{ footerLabel }}
|
|
158
|
+
<UIcon name="i-ph-arrow-right" class="size-4" />
|
|
159
|
+
</DmsLink>
|
|
160
|
+
</template>
|
|
161
|
+
</UCard>
|
|
162
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useDmsRuntimeConfig, useI18n } from '#dms/frontend-module'
|
|
4
|
+
import type { MarketingWebsite } from '../composables/useMarketingApi'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{ website: MarketingWebsite }>()
|
|
7
|
+
const { t } = useI18n()
|
|
8
|
+
const config = useDmsRuntimeConfig()
|
|
9
|
+
|
|
10
|
+
const backendOrigin = computed(() =>
|
|
11
|
+
String((config.public as { dms?: { baseURL?: string } }).dms?.baseURL ?? ''),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const CLOSING_TAG = '</' + 'script>'
|
|
15
|
+
|
|
16
|
+
const snippet = computed(() =>
|
|
17
|
+
`<script defer src="${backendOrigin.value}/api/marketing/tracker.js" data-website-id="${props.website._id}">${CLOSING_TAG}`,
|
|
18
|
+
)
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<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" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { useI18n } from '#dms/frontend-module'
|
|
4
|
+
import {
|
|
5
|
+
parseExperimentVariationsValue,
|
|
6
|
+
type ExperimentVariationDraft,
|
|
7
|
+
} from '../composables/useExperiment'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The arms of an experiment, inside DmsMarketingExperimentInput: a list of
|
|
11
|
+
* {key, weight} rows emitted as the array itself. Bounds mirror the backend
|
|
12
|
+
* MIN/MAX_EXPERIMENT_VARIATIONS; the first arm is the control.
|
|
13
|
+
*/
|
|
14
|
+
const MIN_VARIATIONS = 2
|
|
15
|
+
const MAX_VARIATIONS = 8
|
|
16
|
+
const DEFAULT_WEIGHT = 1
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
modelValue?: ExperimentVariationDraft[] | string | null
|
|
20
|
+
initialValue?: ExperimentVariationDraft[] | string | null
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
'update:modelValue': [value: ExperimentVariationDraft[]]
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const { t } = useI18n()
|
|
29
|
+
|
|
30
|
+
function emptyVariation(): ExperimentVariationDraft {
|
|
31
|
+
return { key: '', weight: DEFAULT_WEIGHT }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseValue(
|
|
35
|
+
raw: ExperimentVariationDraft[] | string | null | undefined,
|
|
36
|
+
): ExperimentVariationDraft[] {
|
|
37
|
+
// Copies, not references: the drafts are mutated by the inputs below and
|
|
38
|
+
// must never write through to the prop value.
|
|
39
|
+
const variations = parseExperimentVariationsValue(raw).map(variation => ({
|
|
40
|
+
...variation,
|
|
41
|
+
}))
|
|
42
|
+
while (variations.length < MIN_VARIATIONS) {
|
|
43
|
+
variations.push(emptyVariation())
|
|
44
|
+
}
|
|
45
|
+
return variations
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const variations = ref<ExperimentVariationDraft[]>(
|
|
49
|
+
parseValue(props.modelValue ?? props.initialValue),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const totalWeight = computed(() =>
|
|
53
|
+
variations.value.reduce(
|
|
54
|
+
(sum, variation) => sum + Math.max(variation.weight || 0, 0),
|
|
55
|
+
0,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
function sharePercent(variation: ExperimentVariationDraft): number {
|
|
60
|
+
if (totalWeight.value <= 0) {
|
|
61
|
+
return Math.round(100 / variations.value.length)
|
|
62
|
+
}
|
|
63
|
+
return Math.round(
|
|
64
|
+
(Math.max(variation.weight || 0, 0) / totalWeight.value) * 100,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Values are emitted untrimmed — trimming here would eat characters
|
|
69
|
+
// mid-typing through the v-model echo; the DataType schema trims server-side.
|
|
70
|
+
function onEdit(): void {
|
|
71
|
+
emit(
|
|
72
|
+
'update:modelValue',
|
|
73
|
+
variations.value.map(variation => ({
|
|
74
|
+
key: variation.key,
|
|
75
|
+
weight: variation.weight,
|
|
76
|
+
})),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function addVariation(): void {
|
|
81
|
+
if (variations.value.length >= MAX_VARIATIONS) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
variations.value.push(emptyVariation())
|
|
85
|
+
onEdit()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function removeVariation(index: number): void {
|
|
89
|
+
if (variations.value.length <= MIN_VARIATIONS) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
variations.value.splice(index, 1)
|
|
93
|
+
onEdit()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
watch(
|
|
97
|
+
() => props.modelValue,
|
|
98
|
+
(value) => {
|
|
99
|
+
const parsed = parseValue(value)
|
|
100
|
+
if (JSON.stringify(parsed) !== JSON.stringify(variations.value)) {
|
|
101
|
+
variations.value = parsed
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<template>
|
|
108
|
+
<div class="flex flex-col gap-2">
|
|
109
|
+
<div
|
|
110
|
+
v-for="(variation, index) in variations"
|
|
111
|
+
:key="index"
|
|
112
|
+
class="flex items-center gap-2"
|
|
113
|
+
>
|
|
114
|
+
<UBadge
|
|
115
|
+
v-if="index === 0"
|
|
116
|
+
:label="t('page.marketing.funnels.results.control')"
|
|
117
|
+
color="neutral"
|
|
118
|
+
variant="subtle"
|
|
119
|
+
size="sm"
|
|
120
|
+
class="w-16 justify-center shrink-0"
|
|
121
|
+
/>
|
|
122
|
+
<span v-else class="w-16 shrink-0" />
|
|
123
|
+
<UInput
|
|
124
|
+
v-model="variation.key"
|
|
125
|
+
:placeholder="t('page.marketing.funnels.form.variation_key')"
|
|
126
|
+
:disabled="props.disabled"
|
|
127
|
+
class="flex-1 font-mono"
|
|
128
|
+
size="sm"
|
|
129
|
+
@update:model-value="onEdit"
|
|
130
|
+
/>
|
|
131
|
+
<UInput
|
|
132
|
+
v-model.number="variation.weight"
|
|
133
|
+
type="number"
|
|
134
|
+
min="0"
|
|
135
|
+
:disabled="props.disabled"
|
|
136
|
+
class="w-20"
|
|
137
|
+
size="sm"
|
|
138
|
+
@update:model-value="onEdit"
|
|
139
|
+
/>
|
|
140
|
+
<span class="w-10 shrink-0 text-right text-xs text-muted">
|
|
141
|
+
{{ sharePercent(variation) }}%
|
|
142
|
+
</span>
|
|
143
|
+
<UButton
|
|
144
|
+
v-if="!props.disabled"
|
|
145
|
+
icon="i-ph-x"
|
|
146
|
+
color="neutral"
|
|
147
|
+
variant="ghost"
|
|
148
|
+
size="xs"
|
|
149
|
+
:disabled="variations.length <= MIN_VARIATIONS"
|
|
150
|
+
@click="removeVariation(index)"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
<UButton
|
|
154
|
+
v-if="!props.disabled && variations.length < MAX_VARIATIONS"
|
|
155
|
+
icon="i-ph-plus"
|
|
156
|
+
:label="t('page.marketing.funnels.form.add_variation')"
|
|
157
|
+
color="neutral"
|
|
158
|
+
variant="subtle"
|
|
159
|
+
size="sm"
|
|
160
|
+
class="self-start"
|
|
161
|
+
@click="addVariation"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derives one stable id per child from the id the DMS page renderer hands the
|
|
5
|
+
* component. dms-ui primitives key their events and their persisted state on
|
|
6
|
+
* it, so two children sharing an id share that state; the `${parent}-child`
|
|
7
|
+
* shape is the one DmsRecursiveComponent uses.
|
|
8
|
+
*/
|
|
9
|
+
export function useChildId(
|
|
10
|
+
componentId: MaybeRefOrGetter<string | undefined>,
|
|
11
|
+
): (suffix: string) => string {
|
|
12
|
+
return (suffix: string) => `${toValue(componentId)}-${suffix}`
|
|
13
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MarketingExperimentRun,
|
|
3
|
+
MarketingExperimentStatus,
|
|
4
|
+
} from './useMarketingApi'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The A/B facet of a funnel as it travels through the form layer.
|
|
8
|
+
*
|
|
9
|
+
* The value reaches components either as the real object or as a JSON string —
|
|
10
|
+
* the input and the list display both have to read the two shapes, so the
|
|
11
|
+
* parse lives once here. Invalid input answers null (or an empty list), never
|
|
12
|
+
* a throw: this runs while rendering a cell.
|
|
13
|
+
*/
|
|
14
|
+
export interface ExperimentVariationDraft {
|
|
15
|
+
key: string
|
|
16
|
+
weight: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExperimentDraft {
|
|
20
|
+
key: string
|
|
21
|
+
status?: MarketingExperimentStatus
|
|
22
|
+
variations: ExperimentVariationDraft[]
|
|
23
|
+
/** Read-only here: the pane reads how many runs the split has had, the
|
|
24
|
+
* form carries them back untouched and the backend re-derives them. */
|
|
25
|
+
runs: MarketingExperimentRun[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseJsonValue(raw: unknown): unknown {
|
|
29
|
+
if (typeof raw !== 'string') {
|
|
30
|
+
return raw
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return raw ? JSON.parse(raw) : null
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseExperimentVariationsValue(
|
|
41
|
+
raw: unknown,
|
|
42
|
+
): ExperimentVariationDraft[] {
|
|
43
|
+
const parsed = parseJsonValue(raw)
|
|
44
|
+
return Array.isArray(parsed) ? (parsed as ExperimentVariationDraft[]) : []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseExperimentValue(raw: unknown): ExperimentDraft | null {
|
|
48
|
+
const parsed = parseJsonValue(raw)
|
|
49
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
const candidate = parsed as Partial<ExperimentDraft>
|
|
53
|
+
return {
|
|
54
|
+
key: typeof candidate.key === 'string' ? candidate.key : '',
|
|
55
|
+
status: candidate.status,
|
|
56
|
+
variations: parseExperimentVariationsValue(candidate.variations),
|
|
57
|
+
runs: Array.isArray(candidate.runs) ? candidate.runs : [],
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funnel steps as they travel through the form layer.
|
|
3
|
+
*
|
|
4
|
+
* The value reaches components either as the real array or as a JSON string —
|
|
5
|
+
* the input and the table display both have to read the two shapes, so the
|
|
6
|
+
* parse lives once here. Invalid input answers an empty list, never a throw:
|
|
7
|
+
* this runs while rendering a cell.
|
|
8
|
+
*/
|
|
9
|
+
export interface FunnelStepDraft {
|
|
10
|
+
kind: 'url' | 'custom'
|
|
11
|
+
value: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseFunnelStepsValue(raw: unknown): FunnelStepDraft[] {
|
|
15
|
+
let parsed: unknown = raw
|
|
16
|
+
if (typeof raw === 'string') {
|
|
17
|
+
try {
|
|
18
|
+
parsed = raw ? JSON.parse(raw) : []
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
parsed = []
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return Array.isArray(parsed) ? (parsed as FunnelStepDraft[]) : []
|
|
25
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A read whose answers may land out of order, resolved so that only the
|
|
5
|
+
* newest one is ever shown.
|
|
6
|
+
*
|
|
7
|
+
* Every surface of this module refetches on a selection, a period and
|
|
8
|
+
* sometimes a search box, and those fetches are not ordered: a slow answer for
|
|
9
|
+
* the site, page or funnel the user just left would otherwise repaint over the
|
|
10
|
+
* one on screen. The rule is the same everywhere — stamp each run, and let a
|
|
11
|
+
* run write only while it is still the latest — which is exactly what this
|
|
12
|
+
* owns instead of leaving it to be re-derived per component.
|
|
13
|
+
*
|
|
14
|
+
* `fetcher` returns `null` when there is nothing to ask for (no selection yet,
|
|
15
|
+
* or server-side, where there is no session to authenticate with). That is a
|
|
16
|
+
* settled empty state, not a failure and not a load: it clears the data and
|
|
17
|
+
* leaves nothing spinning.
|
|
18
|
+
*/
|
|
19
|
+
export interface LatestRequest<T> {
|
|
20
|
+
/** Newest answer, or null before the first one and after any failure. */
|
|
21
|
+
data: Ref<T | null>
|
|
22
|
+
loading: Ref<boolean>
|
|
23
|
+
failed: Ref<boolean>
|
|
24
|
+
/** Cause of the last failure, for callers that surface a message. */
|
|
25
|
+
error: Ref<unknown>
|
|
26
|
+
/**
|
|
27
|
+
* False until a run has settled. This is what tells "not asked yet" from
|
|
28
|
+
* "asked, and there is nothing" — a component that renders an empty state
|
|
29
|
+
* on the former flashes it before its first fetch resolves.
|
|
30
|
+
*/
|
|
31
|
+
settled: Ref<boolean>
|
|
32
|
+
run: () => Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useLatestRequest<T>(
|
|
36
|
+
fetcher: () => Promise<T> | null,
|
|
37
|
+
): LatestRequest<T> {
|
|
38
|
+
const data = ref(null) as Ref<T | null>
|
|
39
|
+
const loading = ref(false)
|
|
40
|
+
const failed = ref(false)
|
|
41
|
+
const error = ref<unknown>(null)
|
|
42
|
+
const settled = ref(false)
|
|
43
|
+
|
|
44
|
+
let latest = 0
|
|
45
|
+
|
|
46
|
+
async function run(): Promise<void> {
|
|
47
|
+
const request = ++latest
|
|
48
|
+
// Cleared before the guard below, so a retry with nothing to fetch cannot
|
|
49
|
+
// leave a stale error on screen.
|
|
50
|
+
failed.value = false
|
|
51
|
+
error.value = null
|
|
52
|
+
|
|
53
|
+
// Called synchronously: the fetcher reads the selection as it is now, not
|
|
54
|
+
// as it will be once the previous request resolves.
|
|
55
|
+
const pending = fetcher()
|
|
56
|
+
if (!pending) {
|
|
57
|
+
data.value = null
|
|
58
|
+
loading.value = false
|
|
59
|
+
settled.value = true
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
loading.value = true
|
|
64
|
+
try {
|
|
65
|
+
const result = await pending
|
|
66
|
+
if (request !== latest) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
data.value = result
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
if (request !== latest) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
failed.value = true
|
|
76
|
+
error.value = cause
|
|
77
|
+
data.value = null
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
// A superseded run owns none of the shared state — not even the flag
|
|
81
|
+
// that says the newest one is still in flight.
|
|
82
|
+
if (request === latest) {
|
|
83
|
+
loading.value = false
|
|
84
|
+
settled.value = true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { data, loading, failed, error, settled, run }
|
|
90
|
+
}
|