Skip to content

Commit

Permalink
website(feature): add cookie banner (#640)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkue authored Nov 19, 2023
1 parent 92d99af commit c4c5ed2
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 54 deletions.
5 changes: 5 additions & 0 deletions shared/locales/de/website-common.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@
"recipient-selection-description": "Erfahre mehr über unseren Auswahlprozess",
"faq-description": "Lerne aus den Fragen anderer Leute",
"whats-next": "Was kommt als nächstes?"
},
"cookie-consent-banner": {
"text": "Dürfen wir Cookies benützen um den Traffic und die Leistung unserer Website zu analysieren? Wir sammeln niemals persönliche Daten. <a href='/privacy' class='underline'>Datenschutzrichtlinie</a>.",
"button-accept": "Akzeptieren",
"button-refuse": "Ablehnen"
}
}
5 changes: 5 additions & 0 deletions shared/locales/en/website-common.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@
"recipient-selection-description": "Learn about our selection process",
"faq-description": "Learn from other people's questions",
"whats-next": "What's next?"
},
"cookie-consent-banner": {
"text": "Do you allow us to use cookies for analyzing our website traffic and performance? We never collect any personal data. <a href='/privacy' class='underline'>Privacy Policy</a>.",
"button-accept": "Accept",
"button-refuse": "Refuse"
}
}
6 changes: 2 additions & 4 deletions ui/src/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@
--radius: 0.5rem;
}

.theme-light-blue {
--background: hsl(216, 54%, 95%);
}

