diff --git a/package.json b/package.json
index c4a6aa5a7..6ac14ccde 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,8 @@
"prepare": "husky"
},
"dependencies": {
+ "@c15t/react": "^2.1.0",
+ "@c15t/scripts": "^2.1.0",
"@fingerprintjs/fingerprintjs-pro-react": "^2.7.1",
"@floating-ui/react": "^0.27.19",
"@kapaai/react-sdk": "^0.9.10",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 95f562c0b..e3548481e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,6 +21,12 @@ importers:
.:
dependencies:
+ '@c15t/react':
+ specifier: ^2.1.0
+ version: 2.1.0(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)(use-sync-external-store@1.6.0(react@19.2.3))
+ '@c15t/scripts':
+ specifier: ^2.1.0
+ version: 2.1.0
'@fingerprintjs/fingerprintjs-pro-react':
specifier: ^2.7.1
version: 2.7.1
@@ -513,6 +519,24 @@ packages:
'@braintree/sanitize-url@7.1.2':
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
+ '@c15t/react@2.1.0':
+ resolution: {integrity: sha512-tlHPjLN8R/M2xXkjoVfoiFxAy/NALVfO+DtDVUO/KL+/fqV9j1ycL489lG7s1p2jAgYeurHEnNmeb8lgjvoIDg==}
+ peerDependencies:
+ react: ^19.0.0 || ^19.0.0-rc || ^18.0.0 || ^17.0.0 || ^16.8.0
+ react-dom: ^19.0.0 || ^19.0.0-rc || ^18.0.0 || ^17.0.0 || ^16.8.0
+
+ '@c15t/schema@2.1.0':
+ resolution: {integrity: sha512-/Kxe4qv6E5cR6wgzxx1CTZuhMFfJQHi3GGFzcRCx4smqN8IrgsVtXv800O7Nz9RqoDu/8DFiHGpJ25aupwMsRQ==}
+
+ '@c15t/scripts@2.1.0':
+ resolution: {integrity: sha512-+4yU0LSeazBaEGGpx9uwVkfoQmFscnjRpx+5Y1E9b34TmoodIGrxX6klmroy71PHt+V+CnuLPU8oCD42m45jPQ==}
+
+ '@c15t/translations@2.1.0':
+ resolution: {integrity: sha512-pB6A5nMKHaAwFOUetcIkyIscMBr5iVm/lxBqzoSk3MU0KMGn4F/OTpcLZOT7k5P87RMkuuKkXN+gyf6jCuvBAw==}
+
+ '@c15t/ui@2.1.0':
+ resolution: {integrity: sha512-tl9mQnQdPbtWQQpXNFRzqrQDk4crjRUX+lIJmd7Le+uZVIuEiNbDeEIjQLj0XH0Cp3Xv3NJn0C7XF5H9CRVHKA==}
+
'@chevrotain/cst-dts-gen@11.1.2':
resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==}
@@ -4110,6 +4134,9 @@ packages:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
+ c15t@2.1.0:
+ resolution: {integrity: sha512-ukWy6y/yNGPrJv4bAr2DeMNkxtcsdehe++AeqzEC6ITDCVVaqCGeePIKo/3qMZzhyyCPtcKsQJKNIfqszyzpvQ==}
+
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -7361,6 +7388,39 @@ snapshots:
'@braintree/sanitize-url@7.1.2': {}
+ '@c15t/react@2.1.0(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@6.0.2)(use-sync-external-store@1.6.0(react@19.2.3))':
+ dependencies:
+ '@c15t/ui': 2.1.0(@types/react@19.2.14)(react@19.2.3)(typescript@6.0.2)(use-sync-external-store@1.6.0(react@19.2.3))
+ c15t: 2.1.0(@types/react@19.2.14)(react@19.2.3)(typescript@6.0.2)(use-sync-external-store@1.6.0(react@19.2.3))
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ - typescript
+ - use-sync-external-store
+
+ '@c15t/schema@2.1.0(typescript@6.0.2)':
+ dependencies:
+ valibot: 1.3.1(typescript@6.0.2)
+ transitivePeerDependencies:
+ - typescript
+
+ '@c15t/scripts@2.1.0': {}
+
+ '@c15t/translations@2.1.0': {}
+
+ '@c15t/ui@2.1.0(@types/react@19.2.14)(react@19.2.3)(typescript@6.0.2)(use-sync-external-store@1.6.0(react@19.2.3))':
+ dependencies:
+ '@c15t/translations': 2.1.0
+ c15t: 2.1.0(@types/react@19.2.14)(react@19.2.3)(typescript@6.0.2)(use-sync-external-store@1.6.0(react@19.2.3))
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ - react
+ - typescript
+ - use-sync-external-store
+
'@chevrotain/cst-dts-gen@11.1.2':
dependencies:
'@chevrotain/gast': 11.1.2
@@ -10533,6 +10593,18 @@ snapshots:
bytes@3.1.2: {}
+ c15t@2.1.0(@types/react@19.2.14)(react@19.2.3)(typescript@6.0.2)(use-sync-external-store@1.6.0(react@19.2.3)):
+ dependencies:
+ '@c15t/schema': 2.1.0(typescript@6.0.2)
+ '@c15t/translations': 2.1.0
+ zustand: 5.0.12(@types/react@19.2.14)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ - react
+ - typescript
+ - use-sync-external-store
+
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
diff --git a/src/components/ConsentManager.tsx b/src/components/ConsentManager.tsx
new file mode 100644
index 000000000..2e3d9d98d
--- /dev/null
+++ b/src/components/ConsentManager.tsx
@@ -0,0 +1,71 @@
+import * as React from 'react'
+import type { ReactNode } from 'react'
+import {
+ ConsentBanner,
+ ConsentDialog,
+ ConsentManagerProvider,
+ type ConsentManagerOptions,
+} from '@c15t/react'
+import { gtag } from '@c15t/scripts/google-tag'
+
+const C15T_BACKEND_URL = 'https://eager-kayak-phobos-tanstack-com.inth.app'
+const GOOGLE_ANALYTICS_ID = 'G-JMT1Z50SPS'
+const LEGAL_LINKS: Array<'privacyPolicy' | 'termsOfService'> = [
+ 'privacyPolicy',
+ 'termsOfService',
+]
+
+const consentManagerOptions = {
+ mode: 'hosted',
+ backendURL: C15T_BACKEND_URL,
+ consentCategories: ['necessary', 'measurement'],
+ overrides: import.meta.env.DEV ? { country: 'DE' } : undefined,
+ legalLinks: {
+ privacyPolicy: {
+ href: '/privacy',
+ target: '_self',
+ },
+ termsOfService: {
+ href: '/terms',
+ target: '_self',
+ },
+ },
+ scripts: [
+ gtag({
+ id: GOOGLE_ANALYTICS_ID,
+ category: 'measurement',
+ }),
+ ],
+} satisfies ConsentManagerOptions
+
+export function ConsentManager({
+ children,
+ showControls = true,
+}: {
+ children: ReactNode
+ showControls?: boolean
+}) {
+ const [hasMounted, setHasMounted] = React.useState(false)
+
+ React.useEffect(() => {
+ setHasMounted(true)
+ }, [])
+
+ // c15t injects runtime theme styles; mount after hydration so Start's SSR
+ // markup stays byte-stable.
+ if (!hasMounted) {
+ return <>{children}>
+ }
+
+ return (
+