Skip to content

Commit

Permalink
feat: add YouTube embed feature and update related components (2) (#1960
Browse files Browse the repository at this point in the history
)

feat: add YouTube embed feature and update related components
  • Loading branch information
wirapratamaz authored Nov 5, 2024
1 parent 46103d4 commit f911f31
Show file tree
Hide file tree
Showing 18 changed files with 281 additions and 20 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@emotion/server": "^11.4.0",
"@emotion/styled": "^11.9.3",
"@fontsource/mulish": "^4.5.7",
"@googleapis/youtube": "^20.0.0",
"@heroicons/react": "^1.0.6",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
Expand Down Expand Up @@ -140,6 +141,7 @@
"@types/bn.js": "^4.11.6",
"@types/node": "^14.14.37",
"@types/react": "^17.0.3",
"@types/youtube": "^0.1.0",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"commitlint-config-jira": "^1.6.3",
"commitlint-plugin-jira-rules": "^1.6.3",
Expand Down
26 changes: 22 additions & 4 deletions src/components/PostCreate/MobileEmbed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { BiVideoPlus } from 'react-icons/bi';
import { CiCircleRemove } from 'react-icons/ci';
import { LuImagePlus } from 'react-icons/lu';
import * as UploadAPI from 'src/lib/api/upload';
import React from 'react';
import { YouTubeEmbed } from '../atoms/Embed/YouTube/YoutubeEmbed';

type MobileEmbedProps = {
uploadVideoFieldRef: any;
Expand Down Expand Up @@ -133,9 +135,25 @@ export const MobileEmbed: React.FC<MobileEmbedProps> = props => {
style={{ position: 'absolute', zIndex: 2, right: '-1%' }}>
<CiCircleRemove size={30} />
</IconButton>
<video width="100%" src={videoUrl[0]}>
Browser does not support video
</video>
{(() => {
// handle the video URL is a YouTube link; extract videoId
const extractYouTubeVideoId = (url: string): string | null => {
const regex =
/^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const match = url.match(regex);
return match ? match[1] : null;
};

const videoId = extractYouTubeVideoId(videoUrl[0]);

return videoId ? (
<YouTubeEmbed videoId={videoId} placeholder={<p>Loading video...</p>} />
) : (
<video width="100%" src={videoUrl[0]}>
Browser does not support video
</video>
);
})()}
</Grid>
</Grid>
</>
Expand Down Expand Up @@ -171,4 +189,4 @@ export const MobileEmbed: React.FC<MobileEmbedProps> = props => {
</Grid>
</>
);
};
};
21 changes: 19 additions & 2 deletions src/components/PostDetail/PostDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import { useToggle } from 'src/hooks/use-toggle.hook';
import { InfoIconYellow } from 'src/images/Icons';
import { ReferenceType } from 'src/interfaces/interaction';
import i18n from 'src/locale';
import { extractYouTubeVideoId } from 'src/helpers/url';

const Reddit = dynamic(() => import('./render/Reddit'), { ssr: false });
const Twitter = dynamic(() => import('./render/Twitter'), { ssr: false });
const YouTube = dynamic(() => import('./render/YouTube'), { ssr: false });
const Gallery = dynamic(() => import('src/components/atoms/Gallery/Gallery'), {
ssr: false,
});
Expand All @@ -49,11 +51,11 @@ export const PostDetail: React.FC<PostDetailProps> = props => {

const downvoted = post.votes
? post.votes.filter(vote => vote.userId === user?.id && !vote.state)
.length > 0
.length > 0
: false;
const upvoted = post.votes
? post.votes.filter(vote => vote.userId === user?.id && vote.state).length >
0
0
: false;

const isPostCreator = post.createdBy === user?.id;
Expand Down Expand Up @@ -99,6 +101,13 @@ export const PostDetail: React.FC<PostDetailProps> = props => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [downvoted, upvoted]);


// Extract YouTube Video ID
let videoId: string | null = null;
if (post.platform === 'youtube' && post.url) {
videoId = extractYouTubeVideoId(post.url);
}

