@gradial/aci 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/bin/aci-darwin-arm64 +0 -0
- package/dist/astro/index.d.ts +1 -0
- package/dist/astro/index.js +7 -0
- package/dist/collect/api.d.ts +24 -0
- package/dist/collect/api.js +63 -0
- package/dist/collect/batcher.d.ts +65 -0
- package/dist/collect/batcher.js +127 -0
- package/dist/collect/config.d.ts +30 -0
- package/dist/collect/config.js +53 -0
- package/dist/collect/consent/adapters/c15t.d.ts +2 -0
- package/dist/collect/consent/adapters/c15t.js +43 -0
- package/dist/collect/consent/adapters/cookiebot.d.ts +2 -0
- package/dist/collect/consent/adapters/cookiebot.js +48 -0
- package/dist/collect/consent/adapters/onetrust.d.ts +2 -0
- package/dist/collect/consent/adapters/onetrust.js +54 -0
- package/dist/collect/consent/adapters/trustarc.d.ts +2 -0
- package/dist/collect/consent/adapters/trustarc.js +65 -0
- package/dist/collect/consent/envelope.d.ts +49 -0
- package/dist/collect/consent/envelope.js +100 -0
- package/dist/collect/consent/mapping.d.ts +62 -0
- package/dist/collect/consent/mapping.js +129 -0
- package/dist/collect/declarative.d.ts +35 -0
- package/dist/collect/declarative.js +100 -0
- package/dist/collect/gapbuffer.d.ts +55 -0
- package/dist/collect/gapbuffer.js +134 -0
- package/dist/collect/index.d.ts +6 -0
- package/dist/collect/index.js +154 -0
- package/dist/collect/linkclicks.d.ts +12 -0
- package/dist/collect/linkclicks.js +35 -0
- package/dist/collect/pageviews.d.ts +22 -0
- package/dist/collect/pageviews.js +42 -0
- package/dist/collect/transport.d.ts +22 -0
- package/dist/collect/transport.js +52 -0
- package/dist/collect/ulid.d.ts +6 -0
- package/dist/collect/ulid.js +36 -0
- package/dist/collect/variationviews.d.ts +31 -0
- package/dist/collect/variationviews.js +80 -0
- package/dist/collect/webvitals.d.ts +38 -0
- package/dist/collect/webvitals.js +99 -0
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.js +1 -0
- package/dist/content/personalization.d.ts +57 -0
- package/dist/content/personalization.js +54 -0
- package/dist/content/types.d.ts +3 -0
- package/dist/next/collect-script.d.ts +12 -0
- package/dist/next/collect-script.js +15 -0
- package/dist/next/config.js +1 -0
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/page.d.ts +2 -0
- package/dist/next/page.js +5 -2
- package/dist/providers/file.js +1 -8
- package/dist/react/VideoPlayer.d.ts +3 -3
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +1 -1
- package/dist/types/image.d.ts +27 -13
- package/dist/types/image.js +17 -17
- package/dist/types/media.d.ts +10 -4
- package/dist/types/video.d.ts +5 -2
- package/package.json +8 -9
- package/src/cli/validate-content.mjs +0 -0
- package/src/types/image.ts +32 -29
- package/templates/astro/template/src/fixtures/sections.ts +1 -1
- package/templates/astro/template/src/layouts/SiteLayout.astro +77 -1
- package/templates/astro/template/src/pages/[...slug].astro +3 -2
- package/templates/common/.content/config/site.json +5 -0
- package/templates/common/.content/schemas/site.schema.json +15 -0
- package/templates/common/README.md.tmpl +15 -0
- package/templates/nextjs/template/package.json.tmpl +2 -0
- package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +132 -0
- package/templates/nextjs/template/src/app/layout.tsx +3 -1
- package/templates/nextjs/template/src/components/sections/HomeHero.tsx +1 -1
package/src/types/image.ts
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
|
-
// Image source
|
|
4
|
+
// Image source variants
|
|
5
5
|
//
|
|
6
|
-
// Each
|
|
7
|
-
//
|
|
6
|
+
// Each source provides a primary src and may include a ready-made srcset
|
|
7
|
+
// scoped by the optional media/sizes of the slot output that produced it.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
export interface ImageSource {
|
|
11
|
-
/**
|
|
11
|
+
/** Primary derivative URL for this rendition candidate. */
|
|
12
12
|
src: string;
|
|
13
|
-
/** Intrinsic width in pixels. */
|
|
14
|
-
width
|
|
15
|
-
/** Intrinsic height in pixels. */
|
|
16
|
-
height
|
|
13
|
+
/** Intrinsic width in pixels (0 when the format carries none, e.g. SVG). */
|
|
14
|
+
width?: number;
|
|
15
|
+
/** Intrinsic height in pixels (0 when the format carries none). */
|
|
16
|
+
height?: number;
|
|
17
|
+
/** Optional width-descriptor rendition candidates. */
|
|
18
|
+
srcset?: string;
|
|
19
|
+
/** sizes attribute from the slot contract. */
|
|
20
|
+
sizes?: string;
|
|
21
|
+
/** Media query scoping this source (art direction). */
|
|
22
|
+
media?: string;
|
|
17
23
|
/** MIME type hint, e.g. image/webp. */
|
|
18
24
|
type: string;
|
|
19
25
|
}
|
|
@@ -35,7 +41,7 @@ export interface Image {
|
|
|
35
41
|
versionId: string;
|
|
36
42
|
/** Accessible description of the image content. */
|
|
37
43
|
alt: string;
|
|
38
|
-
/**
|
|
44
|
+
/** <source> candidates produced for the asset slot. */
|
|
39
45
|
sources: ImageSource[];
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -61,8 +67,11 @@ export interface SlotOutput {
|
|
|
61
67
|
|
|
62
68
|
export const ImageSourceSchema = z.object({
|
|
63
69
|
src: z.string().min(1),
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
srcset: z.string().min(1).optional(),
|
|
71
|
+
width: z.number().int().nonnegative().optional(),
|
|
72
|
+
height: z.number().int().nonnegative().optional(),
|
|
73
|
+
sizes: z.string().min(1).optional(),
|
|
74
|
+
media: z.string().min(1).optional(),
|
|
66
75
|
type: z.string().min(1),
|
|
67
76
|
});
|
|
68
77
|
|
|
@@ -81,38 +90,32 @@ export const ImageSchema = z.object({
|
|
|
81
90
|
export type ImageAttrs = Record<string, unknown>;
|
|
82
91
|
|
|
83
92
|
/**
|
|
84
|
-
*
|
|
85
|
-
* Uses the largest width; ties fall back to the first entry.
|
|
93
|
+
* The primary rendition of an image — its first source entry.
|
|
86
94
|
*/
|
|
87
95
|
export function primaryImageSource(image: Image): ImageSource {
|
|
88
|
-
return image.sources
|
|
89
|
-
source.width > largest.width ? source : largest,
|
|
90
|
-
image.sources[0],
|
|
91
|
-
);
|
|
96
|
+
return image.sources[0];
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
/**
|
|
95
100
|
* Computes the full set of <img> attributes for an Image.
|
|
96
101
|
*
|
|
97
102
|
* Framework components (React <Picture>, Astro <img>, Svelte <img>) call this
|
|
98
|
-
* and spread the result into their native element. No HTML string
|
|
103
|
+
* and spread the result into their native element. No HTML string
|
|
104
|
+
* serialization. Media-scoped sources need a <picture> wrapper and are not
|
|
105
|
+
* representable as plain <img> attributes, so srcset/sizes are taken from the
|
|
106
|
+
* first unscoped source only.
|
|
99
107
|
*/
|
|
100
108
|
export function imageAttrs(image: Image, overrides: ImageAttrs = {}): ImageAttrs {
|
|
101
|
-
const primary =
|
|
102
|
-
const
|
|
103
|
-
.map((s) => `${s.src} ${s.width}w`)
|
|
104
|
-
.join(', ');
|
|
105
|
-
const sizes = image.sources
|
|
106
|
-
.map((s) => `(max-width: ${s.width}px) ${s.width}px`)
|
|
107
|
-
.join(', ');
|
|
109
|
+
const primary = image.sources[0];
|
|
110
|
+
const unscoped = image.sources.find((s) => !s.media) ?? primary;
|
|
108
111
|
|
|
109
112
|
return {
|
|
110
113
|
src: primary.src,
|
|
111
114
|
alt: image.alt,
|
|
112
|
-
width: primary.width > 0 ? primary.width : undefined,
|
|
113
|
-
height: primary.height > 0 ? primary.height : undefined,
|
|
114
|
-
srcSet: srcset || undefined,
|
|
115
|
-
sizes: sizes || undefined,
|
|
115
|
+
width: primary.width && primary.width > 0 ? primary.width : undefined,
|
|
116
|
+
height: primary.height && primary.height > 0 ? primary.height : undefined,
|
|
117
|
+
srcSet: unscoped?.srcset || undefined,
|
|
118
|
+
sizes: unscoped?.sizes || undefined,
|
|
116
119
|
loading: 'lazy',
|
|
117
120
|
decoding: 'async',
|
|
118
121
|
...overrides,
|
|
@@ -28,7 +28,7 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
|
|
|
28
28
|
versionId: 'v0001',
|
|
29
29
|
alt: 'Seattle skyline at dusk',
|
|
30
30
|
sources: [
|
|
31
|
-
{ src: '/assets/seattle.avif', width: 1920, height: 1280, type: 'image/avif' }
|
|
31
|
+
{ src: '/assets/seattle.avif', width: 1920, height: 1280, sizes: '100vw', type: 'image/avif' }
|
|
32
32
|
]
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -6,9 +6,17 @@ interface Props {
|
|
|
6
6
|
description: string;
|
|
7
7
|
canonical?: string;
|
|
8
8
|
siteName?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Pre-rendered Gradial collect script tag (collectScriptTagHTML from
|
|
11
|
+
* '@gradial/aci/astro'). Empty string / undefined = personalization
|
|
12
|
+
* disabled, no tag. Emitted here because the Astro integration has no
|
|
13
|
+
* attribute-bearing HTML-injection seam (injectScript only injects script
|
|
14
|
+
* content).
|
|
15
|
+
*/
|
|
16
|
+
collectTag?: string;
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
const { title, description, canonical, siteName = 'Gradial' } = Astro.props;
|
|
19
|
+
const { title, description, canonical, siteName = 'Gradial', collectTag } = Astro.props;
|
|
12
20
|
---
|
|
13
21
|
|
|
14
22
|
<!doctype html>
|
|
@@ -23,8 +31,76 @@ const { title, description, canonical, siteName = 'Gradial' } = Astro.props;
|
|
|
23
31
|
<meta property="og:title" content={title} />
|
|
24
32
|
<meta property="og:description" content={description} />
|
|
25
33
|
<meta property="og:site_name" content={siteName} />
|
|
34
|
+
{collectTag ? <Fragment set:html={collectTag} /> : null}
|
|
26
35
|
</head>
|
|
27
36
|
<body>
|
|
28
37
|
<slot />
|
|
38
|
+
<script>
|
|
39
|
+
// Gradial collect ↔ c15t bridge. The collect SDK's c15t preset
|
|
40
|
+
// (data-aci-consent="c15t" on the collect tag) reads
|
|
41
|
+
// window.c15t = { consents, onConsentChange }. c15t itself publishes
|
|
42
|
+
// its zustand store as window.c15tStore instead, so this bridge
|
|
43
|
+
// mirrors any c15t store on the page into the shape the SDK reads.
|
|
44
|
+
//
|
|
45
|
+
// c15t has no Astro UI package — add a consent banner via a React
|
|
46
|
+
// island (@astrojs/react + @c15t/react) or any setup built on
|
|
47
|
+
// c15t's createConsentManagerStore; this bridge picks it up either
|
|
48
|
+
// way. Without a c15t setup the poll gives up silently and only the
|
|
49
|
+
// JS consent API (aci('consent', …) under the manual preset) writes
|
|
50
|
+
// envelopes.
|
|
51
|
+
//
|
|
52
|
+
// Consents are mirrored only after a real visitor decision
|
|
53
|
+
// (consentInfo set), never for pre-decision defaults, so the SDK
|
|
54
|
+
// cannot mistake an untouched banner for a withdrawal.
|
|
55
|
+
type Consents = Record<string, boolean>;
|
|
56
|
+
type ConsentListener = (payload: { consents: Consents }) => void;
|
|
57
|
+
interface ConsentStoreState {
|
|
58
|
+
consents?: Consents;
|
|
59
|
+
consentInfo?: unknown;
|
|
60
|
+
}
|
|
61
|
+
interface ConsentStoreLike {
|
|
62
|
+
getState(): ConsentStoreState;
|
|
63
|
+
subscribe(listener: (state: ConsentStoreState) => void): () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const listeners: ConsentListener[] = [];
|
|
67
|
+
|
|
68
|
+
function publish(consents: Consents): void {
|
|
69
|
+
const w = window as typeof window & {
|
|
70
|
+
c15t?: { consents?: Consents; onConsentChange?: (cb: ConsentListener) => void };
|
|
71
|
+
};
|
|
72
|
+
const bridge = (w.c15t ??= {});
|
|
73
|
+
bridge.consents = consents;
|
|
74
|
+
bridge.onConsentChange ??= (cb: ConsentListener) => {
|
|
75
|
+
listeners.push(cb);
|
|
76
|
+
};
|
|
77
|
+
for (const listener of [...listeners]) {
|
|
78
|
+
listener({ consents });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function onState(state: ConsentStoreState): void {
|
|
83
|
+
if (state.consentInfo && state.consents) publish(state.consents);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function connect(store: ConsentStoreLike): void {
|
|
87
|
+
onState(store.getState());
|
|
88
|
+
store.subscribe(onState);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Bounded poll for the store global — same schedule the collect SDK
|
|
92
|
+
// uses for window.c15t (250 ms for up to 10 s, then give up silently).
|
|
93
|
+
let remaining = 40;
|
|
94
|
+
function poll(): void {
|
|
95
|
+
const store = (window as { c15tStore?: ConsentStoreLike }).c15tStore;
|
|
96
|
+
if (store) {
|
|
97
|
+
connect(store);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
remaining -= 1;
|
|
101
|
+
if (remaining > 0) setTimeout(poll, 250);
|
|
102
|
+
}
|
|
103
|
+
poll();
|
|
104
|
+
</script>
|
|
29
105
|
</body>
|
|
30
106
|
</html>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { getGradialStaticPaths, getPageRuntimeRenderInput, createRenderChildren } from '@gradial/aci/astro';
|
|
2
|
+
import { getGradialStaticPaths, getPageRuntimeRenderInput, createRenderChildren, collectScriptTagHTML } from '@gradial/aci/astro';
|
|
3
3
|
import { normalizeRoute, routeMetadataForContent, resolveSlots } from '@gradial/aci/content';
|
|
4
4
|
import { FileContentProvider } from '@gradial/aci/providers/file';
|
|
5
5
|
import type { KernelPage, KernelSiteConfig, LayoutEntry, FragmentData } from '@gradial/aci/content';
|
|
@@ -36,9 +36,10 @@ const resolvedSlots = await resolveSlots(
|
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
const renderChildren = createRenderChildren(registry);
|
|
39
|
+
const collectTag = collectScriptTagHTML(siteConfig);
|
|
39
40
|
---
|
|
40
41
|
|
|
41
|
-
<SiteLayout title={meta.title} description={meta.description} canonical={meta.canonical} siteName={meta.siteName}>
|
|
42
|
+
<SiteLayout title={meta.title} description={meta.description} canonical={meta.canonical} siteName={meta.siteName} collectTag={collectTag}>
|
|
42
43
|
{layout.slots.map((slot) => {
|
|
43
44
|
const blocks = resolvedSlots[slot.name];
|
|
44
45
|
if (!blocks?.length) return null;
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"description": "Global satellite internet from anywhere on Earth.",
|
|
18
18
|
"siteName": "Meridian"
|
|
19
19
|
},
|
|
20
|
+
"personalization": {
|
|
21
|
+
"collect": true,
|
|
22
|
+
"consentPreset": "c15t",
|
|
23
|
+
"consentCategories": "measurement,marketing"
|
|
24
|
+
},
|
|
20
25
|
"navigation": [
|
|
21
26
|
{
|
|
22
27
|
"id": "home",
|
|
@@ -76,6 +76,21 @@
|
|
|
76
76
|
"description": { "type": "string" },
|
|
77
77
|
"siteName": { "type": "string" }
|
|
78
78
|
}
|
|
79
|
+
},
|
|
80
|
+
"personalization": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": true,
|
|
83
|
+
"properties": {
|
|
84
|
+
"collect": { "type": "boolean" },
|
|
85
|
+
"consentPreset": {
|
|
86
|
+
"enum": ["c15t", "onetrust", "cookiebot", "trustarc", "manual"]
|
|
87
|
+
},
|
|
88
|
+
"consentCategories": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"pattern": "^\\s*[A-Za-z0-9._-]+(\\s*,\\s*[A-Za-z0-9._-]+)*\\s*$"
|
|
91
|
+
},
|
|
92
|
+
"debug": { "type": "boolean" }
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
},
|
|
81
96
|
"$defs": {
|
|
@@ -43,3 +43,18 @@ src/design-system/ Design tokens and global styles
|
|
|
43
43
|
.agents/skills/ Skills for humans and agents
|
|
44
44
|
.aci/ Generated artifacts (compiled, schemas)
|
|
45
45
|
```
|
|
46
|
+
|
|
47
|
+
## Personalization
|
|
48
|
+
|
|
49
|
+
The `personalization` block in `.content/config/site.json` controls the
|
|
50
|
+
Gradial collect tag (`<script defer src="/_aci/aci.js" …>`); pages emit it
|
|
51
|
+
automatically when `collect` is `true`, and not at all when the block is
|
|
52
|
+
absent. Consent is bridged from c15t (`consentPreset: "c15t"`): the Next kit
|
|
53
|
+
ships a full c15t setup (`src/app/_components/ConsentManager.tsx`, offline
|
|
54
|
+
mode by default, hosted via `NEXT_PUBLIC_C15T_BACKEND_URL`); the Astro kit
|
|
55
|
+
ships the store bridge in `SiteLayout.astro` — add a banner via a React
|
|
56
|
+
island (`@astrojs/react` + `@c15t/react`) and it is picked up automatically.
|
|
57
|
+
|
|
58
|
+
Personalization requires the deployed site to be served through a Gradial
|
|
59
|
+
edge binding, which provides `/collect`, identity, and the SDK itself. See
|
|
60
|
+
the `@gradial/aci` README ("Personalization: The One-Tag Install").
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ConsentBanner,
|
|
5
|
+
ConsentDialog,
|
|
6
|
+
ConsentManagerProvider,
|
|
7
|
+
useConsentManager,
|
|
8
|
+
type ConsentManagerProviderProps,
|
|
9
|
+
} from '@c15t/nextjs';
|
|
10
|
+
import { useEffect, useMemo, type ReactNode } from 'react';
|
|
11
|
+
|
|
12
|
+
// Hosted c15t backend (consent.io or self-hosted). Unset = offline mode:
|
|
13
|
+
// consent is stored client-side and no network calls are made.
|
|
14
|
+
const C15T_BACKEND_URL = process.env.NEXT_PUBLIC_C15T_BACKEND_URL;
|
|
15
|
+
|
|
16
|
+
// The CMP's own categories. The mapping from categories to the Gradial
|
|
17
|
+
// personalization purpose lives in .content/config/site.json
|
|
18
|
+
// (personalization.consentCategories), not here.
|
|
19
|
+
const CONSENT_CATEGORIES = ['necessary', 'measurement', 'marketing'] as const;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Gradial collect bridge
|
|
23
|
+
//
|
|
24
|
+
// The Gradial collect SDK's c15t preset (the site config's
|
|
25
|
+
// `personalization.consentPreset: "c15t"`, emitted as
|
|
26
|
+
// `data-aci-consent="c15t"` on the collect tag) reads
|
|
27
|
+
// `window.c15t = { consents, onConsentChange }`. c15t itself does not
|
|
28
|
+
// publish that shape, so this bridge mirrors the store onto it — only after
|
|
29
|
+
// a real visitor decision (`consentInfo` set), never for pre-decision
|
|
30
|
+
// defaults, so the SDK cannot mistake an untouched banner for a withdrawal.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
type Consents = Record<string, boolean>;
|
|
34
|
+
type ConsentListener = (payload: { consents: Consents }) => void;
|
|
35
|
+
|
|
36
|
+
declare global {
|
|
37
|
+
interface Window {
|
|
38
|
+
c15t?: {
|
|
39
|
+
consents?: Consents;
|
|
40
|
+
onConsentChange?: (cb: ConsentListener) => void;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const consentListeners: ConsentListener[] = [];
|
|
46
|
+
|
|
47
|
+
function publishAciConsents(consents: Consents): void {
|
|
48
|
+
const bridge = (window.c15t ??= {});
|
|
49
|
+
bridge.consents = consents;
|
|
50
|
+
bridge.onConsentChange ??= (cb: ConsentListener) => {
|
|
51
|
+
consentListeners.push(cb);
|
|
52
|
+
};
|
|
53
|
+
for (const listener of [...consentListeners]) {
|
|
54
|
+
listener({ consents });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function AciConsentBridge() {
|
|
59
|
+
const { consents, consentInfo } = useConsentManager();
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!consentInfo || !consents) return;
|
|
62
|
+
publishAciConsents(consents as Consents);
|
|
63
|
+
}, [consents, consentInfo]);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Consent manager
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
function getConsentOptions(): ConsentManagerProviderProps['options'] {
|
|
72
|
+
const baseOptions = {
|
|
73
|
+
consentCategories: [...CONSENT_CATEGORIES],
|
|
74
|
+
legalLinks: {
|
|
75
|
+
privacyPolicy: {
|
|
76
|
+
href: '/privacy/',
|
|
77
|
+
target: '_self' as const,
|
|
78
|
+
label: 'Privacy policy',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (C15T_BACKEND_URL) {
|
|
84
|
+
return {
|
|
85
|
+
...baseOptions,
|
|
86
|
+
mode: 'hosted',
|
|
87
|
+
backendURL: C15T_BACKEND_URL,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...baseOptions,
|
|
93
|
+
mode: 'offline',
|
|
94
|
+
offlinePolicy: {
|
|
95
|
+
policyPacks: [
|
|
96
|
+
{
|
|
97
|
+
id: 'default-opt-in',
|
|
98
|
+
match: { isDefault: true },
|
|
99
|
+
consent: {
|
|
100
|
+
model: 'opt-in',
|
|
101
|
+
categories: [...CONSENT_CATEGORIES],
|
|
102
|
+
},
|
|
103
|
+
ui: { mode: 'banner' },
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function ConsentManager({ children }: { children: ReactNode }) {
|
|
111
|
+
const options = useMemo(() => getConsentOptions(), []);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<ConsentManagerProvider options={options}>
|
|
115
|
+
{children}
|
|
116
|
+
<AciConsentBridge />
|
|
117
|
+
<ConsentBanner
|
|
118
|
+
title="Cookie preferences"
|
|
119
|
+
description="We use cookies to keep the site working, measure performance, and support marketing."
|
|
120
|
+
acceptButtonText="Accept all"
|
|
121
|
+
rejectButtonText="Reject all"
|
|
122
|
+
customizeButtonText="Customize"
|
|
123
|
+
legalLinks={['privacyPolicy']}
|
|
124
|
+
layout={[['reject', 'accept'], 'customize']}
|
|
125
|
+
primaryButton="accept"
|
|
126
|
+
scrollLock={false}
|
|
127
|
+
hideBranding
|
|
128
|
+
/>
|
|
129
|
+
<ConsentDialog legalLinks={['privacyPolicy']} scrollLock trapFocus hideBranding />
|
|
130
|
+
</ConsentManagerProvider>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import { Inter, Space_Grotesk } from 'next/font/google';
|
|
3
3
|
import { DevRefresh } from '@gradial/aci/next';
|
|
4
|
+
import { ConsentManager } from '@/app/_components/ConsentManager';
|
|
5
|
+
import '@c15t/nextjs/styles.css';
|
|
4
6
|
import '@/design-system/styles.css';
|
|
5
7
|
|
|
6
8
|
const inter = Inter({
|
|
@@ -21,7 +23,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
|
|
21
23
|
return (
|
|
22
24
|
<html lang="en" className={`${inter.variable} ${spaceGrotesk.variable}`}>
|
|
23
25
|
<body>
|
|
24
|
-
{children}
|
|
26
|
+
<ConsentManager>{children}</ConsentManager>
|
|
25
27
|
<DevRefresh />
|
|
26
28
|
</body>
|
|
27
29
|
</html>
|