Skip to content

Commit

Permalink
feat(cookie dialog): initial commit of the cookiedialog component, see
Browse files Browse the repository at this point in the history
  • Loading branch information
bmarshall511 committed Jun 16, 2023
1 parent 4668d35 commit 0a0dc05
Showing 1 changed file with 218 additions and 0 deletions.
218 changes: 218 additions & 0 deletions projects/wp-nextjs-ts/src/components/CookieDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import React, { FC, useState, useEffect, useCallback } from 'react';
import { css } from '@linaria/core';
import { Link } from './Link';

const cookieDialogStyles = css`
bottom: 1rem;
box-sizing: border-box;
display: block;
inset-inline-start: unset;
margin: unset;
max-width: 500px;
position: fixed;
right: 1rem;
width: calc(100dvw - 2rem);
z-index: 1;
> form {
display: flex;
flex-direction: column;
gap: 1em;
}
> form label {
align-items: center;
display: flex;
font-weight: 700;
gap: 0.25em;
}
> form p {
margin-block: 0;
}
`;

// The main cookie checked when determining if the dialog should be displayed
const REVIEWED_DIALOG_COOKIE = 'cookie-dialog-reviewed';

// Helper functions
const getCookie = async (name: string): Promise<{ value: string } | null> => {
try {
const value = document.cookie
.split('; ')
.find((row) => row.startsWith(name))
?.split('=')[1];
return value ? { value } : null;
} catch (error) {
return null;
}
};

const setCookie = async (name: string, value: string): Promise<void> => {
try {
const expiration = new Date();
expiration.setMonth(expiration.getMonth() + 6);
document.cookie = `${name}=${value}; expires=${expiration.toUTCString()}; samesite=strict; secure`;
return null;
} catch (error) {
return null;
}
};

type CookieElementProps = {
cookie: CookieProps;
handleCheckboxChange: (cookieName: string) => void;
acceptedCookies: { [key: string]: boolean };
};

const CookieElement = React.memo<CookieElementProps>(
({ cookie, handleCheckboxChange, acceptedCookies }) => (
<div key={cookie.name}>
<label htmlFor={cookie.name}>
<input
type="checkbox"
name={cookie.name}
id={cookie.name}
value={cookie.name}
aria-describedby={`${cookie.name}-description`}
onChange={() => handleCheckboxChange(cookie.name)}
defaultChecked={acceptedCookies[cookie.name] || cookie.isChecked}
disabled={cookie?.isDisabled}
/>{' '}
{cookie.label}
</label>
<p id={`${cookie.name}-description`}>{cookie.description}</p>
</div>
),
);

export interface CookieProps {
name: string;
label: string;
description: string;
isChecked: boolean;
isDisabled?: boolean;
}

// Ignore the error as it's a false positive since the defaults are actually
// being set.
/* eslint-disable-next-line react/require-default-props */
export const CookieDialog: FC<{
// Ignore the error since it's a false positive, the default is set.
/* eslint-disable-next-line react/require-default-props */
label?: string;
// Ignore the error since it's a false positive, the default is set.
/* eslint-disable-next-line react/require-default-props */
cookies?: [CookieProps, ...CookieProps[]];
// Ignore the error since it's a false positive, the default is set.
/* eslint-disable-next-line react/require-default-props */
onAccept?: (acceptedCookies: { [key: string]: boolean }) => void;
// Ignore the error since it's a false positive, the default is set.
/* eslint-disable-next-line react/require-default-props */
onReject?: () => void;
}> = ({
label = 'Cookie settings',
children = (
<p>
We use cookies on our website to give you the most relevant experience by remembering
your preferences and repeat visits. By clicking &apos;Accept&apos;, you consent to the
use of cookies. For more information, you can check our{' '}
<Link href="/">Privacy Policy</Link> <Link href="/">Cookie Policy</Link>.
</p>
),
cookies = [
{
name: 'essential',
label: 'Essential cookies',
description: `These cookies allow core website functionality. The website won't work without them.`,
isChecked: false,
},
],
onAccept = () => {},
onReject = () => {},
}) => {
const [isDialogVisible, setDialogVisible] = useState(false);
const [acceptedCookies, setAcceptedCookies] = useState(
cookies.reduce((acc, cookie) => {
acc[cookie.name] = cookie.isChecked;
return acc;
}, {}),
);

useEffect(() => {
const checkCookie = async () => {
const cookie = await getCookie(REVIEWED_DIALOG_COOKIE);
if (!cookie) {
setDialogVisible(true);
}
};
checkCookie();
}, [cookies]);

const handleDialogClose = useCallback(
async (value: string) => {
setCookie(REVIEWED_DIALOG_COOKIE, 'reviewed');

const setCookiesPromises = Object.keys(acceptedCookies).map((cookieName) => {
if (acceptedCookies[cookieName]) {
return setCookie(cookieName, value);
}
return Promise.resolve();
});

try {
await Promise.all(setCookiesPromises);
setDialogVisible(false);
if (value === 'accept' && onAccept) {
onAccept(acceptedCookies);
} else if (value === 'reject' && onReject) {
onReject();
}

return null;
} catch (error) {
return null;
}
},
[acceptedCookies, onAccept, onReject],
);

const handleCheckboxChange = useCallback((cookieName: string) => {
setAcceptedCookies((prev) => ({
...prev,
[cookieName]: !prev[cookieName],
}));
}, []);

if (!isDialogVisible) return null;

return (
<dialog
className={cookieDialogStyles}
aria-labelledby="label"
aria-describedby="description"
>
{label && <h2 id="label">{label}</h2>}
<div id="description">{children}</div>
<form method="dialog">
{cookies.length > 1 &&
cookies.map((cookie) => {
return (
<CookieElement
key={cookie.name}
cookie={cookie}
handleCheckboxChange={handleCheckboxChange}
acceptedCookies={acceptedCookies}
/>
);
})}
<button type="button" onClick={() => handleDialogClose('accept')} value="accept">
Accept
</button>
<button type="button" onClick={() => handleDialogClose('reject')} value="reject">
Reject
</button>
</form>
</dialog>
);
};

0 comments on commit 0a0dc05

Please sign in to comment.