@antelopejs/dms-marketing 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +143 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +35 -0
- package/dist/config.js.map +1 -0
- package/dist/data-api/body.d.ts +1 -0
- package/dist/data-api/body.js +15 -0
- package/dist/data-api/body.js.map +1 -0
- package/dist/data-api/funnels.d.ts +32 -0
- package/dist/data-api/funnels.js +302 -0
- package/dist/data-api/funnels.js.map +1 -0
- package/dist/data-api/hidden-filter.d.ts +1 -0
- package/dist/data-api/hidden-filter.js +11 -0
- package/dist/data-api/hidden-filter.js.map +1 -0
- package/dist/data-api/index.d.ts +4 -0
- package/dist/data-api/index.js +21 -0
- package/dist/data-api/index.js.map +1 -0
- package/dist/data-api/tenant-scope.d.ts +15 -0
- package/dist/data-api/tenant-scope.js +43 -0
- package/dist/data-api/tenant-scope.js.map +1 -0
- package/dist/data-api/websites.d.ts +25 -0
- package/dist/data-api/websites.js +73 -0
- package/dist/data-api/websites.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +19 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/models/funnels.model.d.ts +25 -0
- package/dist/db/models/funnels.model.js +39 -0
- package/dist/db/models/funnels.model.js.map +1 -0
- package/dist/db/models/index.d.ts +7 -0
- package/dist/db/models/index.js +24 -0
- package/dist/db/models/index.js.map +1 -0
- package/dist/db/models/marketing_events.model.d.ts +49 -0
- package/dist/db/models/marketing_events.model.js +85 -0
- package/dist/db/models/marketing_events.model.js.map +1 -0
- package/dist/db/models/marketing_sessions.model.d.ts +33 -0
- package/dist/db/models/marketing_sessions.model.js +50 -0
- package/dist/db/models/marketing_sessions.model.js.map +1 -0
- package/dist/db/models/marketing_settings.model.d.ts +23 -0
- package/dist/db/models/marketing_settings.model.js +13 -0
- package/dist/db/models/marketing_settings.model.js.map +1 -0
- package/dist/db/models/page_snapshots.model.d.ts +27 -0
- package/dist/db/models/page_snapshots.model.js +32 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -0
- package/dist/db/models/retention.d.ts +6 -0
- package/dist/db/models/retention.js +26 -0
- package/dist/db/models/retention.js.map +1 -0
- package/dist/db/models/website_statistics.model.d.ts +31 -0
- package/dist/db/models/website_statistics.model.js +63 -0
- package/dist/db/models/website_statistics.model.js.map +1 -0
- package/dist/db/models/websites.model.d.ts +26 -0
- package/dist/db/models/websites.model.js +27 -0
- package/dist/db/models/websites.model.js.map +1 -0
- package/dist/db/tables/funnels.table.d.ts +31 -0
- package/dist/db/tables/funnels.table.js +54 -0
- package/dist/db/tables/funnels.table.js.map +1 -0
- package/dist/db/tables/index.d.ts +7 -0
- package/dist/db/tables/index.js +24 -0
- package/dist/db/tables/index.js.map +1 -0
- package/dist/db/tables/marketing_events.table.d.ts +15 -0
- package/dist/db/tables/marketing_events.table.js +66 -0
- package/dist/db/tables/marketing_events.table.js.map +1 -0
- package/dist/db/tables/marketing_sessions.table.d.ts +26 -0
- package/dist/db/tables/marketing_sessions.table.js +115 -0
- package/dist/db/tables/marketing_sessions.table.js.map +1 -0
- package/dist/db/tables/marketing_settings.table.d.ts +11 -0
- package/dist/db/tables/marketing_settings.table.js +51 -0
- package/dist/db/tables/marketing_settings.table.js.map +1 -0
- package/dist/db/tables/page_snapshots.table.d.ts +17 -0
- package/dist/db/tables/page_snapshots.table.js +72 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -0
- package/dist/db/tables/website_statistics.table.d.ts +14 -0
- package/dist/db/tables/website_statistics.table.js +60 -0
- package/dist/db/tables/website_statistics.table.js.map +1 -0
- package/dist/db/tables/websites.table.d.ts +13 -0
- package/dist/db/tables/websites.table.js +60 -0
- package/dist/db/tables/websites.table.js.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +39 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/implementations/dms-marketing/index.d.ts +2 -0
- package/dist/implementations/dms-marketing/index.js +8 -0
- package/dist/implementations/dms-marketing/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/all.d.ts +5 -0
- package/dist/pages/all.js +22 -0
- package/dist/pages/all.js.map +1 -0
- package/dist/pages/campaigns/page.d.ts +9 -0
- package/dist/pages/campaigns/page.js +27 -0
- package/dist/pages/campaigns/page.js.map +1 -0
- package/dist/pages/funnels/page.d.ts +9 -0
- package/dist/pages/funnels/page.js +27 -0
- package/dist/pages/funnels/page.js.map +1 -0
- package/dist/pages/index.d.ts +4 -0
- package/dist/pages/index.js +21 -0
- package/dist/pages/index.js.map +1 -0
- package/dist/pages/module.d.ts +1 -0
- package/dist/pages/module.js +13 -0
- package/dist/pages/module.js.map +1 -0
- package/dist/pages/overview/page.d.ts +9 -0
- package/dist/pages/overview/page.js +27 -0
- package/dist/pages/overview/page.js.map +1 -0
- package/dist/pages/pages/page.d.ts +9 -0
- package/dist/pages/pages/page.js +27 -0
- package/dist/pages/pages/page.js.map +1 -0
- package/dist/pages/settings/form.d.ts +3 -0
- package/dist/pages/settings/form.js +35 -0
- package/dist/pages/settings/form.js.map +1 -0
- package/dist/pages/settings/page.d.ts +9 -0
- package/dist/pages/settings/page.js +27 -0
- package/dist/pages/settings/page.js.map +1 -0
- package/dist/routes/collect.d.ts +7 -0
- package/dist/routes/collect.js +62 -0
- package/dist/routes/collect.js.map +1 -0
- package/dist/routes/experiments.d.ts +6 -0
- package/dist/routes/experiments.js +63 -0
- package/dist/routes/experiments.js.map +1 -0
- package/dist/routes/funnels.d.ts +16 -0
- package/dist/routes/funnels.js +44 -0
- package/dist/routes/funnels.js.map +1 -0
- package/dist/routes/index.d.ts +8 -0
- package/dist/routes/index.js +25 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/settings.d.ts +8 -0
- package/dist/routes/settings.js +50 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/snapshot.d.ts +10 -0
- package/dist/routes/snapshot.js +147 -0
- package/dist/routes/snapshot.js.map +1 -0
- package/dist/routes/stats.d.ts +44 -0
- package/dist/routes/stats.js +278 -0
- package/dist/routes/stats.js.map +1 -0
- package/dist/routes/tracker.d.ts +6 -0
- package/dist/routes/tracker.js +57 -0
- package/dist/routes/tracker.js.map +1 -0
- package/dist/routes/websites.d.ts +13 -0
- package/dist/routes/websites.js +144 -0
- package/dist/routes/websites.js.map +1 -0
- package/dist/services/acquisition.d.ts +15 -0
- package/dist/services/acquisition.js +153 -0
- package/dist/services/acquisition.js.map +1 -0
- package/dist/services/bounded-cache.d.ts +14 -0
- package/dist/services/bounded-cache.js +45 -0
- package/dist/services/bounded-cache.js.map +1 -0
- package/dist/services/collect.d.ts +11 -0
- package/dist/services/collect.js +193 -0
- package/dist/services/collect.js.map +1 -0
- package/dist/services/experiment-definitions.d.ts +4 -0
- package/dist/services/experiment-definitions.js +36 -0
- package/dist/services/experiment-definitions.js.map +1 -0
- package/dist/services/experiments/assign.d.ts +5 -0
- package/dist/services/experiments/assign.js +52 -0
- package/dist/services/experiments/assign.js.map +1 -0
- package/dist/services/experiments/index.d.ts +3 -0
- package/dist/services/experiments/index.js +20 -0
- package/dist/services/experiments/index.js.map +1 -0
- package/dist/services/experiments/results.d.ts +20 -0
- package/dist/services/experiments/results.js +91 -0
- package/dist/services/experiments/results.js.map +1 -0
- package/dist/services/experiments/stats.d.ts +12 -0
- package/dist/services/experiments/stats.js +62 -0
- package/dist/services/experiments/stats.js.map +1 -0
- package/dist/services/funnels/compute.d.ts +15 -0
- package/dist/services/funnels/compute.js +68 -0
- package/dist/services/funnels/compute.js.map +1 -0
- package/dist/services/funnels/index.d.ts +2 -0
- package/dist/services/funnels/index.js +19 -0
- package/dist/services/funnels/index.js.map +1 -0
- package/dist/services/funnels/results.d.ts +14 -0
- package/dist/services/funnels/results.js +72 -0
- package/dist/services/funnels/results.js.map +1 -0
- package/dist/services/geoip.d.ts +3 -0
- package/dist/services/geoip.js +38 -0
- package/dist/services/geoip.js.map +1 -0
- package/dist/services/heatmap.d.ts +10 -0
- package/dist/services/heatmap.js +180 -0
- package/dist/services/heatmap.js.map +1 -0
- package/dist/services/index.d.ts +26 -0
- package/dist/services/index.js +43 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/ingest-guard.d.ts +11 -0
- package/dist/services/ingest-guard.js +44 -0
- package/dist/services/ingest-guard.js.map +1 -0
- package/dist/services/keyed-queue.d.ts +4 -0
- package/dist/services/keyed-queue.js +19 -0
- package/dist/services/keyed-queue.js.map +1 -0
- package/dist/services/origin-guard.d.ts +5 -0
- package/dist/services/origin-guard.js +43 -0
- package/dist/services/origin-guard.js.map +1 -0
- package/dist/services/period.d.ts +2 -0
- package/dist/services/period.js +19 -0
- package/dist/services/period.js.map +1 -0
- package/dist/services/prune.d.ts +7 -0
- package/dist/services/prune.js +49 -0
- package/dist/services/prune.js.map +1 -0
- package/dist/services/query-param.d.ts +1 -0
- package/dist/services/query-param.js +11 -0
- package/dist/services/query-param.js.map +1 -0
- package/dist/services/request-body.d.ts +2 -0
- package/dist/services/request-body.js +41 -0
- package/dist/services/request-body.js.map +1 -0
- package/dist/services/request-meta.d.ts +3 -0
- package/dist/services/request-meta.js +19 -0
- package/dist/services/request-meta.js.map +1 -0
- package/dist/services/rollup-write.d.ts +4 -0
- package/dist/services/rollup-write.js +84 -0
- package/dist/services/rollup-write.js.map +1 -0
- package/dist/services/sessionize.d.ts +23 -0
- package/dist/services/sessionize.js +78 -0
- package/dist/services/sessionize.js.map +1 -0
- package/dist/services/settings.d.ts +6 -0
- package/dist/services/settings.js +113 -0
- package/dist/services/settings.js.map +1 -0
- package/dist/services/snapshots.d.ts +24 -0
- package/dist/services/snapshots.js +139 -0
- package/dist/services/snapshots.js.map +1 -0
- package/dist/services/tenant-funnel.d.ts +3 -0
- package/dist/services/tenant-funnel.js +17 -0
- package/dist/services/tenant-funnel.js.map +1 -0
- package/dist/services/tenant-website.d.ts +5 -0
- package/dist/services/tenant-website.js +34 -0
- package/dist/services/tenant-website.js.map +1 -0
- package/dist/services/url.d.ts +10 -0
- package/dist/services/url.js +81 -0
- package/dist/services/url.js.map +1 -0
- package/dist/services/user-agent.d.ts +2 -0
- package/dist/services/user-agent.js +17 -0
- package/dist/services/user-agent.js.map +1 -0
- package/dist/services/visitor-id.d.ts +1 -0
- package/dist/services/visitor-id.js +19 -0
- package/dist/services/visitor-id.js.map +1 -0
- package/dist/services/website-cache.d.ts +3 -0
- package/dist/services/website-cache.js +22 -0
- package/dist/services/website-cache.js.map +1 -0
- package/dist/types/constants.d.ts +74 -0
- package/dist/types/constants.js +79 -0
- package/dist/types/constants.js.map +1 -0
- package/dist/types/events.d.ts +114 -0
- package/dist/types/events.js +29 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/settings.d.ts +67 -0
- package/dist/types/settings.js +58 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/snapshots.d.ts +95 -0
- package/dist/types/snapshots.js +41 -0
- package/dist/types/snapshots.js.map +1 -0
- package/dist/types/statistics.d.ts +38 -0
- package/dist/types/statistics.js +33 -0
- package/dist/types/statistics.js.map +1 -0
- package/dist/utils/experiment-type.d.ts +52 -0
- package/dist/utils/experiment-type.js +94 -0
- package/dist/utils/experiment-type.js.map +1 -0
- package/dist/utils/funnel-steps-type.d.ts +42 -0
- package/dist/utils/funnel-steps-type.js +62 -0
- package/dist/utils/funnel-steps-type.js.map +1 -0
- package/frontend-vue/.editorconfig +12 -0
- package/frontend-vue/.prettierrc +7 -0
- package/frontend-vue/app/components/CampaignsView.vue +330 -0
- package/frontend-vue/app/components/ExperimentInput.vue +128 -0
- package/frontend-vue/app/components/FunnelDetail.vue +1026 -0
- package/frontend-vue/app/components/FunnelStepsInput.vue +154 -0
- package/frontend-vue/app/components/FunnelsView.vue +493 -0
- package/frontend-vue/app/components/HeatmapCanvas.vue +116 -0
- package/frontend-vue/app/components/NoWebsiteState.vue +18 -0
- package/frontend-vue/app/components/OverviewView.vue +493 -0
- package/frontend-vue/app/components/PagePreview.vue +915 -0
- package/frontend-vue/app/components/PagesView.vue +378 -0
- package/frontend-vue/app/components/ScrollDepthOverlay.vue +158 -0
- package/frontend-vue/app/components/TextKpiCard.vue +28 -0
- package/frontend-vue/app/components/TopListTabsCard.vue +162 -0
- package/frontend-vue/app/components/TrackerSnippet.vue +31 -0
- package/frontend-vue/app/components/VariationsInput.vue +164 -0
- package/frontend-vue/app/composables/useChildId.ts +13 -0
- package/frontend-vue/app/composables/useExperiment.ts +59 -0
- package/frontend-vue/app/composables/useFunnelSteps.ts +25 -0
- package/frontend-vue/app/composables/useLatestRequest.ts +90 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +420 -0
- package/frontend-vue/app/composables/useMarketingPeriod.ts +66 -0
- package/frontend-vue/app/composables/useMarketingRoutes.ts +51 -0
- package/frontend-vue/app/composables/useMarketingWebsiteForm.ts +69 -0
- package/frontend-vue/app/composables/useMarketingWebsites.ts +80 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +183 -0
- package/frontend-vue/app/plugins/funnel-steps-data-type.client.ts +46 -0
- package/frontend-vue/app/utils/dmsComponent.ts +18 -0
- package/frontend-vue/app/utils/heatmapPalette.ts +41 -0
- package/frontend-vue/app/utils/snapshotViewportUnits.ts +63 -0
- package/frontend-vue/dms.frontend.ts +35 -0
- package/frontend-vue/eslint.config.mjs +16 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +382 -0
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +382 -0
- package/frontend-vue/package.json +30 -0
- package/frontend-vue/pnpm-lock.yaml +5226 -0
- package/frontend-vue/pnpm-workspace.yaml +2 -0
- package/frontend-vue/tests/drafts.test.ts +54 -0
- package/frontend-vue/tests/i18n.test.ts +86 -0
- package/frontend-vue/tests/period.test.ts +35 -0
- package/frontend-vue/tests/registration.test.ts +63 -0
- package/frontend-vue/tests/routes.test.ts +31 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +56 -0
- package/frontend-vue/vitest.config.ts +20 -0
- package/package.json +91 -0
- package/static/snapshot.js +497 -0
- package/static/tracker.js +503 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend marketing facade.
|
|
3
|
+
*
|
|
4
|
+
* Single composable wrapping every /api/marketing/* fetch the module pages
|
|
5
|
+
* need — the one file to update when endpoint shapes change (same swap-point
|
|
6
|
+
* convention as dms-api's useApiIntrospection).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// --- Shapes mirrored from the backend ---------------------------------------
|
|
10
|
+
|
|
11
|
+
export interface MarketingWebsite {
|
|
12
|
+
_id: string
|
|
13
|
+
name: string
|
|
14
|
+
domain: string
|
|
15
|
+
trackingEnabled: boolean
|
|
16
|
+
/** Opt-in: page snapshots store page content. */
|
|
17
|
+
snapshotsEnabled: boolean
|
|
18
|
+
snapshotMaskText?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Editable website fields; only the keys present are touched. */
|
|
22
|
+
export type MarketingWebsitePatch = Partial<
|
|
23
|
+
Pick<MarketingWebsite, 'name' | 'domain' | 'snapshotsEnabled' | 'snapshotMaskText'>
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
export type MarketingSnapshotLayout = 'desktop' | 'tablet' | 'phone'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One captured rendering of a path: a self-contained HTML document the
|
|
30
|
+
* heatmap is drawn over, plus the geometry it was captured in.
|
|
31
|
+
*/
|
|
32
|
+
export interface MarketingPageSnapshot {
|
|
33
|
+
layout: MarketingSnapshotLayout
|
|
34
|
+
/** False when the asked layout has no capture and the nearest stands in. */
|
|
35
|
+
exact: boolean
|
|
36
|
+
capturedAt: number
|
|
37
|
+
origin: string
|
|
38
|
+
viewport: { width: number, height: number }
|
|
39
|
+
document: { width: number, height: number }
|
|
40
|
+
colorScheme?: 'light' | 'dark'
|
|
41
|
+
bytes: number
|
|
42
|
+
html: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MarketingPageSnapshotResponse {
|
|
46
|
+
/** Null while no visit has captured the page yet. */
|
|
47
|
+
snapshot: MarketingPageSnapshot | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One observed path of a tracked site, over the requested window. */
|
|
51
|
+
export interface MarketingPageEntry {
|
|
52
|
+
path: string
|
|
53
|
+
pageviews: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MarketingPages {
|
|
57
|
+
pages: MarketingPageEntry[]
|
|
58
|
+
/** The inventory hit its cap; narrow the search to see the rest. */
|
|
59
|
+
truncated: boolean
|
|
60
|
+
/** Share of visits capturing clicks — what makes a heatmap sparse. */
|
|
61
|
+
heatmapSampleRate: number
|
|
62
|
+
/** Global collection switch; off means nothing new is being recorded. */
|
|
63
|
+
trackerEnabled: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The five-way acquisition grouping the backend derives from referrer
|
|
67
|
+
* domain + utm_medium at rollup time. */
|
|
68
|
+
export type MarketingChannel = 'direct' | 'organic' | 'social' | 'referral' | 'paid'
|
|
69
|
+
|
|
70
|
+
export interface MarketingDayStatistics {
|
|
71
|
+
day: number
|
|
72
|
+
pageviews: number
|
|
73
|
+
sessions: number
|
|
74
|
+
newVisitors: number
|
|
75
|
+
customEvents: number
|
|
76
|
+
/** Counted as transitions: a day un-bouncing sessions opened the day
|
|
77
|
+
* before can go negative. Clamp before showing a rate. */
|
|
78
|
+
bouncedSessions: number
|
|
79
|
+
sessionDurationMs: number
|
|
80
|
+
topPages: Record<string, number>
|
|
81
|
+
topEntryPages?: Record<string, number>
|
|
82
|
+
topExitPages?: Record<string, number>
|
|
83
|
+
topReferrers: Record<string, number>
|
|
84
|
+
topUtmSources: Record<string, number>
|
|
85
|
+
topUtmMediums?: Record<string, number>
|
|
86
|
+
topUtmCampaigns?: Record<string, number>
|
|
87
|
+
topUtmTerms?: Record<string, number>
|
|
88
|
+
topUtmContents?: Record<string, number>
|
|
89
|
+
topChannels?: Record<string, number>
|
|
90
|
+
topDevices?: Record<string, number>
|
|
91
|
+
topBrowsers?: Record<string, number>
|
|
92
|
+
topCountries?: Record<string, number>
|
|
93
|
+
topEvents?: Record<string, number>
|
|
94
|
+
topLanguages?: Record<string, number>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MarketingOverviewTotals {
|
|
98
|
+
pageviews: number
|
|
99
|
+
sessions: number
|
|
100
|
+
newVisitors: number
|
|
101
|
+
customEvents: number
|
|
102
|
+
bouncedSessions: number
|
|
103
|
+
sessionDurationMs: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface MarketingOverview {
|
|
107
|
+
totals: MarketingOverviewTotals
|
|
108
|
+
days: MarketingDayStatistics[]
|
|
109
|
+
topPages: Record<string, number>
|
|
110
|
+
topEntryPages: Record<string, number>
|
|
111
|
+
topExitPages: Record<string, number>
|
|
112
|
+
topReferrers: Record<string, number>
|
|
113
|
+
topUtmSources: Record<string, number>
|
|
114
|
+
topUtmMediums: Record<string, number>
|
|
115
|
+
topUtmCampaigns: Record<string, number>
|
|
116
|
+
topUtmTerms: Record<string, number>
|
|
117
|
+
topUtmContents: Record<string, number>
|
|
118
|
+
topChannels: Record<string, number>
|
|
119
|
+
topDevices: Record<string, number>
|
|
120
|
+
topBrowsers: Record<string, number>
|
|
121
|
+
/** ISO 3166-1 alpha-2 keys; only populated when the deployment configures
|
|
122
|
+
* a GeoIP database — `geoipEnabled` says whether the card should show. */
|
|
123
|
+
topCountries: Record<string, number>
|
|
124
|
+
/** Custom event names by trigger count — which CTA gets clicked. */
|
|
125
|
+
topEvents: Record<string, number>
|
|
126
|
+
/** BCP 47 tags as the visitors' browsers report them (`fr`, `fr-FR`). */
|
|
127
|
+
topLanguages: Record<string, number>
|
|
128
|
+
geoipEnabled: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One (source, medium, campaign) triple observed on new sessions; a blank
|
|
132
|
+
* slot is a UTM parameter the landing URL did not carry. */
|
|
133
|
+
export interface MarketingCampaignEntry {
|
|
134
|
+
source?: string
|
|
135
|
+
medium?: string
|
|
136
|
+
campaign?: string
|
|
137
|
+
sessions: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface MarketingCampaigns {
|
|
141
|
+
/** Exhaustive five-key split, not a top list. */
|
|
142
|
+
channels: Partial<Record<MarketingChannel, number>>
|
|
143
|
+
campaigns: MarketingCampaignEntry[]
|
|
144
|
+
/** The table hit its cap; narrow the search to see the rest. */
|
|
145
|
+
truncated: boolean
|
|
146
|
+
terms: Record<string, number>
|
|
147
|
+
contents: Record<string, number>
|
|
148
|
+
/** Referrer domains across all channels (search and social included). */
|
|
149
|
+
referrers?: Record<string, number>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface MarketingFunnelStep {
|
|
153
|
+
kind: 'url' | 'custom'
|
|
154
|
+
value: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type MarketingExperimentStatus = 'draft' | 'running' | 'stopped'
|
|
158
|
+
|
|
159
|
+
export interface MarketingExperimentVariation {
|
|
160
|
+
key: string
|
|
161
|
+
weight: number
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** One stretch of time the split served traffic; `stoppedAt` null while it
|
|
165
|
+
* still does. Written by the lifecycle transitions alone — the form sends
|
|
166
|
+
* them back untouched and the backend re-derives them. */
|
|
167
|
+
export interface MarketingExperimentRun {
|
|
168
|
+
startedAt: number
|
|
169
|
+
stoppedAt: number | null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The A/B facet of a funnel; the first variation is the control. */
|
|
173
|
+
export interface MarketingExperiment {
|
|
174
|
+
key: string
|
|
175
|
+
status: MarketingExperimentStatus
|
|
176
|
+
variations: MarketingExperimentVariation[]
|
|
177
|
+
/** Oldest first; more than one means the split was stopped and resumed.
|
|
178
|
+
* Empty exactly while the split is a draft. */
|
|
179
|
+
runs: MarketingExperimentRun[]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface MarketingFunnel {
|
|
183
|
+
_id: string
|
|
184
|
+
websiteId: string
|
|
185
|
+
name: string
|
|
186
|
+
steps: MarketingFunnelStep[]
|
|
187
|
+
conversionWindowMs: number
|
|
188
|
+
/** Null on a plain funnel. */
|
|
189
|
+
experiment?: MarketingExperiment | null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** One row of the definitions list — the data-api's Listable fields. Steps
|
|
193
|
+
* and experiment may be JSON strings; read them through parseFunnelStepsValue
|
|
194
|
+
* and parseExperimentValue. The website relation comes back expanded by the
|
|
195
|
+
* list's foreign join — the write path wants the bare id again. */
|
|
196
|
+
export interface MarketingFunnelListItem {
|
|
197
|
+
_id: string
|
|
198
|
+
websiteId: string | { _id: string, name?: string, domain?: string }
|
|
199
|
+
name: string
|
|
200
|
+
steps: MarketingFunnelStep[] | string
|
|
201
|
+
conversionWindowHours: number
|
|
202
|
+
experiment?: MarketingExperiment | string | null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** What the definitions form writes; hours because that is the field the
|
|
206
|
+
* data-api exposes (it owns the ms conversion). `experiment.status` is the
|
|
207
|
+
* transition asked for — it must be legal from the row's current one. */
|
|
208
|
+
export interface MarketingFunnelInput {
|
|
209
|
+
websiteId: string
|
|
210
|
+
name: string
|
|
211
|
+
steps: MarketingFunnelStep[]
|
|
212
|
+
/** Absent keeps the row's current window (edit) or the default (new). */
|
|
213
|
+
conversionWindowHours?: number
|
|
214
|
+
experiment?: MarketingExperiment | null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface MarketingFunnelStepResult {
|
|
218
|
+
step: MarketingFunnelStep
|
|
219
|
+
sessions: number
|
|
220
|
+
conversionRate: number
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface MarketingFunnelComputation {
|
|
224
|
+
totalSessions: number
|
|
225
|
+
steps: MarketingFunnelStepResult[]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** One arm's results. `computation` is the funnel shape, computed over the
|
|
229
|
+
* arm's exposed sessions; `conversionRate` divides the last step by
|
|
230
|
+
* `exposedSessions` — entering the funnel is itself an outcome. */
|
|
231
|
+
export interface MarketingVariationResult {
|
|
232
|
+
key: string
|
|
233
|
+
exposedSessions: number
|
|
234
|
+
computation: MarketingFunnelComputation
|
|
235
|
+
conversionRate: number
|
|
236
|
+
/** Relative to the control's conversion; null on the control itself. */
|
|
237
|
+
uplift: number | null
|
|
238
|
+
/** Two-sided p-value vs the control; null on the control, when the sample
|
|
239
|
+
* is too small, or when the window is truncated. */
|
|
240
|
+
pValue: number | null
|
|
241
|
+
significant: boolean | null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface MarketingExperimentSrm {
|
|
245
|
+
expected: number[]
|
|
246
|
+
observed: number[]
|
|
247
|
+
pValue: number | null
|
|
248
|
+
/** True when the observed split is too far from the configured weights —
|
|
249
|
+
* assignment or exposure reporting is suspect. */
|
|
250
|
+
mismatch: boolean
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface MarketingExperimentResults {
|
|
254
|
+
/** Key of the control arm — the experiment's first variation. */
|
|
255
|
+
control: string
|
|
256
|
+
variations: MarketingVariationResult[]
|
|
257
|
+
srm: MarketingExperimentSrm
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface MarketingFunnelResults {
|
|
261
|
+
funnel: MarketingFunnel
|
|
262
|
+
/** Over every session that entered the funnel, arms or not. */
|
|
263
|
+
computation: MarketingFunnelComputation
|
|
264
|
+
/** Present when the funnel carries an experiment past draft. */
|
|
265
|
+
experiment?: MarketingExperimentResults
|
|
266
|
+
/** The raw-event window hit its cap; verdicts are withheld. */
|
|
267
|
+
truncated: boolean
|
|
268
|
+
/** Epoch ms bounds the numbers cover: the selected period on a plain
|
|
269
|
+
* funnel, the experiment's own run once one is started — a running split
|
|
270
|
+
* ends at "now", a stopped one at its stop. */
|
|
271
|
+
window: { since: number, until: number }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Where the click landed inside the element it hit. This is what the overlay
|
|
276
|
+
* projects from when it can locate the element in the rendered page; `x`/`y`
|
|
277
|
+
* are the fallback, and they depend on the visitor's window on any page
|
|
278
|
+
* shorter than it.
|
|
279
|
+
*/
|
|
280
|
+
export interface MarketingHeatmapAnchor {
|
|
281
|
+
selector: string
|
|
282
|
+
ox: number
|
|
283
|
+
oy: number
|
|
284
|
+
nth: number
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface MarketingHeatmapPoint {
|
|
288
|
+
x: number
|
|
289
|
+
y: number
|
|
290
|
+
weight: number
|
|
291
|
+
/** Absent on clicks whose target was the document. */
|
|
292
|
+
anchor?: MarketingHeatmapAnchor
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface MarketingHeatmap {
|
|
296
|
+
points: MarketingHeatmapPoint[]
|
|
297
|
+
maxWeight: number
|
|
298
|
+
totalClicks: number
|
|
299
|
+
truncated: boolean
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Scroll-depth distribution of one path. `views` counts only sampled views
|
|
304
|
+
* that scrolled at all — a view that never scrolls (every page that fits its
|
|
305
|
+
* screen included) emits no depth — so shares read "of the views that
|
|
306
|
+
* scrolled", never "of all views".
|
|
307
|
+
*/
|
|
308
|
+
export interface MarketingScrollDepth {
|
|
309
|
+
views: number
|
|
310
|
+
/** reached[d] = views whose max depth is ≥ d percent, d = 0..100. */
|
|
311
|
+
reached: number[]
|
|
312
|
+
truncated: boolean
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- Facade -----------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
/** Far above any real tenant's funnel count — the data-api list answers 10
|
|
318
|
+
* rows unless told otherwise, and caps at 100 whatever is asked. */
|
|
319
|
+
const FUNNEL_LIST_LIMIT = 100
|
|
320
|
+
|
|
321
|
+
export function useMarketingApi() {
|
|
322
|
+
const { $authFetch } = useAuthFetch()
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
listWebsites: () => $authFetch<MarketingWebsite[]>('/api/marketing/websites'),
|
|
326
|
+
|
|
327
|
+
createWebsite: (name: string, domain: string) =>
|
|
328
|
+
$authFetch<MarketingWebsite>('/api/marketing/websites', {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
body: { name, domain },
|
|
331
|
+
}),
|
|
332
|
+
|
|
333
|
+
updateWebsite: (id: string, patch: MarketingWebsitePatch) =>
|
|
334
|
+
$authFetch<MarketingWebsite>(
|
|
335
|
+
`/api/marketing/websites/${encodeURIComponent(id)}`,
|
|
336
|
+
{ method: 'PUT', body: patch },
|
|
337
|
+
),
|
|
338
|
+
|
|
339
|
+
// Query strings via ofetch's `query` option — encoding is its job.
|
|
340
|
+
listPages: (website: string, period = '7d', search = '') =>
|
|
341
|
+
$authFetch<MarketingPages>('/api/marketing/stats/pages', {
|
|
342
|
+
query: { website, period, search },
|
|
343
|
+
}),
|
|
344
|
+
|
|
345
|
+
getOverview: (website: string, period = '7d') =>
|
|
346
|
+
$authFetch<MarketingOverview>('/api/marketing/stats/overview', {
|
|
347
|
+
query: { website, period },
|
|
348
|
+
}),
|
|
349
|
+
|
|
350
|
+
getCampaigns: (website: string, period = '7d', search = '') =>
|
|
351
|
+
$authFetch<MarketingCampaigns>('/api/marketing/stats/campaigns', {
|
|
352
|
+
query: { website, period, search },
|
|
353
|
+
}),
|
|
354
|
+
|
|
355
|
+
getHeatmap: (website: string, path: string, period = '7d') =>
|
|
356
|
+
$authFetch<MarketingHeatmap>('/api/marketing/stats/heatmap', {
|
|
357
|
+
query: { website, path, period },
|
|
358
|
+
}),
|
|
359
|
+
|
|
360
|
+
getScrollDepth: (website: string, path: string, period = '7d') =>
|
|
361
|
+
$authFetch<MarketingScrollDepth>('/api/marketing/stats/scroll', {
|
|
362
|
+
query: { website, path, period },
|
|
363
|
+
}),
|
|
364
|
+
|
|
365
|
+
getSnapshot: (website: string, path: string, layout: MarketingSnapshotLayout) =>
|
|
366
|
+
$authFetch<MarketingPageSnapshotResponse>('/api/marketing/stats/snapshot', {
|
|
367
|
+
query: { website, path, layout },
|
|
368
|
+
}),
|
|
369
|
+
|
|
370
|
+
// Whole click and scroll history, every period: without `path` the whole
|
|
371
|
+
// site's.
|
|
372
|
+
resetHeatmap: (website: string, path?: string) =>
|
|
373
|
+
$authFetch<{ deleted: number }>('/api/marketing/stats/heatmap', {
|
|
374
|
+
method: 'DELETE',
|
|
375
|
+
query: { website, path },
|
|
376
|
+
}),
|
|
377
|
+
|
|
378
|
+
getFunnelResults: (id: string, period = '7d') =>
|
|
379
|
+
$authFetch<MarketingFunnelResults>(
|
|
380
|
+
`/api/marketing/funnels/${encodeURIComponent(id)}/results`,
|
|
381
|
+
{ query: { period } },
|
|
382
|
+
),
|
|
383
|
+
|
|
384
|
+
// Definitions CRUD over the funnels data-api routes. `is` is the DMS
|
|
385
|
+
// DataType compare mode — the data-api's own `eq` parses but matches
|
|
386
|
+
// nothing. The list defaults to 10 rows, hence the explicit limit.
|
|
387
|
+
listFunnels: (website: string) =>
|
|
388
|
+
$authFetch<{ results: MarketingFunnelListItem[], total: number }>(
|
|
389
|
+
'/api/marketing/tables/funnels/list',
|
|
390
|
+
{
|
|
391
|
+
query: {
|
|
392
|
+
filter_websiteId: `is:${website}`,
|
|
393
|
+
limit: FUNNEL_LIST_LIMIT,
|
|
394
|
+
sortKey: 'createdAt',
|
|
395
|
+
sortDirection: 'asc',
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
),
|
|
399
|
+
|
|
400
|
+
// The insert answers the generated keys, an array — not the row.
|
|
401
|
+
createFunnel: (input: MarketingFunnelInput) =>
|
|
402
|
+
$authFetch<string[]>('/api/marketing/tables/funnels/new', {
|
|
403
|
+
method: 'POST',
|
|
404
|
+
body: input,
|
|
405
|
+
}),
|
|
406
|
+
|
|
407
|
+
updateFunnel: (id: string, input: MarketingFunnelInput) =>
|
|
408
|
+
$authFetch<unknown>('/api/marketing/tables/funnels/edit', {
|
|
409
|
+
method: 'PUT',
|
|
410
|
+
query: { id },
|
|
411
|
+
body: input,
|
|
412
|
+
}),
|
|
413
|
+
|
|
414
|
+
deleteFunnel: (id: string) =>
|
|
415
|
+
$authFetch<unknown>('/api/marketing/tables/funnels/delete', {
|
|
416
|
+
method: 'DELETE',
|
|
417
|
+
query: { id },
|
|
418
|
+
}),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridge between the DMS period selector and the marketing endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Every marketing read takes an `Nd` window anchored on now, while the
|
|
7
|
+
* selector publishes a from/to range into a scope registry. The conversion
|
|
8
|
+
* belongs in one place, not least because the deep links between those views
|
|
9
|
+
* carry the window as a query parameter and have to map it back onto a preset.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const MS_PER_DAY = 86_400_000
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_PERIOD_DAYS = 7
|
|
15
|
+
export const DEFAULT_PERIOD = `${DEFAULT_PERIOD_DAYS}d`
|
|
16
|
+
|
|
17
|
+
/** Presets every marketing surface offers, in selector order. */
|
|
18
|
+
export const MARKETING_PERIOD_PRESETS = [
|
|
19
|
+
'today',
|
|
20
|
+
'last-7-days',
|
|
21
|
+
'last-30-days',
|
|
22
|
+
'last-90-days',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const PRESET_BY_DAYS: Record<number, string> = {
|
|
26
|
+
1: 'today',
|
|
27
|
+
7: 'last-7-days',
|
|
28
|
+
30: 'last-30-days',
|
|
29
|
+
90: 'last-90-days',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_PERIOD_PRESET = PRESET_BY_DAYS[DEFAULT_PERIOD_DAYS]
|
|
33
|
+
|
|
34
|
+
/** Day count of an `Nd` string, or the default for anything unparseable. */
|
|
35
|
+
export function periodDays(period: string | undefined): number {
|
|
36
|
+
const days = Number.parseInt(period ?? '', 10)
|
|
37
|
+
return Number.isFinite(days) && days > 0 ? days : DEFAULT_PERIOD_DAYS
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Preset matching an `Nd` window. Only exact matches map back — an arbitrary
|
|
42
|
+
* window (a custom range someone linked from) falls back to the default rather
|
|
43
|
+
* than silently rounding to a neighbouring preset.
|
|
44
|
+
*/
|
|
45
|
+
export function ndToPreset(period: string | undefined): string {
|
|
46
|
+
if (!period) {
|
|
47
|
+
return DEFAULT_PERIOD_PRESET
|
|
48
|
+
}
|
|
49
|
+
return PRESET_BY_DAYS[periodDays(period)] ?? DEFAULT_PERIOD_PRESET
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The `Nd` window currently published by a period-selector scope. */
|
|
53
|
+
export function useMarketingPeriod(scope: string): ComputedRef<string> {
|
|
54
|
+
const periodScope = usePeriodScope(scope)
|
|
55
|
+
|
|
56
|
+
return computed(() => {
|
|
57
|
+
const range = periodScope.value?.range
|
|
58
|
+
if (!range) {
|
|
59
|
+
return DEFAULT_PERIOD
|
|
60
|
+
}
|
|
61
|
+
const days = Math.round(
|
|
62
|
+
(range.to.getTime() - range.from.getTime()) / MS_PER_DAY,
|
|
63
|
+
)
|
|
64
|
+
return `${Math.max(1, days)}d`
|
|
65
|
+
})
|
|
66
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links between the module's surfaces.
|
|
3
|
+
*
|
|
4
|
+
* The `/modules/marketing` prefix comes from the page being registered under
|
|
5
|
+
* a DMS module, which also makes it platform-owner-only. Should the surfaces
|
|
6
|
+
* ever move to a tenant-reachable category, this file is the only place that
|
|
7
|
+
* has to know.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MODULE_BASE = '/modules/marketing'
|
|
11
|
+
|
|
12
|
+
interface PagesLink {
|
|
13
|
+
website?: string | null
|
|
14
|
+
path?: string | null
|
|
15
|
+
period?: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Query without the empty keys — a bare link must stay bare. */
|
|
19
|
+
function withQuery(base: string, query: Record<string, string | null | undefined>) {
|
|
20
|
+
const entries = Object.entries(query).filter(([, value]) => !!value)
|
|
21
|
+
return {
|
|
22
|
+
path: base,
|
|
23
|
+
query: Object.fromEntries(entries) as Record<string, string>,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The tracked-pages surface, optionally on a given site, path and window. */
|
|
28
|
+
export function pagesLink({ website, path, period }: PagesLink = {}) {
|
|
29
|
+
return withQuery(`${MODULE_BASE}/pages`, { website, path, period })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface CampaignsLink {
|
|
33
|
+
website?: string | null
|
|
34
|
+
period?: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The acquisition surface, optionally on a given site and window. */
|
|
38
|
+
export function campaignsLink({ website, period }: CampaignsLink = {}) {
|
|
39
|
+
return withQuery(`${MODULE_BASE}/campaigns`, { website, period })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface FunnelsLink {
|
|
43
|
+
website?: string | null
|
|
44
|
+
funnel?: string | null
|
|
45
|
+
period?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The funnels surface, optionally on a given site, funnel and window. */
|
|
49
|
+
export function funnelsLink({ website, funnel, period }: FunnelsLink = {}) {
|
|
50
|
+
return withQuery(`${MODULE_BASE}/funnels`, { website, funnel, period })
|
|
51
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useI18n } from '#dms/frontend-module'
|
|
2
|
+
import TrackerSnippet from '../components/TrackerSnippet.vue'
|
|
3
|
+
import { dmsComponent } from '../utils/dmsComponent'
|
|
4
|
+
import type { MarketingWebsite } from './useMarketingApi'
|
|
5
|
+
|
|
6
|
+
const WEBSITES_URL = '/api/marketing/websites'
|
|
7
|
+
|
|
8
|
+
const WEBSITE_FORM_SCHEMA = {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
name: { type: 'string', minLength: 1, maxLength: 100 },
|
|
12
|
+
domain: { type: 'string', minLength: 1, maxLength: 253 },
|
|
13
|
+
},
|
|
14
|
+
required: ['name', 'domain'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const WEBSITE_FORM_FIELDS = [
|
|
18
|
+
{
|
|
19
|
+
id: 'name',
|
|
20
|
+
label: '$page.marketing.websites.column.name',
|
|
21
|
+
required: true,
|
|
22
|
+
component: {
|
|
23
|
+
componentName: 'dms-input-text',
|
|
24
|
+
options: { placeholder: '$page.marketing.websites.form.name' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'domain',
|
|
29
|
+
label: '$page.marketing.websites.column.domain',
|
|
30
|
+
description: '$page.marketing.websites.form.domain_description',
|
|
31
|
+
required: true,
|
|
32
|
+
component: {
|
|
33
|
+
componentName: 'dms-input-text',
|
|
34
|
+
options: { placeholder: '$page.marketing.websites.form.domain' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
export function useMarketingWebsiteForm() {
|
|
40
|
+
const modal = useModal()
|
|
41
|
+
const { t } = useI18n()
|
|
42
|
+
|
|
43
|
+
function openCreate(): Promise<MarketingWebsite | undefined> {
|
|
44
|
+
const instance = modal.open<MarketingWebsite | undefined>({
|
|
45
|
+
title: t('page.marketing.websites.create_title'),
|
|
46
|
+
component: dmsComponent('dms-form'),
|
|
47
|
+
componentOptions: {
|
|
48
|
+
fields: WEBSITE_FORM_FIELDS,
|
|
49
|
+
schema: WEBSITE_FORM_SCHEMA,
|
|
50
|
+
submitUrl: WEBSITES_URL,
|
|
51
|
+
submitUrlMethod: 'POST',
|
|
52
|
+
onSuccessCallback: (response?: unknown) => {
|
|
53
|
+
instance.close(response as MarketingWebsite)
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
return instance.result
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function openSnippet(website: MarketingWebsite): Promise<void> {
|
|
61
|
+
return modal.open<undefined>({
|
|
62
|
+
title: t('page.marketing.websites.snippet.title'),
|
|
63
|
+
component: TrackerSnippet,
|
|
64
|
+
componentOptions: { website },
|
|
65
|
+
}).result
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { openCreate, openSnippet }
|
|
69
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { computed, ref, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import { useLatestRequest } from './useLatestRequest'
|
|
3
|
+
import { useMarketingApi, type MarketingWebsite } from './useMarketingApi'
|
|
4
|
+
import { useMarketingWebsiteForm } from './useMarketingWebsiteForm'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The tenant's tracked sites and the one currently being looked at — the
|
|
8
|
+
* preamble of every marketing surface that is not scoped by a TableView.
|
|
9
|
+
*
|
|
10
|
+
* The list is what the endpoint returns, most-recently-active first, so the
|
|
11
|
+
* default selection is a site that has something to show. A selection carried
|
|
12
|
+
* in from a deep link wins over that default, but only while it still names a
|
|
13
|
+
* site that exists: a stale link must land on a populated page, not a blank
|
|
14
|
+
* one.
|
|
15
|
+
*/
|
|
16
|
+
export interface MarketingWebsitesState {
|
|
17
|
+
websites: ComputedRef<MarketingWebsite[]>
|
|
18
|
+
/** `{ label, value }` pairs for a USelect. */
|
|
19
|
+
items: ComputedRef<{ label: string, value: string }[]>
|
|
20
|
+
selectedId: Ref<string | undefined>
|
|
21
|
+
selected: ComputedRef<MarketingWebsite | null>
|
|
22
|
+
/** True until the list has answered once, then only while it refetches. */
|
|
23
|
+
loading: ComputedRef<boolean>
|
|
24
|
+
failed: Ref<boolean>
|
|
25
|
+
load: () => Promise<void>
|
|
26
|
+
create: () => Promise<void>
|
|
27
|
+
showSnippet: () => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useMarketingWebsites(
|
|
31
|
+
initialId?: string,
|
|
32
|
+
): MarketingWebsitesState {
|
|
33
|
+
const api = useMarketingApi()
|
|
34
|
+
const form = useMarketingWebsiteForm()
|
|
35
|
+
const selectedId = ref<string | undefined>(initialId || undefined)
|
|
36
|
+
const request = useLatestRequest(() => api.listWebsites())
|
|
37
|
+
|
|
38
|
+
const websites = computed(() => request.data.value ?? [])
|
|
39
|
+
const selected = computed(
|
|
40
|
+
() => websites.value.find(site => site._id === selectedId.value) ?? null,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async function load(): Promise<void> {
|
|
44
|
+
await request.run()
|
|
45
|
+
const linked = websites.value.find(site => site._id === selectedId.value)
|
|
46
|
+
selectedId.value = linked?._id ?? websites.value[0]?._id
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function create(): Promise<void> {
|
|
50
|
+
const created = await form.openCreate()
|
|
51
|
+
if (!created) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
selectedId.value = created._id
|
|
55
|
+
await load()
|
|
56
|
+
await form.openSnippet(created)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function showSnippet(): Promise<void> {
|
|
60
|
+
if (selected.value) {
|
|
61
|
+
await form.openSnippet(selected.value)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
websites,
|
|
67
|
+
items: computed(() =>
|
|
68
|
+
websites.value.map(site => ({ label: site.name, value: site._id })),
|
|
69
|
+
),
|
|
70
|
+
selectedId,
|
|
71
|
+
selected,
|
|
72
|
+
// Not asked yet reads as loading: a component that told them apart would
|
|
73
|
+
// flash its "no website tracked" state before the first answer lands.
|
|
74
|
+
loading: computed(() => !request.settled.value || request.loading.value),
|
|
75
|
+
failed: request.failed,
|
|
76
|
+
load,
|
|
77
|
+
create,
|
|
78
|
+
showSnippet,
|
|
79
|
+
}
|
|
80
|
+
}
|