From 45e056a2faa678d2ede3034727fe8bc507b84a35 Mon Sep 17 00:00:00 2001 From: David Dias Date: Sat, 13 Aug 2022 10:52:18 -0400 Subject: [PATCH] add youtube videos --- .env.example | 1 + data/youtube.json | 106 ++++++++++++ locales/en/common.json | 6 + next.config.js | 4 +- public/rss/feed.xml | 2 +- public/rss/fr/feed.xml | 2 +- .../LatestYoutubeVideos.tsx | 77 +++++++++ src/components/LatestYoutubeVideos/index.ts | 1 + src/components/PodcastSection/index.tsx | 7 +- src/components/YoutubeCard/YoutubeCard.tsx | 25 +++ src/components/YoutubeCard/index.ts | 1 + src/declarations/env.d.ts | 1 + src/pages/api/youtube/stats.ts | 38 +++++ src/pages/api/youtube/videos.ts | 39 +++++ src/pages/index.tsx | 151 ++++++++++-------- 15 files changed, 390 insertions(+), 71 deletions(-) create mode 100644 data/youtube.json create mode 100644 src/components/LatestYoutubeVideos/LatestYoutubeVideos.tsx create mode 100644 src/components/LatestYoutubeVideos/index.ts create mode 100644 src/components/YoutubeCard/YoutubeCard.tsx create mode 100644 src/components/YoutubeCard/index.ts create mode 100644 src/pages/api/youtube/stats.ts create mode 100644 src/pages/api/youtube/videos.ts diff --git a/.env.example b/.env.example index 1016d990..5818fafe 100644 --- a/.env.example +++ b/.env.example @@ -36,5 +36,6 @@ SPOTIFY_CLIENT_SECRET= SPOTIFY_REFRESH_TOKEN= # YouTube metrics +YOUTUBE_CHANNEL_ID= GOOGLE_CLIENT_EMAIL= GOOGLE_PRIVATE_KEY= diff --git a/data/youtube.json b/data/youtube.json new file mode 100644 index 00000000..7eead956 --- /dev/null +++ b/data/youtube.json @@ -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" + } + } + ] +} diff --git a/locales/en/common.json b/locales/en/common.json index 39bea0c6..4f85b645 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -226,5 +226,11 @@ "sections": { "viewAll": "→ View my profile" } + }, + "youtube": { + "sections": { + "latest_videos": "Latest Youtube videos", + "viewAll": "→ Access my Youtube channel" + } } } diff --git a/next.config.js b/next.config.js index fb8e69fb..41609c10 100644 --- a/next.config.js +++ b/next.config.js @@ -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 [ diff --git a/public/rss/feed.xml b/public/rss/feed.xml index 2df8de45..b1f0ae6f 100644 --- a/public/rss/feed.xml +++ b/public/rss/feed.xml @@ -4,7 +4,7 @@ The David Dias | Front-End Developer, podcaster & content creator https://thedaviddias.dev Hey, I'm David Dias! Front-End Developer based in Toronto/Canada. I love talking about code, technology, expatriation and life. - Sat, 13 Aug 2022 00:15:43 GMT + Sat, 13 Aug 2022 14:52:06 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed en diff --git a/public/rss/fr/feed.xml b/public/rss/fr/feed.xml index 911cfddc..5591bcfe 100644 --- a/public/rss/fr/feed.xml +++ b/public/rss/fr/feed.xml @@ -4,7 +4,7 @@ The David Dias | Développeur Front-End, podcasteur & créateur de contenu https://thedaviddias.dev 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. - Sat, 13 Aug 2022 00:15:43 GMT + Sat, 13 Aug 2022 14:52:06 GMT https://validator.w3.org/feed/docs/rss2.html https://github.com/jpmonette/feed fr diff --git a/src/components/LatestYoutubeVideos/LatestYoutubeVideos.tsx b/src/components/LatestYoutubeVideos/LatestYoutubeVideos.tsx new file mode 100644 index 00000000..26dc853a --- /dev/null +++ b/src/components/LatestYoutubeVideos/LatestYoutubeVideos.tsx @@ -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('/api/youtube/videos', fetcher, { + revalidateOnFocus: false, + }) + + if (error) return <> + if (!data) return + + const videos = data?.videos + + return ( +
+
+
{t('youtube.sections.latest_videos')}
+
+
+ {videos?.map((video, i) => ( +
+
+ +
+

+ + {video.snippet.title} + +

+
+ ))} +
+
+ + {t('youtube.sections.viewAll')} + +
+
+ ) +} + +export default LatestYoutubeVideos diff --git a/src/components/LatestYoutubeVideos/index.ts b/src/components/LatestYoutubeVideos/index.ts new file mode 100644 index 00000000..e184863a --- /dev/null +++ b/src/components/LatestYoutubeVideos/index.ts @@ -0,0 +1 @@ +export * from './LatestYoutubeVideos' diff --git a/src/components/PodcastSection/index.tsx b/src/components/PodcastSection/index.tsx index 080027d9..23ed4f24 100644 --- a/src/components/PodcastSection/index.tsx +++ b/src/components/PodcastSection/index.tsx @@ -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(`/api/spotify/podcasts?lang=${lang}`, fetcher) + const { data, error } = useSWR(`/api/spotify/podcasts?lang=${lang}`, fetcher) + + if (error) return <> + if (!data) return return (
diff --git a/src/components/YoutubeCard/YoutubeCard.tsx b/src/components/YoutubeCard/YoutubeCard.tsx new file mode 100644 index 00000000..d9497c39 --- /dev/null +++ b/src/components/YoutubeCard/YoutubeCard.tsx @@ -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('/api/youtube', fetcher) + + const subscriberCount = data?.subscriberCount + const viewCount = data?.viewCount + const link = 'https://www.youtube.com/channel/UCXYs_tVa-VFm5f6bWrPybhA' + + return ( +
+ + +
+ ) +} diff --git a/src/components/YoutubeCard/index.ts b/src/components/YoutubeCard/index.ts new file mode 100644 index 00000000..7d3c88dc --- /dev/null +++ b/src/components/YoutubeCard/index.ts @@ -0,0 +1 @@ +export * from './YoutubeCard' diff --git a/src/declarations/env.d.ts b/src/declarations/env.d.ts index 953b9599..97e4fb15 100644 --- a/src/declarations/env.d.ts +++ b/src/declarations/env.d.ts @@ -41,6 +41,7 @@ declare global { SPOTIFY_REFRESH_TOKEN: string // YouTube metrics + YOUTUBE_CHANNEL_ID: string GOOGLE_CLIENT_EMAIL: string GOOGLE_PRIVATE_KEY: string } diff --git a/src/pages/api/youtube/stats.ts b/src/pages/api/youtube/stats.ts new file mode 100644 index 00000000..c760c7be --- /dev/null +++ b/src/pages/api/youtube/stats.ts @@ -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) diff --git a/src/pages/api/youtube/videos.ts b/src/pages/api/youtube/videos.ts new file mode 100644 index 00000000..bf355721 --- /dev/null +++ b/src/pages/api/youtube/videos.ts @@ -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) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index af8da352..fea07832 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,6 +3,7 @@ import dynamic from 'next/dynamic' import Image from 'next/future/image' import { NextSeo } from 'next-seo' import useTranslation from 'next-translate/useTranslation' +import { SWRConfig } from 'swr' import generateRssFeed from '@/lib/generateRss' import { fetchRepos } from '@/lib/github' @@ -14,109 +15,122 @@ import { Dashboard } from '@/components/Dashboard' import { LatestGithubSection } from '@/components/LatestGithubSection' import { LatestNotesSection } from '@/components/LatestNotesSection' import { LatestPostsSection } from '@/components/LatestPostsSection' +import LatestYoutubeVideos from '@/components/LatestYoutubeVideos/LatestYoutubeVideos' import { Loader } from '@/components/Loader' import { ToRead } from '@/components/ToRead' import { routes } from '@/config/routes' import { HERO_LINKS } from '@/constants' import { getAllPostsWithFrontMatter } from '@/utils/get-articles-posts' +import { readData } from '@/utils/read-data' const PodcastSection = dynamic(() => import('../components/PodcastSection'), { loading: () => , ssr: false, }) +// const LatestYoutubeVideos = dynamic(() => import('../components/LatestYoutubeVideos'), { +// loading: () => , +// ssr: false, +// }) + type HomeProps = { articles: any[] notes: any[] ghProjects: any[] + fallback: any } -const Home: NextPage = ({ articles, notes, ghProjects }) => { +const Home: NextPage = ({ articles, notes, ghProjects, fallback }) => { const { t } = useTranslation('common') return ( - - -
-
-
-

-

{t('home.hero.greetings1')}

- - {t('home.hero.greetings2')} - -

-
-
- {HERO_LINKS.map(({ label, link, rel }) => ( - - {label} - - ))} + + + +
+
+
+

+

{t('home.hero.greetings1')}

+ + {t('home.hero.greetings2')} + +

+
+
+ {HERO_LINKS.map(({ label, link, rel }) => ( + + {label} + + ))} +
-
-
- Photo of David Dias -
-
+
+ Photo of David Dias +
+
- + -
-
- - +
+
+ + +
-
- + + + - + {process.env.NODE_ENV === 'production' && } - {process.env.NODE_ENV === 'production' && } + {process.env.NODE_ENV === 'production' && } - + - {/* */} -
-
+ {/* */} + + + ) } export const getStaticProps: GetStaticProps = async ({ locale }) => { const posts = await getAllPostsWithFrontMatter({ dataType: 'articles', locale, limit: 4 }) const notes = await getAllPostsWithFrontMatter({ dataType: 'notes', locale, limit: 4 }) + const youtubeVideos = await readData('data/youtube.json') const ghProjects = await fetchRepos('PUSHED_AT', 2) await generateRssFeed().then(null) @@ -125,6 +139,9 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => { articles: JSON.parse(JSON.stringify(posts)), notes: JSON.parse(JSON.stringify(notes)), ghProjects, + fallback: { + '/api/youtube/videos': youtubeVideos, + }, } return {