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