Skip to content

Commit

Permalink
Added player functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcelOlsen committed Nov 5, 2024
1 parent 12c656e commit 97c80ff
Show file tree
Hide file tree
Showing 15 changed files with 458 additions and 7 deletions.
12 changes: 11 additions & 1 deletion app/(site)/components/PageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import SongItem from "@/components/SongItem";
import useOnPlay from "@/hooks/useOnPlay";
import { Song } from "@/types";
import React from "react";

Expand All @@ -9,12 +10,21 @@ interface PageContentProps {
}

const PageContent: React.FC<PageContentProps> = ({ songs }) => {
const onPlay = useOnPlay(songs);

if (songs.length === 0)
return <div className="mt-4 text-neutral-400">No songs available</div>;

return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-8 gap-4 mt-4">
{songs.map((song) => {
return <SongItem key={song.id} onClick={() => {}} data={song} />;
return (
<SongItem
key={song.id}
onClick={(id: string) => onPlay(id)}
data={song}
/>
);
})}
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UserProvider from "@/providers/UserProvider";
import ModalProvider from "@/providers/ModalProvider";
import ToasterProvider from "@/providers/ToasterProvider";
import getSongsByUserId from "@/actions/getSongsByUserId";
import Player from "@/components/Player";

const font = Figtree({ subsets: ["latin"] });

Expand All @@ -31,6 +32,7 @@ export default async function RootLayout({
<UserProvider>
<ModalProvider />
<Sidebar songs={userSongs}>{children}</Sidebar>
<Player />
</UserProvider>
</SupabseProvider>
</body>
Expand Down
4 changes: 3 additions & 1 deletion app/liked/components/LikedContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import LikeButton from "@/components/LikeButton";
import MediaItem from "@/components/MediaItem";
import useOnPlay from "@/hooks/useOnPlay";
import { useUser } from "@/hooks/useUser";
import { Song } from "@/types";
import { useRouter } from "next/navigation";
Expand All @@ -14,6 +15,7 @@ interface LikedContentProps {
const LikedContent: React.FC<LikedContentProps> = ({ songs }) => {
const router = useRouter();
const { isLoading, user } = useUser();
const onPlay = useOnPlay(songs);

useEffect(() => {
if (!isLoading && !user) router.replace("/");
Expand All @@ -32,7 +34,7 @@ const LikedContent: React.FC<LikedContentProps> = ({ songs }) => {
return (
<div key={song.id} className="flex items-center gap-x-4 w-full">
<div className="flex-1">
<MediaItem onClick={() => {}} data={song} />
<MediaItem onClick={(id: string) => onPlay(id)} data={song} />
</div>
<LikeButton songId={song.id} />
</div>
Expand Down
4 changes: 3 additions & 1 deletion app/search/components/SearchContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import LikeButton from "@/components/LikeButton";
import MediaItem from "@/components/MediaItem";
import useOnPlay from "@/hooks/useOnPlay";
import { Song } from "@/types";
import React from "react";

Expand All @@ -10,6 +11,7 @@ interface SearchContentProps {
}

const SearchContent: React.FC<SearchContentProps> = ({ songs }) => {
const onPlay = useOnPlay(songs);
if (songs.length === 0)
return (
<div className="flex flex-col gap-y-2 w-full px-6 text-neutral-400">
Expand All @@ -22,7 +24,7 @@ const SearchContent: React.FC<SearchContentProps> = ({ songs }) => {
return (
<div key={song.id} className="flex items-center gap-x-4 w-full">
<div className="flex-1">
<MediaItem onClick={() => {}} data={song} />
<MediaItem onClick={(id: string) => onPlay(id)} data={song} />
</div>
<LikeButton songId={song.id} />
</div>
Expand Down
10 changes: 7 additions & 3 deletions components/Library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React from "react";
import { AiOutlinePlus } from "react-icons/ai";
import { TbPlaylist } from "react-icons/tb";
import MediaItem from "./MediaItem";
import useOnPlay from "@/hooks/useOnPlay";

interface LibraryProps {
songs: Song[];
Expand All @@ -17,6 +18,7 @@ const Library: React.FC<LibraryProps> = ({ songs }) => {
const authModal = useAuthModal();
const { user } = useUser();
const uploadModal = useUploadModal();
const onPlay = useOnPlay(songs);

const onClick = () => {
if (!user) return authModal.onOpen();
Expand All @@ -42,9 +44,11 @@ const Library: React.FC<LibraryProps> = ({ songs }) => {
<div className="flex flex-col gap-y-2 mt-4 px-3">
{songs.map((song) => {
return (
<MediaItem onClick={() => {}} key={song.id} data={song}>
{song.title}
</MediaItem>
<MediaItem
onClick={(id: string) => onPlay(id)}
key={song.id}
data={song}
/>
);
})}
</div>
Expand Down
26 changes: 26 additions & 0 deletions components/Player.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import useGetSongById from "@/hooks/useGetSongById";
import useLoadSongUrl from "@/hooks/useLoadSongUrl";
import usePlayer from "@/hooks/usePlayer";
import React from "react";
import PlayerContent from "./PlayerContent";

const Player = () => {
const player = usePlayer();
const { song } = useGetSongById(player.activeId);

const songUrl = useLoadSongUrl(song!);

if (!song || !songUrl || !player.activeId) {
return null;
}

return (
<div className="fixed bottom-0 bg-black w-full py-2 h-[80px] px-4">
<PlayerContent song={song} songUrl={songUrl} key={songUrl} />
</div>
);
};

export default Player;
130 changes: 130 additions & 0 deletions components/PlayerContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Song } from "@/types";
import React, { useEffect, useState } from "react";
import LikeButton from "./LikeButton";
import MediaItem from "./MediaItem";
import { BsPauseFill, BsPlayFill } from "react-icons/bs";
import { AiFillStepBackward, AiFillStepForward } from "react-icons/ai";
import { HiSpeakerWave, HiSpeakerXMark } from "react-icons/hi2";
import Slider from "./Slider";
import usePlayer from "@/hooks/usePlayer";
import next from "next";
import useSound from "use-sound";

interface PlayerContentProps {
song: Song;
songUrl: string;
}

const PlayerContent: React.FC<PlayerContentProps> = ({ song, songUrl }) => {
const player = usePlayer();
const [volume, setVolume] = useState(1);
const [isPlaying, setIsPlaying] = useState<boolean>(false);

const Icon = isPlaying ? BsPauseFill : BsPlayFill;
const VolumeIcon = volume === 0 ? HiSpeakerXMark : HiSpeakerWave;

const onPlayNext = () => {
if (player.ids.length === 0) {
return;
}

const currentIdx = player.ids.findIndex((id) => id === player.activeId);
const nextSong = player.ids[currentIdx + 1];

if (!nextSong) return player.setId(player.ids[0]);

player.setId(nextSong);
};

const onPlayPrevious = () => {
if (player.ids.length === 0) {
return;
}

const currentIdx = player.ids.findIndex((id) => id === player.activeId);
const previousSong = player.ids[currentIdx - 1];

if (!previousSong) return player.setId(player.ids[player.ids.length - 1]);

player.setId(previousSong);
};

const [play, { pause, sound }] = useSound(songUrl, {
volume: volume,
onplay: () => setIsPlaying(true),
onend: () => {
setIsPlaying(false);
onPlayNext();
},
onpause: () => setIsPlaying(false),
format: ["mp3"],
});

useEffect(() => {
sound?.play();
return () => {
sound?.unload();
};
}, [sound]);

const handlePlay = () => {
if (!isPlaying) play();
else pause();
};

const toggleMute = () => {
if (volume === 0) setVolume(1);
else setVolume(0);
};

return (
<div className="grid grid-cols-2 md:grid-cols-3 h-full">
<div className="flex w-full justify-start">
<div className="flex items-center gap-x-4">
<MediaItem data={song} />
<LikeButton songId={song.id} />
</div>
</div>
<div className="flex md:hidden col-auto w-full justify-end items-center">
<div
onClick={handlePlay}
className="h-10 w-10 flex items-center justify-center rounded-full bg-white p-1 cursor-pointer"
>
<Icon size={30} className="text-black" />
</div>
</div>

<div className="hidden h-full md:flex justify-center items-center w-full max-w-[722px] gap-x-6">
<AiFillStepBackward
onClick={onPlayPrevious}
size={30}
className="text-neutral-400 cursor-pointer hover:text-white transition"
/>
<div
onClick={handlePlay}
className="flex items-center justify-center h-10 w-10 rounded-full bg-white p-1 cursor-pointer"
>
<Icon size={30} className="text-black" />
</div>
<AiFillStepForward
onClick={onPlayNext}
size={30}
className="text-neutral-400 cursor-pointer hover:text-white transition"
/>

<div className="hidden md:flex w-full justify-end pr-2">
<div className="flex items-center gap-x-2 w-[120px]">
<VolumeIcon
onClick={toggleMute}
className="cursor-pointer"
size={30}
/>
<Slider value={volume} onChange={(val) => setVolume(val)} />
</div>
</div>
</div>
</div>
);
};

export default PlayerContent;
11 changes: 10 additions & 1 deletion components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Box } from "./Box";
import { SidebarItem } from "./SidebarItem";
import Library from "./Library";
import { Song } from "@/types";
import usePlayer from "@/hooks/usePlayer";
import { twMerge } from "tailwind-merge";

type SidebarProps = {
children: React.ReactNode;
Expand All @@ -16,6 +18,8 @@ type SidebarProps = {

const Sidebar: React.FC<SidebarProps> = ({ children, songs }) => {
const pathname = usePathname();
const player = usePlayer();

const routes = useMemo(() => {
return [
{
Expand All @@ -34,7 +38,12 @@ const Sidebar: React.FC<SidebarProps> = ({ children, songs }) => {
}, [pathname]);

return (
<div className="flex h-full">
<div
className={twMerge(
"flex h-full",
player.activeId && "h-[calc(100%-80px)]"
)}
>
<div className="hidden md:flex flex-col gap-y-2 bg-black h-full w-[300px] p-2">
<Box>
<div className="flex flex-col gap-y-4 px-5 py-4">
Expand Down
33 changes: 33 additions & 0 deletions components/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import React from "react";

import * as RadixSlider from "@radix-ui/react-slider";

interface SliderProps {
value?: number;
onChange?: (value: number) => void;
}

const Slider: React.FC<SliderProps> = ({ value = 1, onChange }) => {
const handleChange = (newValue: number[]) => {
onChange?.(newValue[0]);
};
return (
<RadixSlider.Root
className="relative flex items-center select-none touchnone w-full h-10"
defaultValue={[1]}
value={[value]}
onValueChange={handleChange}
max={1}
step={0.1}
aria-label="Volume"
>
<RadixSlider.Track className="bg-neutral-600 relative grow rounded-full h-[3px]">
<RadixSlider.Range className="absolute bg-white rounded-full h-full" />
</RadixSlider.Track>
</RadixSlider.Root>
);
};

export default Slider;
36 changes: 36 additions & 0 deletions hooks/useGetSongById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Song } from "@/types";
import { useSessionContext } from "@supabase/auth-helpers-react";
import { useEffect, useMemo, useState } from "react"
import toast from "react-hot-toast";

const useGetSongById = (id?: string) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [song, setSong] = useState<Song | undefined>(undefined);
const { supabaseClient } = useSessionContext();

useEffect(() => {
if (!id) return
setIsLoading(true);

const fetchSong = async () => {
const { data, error } = await supabaseClient.from('songs').select('*').eq('id', id).single();

if (error) {
setIsLoading(false)
return toast.error(error.message)
}

setSong(data as Song)
setIsLoading(false)
}

fetchSong();
}, [id, supabaseClient])

return useMemo(() => ({
isLoading,
song
}), [isLoading, song])
}

export default useGetSongById
Loading

0 comments on commit 97c80ff

Please sign in to comment.