.theme-dark-blue {
--si-yellow: hsl(48, 100%, 49%);
--background: hsl(216, 54%, 55%);
Expand All @@ -51,6 +47,8 @@

--primary: hsl(48, 100%, 49%);
--primary-foreground: hsl(0, 0%, 20%);

--border: hsl(60 9.1% 97.8%);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ type Section1InputProps = {

export default function Section1Form({ translations, lang, region }: Section1InputProps) {
const router = useRouter();

const formSchema = z.object({
amount: z.coerce.number().min(1).optional(),
});
Expand Down
4 changes: 0 additions & 4 deletions website/src/app/[lang]/[region]/(website)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { DefaultLayoutProps } from '@/app/[lang]/[region]';
import Footer from '@/components/footer/footer';
import Navbar from '@/components/navbar/navbar';
import { mainWebsiteLanguages, websiteRegions } from '@/i18n';
import { PropsWithChildren } from 'react';

export const generateStaticParams = () =>
websiteRegions.flatMap((region) => mainWebsiteLanguages.map((lang) => ({ lang, region })));

export default function Layout({ children, params }: PropsWithChildren<DefaultLayoutProps>) {
return (
<div className="mx-auto flex flex-col">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { CURRENCY_COOKIE } from '@/app/[lang]/[region]';
import { defaultCurrency } from '@/i18n';
import { cookies } from 'next/headers';
'use client';

import { useI18n } from '@/app/context-providers';
import { redirect } from 'next/navigation';
import { useEffect } from 'react';

export const dynamic = 'force-dynamic';

export default function Page() {
const currency = cookies().get(CURRENCY_COOKIE)?.value.toLowerCase() || defaultCurrency;
redirect('./finances/' + currency);
const { currency } = useI18n();

useEffect(() => {
redirect('./finances/' + currency?.toLowerCase());
}, []);
}
2 changes: 1 addition & 1 deletion website/src/app/[lang]/[region]/donate/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { PropsWithChildren } from 'react';

export default function Layout({ children, params: { lang, region } }: PropsWithChildren<DefaultLayoutProps>) {
return (
<div>
<div className="theme-dark-blue min-h-screen">
<Navbar lang={lang} region={region} showNavigation={false} />
<main>{children}</main>
</div>
Expand Down
30 changes: 30 additions & 0 deletions website/src/app/[lang]/[region]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { DefaultLayoutProps } from '@/app/[lang]/[region]/index';
import { CookieConsentBanner } from '@/components/tracking/cookie-consent-banner';
import { mainWebsiteLanguages, websiteRegions } from '@/i18n';
import { Translator } from '@socialincome/shared/src/utils/i18n';
import { PropsWithChildren } from 'react';
import { Toaster } from 'react-hot-toast';

export const generateStaticParams = () =>
websiteRegions.flatMap((region) => mainWebsiteLanguages.map((lang) => ({ lang, region })));

export default async function Layout({ children, params }: PropsWithChildren<DefaultLayoutProps>) {
const translator = await Translator.getInstance({
language: params.lang,
namespaces: ['website-common'],
});

return (
<>
<Toaster />
{children}
<CookieConsentBanner
translations={{
text: translator.t('cookie-consent-banner.text'),
buttonAccept: translator.t('cookie-consent-banner.button-accept'),
buttonRefuse: translator.t('cookie-consent-banner.button-refuse'),
}}
/>
</>
);
}
87 changes: 49 additions & 38 deletions website/src/app/context-providers.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
'use client';

import { CURRENCY_COOKIE, LANGUAGE_COOKIE, REGION_COOKIE } from '@/app/[lang]/[region]';
import { FacebookTracking } from '@/components/tracking/facebook-tracking';
import { LinkedInTracking } from '@/components/tracking/linkedin-tracking';
import { useCookieState } from '@/hooks/useCookieState';
import { WebsiteCurrency, WebsiteLanguage, WebsiteRegion } from '@/i18n';
import { initializeAnalytics } from '@firebase/analytics';
import { DEFAULT_REGION } from '@socialincome/shared/src/firebase';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Analytics, getAnalytics, isSupported as isAnalyticsSupported } from 'firebase/analytics';
import { ConsentSettings, ConsentStatusString, setConsent } from 'firebase/analytics';
import { connectAuthEmulator, getAuth } from 'firebase/auth';
import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore';
import { connectFunctionsEmulator, getFunctions } from 'firebase/functions';
import { connectStorageEmulator, getStorage } from 'firebase/storage';
import _ from 'lodash';
import { usePathname, useRouter } from 'next/navigation';
import { useRouter } from 'next/navigation';
import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react';
import { Toaster } from 'react-hot-toast';
import {
AnalyticsProvider,
AuthProvider,
Expand All @@ -30,26 +32,56 @@ let connectFirestoreEmulatorCalled = false;
let connectStorageEmulatorCalled = false;
let connectFunctionsEmulatorCalled = false;

export const getAnalyticsCookieConsent = (mode: ConsentStatusString) =>
({
analytics_storage: mode,
ad_storage: mode,
functionality_storage: mode,
security_storage: mode,
personalization_storage: mode,
}) as ConsentSettings;

if (typeof window !== 'undefined') {
const cookieConsent = localStorage.getItem('cookie_consent');
if (cookieConsent === 'granted') {
setConsent(getAnalyticsCookieConsent('granted'));
console.debug('Set default consent mode to granted');
} else {
setConsent(getAnalyticsCookieConsent('denied'));
console.debug('Set default consent mode to denied');
}
}

function AnalyticsProviderWrapper({ children }: PropsWithChildren) {
const app = useFirebaseApp();
const [analytics, setAnalytics] = useState<Analytics | null>(null);
const [allowTracking, setAllowTracking] = useState(false);

useEffect(() => {
if (process.env.NEXT_PUBLIC_FIREBASE_APP_ID) {
isAnalyticsSupported().then((isSupported) => {
if (isSupported) {
console.log('Using analytics');
setAnalytics(getAnalytics(app));
}
});
initializeAnalytics(app);
const cookieConsent = localStorage.getItem('cookie_consent');
if (cookieConsent === 'granted') {
setConsent(getAnalyticsCookieConsent('granted'));
setAllowTracking(true);
} else {
setConsent(getAnalyticsCookieConsent('denied'));
}
}
}, [app]);
}, [allowTracking]);

if (analytics) {
return <AnalyticsProvider sdk={analytics}>{children}</AnalyticsProvider>;
} else {
return children;
}
return (
<>
{allowTracking ? (
<>
<FacebookTracking />
<LinkedInTracking />
<AnalyticsProvider sdk={initializeAnalytics(app)}>{children}</AnalyticsProvider>
</>
) : (
children
)}
</>
);
}

function FirebaseSDKProviders({ children }: PropsWithChildren) {
Expand Down Expand Up @@ -99,22 +131,6 @@ function FirebaseSDKProviders({ children }: PropsWithChildren) {
);
}

function ThemeProvider({ children }: PropsWithChildren) {
const pathname = usePathname();
const baseSegment = pathname?.split('/')[3];

let theme;
switch (baseSegment) {
case 'donate':
theme = 'theme-dark-blue';
break;
default:
theme = 'theme-default';
}

return <body className={theme}>{children}</body>;
}

type I18nContextType = {
language: WebsiteLanguage | undefined;
setLanguage: (language: WebsiteLanguage) => void;
Expand Down Expand Up @@ -188,12 +204,7 @@ export function ContextProviders({ children }: PropsWithChildren) {
<FirebaseAppProvider firebaseConfig={firebaseConfig}>
<FirebaseSDKProviders>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<I18nProvider>
<Toaster />
{children}
</I18nProvider>
</ThemeProvider>
<I18nProvider>{children}</I18nProvider>
</QueryClientProvider>
</FirebaseSDKProviders>
</FirebaseAppProvider>
Expand Down
4 changes: 3 additions & 1 deletion website/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export const metadata = {
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html suppressHydrationWarning={true}>
<ContextProviders>{children}</ContextProviders>
<ContextProviders>
<body>{children}</body>
</ContextProviders>
</html>
);
}
46 changes: 46 additions & 0 deletions website/src/components/tracking/cookie-consent-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { Button, Card, CardContent, Typography } from '@socialincome/ui';
import { ConsentStatusString } from 'firebase/analytics';
import { useEffect, useState } from 'react';

type CookieConsentBannerClientProps = {
translations: {
text: string;
buttonAccept: string;
buttonRefuse: string;
};
};

export function CookieConsentBanner({ translations }: CookieConsentBannerClientProps) {
const [hideBanner, setHideBanner] = useState(true);

useEffect(() => {
const cookieConsent = localStorage.getItem('cookie_consent');
setHideBanner(Boolean(cookieConsent));
}, [setHideBanner]);

const setCookieConsent = (mode: ConsentStatusString) => {
localStorage.setItem('cookie_consent', mode);
location.reload();
};
if (hideBanner) return null;

return (
<Card className="fixed bottom-12 left-4 right-4 mx-auto max-w-6xl shadow-xl">
<CardContent className="flex flex-col space-y-2 p-4">
<Typography className="md:col-span-3">
<Typography as="span" dangerouslySetInnerHTML={{ __html: translations.text }} />
</Typography>
<div className="flex space-x-2">
<Button variant="outline" onClick={() => setCookieConsent('granted')}>
{translations.buttonAccept}
</Button>
<Button variant="destructive" onClick={() => setCookieConsent('denied')}>
{translations.buttonRefuse}
</Button>
</div>
</CardContent>
</Card>
);
}
28 changes: 28 additions & 0 deletions website/src/components/tracking/facebook-tracking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import { useEffect } from 'react';

export function FacebookTracking() {
useEffect(() => {
if (process.env.NEXT_PUBLIC_FACEBOOK_TRACKING_ID) {
console.debug('Enabling Facebook tracking');

const fbeventsScript = `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${process.env.NEXT_PUBLIC_FACEBOOK_TRACKING_ID}');
fbq('track', 'PageView');`;
const scriptElement = document.createElement('script');
scriptElement.textContent = fbeventsScript;
document.head.appendChild(scriptElement);
}
}, []);

return null;
}
22 changes: 22 additions & 0 deletions website/src/components/tracking/linkedin-tracking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

import { useEffect } from 'react';

export function LinkedInTracking() {
useEffect(() => {
if (process.env.NEXT_PUBLIC_LINKEDIN_TRACKING_ID) {
console.debug('Enabling LinkedIn tracking');
// @ts-ignore
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
// @ts-ignore
window._linkedin_data_partner_ids.push(process.env.NEXT_PUBLIC_LINKEDIN_TRACKING_ID);
const scriptElement = document.createElement('script');
scriptElement.type = 'text/javascript';
scriptElement.async = true;
scriptElement.src = 'https://snap.licdn.com/li.lms-analytics/insight.min.js';
document.head.appendChild(scriptElement);
}
}, []);

return null;
}

0 comments on commit c4c5ed2

Please sign in to comment.