Skip to content

Commit

Permalink
feat: Cookie banner / consent (#100)
Browse files Browse the repository at this point in the history
* feat: add CookieBanner component
* chore: add js-cookie package
* feat: add darkBlue shadow mixin

* feat: add cookie consent banner
* refactor: put GA in separate component, only use if cookies accepted
* refactor: setNoScroll affects html el instead of body

* feat: add focus trap
  • Loading branch information
chailandau authored Nov 30, 2023
1 parent 98ec3f9 commit c78406f
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 24 deletions.
23 changes: 4 additions & 19 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Metadata } from 'next';
import Script from 'next/script';

import { FOOTER_QUERY, HEADER_QUERY } from '@/api/graphqlQueries';
import CookieBanner from '@/components/CookieBanner';
import GoogleScript from '@/components/CookieBanner/GoogleScript';
import Footer from '@/components/Footer';
import Header from '@/components/Header/Header';
import BackToTop from '@/molecules/BackToTop';
import { getData } from '@/utils/getData';

import '@/assets/fonts/fonts.css';
import '@/styles/main.scss';

Expand All @@ -24,24 +24,9 @@ export default async function RootLayout({

return (
<html lang='en'>
<Script
strategy='afterInteractive'
src='https://www.googletagmanager.com/gtag/js?id=G-7Y1YQ9834N'
/>
<Script
id='google-analytics'
strategy='afterInteractive'
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-SPPSLWSVHZ');
`
}}
/>
<GoogleScript />
<body>
<CookieBanner />
<Header
menuItems={HeaderData?.menuItems || null}
callToAction={HeaderData?.callToAction || null}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/js-cookie": "^3.0.6",
"classnames": "^2.3.2",
"focus-trap-react": "^10.1.4",
"framer-motion": "^10.12.16",
"graphql-request": "^6.1.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"js-cookie": "^3.0.5",
"next": "^13.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
67 changes: 67 additions & 0 deletions src/components/CookieBanner/CookieBanner.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.cookie-banner {
display: flex;
position: fixed;
left: rem(12);
bottom: rem(8);
z-index: 100001;
width: calc(100% - rem(24));
p + p {
margin-top: rem(10);
}
a {
margin: rem(-10) !important;
}
&__content {
display: flex;
flex-direction: column;
gap: rem(8);
align-items: flex-start;
justify-content: space-between;
border: 2px solid color(neon, green);
border-radius: rem(20);
box-shadow: shadow(darkBlue);
background-color: color(dark, blue);
gap: rem(32);
flex-direction: column;
padding: rem(12) rem(24);
@include laptop {
flex-direction: row;
align-items: center;
}
}
&__btns {
gap: rem(12);
margin-right: auto;
@include tablet {
margin-right: unset;
}
button {
&:last-child {
&:before {
bottom: rem(16);
}
}
}
}
&__text {
font-size: rem(18) !important;
&-sm {
font-size: rem(16) !important;
}
&-container {
flex-direction: column;
gap: rem(8);
}
}
&__overlay {
display: block;
content: '';
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 100000;
background: rgba(color(neutral, black), 0.5);
}
}
124 changes: 124 additions & 0 deletions src/components/CookieBanner/CookieBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
'use client';

import FocusTrap from 'focus-trap-react';
import { m, useReducedMotion } from 'framer-motion';
import cookie from 'js-cookie';
import React, { useEffect } from 'react';

import linkStyles from '../../atoms/Link/Link.module.scss';

import styles from './CookieBanner.module.scss';

import Container from '@/atoms/Container';
import Link from '@/atoms/Link';
import Text from '@/atoms/Text';
import ButtonMolecule from '@/molecules/ButtonMolecule/ButtonMolecule';
import Flex from '@/molecules/Flex';
import Section from '@/molecules/Section';
import useStore from '@/store/useStore';
import { backToTop } from '@/utils/framer/animations';
import LazyAnimatePresence from '@/utils/framer/LazyAnimatePresence';
import { setNoScroll } from '@/utils/setNoScroll';

const CookieBanner = () => {
const { showCookieBanner, setShowCookieBanner } = useStore();

const prefersReducedMotion = useReducedMotion() || false;

useEffect(() => {
const consentCookie = cookie.get('cookieConsent');

if (!consentCookie) {
setShowCookieBanner(true);
}
}, []);
useEffect(() => {
setNoScroll(showCookieBanner);
}, [showCookieBanner]);

const handleAccept = () => {
setShowCookieBanner(false);
cookie.set('cookieConsent', 'accepted', { expires: 365 });
};

const handleReject = () => {
setShowCookieBanner(false);
cookie.set('cookieConsent', 'rejected', { expires: 365 });
};

if (!showCookieBanner) {
return null;
}

return (
<LazyAnimatePresence>
{showCookieBanner && (
<m.div
className={styles['cookie-banner']}
variants={backToTop(prefersReducedMotion)}
initial='hidden'
animate={showCookieBanner ? 'visible' : 'hidden'}
exit='hidden'
>
<FocusTrap
active={showCookieBanner}
focusTrapOptions={{
initialFocus: false
}}
>
<Section
as='div'
className={styles['cookie-banner__content']}
>
<Flex
className={
styles['cookie-banner__text-container']
}
>
<Text className={styles['cookie-banner__text']}>
Long Island Laser Tag uses cookies to
improve your browsing experience.
</Text>
<Text
size='sm'
as='span'
className={styles['cookie-banner__text-sm']}
>
Read our
<Link
href={`${
process.env
.NEXT_PUBLIC_BASE_URL as string
}/privacy-policy`}
className={
linkStyles['link__rich-text']
}
>
Privacy Policy
</Link>
to learn more.
</Text>
</Flex>

<Flex className={styles['cookie-banner__btns']}>
<ButtonMolecule onClick={handleAccept}>
Accept
</ButtonMolecule>
<ButtonMolecule
variant='link'
onClick={handleReject}
className={styles['cookie-banner__reject']}
>
Reject
</ButtonMolecule>
</Flex>
</Section>
</FocusTrap>
</m.div>
)}
<Container className={styles['cookie-banner__overlay']}></Container>
</LazyAnimatePresence>
);
};

export default CookieBanner;
43 changes: 43 additions & 0 deletions src/components/CookieBanner/GoogleScript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import cookie from 'js-cookie';
import Script from 'next/script';
import { useEffect, useState } from 'react';

import useStore from '@/store/useStore';

const GoogleScript = () => {
const [cookiesAccepted, setCookiesAccepted] = useState(false);

const { showCookieBanner } = useStore();

useEffect(() => {
setCookiesAccepted(cookie.get('cookieConsent') === 'accepted');
}, [showCookieBanner]);

return (
cookiesAccepted && (
<>
<Script
strategy='afterInteractive'
src='https://www.googletagmanager.com/gtag/js?id=G-7Y1YQ9834N'
/>
<Script
id='google-analytics'
strategy='afterInteractive'
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-SPPSLWSVHZ');
`
}}
/>
</>
)
);
};

