@antelopejs/dms-marketing 0.6.1 → 0.7.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/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/data-api/websites.d.ts +10 -4
- package/dist/data-api/websites.js +79 -3
- package/dist/data-api/websites.js.map +1 -1
- package/dist/db/models/page_snapshots.model.d.ts +2 -0
- package/dist/db/models/page_snapshots.model.js +6 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -1
- package/dist/db/models/websites.model.d.ts +1 -0
- package/dist/db/models/websites.model.js +9 -0
- package/dist/db/models/websites.model.js.map +1 -1
- package/dist/db/tables/page_snapshots.table.d.ts +1 -0
- package/dist/db/tables/page_snapshots.table.js +4 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -1
- package/dist/db/tables/websites.table.d.ts +2 -0
- package/dist/db/tables/websites.table.js +4 -0
- package/dist/db/tables/websites.table.js.map +1 -1
- package/dist/frontend-module.d.ts +6 -0
- package/dist/frontend-module.js +46 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/index.js +3 -11
- package/dist/index.js.map +1 -1
- package/dist/pages/acquisition/page.d.ts +1 -0
- package/dist/pages/acquisition/page.js +9 -4
- package/dist/pages/acquisition/page.js.map +1 -1
- package/dist/pages/blocks.d.ts +11 -2
- package/dist/pages/blocks.js +43 -5
- package/dist/pages/blocks.js.map +1 -1
- package/dist/pages/funnel/page.d.ts +1 -0
- package/dist/pages/funnel/page.js +1 -0
- package/dist/pages/funnel/page.js.map +1 -1
- package/dist/pages/funnels/page.d.ts +1 -0
- package/dist/pages/funnels/page.js +4 -2
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/pages/overview/page.d.ts +1 -0
- package/dist/pages/overview/page.js +5 -6
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/pages/page.d.ts +1 -0
- package/dist/pages/pages/page.js +1 -0
- package/dist/pages/pages/page.js.map +1 -1
- package/dist/pages/settings/form.js +1 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/websites/page.d.ts +2 -3
- package/dist/pages/websites/page.js +63 -5
- package/dist/pages/websites/page.js.map +1 -1
- package/dist/routes/dashboard-tracking.js +5 -1
- package/dist/routes/dashboard-tracking.js.map +1 -1
- package/dist/routes/funnels.d.ts +2 -0
- package/dist/routes/funnels.js +6 -5
- package/dist/routes/funnels.js.map +1 -1
- package/dist/routes/public-endpoints.js +1 -1
- package/dist/routes/public-endpoints.js.map +1 -1
- package/dist/routes/snapshot.d.ts +1 -1
- package/dist/routes/snapshot.js +30 -13
- package/dist/routes/snapshot.js.map +1 -1
- package/dist/routes/stats.d.ts +10 -4
- package/dist/routes/stats.js +77 -23
- package/dist/routes/stats.js.map +1 -1
- package/dist/routes/websites.d.ts +5 -1
- package/dist/routes/websites.js +31 -13
- package/dist/routes/websites.js.map +1 -1
- package/dist/services/acquisition-report.d.ts +1 -0
- package/dist/services/acquisition-report.js +1 -0
- package/dist/services/acquisition-report.js.map +1 -1
- package/dist/services/collect.js +7 -2
- package/dist/services/collect.js.map +1 -1
- package/dist/services/dashboard-tracking.d.ts +7 -14
- package/dist/services/dashboard-tracking.js +42 -26
- package/dist/services/dashboard-tracking.js.map +1 -1
- package/dist/services/funnels-table.d.ts +3 -2
- package/dist/services/funnels-table.js +9 -9
- package/dist/services/funnels-table.js.map +1 -1
- package/dist/services/heatmap.d.ts +6 -0
- package/dist/services/heatmap.js +82 -17
- package/dist/services/heatmap.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/prune.js +6 -1
- package/dist/services/prune.js.map +1 -1
- package/dist/services/realtime.d.ts +3 -0
- package/dist/services/realtime.js +49 -0
- package/dist/services/realtime.js.map +1 -0
- package/dist/services/request-body.d.ts +1 -0
- package/dist/services/request-body.js +18 -0
- package/dist/services/request-body.js.map +1 -1
- package/dist/services/sessionize.d.ts +1 -0
- package/dist/services/sessionize.js +9 -0
- package/dist/services/sessionize.js.map +1 -1
- package/dist/services/snapshots.d.ts +28 -1
- package/dist/services/snapshots.js +147 -26
- package/dist/services/snapshots.js.map +1 -1
- package/dist/services/tenant-website.d.ts +11 -0
- package/dist/services/tenant-website.js +27 -5
- package/dist/services/tenant-website.js.map +1 -1
- package/dist/services/website-report.d.ts +5 -0
- package/dist/services/website-report.js +13 -1
- package/dist/services/website-report.js.map +1 -1
- package/dist/services/workspace-scope.d.ts +31 -0
- package/dist/services/workspace-scope.js +72 -0
- package/dist/services/workspace-scope.js.map +1 -0
- package/dist/types/constants.d.ts +7 -0
- package/dist/types/constants.js +8 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/types/events.d.ts +25 -25
- package/dist/types/snapshots.d.ts +29 -29
- package/dist/types/snapshots.js +1 -1
- package/dist/types/snapshots.js.map +1 -1
- package/dist/utils/funnel-steps-type.d.ts +13 -13
- package/frontend-vue/app/components/ChannelsCard.vue +19 -2
- package/frontend-vue/app/components/Context.vue +50 -376
- package/frontend-vue/app/components/FunnelBuilder.vue +28 -22
- package/frontend-vue/app/components/FunnelFigure.vue +15 -2
- package/frontend-vue/app/components/FunnelReport.vue +3 -0
- package/frontend-vue/app/components/FunnelTemplates.vue +15 -13
- package/frontend-vue/app/components/InstallGuide.vue +1 -1
- package/frontend-vue/app/components/PagePreview.vue +330 -74
- package/frontend-vue/app/components/PagesExplorer.vue +230 -85
- package/frontend-vue/app/components/TopListTabs.vue +51 -18
- package/frontend-vue/app/components/WebsiteCard.vue +394 -0
- package/frontend-vue/app/components/WebsiteCreate.vue +159 -0
- package/frontend-vue/app/components/WebsitePicker.vue +210 -0
- package/frontend-vue/app/components/WebsiteSettings.vue +539 -0
- package/frontend-vue/app/components/WorkspaceScope.vue +119 -0
- package/frontend-vue/app/composables/useContextWebsite.ts +105 -0
- package/frontend-vue/app/composables/useCopyDraft.ts +47 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +92 -4
- package/frontend-vue/app/composables/useMarketingReads.ts +32 -23
- package/frontend-vue/app/composables/useMarketingRoutes.ts +25 -2
- package/frontend-vue/app/composables/useRecordCrumb.ts +17 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +35 -1
- package/frontend-vue/app/utils/anchor-drift.ts +58 -0
- package/frontend-vue/app/utils/drift-dismissals.ts +20 -0
- package/frontend-vue/app/utils/fill-height.ts +141 -0
- package/frontend-vue/app/utils/format.ts +21 -2
- package/frontend-vue/app/utils/funnel-draft.ts +22 -0
- package/frontend-vue/app/utils/heatmap.ts +17 -0
- package/frontend-vue/app/utils/page-snapshot.ts +75 -0
- package/frontend-vue/app/utils/snapshot-options.ts +148 -0
- package/frontend-vue/app/utils/snapshotScrollRoot.ts +93 -0
- package/frontend-vue/app/utils/website-card.ts +93 -0
- package/frontend-vue/app/utils/website-purge.ts +95 -0
- package/frontend-vue/app/utils/website-settings.ts +121 -0
- package/frontend-vue/app/utils/website-state.ts +93 -0
- package/frontend-vue/app/utils/workspace-scope.ts +51 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +112 -36
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +112 -36
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/anchor-drift.test.ts +59 -0
- package/frontend-vue/tests/build-flags.test.ts +38 -0
- package/frontend-vue/tests/context-bar.test.ts +226 -0
- package/frontend-vue/tests/copy-draft.test.ts +64 -0
- package/frontend-vue/tests/drift-dismissals.test.ts +12 -0
- package/frontend-vue/tests/fill-height.test.ts +110 -0
- package/frontend-vue/tests/format.test.ts +14 -0
- package/frontend-vue/tests/funnel-draft.test.ts +37 -0
- package/frontend-vue/tests/heatmap.test.ts +25 -0
- package/frontend-vue/tests/install-guide.test.ts +25 -0
- package/frontend-vue/tests/marketing-reads.test.ts +20 -6
- package/frontend-vue/tests/page-snapshot.test.ts +62 -0
- package/frontend-vue/tests/record-crumb.test.ts +43 -0
- package/frontend-vue/tests/registration.test.ts +4 -1
- package/frontend-vue/tests/routes.test.ts +21 -0
- package/frontend-vue/tests/save-funnel.test.ts +47 -0
- package/frontend-vue/tests/server-parity.test.ts +3 -0
- package/frontend-vue/tests/snapshot-options.test.ts +148 -0
- package/frontend-vue/tests/snapshot-scroll-root.test.ts +84 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +5 -1
- package/frontend-vue/tests/top-list-tabs.test.ts +158 -0
- package/frontend-vue/tests/website-card.test.ts +119 -0
- package/frontend-vue/tests/website-purge.test.ts +70 -0
- package/frontend-vue/tests/website-settings.test.ts +133 -0
- package/frontend-vue/tests/website-state.test.ts +96 -0
- package/frontend-vue/tests/workspace-scope-control.test.ts +113 -0
- package/frontend-vue/tests/workspace-scope.test.ts +55 -0
- package/package.json +5 -4
- package/skills/dms-marketing/SKILL.md +47 -6
- package/static/snapshot.js +539 -48
- package/static/tracker.js +154 -5
- package/frontend-vue/app/components/WebsitesGrid.vue +0 -812
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { useDmsRouter, useI18n } from '#dms/frontend-module'
|
|
4
|
+
import { useMarketingApi } from '../composables/useMarketingApi'
|
|
5
|
+
import { installLink, overviewLink } from '../composables/useMarketingRoutes'
|
|
6
|
+
import { useMarketingNow } from '../composables/useMarketingNow'
|
|
7
|
+
import {
|
|
8
|
+
formatNumber,
|
|
9
|
+
formatRelativeTime,
|
|
10
|
+
formatShortDate,
|
|
11
|
+
initials,
|
|
12
|
+
} from '../utils/format'
|
|
13
|
+
import { allowHostConfirmation, hostConflictOf } from '../utils/host-conflict'
|
|
14
|
+
import { websiteCardOf, type WebsiteRow } from '../utils/website-card'
|
|
15
|
+
import {
|
|
16
|
+
WEBSITE_STATE_DOTS,
|
|
17
|
+
websiteDisplayState,
|
|
18
|
+
websiteStateLine,
|
|
19
|
+
} from '../utils/website-state'
|
|
20
|
+
import { requestWebsiteSetting } from '../utils/website-settings'
|
|
21
|
+
import { fractionDigits } from '../constants'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One card of the Websites page's table (its `card.component`): the site's
|
|
25
|
+
* state, volumes, accepted hosts and capture options; a site still waiting
|
|
26
|
+
* for its first pageview leads to the install guide. Hosts the guard refused
|
|
27
|
+
* show with the fix. A click on the card, or its pencil, opens the site's
|
|
28
|
+
* settings modal (the table's default row action); the snapshot pill opens
|
|
29
|
+
* it on the snapshot switch.
|
|
30
|
+
*/
|
|
31
|
+
const props = withDefaults(
|
|
32
|
+
defineProps<{
|
|
33
|
+
row: WebsiteRow
|
|
34
|
+
rowId?: string
|
|
35
|
+
selected?: boolean
|
|
36
|
+
open?: () => void
|
|
37
|
+
}>(),
|
|
38
|
+
{ rowId: undefined, selected: false, open: undefined },
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const CLOCK_TICK_MS = 15_000
|
|
42
|
+
|
|
43
|
+
const { t, locale } = useI18n()
|
|
44
|
+
const router = useDmsRouter()
|
|
45
|
+
const api = useMarketingApi()
|
|
46
|
+
const toast = useToast()
|
|
47
|
+
const { confirm } = useConfirm()
|
|
48
|
+
const fieldErrors = useFieldErrors({ fields: {} })
|
|
49
|
+
|
|
50
|
+
const card = computed(() => websiteCardOf(props.row))
|
|
51
|
+
|
|
52
|
+
const now = useMarketingNow(CLOCK_TICK_MS)
|
|
53
|
+
/** The browser's zone, which a server render writes the dates in. */
|
|
54
|
+
const readerZone = readerTimeZone()
|
|
55
|
+
|
|
56
|
+
// \`live\` only while the last event is recent (website-state), as everywhere
|
|
57
|
+
// a site's state shows.
|
|
58
|
+
const displayState = computed(() => websiteDisplayState(card.value, now.value))
|
|
59
|
+
const stateLine = computed(() =>
|
|
60
|
+
websiteStateLine(card.value, now.value, t, locale.value),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
/** Opens the settings modal, on `setting` when given. */
|
|
64
|
+
function edit(setting?: 'snapshots'): void {
|
|
65
|
+
if (setting) {
|
|
66
|
+
requestWebsiteSetting(card.value.id, setting)
|
|
67
|
+
}
|
|
68
|
+
props.open?.()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A click on the card, outside its own controls, opens the settings. */
|
|
72
|
+
function onCardClick(event: MouseEvent): void {
|
|
73
|
+
const target = event.target as HTMLElement | null
|
|
74
|
+
if (target?.closest('a, button, input, [role="button"]')) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
edit()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function openOverview(): Promise<void> {
|
|
81
|
+
await api.selectContextWebsite(card.value.id)
|
|
82
|
+
await router.push(overviewLink(card.value.id))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The card action running (`resume` or a host), for its spinner. */
|
|
86
|
+
const busy = ref<string | null>(null)
|
|
87
|
+
|
|
88
|
+
async function runCardAction(
|
|
89
|
+
key: string,
|
|
90
|
+
action: () => Promise<unknown>,
|
|
91
|
+
successTitle: string,
|
|
92
|
+
errorTitle: string,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
busy.value = key
|
|
95
|
+
try {
|
|
96
|
+
await action()
|
|
97
|
+
toast.add({ color: 'success', title: successTitle })
|
|
98
|
+
// Its state and counts are the backend's: the page reads them again,
|
|
99
|
+
// the card's spinner on until it shows them.
|
|
100
|
+
await refreshPageBlocks()
|
|
101
|
+
} catch (error) {
|
|
102
|
+
const conflict = hostConflictOf(error)
|
|
103
|
+
if (conflict) {
|
|
104
|
+
toast.add({
|
|
105
|
+
color: 'error',
|
|
106
|
+
title: t(errorTitle),
|
|
107
|
+
description: t('page.marketing.errors.duplicate_host_named', {
|
|
108
|
+
...conflict,
|
|
109
|
+
}),
|
|
110
|
+
})
|
|
111
|
+
} else {
|
|
112
|
+
fieldErrors.toastError(error, { toastTitle: errorTitle })
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
busy.value = null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function allowHost(hostname: string): Promise<void> {
|
|
120
|
+
if (!(await confirm(allowHostConfirmation(t, hostname)))) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
await runCardAction(
|
|
124
|
+
hostname,
|
|
125
|
+
() => api.allowHost(card.value.id, hostname),
|
|
126
|
+
t('page.marketing.install.allowed', { host: hostname }),
|
|
127
|
+
'page.marketing.install.allow_error',
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resume(): Promise<void> {
|
|
132
|
+
return runCardAction(
|
|
133
|
+
'resume',
|
|
134
|
+
() => api.updateWebsite(card.value.id, { trackingEnabled: true }),
|
|
135
|
+
t('page.marketing.websites.resumed'),
|
|
136
|
+
'page.marketing.websites.save_error',
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<template>
|
|
142
|
+
<article
|
|
143
|
+
class="@container flex cursor-pointer flex-col overflow-hidden rounded-xl border bg-(--ui-bg) transition-colors"
|
|
144
|
+
:class="[
|
|
145
|
+
card.state === 'waiting'
|
|
146
|
+
? 'border-dashed border-warning/60'
|
|
147
|
+
: 'dms-card p-0 hover:border-primary/35',
|
|
148
|
+
{ 'ring-1 ring-primary': props.selected },
|
|
149
|
+
]"
|
|
150
|
+
@click="onCardClick"
|
|
151
|
+
>
|
|
152
|
+
<header class="flex items-start gap-3 px-4 pt-4">
|
|
153
|
+
<span
|
|
154
|
+
class="grid size-10 shrink-0 place-items-center rounded-lg bg-primary/15 font-mono text-sm font-semibold text-(--ui-color-primary-800) dark:text-primary"
|
|
155
|
+
>
|
|
156
|
+
{{ initials(card.name) }}
|
|
157
|
+
</span>
|
|
158
|
+
<span class="min-w-0 flex-1">
|
|
159
|
+
<span class="block truncate font-semibold text-highlighted">
|
|
160
|
+
{{ card.name }}
|
|
161
|
+
</span>
|
|
162
|
+
<span class="block truncate font-mono text-xs text-dimmed">
|
|
163
|
+
{{ card.domain }}
|
|
164
|
+
</span>
|
|
165
|
+
</span>
|
|
166
|
+
<UButton
|
|
167
|
+
icon="i-ph-pencil-simple"
|
|
168
|
+
size="xs"
|
|
169
|
+
color="neutral"
|
|
170
|
+
variant="ghost"
|
|
171
|
+
:aria-label="
|
|
172
|
+
t('page.marketing.websites.edit_named', { name: card.name })
|
|
173
|
+
"
|
|
174
|
+
@click="edit()"
|
|
175
|
+
/>
|
|
176
|
+
</header>
|
|
177
|
+
<p
|
|
178
|
+
class="flex items-center gap-2 px-4 pt-3 pb-3 font-mono text-xs text-muted"
|
|
179
|
+
>
|
|
180
|
+
<span
|
|
181
|
+
class="size-2 rounded-full"
|
|
182
|
+
:class="WEBSITE_STATE_DOTS[displayState]"
|
|
183
|
+
/>
|
|
184
|
+
{{ stateLine }}
|
|
185
|
+
</p>
|
|
186
|
+
|
|
187
|
+
<div
|
|
188
|
+
v-if="card.state === 'paused' || card.rejected.length > 0"
|
|
189
|
+
class="flex flex-col gap-2 px-4 pb-3"
|
|
190
|
+
>
|
|
191
|
+
<DmsBanner
|
|
192
|
+
v-if="card.state === 'paused'"
|
|
193
|
+
tone="info"
|
|
194
|
+
size="sm"
|
|
195
|
+
icon="i-ph-pause-circle"
|
|
196
|
+
:title="t('page.marketing.websites.paused_description')"
|
|
197
|
+
>
|
|
198
|
+
<template #actions>
|
|
199
|
+
<UButton
|
|
200
|
+
size="xs"
|
|
201
|
+
color="neutral"
|
|
202
|
+
variant="outline"
|
|
203
|
+
icon="i-ph-play"
|
|
204
|
+
:loading="busy === 'resume'"
|
|
205
|
+
:label="t('page.marketing.websites.resume')"
|
|
206
|
+
@click="resume"
|
|
207
|
+
/>
|
|
208
|
+
</template>
|
|
209
|
+
</DmsBanner>
|
|
210
|
+
<DmsBanner
|
|
211
|
+
v-for="refused in card.rejected"
|
|
212
|
+
:key="refused.hostname"
|
|
213
|
+
tone="warning"
|
|
214
|
+
size="sm"
|
|
215
|
+
icon="i-ph-prohibit"
|
|
216
|
+
:title="
|
|
217
|
+
t(
|
|
218
|
+
'page.marketing.websites.refused',
|
|
219
|
+
{ count: refused.count, host: refused.hostname },
|
|
220
|
+
refused.count,
|
|
221
|
+
)
|
|
222
|
+
"
|
|
223
|
+
>
|
|
224
|
+
<template #actions>
|
|
225
|
+
<UButton
|
|
226
|
+
size="xs"
|
|
227
|
+
color="neutral"
|
|
228
|
+
variant="outline"
|
|
229
|
+
:loading="busy === refused.hostname"
|
|
230
|
+
:label="t('page.marketing.install.allow')"
|
|
231
|
+
@click="allowHost(refused.hostname)"
|
|
232
|
+
/>
|
|
233
|
+
</template>
|
|
234
|
+
</DmsBanner>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<template v-if="card.state !== 'waiting'">
|
|
238
|
+
<div class="border-y border-default">
|
|
239
|
+
<!-- The window the three counts cover, whatever period the other
|
|
240
|
+
pages show. -->
|
|
241
|
+
<p class="px-4 pt-1.5 text-right font-mono text-[10px] text-dimmed">
|
|
242
|
+
{{ t('page.marketing.websites.stats.window') }}
|
|
243
|
+
</p>
|
|
244
|
+
<dl class="grid grid-cols-3">
|
|
245
|
+
<div
|
|
246
|
+
v-for="stat in ['sessions', 'pages', 'funnels'] as const"
|
|
247
|
+
:key="stat"
|
|
248
|
+
class="min-w-0 border-default px-4 pt-0.5 pb-3 not-first:border-l @max-[20rem]:px-3"
|
|
249
|
+
>
|
|
250
|
+
<dt>
|
|
251
|
+
<DmsEyebrow :label="t(`page.marketing.websites.stats.${stat}`)" />
|
|
252
|
+
</dt>
|
|
253
|
+
<dd
|
|
254
|
+
class="mt-1 font-mono text-lg font-semibold tabular-nums text-highlighted @max-[20rem]:text-base"
|
|
255
|
+
>
|
|
256
|
+
{{ formatNumber(card[stat], locale) }}
|
|
257
|
+
</dd>
|
|
258
|
+
</div>
|
|
259
|
+
</dl>
|
|
260
|
+
</div>
|
|
261
|
+
<dl class="flex flex-col gap-2 px-4 py-3 text-sm">
|
|
262
|
+
<div class="flex items-start justify-between gap-3">
|
|
263
|
+
<dt
|
|
264
|
+
class="flex shrink-0 items-center gap-2 whitespace-nowrap text-muted"
|
|
265
|
+
>
|
|
266
|
+
<UIcon name="i-ph-link" class="size-3.5" />
|
|
267
|
+
{{ t('page.marketing.websites.also_accepts') }}
|
|
268
|
+
</dt>
|
|
269
|
+
<dd class="flex min-w-0 flex-wrap justify-end gap-1">
|
|
270
|
+
<span
|
|
271
|
+
v-for="host in card.extraDomains"
|
|
272
|
+
:key="host"
|
|
273
|
+
class="max-w-full truncate rounded bg-elevated px-1.5 py-0.5 font-mono text-[11px] text-toned"
|
|
274
|
+
>
|
|
275
|
+
{{ host }}
|
|
276
|
+
</span>
|
|
277
|
+
<span
|
|
278
|
+
v-if="card.extraDomains.length === 0"
|
|
279
|
+
class="text-xs text-dimmed"
|
|
280
|
+
>
|
|
281
|
+
{{ t('page.marketing.websites.subdomains_only') }}
|
|
282
|
+
</span>
|
|
283
|
+
</dd>
|
|
284
|
+
</div>
|
|
285
|
+
<div
|
|
286
|
+
class="flex flex-wrap items-center justify-between gap-x-3 gap-y-1"
|
|
287
|
+
>
|
|
288
|
+
<dt
|
|
289
|
+
class="flex shrink-0 items-center gap-2 whitespace-nowrap text-muted"
|
|
290
|
+
>
|
|
291
|
+
<UIcon name="i-ph-camera" class="size-3.5" />
|
|
292
|
+
{{ t('page.marketing.websites.snapshots') }}
|
|
293
|
+
</dt>
|
|
294
|
+
<dd class="ml-auto">
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
class="cursor-pointer rounded-full"
|
|
298
|
+
:title="
|
|
299
|
+
t('page.marketing.websites.snapshots_edit', { name: card.name })
|
|
300
|
+
"
|
|
301
|
+
@click="edit('snapshots')"
|
|
302
|
+
>
|
|
303
|
+
<DmsStatusPill
|
|
304
|
+
size="sm"
|
|
305
|
+
:mono="false"
|
|
306
|
+
:tone="card.snapshotsEnabled ? 'success' : 'neutral'"
|
|
307
|
+
:label="
|
|
308
|
+
card.snapshotsEnabled
|
|
309
|
+
? t(
|
|
310
|
+
card.snapshotMaskText
|
|
311
|
+
? 'page.marketing.websites.snapshots_masked'
|
|
312
|
+
: 'page.marketing.websites.snapshots_on',
|
|
313
|
+
)
|
|
314
|
+
: t('page.marketing.websites.snapshots_off')
|
|
315
|
+
"
|
|
316
|
+
/>
|
|
317
|
+
</button>
|
|
318
|
+
</dd>
|
|
319
|
+
</div>
|
|
320
|
+
<div
|
|
321
|
+
class="flex flex-wrap items-center justify-between gap-x-3 gap-y-1"
|
|
322
|
+
>
|
|
323
|
+
<dt
|
|
324
|
+
class="flex shrink-0 items-center gap-2 whitespace-nowrap text-muted"
|
|
325
|
+
>
|
|
326
|
+
<UIcon name="i-ph-cursor-click" class="size-3.5" />
|
|
327
|
+
{{ t('page.marketing.websites.sampling') }}
|
|
328
|
+
</dt>
|
|
329
|
+
<dd
|
|
330
|
+
class="ml-auto text-right font-mono text-xs whitespace-nowrap text-highlighted"
|
|
331
|
+
>
|
|
332
|
+
{{ formatPercentage(card.sampleRate, locale, fractionDigits(0)) }}
|
|
333
|
+
·
|
|
334
|
+
{{ t('page.marketing.websites.sampling_default') }}
|
|
335
|
+
</dd>
|
|
336
|
+
</div>
|
|
337
|
+
</dl>
|
|
338
|
+
<footer
|
|
339
|
+
class="mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-default px-4 py-3"
|
|
340
|
+
>
|
|
341
|
+
<UButton
|
|
342
|
+
icon="i-ph-code"
|
|
343
|
+
size="sm"
|
|
344
|
+
color="neutral"
|
|
345
|
+
variant="outline"
|
|
346
|
+
:label="t('page.marketing.websites.snippet_button')"
|
|
347
|
+
:aria-label="
|
|
348
|
+
t('page.marketing.websites.snippet_named', { name: card.name })
|
|
349
|
+
"
|
|
350
|
+
:to="installLink(card.id)"
|
|
351
|
+
/>
|
|
352
|
+
<UButton
|
|
353
|
+
trailing-icon="i-ph-arrow-right"
|
|
354
|
+
size="sm"
|
|
355
|
+
color="neutral"
|
|
356
|
+
variant="ghost"
|
|
357
|
+
:label="t('page.marketing.websites.open_overview')"
|
|
358
|
+
:aria-label="
|
|
359
|
+
t('page.marketing.websites.open_overview_named', {
|
|
360
|
+
name: card.name,
|
|
361
|
+
})
|
|
362
|
+
"
|
|
363
|
+
@click="openOverview"
|
|
364
|
+
/>
|
|
365
|
+
</footer>
|
|
366
|
+
</template>
|
|
367
|
+
|
|
368
|
+
<template v-else>
|
|
369
|
+
<p class="px-4 text-sm text-muted">
|
|
370
|
+
{{
|
|
371
|
+
t(
|
|
372
|
+
card.rejected.length > 0
|
|
373
|
+
? 'page.marketing.websites.waiting_refused'
|
|
374
|
+
: 'page.marketing.websites.waiting_description',
|
|
375
|
+
{ date: formatShortDate(card.createdAt, locale, readerZone) },
|
|
376
|
+
)
|
|
377
|
+
}}
|
|
378
|
+
</p>
|
|
379
|
+
<footer class="mt-auto px-4 py-4">
|
|
380
|
+
<UButton
|
|
381
|
+
icon="i-ph-pulse"
|
|
382
|
+
size="sm"
|
|
383
|
+
:label="t('page.marketing.websites.finish_install')"
|
|
384
|
+
:aria-label="
|
|
385
|
+
t('page.marketing.websites.finish_install_named', {
|
|
386
|
+
name: card.name,
|
|
387
|
+
})
|
|
388
|
+
"
|
|
389
|
+
:to="installLink(card.id)"
|
|
390
|
+
/>
|
|
391
|
+
</footer>
|
|
392
|
+
</template>
|
|
393
|
+
</article>
|
|
394
|
+
</template>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useDmsRouter, useI18n } from '#dms/frontend-module'
|
|
4
|
+
import { useMarketingApi } from '../composables/useMarketingApi'
|
|
5
|
+
import { installLink } from '../composables/useMarketingRoutes'
|
|
6
|
+
import { hostConflictOf } from '../utils/host-conflict'
|
|
7
|
+
import { blankRequiredFields } from '../utils/website-settings'
|
|
8
|
+
import {
|
|
9
|
+
MAX_WEBSITE_DOMAIN_LENGTH,
|
|
10
|
+
MAX_WEBSITE_NAME_LENGTH,
|
|
11
|
+
} from '../constants'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* "Add a website", in the modal the Websites page's header button opens:
|
|
15
|
+
* the site's name and domain. Once created, the site becomes the context's
|
|
16
|
+
* and the install guide opens on it, at the tag to paste.
|
|
17
|
+
*/
|
|
18
|
+
const props = withDefaults(defineProps<{ containerId?: string }>(), {
|
|
19
|
+
containerId: undefined,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{ success: [] }>()
|
|
23
|
+
|
|
24
|
+
const { t } = useI18n()
|
|
25
|
+
const router = useDmsRouter()
|
|
26
|
+
const api = useMarketingApi()
|
|
27
|
+
// A field-tied error from the creation shows under its field, the rest as a toast.
|
|
28
|
+
const fieldErrors = useFieldErrors({
|
|
29
|
+
fields: { name: 'website-create-name', domain: 'website-create-domain' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const name = ref('')
|
|
33
|
+
const domain = ref('')
|
|
34
|
+
const creating = ref(false)
|
|
35
|
+
/** Created: leaving for the install guide loses nothing. */
|
|
36
|
+
const created = ref(false)
|
|
37
|
+
|
|
38
|
+
const { confirmLeave } = useUnsavedChanges({
|
|
39
|
+
dirty: () => !created.value && !!(name.value.trim() || domain.value.trim()),
|
|
40
|
+
containerId: () => props.containerId,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** Empty required fields say so under themselves. */
|
|
44
|
+
async function requireFields(): Promise<boolean> {
|
|
45
|
+
const blank = blankRequiredFields({ name: name.value, domain: domain.value })
|
|
46
|
+
for (const field of blank) {
|
|
47
|
+
await fieldErrors.setError(field, t('dms.field_errors.required'))
|
|
48
|
+
}
|
|
49
|
+
return blank.length === 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function create(): Promise<void> {
|
|
53
|
+
fieldErrors.clear()
|
|
54
|
+
if (!(await requireFields())) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
creating.value = true
|
|
58
|
+
try {
|
|
59
|
+
const website = await api.createWebsite({
|
|
60
|
+
name: name.value.trim(),
|
|
61
|
+
domain: domain.value.trim(),
|
|
62
|
+
})
|
|
63
|
+
created.value = true
|
|
64
|
+
await api.selectContextWebsite(website._id)
|
|
65
|
+
// Another page: the modal closes on its own.
|
|
66
|
+
await router.push(installLink(website._id))
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const conflict = hostConflictOf(error)
|
|
69
|
+
if (conflict) {
|
|
70
|
+
await fieldErrors.setError(
|
|
71
|
+
'domain',
|
|
72
|
+
t('page.marketing.errors.duplicate_host_named', { ...conflict }),
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
await fieldErrors.handleApiError(error, {
|
|
77
|
+
toastTitle: 'page.marketing.install.create_error',
|
|
78
|
+
})
|
|
79
|
+
} finally {
|
|
80
|
+
creating.value = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function cancel(): Promise<void> {
|
|
85
|
+
if (await confirmLeave()) {
|
|
86
|
+
created.value = true
|
|
87
|
+
emit('success')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<template>
|
|
93
|
+
<form class="flex flex-col gap-4" novalidate @submit.prevent="create">
|
|
94
|
+
<div class="grid gap-x-6 gap-y-1 md:grid-cols-2">
|
|
95
|
+
<DmsFieldRow
|
|
96
|
+
layout="stack"
|
|
97
|
+
spacing="list"
|
|
98
|
+
:inset="false"
|
|
99
|
+
label="$page.marketing.websites.fields.name"
|
|
100
|
+
label-for="website-create-name"
|
|
101
|
+
required
|
|
102
|
+
>
|
|
103
|
+
<UInput
|
|
104
|
+
id="website-create-name"
|
|
105
|
+
v-model="name"
|
|
106
|
+
class="w-full"
|
|
107
|
+
placeholder="Acme shop"
|
|
108
|
+
:maxlength="MAX_WEBSITE_NAME_LENGTH"
|
|
109
|
+
v-bind="fieldErrors.aria('name')"
|
|
110
|
+
@update:model-value="fieldErrors.clear('name')"
|
|
111
|
+
/>
|
|
112
|
+
<DmsFieldError
|
|
113
|
+
:id="fieldErrors.errorId('name')"
|
|
114
|
+
:message="fieldErrors.errors.name"
|
|
115
|
+
/>
|
|
116
|
+
</DmsFieldRow>
|
|
117
|
+
<DmsFieldRow
|
|
118
|
+
layout="stack"
|
|
119
|
+
spacing="list"
|
|
120
|
+
:inset="false"
|
|
121
|
+
label="$page.marketing.websites.fields.domain"
|
|
122
|
+
description="$page.marketing.websites.fields.domain_hint"
|
|
123
|
+
label-for="website-create-domain"
|
|
124
|
+
required
|
|
125
|
+
>
|
|
126
|
+
<UInput
|
|
127
|
+
id="website-create-domain"
|
|
128
|
+
v-model="domain"
|
|
129
|
+
class="w-full font-mono"
|
|
130
|
+
placeholder="shop.example.com"
|
|
131
|
+
:maxlength="MAX_WEBSITE_DOMAIN_LENGTH"
|
|
132
|
+
v-bind="fieldErrors.aria('domain')"
|
|
133
|
+
@update:model-value="fieldErrors.clear('domain')"
|
|
134
|
+
/>
|
|
135
|
+
<DmsFieldError
|
|
136
|
+
:id="fieldErrors.errorId('domain')"
|
|
137
|
+
:message="fieldErrors.errors.domain"
|
|
138
|
+
/>
|
|
139
|
+
</DmsFieldRow>
|
|
140
|
+
</div>
|
|
141
|
+
<DmsFormRequiredLegend />
|
|
142
|
+
<footer
|
|
143
|
+
class="flex items-center justify-end gap-2 border-t border-default pt-4"
|
|
144
|
+
>
|
|
145
|
+
<UButton
|
|
146
|
+
color="neutral"
|
|
147
|
+
variant="ghost"
|
|
148
|
+
:label="t('page.marketing.common.cancel')"
|
|
149
|
+
@click="cancel"
|
|
150
|
+
/>
|
|
151
|
+
<UButton
|
|
152
|
+
type="submit"
|
|
153
|
+
icon="i-ph-plus"
|
|
154
|
+
:loading="creating"
|
|
155
|
+
:label="t('page.marketing.install.add')"
|
|
156
|
+
/>
|
|
157
|
+
</footer>
|
|
158
|
+
</form>
|
|
159
|
+
</template>
|