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 `
`
+}
+
+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
+
+
+ )}
+
)}