Skip to content

Commit

Permalink
add youtube videos
Browse files Browse the repository at this point in the history
  • Loading branch information
thedaviddias committed Aug 13, 2022
1 parent acf0815 commit 45e056a
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 71 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ SPOTIFY_CLIENT_SECRET=
SPOTIFY_REFRESH_TOKEN=

# YouTube metrics
YOUTUBE_CHANNEL_ID=
GOOGLE_CLIENT_EMAIL=
GOOGLE_PRIVATE_KEY=
106 changes: 106 additions & 0 deletions data/youtube.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"videos": [
{
"kind": "youtube#searchResult",
"etag": "3g27Qd3oIQ4_4s10Ss9mvcZpnHY",
"id": {
"kind": "youtube#video",
"videoId": "CcJJhJWEERk"
},
"snippet": {
"publishedAt": "2019-01-18T13:33:55Z",
"channelId": "UCXYs_tVa-VFm5f6bWrPybhA",
"title": "Ressources INDISPENSABLES pour Développeur Web Débutant",
"description": "J'ai compilé la liste des ressources indispensables pour apprendre à devenir développeur Front-End. Vous trouverez dans cette ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/CcJJhJWEERk/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/CcJJhJWEERk/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/CcJJhJWEERk/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "The David Dias",
"liveBroadcastContent": "none",
"publishTime": "2019-01-18T13:33:55Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "NzkjLq__np8onlw7oacgS4uTuug",
"id": {
"kind": "youtube#video",
"videoId": "G9Q8KthzPpM"
},
"snippet": {
"publishedAt": "2019-01-12T18:56:26Z",
"channelId": "UCXYs_tVa-VFm5f6bWrPybhA",
"title": "Comment synchroniser Chrome avec son compte Google",
"description": "Vous venez d'installer Chrome... Et vous ne savez par quoi commencer. Google Chrome Sync vous permet de vous connecter à ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/G9Q8KthzPpM/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/G9Q8KthzPpM/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/G9Q8KthzPpM/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "The David Dias",
"liveBroadcastContent": "none",
"publishTime": "2019-01-12T18:56:26Z"
}
},
{
"kind": "youtube#searchResult",
"etag": "sitAu4ZP7evJ7p39JdGiBdklHgY",
"id": {
"kind": "youtube#video",
"videoId": "FL0hVIPFPc8"
},
"snippet": {
"publishedAt": "2019-01-08T09:00:00Z",
"channelId": "UCXYs_tVa-VFm5f6bWrPybhA",
"title": "Astuces pour s'expatrier au 🇨🇦CANADA",
"description": "Vous avez envie de partir vivre à l'étranger ? Travailler et gagner de l'expérience en travaillant dans le web ? Voici quelques ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/FL0hVIPFPc8/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/FL0hVIPFPc8/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/FL0hVIPFPc8/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "The David Dias",
"liveBroadcastContent": "none",
"publishTime": "2019-01-08T09:00:00Z"
}
}
]
}
6 changes: 6 additions & 0 deletions locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,5 +226,11 @@
"sections": {
"viewAll": "→ View my profile"
}
},
"youtube": {
"sections": {
"latest_videos": "Latest Youtube videos",
"viewAll": "→ Access my Youtube channel"
}
}
}
4 changes: 3 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ const nextConfig = nextTranslate({
pageExtensions: ['ts', 'tsx', 'mdx'],
poweredByHeader: false,
images: {
domains: ['webmention.io', 'i.gr-assets.com'],
domains: ['webmention.io', 'i.gr-assets.com', 'i.ytimg.com'],
formats: ['image/avif', 'image/webp'],
},
experimental: {
images: {
allowFutureImage: true,
},
workerThreads: false,
cpus: 1,
},
async redirects() {
return [
Expand Down
2 changes: 1 addition & 1 deletion public/rss/feed.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title>The David Dias | Front-End Developer, podcaster &amp; content creator</title>
<link>https://thedaviddias.dev</link>
<description>Hey, I'm David Dias! Front-End Developer based in Toronto/Canada. I love talking about code, technology, expatriation and life.</description>
<lastBuildDate>Sat, 13 Aug 2022 00:15:43 GMT</lastBuildDate>
<lastBuildDate>Sat, 13 Aug 2022 14:52:06 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator>
<language>en</language>
Expand Down
2 changes: 1 addition & 1 deletion public/rss/fr/feed.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title>The David Dias | Développeur Front-End, podcasteur &amp; créateur de contenu</title>
<link>https://thedaviddias.dev</link>
<description>Salut toi! Je m'appele David Dias. Je suis développeur Front-End, podcasteur, créateur de contenu numérique passioné pour résoudre les problèmes digitaux et humains! J'aime rencontré de nouvelles personnes, bâtir des communautées et parler de tech, d'expatriation et de web.</description>
<lastBuildDate>Sat, 13 Aug 2022 00:15:43 GMT</lastBuildDate>
<lastBuildDate>Sat, 13 Aug 2022 14:52:06 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>https://github.com/jpmonette/feed</generator>
<language>fr</language>
Expand Down
77 changes: 77 additions & 0 deletions src/components/LatestYoutubeVideos/LatestYoutubeVideos.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Image from 'next/future/image'
import useTranslation from 'next-translate/useTranslation'
import useSWR from 'swr'

import fetcher from '@/utils/fetcher'

import { CustomLink } from '../CustomLink'
import { H4, H5 } from '../Headings'
import { Loader } from '../Loader'

type LatestYoutubeVideosRes = {
videos: {
id: {
videoId: string
}
snippet: {
title: string
thumbnails: {
high: {
url: string
width: string
height: string
}
}
}
}[]
}

const LatestYoutubeVideos = () => {
const { t } = useTranslation('common')
const { data, error } = useSWR<LatestYoutubeVideosRes>('/api/youtube/videos', fetcher, {
revalidateOnFocus: false,
})

if (error) return <></>
if (!data) return <Loader />

const videos = data?.videos

return (
<section className="border-none mb-10">
<header>
<H5 as="h2">{t('youtube.sections.latest_videos')}</H5>
</header>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 my-3 max-w-5xl">
{videos?.map((video, i) => (
<div key={i} className="relative mt-3">
<div className="mb-3">
<Image
src={video.snippet.thumbnails.high.url}
alt=""
width={video.snippet.thumbnails.high.width}
height={video.snippet.thumbnails.high.height}
className="aspect-video object-cover h-44 rounded-lg"
/>
</div>
<H4 as="h3">
<CustomLink
href={`https://youtube.com/watch?v=${video.id.videoId}`}
className="dark:!text-white before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:bottom-0"
>
{video.snippet.title}
</CustomLink>
</H4>
</div>
))}
</div>
<footer className="text-right">
<CustomLink href="https://www.youtube.com/c/TheDavidDias" className="dark:!text-white">
{t('youtube.sections.viewAll')}
</CustomLink>
</footer>
</section>
)
}

export default LatestYoutubeVideos
1 change: 1 addition & 0 deletions src/components/LatestYoutubeVideos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './LatestYoutubeVideos'
7 changes: 6 additions & 1 deletion src/components/PodcastSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { H5 } from '@/components/Headings'
import { PodcastsResponse } from '@/pages/api/spotify/podcasts'
import fetcher from '@/utils/fetcher'

import { Loader } from '../Loader'

const PodcastSection = () => {
const { t, lang } = useTranslation('common')
const { theme, resolvedTheme } = useTheme()
const { data } = useSWR<PodcastsResponse>(`/api/spotify/podcasts?lang=${lang}`, fetcher)
const { data, error } = useSWR<PodcastsResponse>(`/api/spotify/podcasts?lang=${lang}`, fetcher)

if (error) return <></>
if (!data) return <Loader />

return (
<section className="grid grid-cols-1 gap-y-10 border-none mb-10">
Expand Down
25 changes: 25 additions & 0 deletions src/components/YoutubeCard/YoutubeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import useSWR from 'swr'

import fetcher from '@/utils/fetcher'

import { MetricsCard } from '../MetricsCard'

export type YouTube = {
subscriberCount: number
viewCount: number
}

export const YouTubeCard = () => {
const { data } = useSWR<YouTube>('/api/youtube', fetcher)

const subscriberCount = data?.subscriberCount
const viewCount = data?.viewCount
const link = 'https://www.youtube.com/channel/UCXYs_tVa-VFm5f6bWrPybhA'

return (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 my-2 w-full">
<MetricsCard header="YouTube Subscribers" link={link} metric={subscriberCount} />
<MetricsCard header="YouTube Views" link={link} metric={viewCount} />
</div>
)
}
1 change: 1 addition & 0 deletions src/components/YoutubeCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './YoutubeCard'
1 change: 1 addition & 0 deletions src/declarations/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ declare global {
SPOTIFY_REFRESH_TOKEN: string

// YouTube metrics
YOUTUBE_CHANNEL_ID: string
GOOGLE_CLIENT_EMAIL: string
GOOGLE_PRIVATE_KEY: string
}
Expand Down
38 changes: 38 additions & 0 deletions src/pages/api/youtube/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { withSentry } from '@sentry/nextjs'
import { google } from 'googleapis'
import type { NextApiRequest, NextApiResponse } from 'next'

import googleAuth from '@/lib/google'

const YoutubeStatsHandler = async (req: NextApiRequest, res: NextApiResponse) => {
const youtubeId = process.env.YOUTUBE_CHANNEL_ID

try {
const auth = await googleAuth.getClient()
const youtube = google.youtube({
auth,
version: 'v3',
})

const response = await youtube.channels.list({
id: [youtubeId],
part: ['statistics'],
})

const channel = response.data.items && response?.data.items[0]
const { subscriberCount, viewCount, videoCount } = channel?.statistics as any

res.setHeader('Cache-Control', 'public, s-maxage=1200, stale-while-revalidate=600')

return res.status(200).json({
subscriberCount,
viewCount,
videoCount,
})
} catch (error) {
res.json(error)
res.status(405).end()
}
}

export default withSentry(YoutubeStatsHandler)
39 changes: 39 additions & 0 deletions src/pages/api/youtube/videos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { withSentry } from '@sentry/nextjs'
import { google } from 'googleapis'
import type { NextApiRequest, NextApiResponse } from 'next'

import googleAuth from '@/lib/google'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const youtubeId = process.env.YOUTUBE_CHANNEL_ID

try {
const auth = await googleAuth.getClient()
const youtube = google.youtube({
auth,
version: 'v3',
})

const listVideos = await youtube.search.list({
channelId: youtubeId,
maxResults: 3,
order: 'date',
type: ['video'],
regionCode: 'CA',
part: ['snippet'],
})

const videos = listVideos.data.items

res.setHeader('Cache-Control', 'public, s-maxage=1200, stale-while-revalidate=600')

return res.status(200).json({
videos,
})
} catch (error) {
res.json(error)
res.status(405).end()
}
}

export default withSentry(handler)
Loading

1 comment on commit 45e056a

@vercel
Copy link

@vercel vercel bot commented on 45e056a Aug 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.