Skip to content

Commit

Permalink
Replace the basic progress bar with a functional slider and revamp th…
Browse files Browse the repository at this point in the history
…e podcast player
  • Loading branch information
deepsingh132 committed Nov 12, 2024
1 parent 3206325 commit b22faed
Show file tree
Hide file tree
Showing 12 changed files with 486 additions and 109 deletions.
2 changes: 1 addition & 1 deletion components/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const LeftSidebar = () => {

return (
<section className={cn("left_sidebar h-[calc(100vh-1px)]", {
'h-[calc(100vh-140px)]': audio?.audioUrl
'h-[calc(100vh-113px)]': audio?.audioUrl
})}>
<nav className="flex flex-col gap-6">
<Link href="/" className="flex cursor-pointer items-center gap-1 pb-10 max-lg:justify-center">
Expand Down
12 changes: 2 additions & 10 deletions components/PodcastDetailPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { PodcastDetailPlayerProps } from "@/types";
import LoaderSpinner from "./LoaderSpinner";
import { Button } from "./ui/button";
import { useToast } from "./ui/use-toast";
import { useClerk } from "@clerk/nextjs";

const PodcastDetailPlayer = ({
audioUrl,
Expand All @@ -26,12 +25,10 @@ const PodcastDetailPlayer = ({
authorId,
}: PodcastDetailPlayerProps) => {
const router = useRouter();
const { audio, setAudio } = useAudio();
const { setAudio } = useAudio();
const { toast } = useToast();
const { user } = useClerk();
const [isDeleting, setIsDeleting] = useState(false);
const deletePodcast = useMutation(api.podcasts.deletePodcast);
const incrementViews = useMutation(api.podcasts.incrementPodcastViews);

const handleDelete = async () => {
try {
Expand All @@ -55,14 +52,9 @@ const PodcastDetailPlayer = ({
audioUrl,
imageUrl,
author,
authorId,
podcastId,
});
// increment views if user is not the author and the current audio is not the same as the audio being played
if (!isOwner && audio?.audioUrl !== audioUrl) {
setTimeout(() => {
incrementViews({ podcastId: podcastId });
}, 10000);
};
};

if (!imageUrl || !authorImageUrl) return <LoaderSpinner />;
Expand Down
169 changes: 130 additions & 39 deletions components/PodcastPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import Image from "next/image";
import Link from "next/link";
Expand All @@ -6,16 +7,30 @@ import { useEffect, useRef, useState } from "react";
import { formatTime } from "@/lib/formatTime";
import { cn } from "@/lib/utils";
import { useAudio } from "@/providers/AudioProvider";
import useIncrementPodcastViews from "@/hooks/useIncrementPodcastViews";
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { Id } from "@/convex/_generated/dataModel";
import { useClerk } from "@clerk/nextjs";
import { Slider } from "./ui/slider";

import { Progress } from "./ui/progress";
const PLAY_TIME_REQUIRED_FOR_VIEW_IN_SECONDS = 10;

const PodcastPlayer = () => {
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [isMuted, setIsMuted] = useState(false);
const [volume, setVolume] = useState(100);
const [currentTime, setCurrentTime] = useState(0);
const { audio } = useAudio();
const { audio, setAudio } = useAudio();
const { isViewValid, setResetTimer } = useIncrementPodcastViews({
targetTime: PLAY_TIME_REQUIRED_FOR_VIEW_IN_SECONDS,
isPlaying: isPlaying,
duration: audioRef?.current?.duration ?? 0,
});
const incrementViews = useMutation(api.podcasts.incrementPodcastViews);
const { user } = useClerk();

const togglePlayPause = () => {
if (audioRef.current?.paused) {
Expand Down Expand Up @@ -80,7 +95,7 @@ const PodcastPlayer = () => {
}
} else {
audioElement?.pause();
setIsPlaying(true);
setIsPlaying(false);
}
}, [audio]);
const handleLoadedMetadata = () => {
Expand All @@ -93,26 +108,64 @@ const PodcastPlayer = () => {
setIsPlaying(false);
};

const handlePlayerClose = () => {
setAudio(undefined);
setIsPlaying(false);
setResetTimer();
};

const handleVolumeChange = (value: number) => {
if (!audioRef.current) return;
audioRef.current.volume = value / 100;
setVolume(value);
};

const handleProgressChange = (value: number) => {
if (!audioRef.current) return;
audioRef.current.currentTime = value
setCurrentTime(value);
}

// mute the audio when the volume is 0
useEffect(() => {
if (!audioRef.current) return;
setIsMuted(volume === 0);
}, [volume]);

// set the volume to 0 when the audio is muted (Vice versa as above)
useEffect(() => {
if (!audioRef.current) return;
setVolume(isMuted ? 0 : 100);
}, [isMuted]);

// increment views if user is not the author of the podcast
useEffect(() => {
if (!audio || !user || audio.authorId === user.id) return;
if (audio.podcastId && isViewValid) {
incrementViews({ podcastId: audio.podcastId as Id<"podcasts"> });
}
}, [isViewValid]);

// reset the timer when the audio changes
useEffect(() => {
setResetTimer();
}, [audio]);

return (
<div
className={cn("sticky bottom-0 left-0 flex size-full flex-col", {
hidden: !audio?.audioUrl || audio?.audioUrl === "",
})}
>
{/* change the color for indicator inside the Progress component in ui folder */}
<Progress
value={(currentTime / duration) * 100}
className="w-full"
max={duration}
/>
<section className="glassmorphism-black flex h-[112px] w-full items-center justify-between px-4 max-md:justify-center max-md:gap-5 md:px-12">
<section className="glassmorphism-black relative flex h-[112px] w-full items-center justify-between px-4 max-md:justify-center md:gap-5 md:px-12">
<audio
ref={audioRef}
src={audio?.audioUrl}
className="hidden"
onLoadedMetadata={handleLoadedMetadata}
onEnded={handleAudioEnded}
/>

<div className="flex items-center gap-4 max-md:hidden">
<Link href={`/podcast/${audio?.podcastId}`}>
<Image
Expand All @@ -130,50 +183,88 @@ const PodcastPlayer = () => {
<p className="text-12 font-normal text-white-2">{audio?.author}</p>
</div>
</div>
<div className="flex-center cursor-pointer gap-3 md:gap-6">
<div className="flex items-center gap-1.5">
<div className="flex-center w-full max-w-[600px] flex-col gap-3">
<div className="flex items-center cursor-pointer gap-3 md:gap-6">
<div className="flex items-center gap-1.5">
<Image
src={"/icons/reverse.svg"}
width={24}
height={24}
alt="rewind"
onClick={rewind}
aria-label="Rewind"
/>
<h2 className="text-12 font-bold text-white-4">-5</h2>
</div>
<Image
src={"/icons/reverse.svg"}
width={24}
height={24}
alt="rewind"
onClick={rewind}
src={isPlaying ? "/icons/Pause.svg" : "/icons/Play.svg"}
width={30}
height={30}
alt="play"
onClick={togglePlayPause}
aria-label={isPlaying ? "Pause" : "Play"}
/>
<h2 className="text-12 font-bold text-white-4">-5</h2>
<div className="flex items-center gap-1.5">
<h2 className="text-12 font-bold text-white-4">+5</h2>
<Image
src={"/icons/forward.svg"}
width={24}
height={24}
alt="forward"
onClick={forward}
aria-label="Forward"
/>
</div>
</div>
<Image
src={isPlaying ? "/icons/Pause.svg" : "/icons/Play.svg"}
width={30}
height={30}
alt="play"
onClick={togglePlayPause}
/>
<div className="flex items-center gap-1.5">
<h2 className="text-12 font-bold text-white-4">+5</h2>
<Image
src={"/icons/forward.svg"}
width={24}
height={24}
alt="forward"
onClick={forward}
/>
<div className="flex w-full justify-between items-center gap-2">
<div className="min-w-[40px] text-right text-sm font-normal text-white-2 max-md:hidden">
{formatTime(currentTime)}
</div>
<div className="flex w-full h-4 items-center">
<Slider
min={0}
max={duration}
onValueChange={(value) => handleProgressChange(value[0])}
value={[currentTime]}
aria-label="Progress"
/>
</div>

<div className="min-w-[40px] text-left text-sm font-normal text-white-2 max-md:hidden">
{formatTime(duration)}
</div>
</div>
</div>
<div className="flex items-center gap-6">
<h2 className="text-16 font-normal text-white-2 max-md:hidden">
{formatTime(duration)}
</h2>
<div className="flex w-full gap-2">
<div className="flex w-full items-center justify-center h-full gap-2">
<Image
src={isMuted ? "/icons/unmute.svg" : "/icons/mute.svg"}
width={24}
height={24}
alt="mute unmute"
onClick={toggleMute}
className="cursor-pointer"
className="md:block hidden cursor-pointer"
aria-label="mute unmute"
/>
<Slider
min={0}
max={100}
onValueChange={(value) => handleVolumeChange(value[0])}
value={[volume]}
className="w-full md:min-w-[93px] hidden md:flex h-4"
aria-label="Volume"
/>
</div>
</div>
<Image
src="/icons/close-circle.svg"
width={24}
height={24}
className="absolute top-2 right-4 cursor-pointer"
onClick={() => handlePlayerClose()}
alt="close"
aria-label="close"
/>
</section>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions components/ProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const ProfileCard = ({
audioUrl: randomPodcast.audioUrl || "",
imageUrl: randomPodcast.imageUrl || "",
author: randomPodcast.author,
authorId: randomPodcast.authorId,
podcastId: randomPodcast._id,
});
}
Expand Down
13 changes: 8 additions & 5 deletions components/RightSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Carousel from './Carousel';
import { useQuery } from 'convex/react';
import { api } from '@/convex/_generated/api';
import { useRouter } from 'next/navigation';
import LoaderSpinner from './LoaderSpinner';
import { useAudio } from '@/providers/AudioProvider';
import { cn } from '@/lib/utils';

Expand All @@ -22,8 +21,8 @@ const RightSidebar = () => {

return (
<section className={cn('right_sidebar h-[calc(100vh-5px)]', {
'h-[calc(100vh-140px)]': audio?.audioUrl
})}>
'h-[calc(100vh-113px)]': audio?.audioUrl
})}>
<SignedIn>
<Link href={`/profile/${user?.id}`} className="flex gap-3 pb-12">
<UserButton />
Expand All @@ -45,8 +44,12 @@ const RightSidebar = () => {
<section className="flex flex-col gap-8 pt-12">
<Header headerTitle="Top Creators" />
<div className="flex flex-col gap-6">
{topPodcasters?.slice(0, 3).map((podcaster) => (
<div key={podcaster._id} className="flex cursor-pointer justify-between" onClick={() => router.push(`/profile/${podcaster.clerkId}`)}>
{topPodcasters?.slice(0, audio?.audioUrl ? 2 : 3).map((podcaster) => (
<div
key={podcaster._id}
className="flex cursor-pointer justify-between"
onClick={() => router.push(`/profile/${podcaster.clerkId}`)}
>
<figure className="flex items-center gap-2">
<Image
src={podcaster.imageUrl}
Expand Down
28 changes: 0 additions & 28 deletions components/ui/progress.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions components/ui/slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

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

import { cn } from "@/lib/utils";

const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative group flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative cursor-pointer h-4 w-full grow overflow-hidden rounded-full bg-black-5">
<SliderPrimitive.Range className="SliderRange absolute h-full bg-white-1 group-hover:bg-[--accent-color]" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="SliderThumb block invisible group-hover:visible cursor-grabbing h-5 w-5 group-hover:bg-white-1 !ring-0 outline-none focus:outline-none focus:ring-0 rounded-full bg-white-1 transition-colors" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

export { Slider };
Loading

0 comments on commit b22faed

Please sign in to comment.