return (
<div className={styles.wrapper}>
<PostHeader
Expand Down Expand Up @@ -161,6 +170,14 @@ export const PostDetail: React.FC<PostDetailProps> = props => {
/>
</ShowIf>

<ShowIf condition={post.platform === 'youtube' && Boolean(videoId)}>
<YouTube
text={post.text}
onHashtagClicked={handleHashtagClicked}
videoId={videoId}
/>
</ShowIf>

{post.asset?.exclusiveContents &&
post.asset?.exclusiveContents.length > 0 &&
!exclusiveContent && (
Expand Down
3 changes: 3 additions & 0 deletions src/components/PostDetail/render/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export const PostHeader: React.FC<PostHeaderProps> = props => {
case SocialsEnum.REDDIT:
url = `https://reddit.com/user/${post.people?.username as string}`;
break;
case SocialsEnum.YOUTUBE:
url = `https://youtube.com/channel/${post.people?.username as string}`;
break;
case 'myriad':
url = `/profile/${post.createdBy}`;
break;
Expand Down
30 changes: 30 additions & 0 deletions src/components/PostDetail/render/YouTube.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { YouTubeEmbed } from 'src/components/atoms/Embed/YouTube/YoutubeEmbed';
import LinkifyComponent from 'src/components/common/Linkify.component';

type RenderYouTubeProps = {
text: string;
onHashtagClicked: (hashtag: string) => void;
videoId: string;
};

export const RenderYouTube: React.FC<RenderYouTubeProps> = ({
text,
onHashtagClicked,
videoId,
}) => {
return (
<div style={{ paddingBottom: 6 }}>
<YouTubeEmbed videoId={videoId} placeholder={<p>Loading video...</p>} />
<LinkifyComponent
text={text}
handleClick={onHashtagClicked}
variant="body1"
color="textPrimary"
gutterBottom
/>
</div>
);
};

export default RenderYouTube;
34 changes: 26 additions & 8 deletions src/components/PostDetailExperience/PostDetailExperience.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ShowIf from 'src/components/common/show-if.component';
import { isJson } from 'src/helpers/string';
import { Post } from 'src/interfaces/post';
import { User } from 'src/interfaces/user';
import { extractYouTubeVideoId } from 'src/helpers/url';

const Gallery = dynamic(() => import('../atoms/Gallery/Gallery'), {
ssr: false,
Expand All @@ -26,6 +27,9 @@ const Reddit = dynamic(() => import('../PostDetail/render/Reddit'), {
const Twitter = dynamic(() => import('../PostDetail/render/Twitter'), {
ssr: false,
});
const YouTube = dynamic(() => import('../PostDetail/render/YouTube'), {
ssr: false,
});

type PostDetailProps = {
user?: User;
Expand Down Expand Up @@ -77,6 +81,12 @@ export const PostDetailExperience: React.FC<PostDetailProps> = props => {
setMaxLength(undefined);
};

// Extract YouTube Video ID
let videoId: string | null = null;
if (post.platform === 'youtube' && post.url) {
videoId = extractYouTubeVideoId(post.url);
}

return (
<Paper square className={styles.root} ref={ref}>
<HeaderComponentExperience
Expand Down Expand Up @@ -118,20 +128,28 @@ export const PostDetailExperience: React.FC<PostDetailProps> = props => {
/>
</ShowIf>

{post?.asset?.images && post?.asset?.images?.length > 0 && (
<Gallery images={post.asset?.images} variant="vertical" />
)}

{post?.asset?.videos && post?.asset?.videos?.length > 0 && (
<Video url={post.asset.videos[0]} width={560} />
)}
<ShowIf condition={post.platform === 'youtube' && Boolean(videoId)}>
<YouTube
text={post.text}
onHashtagClicked={onHashtagClicked}
videoId={videoId}
/>
</ShowIf>
</ShowIf>

{post?.asset?.images && post?.asset?.images?.length > 0 && (
<Gallery images={post.asset?.images} variant="vertical" />
)}

{post?.asset?.videos && post?.asset?.videos?.length > 0 && (
<Video url={post.asset.videos[0]} width={560} />
)}

{post?.asset?.images?.length === 0 &&
post?.asset?.videos?.length === 0 &&
post.embeddedURL &&
!post.deletedAt && <LinkPreview embed={post.embeddedURL} />}
</div>
</Paper>
</Paper >
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const HeaderComponentExperience: React.FC<PostHeaderExperienceProps> =
case SocialsEnum.REDDIT:
url = `https://reddit.com/user/${post.people?.username as string}`;
break;
case SocialsEnum.YOUTUBE:
url = `https://youtube.com/channel/${post.people?.username as string}`;
break;
case 'myriad':
url = `/profile/${post.createdBy}`;
break;
Expand Down
3 changes: 3 additions & 0 deletions src/components/PostHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export const HeaderComponent: React.FC<PostHeaderProps> = props => {
case SocialsEnum.REDDIT:
url = `https://reddit.com/user/${post.people?.username as string}`;
break;
case SocialsEnum.YOUTUBE:
url = `https://youtube.com/channel/${post.people?.username as string}`;
break;
case 'myriad':
url = `/profile/${post.createdBy}`;
break;
Expand Down
14 changes: 14 additions & 0 deletions src/components/PostImport/PostImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const regex = {
/^(?:https?:\/\/)?(?:www\.|m\.|mobile\.|touch\.|mbasic\.)?(?:facebook\.com|fb(?:\.me|\.com))\/(?!$)(?:(?:\w)*#!\/)?(?:pages\/)?(?:photo\.php\?fbid=)?(?:[\w\-]*\/)*?(?:\/)?(?:profile\.php\?id=)?([^\/?&\s]*)(?:\/|&|\?)?.*$/s,
[SocialsEnum.REDDIT]:
/(?:^.+?)(?:reddit.com)(\/r|\/user)(?:\/[\w\d]+){2}(?:\/)([\w\d]*)/,
[SocialsEnum.YOUTUBE]:
/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/(?:watch\?v=)?([\w-]{11})$/,
};

export const PostImport: React.FC<PostImportProps> = props => {
Expand All @@ -52,6 +54,9 @@ export const PostImport: React.FC<PostImportProps> = props => {
}, [value]);

const handleSanitizeUrl = () => {
if (social === SocialsEnum.YOUTUBE && postId) {
return `https://www.youtube.com/embed/${postId}`;
}
return url.replace(/x\.com/, 'twitter.com');
};

Expand Down Expand Up @@ -101,6 +106,15 @@ export const PostImport: React.FC<PostImportProps> = props => {
return;
}

const matchYoutube = regex[SocialsEnum.YOUTUBE].exec(url);

if (matchYoutube) {
setSocial(SocialsEnum.YOUTUBE);
setPostId(matchYoutube[3]);

return;
}

setError('unsupported');
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/PostList/hooks/use-profile-filter.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const useProfileFilter = (
filterFields.owner = people.id;
break;
case 'imported':
filterFields.platform = ['facebook', 'reddit', 'twitter'];
filterFields.platform = ['facebook', 'reddit', 'twitter', 'youtube'];
filterFields.owner = people.id;
break;

Expand Down
2 changes: 1 addition & 1 deletion src/components/PostList/hooks/use-timeline-filter.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export const useTimelineFilter = (filters?: TimelineFilterFields) => {
filterFields.owner = people.id;
break;
case 'imported':
filterFields.platform = ['facebook', 'reddit', 'twitter'];
filterFields.platform = ['facebook', 'reddit', 'twitter', 'youtube'];
filterFields.owner = people.id;
break;

Expand Down
5 changes: 5 additions & 0 deletions src/components/UserSocials/UserSocials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export const UserSocials: React.FC<UserSocialsProps> = props => {
selectedSocials.people?.username as string
}`;
break;
case SocialsEnum.YOUTUBE:
url = `https://youtube.com/channel/${
selectedSocials.people?.username as string
}`;
break;
}

return url;
Expand Down
26 changes: 23 additions & 3 deletions src/components/atoms/Embed/Embed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import React from 'react';
import { useStyles } from './Embed.styles';
import { RedditEmbed } from './Reddit/RedditEmbed';
import { TweetEmbed } from './Twitter/TweetEmbed';
import { YouTubeEmbed } from './YouTube/YoutubeEmbed';

import { Loading } from 'src/components/atoms/Loading';
import ShowIf from 'src/components/common/show-if.component';
import { SocialsEnum } from 'src/interfaces/social';
import { extractYouTubeVideoId } from 'src/helpers/url';

type EmbedProps = {
social: SocialsEnum;
Expand All @@ -18,14 +20,23 @@ type EmbedProps = {
onClick?: () => void;
};

export const Embed: React.FC<EmbedProps> = props => {
const { social, url, postId, onClick, onError, onLoad } = props;
export const Embed: React.FC<EmbedProps> = ({
social,
url,
postId,
onClick,
onError,
onLoad,
}) => {
const styles = useStyles();

const handleClick = (): void => {
onClick && onClick();
if (onClick) onClick();
};

// Extract YouTube Video ID
const youtubeVideoId = social === SocialsEnum.YOUTUBE ? extractYouTubeVideoId(url) : null;

return (
<div className={styles.root} onClick={handleClick}>
<ShowIf condition={social === SocialsEnum.TWITTER}>
Expand All @@ -48,6 +59,15 @@ export const Embed: React.FC<EmbedProps> = props => {
onError={onError}
/>
</ShowIf>

<ShowIf condition={social === SocialsEnum.YOUTUBE && !!youtubeVideoId}>
<YouTubeEmbed
videoId={youtubeVideoId}
placeholder={<Loading />}
onLoad={onLoad}
onError={onError}
/>
</ShowIf>
</div>
);
};
Loading

0 comments on commit f911f31

Please sign in to comment.