From c6c57beea50b593f8efd42df79bf5ca197c070d9 Mon Sep 17 00:00:00 2001 From: Julie <43496356+julieg18@users.noreply.github.com> Date: Mon, 7 Feb 2022 07:47:48 -0600 Subject: [PATCH] Make YouTube Embeds GDPR compliant (#3253) * Make all youtube embeds gdpr compliant with on hover message * add custom youtube transformer to gatsby-remark-embedder * add needed js and css code in doc/blog components --- .../custom-yt-embedder.js | 59 +++++++++++++++++ gatsby-config.js | 8 ++- .../Documentation/Markdown/Main/index.tsx | 2 + .../Markdown/Main/styles.module.css | 54 ++++++++++++++++ .../src/utils/front/useCustomYtEmbeds.ts | 61 ++++++++++++++++++ .../Blog/Post/Markdown/styles.module.css | 64 +++++++++++++++++++ src/components/Blog/Post/index.tsx | 3 + src/components/Home/UseCases/Video/index.tsx | 53 +++++++++++---- .../Home/UseCases/Video/styles.module.css | 39 +++++++++++ 9 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 config/gatsby-remark-embedder/custom-yt-embedder.js create mode 100644 plugins/gatsby-theme-iterative-docs/src/utils/front/useCustomYtEmbeds.ts diff --git a/config/gatsby-remark-embedder/custom-yt-embedder.js b/config/gatsby-remark-embedder/custom-yt-embedder.js new file mode 100644 index 0000000000..7acf6a2d10 --- /dev/null +++ b/config/gatsby-remark-embedder/custom-yt-embedder.js @@ -0,0 +1,59 @@ +const shouldTransform = url => { + const { host, pathname, searchParams } = new URL(url) + + return ( + host === 'youtu.be' || + (['youtube.com', 'www.youtube.com'].includes(host) && + pathname.includes('/watch') && + Boolean(searchParams.get('v'))) + ) +} + +const getTimeValueInSeconds = timeValue => { + if (Number(timeValue).toString() === timeValue) { + return timeValue + } + + const { + 1: hours = '0', + 2: minutes = '0', + 3: seconds = '0' + } = timeValue.match(/(?:(\d*)h)?(?:(\d*)m)?(?:(\d*)s)?/) + + return String((Number(hours) * 60 + Number(minutes)) * 60 + Number(seconds)) +} + +const getYouTubeIFrameSrc = urlString => { + const url = new URL(urlString) + const id = + url.host === 'youtu.be' ? url.pathname.slice(1) : url.searchParams.get('v') + + const embedUrl = new URL( + `https://www.youtube-nocookie.com/embed/${id}?rel=0&&showinfo=0;` + ) + + url.searchParams.forEach((value, name) => { + if (name === 'v') { + return + } + + if (name === 't') { + embedUrl.searchParams.append('start', getTimeValueInSeconds(value)) + } else { + embedUrl.searchParams.append(name, value) + } + }) + return embedUrl.toString() +} + +// all code above taken from gatsby-remark-embedder (https://github.com/MichaelDeBoey/gatsby-remark-embedder) + +const name = 'YouTubeCustom' + +const getHTML = url => { + const iframeSrc = getYouTubeIFrameSrc(url) + + return `
By clicking play, you agree to YouTube's Privacy Policy and Terms of Service
` +} + +module.exports = { getHTML, name, shouldTransform } diff --git a/gatsby-config.js b/gatsby-config.js index d3adb03408..854ace5791 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -7,6 +7,7 @@ require('./config/prismjs/dvc') require('./config/prismjs/usage') require('./config/prismjs/dvctable') +const customYoutubeTransformer = require('./config/gatsby-remark-embedder/custom-yt-embedder') const apiMiddleware = require('./src/server/middleware/api') const redirectsMiddleware = require('./src/server/middleware/redirects') const makeFeedHtml = require('./plugins/utils/makeFeedHtml') @@ -70,7 +71,12 @@ const plugins = [ resolve: 'gatsby-transformer-remark', options: { plugins: [ - 'gatsby-remark-embedder', + { + resolve: 'gatsby-remark-embedder', + options: { + customTransformers: [customYoutubeTransformer] + } + }, 'gatsby-remark-dvc-linker', { resolve: 'gatsby-remark-args-linker', diff --git a/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/index.tsx b/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/index.tsx index 5979791159..e51111aa42 100644 --- a/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/index.tsx +++ b/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/index.tsx @@ -5,6 +5,7 @@ import { navigate } from '@reach/router' import Link from '../../../Link' import Tutorials from '../../TutorialsLinks' import { getPathWithSource } from '../../../../utils/shared/sidebar' +import useCustomYtEmbeds from '../../../../utils/front/useCustomYtEmbeds' import 'github-markdown-css/github-markdown-light.css' import * as sharedStyles from '../../styles.module.css' @@ -43,6 +44,7 @@ const Main: React.FC = ({ const touchstartXRef = useRef(0) const touchendXRef = useRef(0) const isCodeBlockRef = useRef(false) + useCustomYtEmbeds() const handleSwipeGesture = useCallback(() => { if (isCodeBlockRef.current) return diff --git a/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/styles.module.css b/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/styles.module.css index 274aad58a4..6269e61494 100644 --- a/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/styles.module.css +++ b/plugins/gatsby-theme-iterative-docs/src/components/Documentation/Markdown/Main/styles.module.css @@ -273,6 +273,60 @@ margin-left: 20px; margin-right: 10px; } + + .yt-embed-wrapper { + position: relative; + display: flex; + + &:hover &__tooltip { + opacity: 1; + } + + &__overlay { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: flex-end; + + &:hover { + cursor: pointer; + } + + &.hidden { + display: none; + } + } + + &__tooltip { + padding: 10px; + box-sizing: border-box; + color: #fff; + opacity: 0; + width: 100%; + font-size: 16px; + background-color: rgb(23 23 23 / 59%); + text-shadow: 0 1px 0 rgb(33 45 69 / 25%); + transition: opacity 0.2s ease-in-out; + + &:hover { + cursor: auto; + } + + a { + @mixin focus; + + color: #fff; + text-decoration: underline; + + &::after { + display: none; + } + } + } + } } .content { diff --git a/plugins/gatsby-theme-iterative-docs/src/utils/front/useCustomYtEmbeds.ts b/plugins/gatsby-theme-iterative-docs/src/utils/front/useCustomYtEmbeds.ts new file mode 100644 index 0000000000..dc5e74b822 --- /dev/null +++ b/plugins/gatsby-theme-iterative-docs/src/utils/front/useCustomYtEmbeds.ts @@ -0,0 +1,61 @@ +import { useEffect } from 'react' + +const hideAllEmbedOverlays = (embeds: NodeListOf) => { + embeds.forEach(embed => { + const overlay = embed.querySelector('.yt-embed-wrapper__overlay') + overlay?.classList.add('hidden') + }) +} + +const setUpEmbedClickListeners = (embeds: NodeListOf) => { + const removeClickListeners: Array<() => void> = [] + + embeds.forEach(embed => { + const iframe = embed.querySelector('iframe') + const overlay = embed.querySelector('.yt-embed-wrapper__overlay') + const tooltip = embed.querySelector('.yt-embed-wrapper__tooltip') + + const handleOverlayClick = (event: MouseEvent) => { + if (event.target === tooltip || tooltip?.contains(event.target as Node)) { + return + } + + if (iframe && iframe.src) { + iframe.src = iframe?.src + `&autoplay=1` + } + hideAllEmbedOverlays(embeds) + localStorage.setItem('yt-embed-consent', 'true') + } + const removeListener = () => { + overlay?.removeEventListener('click', handleOverlayClick as EventListener) + } + overlay?.addEventListener('click', handleOverlayClick as EventListener) + removeClickListeners.push(removeListener) + }) + + return () => { + removeClickListeners.forEach(rmListener => rmListener()) + } +} + +const useCustomYtEmbeds = () => { + useEffect(() => { + const hasUserGivenConsent = Boolean( + localStorage.getItem('yt-embed-consent') + ) + const embeds = document.querySelectorAll('.yt-embed-wrapper') + + if (hasUserGivenConsent) { + hideAllEmbedOverlays(embeds) + return + } + + const cleanUpEventListeners = setUpEmbedClickListeners(embeds) + + return () => { + cleanUpEventListeners() + } + }, []) +} + +export default useCustomYtEmbeds diff --git a/src/components/Blog/Post/Markdown/styles.module.css b/src/components/Blog/Post/Markdown/styles.module.css index 9ff7a5a54a..8ca158573f 100644 --- a/src/components/Blog/Post/Markdown/styles.module.css +++ b/src/components/Blog/Post/Markdown/styles.module.css @@ -564,4 +564,68 @@ border-bottom: none; } } + + :global { + .yt-embed-wrapper { + position: relative; + display: flex; + + &:hover &__tooltip { + opacity: 1; + } + + &__overlay { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: flex-end; + + &:hover { + cursor: pointer; + } + + &.hidden { + display: none; + } + } + + &__tooltip { + padding: 10px; + box-sizing: border-box; + color: #fff; + opacity: 0; + width: 100%; + font-size: 16px; + background-color: rgb(23 23 23 / 59%); + text-shadow: 0 1px 0 rgb(33 45 69 / 25%); + transition: opacity 0.2s ease-in-out; + + &:hover { + cursor: auto; + } + + a { + @mixin focus; + + color: #fff; + text-decoration: underline; + + &:hover { + opacity: 1; + } + + &:focus { + position: static; + } + + &::after { + display: none; + } + } + } + } + } } diff --git a/src/components/Blog/Post/index.tsx b/src/components/Blog/Post/index.tsx index f16b8dbbb6..dbb3de0935 100644 --- a/src/components/Blog/Post/index.tsx +++ b/src/components/Blog/Post/index.tsx @@ -8,6 +8,7 @@ import { IBlogPostData } from '../../../templates/blog-post' import { useCommentsCount } from 'gatsby-theme-iterative-docs/src/utils/front/api' import { pluralizeComments } from 'gatsby-theme-iterative-docs/src/utils/front/i18n' import tagToSlug from 'gatsby-theme-iterative-docs/src/utils/shared/tagToSlug' +import useCustomYtEmbeds from 'gatsby-theme-iterative-docs/src/utils/front/useCustomYtEmbeds' import Markdown from './Markdown' import FeedMeta from '../FeedMeta' @@ -38,6 +39,8 @@ const Post: React.FC = ({ const { width, height } = useWindowSize() const { y } = useWindowScroll() + useCustomYtEmbeds() + const isFixed = useMemo(() => { if (!wrapperRef.current) { return false diff --git a/src/components/Home/UseCases/Video/index.tsx b/src/components/Home/UseCases/Video/index.tsx index 0191c7d56f..e749cc6879 100644 --- a/src/components/Home/UseCases/Video/index.tsx +++ b/src/components/Home/UseCases/Video/index.tsx @@ -1,16 +1,26 @@ -import React, { useState, useCallback } from 'react' +import React, { useState, useCallback, useEffect } from 'react' import TwoRowsButton from '../../../TwoRowsButton' import { logEvent } from 'gatsby-theme-iterative-docs/src/utils/front/plausible' +import Link from 'gatsby-theme-iterative-docs/src/components/Link' + import * as styles from './styles.module.css' const Video: React.FC<{ id: string }> = ({ id }) => { const [isWatching, setWatching] = useState(false) + const [hasUserGivenConsent, setHasUserGivenConsent] = useState(false) + + useEffect(() => { + const givenConsent = Boolean(localStorage.getItem('yt-embed-consent')) + + setHasUserGivenConsent(givenConsent) + }, []) const watchVideo = useCallback(() => { logEvent('Button', { Item: 'video' }) setWatching(true) + localStorage.setItem('yt-embed-consent', 'true') }, []) return ( @@ -18,19 +28,34 @@ const Video: React.FC<{ id: string }> = ({ id }) => {
{!isWatching && (
- - } - onClick={watchVideo} - /> +
+ + } + onClick={watchVideo} + /> + {!hasUserGivenConsent && ( +
+ By clicking play, you agree to YouTube's{' '} + + Privacy Policy + {' '} + and{' '} + + Terms of Service + +
+ )} +
)}