Skip to content

Commit

Permalink
fix: display Youtube playlists in RichText (#280)
Browse files Browse the repository at this point in the history
* fix: enhance isYoutubeUrl to accept videos from playlists

* embed playlists in the YouTube iframe

* Refactor extract search params function. Prefer URL object methods over ternaries.
  • Loading branch information
DiogoSoaress authored Mar 4, 2024
1 parent 0e3ac7c commit 620cbf6
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 6 deletions.
8 changes: 4 additions & 4 deletions src/components/Blog/YouTube/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { extractYouTubeVideoId } from '@/lib/urlPatterns'
import { getYoutubeVideoSrc } from '@/lib/urlPatterns'
import css from './styles.module.css'

const YouTube = ({ url }: { url: string }) => {
const videoId = extractYouTubeVideoId(url)
const videoSrc = getYoutubeVideoSrc(url)

if (!videoId) return null
if (!videoSrc) return null

return (
<div className={css.videoResponsive}>
<iframe
width="853"
height="480"
src={`https://www.youtube-nocookie.com/embed/${videoId}`}
src={videoSrc}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; fullscreen"
title="Embedded YouTube"
/>
Expand Down
62 changes: 61 additions & 1 deletion src/lib/__test__/urlPatterns.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { extractLastPathname, extractYouTubeVideoId, isTwitterUrl, isYouTubeUrl } from '@/lib/urlPatterns'
import {
extractLastPathname,
extractURLSearchParams,
extractYouTubeVideoId,
getYoutubeVideoSrc,
isTwitterUrl,
isYouTubeUrl,
} from '@/lib/urlPatterns'

describe('urlPatterns', () => {
describe('isYouTubeUrl', () => {
Expand All @@ -7,6 +14,19 @@ describe('urlPatterns', () => {
expect(isYouTubeUrl('https://youtu.be/dQw4w9WgXcQ')).toBe(true)
})

it('should return true for valid videos from YouTube playlists', () => {
expect(isYouTubeUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PL0knnt70iEZpZ_40NHziZ4u7u9f_OW9p6')).toBe(
true,
)
expect(
isYouTubeUrl('https://www.youtube.com/watch?v=VbYPL4SVXZw&list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd&index=1'),
).toBe(true)
})

it('should return true for valid YouTube playlist URLs', () => {
expect(isYouTubeUrl('https://www.youtube.com/playlist?list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd')).toBe(true)
})

it('should return false for invalid YouTube URLs', () => {
expect(isYouTubeUrl('https://www.google.com')).toBe(false)
expect(isYouTubeUrl('https://twitter.com')).toBe(false)
Expand All @@ -25,6 +45,46 @@ describe('urlPatterns', () => {
})
})

describe('extractURLSearchParams', () => {
it('should extract search parameters from URL', () => {
const url = 'https://www.youtube.com/playlist?list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd&index=3'
const params = extractURLSearchParams(url)
expect(params.list).toBe('PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd')
expect(params.index).toBe('3')
})

it('should return null for parameters not present in URL', () => {
const url = 'https://www.youtube.com/playlist?list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd'
const params = extractURLSearchParams(url)
expect(params.list).toBe('PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd')
expect(params.index).toBe(undefined)
})
})

describe('getYoutubeVideoSrc', () => {
test('should return correct video src for a video URL', () => {
const url = 'https://www.youtube.com/watch?v=Xe5FcBK9vFw'
const src = getYoutubeVideoSrc(url)
expect(src).toBe('https://www.youtube-nocookie.com/embed/Xe5FcBK9vFw')
})

test('should return correct video src for a playlist URL with index', () => {
const url = 'https://www.youtube.com/playlist?list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd&index=0'
const src = getYoutubeVideoSrc(url)
expect(src).toBe(
'https://www.youtube-nocookie.com/embed/videoseries?list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd&index=0',
)
})

test('should return correct video src for a playlist URL without index', () => {
const url = 'https://www.youtube.com/playlist?list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd'
const src = getYoutubeVideoSrc(url)
expect(src).toBe(
'https://www.youtube-nocookie.com/embed/videoseries?list=PL0knnt70iEZry_pACXB4sKGSuZz5XNoOd&index=1',
)
})
})

describe('isTwitterUrl', () => {
it('should return true for valid Twitter URLs', () => {
expect(isTwitterUrl('https://twitter.com/safe/status/1754882331871944935')).toBe(true)
Expand Down
26 changes: 25 additions & 1 deletion src/lib/urlPatterns.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export function isYouTubeUrl(url: string) {
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)[\w-]{11}$/
const youtubeRegex =
/^(https?:\/\/)?(www\.)?(youtube\.com\/(watch\?v=|playlist\?)|youtu\.be\/)([\w-]{11})?(&?list=[\w]{34})?/

return youtubeRegex.test(url)
}

Expand All @@ -10,16 +12,38 @@ export function extractYouTubeVideoId(url: string) {
// long form
const urlObj = new URL(url)
const searchParams = new URLSearchParams(urlObj.search)

return searchParams.get('v')
}

export const extractURLSearchParams = (url: string) => Object.fromEntries(new URL(url).searchParams.entries())

export const getYoutubeVideoSrc = (url: string) => {
const videoId = extractYouTubeVideoId(url)
const { list, index } = extractURLSearchParams(url)

const urlObj = new URL(`https://www.youtube-nocookie.com/embed/${videoId ? videoId : 'videoseries'}`)

if (list) {
urlObj.searchParams.set('list', list)
}

if (!videoId) {
urlObj.searchParams.set('index', index ?? 1)
}

return urlObj.toString()
}

export function isTwitterUrl(url: string) {
const tweeterRegex = /^(https?:\/\/)?(?:www\.)?(twitter\.com\/|x\.com\/)[a-zA-Z0-9_]+\/status\/\d+(\?s=\d+)?$/

return tweeterRegex.test(url)
}

export function extractLastPathname(url: string) {
const urlObj = new URL(url)
const pathnameParts = urlObj.pathname.split('/').filter((part) => part !== '')

return pathnameParts[pathnameParts.length - 1] || ''
}

0 comments on commit 620cbf6

Please sign in to comment.