Skip to content

Commit

Permalink
feat: support embedded youtube videos in blog posts (#803)
Browse files Browse the repository at this point in the history
* feat: support embedded youtube videos in blog posts

* chore: revert unintended changes
  • Loading branch information
hofmann-felix authored Sep 18, 2024
1 parent 6a62a07 commit 7999f3a
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 38 deletions.
14 changes: 10 additions & 4 deletions src/components/content/rich-text/rich-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,19 @@ export const ContentfulRichText = ({ data }: ContentfulRichTextProps) => {
[MARKS.CODE]: (text) => <code className="language-text">{text}</code>,
},
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) {
Expand Down
4 changes: 4 additions & 0 deletions src/components/legacy/markdown/custom-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -235,6 +236,9 @@ const customSatellytesComponents = {
const { children, ...rest } = props;
return <Figcaption {...rest}>{children}</Figcaption>;
},
youtube(props) {
return <YoutubeEmbed {...props} />;
},
};

export default customSatellytesComponents;
96 changes: 96 additions & 0 deletions src/components/legacy/markdown/youtube-embed.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<YoutubeEmbedWrapper>
{consentGiven ? (
<StyledIframe
title="YouTube video player"
width="560"
height="315"
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
frameBorder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></StyledIframe>
) : (
<YouTubePrivacyBanner giveConsent={giveConsent} />
)}
</YoutubeEmbedWrapper>
);
};

const YouTubePrivacyBanner = ({ giveConsent }) => {
return (
<YouTubePrivacyBannerWrapper>
<LegalText>
By clicking on &quot;Show YouTube Content&quot; you accept{' '}
<SyledLink to={'https://policies.google.com/privacy?hl=en'}>
YouTube&apos;s privacy policy
</SyledLink>
.
</LegalText>
<StyledButton onClick={giveConsent}>Show YouTube Content</StyledButton>
</YouTubePrivacyBannerWrapper>
);
};
71 changes: 37 additions & 34 deletions src/components/pages/blog-post/blog-post.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,41 +64,43 @@ export const BlogPostPage = ({
const heroByLine = `${formattedDate}${readingTime}${byLine}`;

return (
<Layout
transparentHeader
siteTitleUrl={'/blog'}
light
contentAs={'article'}
hero={
<BlogHero
attribution={blogPost.heroImage}
image={blogPost.heroImage.fullImage}
naturalHeight={blogPost.heroImage.naturalHeight}
/>
}
leadbox={leadbox}
showLanguageSwitch={false}
breadcrumb={breadcrumb}
>
<BlogHeader headline={blogPost.title} byline={heroByLine}>
{blogPost.introRichText && (
<RichTextContainer>
<ContentfulRichText
data={{
...blogPost.introRichText,
references: [],
}}
/>
</RichTextContainer>
)}
</BlogHeader>
<YouTubeConsentProvider>
<Layout
transparentHeader
siteTitleUrl={'/blog'}
light
contentAs={'article'}
hero={
<BlogHero
attribution={blogPost.heroImage}
image={blogPost.heroImage.fullImage}
naturalHeight={blogPost.heroImage.naturalHeight}
/>
}
leadbox={leadbox}
showLanguageSwitch={false}
breadcrumb={breadcrumb}
>
<BlogHeader headline={blogPost.title} byline={heroByLine}>
{blogPost.introRichText && (
<RichTextContainer>
<ContentfulRichText
data={{
...blogPost.introRichText,
references: [],
}}
/>
</RichTextContainer>
)}
</BlogHeader>

<ContentfulRichText data={blogPost.content} />
<ContentfulRichText data={blogPost.content} />

<PanelContainer>
<SharePanel title={blogPost.title} />
<FollowPanel />
</PanelContainer>
</Layout>
<PanelContainer>
<SharePanel title={blogPost.title} />
<FollowPanel />
</PanelContainer>
</Layout>
</YouTubeConsentProvider>
);
};
41 changes: 41 additions & 0 deletions src/context/youtube-consent-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { createContext, useState, useEffect, ReactNode } from 'react';

interface YouTubeConsentContextType {
consentGiven: boolean;
giveConsent: () => void;
}

const YouTubeConsentContext = createContext<YouTubeConsentContextType>({
consentGiven: false,
giveConsent: () => {},
});

interface YouTubeConsentProviderProps {
children: ReactNode;
}

export const YouTubeConsentProvider = ({
children,
}: YouTubeConsentProviderProps) => {
const [consentGiven, setConsentGiven] = useState<boolean>(false);

useEffect(() => {
const consent = localStorage.getItem('youtube-consent');
if (consent === 'true') {
setConsentGiven(true);
}
}, []);

const giveConsent = () => {
setConsentGiven(true);
localStorage.setItem('youtube-consent', 'true');
};

return (
<YouTubeConsentContext.Provider value={{ consentGiven, giveConsent }}>
{children}
</YouTubeConsentContext.Provider>
);
};

export default YouTubeConsentContext;

0 comments on commit 7999f3a

Please sign in to comment.