From 7999f3aef7652f1831971a565f54aa49ae775a9c Mon Sep 17 00:00:00 2001
From: Felix Hofmann <57712895+hofmann-felix@users.noreply.github.com>
Date: Wed, 18 Sep 2024 12:57:30 +0200
Subject: [PATCH] feat: support embedded youtube videos in blog posts (#803)
* feat: support embedded youtube videos in blog posts
* chore: revert unintended changes
---
.../content/rich-text/rich-text.tsx | 14 ++-
.../legacy/markdown/custom-components.tsx | 4 +
.../legacy/markdown/youtube-embed.tsx | 96 +++++++++++++++++++
src/components/pages/blog-post/blog-post.tsx | 71 +++++++-------
src/context/youtube-consent-context.tsx | 41 ++++++++
5 files changed, 188 insertions(+), 38 deletions(-)
create mode 100644 src/components/legacy/markdown/youtube-embed.tsx
create mode 100644 src/context/youtube-consent-context.tsx
diff --git a/src/components/content/rich-text/rich-text.tsx b/src/components/content/rich-text/rich-text.tsx
index bf43d7407..e6eccfe35 100644
--- a/src/components/content/rich-text/rich-text.tsx
+++ b/src/components/content/rich-text/rich-text.tsx
@@ -76,13 +76,19 @@ export const ContentfulRichText = ({ data }: ContentfulRichTextProps) => {
[MARKS.CODE]: (text) => {text}
,
},
renderNode: {
- [INLINES.HYPERLINK]: (props, children) =>
- customComponents.a({
+ [INLINES.HYPERLINK]: (props, children) => {
+ const uri = props.data.uri;
+ if (uri.startsWith('youtube://')) {
+ const videoId = uri.split('//')[1];
+ return customComponents.youtube({ videoId });
+ }
+ return customComponents.a({
children,
- href: props.data.uri,
+ href: uri,
target: '_blank',
rel: 'nofollow noopener noreferrer',
- }),
+ });
+ },
[INLINES.EMBEDDED_ENTRY]: (node) => {
const { __typename } = node.data.target;
switch (__typename) {
diff --git a/src/components/legacy/markdown/custom-components.tsx b/src/components/legacy/markdown/custom-components.tsx
index a0d33b79b..e4f6a2d5e 100644
--- a/src/components/legacy/markdown/custom-components.tsx
+++ b/src/components/legacy/markdown/custom-components.tsx
@@ -8,6 +8,7 @@ import { TextStyles } from '../../typography';
import { Quote } from '../../ui/quote/quote';
import { Link } from '../links/links';
import { WithAnchorHOC } from '../../layout/with-anchor-hoc';
+import { YoutubeEmbed } from './youtube-embed';
/**
* Override markdown generated html content with custom React components (for us mostly to pass in custom styling)
@@ -235,6 +236,9 @@ const customSatellytesComponents = {
const { children, ...rest } = props;
return {children};
},
+ youtube(props) {
+ return ;
+ },
};
export default customSatellytesComponents;
diff --git a/src/components/legacy/markdown/youtube-embed.tsx b/src/components/legacy/markdown/youtube-embed.tsx
new file mode 100644
index 000000000..c69bdfcef
--- /dev/null
+++ b/src/components/legacy/markdown/youtube-embed.tsx
@@ -0,0 +1,96 @@
+import React, { useContext } from 'react';
+import styled from 'styled-components';
+import { theme } from '../../layout/theme';
+import { Button } from '../../ui/buttons/button';
+import { TextStyles } from '../../typography';
+import YouTubeConsentContext from '../../../context/youtube-consent-context';
+import { Link } from '../links/links';
+
+interface YoutubeEmbedProps {
+ videoId: string;
+}
+
+// how to make iframe responsive: https://stackoverflow.com/questions/17838607/making-an-iframe-responsive
+const StyledIframe = styled.iframe`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+`;
+
+const YoutubeEmbedWrapper = styled.div`
+ position: relative;
+ padding-bottom: 56.25%; /* 16:9 */
+ padding-top: 25px;
+ height: 0;
+`;
+
+const YouTubePrivacyBannerWrapper = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ padding: 16px;
+ gap: 16px;
+ background-color: ${theme.palette.background.card};
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+const LegalText = styled.p`
+ ${TextStyles.label}
+ text-align: center;
+`;
+
+const StyledButton = styled(Button)`
+ width: fit-content;
+`;
+
+const SyledLink = styled(Link)`
+ color: ${theme.palette.text.link.default};
+
+ &:hover {
+ border-bottom: 1px solid ${theme.palette.text.link.default};
+ }
+`;
+
+export const YoutubeEmbed = ({ videoId }: YoutubeEmbedProps) => {
+ const { consentGiven, giveConsent } = useContext(YouTubeConsentContext);
+
+ return (
+
+ {consentGiven ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const YouTubePrivacyBanner = ({ giveConsent }) => {
+ return (
+
+
+ By clicking on "Show YouTube Content" you accept{' '}
+
+ YouTube's privacy policy
+
+ .
+
+ Show YouTube Content
+
+ );
+};
diff --git a/src/components/pages/blog-post/blog-post.tsx b/src/components/pages/blog-post/blog-post.tsx
index 8e913020e..41a44a654 100644
--- a/src/components/pages/blog-post/blog-post.tsx
+++ b/src/components/pages/blog-post/blog-post.tsx
@@ -12,6 +12,7 @@ import FollowPanel from './follow-panel';
import SharePanel from './share-panel';
import { ContentfulRichText } from '../../content/rich-text/rich-text';
import { TextStyles } from '../../typography';
+import { YouTubeConsentProvider } from '../../../context/youtube-consent-context';
interface BlogPostPageProps {
blogPost: BlogArticleQueryData;
@@ -63,41 +64,43 @@ export const BlogPostPage = ({
const heroByLine = `${formattedDate} • ${readingTime} • ${byLine}`;
return (
-
- }
- leadbox={leadbox}
- showLanguageSwitch={false}
- breadcrumb={breadcrumb}
- >
-
- {blogPost.introRichText && (
-
-
-
- )}
-
+
+
+ }
+ leadbox={leadbox}
+ showLanguageSwitch={false}
+ breadcrumb={breadcrumb}
+ >
+
+ {blogPost.introRichText && (
+
+
+
+ )}
+
-
+
-
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/src/context/youtube-consent-context.tsx b/src/context/youtube-consent-context.tsx
new file mode 100644
index 000000000..10ecb4d37
--- /dev/null
+++ b/src/context/youtube-consent-context.tsx
@@ -0,0 +1,41 @@
+import React, { createContext, useState, useEffect, ReactNode } from 'react';
+
+interface YouTubeConsentContextType {
+ consentGiven: boolean;
+ giveConsent: () => void;
+}
+
+const YouTubeConsentContext = createContext({
+ consentGiven: false,
+ giveConsent: () => {},
+});
+
+interface YouTubeConsentProviderProps {
+ children: ReactNode;
+}
+
+export const YouTubeConsentProvider = ({
+ children,
+}: YouTubeConsentProviderProps) => {
+ const [consentGiven, setConsentGiven] = useState(false);
+
+ useEffect(() => {
+ const consent = localStorage.getItem('youtube-consent');
+ if (consent === 'true') {
+ setConsentGiven(true);
+ }
+ }, []);
+
+ const giveConsent = () => {
+ setConsentGiven(true);
+ localStorage.setItem('youtube-consent', 'true');
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default YouTubeConsentContext;