@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,479 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
|
3
|
+
import { useDmsRoute, useDmsRouter, useI18n } from '#dms/frontend-module'
|
|
4
|
+
import {
|
|
5
|
+
MARKETING_CONTEXT_KEY,
|
|
6
|
+
type ContextWebsite,
|
|
7
|
+
type MarketingContextPayload,
|
|
8
|
+
} from '../composables/useMarketingContext'
|
|
9
|
+
import { useMarketingApi } from '../composables/useMarketingApi'
|
|
10
|
+
import { installLink } from '../composables/useMarketingRoutes'
|
|
11
|
+
import { formatRelativeTime, initials } from '../utils/format'
|
|
12
|
+
import { MAX_PERIOD_DAYS, MS_PER_DAY, PERIOD_SCOPE } from '../constants'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The context bar of every analytics page and the gate in front of its blocks.
|
|
16
|
+
*
|
|
17
|
+
* The page URL is the context: a reload, a copied link or Back opens on what
|
|
18
|
+
* it names, and each tab keeps its own. The period is the DMS
|
|
19
|
+
* `PeriodSelector` in the bar, publishing under `periodScope`, the scope every
|
|
20
|
+
* block of the page binds to: it keeps the period in the URL
|
|
21
|
+
* (`?<scope>.period=…&<scope>.compare=…`, `<scope>.from`/`.to` for a custom
|
|
22
|
+
* range) and the user's last choice. The website is this bar's own
|
|
23
|
+
* (`?website=`): the blocks read it through a `{{query.website}}` token, so a
|
|
24
|
+
* switch reads them again.
|
|
25
|
+
*
|
|
26
|
+
* What the URL leaves out comes from the reader's last explicit pick: the
|
|
27
|
+
* selector's remembered period and the website saved server-side. Only a pick
|
|
28
|
+
* in the bar writes the website, so opening a link never moves another tab.
|
|
29
|
+
*
|
|
30
|
+
* Its children render once the URL names one of the tenant's websites; with
|
|
31
|
+
* no website at all the first-run hero takes their place, and with
|
|
32
|
+
* `waitingGuide` a website still waiting for its first event shows the way
|
|
33
|
+
* to its install guide instead.
|
|
34
|
+
*/
|
|
35
|
+
const props = withDefaults(
|
|
36
|
+
defineProps<{
|
|
37
|
+
periodScope?: string
|
|
38
|
+
waitingGuide?: boolean
|
|
39
|
+
componentId?: string
|
|
40
|
+
pageId?: string
|
|
41
|
+
}>(),
|
|
42
|
+
{
|
|
43
|
+
periodScope: PERIOD_SCOPE,
|
|
44
|
+
waitingGuide: false,
|
|
45
|
+
componentId: undefined,
|
|
46
|
+
pageId: undefined,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const PRESETS = [
|
|
51
|
+
'today',
|
|
52
|
+
'last-7-days',
|
|
53
|
+
'last-30-days',
|
|
54
|
+
'last-90-days',
|
|
55
|
+
'custom',
|
|
56
|
+
]
|
|
57
|
+
const COMPARISONS = ['previous-period', 'previous-year', 'none']
|
|
58
|
+
const CLOCK_TICK_MS = 15_000
|
|
59
|
+
/**
|
|
60
|
+
* How often the blocks of a period reaching today and the websites' last
|
|
61
|
+
* events are read again (the pages' `live` option caps the blocks alike).
|
|
62
|
+
*/
|
|
63
|
+
const LIVE_REFRESH_MS = 60_000
|
|
64
|
+
/** A website is live while its last event is this recent. */
|
|
65
|
+
const LIVE_WINDOW_MS = 30 * 60 * 1000
|
|
66
|
+
|
|
67
|
+
const { t, locale } = useI18n()
|
|
68
|
+
const api = useMarketingApi()
|
|
69
|
+
const toast = useToast()
|
|
70
|
+
const route = useDmsRoute()
|
|
71
|
+
const router = useDmsRouter()
|
|
72
|
+
|
|
73
|
+
const presetLabels = computed(() =>
|
|
74
|
+
Object.fromEntries(
|
|
75
|
+
PRESETS.map((preset) => [
|
|
76
|
+
preset,
|
|
77
|
+
t(`page.marketing.context.presets.${preset}`),
|
|
78
|
+
]),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const comparisonLabels = computed(() =>
|
|
83
|
+
Object.fromEntries(
|
|
84
|
+
COMPARISONS.map((comparison) => [
|
|
85
|
+
comparison,
|
|
86
|
+
t(`page.marketing.context.comparisons.${comparison}`),
|
|
87
|
+
]),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const context = ref<MarketingContextPayload | null>(null)
|
|
92
|
+
const loading = ref(true)
|
|
93
|
+
const failed = ref(false)
|
|
94
|
+
const switching = ref(false)
|
|
95
|
+
const refreshToken = ref(0)
|
|
96
|
+
|
|
97
|
+
const websites = computed(() => context.value?.websites ?? [])
|
|
98
|
+
/** The URL's website, while it is one of the tenant's. */
|
|
99
|
+
const linkedId = computed(() => {
|
|
100
|
+
const id = route.query.website
|
|
101
|
+
return typeof id === 'string' && websites.value.some((site) => site.id === id)
|
|
102
|
+
? id
|
|
103
|
+
: null
|
|
104
|
+
})
|
|
105
|
+
const selectedId = computed(
|
|
106
|
+
() => linkedId.value ?? context.value?.selectedId ?? null,
|
|
107
|
+
)
|
|
108
|
+
const selected = computed<ContextWebsite | null>(
|
|
109
|
+
() => websites.value.find((site) => site.id === selectedId.value) ?? null,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async function reload(): Promise<void> {
|
|
113
|
+
loading.value = true
|
|
114
|
+
failed.value = false
|
|
115
|
+
try {
|
|
116
|
+
context.value = await api.getContext()
|
|
117
|
+
} catch {
|
|
118
|
+
failed.value = true
|
|
119
|
+
} finally {
|
|
120
|
+
loading.value = false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The websites' last events again, quietly: a failure keeps the bar as is. */
|
|
125
|
+
async function refreshActivity(): Promise<void> {
|
|
126
|
+
try {
|
|
127
|
+
context.value = await api.getContext()
|
|
128
|
+
} catch {
|
|
129
|
+
// The next refresh tries again.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function select(id: string): Promise<void> {
|
|
134
|
+
if (id === selectedId.value) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
switching.value = true
|
|
138
|
+
try {
|
|
139
|
+
// Saved first, as the website of the pages opened without one; the bar
|
|
140
|
+
// and the blocks follow the URL once it names it. A pick is a history
|
|
141
|
+
// entry, so Back takes it back.
|
|
142
|
+
context.value = await api.selectContextWebsite(id)
|
|
143
|
+
await router.push({ query: { ...route.query, website: id } })
|
|
144
|
+
} catch {
|
|
145
|
+
toast.add({
|
|
146
|
+
color: 'error',
|
|
147
|
+
title: t('page.marketing.context.switch_error'),
|
|
148
|
+
})
|
|
149
|
+
await reload()
|
|
150
|
+
} finally {
|
|
151
|
+
switching.value = false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The URL names no website, or one the tenant does not have: fill in the
|
|
156
|
+
// saved one, without a history entry. Watched with the query too, so a block
|
|
157
|
+
// rewriting the URL for its own state (the heatmap path) keeps it.
|
|
158
|
+
watch(
|
|
159
|
+
[selectedId, () => route.query],
|
|
160
|
+
([id]) => {
|
|
161
|
+
if (id && route.query.website !== id) {
|
|
162
|
+
void router.replace({ query: { ...route.query, website: id } })
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{ immediate: true },
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
watch(refreshToken, () => refreshPageBlocks())
|
|
169
|
+
|
|
170
|
+
// --- Period --------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
const period = usePeriodScope(props.periodScope)
|
|
173
|
+
|
|
174
|
+
// Read once the bar shows the selector: the scope registers with it, and a
|
|
175
|
+
// read before has the DMS warn about a missing scope.
|
|
176
|
+
const range = computed(() => (selected.value ? period.value?.range : null))
|
|
177
|
+
|
|
178
|
+
/** A custom range longer than the routes read: only its last days are shown. */
|
|
179
|
+
const cutHint = computed(() => {
|
|
180
|
+
if (!range.value) {
|
|
181
|
+
return ''
|
|
182
|
+
}
|
|
183
|
+
const { from, to } = range.value
|
|
184
|
+
const days = Math.round((to.getTime() - from.getTime()) / MS_PER_DAY)
|
|
185
|
+
return days > MAX_PERIOD_DAYS
|
|
186
|
+
? t('page.marketing.context.range_cut', { days: MAX_PERIOD_DAYS })
|
|
187
|
+
: ''
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
provide(MARKETING_CONTEXT_KEY, {
|
|
191
|
+
websites,
|
|
192
|
+
selected,
|
|
193
|
+
refreshToken,
|
|
194
|
+
select,
|
|
195
|
+
reload,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
onMounted(reload)
|
|
199
|
+
|
|
200
|
+
// --- Clock -------------------------------------------------------------------
|
|
201
|
+
// A period reaching today follows the clock: its blocks read again every
|
|
202
|
+
// minute, through the page's live gate. The selector itself moves a day
|
|
203
|
+
// preset on at midnight.
|
|
204
|
+
|
|
205
|
+
const now = ref(Date.now())
|
|
206
|
+
|
|
207
|
+
/** The period covers the present: its counts are still moving. */
|
|
208
|
+
const reachesToday = computed(
|
|
209
|
+
() => !!range.value && range.value.to.getTime() >= now.value,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
/** Refresh: every block reads again. */
|
|
213
|
+
function refresh(): void {
|
|
214
|
+
refreshToken.value++
|
|
215
|
+
void refreshActivity()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function tick(): void {
|
|
219
|
+
now.value = Date.now()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function liveRefresh(): void {
|
|
223
|
+
if (reachesToday.value && linkedId.value) {
|
|
224
|
+
refreshLivePageBlocks()
|
|
225
|
+
}
|
|
226
|
+
void refreshActivity()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let clock: ReturnType<typeof setInterval> | null = null
|
|
230
|
+
let live: ReturnType<typeof setInterval> | null = null
|
|
231
|
+
onMounted(() => {
|
|
232
|
+
clock = setInterval(tick, CLOCK_TICK_MS)
|
|
233
|
+
live = setInterval(liveRefresh, LIVE_REFRESH_MS)
|
|
234
|
+
})
|
|
235
|
+
onBeforeUnmount(() => {
|
|
236
|
+
if (clock) {
|
|
237
|
+
clearInterval(clock)
|
|
238
|
+
}
|
|
239
|
+
if (live) {
|
|
240
|
+
clearInterval(live)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// --- Bar ---------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
/** `live` only while events are recent; a quiet site is `idle`. */
|
|
247
|
+
type DisplayState = ContextWebsite['state'] | 'idle'
|
|
248
|
+
|
|
249
|
+
const STATE_TONES: Record<DisplayState, string> = {
|
|
250
|
+
live: 'bg-success',
|
|
251
|
+
idle: 'bg-(--ui-text-muted)',
|
|
252
|
+
waiting: 'bg-warning',
|
|
253
|
+
paused: 'bg-(--ui-text-dimmed)',
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function displayState(site: ContextWebsite): DisplayState {
|
|
257
|
+
if (
|
|
258
|
+
site.state === 'live' &&
|
|
259
|
+
(site.lastActivityAt === null ||
|
|
260
|
+
now.value - site.lastActivityAt > LIVE_WINDOW_MS)
|
|
261
|
+
) {
|
|
262
|
+
return 'idle'
|
|
263
|
+
}
|
|
264
|
+
return site.state
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function stateLine(site: ContextWebsite): string {
|
|
268
|
+
const state = displayState(site)
|
|
269
|
+
if ((state === 'live' || state === 'idle') && site.lastActivityAt) {
|
|
270
|
+
return t(`page.marketing.context.${state}`, {
|
|
271
|
+
ago: formatRelativeTime(site.lastActivityAt, now.value, locale.value),
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
return t(`page.marketing.context.state.${site.state}`)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const websiteItems = computed(() => [
|
|
278
|
+
websites.value.map((site) => ({
|
|
279
|
+
label: site.name,
|
|
280
|
+
description: site.domain,
|
|
281
|
+
type: 'checkbox' as const,
|
|
282
|
+
checked: site.id === selectedId.value,
|
|
283
|
+
slot: 'website' as const,
|
|
284
|
+
site,
|
|
285
|
+
onSelect: () => void select(site.id),
|
|
286
|
+
})),
|
|
287
|
+
[
|
|
288
|
+
{
|
|
289
|
+
label: t('page.marketing.context.add_website'),
|
|
290
|
+
icon: 'i-ph-plus',
|
|
291
|
+
to: installLink(),
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The statistics are kept per UTC day: where the reader's midnight is not
|
|
298
|
+
* UTC's, the bar says when their days really start.
|
|
299
|
+
*/
|
|
300
|
+
const utcHint = computed(() => {
|
|
301
|
+
const today = new Date(now.value)
|
|
302
|
+
if (today.getTimezoneOffset() === 0) {
|
|
303
|
+
return ''
|
|
304
|
+
}
|
|
305
|
+
const utcMidnight = new Date(
|
|
306
|
+
Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()),
|
|
307
|
+
)
|
|
308
|
+
const time = new Intl.DateTimeFormat(locale.value, {
|
|
309
|
+
hour: '2-digit',
|
|
310
|
+
minute: '2-digit',
|
|
311
|
+
}).format(utcMidnight)
|
|
312
|
+
return t('page.marketing.context.utc_days', { time })
|
|
313
|
+
})
|
|
314
|
+
</script>
|
|
315
|
+
|
|
316
|
+
<template>
|
|
317
|
+
<div class="flex flex-col gap-4">
|
|
318
|
+
<div
|
|
319
|
+
class="dms-card flex flex-wrap items-center gap-x-4 gap-y-2 px-3 py-2"
|
|
320
|
+
role="toolbar"
|
|
321
|
+
:aria-label="t('page.marketing.context.label')"
|
|
322
|
+
>
|
|
323
|
+
<USkeleton v-if="loading && !context" class="h-7 w-72" />
|
|
324
|
+
<template v-else-if="selected">
|
|
325
|
+
<UDropdownMenu :items="websiteItems" :content="{ align: 'start' }">
|
|
326
|
+
<UButton
|
|
327
|
+
color="neutral"
|
|
328
|
+
variant="outline"
|
|
329
|
+
size="sm"
|
|
330
|
+
trailing-icon="i-ph-caret-up-down"
|
|
331
|
+
:loading="switching"
|
|
332
|
+
:aria-label="t('page.marketing.context.switch_website')"
|
|
333
|
+
>
|
|
334
|
+
<span
|
|
335
|
+
class="grid size-5 place-items-center rounded bg-primary/15 font-mono text-[10px] font-semibold text-(--ui-color-primary-800) dark:text-primary"
|
|
336
|
+
>
|
|
337
|
+
{{ initials(selected.name) }}
|
|
338
|
+
</span>
|
|
339
|
+
<span class="font-semibold">{{ selected.name }}</span>
|
|
340
|
+
</UButton>
|
|
341
|
+
<template #website="{ item }">
|
|
342
|
+
<span
|
|
343
|
+
class="grid size-6 shrink-0 place-items-center rounded bg-primary/15 font-mono text-[10px] font-semibold text-(--ui-color-primary-800) dark:text-primary"
|
|
344
|
+
>
|
|
345
|
+
{{ initials(item.site.name) }}
|
|
346
|
+
</span>
|
|
347
|
+
<span class="min-w-0 flex-1 text-left">
|
|
348
|
+
<span class="block truncate text-sm font-medium text-highlighted">
|
|
349
|
+
{{ item.site.name }}
|
|
350
|
+
</span>
|
|
351
|
+
<span
|
|
352
|
+
class="flex min-w-0 items-center gap-1.5 text-xs text-dimmed"
|
|
353
|
+
>
|
|
354
|
+
<span
|
|
355
|
+
class="size-1.5 shrink-0 rounded-full"
|
|
356
|
+
:class="STATE_TONES[displayState(item.site)]"
|
|
357
|
+
aria-hidden="true"
|
|
358
|
+
/>
|
|
359
|
+
<span class="truncate font-mono">{{ item.site.domain }}</span>
|
|
360
|
+
<span class="shrink-0">· {{ stateLine(item.site) }}</span>
|
|
361
|
+
</span>
|
|
362
|
+
</span>
|
|
363
|
+
<UIcon
|
|
364
|
+
v-if="item.checked"
|
|
365
|
+
name="i-ph-check"
|
|
366
|
+
class="size-4 shrink-0 text-primary"
|
|
367
|
+
/>
|
|
368
|
+
</template>
|
|
369
|
+
</UDropdownMenu>
|
|
370
|
+
|
|
371
|
+
<span class="flex items-center gap-2 font-mono text-xs text-muted">
|
|
372
|
+
<span
|
|
373
|
+
class="size-2 rounded-full"
|
|
374
|
+
:class="STATE_TONES[displayState(selected)]"
|
|
375
|
+
aria-hidden="true"
|
|
376
|
+
/>
|
|
377
|
+
{{ stateLine(selected) }}
|
|
378
|
+
</span>
|
|
379
|
+
|
|
380
|
+
<span class="hidden h-5 w-px bg-(--ui-border) md:block" />
|
|
381
|
+
|
|
382
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
383
|
+
<DmsPeriodSelector
|
|
384
|
+
:id="periodScope"
|
|
385
|
+
:component-id="`${componentId ?? 'marketing-context'}-period`"
|
|
386
|
+
:page-id="pageId"
|
|
387
|
+
variant="segmented"
|
|
388
|
+
size="xs"
|
|
389
|
+
align="left"
|
|
390
|
+
default-preset="last-30-days"
|
|
391
|
+
default-comparison="previous-period"
|
|
392
|
+
:presets="PRESETS"
|
|
393
|
+
:comparisons="COMPARISONS"
|
|
394
|
+
:preset-labels="presetLabels"
|
|
395
|
+
:comparison-labels="comparisonLabels"
|
|
396
|
+
show-range-label
|
|
397
|
+
/>
|
|
398
|
+
<span
|
|
399
|
+
v-if="cutHint"
|
|
400
|
+
class="flex items-center gap-1 text-xs text-warning"
|
|
401
|
+
role="status"
|
|
402
|
+
>
|
|
403
|
+
<UIcon name="i-ph-warning" class="size-3.5 shrink-0" />
|
|
404
|
+
{{ cutHint }}
|
|
405
|
+
</span>
|
|
406
|
+
<UTooltip v-if="utcHint" :text="utcHint">
|
|
407
|
+
<span
|
|
408
|
+
class="cursor-help rounded border border-default px-1 font-mono text-[10px] text-dimmed"
|
|
409
|
+
tabindex="0"
|
|
410
|
+
:aria-label="utcHint"
|
|
411
|
+
>
|
|
412
|
+
UTC
|
|
413
|
+
</span>
|
|
414
|
+
</UTooltip>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<UButton
|
|
418
|
+
class="ms-auto"
|
|
419
|
+
icon="i-ph-arrow-clockwise"
|
|
420
|
+
color="neutral"
|
|
421
|
+
variant="ghost"
|
|
422
|
+
size="xs"
|
|
423
|
+
:aria-label="t('page.marketing.context.refresh')"
|
|
424
|
+
@click="refresh"
|
|
425
|
+
/>
|
|
426
|
+
</template>
|
|
427
|
+
<span v-else-if="!failed" class="text-sm text-muted">
|
|
428
|
+
{{ t('page.marketing.context.no_website') }}
|
|
429
|
+
</span>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<DmsEmptyState
|
|
433
|
+
v-if="failed"
|
|
434
|
+
variant="error"
|
|
435
|
+
:title="t('page.marketing.context.error.title')"
|
|
436
|
+
:description="t('page.marketing.context.error.description')"
|
|
437
|
+
class="dms-card"
|
|
438
|
+
>
|
|
439
|
+
<template #actions>
|
|
440
|
+
<UButton
|
|
441
|
+
icon="i-ph-arrow-clockwise"
|
|
442
|
+
color="neutral"
|
|
443
|
+
variant="outline"
|
|
444
|
+
size="sm"
|
|
445
|
+
:label="t('page.marketing.common.retry')"
|
|
446
|
+
@click="reload"
|
|
447
|
+
/>
|
|
448
|
+
</template>
|
|
449
|
+
</DmsEmptyState>
|
|
450
|
+
|
|
451
|
+
<DmsMarketingFirstRun v-else-if="context && websites.length === 0" />
|
|
452
|
+
|
|
453
|
+
<DmsEmptyState
|
|
454
|
+
v-else-if="waitingGuide && linkedId && selected?.state === 'waiting'"
|
|
455
|
+
icon="i-ph-plugs"
|
|
456
|
+
:title="
|
|
457
|
+
t('page.marketing.context.waiting_guide.title', { name: selected.name })
|
|
458
|
+
"
|
|
459
|
+
:description="t('page.marketing.context.waiting_guide.description')"
|
|
460
|
+
class="dms-card"
|
|
461
|
+
>
|
|
462
|
+
<template #actions>
|
|
463
|
+
<UButton
|
|
464
|
+
icon="i-ph-book-open-text"
|
|
465
|
+
size="sm"
|
|
466
|
+
:label="t('page.marketing.context.waiting_guide.action')"
|
|
467
|
+
@click="router.push(installLink(selected.id))"
|
|
468
|
+
/>
|
|
469
|
+
</template>
|
|
470
|
+
</DmsEmptyState>
|
|
471
|
+
|
|
472
|
+
<slot v-else-if="linkedId" />
|
|
473
|
+
|
|
474
|
+
<div v-else class="grid gap-4 md:grid-cols-4">
|
|
475
|
+
<USkeleton v-for="index in 4" :key="index" class="h-28 rounded-xl" />
|
|
476
|
+
<USkeleton class="h-72 rounded-xl md:col-span-4" />
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
</template>
|
|
@@ -1,6 +1,7 @@
|
|
|
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
|
parseExperimentValue,
|
|
6
7
|
type ExperimentDraft,
|
|
@@ -57,7 +58,7 @@ function currentValue(): ExperimentDraft | null {
|
|
|
57
58
|
}
|
|
58
59
|
return {
|
|
59
60
|
...draft.value,
|
|
60
|
-
variations: draft.value.variations.map(variation => ({ ...variation })),
|
|
61
|
+
variations: draft.value.variations.map((variation) => ({ ...variation })),
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -98,6 +99,7 @@ watch(
|
|
|
98
99
|
<UInput
|
|
99
100
|
v-model="draft.key"
|
|
100
101
|
:placeholder="t('page.marketing.funnels.form.key')"
|
|
102
|
+
:maxlength="MAX_EXPERIMENT_KEY_LENGTH"
|
|
101
103
|
:disabled="inputsDisabled"
|
|
102
104
|
class="font-mono"
|
|
103
105
|
size="sm"
|