Skip to content

Commit c95018b

Browse files
authored
feat: Pods are own page and searchable (#38)
1 parent 4a19429 commit c95018b

File tree

10 files changed

+1541
-460
lines changed

10 files changed

+1541
-460
lines changed

package-lock.json

Lines changed: 1387 additions & 353 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"astro-pagefind": "^1.8.3",
1919
"canvas-confetti": "^1.9.3",
2020
"googleapis": "^144.0.0",
21-
"swiper": "^11.2.6",
21+
"swiper": "^12.1.2",
2222
"tailwindcss": "^4.0.3"
2323
},
2424
"devDependencies": {

src/components/PodsGrid.astro

Lines changed: 5 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,16 @@
11
---
2-
import { getPlaylistItems } from "@utils/youtubeApi";
3-
const fullPodPlaylistId = import.meta.env.COA_PODCASTS_FULL_PLAYLIST_ID;
4-
5-
type PodcastItem = {
6-
id: string;
7-
title: string;
8-
thumbnail: string;
9-
publishedAt: string;
10-
};
11-
12-
const podCount = 20;
13-
let data = await getPlaylistItems(podCount, fullPodPlaylistId);
14-
15-
let privateVidCount = 0;
16-
data?.items?.forEach((item) => {
17-
item.snippet.title === "Private video" ? privateVidCount++ : null;
18-
});
19-
data = await getPlaylistItems(podCount + privateVidCount, fullPodPlaylistId);
2+
import { getPodcasts } from "@utils/podcasts";
203
21-
const podcasts = data?.items
22-
?.map(
23-
(item): PodcastItem => ({
24-
id: item.contentDetails.videoId,
25-
title: item.snippet.title,
26-
thumbnail: item.snippet.thumbnails.maxres?.url || item.snippet.thumbnails.high?.url,
27-
publishedAt: new Date(item.contentDetails.videoPublishedAt).toLocaleDateString("en-US", {
28-
year: "numeric",
29-
month: "long",
30-
day: "numeric",
31-
}),
32-
})
33-
)
34-
.filter((podcast) => podcast.title !== "Private video");
4+
const fullPodPlaylistId = import.meta.env.COA_PODCASTS_FULL_PLAYLIST_ID;
5+
const podcasts = await getPodcasts(fullPodPlaylistId);
356
---
367

378
<div class="bg-gray p-8 lg:p-12 xl:p-19">
389
<div class="mx-auto">
3910
<div class="grid grid-cols-1 w-full md:grid-cols-2 lg:grid-cols-4 gap-14 sm:gap-8 lg:gap-8">
4011
{
4112
podcasts?.map((podcast) => (
42-
<div class="group cursor-pointer video-item" data-video-id={podcast.id}>
13+
<a href={`/podcasts/${podcast.slug}`} class="group cursor-pointer">
4314
<div class="relative aspect-video overflow-hidden rounded-lg">
4415
<img
4516
src={podcast.thumbnail}
@@ -51,77 +22,9 @@ const podcasts = data?.items
5122
</div>
5223
<h3 class="mt-2 text-pearl text-sm font-barlow line-clamp-2">{podcast.title}</h3>
5324
<p class="text-jonquil text-xs mt-1">{podcast.publishedAt}</p>
54-
</div>
25+
</a>
5526
))
5627
}
5728
</div>
5829
</div>
5930
</div>
60-
61-
<!-- Video Modal -->
62-
<div id="videoModal" class="fixed inset-0 bg-black/80 z-50 hidden">
63-
<button id="closeModalButton" class="fixed top-4 right-4 text-white hover:text-white/80 transition-colors z-50">
64-
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
65-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
66-
</svg>
67-
</button>
68-
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[95vw] max-w-7xl lg:max-w-[85vw]">
69-
<div class="aspect-video w-full">
70-
<iframe
71-
id="modalVideoFrame"
72-
class="w-full h-full"
73-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
74-
allowfullscreen
75-
>
76-
</iframe>
77-
</div>
78-
</div>
79-
</div>
80-
81-
<script>
82-
function openVideoModal(videoId: string) {
83-
const modal = document.getElementById("videoModal");
84-
const iframe = document.getElementById("modalVideoFrame") as HTMLIFrameElement;
85-
if (modal && iframe) {
86-
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
87-
modal.classList.remove("hidden");
88-
document.body.style.overflow = "hidden";
89-
}
90-
}
91-
92-
function closeVideoModal() {
93-
const modal = document.getElementById("videoModal");
94-
const iframe = document.getElementById("modalVideoFrame") as HTMLIFrameElement;
95-
if (modal && iframe) {
96-
iframe.src = "";
97-
modal.classList.add("hidden");
98-
document.body.style.overflow = "";
99-
}
100-
}
101-
102-
// Add click event listeners to all video items
103-
document.addEventListener("DOMContentLoaded", () => {
104-
const videoItems = document.querySelectorAll(".video-item");
105-
videoItems.forEach((item) => {
106-
item.addEventListener("click", () => {
107-
const videoId = item.getAttribute("data-video-id");
108-
if (videoId) {
109-
openVideoModal(videoId);
110-
}
111-
});
112-
});
113-
114-
// Add click event listener to close button
115-
const closeButton = document.getElementById("closeModalButton");
116-
if (closeButton) {
117-
closeButton.addEventListener("click", closeVideoModal);
118-
}
119-
});
120-
121-
// Close modal when clicking outside the video
122-
document.getElementById("videoModal")?.addEventListener("click", (e) => {
123-
if (e.target === e.currentTarget) {
124-
closeVideoModal();
125-
}
126-
});
127-
</script>

src/layouts/Post.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const { Content } = await render(post);
1313
data-pagefind-body
1414
>
1515
<h4
16-
data-pagefind-weight="10"
16+
data-pagefind-weight="9"
1717
class="font-noto text-2xl sm:text-3xl md:text-4xl text-pearl text-center pb-4 sm:pb-8 md:pb-12 lg:pb-21 max-w-full sm:max-w-[90%] md:max-w-[80%] lg:max-w-[66%] place-self-center"
1818
>
1919
{post.data.title}
@@ -34,7 +34,7 @@ const { Content } = await render(post);
3434
</div>
3535
)
3636
}
37-
<div class="font-barlow text-base sm:text-lg md:text-xl text-pearl" data-pagefind-weight="9">
37+
<div class="font-barlow text-base sm:text-lg md:text-xl text-pearl" data-pagefind-weight="7">
3838
<Content />
3939
</div>
4040
</div>

