@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
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import {
|
|
4
|
-
useDmsRoute,
|
|
5
|
-
useDmsRouter,
|
|
6
|
-
useI18n,
|
|
7
|
-
} from '#dms/frontend-module'
|
|
8
|
-
import { useChildId } from '../composables/useChildId'
|
|
9
|
-
import { useLatestRequest } from '../composables/useLatestRequest'
|
|
10
|
-
import {
|
|
11
|
-
useMarketingApi,
|
|
12
|
-
type MarketingPages,
|
|
13
|
-
} from '../composables/useMarketingApi'
|
|
14
|
-
import {
|
|
15
|
-
MARKETING_PERIOD_PRESETS,
|
|
16
|
-
ndToPreset,
|
|
17
|
-
useMarketingPeriod,
|
|
18
|
-
} from '../composables/useMarketingPeriod'
|
|
19
|
-
import { funnelsLink, pagesLink } from '../composables/useMarketingRoutes'
|
|
20
|
-
import { useMarketingWebsites } from '../composables/useMarketingWebsites'
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Tracked pages of a site: the inventory on the left, the clicks and scroll
|
|
24
|
-
* depth of the selected page on the right.
|
|
25
|
-
*
|
|
26
|
-
* The surface is deliberately keyed on nothing but a website id and a bare
|
|
27
|
-
* pathname the tracker itself reported — no route registry, no sitemap, no
|
|
28
|
-
* build manifest, no framework convention. That is what makes it hold for any
|
|
29
|
-
* tracked site, whatever it is built with.
|
|
30
|
-
*
|
|
31
|
-
* The search box is also a path field, and that is not a fallback: a day keeps
|
|
32
|
-
* at most MAX_ENTRIES_PER_TOP_MAP paths and older days keep counters only, so
|
|
33
|
-
* a page can be missing from the inventory while its clicks are perfectly
|
|
34
|
-
* queryable — the backend matches the path exactly either way.
|
|
35
|
-
*/
|
|
36
|
-
const PERIOD_SCOPE = 'dms-marketing-pages'
|
|
37
|
-
const SEARCH_DEBOUNCE_MS = 300
|
|
38
|
-
|
|
39
|
-
// Declared, not inherited as attributes: the DMS page renderer hands every
|
|
40
|
-
// component its identity, and an undeclared prop lands on the root element.
|
|
41
|
-
const props = withDefaults(
|
|
42
|
-
defineProps<{ componentId?: string, pageId?: string }>(),
|
|
43
|
-
{ componentId: 'marketing-pages', pageId: 'marketing' },
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
const childId = useChildId(() => props.componentId)
|
|
47
|
-
|
|
48
|
-
const api = useMarketingApi()
|
|
49
|
-
const { t } = useI18n()
|
|
50
|
-
const route = useDmsRoute()
|
|
51
|
-
const router = useDmsRouter()
|
|
52
|
-
|
|
53
|
-
const queryParam = (key: string): string => {
|
|
54
|
-
const value = route.query[key]
|
|
55
|
-
return typeof value === 'string' ? value : ''
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Read once: the selector seeds its preset at init and never reactively, so a
|
|
59
|
-
// deep link's window has to be resolved before it renders.
|
|
60
|
-
const initialPreset = ndToPreset(queryParam('period'))
|
|
61
|
-
|
|
62
|
-
const {
|
|
63
|
-
websites,
|
|
64
|
-
items: websiteItems,
|
|
65
|
-
selectedId: selectedWebsiteId,
|
|
66
|
-
selected: selectedWebsite,
|
|
67
|
-
loading: loadingWebsites,
|
|
68
|
-
failed: websitesFailed,
|
|
69
|
-
load: loadWebsites,
|
|
70
|
-
create: createWebsite,
|
|
71
|
-
} = useMarketingWebsites(queryParam('website'))
|
|
72
|
-
|
|
73
|
-
// `undefined`, not null: this is what DmsMasterDetail's model is typed on.
|
|
74
|
-
const selectedPath = ref<string | undefined>(queryParam('path') || undefined)
|
|
75
|
-
const search = ref('')
|
|
76
|
-
|
|
77
|
-
const period = useMarketingPeriod(PERIOD_SCOPE)
|
|
78
|
-
|
|
79
|
-
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
|
80
|
-
|
|
81
|
-
const {
|
|
82
|
-
data: inventory,
|
|
83
|
-
loading: loadingPages,
|
|
84
|
-
settled: pagesSettled,
|
|
85
|
-
failed: pagesFailed,
|
|
86
|
-
run: loadPages,
|
|
87
|
-
} = useLatestRequest<MarketingPages>(() =>
|
|
88
|
-
// Server-side there is no session to authenticate with: the fetch belongs to
|
|
89
|
-
// the client, like every other read here.
|
|
90
|
-
selectedWebsiteId.value && !import.meta.env.SSR
|
|
91
|
-
? api.listPages(selectedWebsiteId.value, period.value, search.value.trim())
|
|
92
|
-
: null,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
const pages = computed(() => inventory.value?.pages ?? [])
|
|
96
|
-
const truncated = computed(() => inventory.value?.truncated ?? false)
|
|
97
|
-
const sampleRate = computed(() => inventory.value?.heatmapSampleRate ?? 0)
|
|
98
|
-
const trackerEnabled = computed(() => inventory.value?.trackerEnabled ?? true)
|
|
99
|
-
|
|
100
|
-
// Two failures, two flags: the inventory fetch clears its own error on every
|
|
101
|
-
// attempt, and must not clear the one that says the site list never loaded.
|
|
102
|
-
const failed = computed(() => websitesFailed.value || pagesFailed.value)
|
|
103
|
-
|
|
104
|
-
// A selected site means an inventory request is expected. Keep the surface
|
|
105
|
-
// visibly loading through the first answer instead of rendering an empty
|
|
106
|
-
// master/detail shell while websites, pages and the preview requests settle.
|
|
107
|
-
const loading = computed(
|
|
108
|
-
() =>
|
|
109
|
-
loadingWebsites.value
|
|
110
|
-
|| loadingPages.value
|
|
111
|
-
|| (!!selectedWebsiteId.value && !pagesSettled.value),
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
// Land on something rather than on an empty pane, but never override a path
|
|
115
|
-
// that was linked to or typed. Watching the answer rather than writing from
|
|
116
|
-
// the fetch keeps superseded ones out: only the newest ever reaches here.
|
|
117
|
-
watch(inventory, (data) => {
|
|
118
|
-
if (!selectedPath.value) {
|
|
119
|
-
selectedPath.value = data?.pages[0]?.path
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Retry entry point. Both fetches, in order, because either can be the one
|
|
125
|
-
* that failed: on a deep link the inventory runs even when the site list did
|
|
126
|
-
* not, and reloading the list alone leaves the selection unchanged, so the
|
|
127
|
-
* watcher never fires and the inventory is never retried.
|
|
128
|
-
*/
|
|
129
|
-
async function reload() {
|
|
130
|
-
if (websites.value.length === 0) {
|
|
131
|
-
await loadWebsites()
|
|
132
|
-
}
|
|
133
|
-
await loadPages()
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
onMounted(loadWebsites)
|
|
137
|
-
|
|
138
|
-
// The watcher owns every inventory fetch, including the first: seeding it from
|
|
139
|
-
// onMounted as well would issue the initial request twice on a bare link, and
|
|
140
|
-
// not at all on a link that already names the site the list would have picked.
|
|
141
|
-
watch([selectedWebsiteId, period], ([websiteId], previous) => {
|
|
142
|
-
// Switching site invalidates the path — the same URL on another site is
|
|
143
|
-
// another page, and usually does not exist there. Only a real site-to-site
|
|
144
|
-
// change may clear: the immediate run hands multi-source watchers an EMPTY
|
|
145
|
-
// previous array (truthy — `previous &&` does not guard it), and the site
|
|
146
|
-
// list resolving undefined → id is not a switch either. Both carry a linked
|
|
147
|
-
// path that must survive.
|
|
148
|
-
const previousWebsiteId = previous?.[0]
|
|
149
|
-
if (previousWebsiteId !== undefined && websiteId !== previousWebsiteId) {
|
|
150
|
-
selectedPath.value = undefined
|
|
151
|
-
}
|
|
152
|
-
void loadPages()
|
|
153
|
-
}, { immediate: true })
|
|
154
|
-
|
|
155
|
-
watch(search, () => {
|
|
156
|
-
if (searchTimer) {
|
|
157
|
-
clearTimeout(searchTimer)
|
|
158
|
-
}
|
|
159
|
-
searchTimer = setTimeout(() => void loadPages(), SEARCH_DEBOUNCE_MS)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
onBeforeUnmount(() => {
|
|
163
|
-
if (searchTimer) {
|
|
164
|
-
clearTimeout(searchTimer)
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
/** Enter inspects what was typed, whether or not the inventory lists it. */
|
|
169
|
-
function inspectTyped(): void {
|
|
170
|
-
const typed = search.value.trim()
|
|
171
|
-
if (!typed.startsWith('/')) {
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
selectedPath.value = typed
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// State lives in the URL so a heatmap can be linked to. Query-only navigation
|
|
178
|
-
// does not remount the page, so this is free.
|
|
179
|
-
watch([selectedWebsiteId, selectedPath, period], () => {
|
|
180
|
-
void router.replace(
|
|
181
|
-
pagesLink({
|
|
182
|
-
website: selectedWebsiteId.value,
|
|
183
|
-
path: selectedPath.value,
|
|
184
|
-
period: period.value,
|
|
185
|
-
}),
|
|
186
|
-
)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const pageviewsBySelected = computed(
|
|
190
|
-
() => pages.value.find(page => page.path === selectedPath.value)?.pageviews,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
const listItems = computed(() =>
|
|
194
|
-
pages.value.map((page) => {
|
|
195
|
-
// Sampled PAGE LOADS, not clicks: the tracker draws once per page load,
|
|
196
|
-
// so this says whether a heatmap can exist at all — a click estimate here
|
|
197
|
-
// would contradict the real count shown beside it in the detail pane.
|
|
198
|
-
const sampled = Math.round(page.pageviews * sampleRate.value)
|
|
199
|
-
// Two counts, two plural choices: one message could only ever inflect on
|
|
200
|
-
// one of them, and the two rarely agree.
|
|
201
|
-
return {
|
|
202
|
-
value: page.path,
|
|
203
|
-
label: page.path,
|
|
204
|
-
sublabel: [
|
|
205
|
-
t('page.marketing.pages.row_hint.views', { count: page.pageviews }, page.pageviews),
|
|
206
|
-
t('page.marketing.pages.row_hint.sampled', { count: sampled }, sampled),
|
|
207
|
-
].join(' · '),
|
|
208
|
-
}
|
|
209
|
-
}),
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
// --- Site-wide reset --------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
const { confirm } = useConfirm()
|
|
215
|
-
const toast = useToast()
|
|
216
|
-
const resettingHeatmaps = ref(false)
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* The detail pane refetches on this bump: a site-wide reset changes none of
|
|
220
|
-
* the keys it watches (site, path, period), so without it the pane would keep
|
|
221
|
-
* showing clicks that no longer exist. The inventory needs no reload — its
|
|
222
|
-
* pageview counts come from the rollup, which a click purge never touches.
|
|
223
|
-
*/
|
|
224
|
-
const heatmapRefreshToken = ref(0)
|
|
225
|
-
|
|
226
|
-
async function resetSiteHeatmaps() {
|
|
227
|
-
const site = selectedWebsite.value
|
|
228
|
-
if (!site) {
|
|
229
|
-
return
|
|
230
|
-
}
|
|
231
|
-
const confirmed = await confirm({
|
|
232
|
-
title: t('page.marketing.pages.reset.site_title'),
|
|
233
|
-
description: t('page.marketing.pages.reset.site_description', { name: site.name }),
|
|
234
|
-
confirmLabel: t('page.marketing.pages.reset.confirm'),
|
|
235
|
-
confirmColor: 'error',
|
|
236
|
-
})
|
|
237
|
-
if (!confirmed) {
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
resettingHeatmaps.value = true
|
|
241
|
-
try {
|
|
242
|
-
const { deleted } = await api.resetHeatmap(site._id)
|
|
243
|
-
toast.add({
|
|
244
|
-
color: 'success',
|
|
245
|
-
title: t('page.marketing.pages.reset.done', { count: deleted }, deleted),
|
|
246
|
-
})
|
|
247
|
-
heatmapRefreshToken.value++
|
|
248
|
-
}
|
|
249
|
-
catch {
|
|
250
|
-
toast.add({ color: 'error', title: t('page.marketing.pages.reset.error') })
|
|
251
|
-
}
|
|
252
|
-
finally {
|
|
253
|
-
resettingHeatmaps.value = false
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
</script>
|
|
257
|
-
|
|
258
|
-
<template>
|
|
259
|
-
<div class="space-y-6 p-6">
|
|
260
|
-
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
261
|
-
<div>
|
|
262
|
-
<h1 class="text-xl font-semibold">{{ t('page.marketing.pages.title') }}</h1>
|
|
263
|
-
<p class="text-sm text-muted">{{ t('page.marketing.pages.subtitle') }}</p>
|
|
264
|
-
</div>
|
|
265
|
-
<div class="flex flex-wrap items-center gap-3">
|
|
266
|
-
<USelect
|
|
267
|
-
v-if="websites.length > 1"
|
|
268
|
-
v-model="selectedWebsiteId"
|
|
269
|
-
:items="websiteItems"
|
|
270
|
-
class="w-56"
|
|
271
|
-
/>
|
|
272
|
-
<UInput
|
|
273
|
-
v-model="search"
|
|
274
|
-
icon="i-ph-magnifying-glass"
|
|
275
|
-
class="w-64"
|
|
276
|
-
:placeholder="t('page.marketing.pages.search_placeholder')"
|
|
277
|
-
@keyup.enter="inspectTyped"
|
|
278
|
-
/>
|
|
279
|
-
<DmsPeriodSelector
|
|
280
|
-
:id="PERIOD_SCOPE"
|
|
281
|
-
:component-id="childId('period')"
|
|
282
|
-
:page-id="props.pageId"
|
|
283
|
-
:default-preset="initialPreset"
|
|
284
|
-
:presets="MARKETING_PERIOD_PRESETS"
|
|
285
|
-
:comparisons="['none']"
|
|
286
|
-
:show-range-label="false"
|
|
287
|
-
/>
|
|
288
|
-
<UButton
|
|
289
|
-
v-if="selectedWebsiteId"
|
|
290
|
-
icon="i-ph-funnel"
|
|
291
|
-
color="neutral"
|
|
292
|
-
variant="ghost"
|
|
293
|
-
size="sm"
|
|
294
|
-
:to="funnelsLink({ website: selectedWebsiteId })"
|
|
295
|
-
:label="t('page.marketing.pages.to_funnels')"
|
|
296
|
-
/>
|
|
297
|
-
<UButton
|
|
298
|
-
v-if="selectedWebsiteId"
|
|
299
|
-
icon="i-ph-eraser"
|
|
300
|
-
color="neutral"
|
|
301
|
-
variant="ghost"
|
|
302
|
-
size="sm"
|
|
303
|
-
:loading="resettingHeatmaps"
|
|
304
|
-
:label="t('page.marketing.pages.reset.site_button')"
|
|
305
|
-
@click="resetSiteHeatmaps"
|
|
306
|
-
/>
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
|
|
310
|
-
<UAlert
|
|
311
|
-
v-if="!trackerEnabled"
|
|
312
|
-
icon="i-ph-plugs"
|
|
313
|
-
color="warning"
|
|
314
|
-
variant="subtle"
|
|
315
|
-
:title="t('page.marketing.pages.tracking_off.title')"
|
|
316
|
-
:description="t('page.marketing.pages.tracking_off.description')"
|
|
317
|
-
/>
|
|
318
|
-
|
|
319
|
-
<div v-if="failed" class="flex flex-col items-start gap-3">
|
|
320
|
-
<UAlert
|
|
321
|
-
color="error"
|
|
322
|
-
variant="subtle"
|
|
323
|
-
icon="i-ph-warning-circle"
|
|
324
|
-
:title="t('page.marketing.pages.error')"
|
|
325
|
-
:description="t('page.marketing.pages.error_hint')"
|
|
326
|
-
/>
|
|
327
|
-
<UButton
|
|
328
|
-
icon="i-ph-arrow-clockwise"
|
|
329
|
-
:label="t('page.marketing.pages.retry')"
|
|
330
|
-
color="neutral"
|
|
331
|
-
variant="subtle"
|
|
332
|
-
size="sm"
|
|
333
|
-
@click="reload"
|
|
334
|
-
/>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<USkeleton v-else-if="loadingWebsites" class="h-96 w-full" />
|
|
338
|
-
|
|
339
|
-
<DmsMarketingNoWebsiteState
|
|
340
|
-
v-else-if="websites.length === 0"
|
|
341
|
-
icon="i-ph-globe"
|
|
342
|
-
:title="t('page.marketing.pages.no_website.title')"
|
|
343
|
-
:description="t('page.marketing.pages.no_website.description')"
|
|
344
|
-
@create="createWebsite"
|
|
345
|
-
/>
|
|
346
|
-
|
|
347
|
-
<template v-else>
|
|
348
|
-
<USkeleton v-if="loading" class="h-96 w-full" />
|
|
349
|
-
|
|
350
|
-
<UAlert
|
|
351
|
-
v-else-if="truncated"
|
|
352
|
-
icon="i-ph-list"
|
|
353
|
-
color="neutral"
|
|
354
|
-
variant="subtle"
|
|
355
|
-
:title="t('page.marketing.pages.list_truncated')"
|
|
356
|
-
:description="t('page.marketing.pages.list_truncated_hint')"
|
|
357
|
-
/>
|
|
358
|
-
|
|
359
|
-
<!-- An empty inventory is not an empty surface: a typed path is
|
|
360
|
-
inspectable whether or not the rollup ever listed it. -->
|
|
361
|
-
<UAlert
|
|
362
|
-
v-if="!loadingPages && pages.length === 0 && !selectedPath"
|
|
363
|
-
icon="i-ph-cursor-click"
|
|
364
|
-
:title="t('page.marketing.pages.no_pages.title')"
|
|
365
|
-
:description="t('page.marketing.pages.no_pages.description')"
|
|
366
|
-
/>
|
|
367
|
-
|
|
368
|
-
<DmsMasterDetail
|
|
369
|
-
v-else-if="!loading"
|
|
370
|
-
v-model="selectedPath"
|
|
371
|
-
:items="listItems"
|
|
372
|
-
:list-label="t('page.marketing.pages.inventory')"
|
|
373
|
-
>
|
|
374
|
-
<DmsMarketingPagePreview
|
|
375
|
-
v-if="selectedPath"
|
|
376
|
-
:website="selectedWebsite"
|
|
377
|
-
:path="selectedPath"
|
|
378
|
-
:period="period"
|
|
379
|
-
:sample-rate="sampleRate"
|
|
380
|
-
:tracker-enabled="trackerEnabled"
|
|
381
|
-
:pageviews="pageviewsBySelected"
|
|
382
|
-
:refresh-token="heatmapRefreshToken"
|
|
383
|
-
@website-updated="loadWebsites"
|
|
384
|
-
/>
|
|
385
|
-
<div v-else class="p-8 text-center text-sm text-muted">
|
|
386
|
-
{{ t('page.marketing.pages.select_hint') }}
|
|
387
|
-
</div>
|
|
388
|
-
</DmsMasterDetail>
|
|
389
|
-
</template>
|
|
390
|
-
</div>
|
|
391
|
-
</template>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* The stat-variant DmsKpiCard look for a value that is not a plain number:
|
|
4
|
-
* dms-ui's card only formats numeric values (number / currency / percent),
|
|
5
|
-
* and a session duration reads as "1m 32s", which is none of them. Classes
|
|
6
|
-
* are copied from KpiCard's stat variant so the card sits in the same row
|
|
7
|
-
* without a seam.
|
|
8
|
-
*/
|
|
9
|
-
defineProps<{ title: string, value: string, icon: string }>()
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<template>
|
|
13
|
-
<DmsCard :padded="false" class="p-4 sm:p-5">
|
|
14
|
-
<div class="flex items-start justify-between gap-3">
|
|
15
|
-
<div class="min-w-0 flex-1">
|
|
16
|
-
<p class="text-dimmed truncate font-mono text-[10px] font-medium uppercase tracking-widest">
|
|
17
|
-
{{ title }}
|
|
18
|
-
</p>
|
|
19
|
-
<p class="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
|
|
20
|
-
{{ value }}
|
|
21
|
-
</p>
|
|
22
|
-
</div>
|
|
23
|
-
<div class="text-dimmed flex shrink-0 items-center justify-center">
|
|
24
|
-
<UIcon :name="icon" class="size-4" aria-hidden="true" />
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
</DmsCard>
|
|
28
|
-
</template>
|
|
@@ -1,162 +0,0 @@
|
|
|
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>
|
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { computed, type ComputedRef } from 'vue'
|
|
2
|
-
import { MS_PER_DAY } from '../constants'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Bridge between the DMS period selector and the marketing endpoints.
|
|
6
|
-
*
|
|
7
|
-
* Every marketing read takes an `Nd` window anchored on now, while the
|
|
8
|
-
* selector publishes a from/to range into a scope registry. The conversion
|
|
9
|
-
* belongs in one place, not least because the deep links between those views
|
|
10
|
-
* carry the window as a query parameter and have to map it back onto a preset.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export const DEFAULT_PERIOD_DAYS = 7
|
|
14
|
-
export const DEFAULT_PERIOD = `${DEFAULT_PERIOD_DAYS}d`
|
|
15
|
-
|
|
16
|
-
/** Presets every marketing surface offers, in selector order. */
|
|
17
|
-
export const MARKETING_PERIOD_PRESETS = [
|
|
18
|
-
'today',
|
|
19
|
-
'last-7-days',
|
|
20
|
-
'last-30-days',
|
|
21
|
-
'last-90-days',
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
const PRESET_BY_DAYS: Record<number, string> = {
|
|
25
|
-
1: 'today',
|
|
26
|
-
7: 'last-7-days',
|
|
27
|
-
30: 'last-30-days',
|
|
28
|
-
90: 'last-90-days',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const DEFAULT_PERIOD_PRESET = PRESET_BY_DAYS[DEFAULT_PERIOD_DAYS]
|
|
32
|
-
|
|
33
|
-
/** Day count of an `Nd` string, or the default for anything unparseable. */
|
|
34
|
-
export function periodDays(period: string | undefined): number {
|
|
35
|
-
const days = Number.parseInt(period ?? '', 10)
|
|
36
|
-
return Number.isFinite(days) && days > 0 ? days : DEFAULT_PERIOD_DAYS
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Preset matching an `Nd` window. Only exact matches map back — an arbitrary
|
|
41
|
-
* window (a custom range someone linked from) falls back to the default rather
|
|
42
|
-
* than silently rounding to a neighbouring preset.
|
|
43
|
-
*/
|
|
44
|
-
export function ndToPreset(period: string | undefined): string {
|
|
45
|
-
if (!period) {
|
|
46
|
-
return DEFAULT_PERIOD_PRESET
|
|
47
|
-
}
|
|
48
|
-
return PRESET_BY_DAYS[periodDays(period)] ?? DEFAULT_PERIOD_PRESET
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The `Nd` window currently published by a period-selector scope. */
|
|
52
|
-
export function useMarketingPeriod(scope: string): ComputedRef<string> {
|
|
53
|
-
const periodScope = usePeriodScope(scope)
|
|
54
|
-
|
|
55
|
-
return computed(() => {
|
|
56
|
-
const range = periodScope.value?.range
|
|
57
|
-
if (!range) {
|
|
58
|
-
return DEFAULT_PERIOD
|
|
59
|
-
}
|
|
60
|
-
const days = Math.round(
|
|
61
|
-
(range.to.getTime() - range.from.getTime()) / MS_PER_DAY,
|
|
62
|
-
)
|
|
63
|
-
return `${Math.max(1, days)}d`
|
|
64
|
-
})
|
|
65
|
-
}
|