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}
+
+
+
+ ))}
+
+
+
+ )
+}
+
+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}
+
+ ))}
+
-
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
+ {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 {