src/pages/fr/podcasts/[slug].astro

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
import Main from "@layouts/Main.astro";
3+
import { getPodcasts } from "@utils/podcasts";
4+
import * as m from "@paraglide/messages.js";
5+
6+
export const prerender = true;
7+
8+
export async function getStaticPaths() {
9+
const playlistId = import.meta.env.COA_PODCASTS_FULL_PLAYLIST_ID;
10+
const podcasts = await getPodcasts(playlistId);
11+
return podcasts.map((podcast) => ({
12+
params: { slug: podcast.slug },
13+
props: { podcast },
14+
}));
15+
}
16+
17+
const { podcast } = Astro.props;
18+
---
19+
20+
<Main title={`${podcast.title} | ${m.podcastsLabel()} | ${m.siteName()}`}>
21+
<div class="bg-gray w-full">
22+
<div data-pagefind-body class="flex flex-col justify-center py-8 sm:py-12 md:py-16 lg:py-21">
23+
<h4
24+
class="font-noto text-2xl sm:text-3xl md:text-4xl text-pearl text-center pb-4 sm:pb-8 md:pb-12 lg:pb-21 max-w-full sm:max-w-[90%] md:max-w-[80%] lg:max-w-[66%] place-self-center"
25+
data-pagefind-weight="10"
26+
>
27+
{podcast.title}
28+
</h4>
29+
<p class="text-jonquil text-sm text-center pb-4 sm:pb-8 md:pb-12 lg:pb-21">{podcast.publishedAt}</p>
30+
<div class="relative w-full px-40">
31+
<div class="relative pb-[56.25%] h-0">
32+
<iframe
33+
class="absolute top-0 left-0 w-full h-full"
34+
src={`https://www.youtube.com/embed/${podcast.id}`}
35+
title={podcast.title}
36+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
37+
allowfullscreen
38+
loading="lazy"
39+
>
40+
</iframe>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
</Main>

