Skip to content

Commit cd701f7

Browse files
author
Alex Patterson
committed
add podcast player
1 parent 0fd4184 commit cd701f7

8 files changed

Lines changed: 372 additions & 227 deletions

File tree

‎app/(main)/(course)/course/[courseSlug]/lesson/[lessonSlug]/lesson-panel.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function LessonPanel({
3232
course: NonNullable<LessonsInCourseQueryResult>;
3333
}) {
3434
const [defaultLayout, saveDefaultLayout] = useLocalStorage(
35-
"react-resizable-panels:layout",
35+
"codingcatdev:lesson:layout",
3636
[25, 75]
3737
);
3838

‎app/(main)/(podcast)/podcast/[slug]/page.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default async function PodcastPage({ params }: Props) {
6060
return notFound();
6161
}
6262

63-
const mp3 = podcast?.spotify?.enclosures?.at(0)?.url;
63+
const src = podcast?.spotify?.enclosures?.at(0)?.url;
6464

6565
return (
6666
<div className="container px-5 mx-auto">
@@ -99,8 +99,8 @@ export default async function PodcastPage({ params }: Props) {
9999
<div className="text-lg">
100100
<DateComponent dateString={podcast.date} />
101101
</div>
102-
{mp3 && (
103-
<PlayerPlayButton src={mp3} />
102+
{src && (
103+
<PlayerPlayButton podcast={podcast} />
104104
)}
105105
</div>
106106
</div>
@@ -138,8 +138,8 @@ export default async function PodcastPage({ params }: Props) {
138138
<div className="mb-4 text-lg">
139139
<DateComponent dateString={podcast.date} />
140140
</div>
141-
{mp3 && (
142-
<PlayerPlayButton src={mp3} />
141+
{src && (
142+
<PlayerPlayButton podcast={podcast} />
143143
)}
144144
</div>
145145
</div>

‎app/(main)/layout.tsx‎

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import AvatarDropdown from "./avatar-dropdown";
2626
import AlgoliaDialog from "@/components/algolia-dialog";
2727
import { FaBars } from "react-icons/fa6";
2828
import PlayerFloating from "@/components/player-floating";
29+
import { PlayerProvider } from "@/components/player-context";
2930

3031
const nunito = Nunito({
3132
subsets: ["latin"],
@@ -89,54 +90,54 @@ export default async function RootLayout({
8990
inter.variable
9091
)}
9192
>
92-
<ThemeProvider
93-
attribute="class"
94-
defaultTheme="system"
95-
enableSystem
96-
disableTransitionOnChange
97-
>
98-
{draftMode().isEnabled && <AlertBanner />}
99-
<NextTopLoader color="hsl(var(--primary))" />
100-
<section className="flex flex-col min-h-[100dvh]">
101-
<header className="fixed left-0 right-0 z-40 flex items-center justify-between px-6 py-4 shadow-md bg-background">
102-
<div className="flex items-center gap-4">
103-
<Link className="text-lg font-bold md:text-2xl" href="/">
104-
CodingCat.dev
105-
</Link>
106-
<nav className="items-center hidden md:flex gap-6">
107-
<NavHeader navLinks={data?.navLinks} />
108-
</nav>
109-
</div>
110-
<div className="flex items-center gap-2">
111-
<AlgoliaDialog />
112-
<AvatarDropdown />
113-
<Sheet>
114-
<SheetTrigger asChild>
115-
<Button className="md:hidden" size="icon" variant="ghost">
116-
<FaBars className="w-6 h-6" />
117-
<span className="sr-only">Toggle navigation menu</span>
118-
</Button>
119-
</SheetTrigger>
120-
<SheetContent side="right">
121-
<nav className="py-6 grid gap-2">
122-
<SheetTrigger asChild>
123-
<NavHeader navLinks={data?.navLinks} sideOnly />
124-
</SheetTrigger>
125-
</nav>
126-
</SheetContent>
127-
</Sheet>
128-
</div>
129-
</header>
130-
<main className="mt-20">{children}</main>
131-
<Toaster />
132-
<Suspense>
93+
<PlayerProvider>
94+
<ThemeProvider
95+
attribute="class"
96+
defaultTheme="system"
97+
enableSystem
98+
disableTransitionOnChange
99+
>
100+
{draftMode().isEnabled && <AlertBanner />}
101+
<NextTopLoader color="hsl(var(--primary))" />
102+
<section className="flex flex-col min-h-[100dvh]">
103+
<header className="fixed left-0 right-0 z-40 flex items-center justify-between px-6 py-4 shadow-md bg-background">
104+
<div className="flex items-center gap-4">
105+
<Link className="text-lg font-bold md:text-2xl" href="/">
106+
CodingCat.dev
107+
</Link>
108+
<nav className="items-center hidden md:flex gap-6">
109+
<NavHeader navLinks={data?.navLinks} />
110+
</nav>
111+
</div>
112+
<div className="flex items-center gap-2">
113+
<AlgoliaDialog />
114+
<AvatarDropdown />
115+
<Sheet>
116+
<SheetTrigger asChild>
117+
<Button className="md:hidden" size="icon" variant="ghost">
118+
<FaBars className="w-6 h-6" />
119+
<span className="sr-only">Toggle navigation menu</span>
120+
</Button>
121+
</SheetTrigger>
122+
<SheetContent side="right">
123+
<nav className="py-6 grid gap-2">
124+
<SheetTrigger asChild>
125+
<NavHeader navLinks={data?.navLinks} sideOnly />
126+
</SheetTrigger>
127+
</nav>
128+
</SheetContent>
129+
</Sheet>
130+
</div>
131+
</header>
132+
<main className="mt-20">{children}</main>
133+
<Toaster />
133134
<Footer />
134-
</Suspense>
135-
</section>
136-
<PlayerFloating />
137-
{draftMode().isEnabled && <VisualEditing />}
138-
<SpeedInsights />
139-
</ThemeProvider>
135+
</section>
136+
<PlayerFloating />
137+
{draftMode().isEnabled && <VisualEditing />}
138+
<SpeedInsights />
139+
</ThemeProvider>
140+
</PlayerProvider>
140141
</body>
141142
</html>
142143
);

‎components/player-context.tsx‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"use client"
2+
import { PodcastQueryResult } from "@/sanity.types";
3+
import { useLocalStorage } from "@uidotdev/usehooks";
4+
import { Dispatch, MutableRefObject, SetStateAction, createContext, useEffect, useRef, useState } from "react";
5+
6+
export const PlayerContext = createContext<{
7+
podcast: PodcastQueryResult,
8+
setPodcast: Dispatch<SetStateAction<PodcastQueryResult>>,
9+
isOpen: boolean, setIsOpen: Dispatch<SetStateAction<boolean>>,
10+
isMinimized: boolean, setIsMinimized: Dispatch<SetStateAction<boolean>>,
11+
audio: {
12+
id: number,
13+
src: string,
14+
title: string,
15+
artist: string,
16+
isPlaying: boolean,
17+
curTime: number,
18+
duration: number,
19+
playbackRate: number,
20+
}, setAudio: Dispatch<SetStateAction<{
21+
id: number;
22+
src: string;
23+
title: string;
24+
artist: string;
25+
isPlaying: boolean;
26+
curTime: number;
27+
duration: number;
28+
playbackRate: number;
29+
}>>,
30+
audioRef: MutableRefObject<HTMLAudioElement | undefined> | undefined,
31+
volume: number, setVolume: Dispatch<SetStateAction<number>>
32+
}
33+
>({
34+
podcast: null, setPodcast: () => null,
35+
isOpen: false, setIsOpen: () => null, isMinimized: false,
36+
setIsMinimized: () => null, audio: {
37+
id: 1,
38+
src: "",
39+
title: "",
40+
artist: "",
41+
isPlaying: false,
42+
curTime: 0,
43+
duration: 100,
44+
playbackRate: 1.0,
45+
}, setAudio: () => null, audioRef: undefined, volume: 100, setVolume: () => null
46+
}
47+
);
48+
49+
export const PlayerProvider = ({ children }: { children: JSX.Element }) => {
50+
const [podcast, setPodcast] = useState<PodcastQueryResult>(null)
51+
const [isOpen, setIsOpen] = useState(true);
52+
const [isMinimized, setIsMinimized] = useState(false);
53+
//TODO: make this recent
54+
const [audio, setAudio] = useState({
55+
id: 1,
56+
src: "",
57+
title: "",
58+
artist: "",
59+
isPlaying: false,
60+
curTime: 0,
61+
duration: 100,
62+
playbackRate: 1.0,
63+
});
64+
const audioRef = useRef<HTMLAudioElement>();
65+
const [volume, setVolume] = useState(100);
66+
const [defaultVolume, saveDefaultVolume] = useLocalStorage(
67+
"codingcatdev:player:volume",
68+
100
69+
);
70+
71+
useEffect(() => {
72+
const src = podcast?.spotify?.enclosures?.at(0)?.url;
73+
if (!src) return;
74+
if (src === audio.src) return;
75+
setAudio((p) => { return { ...p, src } });
76+
setVolume(defaultVolume);
77+
setIsOpen(true);
78+
}, [podcast]);
79+
80+
useEffect(() => {
81+
if (!audioRef.current) return;
82+
audioRef.current.volume = volume / 100;
83+
}, [volume]);
84+
85+
useEffect(() => {
86+
if (audioRef?.current && podcast) {
87+
setIsOpen(true);
88+
}
89+
}, [audioRef?.current, podcast]);
90+
91+
useEffect(() => {
92+
if (audioRef.current) audioRef.current.playbackRate = audio.playbackRate;
93+
}, [audio.playbackRate]);
94+
95+
useEffect(() => {
96+
if (!audio?.src || (audioRef.current?.src === audio.src)) return;
97+
98+
audioRef.current = new Audio(audio.src);
99+
100+
// play and pause
101+
audioRef.current.addEventListener("play", () => {
102+
setAudio((p) => { return { ...p, isPlaying: true } });
103+
});
104+
audioRef.current.addEventListener("pause", () => {
105+
setAudio((p) => { return { ...p, isPlaying: false } });
106+
});
107+
108+
//lets trigger when audio is ready
109+
audioRef.current.addEventListener("canplay", () => {
110+
audioRef.current?.play();
111+
});
112+
113+
// time and duration
114+
audioRef.current.addEventListener("loadedmetadata", (e: any) => {
115+
setAudio((p) => {
116+
if (!audioRef?.current) return p;
117+
return {
118+
...p, curTime: audioRef.current.currentTime,
119+
duration: audioRef.current.duration,
120+
}
121+
});
122+
123+
});
124+
audioRef.current.addEventListener("timeupdate", (e: any) => {
125+
setAudio((p) => {
126+
if (!e.target) return p;
127+
return {
128+
...p, curTime: e.target.currentTime,
129+
}
130+
});
131+
});
132+
133+
return () => {
134+
audioRef.current?.pause();
135+
};
136+
}, [audio.src]);
137+
138+
return (
139+
<PlayerContext.Provider value={{
140+
podcast, setPodcast,
141+
isOpen, setIsOpen,
142+
isMinimized, setIsMinimized,
143+
audio, setAudio,
144+
audioRef,
145+
volume, setVolume
146+
}}>
147+
{children}
148+
</PlayerContext.Provider>
149+
)
150+
}

0 commit comments

Comments
 (0)