@xenterprises/nuxt-x-marketing 1.2.2 → 1.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/CHANGELOG.md +64 -0
- package/README.md +107 -284
- package/app/app.config.ts +29 -123
- package/app/app.vue +31 -100
- package/app/assets/css/x-marketing.css +10 -0
- package/app/components/X/Footer/index.vue +1 -1
- package/app/components/X/Mark/Button/BackToTop.vue +2 -2
- package/app/components/X/Mark/CTA/index.vue +19 -6
- package/app/components/X/Mark/Privacy/CookieConsent.vue +15 -16
- package/app/components/X/Mark/Privacy/GDPR.vue +1 -1
- package/app/composables/useXBlog.js +168 -8
- package/app/pages/blog/[...slug].vue +81 -69
- package/app/pages/blog/index.vue +46 -50
- package/app/pages/index.vue +7 -209
- package/app/types/config.ts +134 -0
- package/content.config.ts +35 -0
- package/nuxt.config.ts +18 -5
- package/package.json +33 -11
- package/playwright-report-smoke/index.html +90 -0
- package/app/components/X/Mark/Affiliate/ComparisonTable.vue +0 -382
- package/app/components/X/Mark/Affiliate/Disclosure.vue +0 -138
- package/app/components/X/Mark/Affiliate/ProductCard.vue +0 -431
- package/app/components/X/Mark/Affiliate/ProductDetail.vue +0 -410
- package/app/components/X/Mark/Affiliate/ProductGrid.vue +0 -101
package/app/app.config.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { XMarketingConfig } from "./types/config";
|
|
2
|
+
|
|
1
3
|
export default defineAppConfig({
|
|
2
4
|
ui: {
|
|
3
5
|
primary: "brand",
|
|
@@ -25,19 +27,24 @@ export default defineAppConfig({
|
|
|
25
27
|
},
|
|
26
28
|
},
|
|
27
29
|
xMarketing: {
|
|
28
|
-
name
|
|
29
|
-
|
|
30
|
+
/** Site name — used by the shipped shell for the SEO title template
|
|
31
|
+
* ("%s | name") and og:site_name. Leave undefined to keep bare titles. */
|
|
32
|
+
name: undefined,
|
|
33
|
+
/** Canonical site URL — used as og:url on the shipped home page. */
|
|
34
|
+
url: undefined,
|
|
30
35
|
config: {
|
|
31
|
-
markerProjectId:
|
|
36
|
+
markerProjectId: undefined,
|
|
32
37
|
emailMarketingNewsletters: {
|
|
33
|
-
organizationId:
|
|
38
|
+
organizationId: undefined,
|
|
34
39
|
},
|
|
35
40
|
},
|
|
36
41
|
header: {
|
|
42
|
+
/** Set false to opt out of the default navbar in the shipped shell. */
|
|
43
|
+
active: true as boolean,
|
|
37
44
|
logo: {
|
|
38
|
-
src:
|
|
39
|
-
srcDark:
|
|
40
|
-
alt:
|
|
45
|
+
src: undefined,
|
|
46
|
+
srcDark: undefined,
|
|
47
|
+
alt: undefined,
|
|
41
48
|
},
|
|
42
49
|
nav: {
|
|
43
50
|
buttons: [],
|
|
@@ -45,23 +52,29 @@ export default defineAppConfig({
|
|
|
45
52
|
},
|
|
46
53
|
},
|
|
47
54
|
footer: {
|
|
55
|
+
/** Set false to opt out of the default footer in the shipped shell. */
|
|
56
|
+
active: true as boolean,
|
|
48
57
|
logo: {
|
|
49
|
-
src:
|
|
50
|
-
alt:
|
|
58
|
+
src: undefined,
|
|
59
|
+
alt: undefined,
|
|
51
60
|
},
|
|
52
61
|
bg: {
|
|
53
|
-
color:
|
|
62
|
+
color: undefined,
|
|
54
63
|
img: {
|
|
55
|
-
src:
|
|
56
|
-
alt:
|
|
64
|
+
src: undefined,
|
|
65
|
+
alt: undefined,
|
|
57
66
|
},
|
|
58
67
|
},
|
|
59
|
-
body:
|
|
68
|
+
body: undefined,
|
|
60
69
|
socials: [],
|
|
61
70
|
columns: [],
|
|
62
71
|
},
|
|
72
|
+
/** Cookie-consent banner in the shipped shell; gates tracking injection. */
|
|
73
|
+
consent: {
|
|
74
|
+
active: true as boolean,
|
|
75
|
+
},
|
|
63
76
|
blog: {
|
|
64
|
-
active: true,
|
|
77
|
+
active: true as boolean,
|
|
65
78
|
title: "Blog",
|
|
66
79
|
description:
|
|
67
80
|
"Welcome to our blog. Here you can find the latest news, updates, and articles.",
|
|
@@ -94,121 +107,14 @@ export default defineAppConfig({
|
|
|
94
107
|
scripts: [],
|
|
95
108
|
// ---- Behavior ----
|
|
96
109
|
/** Auto-inject configured scripts on consent. Default: true. */
|
|
97
|
-
autoInject: true,
|
|
110
|
+
autoInject: true as boolean,
|
|
98
111
|
},
|
|
99
112
|
},
|
|
100
113
|
});
|
|
101
114
|
|
|
102
115
|
declare module "@nuxt/schema" {
|
|
103
116
|
interface AppConfigInput {
|
|
104
|
-
xMarketing?:
|
|
105
|
-
/** Project name */
|
|
106
|
-
name?: string;
|
|
107
|
-
/** Project URL */
|
|
108
|
-
url?: string;
|
|
109
|
-
config?: {
|
|
110
|
-
markerProjectId?: string;
|
|
111
|
-
emailMarketingNewsletters?: {
|
|
112
|
-
organizationId?: string;
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
header?: {
|
|
116
|
-
logo?: {
|
|
117
|
-
src?: string;
|
|
118
|
-
srcDark?: string;
|
|
119
|
-
alt?: string;
|
|
120
|
-
};
|
|
121
|
-
nav?: {
|
|
122
|
-
buttons?: {
|
|
123
|
-
label: string;
|
|
124
|
-
to: string;
|
|
125
|
-
icon?: string;
|
|
126
|
-
target?: string;
|
|
127
|
-
variant?: string;
|
|
128
|
-
square?: boolean;
|
|
129
|
-
color?: string;
|
|
130
|
-
}[];
|
|
131
|
-
links?: NavLink[];
|
|
132
|
-
};
|
|
133
|
-
};
|
|
134
|
-
footer?: {
|
|
135
|
-
body?: string;
|
|
136
|
-
bg?: {
|
|
137
|
-
color?: string;
|
|
138
|
-
img?: {
|
|
139
|
-
src?: string;
|
|
140
|
-
alt?: string;
|
|
141
|
-
};
|
|
142
|
-
};
|
|
143
|
-
logo?: {
|
|
144
|
-
src?: string;
|
|
145
|
-
alt?: string;
|
|
146
|
-
};
|
|
147
|
-
socials?: {
|
|
148
|
-
name: string;
|
|
149
|
-
url: string;
|
|
150
|
-
icon: string;
|
|
151
|
-
}[];
|
|
152
|
-
columns?: {
|
|
153
|
-
headerLabel: string;
|
|
154
|
-
links: {
|
|
155
|
-
label: string;
|
|
156
|
-
to: string;
|
|
157
|
-
target?: string;
|
|
158
|
-
}[];
|
|
159
|
-
}[];
|
|
160
|
-
/**
|
|
161
|
-
* Tracking + analytics config. Read by
|
|
162
|
-
* `<XMarkPrivacyCookieConsent>` and `useConsentTracking()`.
|
|
163
|
-
* Only fires after the matching consent category is granted.
|
|
164
|
-
*/
|
|
165
|
-
tracking?: XMarketingTracking;
|
|
166
|
-
};
|
|
117
|
+
xMarketing?: XMarketingConfig;
|
|
167
118
|
}
|
|
168
119
|
}
|
|
169
120
|
|
|
170
|
-
/**
|
|
171
|
-
* Tracking entry — use the convenience IDs for the common tools
|
|
172
|
-
* (GTM, GA4, Clarity) or `scripts[]` for everything else. Scripts
|
|
173
|
-
* are injected only after the visitor grants consent for the
|
|
174
|
-
* matching `category` (default: "analytics").
|
|
175
|
-
*/
|
|
176
|
-
interface XMarketingTracking {
|
|
177
|
-
/** Google Tag Manager container ID, e.g. "GTM-XXXXXXX". */
|
|
178
|
-
gtmId?: string;
|
|
179
|
-
/** Google Analytics 4 measurement ID, e.g. "G-XXXXXXXX". */
|
|
180
|
-
ga4Id?: string;
|
|
181
|
-
/** Microsoft Clarity project ID, e.g. "abc123def4". */
|
|
182
|
-
clarityId?: string;
|
|
183
|
-
/**
|
|
184
|
-
* Arbitrary scripts (Meta Pixel, Hotjar, Segment, LinkedIn Insight,
|
|
185
|
-
* Pinterest, TikTok, etc.). Each fires once the matching consent
|
|
186
|
-
* category is granted. Use `inline` for hand-written snippets like
|
|
187
|
-
* `gtag('js', new Date())`.
|
|
188
|
-
*/
|
|
189
|
-
scripts?: XMarketingTrackingScript[];
|
|
190
|
-
/** Auto-inject configured scripts on consent. Default: true. */
|
|
191
|
-
autoInject?: boolean;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
interface XMarketingTrackingScript {
|
|
195
|
-
/** Stable ID — used to dedupe re-injection. */
|
|
196
|
-
id: string;
|
|
197
|
-
/** External script URL (e.g. "https://www.googletagmanager.com/gtag/js?id=G-XXX"). */
|
|
198
|
-
src?: string;
|
|
199
|
-
/** Inline script body (no `src` required). */
|
|
200
|
-
inline?: string;
|
|
201
|
-
/** Consent category gate. "necessary" fires immediately; "analytics" / "marketing" gated. */
|
|
202
|
-
category?: "necessary" | "analytics" | "marketing";
|
|
203
|
-
/** Extra attributes applied to the injected <script>. */
|
|
204
|
-
attrs?: Record<string, string>;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
interface NavLink {
|
|
209
|
-
label: string;
|
|
210
|
-
to?: string;
|
|
211
|
-
target?: string;
|
|
212
|
-
children?: NavLink[];
|
|
213
|
-
}
|
|
214
|
-
|
package/app/app.vue
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="min-h-screen flex flex-col">
|
|
3
|
-
<!--
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:logo-dark="logoDark"
|
|
8
|
-
logo-alt="Company"
|
|
9
|
-
>
|
|
10
|
-
<template #actions>
|
|
11
|
-
<UButton variant="ghost" color="neutral">Sign In</UButton>
|
|
12
|
-
<UButton color="primary">Get Started</UButton>
|
|
13
|
-
</template>
|
|
14
|
-
</XMarkNavbar>
|
|
3
|
+
<!-- Default navbar — reads appConfig.xMarketing.header (logo, nav.links,
|
|
4
|
+
nav.buttons). Opt out with xMarketing.header.active: false or by
|
|
5
|
+
shipping your own app/app.vue. -->
|
|
6
|
+
<XHeaderNav v-if="headerActive" />
|
|
15
7
|
|
|
16
8
|
<!-- Main Content -->
|
|
17
9
|
<main class="flex-grow">
|
|
@@ -23,30 +15,20 @@
|
|
|
23
15
|
</UApp>
|
|
24
16
|
</main>
|
|
25
17
|
|
|
26
|
-
<!--
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
variant="default"
|
|
32
|
-
@submit="handleNewsletterSubmit"
|
|
33
|
-
/>
|
|
34
|
-
</XMarkSection>
|
|
35
|
-
|
|
36
|
-
<!-- Footer -->
|
|
37
|
-
<XMarkFooter
|
|
38
|
-
:logo="logoDark"
|
|
39
|
-
logo-alt="Company"
|
|
40
|
-
description="Building the future of modern software."
|
|
41
|
-
:social="socialLinks"
|
|
42
|
-
:columns="footerColumns"
|
|
43
|
-
:legal-links="legalLinks"
|
|
44
|
-
/>
|
|
18
|
+
<!-- Default footer — reads appConfig.xMarketing.footer (logo, body,
|
|
19
|
+
socials, columns, bg). Shows the newsletter form when
|
|
20
|
+
xMarketing.config.emailMarketingNewsletters.organizationId is set.
|
|
21
|
+
Opt out with xMarketing.footer.active: false. -->
|
|
22
|
+
<XFooter v-if="footerActive" />
|
|
45
23
|
|
|
46
24
|
<!-- Cookie consent banner — auto-shows on first visit and fires
|
|
47
25
|
any tracking scripts configured in app.config.xMarketing.tracking
|
|
48
|
-
once the visitor accepts. -->
|
|
49
|
-
<XMarkPrivacyCookieConsent
|
|
26
|
+
once the visitor accepts. Opt out with xMarketing.consent.active: false. -->
|
|
27
|
+
<XMarkPrivacyCookieConsent
|
|
28
|
+
v-if="consentActive"
|
|
29
|
+
policy-url="/cookies"
|
|
30
|
+
privacy-url="/privacy"
|
|
31
|
+
/>
|
|
50
32
|
</div>
|
|
51
33
|
</template>
|
|
52
34
|
<script setup>
|
|
@@ -54,79 +36,28 @@ import { onMounted } from "vue";
|
|
|
54
36
|
import markerSDK from "@marker.io/browser";
|
|
55
37
|
|
|
56
38
|
const appConfig = useAppConfig();
|
|
39
|
+
const xMarketing = appConfig?.xMarketing;
|
|
57
40
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// Logo configuration (replace with actual logos)
|
|
63
|
-
const logoLight = "/logo-white.svg";
|
|
64
|
-
const logoDark = "/logo-dark.svg";
|
|
41
|
+
const headerActive = computed(() => xMarketing?.header?.active !== false);
|
|
42
|
+
const footerActive = computed(() => xMarketing?.footer?.active !== false);
|
|
43
|
+
const consentActive = computed(() => xMarketing?.consent?.active !== false);
|
|
65
44
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{ label: "Blog", to: "/blog" },
|
|
71
|
-
{ label: "About", to: "/about" },
|
|
72
|
-
];
|
|
45
|
+
// xMarketing.name/url feed the default SEO wiring: when `name` is set the
|
|
46
|
+
// title template becomes "<page> | <name>" and og:site_name is emitted;
|
|
47
|
+
// the shipped home page also reads `url` for og:url.
|
|
48
|
+
const siteName = computed(() => xMarketing?.name);
|
|
73
49
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
// Footer columns
|
|
82
|
-
const footerColumns = [
|
|
83
|
-
{
|
|
84
|
-
title: "Product",
|
|
85
|
-
links: [
|
|
86
|
-
{ label: "Features", to: "/#features" },
|
|
87
|
-
{ label: "Pricing", to: "/pricing" },
|
|
88
|
-
{ label: "Integrations", to: "/integrations" },
|
|
89
|
-
{ label: "Changelog", to: "/changelog" },
|
|
90
|
-
],
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
title: "Company",
|
|
94
|
-
links: [
|
|
95
|
-
{ label: "About", to: "/about" },
|
|
96
|
-
{ label: "Blog", to: "/blog" },
|
|
97
|
-
{ label: "Careers", to: "/careers" },
|
|
98
|
-
{ label: "Contact", to: "/contact" },
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
title: "Resources",
|
|
103
|
-
links: [
|
|
104
|
-
{ label: "Documentation", to: "/docs" },
|
|
105
|
-
{ label: "Help Center", to: "/help" },
|
|
106
|
-
{ label: "Community", to: "/community" },
|
|
107
|
-
{ label: "Status", to: "/status" },
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
// Legal links
|
|
113
|
-
const legalLinks = [
|
|
114
|
-
{ label: "Privacy Policy", to: "/privacy" },
|
|
115
|
-
{ label: "Terms of Service", to: "/terms" },
|
|
116
|
-
{ label: "Cookie Policy", to: "/cookies" },
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
// Newsletter submit handler
|
|
120
|
-
const handleNewsletterSubmit = (email) => {
|
|
121
|
-
console.log("Newsletter signup:", email);
|
|
122
|
-
// Implement your newsletter signup logic here
|
|
123
|
-
};
|
|
50
|
+
useSeoMeta({
|
|
51
|
+
titleTemplate: (title) =>
|
|
52
|
+
title ? (siteName.value ? `${title} | ${siteName.value}` : title) : (siteName.value ?? ""),
|
|
53
|
+
ogSiteName: siteName,
|
|
54
|
+
});
|
|
124
55
|
|
|
125
|
-
// Marker.io widget
|
|
126
|
-
if (
|
|
56
|
+
// Marker.io widget — only loads when a project ID is configured.
|
|
57
|
+
if (xMarketing?.config?.markerProjectId) {
|
|
127
58
|
onMounted(() => {
|
|
128
59
|
markerSDK.loadWidget({
|
|
129
|
-
project:
|
|
60
|
+
project: xMarketing?.config?.markerProjectId,
|
|
130
61
|
});
|
|
131
62
|
});
|
|
132
63
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/* NOTE — the `x*` utilities and `--x-*` tokens in this file are duplicated in
|
|
2
|
+
* `nuxt-x-affiliate` (`app/assets/css/main.css`), which was ported from here.
|
|
3
|
+
* 32 class names overlap with identical values. A site extending BOTH layers
|
|
4
|
+
* loads both stylesheets and gets whichever is injected last — harmless while
|
|
5
|
+
* they match, a silent divergence if either is edited alone. Keep them in sync.
|
|
6
|
+
*
|
|
7
|
+
* Only `.xGlow-divider` / `.xGlow-divider-subtle` are unique to this file;
|
|
8
|
+
* the affiliate layer draws that rule with `<XAFDividerGlow>` instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
1
11
|
@import "tailwindcss";
|
|
2
12
|
@import "@nuxt/ui";
|
|
3
13
|
@plugin "@tailwindcss/typography";
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
class="flex flex-col lg:flex-row lg:justify-between gap-8 lg:gap-12"
|
|
33
33
|
>
|
|
34
34
|
<div class="shrink-0 lg:max-w-xl">
|
|
35
|
-
<a class="mb-8 inline-block" href="/">
|
|
35
|
+
<a v-if="footer?.logo?.src" class="mb-8 inline-block" href="/">
|
|
36
36
|
<img
|
|
37
37
|
:src="footer?.logo?.src"
|
|
38
38
|
:alt="footer?.logo?.alt"
|
|
@@ -85,14 +85,14 @@ const handleScroll = () => {
|
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
onMounted(() => {
|
|
88
|
-
if (
|
|
88
|
+
if (import.meta.client) {
|
|
89
89
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
90
90
|
handleScroll();
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
onUnmounted(() => {
|
|
95
|
-
if (
|
|
95
|
+
if (import.meta.client) {
|
|
96
96
|
window.removeEventListener('scroll', handleScroll);
|
|
97
97
|
}
|
|
98
98
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section
|
|
3
3
|
:class="[
|
|
4
|
-
'relative overflow-hidden
|
|
4
|
+
'relative overflow-hidden',
|
|
5
|
+
rounded ? 'rounded-2xl' : 'rounded-none',
|
|
5
6
|
paddingClasses,
|
|
6
7
|
bgColorClasses,
|
|
7
8
|
]"
|
|
@@ -127,6 +128,11 @@ const props = defineProps({
|
|
|
127
128
|
default: "md",
|
|
128
129
|
validator: (v) => ["sm", "md", "lg"].includes(v),
|
|
129
130
|
},
|
|
131
|
+
/** Card corners. Set false for full-bleed pre-footer CTAs. */
|
|
132
|
+
rounded: {
|
|
133
|
+
type: Boolean,
|
|
134
|
+
default: true,
|
|
135
|
+
},
|
|
130
136
|
});
|
|
131
137
|
|
|
132
138
|
// Default bg values
|
|
@@ -201,23 +207,30 @@ const containerClasses = computed(() => {
|
|
|
201
207
|
});
|
|
202
208
|
|
|
203
209
|
const iconContainerClasses = computed(() => {
|
|
210
|
+
// Keep the badge square: flex parents (inline/default column layout)
|
|
211
|
+
// default to stretch on the cross axis and can squash fixed width/height.
|
|
212
|
+
const centered = ["stacked", "centered"].includes(props.variant);
|
|
204
213
|
return [
|
|
205
|
-
"flex items-center justify-center rounded-xl",
|
|
206
|
-
|
|
214
|
+
"flex shrink-0 items-center justify-center rounded-xl aspect-square",
|
|
215
|
+
centered ? "self-center" : "self-start",
|
|
216
|
+
props.size === "lg"
|
|
217
|
+
? "size-16 min-w-16 min-h-16"
|
|
218
|
+
: "size-12 min-w-12 min-h-12",
|
|
207
219
|
isBoldBg.value ? "bg-white/20" : "bg-primary-100 dark:bg-primary-900/50",
|
|
208
220
|
];
|
|
209
221
|
});
|
|
210
222
|
|
|
211
223
|
const iconClasses = computed(() => {
|
|
212
224
|
return [
|
|
213
|
-
|
|
225
|
+
"shrink-0 block",
|
|
226
|
+
props.size === "lg" ? "size-8" : "size-6",
|
|
214
227
|
isBoldBg.value ? "text-white" : "text-primary-500",
|
|
215
228
|
];
|
|
216
229
|
});
|
|
217
230
|
|
|
218
231
|
const contentClasses = computed(() => {
|
|
219
|
-
if (props.variant === "default") {
|
|
220
|
-
return "flex-1";
|
|
232
|
+
if (props.variant === "default" || props.variant === "inline") {
|
|
233
|
+
return "flex-1 min-w-0";
|
|
221
234
|
}
|
|
222
235
|
return "";
|
|
223
236
|
});
|
|
@@ -93,10 +93,10 @@
|
|
|
93
93
|
{{ category.description }}
|
|
94
94
|
</p>
|
|
95
95
|
</div>
|
|
96
|
-
<
|
|
96
|
+
<USwitch
|
|
97
97
|
:model-value="selections[category.id]"
|
|
98
98
|
:disabled="category.required"
|
|
99
|
-
@update:model-value="(val) => (selections[category.id] = val)"
|
|
99
|
+
@update:model-value="(val: boolean) => (selections[category.id] = val)"
|
|
100
100
|
/>
|
|
101
101
|
</div>
|
|
102
102
|
</div>
|
|
@@ -149,6 +149,13 @@
|
|
|
149
149
|
<script setup lang="ts">
|
|
150
150
|
type CategoryId = "necessary" | "analytics" | "marketing" | "preferences";
|
|
151
151
|
|
|
152
|
+
const DEFAULT_SELECTIONS: Record<CategoryId, boolean> = {
|
|
153
|
+
necessary: false,
|
|
154
|
+
analytics: false,
|
|
155
|
+
marketing: false,
|
|
156
|
+
preferences: false,
|
|
157
|
+
};
|
|
158
|
+
|
|
152
159
|
interface Category {
|
|
153
160
|
id: CategoryId;
|
|
154
161
|
label: string;
|
|
@@ -251,17 +258,12 @@ const tracking = useConsentTracking();
|
|
|
251
258
|
// - The visitor hasn't made a decision yet (no localStorage key).
|
|
252
259
|
const bannerVisible = ref(false);
|
|
253
260
|
const prefsOpen = ref(false);
|
|
254
|
-
const selections = ref<Record<CategoryId, boolean>>({});
|
|
261
|
+
const selections = ref<Record<CategoryId, boolean>>({ ...DEFAULT_SELECTIONS });
|
|
255
262
|
|
|
256
263
|
const effectiveCategories = computed(() => props.categories);
|
|
257
264
|
|
|
258
265
|
function buildStateFromSelections(): Record<CategoryId, boolean> {
|
|
259
|
-
const out: Record<CategoryId, boolean> = {
|
|
260
|
-
necessary: true,
|
|
261
|
-
analytics: false,
|
|
262
|
-
marketing: false,
|
|
263
|
-
preferences: false,
|
|
264
|
-
};
|
|
266
|
+
const out: Record<CategoryId, boolean> = { ...DEFAULT_SELECTIONS };
|
|
265
267
|
for (const c of props.categories) {
|
|
266
268
|
out[c.id] = c.required ? true : Boolean(selections.value[c.id]);
|
|
267
269
|
}
|
|
@@ -269,12 +271,7 @@ function buildStateFromSelections(): Record<CategoryId, boolean> {
|
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
function buildStateAll(value: boolean): Record<CategoryId, boolean> {
|
|
272
|
-
const out: Record<CategoryId, boolean> = {
|
|
273
|
-
necessary: true,
|
|
274
|
-
analytics: false,
|
|
275
|
-
marketing: false,
|
|
276
|
-
preferences: false,
|
|
277
|
-
};
|
|
274
|
+
const out: Record<CategoryId, boolean> = { ...DEFAULT_SELECTIONS };
|
|
278
275
|
for (const c of props.categories) {
|
|
279
276
|
out[c.id] = c.required ? true : value;
|
|
280
277
|
}
|
|
@@ -282,11 +279,13 @@ function buildStateAll(value: boolean): Record<CategoryId, boolean> {
|
|
|
282
279
|
}
|
|
283
280
|
|
|
284
281
|
function initSelections() {
|
|
282
|
+
const next: Record<CategoryId, boolean> = { ...DEFAULT_SELECTIONS };
|
|
285
283
|
for (const c of props.categories) {
|
|
286
|
-
|
|
284
|
+
next[c.id] = c.required
|
|
287
285
|
? true
|
|
288
286
|
: Boolean(c.enabledByDefault ?? false);
|
|
289
287
|
}
|
|
288
|
+
selections.value = next;
|
|
290
289
|
}
|
|
291
290
|
|
|
292
291
|
function syncFromStored() {
|