src/pages/podcasts/[slug].astro

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
import Main from "@layouts/Main.astro";
3+
import { getPodcasts } from "@utils/podcasts";
4+
import * as m from "@paraglide/messages.js";
5+
6+
export const prerender = true;
7+
8+
export async function getStaticPaths() {
9+
const playlistId = import.meta.env.COA_PODCASTS_FULL_PLAYLIST_ID;
10+
const podcasts = await getPodcasts(playlistId);
11+
return podcasts.map((podcast) => ({
12+
params: { slug: podcast.slug },
13+
props: { podcast },
14+
}));
15+
}
16+
17+
const { podcast } = Astro.props;
18+
---
19+
20+
<Main title={`${podcast.title} | ${m.podcastsLabel()}`}>
21+
<div class="bg-gray w-full">
22+
<div data-pagefind-body class="flex flex-col justify-center py-8 sm:py-12 md:py-16 lg:py-21">
23+
<h4
24+
class="font-noto text-2xl sm:text-3xl md:text-4xl text-pearl text-center pb-4 sm:pb-8 md:pb-12 lg:pb-21 max-w-full sm:max-w-[90%] md:max-w-[80%] lg:max-w-[66%] place-self-center"
25+
data-pagefind-weight="10"
26+
>
27+
{podcast.title}
28+
</h4>
29+
<p class="text-jonquil text-sm text-center pb-4 sm:pb-8 md:pb-12 lg:pb-21">{podcast.publishedAt}</p>
30+
<div class="relative w-full px-40 mb-4">
31+
<div class="relative pb-[56.25%] h-0">
32+
<iframe
33+
class="absolute top-0 left-0 w-full h-full"
34+
src={`https://www.youtube.com/embed/${podcast.id}`}
35+
title={podcast.title}
36+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
37+
allowfullscreen
38+
loading="lazy"
39+
>
40+
</iframe>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
</Main>

src/utils/podcasts.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { getPlaylistItems } from "@utils/youtubeApi";
2+
3+
export type PodcastItem = {
4+
id: string;
5+
slug: string;
6+
title: string;
7+
thumbnail: string;
8+
publishedAt: string;
9+
};
10+
11+
export function slugify(text: string): string {
12+
return text
13+
.toLowerCase()
14+
.replace(/['']/g, "")
15+
.replace(/[^a-z0-9]+/g, "-")
16+
.replace(/^-+|-+$/g, "");
17+
}
18+
19+
export async function getPodcasts(
20+
playlistId: string,
21+
count = 20,
22+
): Promise<PodcastItem[]> {
23+
let data = await getPlaylistItems(count, playlistId);
24+
25+
let privateVidCount = 0;
26+
data?.items?.forEach((item) => {
27+
if (item.snippet.title === "Private video") privateVidCount++;
28+
});
29+
if (privateVidCount > 0) {
30+
data = await getPlaylistItems(count + privateVidCount, playlistId);
31+
}
32+
33+
return (
34+
data?.items
35+
?.map(
36+
(item): PodcastItem => ({
37+
id: item.contentDetails.videoId,
38+
slug: slugify(item.snippet.title),
39+
title: item.snippet.title,
40+
thumbnail:
41+
item.snippet.thumbnails.maxres?.url ||
42+
item.snippet.thumbnails.high?.url,
43+
publishedAt: new Date(
44+
item.contentDetails.videoPublishedAt,
45+
).toLocaleDateString("en-US", {
46+
year: "numeric",
47+
month: "long",
48+
day: "numeric",
49+
}),
50+
}),
51+
)
52+
.filter((podcast) => podcast.title !== "Private video") ?? []
53+
);
54+
}

src/utils/youtubeApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ type PlaylistItem = {
1717
videoId: string;
1818
videoPublishedAt: string;
1919
};
20-
}
20+
},
2121
];
2222
};
2323

2424
// missing return type interface
2525
export const getPlaylistItems = async (
2626
maxResults: number,
27-
playlistId: string
27+
playlistId: string,
2828
): Promise<PlaylistItem> => {
2929
let data = {};
3030
const res = await youtube.playlistItems.list({

0 commit comments

Comments
 (0)