@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
package/static/tracker.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Optional attributes:
|
|
9
9
|
* data-host-url override the collect origin (defaults to the script origin)
|
|
10
10
|
* data-heatmap-sample 0-1 click/scroll sampling rate; overrides the module setting
|
|
11
|
-
* data-do-not-track "false" to ignore the browser DNT
|
|
11
|
+
* data-do-not-track "false" to ignore the browser DNT and GPC signals
|
|
12
12
|
*
|
|
13
13
|
* Cookieless by design: no identifier is generated or stored client-side —
|
|
14
14
|
* the backend derives an anonymous visitor hash per request. Payload shape
|
|
@@ -36,12 +36,17 @@
|
|
|
36
36
|
const CONFIG_GLOBAL = "__dmsMarketingTrackerConfig";
|
|
37
37
|
/** Global the capture script (snapshot.js) reads its page context from. */
|
|
38
38
|
const SNAPSHOT_CONTEXT_GLOBAL = "__dmsMarketingSnapshotContext";
|
|
39
|
+
/** Set by the first tracker that starts collecting on the page. */
|
|
40
|
+
const INSTALLED_GLOBAL = "__dmsMarketingInstalled";
|
|
39
41
|
|
|
40
42
|
const DEFAULT_SAMPLE_RATE = 0.1;
|
|
41
43
|
const FLUSH_INTERVAL_MS = 5000;
|
|
42
44
|
const MAX_BATCH = 25;
|
|
43
|
-
/**
|
|
45
|
+
/** Mirror the backend caps (MAX_URL_LENGTH, MAX_TITLE_LENGTH,
|
|
46
|
+
* MAX_EVENT_NAME_LENGTH): cut here, nothing is lost to validation. */
|
|
44
47
|
const MAX_URL_LENGTH = 500;
|
|
48
|
+
const MAX_TITLE_LENGTH = 200;
|
|
49
|
+
const MAX_EVENT_NAME_LENGTH = 50;
|
|
45
50
|
const MAX_SELECTOR_LENGTH = 120;
|
|
46
51
|
const SELECTOR_DEPTH = 3;
|
|
47
52
|
const COORDINATE_PRECISION = 4;
|
|
@@ -51,6 +56,11 @@
|
|
|
51
56
|
const SNAPSHOT_DELAY_MS = 3000;
|
|
52
57
|
/** SPA navigations may ask again, this many times per page load. */
|
|
53
58
|
const MAX_SNAPSHOTS_PER_LOAD = 3;
|
|
59
|
+
/** After a SPA navigation, time for the new view to render before its
|
|
60
|
+
* unscrolled depth is measured. */
|
|
61
|
+
const SCROLL_MEASURE_DELAY_MS = 1000;
|
|
62
|
+
/** Where a keyboard-activated click is anchored inside its element. */
|
|
63
|
+
const ELEMENT_CENTER = 0.5;
|
|
54
64
|
|
|
55
65
|
// --- Public API -----------------------------------------------------------
|
|
56
66
|
|
|
@@ -72,11 +82,6 @@
|
|
|
72
82
|
|
|
73
83
|
// --- Configuration --------------------------------------------------------
|
|
74
84
|
|
|
75
|
-
const script = document.currentScript;
|
|
76
|
-
if (!script) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
85
|
/** Read once and removed: the injected settings are not a page-facing API. */
|
|
81
86
|
function servedConfig() {
|
|
82
87
|
const injected = window[CONFIG_GLOBAL];
|
|
@@ -84,6 +89,21 @@
|
|
|
84
89
|
return injected && typeof injected === "object" ? injected : {};
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
const config = servedConfig();
|
|
93
|
+
|
|
94
|
+
// Collection paused (Settings): the page API without the collection, so
|
|
95
|
+
// `variation()` answers the control. A page already running the tracker
|
|
96
|
+
// keeps it.
|
|
97
|
+
if (config.paused === true) {
|
|
98
|
+
window.dmsMarketing = window.dmsMarketing || NOOP_API;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const script = document.currentScript;
|
|
103
|
+
if (!script) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
87
107
|
function firstFiniteNumber(values) {
|
|
88
108
|
return values.find((value) => Number.isFinite(value));
|
|
89
109
|
}
|
|
@@ -98,16 +118,27 @@
|
|
|
98
118
|
return Math.min(Math.max(rate, 0), 1);
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
const config = servedConfig();
|
|
102
121
|
const websiteId = script.getAttribute("data-website-id");
|
|
103
122
|
if (!websiteId) {
|
|
104
123
|
publishApi();
|
|
105
124
|
return;
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
// A tag included twice (theme and tag manager) would count everything twice.
|
|
128
|
+
if (window[INSTALLED_GLOBAL]) {
|
|
129
|
+
console.warn("[dms-marketing] tracker included twice; ignoring the copy.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
window[INSTALLED_GLOBAL] = true;
|
|
133
|
+
|
|
134
|
+
/** Global Privacy Control is the opt-out signal browsers ship now that DNT
|
|
135
|
+
* is deprecated; both are honoured alike. */
|
|
108
136
|
const respectDnt = script.getAttribute("data-do-not-track") !== "false";
|
|
109
|
-
const
|
|
110
|
-
|
|
137
|
+
const optedOut =
|
|
138
|
+
navigator.doNotTrack === "1" ||
|
|
139
|
+
window.doNotTrack === "1" ||
|
|
140
|
+
navigator.globalPrivacyControl === true;
|
|
141
|
+
if (respectDnt && optedOut) {
|
|
111
142
|
publishApi();
|
|
112
143
|
return;
|
|
113
144
|
}
|
|
@@ -135,7 +166,7 @@
|
|
|
135
166
|
const base = () => ({
|
|
136
167
|
url: currentUrl(),
|
|
137
168
|
referrer: document.referrer.slice(0, MAX_URL_LENGTH) || undefined,
|
|
138
|
-
title: document.title || undefined,
|
|
169
|
+
title: document.title.slice(0, MAX_TITLE_LENGTH) || undefined,
|
|
139
170
|
screen: `${screen.width}x${screen.height}`,
|
|
140
171
|
language: navigator.language || undefined,
|
|
141
172
|
at: Date.now(),
|
|
@@ -179,7 +210,8 @@
|
|
|
179
210
|
* navigation rewrites `location` before the depth of the page being left
|
|
180
211
|
* is flushed. */
|
|
181
212
|
let scrolledUrl = null;
|
|
182
|
-
|
|
213
|
+
/** -1 until the page has been measured once. */
|
|
214
|
+
let maxScrollDepth = -1;
|
|
183
215
|
|
|
184
216
|
function trackPageview() {
|
|
185
217
|
if (location.pathname === lastPath) {
|
|
@@ -188,17 +220,34 @@
|
|
|
188
220
|
flushScrollDepth();
|
|
189
221
|
lastPath = location.pathname;
|
|
190
222
|
scrolledUrl = currentUrl();
|
|
191
|
-
maxScrollDepth =
|
|
192
|
-
|
|
223
|
+
maxScrollDepth = -1;
|
|
224
|
+
// The rate it applies, for the server's "recorded on N % of page loads":
|
|
225
|
+
// counting scroll events against pageviews undercounts the views left
|
|
226
|
+
// before their depth is sent.
|
|
227
|
+
enqueue(
|
|
228
|
+
Object.assign(base(), { kind: "pageview", data: { sample: sampleRate } }),
|
|
229
|
+
);
|
|
230
|
+
scheduleScrollMeasure();
|
|
193
231
|
scheduleSnapshot();
|
|
194
232
|
}
|
|
195
233
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
234
|
+
/** pushState and replaceState (a client-side redirect lands on a page
|
|
235
|
+
* too); a same-path call — a query or hash update — is no navigation. */
|
|
236
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
237
|
+
const original = history[method];
|
|
238
|
+
history[method] = function navigate(...args) {
|
|
239
|
+
original.apply(this, args);
|
|
240
|
+
trackPageview();
|
|
241
|
+
};
|
|
242
|
+
}
|
|
201
243
|
window.addEventListener("popstate", trackPageview);
|
|
244
|
+
// A page restored from the back/forward cache runs no script again.
|
|
245
|
+
window.addEventListener("pageshow", (event) => {
|
|
246
|
+
if (event.persisted) {
|
|
247
|
+
lastPath = null;
|
|
248
|
+
trackPageview();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
202
251
|
|
|
203
252
|
// --- Scroll depth (max reached per page, flushed on leave) ----------------
|
|
204
253
|
|
|
@@ -215,19 +264,30 @@
|
|
|
215
264
|
);
|
|
216
265
|
}
|
|
217
266
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
267
|
+
function measureScrollDepth() {
|
|
268
|
+
const depth = currentScrollDepth();
|
|
269
|
+
if (depth > maxScrollDepth) {
|
|
270
|
+
maxScrollDepth = depth;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
window.addEventListener("scroll", measureScrollDepth, { passive: true });
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Every sampled view reports a depth, scrolled or not: what shows without
|
|
278
|
+
* scrolling is reached too (all of a page shorter than the window), and a
|
|
279
|
+
* view that never scrolls belongs in the denominator.
|
|
280
|
+
*/
|
|
281
|
+
function scheduleScrollMeasure() {
|
|
282
|
+
if (document.readyState === "complete") {
|
|
283
|
+
setTimeout(measureScrollDepth, SCROLL_MEASURE_DELAY_MS);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
window.addEventListener("load", measureScrollDepth, { once: true });
|
|
287
|
+
}
|
|
228
288
|
|
|
229
289
|
function flushScrollDepth() {
|
|
230
|
-
if (!heatmapSampled || maxScrollDepth
|
|
290
|
+
if (!heatmapSampled || maxScrollDepth < 0) {
|
|
231
291
|
return;
|
|
232
292
|
}
|
|
233
293
|
enqueue(
|
|
@@ -237,7 +297,7 @@
|
|
|
237
297
|
data: { depth: maxScrollDepth },
|
|
238
298
|
}),
|
|
239
299
|
);
|
|
240
|
-
maxScrollDepth =
|
|
300
|
+
maxScrollDepth = -1;
|
|
241
301
|
}
|
|
242
302
|
|
|
243
303
|
// --- Click capture (sampled, viewport-normalized) -------------------------
|
|
@@ -257,19 +317,16 @@
|
|
|
257
317
|
return parts.join(">").slice(0, MAX_SELECTOR_LENGTH);
|
|
258
318
|
}
|
|
259
319
|
|
|
260
|
-
function normalized(value, total) {
|
|
261
|
-
if (total <= 0) {
|
|
262
|
-
return 0;
|
|
263
|
-
}
|
|
264
|
-
return Number(Math.min(value / total, 1).toFixed(COORDINATE_PRECISION));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
320
|
function rounded(value) {
|
|
268
321
|
return Number(
|
|
269
322
|
Math.min(Math.max(value, 0), 1).toFixed(COORDINATE_PRECISION),
|
|
270
323
|
);
|
|
271
324
|
}
|
|
272
325
|
|
|
326
|
+
function normalized(value, total) {
|
|
327
|
+
return total <= 0 ? 0 : rounded(value / total);
|
|
328
|
+
}
|
|
329
|
+
|
|
273
330
|
/**
|
|
274
331
|
* Where the click landed inside the element it hit, so the point survives
|
|
275
332
|
* being read back at another window size. Document fractions do not: they
|
|
@@ -284,7 +341,7 @@
|
|
|
284
341
|
* several elements, and the embedder resolving it would otherwise land on
|
|
285
342
|
* whichever comes first in the document.
|
|
286
343
|
*/
|
|
287
|
-
function elementAnchor(target,
|
|
344
|
+
function elementAnchor(target, point) {
|
|
288
345
|
if (target === document.body || target === document.documentElement) {
|
|
289
346
|
return null;
|
|
290
347
|
}
|
|
@@ -310,16 +367,37 @@
|
|
|
310
367
|
}
|
|
311
368
|
return {
|
|
312
369
|
selector: selector,
|
|
313
|
-
ox: rounded((
|
|
314
|
-
oy: rounded((
|
|
370
|
+
ox: rounded((point.clientX - box.left) / box.width),
|
|
371
|
+
oy: rounded((point.clientY - box.top) / box.height),
|
|
315
372
|
nth: nth,
|
|
316
373
|
};
|
|
317
374
|
}
|
|
318
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Where the click happened. A click from the keyboard (Enter, Space) has no
|
|
378
|
+
* pointer — detail 0, coordinates 0,0 — and is placed at the centre of the
|
|
379
|
+
* element it activated instead of the window's corner.
|
|
380
|
+
*/
|
|
381
|
+
function clickPoint(target, event) {
|
|
382
|
+
if (event.detail !== 0 || event.clientX !== 0 || event.clientY !== 0) {
|
|
383
|
+
return event;
|
|
384
|
+
}
|
|
385
|
+
const box = target.getBoundingClientRect();
|
|
386
|
+
const clientX = box.left + box.width * ELEMENT_CENTER;
|
|
387
|
+
const clientY = box.top + box.height * ELEMENT_CENTER;
|
|
388
|
+
return {
|
|
389
|
+
clientX: clientX,
|
|
390
|
+
clientY: clientY,
|
|
391
|
+
pageX: clientX + scrollX,
|
|
392
|
+
pageY: clientY + scrollY,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
319
396
|
document.addEventListener(
|
|
320
397
|
"click",
|
|
321
398
|
(event) => {
|
|
322
|
-
|
|
399
|
+
// Script-dispatched clicks (element.click()) are no visitor's.
|
|
400
|
+
if (!heatmapSampled || !event.isTrusted) {
|
|
323
401
|
return;
|
|
324
402
|
}
|
|
325
403
|
const target = event.target;
|
|
@@ -327,18 +405,19 @@
|
|
|
327
405
|
return;
|
|
328
406
|
}
|
|
329
407
|
const doc = document.documentElement;
|
|
408
|
+
const point = clickPoint(target, event);
|
|
330
409
|
enqueue(
|
|
331
410
|
Object.assign(base(), {
|
|
332
411
|
kind: "click",
|
|
333
412
|
data: Object.assign(
|
|
334
413
|
{
|
|
335
|
-
x: normalized(
|
|
336
|
-
y: normalized(
|
|
337
|
-
dx: normalized(
|
|
338
|
-
dy: normalized(
|
|
414
|
+
x: normalized(point.clientX, innerWidth),
|
|
415
|
+
y: normalized(point.clientY, innerHeight),
|
|
416
|
+
dx: normalized(point.pageX, doc.scrollWidth),
|
|
417
|
+
dy: normalized(point.pageY, doc.scrollHeight),
|
|
339
418
|
selector: shortSelector(target),
|
|
340
419
|
},
|
|
341
|
-
elementAnchor(target,
|
|
420
|
+
elementAnchor(target, point),
|
|
342
421
|
),
|
|
343
422
|
}),
|
|
344
423
|
);
|
|
@@ -369,6 +448,9 @@
|
|
|
369
448
|
return;
|
|
370
449
|
}
|
|
371
450
|
snapshotsRequested++;
|
|
451
|
+
// The upload is only accepted for the page the visit's last pageview
|
|
452
|
+
// named: that pageview must be on its way before the capture is.
|
|
453
|
+
flush();
|
|
372
454
|
const path = location.pathname;
|
|
373
455
|
window[SNAPSHOT_CONTEXT_GLOBAL] = {
|
|
374
456
|
website: websiteId,
|
|
@@ -466,7 +548,16 @@
|
|
|
466
548
|
|
|
467
549
|
publishApi({
|
|
468
550
|
track(name, data) {
|
|
469
|
-
enqueue(
|
|
551
|
+
enqueue(
|
|
552
|
+
Object.assign(base(), {
|
|
553
|
+
kind: "custom",
|
|
554
|
+
name:
|
|
555
|
+
typeof name === "string"
|
|
556
|
+
? name.slice(0, MAX_EVENT_NAME_LENGTH)
|
|
557
|
+
: name,
|
|
558
|
+
data,
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
470
561
|
},
|
|
471
562
|
/** Assigned variation of a running experiment, or null. Reports the
|
|
472
563
|
* exposure, so only call it where the variation is actually applied. */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../../src/pages/campaigns/page.ts"],"names":[],"mappings":";;;;;;;;;AAAA,kEAAwE;AACxE,oEAAuE;AACvE,yDAA8E;AAC9E,iDAAwD;AASjD,IAAM,sBAAsB,GAA5B,MAAM,sBAAuB,SAAQ,IAAA,qBAAc,EACxD,WAAW,EACX;IACE,WAAW,EAAE,iCAAiC;IAC9C,WAAW,EAAE,uCAAuC;IACpD,IAAI,EAAE,gBAAgB;IACtB,MAAM,EAAE,+BAAmB;IAC3B,KAAK,EAAE,CAAC;CACT,EACD,IAAA,uBAAa,EAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CACnC;IACC,MAAM,CAAC,OAAO,GAAG,IAAA,wBAAe,EAAC,2BAA2B,CAAC,CAAC;;AAXnD,wDAAsB;iCAAtB,sBAAsB;IADlC,IAAA,mBAAY,GAAE;GACF,sBAAsB,CAYlC"}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import { useDmsRoute, useI18n } from '#dms/frontend-module'
|
|
4
|
-
import { useChildId } from '../composables/useChildId'
|
|
5
|
-
import { useLatestRequest } from '../composables/useLatestRequest'
|
|
6
|
-
import {
|
|
7
|
-
useMarketingApi,
|
|
8
|
-
type MarketingCampaignEntry,
|
|
9
|
-
type MarketingCampaigns,
|
|
10
|
-
type MarketingChannel,
|
|
11
|
-
} from '../composables/useMarketingApi'
|
|
12
|
-
import {
|
|
13
|
-
MARKETING_PERIOD_PRESETS,
|
|
14
|
-
ndToPreset,
|
|
15
|
-
useMarketingPeriod,
|
|
16
|
-
} from '../composables/useMarketingPeriod'
|
|
17
|
-
import { useMarketingWebsites } from '../composables/useMarketingWebsites'
|
|
18
|
-
|
|
19
|
-
import type { TableColumn } from '@nuxt/ui'
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Acquisition surface: the five-channel split of sessions (donut) next to the
|
|
23
|
-
* UTM campaign table — one row per (source, medium, campaign) triple observed
|
|
24
|
-
* on new sessions — with UTM terms and contents as top lists below.
|
|
25
|
-
*
|
|
26
|
-
* Everything reads the daily rollups through one campaigns fetch; the search
|
|
27
|
-
* box filters the table server-side because a day's map caps its entries and
|
|
28
|
-
* the merged table can exceed what one response carries.
|
|
29
|
-
*/
|
|
30
|
-
const PERIOD_SCOPE = 'dms-marketing-campaigns'
|
|
31
|
-
const SEARCH_DEBOUNCE_MS = 300
|
|
32
|
-
|
|
33
|
-
// The DMS page renderer hands every component its identity; keeping it is what
|
|
34
|
-
// lets dms-ui primitives key their events and persisted state per child.
|
|
35
|
-
const props = withDefaults(
|
|
36
|
-
defineProps<{ componentId?: string, pageId?: string }>(),
|
|
37
|
-
{ componentId: 'marketing-campaigns', pageId: 'marketing' },
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
const childId = useChildId(() => props.componentId)
|
|
41
|
-
|
|
42
|
-
const api = useMarketingApi()
|
|
43
|
-
const { t, te } = useI18n()
|
|
44
|
-
const route = useDmsRoute()
|
|
45
|
-
|
|
46
|
-
const queryParam = (key: string): string => {
|
|
47
|
-
const value = route.query[key]
|
|
48
|
-
return typeof value === 'string' ? value : ''
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Read once: the selector seeds its preset at init and never reactively, so a
|
|
52
|
-
// deep link's window has to be resolved before it renders.
|
|
53
|
-
const initialPreset = ndToPreset(queryParam('period'))
|
|
54
|
-
|
|
55
|
-
const {
|
|
56
|
-
websites,
|
|
57
|
-
items: websiteItems,
|
|
58
|
-
selectedId,
|
|
59
|
-
loading: loadingWebsites,
|
|
60
|
-
failed: websitesFailed,
|
|
61
|
-
load: loadWebsites,
|
|
62
|
-
create: createWebsite,
|
|
63
|
-
} = useMarketingWebsites(queryParam('website'))
|
|
64
|
-
|
|
65
|
-
const period = useMarketingPeriod(PERIOD_SCOPE)
|
|
66
|
-
const search = ref('')
|
|
67
|
-
|
|
68
|
-
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
|
69
|
-
|
|
70
|
-
const {
|
|
71
|
-
data: acquisition,
|
|
72
|
-
loading: loadingCampaigns,
|
|
73
|
-
failed: campaignsFailed,
|
|
74
|
-
settled: campaignsSettled,
|
|
75
|
-
run: loadCampaigns,
|
|
76
|
-
} = useLatestRequest<MarketingCampaigns>(() =>
|
|
77
|
-
// Server-side there is no session to authenticate with: the fetch belongs to
|
|
78
|
-
// the client, like every other read here.
|
|
79
|
-
selectedId.value && !import.meta.env.SSR
|
|
80
|
-
? api.getCampaigns(selectedId.value, period.value, search.value.trim())
|
|
81
|
-
: null,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
// The watcher owns every fetch, including the first: seeding it from onMounted
|
|
85
|
-
// as well would issue the initial request twice on a bare link, and not at all
|
|
86
|
-
// on a link that already names the site the list would have picked — resolving
|
|
87
|
-
// it writes back the same id, which is no change to watch.
|
|
88
|
-
watch([selectedId, period], () => {
|
|
89
|
-
void loadCampaigns()
|
|
90
|
-
}, { immediate: true })
|
|
91
|
-
|
|
92
|
-
watch(search, () => {
|
|
93
|
-
if (searchTimer) {
|
|
94
|
-
clearTimeout(searchTimer)
|
|
95
|
-
}
|
|
96
|
-
searchTimer = setTimeout(() => void loadCampaigns(), SEARCH_DEBOUNCE_MS)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
onBeforeUnmount(() => {
|
|
100
|
-
if (searchTimer) {
|
|
101
|
-
clearTimeout(searchTimer)
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const loading = computed(
|
|
106
|
-
() =>
|
|
107
|
-
loadingWebsites.value
|
|
108
|
-
|| loadingCampaigns.value
|
|
109
|
-
|| (!!selectedId.value && !campaignsSettled.value),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
// Two failures, two flags: retrying the campaigns fetch must not clear the
|
|
113
|
-
// one that says the site list itself never loaded.
|
|
114
|
-
const failed = computed(() => websitesFailed.value || campaignsFailed.value)
|
|
115
|
-
|
|
116
|
-
/** Retry entry point: whichever of the two fetches has nothing to show. */
|
|
117
|
-
function reload() {
|
|
118
|
-
return websites.value.length === 0 ? loadWebsites() : loadCampaigns()
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
onMounted(loadWebsites)
|
|
122
|
-
|
|
123
|
-
// --- Channels ---------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
/** Display order mirrors the backend's MARKETING_CHANNELS. */
|
|
126
|
-
const CHANNEL_ORDER: MarketingChannel[] = ['direct', 'organic', 'social', 'referral', 'paid']
|
|
127
|
-
|
|
128
|
-
/** Known channels localize; a key minted by a future classifier still shows. */
|
|
129
|
-
function channelLabel(channel: string): string {
|
|
130
|
-
const key = `page.marketing.campaigns.channels.${channel}`
|
|
131
|
-
return te(key) ? t(key) : channel
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const channelRecords = computed(() => {
|
|
135
|
-
const channels = acquisition.value?.channels ?? {}
|
|
136
|
-
return CHANNEL_ORDER
|
|
137
|
-
.map(channel => ({ label: channelLabel(channel), value: channels[channel] ?? 0 }))
|
|
138
|
-
.filter(record => record.value > 0)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
// --- Campaign table ---------------------------------------------------------
|
|
142
|
-
|
|
143
|
-
const campaignRows = computed(() => acquisition.value?.campaigns ?? [])
|
|
144
|
-
const truncated = computed(() => acquisition.value?.truncated ?? false)
|
|
145
|
-
|
|
146
|
-
const columns = computed<TableColumn<MarketingCampaignEntry>[]>(() => [
|
|
147
|
-
{ accessorKey: 'source', header: t('page.marketing.campaigns.table.source') },
|
|
148
|
-
{ accessorKey: 'medium', header: t('page.marketing.campaigns.table.medium') },
|
|
149
|
-
{ accessorKey: 'campaign', header: t('page.marketing.campaigns.table.campaign') },
|
|
150
|
-
{
|
|
151
|
-
accessorKey: 'sessions',
|
|
152
|
-
header: t('page.marketing.campaigns.table.sessions'),
|
|
153
|
-
meta: { class: { th: 'text-right', td: 'text-right' } },
|
|
154
|
-
},
|
|
155
|
-
])
|
|
156
|
-
|
|
157
|
-
// --- Referrer / term / content top lists ------------------------------------
|
|
158
|
-
|
|
159
|
-
const TOP_LIST_LIMIT = 8
|
|
160
|
-
|
|
161
|
-
function toTopItems(record: Record<string, number> | undefined) {
|
|
162
|
-
return Object.entries(record ?? {})
|
|
163
|
-
.sort((a, b) => b[1] - a[1])
|
|
164
|
-
.slice(0, TOP_LIST_LIMIT)
|
|
165
|
-
.map(([label, value]) => ({ id: label, title: label, value }))
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const topLists = computed(() => [
|
|
169
|
-
// Referrer domains across all channels — the overview's acquisition card
|
|
170
|
-
// links here for this detail.
|
|
171
|
-
{
|
|
172
|
-
id: 'referrers',
|
|
173
|
-
title: t('page.marketing.campaigns.top.referrers'),
|
|
174
|
-
items: toTopItems(acquisition.value?.referrers),
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
id: 'terms',
|
|
178
|
-
title: t('page.marketing.campaigns.top.terms'),
|
|
179
|
-
items: toTopItems(acquisition.value?.terms),
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
id: 'contents',
|
|
183
|
-
title: t('page.marketing.campaigns.top.contents'),
|
|
184
|
-
items: toTopItems(acquisition.value?.contents),
|
|
185
|
-
},
|
|
186
|
-
])
|
|
187
|
-
</script>
|
|
188
|
-
|
|
189
|
-
<template>
|
|
190
|
-
<div class="space-y-6 p-6">
|
|
191
|
-
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
192
|
-
<div>
|
|
193
|
-
<h1 class="text-xl font-semibold">{{ t('page.marketing.campaigns.title') }}</h1>
|
|
194
|
-
<p class="text-sm text-muted">{{ t('page.marketing.campaigns.subtitle') }}</p>
|
|
195
|
-
</div>
|
|
196
|
-
<div class="flex items-center gap-3">
|
|
197
|
-
<USelect
|
|
198
|
-
v-if="websites.length > 1"
|
|
199
|
-
v-model="selectedId"
|
|
200
|
-
:items="websiteItems"
|
|
201
|
-
class="w-56"
|
|
202
|
-
/>
|
|
203
|
-
<DmsPeriodSelector
|
|
204
|
-
:id="PERIOD_SCOPE"
|
|
205
|
-
:component-id="childId('period')"
|
|
206
|
-
:page-id="props.pageId"
|
|
207
|
-
:default-preset="initialPreset"
|
|
208
|
-
:presets="MARKETING_PERIOD_PRESETS"
|
|
209
|
-
:comparisons="['none']"
|
|
210
|
-
:show-range-label="false"
|
|
211
|
-
/>
|
|
212
|
-
<UButton
|
|
213
|
-
icon="i-ph-arrow-clockwise"
|
|
214
|
-
color="neutral"
|
|
215
|
-
variant="subtle"
|
|
216
|
-
size="sm"
|
|
217
|
-
:loading="loading"
|
|
218
|
-
@click="loadCampaigns"
|
|
219
|
-
/>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<div v-if="failed" class="flex flex-col items-start gap-3">
|
|
224
|
-
<UAlert
|
|
225
|
-
color="error"
|
|
226
|
-
variant="subtle"
|
|
227
|
-
icon="i-ph-warning-circle"
|
|
228
|
-
:title="t('page.marketing.campaigns.error.title')"
|
|
229
|
-
:description="t('page.marketing.campaigns.error.description')"
|
|
230
|
-
/>
|
|
231
|
-
<UButton
|
|
232
|
-
icon="i-ph-arrow-clockwise"
|
|
233
|
-
:label="t('page.marketing.campaigns.retry')"
|
|
234
|
-
color="neutral"
|
|
235
|
-
variant="subtle"
|
|
236
|
-
size="sm"
|
|
237
|
-
@click="reload"
|
|
238
|
-
/>
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
<DmsMarketingNoWebsiteState
|
|
242
|
-
v-else-if="!loading && websites.length === 0"
|
|
243
|
-
icon="i-ph-megaphone"
|
|
244
|
-
:title="t('page.marketing.campaigns.empty.title')"
|
|
245
|
-
:description="t('page.marketing.campaigns.empty.description')"
|
|
246
|
-
@create="createWebsite"
|
|
247
|
-
/>
|
|
248
|
-
|
|
249
|
-
<!-- Only before the first answer: a refetch keeps the tables on screen
|
|
250
|
-
and spins the refresh button instead. -->
|
|
251
|
-
<USkeleton v-else-if="loading && !acquisition" class="h-96 w-full" />
|
|
252
|
-
|
|
253
|
-
<template v-else>
|
|
254
|
-
<div class="grid gap-4 lg:grid-cols-3">
|
|
255
|
-
<UCard>
|
|
256
|
-
<template #header>
|
|
257
|
-
<div>
|
|
258
|
-
<p class="font-medium">{{ t('page.marketing.campaigns.channels.title') }}</p>
|
|
259
|
-
<p class="text-sm text-muted">{{ t('page.marketing.campaigns.channels.subtitle') }}</p>
|
|
260
|
-
</div>
|
|
261
|
-
</template>
|
|
262
|
-
<DmsChart
|
|
263
|
-
v-if="channelRecords.length > 0"
|
|
264
|
-
:component-id="childId('channels')"
|
|
265
|
-
:page-id="props.pageId"
|
|
266
|
-
type="donut"
|
|
267
|
-
:static-dataset="channelRecords"
|
|
268
|
-
height="300px"
|
|
269
|
-
:show-total="true"
|
|
270
|
-
/>
|
|
271
|
-
<p v-else class="py-12 text-center text-sm text-muted">
|
|
272
|
-
{{ t('page.marketing.campaigns.channels.empty') }}
|
|
273
|
-
</p>
|
|
274
|
-
</UCard>
|
|
275
|
-
|
|
276
|
-
<UCard class="lg:col-span-2">
|
|
277
|
-
<template #header>
|
|
278
|
-
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
279
|
-
<div>
|
|
280
|
-
<p class="font-medium">{{ t('page.marketing.campaigns.table.title') }}</p>
|
|
281
|
-
<p class="text-sm text-muted">{{ t('page.marketing.campaigns.table.subtitle') }}</p>
|
|
282
|
-
</div>
|
|
283
|
-
<UInput
|
|
284
|
-
v-model="search"
|
|
285
|
-
icon="i-ph-magnifying-glass"
|
|
286
|
-
:placeholder="t('page.marketing.campaigns.table.search_placeholder')"
|
|
287
|
-
class="w-56"
|
|
288
|
-
/>
|
|
289
|
-
</div>
|
|
290
|
-
</template>
|
|
291
|
-
<UTable :data="campaignRows" :columns="columns">
|
|
292
|
-
<template #source-cell="{ row }">
|
|
293
|
-
<span v-if="row.original.source">{{ row.original.source }}</span>
|
|
294
|
-
<span v-else class="text-muted">—</span>
|
|
295
|
-
</template>
|
|
296
|
-
<template #medium-cell="{ row }">
|
|
297
|
-
<span v-if="row.original.medium">{{ row.original.medium }}</span>
|
|
298
|
-
<span v-else class="text-muted">—</span>
|
|
299
|
-
</template>
|
|
300
|
-
<template #campaign-cell="{ row }">
|
|
301
|
-
<span v-if="row.original.campaign">{{ row.original.campaign }}</span>
|
|
302
|
-
<span v-else class="text-muted">—</span>
|
|
303
|
-
</template>
|
|
304
|
-
<template #empty>
|
|
305
|
-
<p class="py-8 text-center text-sm text-muted">
|
|
306
|
-
{{ t('page.marketing.campaigns.table.empty') }}
|
|
307
|
-
</p>
|
|
308
|
-
</template>
|
|
309
|
-
</UTable>
|
|
310
|
-
<p v-if="truncated" class="mt-2 text-sm text-muted">
|
|
311
|
-
{{ t('page.marketing.campaigns.table.truncated') }}
|
|
312
|
-
</p>
|
|
313
|
-
</UCard>
|
|
314
|
-
</div>
|
|
315
|
-
|
|
316
|
-
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
317
|
-
<DmsTopListCard
|
|
318
|
-
v-for="list in topLists"
|
|
319
|
-
:key="list.id"
|
|
320
|
-
:component-id="childId(`top-${list.id}`)"
|
|
321
|
-
:page-id="props.pageId"
|
|
322
|
-
:title="list.title"
|
|
323
|
-
:static-items="list.items"
|
|
324
|
-
:show-delta="false"
|
|
325
|
-
:empty-label="t('page.marketing.campaigns.top.empty')"
|
|
326
|
-
/>
|
|
327
|
-
</div>
|
|
328
|
-
</template>
|
|
329
|
-
</div>
|
|
330
|
-
</template>
|