Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make YouTube Embeds GDPR compliant #3253

Merged
merged 14 commits into from
Feb 7, 2022
59 changes: 59 additions & 0 deletions config/gatsby-remark-embedder/custom-yt-embedder.js
Original file line number Diff line number Diff line change
@@ -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')))
)
julieg18 marked this conversation as resolved.
Show resolved Hide resolved
}

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 `<div class="yt-embed-wrapper"><iframe width="100%" height="315" src="${iframeSrc}" frameBorder="0" allow="autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe><div className="yt-embed-wrapper__overlay"><span class="yt-embed-wrapper__tooltip">By clicking play, you agree to YouTube&apos;s <a href="https://policies.google.com/u/3/privacy?hl=en" target="_blank" rel="nofollow noopener noreferrer">Privacy Policy</a> and <a href="https://www.youtube.com/static?template=terms" target="_blank" rel="nofollow noopener noreferrer">Terms of Service</a></span></div></div>`
}

module.exports = { getHTML, name, shouldTransform }
8 changes: 7 additions & 1 deletion gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -43,6 +44,7 @@ const Main: React.FC<IMainProps> = ({
const touchstartXRef = useRef(0)
const touchendXRef = useRef(0)
const isCodeBlockRef = useRef(false)
useCustomYtEmbeds()
const handleSwipeGesture = useCallback(() => {
if (isCodeBlockRef.current) return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,60 @@
margin-left: 20px;
margin-right: 10px;
}

.yt-embed-wrapper {
julieg18 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useEffect } from 'react'

const hideAllEmbedOverlays = (embeds: NodeListOf<Element>) => {
embeds.forEach(embed => {
const overlay = embed.querySelector('.yt-embed-wrapper__overlay')
overlay?.classList.add('hidden')
})
}

const setUpEmbedClickListeners = (embeds: NodeListOf<Element>) => {
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
64 changes: 64 additions & 0 deletions src/components/Blog/Post/Markdown/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/components/Blog/Post/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -38,6 +39,8 @@ const Post: React.FC<IBlogPostData> = ({
const { width, height } = useWindowSize()
const { y } = useWindowScroll()

useCustomYtEmbeds()

const isFixed = useMemo(() => {
if (!wrapperRef.current) {
return false
Expand Down
53 changes: 39 additions & 14 deletions src/components/Home/UseCases/Video/index.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,61 @@
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 (
<div className={styles.container}>
<div className={styles.handler}>
{!isWatching && (
<div className={styles.overlay}>
<TwoRowsButton
mode="azure"
title="Watch video"
description="How it works"
icon={
<img
className={styles.buttonIcon}
src="/img/watch_white.svg"
alt="Watch video"
/>
}
onClick={watchVideo}
/>
<div className={styles.content}>
<TwoRowsButton
mode="azure"
title="Watch video"
description="How it works"
className={styles.button}
icon={
<img
className={styles.buttonIcon}
src="/img/watch_white.svg"
alt="Watch video"
/>
}
onClick={watchVideo}
/>
{!hasUserGivenConsent && (
<div className={styles.tooltip}>
By clicking play, you agree to YouTube&apos;s{' '}
<Link href="https://policies.google.com/u/3/privacy?hl=en">
Privacy Policy
</Link>{' '}
and{' '}
<Link href="https://www.youtube.com/static?template=terms">
Terms of Service
</Link>
</div>
)}
</div>
</div>
)}
<iframe
Expand Down
Loading