export default GoogleScript;
3 changes: 3 additions & 0 deletions src/components/CookieBanner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import CookieBanner from './CookieBanner';

export default CookieBanner;
15 changes: 15 additions & 0 deletions src/store/createCookieBannerSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ZuSlice } from 'declarations';

export interface CookieBannerSlice {
showCookieBanner: boolean;
setShowCookieBanner: (arg: CookieBannerSlice['showCookieBanner']) => void;
}

const createCookieBannerSlice: ZuSlice<CookieBannerSlice> = (
set: (arg: () => Partial<CookieBannerSlice>) => void
) => ({
showCookieBanner: false,
setShowCookieBanner: (arg) => set(() => ({ showCookieBanner: arg }))
});

export default createCookieBannerSlice;
9 changes: 7 additions & 2 deletions src/store/useStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { create } from 'zustand';

import createCookieBannerSlice, {
CookieBannerSlice
} from './createCookieBannerSlice';

import createMenuSlice, { MenuSlice } from '@/store/createMenuSlice';

type StoreState = MenuSlice;
type StoreState = MenuSlice & CookieBannerSlice;

const useStore = create<StoreState>()((...args) => ({
...createMenuSlice(...args)
...createMenuSlice(...args),
...createCookieBannerSlice(...args)
}));

export default useStore;
3 changes: 2 additions & 1 deletion src/styles/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ $colors: (
$shadows: (
lightFuchsia: 0px 0px 20px rgba(map-get($colors, neon, fuchsia), 0.15),
fuchsia: 0px 0px 20px rgba(map-get($colors, neon, fuchsia), 0.3),
purple: 0px 0px 20px 5px rgba(map-get($colors, dark, purple), 0.2)
purple: 0px 0px 20px 5px rgba(map-get($colors, dark, purple), 0.2),
darkBlue: 0px 20px 20px 18px rgba(map-get($colors, dark, blue), 0.8)
);

$gradients: (
Expand Down
4 changes: 2 additions & 2 deletions src/utils/setNoScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export const setNoScroll = (condition: boolean) => {
return;
}
if (condition) {
document.body.style.overflowY = 'hidden';
document.documentElement.style.overflowY = 'hidden';
} else {
document.body.style.overflowY = 'unset';
document.documentElement.style.overflowY = '';
}
};
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4863,6 +4863,11 @@
expect "^29.0.0"
pretty-format "^29.0.0"

"@types/js-cookie@^3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95"
integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==

"@types/js-yaml@^4.0.0":
version "4.0.5"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
Expand Down Expand Up @@ -10536,6 +10541,11 @@ jpeg-js@^0.4.0, jpeg-js@^0.4.2:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==

js-cookie@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==

"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
Expand Down

0 comments on commit c78406f

Please sign in to comment.