Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Basic Progress Bar with Functional Slider, Revamp Podcast Player, and Implement Accurate View Tracking with Internal Timer #7

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading