Skip to content

Commit 1f03d80

Browse files
committed
feat(scripts): add consent manager ui and internationalization
1 parent 391e20d commit 1f03d80

5 files changed

Lines changed: 315 additions & 9 deletions

File tree

.changeset/strong-beds-dream.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Added consent manager UI components with Catalyst styling and next-intl integration. The `CookieBanner` and `ConsentManagerDialog` provide a customizable banner and preference dialog for cookie consent.

core/app/[locale]/layout.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ import { graphql } from '~/client/graphql';
1919
import { revalidate } from '~/client/revalidate-target';
2020
import { WebAnalyticsFragment } from '~/components/analytics/fragment';
2121
import { StreamableAnalyticsProvider } from '~/components/analytics/streamable-provider';
22+
import { ConsentManagerDialog } from '~/components/consent-manager/consent-manager-dialog';
23+
import { CookieBanner } from '~/components/consent-manager/cookie-banner';
2224
import { ContainerQueryPolyfill } from '~/components/polyfills/container-query';
2325
import { ScriptManagerScripts, ScriptsFragment } from '~/components/scripts';
2426
import { routing } from '~/i18n/routing';
27+
import { ConsentManagerProvider } from '~/lib/consent-manager';
2528
import { getToastNotification } from '~/lib/server-toast';
2629

2730
const RootLayoutMetadataQuery = graphql(
@@ -131,15 +134,19 @@ export default async function RootLayout({ params, children }: Props) {
131134
</head>
132135
<body className="flex min-h-screen flex-col">
133136
<NextIntlClientProvider>
134-
<NuqsAdapter>
135-
<StreamableAnalyticsProvider data={streamableAnalyticsData} />
136-
<Providers>
137-
{toastNotificationCookieData && (
138-
<CookieNotifications {...toastNotificationCookieData} />
139-
)}
140-
{children}
141-
</Providers>
142-
</NuqsAdapter>
137+
<ConsentManagerProvider>
138+
<NuqsAdapter>
139+
<StreamableAnalyticsProvider data={streamableAnalyticsData} />
140+
<Providers>
141+
{toastNotificationCookieData && (
142+
<CookieNotifications {...toastNotificationCookieData} />
143+
)}
144+
<ConsentManagerDialog />
145+
<CookieBanner />
146+
{children}
147+
</Providers>
148+
</NuqsAdapter>
149+
</ConsentManagerProvider>
143150
</NextIntlClientProvider>
144151
<VercelComponents />
145152
<ContainerQueryPolyfill />
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use client';
2+
3+
import {
4+
ConsentManagerDialog as C15TConsentManagerDialog,
5+
ConsentManagerWidget as C15TConsentManagerWidget,
6+
ConsentManagerDialogProps,
7+
ConsentManagerWidgetProps,
8+
useConsentManager,
9+
} from '@c15t/nextjs';
10+
import { useTranslations } from 'next-intl';
11+
import { useCallback } from 'react';
12+
13+
import { Checkbox } from '@/vibes/soul/form/checkbox';
14+
import { Button } from '@/vibes/soul/primitives/button';
15+
16+
function ConsentManagerDialogHeaderTitle() {
17+
const t = useTranslations('Components.ConsentManager.Dialog');
18+
19+
return (
20+
<C15TConsentManagerDialog.HeaderTitle asChild>
21+
<div className="font-heading !text-2xl !tracking-normal">{t('title')}</div>
22+
</C15TConsentManagerDialog.HeaderTitle>
23+
);
24+
}
25+
26+
function ConsentManagerDialogHeaderDescription() {
27+
const t = useTranslations('Components.ConsentManager.Dialog');
28+
29+
return (
30+
<C15TConsentManagerDialog.HeaderDescription asChild>
31+
<div className="font-body">{t('description')}</div>
32+
</C15TConsentManagerDialog.HeaderDescription>
33+
);
34+
}
35+
36+
export function ConsentManagerDialog(props: ConsentManagerDialogProps) {
37+
return (
38+
<C15TConsentManagerDialog.Root {...props}>
39+
<C15TConsentManagerDialog.Card>
40+
<C15TConsentManagerDialog.Header>
41+
<ConsentManagerDialogHeaderTitle />
42+
<ConsentManagerDialogHeaderDescription />
43+
</C15TConsentManagerDialog.Header>
44+
<C15TConsentManagerDialog.Content>
45+
<ConsentManagerWidget />
46+
</C15TConsentManagerDialog.Content>
47+
</C15TConsentManagerDialog.Card>
48+
</C15TConsentManagerDialog.Root>
49+
);
50+
}
51+
52+
function ConsentManagerWidgetRejectButton() {
53+
const t = useTranslations('Components.ConsentManager.Common');
54+
55+
return (
56+
<C15TConsentManagerWidget.RejectButton asChild noStyle themeKey="widget.footer.reject-button">
57+
<Button size="small" variant="tertiary">
58+
{t('rejectAll')}
59+
</Button>
60+
</C15TConsentManagerWidget.RejectButton>
61+
);
62+
}
63+
64+
function ConsentManagerWidgetAcceptAllButton() {
65+
const t = useTranslations('Components.ConsentManager.Common');
66+
67+
return (
68+
<C15TConsentManagerWidget.AcceptAllButton
69+
asChild
70+
noStyle
71+
themeKey="widget.footer.accept-button"
72+
>
73+
<Button size="small" variant="primary">
74+
{t('acceptAll')}
75+
</Button>
76+
</C15TConsentManagerWidget.AcceptAllButton>
77+
);
78+
}
79+
80+
function ConsentManagerWidgetSaveButton() {
81+
const t = useTranslations('Components.ConsentManager.Common');
82+
83+
return (
84+
<C15TConsentManagerWidget.SaveButton asChild noStyle themeKey="widget.footer.save-button">
85+
<Button size="small" variant="secondary">
86+
{t('save')}
87+
</Button>
88+
</C15TConsentManagerWidget.SaveButton>
89+
);
90+
}
91+
92+
function ConsentManagerAccordionItems() {
93+
const { selectedConsents, setSelectedConsent, getDisplayedConsents } = useConsentManager();
94+
const t = useTranslations('Components.ConsentManager.ConsentTypes');
95+
const handleConsentChange = useCallback(
96+
(
97+
name: 'necessary' | 'functionality' | 'marketing' | 'measurement' | 'experience',
98+
checked: boolean,
99+
) => {
100+
setSelectedConsent(name, checked);
101+
},
102+
[setSelectedConsent],
103+
);
104+
105+
return getDisplayedConsents().map((consent) => (
106+
<C15TConsentManagerWidget.AccordionItem
107+
key={consent.name}
108+
themeKey="widget.accordion.item"
109+
value={consent.name}
110+
>
111+
<C15TConsentManagerWidget.AccordionTrigger themeKey="widget.accordion.trigger">
112+
<C15TConsentManagerWidget.AccordionTriggerInner themeKey="widget.accordion.trigger-inner">
113+
<C15TConsentManagerWidget.AccordionArrow />
114+
{t(`${consent.name}.title`)}
115+
</C15TConsentManagerWidget.AccordionTriggerInner>
116+
117+
<Checkbox
118+
checked={selectedConsents[consent.name]}
119+
disabled={consent.disabled}
120+
onCheckedChange={(checked: boolean) => handleConsentChange(consent.name, checked)}
121+
onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation()}
122+
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => e.stopPropagation()}
123+
onKeyUp={(e: React.KeyboardEvent<HTMLButtonElement>) => e.stopPropagation()}
124+
/>
125+
</C15TConsentManagerWidget.AccordionTrigger>
126+
<C15TConsentManagerWidget.AccordionContent
127+
theme={{
128+
content: { themeKey: 'widget.accordion.content' },
129+
contentInner: { themeKey: 'widget.accordion.content-inner' },
130+
}}
131+
>
132+
{t(`${consent.name}.description`)}
133+
</C15TConsentManagerWidget.AccordionContent>
134+
</C15TConsentManagerWidget.AccordionItem>
135+
));
136+
}
137+
138+
function ConsentManagerWidget(props: ConsentManagerWidgetProps) {
139+
return (
140+
<C15TConsentManagerWidget.Root {...props}>
141+
<C15TConsentManagerWidget.Accordion type="multiple">
142+
<ConsentManagerAccordionItems />
143+
</C15TConsentManagerWidget.Accordion>
144+
<C15TConsentManagerWidget.Footer>
145+
<C15TConsentManagerWidget.FooterSubGroup themeKey="widget.footer.sub-group">
146+
<ConsentManagerWidgetRejectButton />
147+
<ConsentManagerWidgetAcceptAllButton />
148+
</C15TConsentManagerWidget.FooterSubGroup>
149+
<ConsentManagerWidgetSaveButton />
150+
</C15TConsentManagerWidget.Footer>
151+
</C15TConsentManagerWidget.Root>
152+
);
153+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use client';
2+
3+
import { CookieBanner as C15TCookieBanner, CookieBannerProps } from '@c15t/nextjs';
4+
import { useTranslations } from 'next-intl';
5+
import { PropsWithChildren } from 'react';
6+
7+
import { Button } from '@/vibes/soul/primitives/button';
8+
9+
function CookieBannerTitle() {
10+
const t = useTranslations('Components.ConsentManager.CookieBanner');
11+
12+
return (
13+
<C15TCookieBanner.Title asChild>
14+
<div className="font-heading !text-xl">{t('title')}</div>
15+
</C15TCookieBanner.Title>
16+
);
17+
}
18+
19+
function CookieBannerDescription() {
20+
const t = useTranslations('Components.ConsentManager.CookieBanner');
21+
22+
return (
23+
<C15TCookieBanner.Description asChild>
24+
<div className="font-body">{t('description')}</div>
25+
</C15TCookieBanner.Description>
26+
);
27+
}
28+
29+
function CookieBannerFooter({ children }: PropsWithChildren) {
30+
return (
31+
<C15TCookieBanner.Footer asChild>
32+
<div className="!border-none !bg-transparent !pt-0">{children}</div>
33+
</C15TCookieBanner.Footer>
34+
);
35+
}
36+
37+
function CookieBannerRejectButton() {
38+
const t = useTranslations('Components.ConsentManager.Common');
39+
40+
return (
41+
<C15TCookieBanner.RejectButton asChild noStyle themeKey="banner.footer.reject-button">
42+
<Button size="small" variant="tertiary">
43+
{t('rejectAll')}
44+
</Button>
45+
</C15TCookieBanner.RejectButton>
46+
);
47+
}
48+
49+
function CookieBannerAcceptButton() {
50+
const t = useTranslations('Components.ConsentManager.Common');
51+
52+
return (
53+
<C15TCookieBanner.AcceptButton asChild noStyle themeKey="banner.footer.accept-button">
54+
<Button size="small" variant="primary">
55+
{t('acceptAll')}
56+
</Button>
57+
</C15TCookieBanner.AcceptButton>
58+
);
59+
}
60+
61+
function CookieBannerCustomizeButton() {
62+
const t = useTranslations('Components.ConsentManager.Common');
63+
64+
return (
65+
<C15TCookieBanner.CustomizeButton asChild noStyle themeKey="banner.footer.customize-button">
66+
<Button size="small" variant="secondary">
67+
{t('customize')}
68+
</Button>
69+
</C15TCookieBanner.CustomizeButton>
70+
);
71+
}
72+
73+
export function CookieBanner({
74+
theme,
75+
noStyle,
76+
disableAnimation,
77+
scrollLock,
78+
trapFocus,
79+
}: CookieBannerProps) {
80+
return (
81+
<C15TCookieBanner.Root
82+
disableAnimation={disableAnimation}
83+
noStyle={noStyle}
84+
scrollLock={scrollLock}
85+
theme={theme}
86+
trapFocus={trapFocus}
87+
>
88+
<C15TCookieBanner.Card className="!max-w-lg">
89+
<C15TCookieBanner.Header>
90+
<CookieBannerTitle />
91+
<CookieBannerDescription />
92+
</C15TCookieBanner.Header>
93+
<CookieBannerFooter>
94+
<C15TCookieBanner.FooterSubGroup>
95+
<CookieBannerRejectButton />
96+
<CookieBannerAcceptButton />
97+
</C15TCookieBanner.FooterSubGroup>
98+
<CookieBannerCustomizeButton />
99+
</CookieBannerFooter>
100+
</C15TCookieBanner.Card>
101+
</C15TCookieBanner.Root>
102+
);
103+
}

core/messages/en.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,44 @@
470470
"placeholder": "Enter your email",
471471
"description": "Stay up to date with the latest news and offers from our store.",
472472
"success": "Thank you for your interest! Newsletter feature is coming soon!"
473+
},
474+
"ConsentManager": {
475+
"Common": {
476+
"rejectAll": "Reject All",
477+
"acceptAll": "Accept All",
478+
"customize": "Customize",
479+
"save": "Save Settings"
480+
},
481+
"CookieBanner": {
482+
"title": "We value your privacy",
483+
"description": "This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content."
484+
},
485+
"Dialog": {
486+
"title": "Privacy Settings",
487+
"description": "Customize your privacy settings here. You can choose which types of cookies and tracking technologies you would like to allow."
488+
},
489+
"ConsentTypes": {
490+
"necessary": {
491+
"title": "Strictly Necessary",
492+
"description": "These cookies are essential for the website to function properly and cannot be disabled."
493+
},
494+
"functionality": {
495+
"title": "Functionality",
496+
"description": "These cookies enable enhanced functionality and personalization of the website."
497+
},
498+
"marketing": {
499+
"title": "Marketing",
500+
"description": "These cookies are used to deliver relevant advertisements and track their effectiveness."
501+
},
502+
"measurement": {
503+
"title": "Analytics",
504+
"description": "These cookies help us understand how visitors interact with the website and improve its performance."
505+
},
506+
"experience": {
507+
"title": "Experience",
508+
"description": "These cookies help us provide a better user experience and test new features."
509+
}
510+
}
473511
}
474512
}
475513
}

0 commit comments

Comments
 (0)