From 0a0dc05c91ee9036e8319e50bc092e8cf4809a8a Mon Sep 17 00:00:00 2001 From: Ben Marshall Date: Fri, 16 Jun 2023 14:50:17 -0500 Subject: [PATCH 1/2] feat(cookie dialog): initial commit of the cookiedialog component, see #419 --- .../src/components/CookieDialog.tsx | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 projects/wp-nextjs-ts/src/components/CookieDialog.tsx diff --git a/projects/wp-nextjs-ts/src/components/CookieDialog.tsx b/projects/wp-nextjs-ts/src/components/CookieDialog.tsx new file mode 100644 index 000000000..e9aa413e9 --- /dev/null +++ b/projects/wp-nextjs-ts/src/components/CookieDialog.tsx @@ -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 => { + 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( + ({ cookie, handleCheckboxChange, acceptedCookies }) => ( +
+ +

{cookie.description}

+
+ ), +); + +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 = ( +

+ We use cookies on our website to give you the most relevant experience by remembering + your preferences and repeat visits. By clicking 'Accept', you consent to the + use of cookies. For more information, you can check our{' '} + Privacy Policy Cookie Policy. +

+ ), + 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 ( + + {label &&

{label}

} +
{children}
+
+ {cookies.length > 1 && + cookies.map((cookie) => { + return ( + + ); + })} + + + +
+ ); +}; From fc7736fb04f49a058b0cf5cb49c9a8f9394aa769 Mon Sep 17 00:00:00 2001 From: Ben Marshall Date: Fri, 16 Jun 2023 15:04:15 -0500 Subject: [PATCH 2/2] feat(cookie dialog): making the individual cookie desc accept a react element --- .../wp-nextjs-ts/src/components/CookieDialog.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/projects/wp-nextjs-ts/src/components/CookieDialog.tsx b/projects/wp-nextjs-ts/src/components/CookieDialog.tsx index e9aa413e9..dda3434ab 100644 --- a/projects/wp-nextjs-ts/src/components/CookieDialog.tsx +++ b/projects/wp-nextjs-ts/src/components/CookieDialog.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState, useEffect, useCallback } from 'react'; +import React, { FC, useState, useEffect, useCallback, ReactElement } from 'react'; import { css } from '@linaria/core'; import { Link } from './Link'; @@ -89,7 +89,7 @@ const CookieElement = React.memo( export interface CookieProps { name: string; label: string; - description: string; + description: string | ReactElement; isChecked: boolean; isDisabled?: boolean; } @@ -124,7 +124,12 @@ export const CookieDialog: FC<{ { name: 'essential', label: 'Essential cookies', - description: `These cookies allow core website functionality. The website won't work without them.`, + description: ( + <> + By continuing to use our site, you accept our use of cookies as described in our{' '} + Privacy Policy. + + ), isChecked: false, }, ], @@ -132,8 +137,8 @@ export const CookieDialog: FC<{ onReject = () => {}, }) => { const [isDialogVisible, setDialogVisible] = useState(false); - const [acceptedCookies, setAcceptedCookies] = useState( - cookies.reduce((acc, cookie) => { + const [acceptedCookies, setAcceptedCookies] = useState<{ [key: string]: boolean }>( + cookies.reduce((acc: { [key: string]: boolean }, cookie: CookieProps) => { acc[cookie.name] = cookie.isChecked; return acc; }, {}),