From d2a5abc8267d04e9c6eea5a88552eb57e968a652 Mon Sep 17 00:00:00 2001 From: Rava Maulana <ravamaulana14@gmail.com> Date: Wed, 15 Nov 2023 17:12:24 +0700 Subject: [PATCH] feat: connected podcast and episode pages --- src/components/EpisodeHeader.tsx | 194 +++++++++++++++++++---------- src/components/EpisodeList.tsx | 196 ++++++++++++++++++++++-------- src/components/PodcastCard.tsx | 31 ++++- src/components/PodcastHeader.tsx | 45 +++++++ src/components/layouts/Player.tsx | 37 ++++-- src/lib/handleAddToQueue.ts | 33 +++++ src/pages/episode/index.tsx | 51 ++++---- src/pages/podcast/index.tsx | 8 +- 8 files changed, 438 insertions(+), 157 deletions(-) create mode 100644 src/lib/handleAddToQueue.ts diff --git a/src/components/EpisodeHeader.tsx b/src/components/EpisodeHeader.tsx index 3c1a7bd..7ac93ea 100644 --- a/src/components/EpisodeHeader.tsx +++ b/src/components/EpisodeHeader.tsx @@ -1,74 +1,146 @@ +import axios from "axios"; import Placeholder from "../assets/placeholder_image.jpg"; import PlayIcon from "../assets/play-icon.svg"; import PlusIcon from "../assets/plus-icon.svg"; - +import { Queue, useQueue, useQueueDispatch } from "../contexts/QueueContext"; export type headerProps = { - title: string, - description: string, - imageurl: string - } - - export default function EpisodeHeader({title, description, imageurl}: headerProps): JSX.Element { - const urlPrefix = "http://localhost:3000/images/" - return ( - <div className="block"> - <div className="w-[950px] inline-flex mt-[20px]"> - - <div className=""> - <div className="w-[225px] h-[225px]"> - <img className="rounded-[20px] w-[225px] h-[225px] object-cover object-center" - width={225} - height={225} - src={ urlPrefix + imageurl || Placeholder} - alt="podcast thumbnail" /> - </div> - - </div> - - <div className="ml-[30px]"> - <div className="block"> - - </div> - <div className="block"> - <h1 className="h1 my-[10px] leading-tight">{title}</h1> - </div> - - <div className="block"> - <p className="b3 text-gray-600 ">{description}</p> - </div> - </div> + title: string; + description: string; + url_thumbnail: string; + id_episode: number; +}; + +export default function EpisodeHeader({ + title, + description, + url_thumbnail, + id_episode, +}: headerProps): JSX.Element { + const urlPrefix = "http://localhost:3000/images/"; + + const queue = useQueue(); + const dispatchQueue = useQueueDispatch(); + + const handlePlay = async () => { + try { + const axiosInstance = axios.create({ + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + await axiosInstance.delete(`${import.meta.env.VITE_REST_URL}/queue`); + + await axiosInstance.post( + `${import.meta.env.VITE_REST_URL}/queue/episode`, + { + idEpisode: id_episode, + } + ); + + if (queue.current) { + await axiosInstance.post( + `${import.meta.env.VITE_REST_URL}/queue/forward` + ); + } + + const [current, next, prev] = await Promise.all([ + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/current`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/next`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/previous`), + ]); + + const tempQueue: Queue = { + prev: prev.data.result, + current: current.data.result, + next: next.data.result, + }; + + dispatchQueue({ type: "SET_QUEUE", payload: tempQueue }); + } catch (err) { + console.log(err); + } + }; + const handleAddToQueue = async () => { + try { + await axios.post( + `${import.meta.env.VITE_REST_URL}/queue/episode`, + { + idEpisode: id_episode, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + } catch (err) { + console.log(err); + } + }; + + return ( + <div className="block"> + <div className="w-[950px] inline-flex mt-[20px]"> + <div className=""> + <div className="w-[225px] h-[225px]"> + <img + className="rounded-[20px] w-[225px] h-[225px] object-cover object-center" + width={225} + height={225} + src={urlPrefix + url_thumbnail || Placeholder} + alt="podcast thumbnail" + /> + </div> </div> - - <div className="block mt-[20px]"> - <button - data-te-toggle="tooltip" - title="play episode" - className=" w-[225px] h-[50px] bg-NAVY-5 text-white rounded-[32px] h4 leading-4"> - Play Episode - <img + + <div className="ml-[30px]"> + <div className="block"></div> + <div className="block"> + <h1 className="h1 my-[10px] leading-tight">{title}</h1> + </div> + + <div className="block"> + <p className="b3 text-gray-600 ">{description}</p> + </div> + </div> + </div> + + <div className="block mt-[20px]"> + <button + onClick={handlePlay} + data-te-toggle="tooltip" + title="play episode" + className=" w-[225px] h-[50px] bg-NAVY-5 text-white rounded-[32px] h4 leading-4" + > + Play Episode + <img className="inline ml-[45px]" width={16} height={16} - src={PlayIcon} - alt="" /> - </button> - - <button - data-te-toggle="tooltip" - title="add episode to queue" - className=" w-[225px] h-[50px] bg-NAVY-5 text-white rounded-[32px] h4 leading-4 ml-[30px]"> - Add To Queue - <img + src={PlayIcon} + alt="" + /> + </button> + + <button + onClick={handleAddToQueue} + data-te-toggle="tooltip" + title="add episode to queue" + className=" w-[225px] h-[50px] bg-NAVY-5 text-white rounded-[32px] h4 leading-4 ml-[30px]" + > + Add To Queue + <img className="inline ml-[45px]" width={16} height={16} - src={PlusIcon} - alt="" /> - </button> - - </div> - </div> - ); - } \ No newline at end of file + src={PlusIcon} + alt="" + /> + </button> + </div> + </div> + ); +} diff --git a/src/components/EpisodeList.tsx b/src/components/EpisodeList.tsx index d011239..500091a 100644 --- a/src/components/EpisodeList.tsx +++ b/src/components/EpisodeList.tsx @@ -1,58 +1,148 @@ +import axios from "axios"; import Placeholder from "../assets/placeholder_image.jpg"; import PlayIcon from "../assets/play-icon.svg"; import PlusIcon from "../assets/plus-icon.svg"; - +import { Queue, useQueue, useQueueDispatch } from "../contexts/QueueContext"; +import { useNavigate } from "react-router-dom"; export type episodeProps = { - order: number, - title: string, - description: string, - url_thumbnail: string - } - - export default function EpisodeList({order, title, description, url_thumbnail}: episodeProps): JSX.Element { - const urlPrefix = "http://localhost:3000/images/" - return ( - <div className="group/item flex items-center w-[1100px] h-[110px] rounded-xl bg-white hover:bg-NAVY-5 text-black hover:text-white"> - - <h1 className="h2 ml-[30px] mr-[20px]">{order}</h1> - - <img - className="rounded-lg w-[75px] h-[75px] object-cover object-center" - width={75} - height={75} - src={ urlPrefix + url_thumbnail || Placeholder} - alt="episode thumbnail" /> - - <div className="w-[650px] h-[59px] ml-5"> - <h2 className="h3 text-ellipsis whitespace-nowrap overflow-hidden">{title}</h2> - <p className="b4 text-ellipsis whitespace-nowrap overflow-hidden">{description}</p> - </div> - - <button - data-te-toggle="tooltip" - title="play episode" - className="invisible group-hover/item:visible flex items-center justify-center rounded-full bg-black w-[48px] h-[48px] ml-[70px] hover:bg-gray-600"> - <img - className="ml-[5px]" - width={18} - height={18} - src={PlayIcon} - alt="play episode"/> - </button> - - <button - data-te-toggle="tooltip" - title="add to queue" - className="invisible group-hover/item:visible flex items-center justify-center rounded-full bg-black w-[48px] h-[48px] ml-[30px] hover:bg-gray-600"> - <img - className="" - width={18} - height={18} - src={PlusIcon} - alt="play episode"/> - </button> - - </div> - ); - } \ No newline at end of file + order: number; + id_episode: number; + title: string; + description: string; + url_thumbnail: string; +}; + +export default function EpisodeList({ + order, + title, + description, + url_thumbnail, + id_episode, +}: episodeProps): JSX.Element { + const urlPrefix = "http://localhost:3000/images/"; + + const dispatchQueue = useQueueDispatch(); + const queue = useQueue(); + const navigate = useNavigate(); + + const handleNavigate = () => { + navigate(`/episode/${id_episode}`); + }; + + const handlePlay = async (e: React.MouseEvent<HTMLElement>) => { + e.stopPropagation(); + + try { + const axiosInstance = axios.create({ + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + await axiosInstance.delete(`${import.meta.env.VITE_REST_URL}/queue`); + + await axiosInstance.post( + `${import.meta.env.VITE_REST_URL}/queue/episode`, + { + idEpisode: id_episode, + } + ); + + if (queue.current) { + await axiosInstance.post( + `${import.meta.env.VITE_REST_URL}/queue/forward` + ); + } + + const [current, next, prev] = await Promise.all([ + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/current`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/next`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/previous`), + ]); + + const tempQueue: Queue = { + prev: prev.data.result, + current: current.data.result, + next: next.data.result, + }; + + dispatchQueue({ type: "SET_QUEUE", payload: tempQueue }); + } catch (err) { + console.log(err); + } + }; + + const handleAddToQueue = async (e: React.MouseEvent<HTMLElement>) => { + e.stopPropagation(); + + try { + await axios.post( + `${import.meta.env.VITE_REST_URL}/queue/episode`, + { + idEpisode: id_episode, + }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + } catch (err) { + console.log(err); + } + }; + + return ( + <div + onClick={handleNavigate} + className="cursor-pointer group/item flex items-center w-[1100px] h-[110px] rounded-xl bg-white hover:bg-NAVY-5 text-black hover:text-white" + > + <h1 className="h2 ml-[30px] mr-[20px]">{order}</h1> + <img + className="rounded-lg w-[75px] h-[75px] object-cover object-center" + width={75} + height={75} + src={urlPrefix + url_thumbnail || Placeholder} + alt="episode thumbnail" + /> + + <div className="w-[650px] h-[59px] ml-5"> + <h2 className="h3 text-ellipsis whitespace-nowrap overflow-hidden"> + {title} + </h2> + <p className="b4 text-ellipsis whitespace-nowrap overflow-hidden"> + {description} + </p> + </div> + <button + onClick={handlePlay} + data-te-toggle="tooltip" + title="play episode" + className="invisible hover:scale-110 group-hover/item:visible flex items-center justify-center rounded-full bg-black w-[48px] h-[48px] ml-[70px] hover:bg-gray-600" + > + <img + className="ml-[5px]" + width={18} + height={18} + src={PlayIcon} + alt="play episode" + /> + </button> + <button + onClick={handleAddToQueue} + data-te-toggle="tooltip" + title="add to queue" + className="invisible hover:scale-110 group-hover/item:visible flex items-center justify-center rounded-full bg-black w-[48px] h-[48px] ml-[30px] hover:bg-gray-600" + > + <img + className="" + width={18} + height={18} + src={PlusIcon} + alt="play episode" + /> + </button> + </div> + ); +} diff --git a/src/components/PodcastCard.tsx b/src/components/PodcastCard.tsx index f3b5fd8..c95a038 100644 --- a/src/components/PodcastCard.tsx +++ b/src/components/PodcastCard.tsx @@ -2,7 +2,8 @@ import axios from "axios"; import Placeholder from "../assets/placeholder_image.jpg"; import PlayIcon from "../assets/play-icon.svg"; -import { Queue, useQueueDispatch } from "../contexts/QueueContext"; +import { Queue, useQueue, useQueueDispatch } from "../contexts/QueueContext"; +import { useNavigate } from "react-router-dom"; export type cardProps = { idpodcast: number; @@ -17,19 +18,36 @@ export default function PodcastCard({ description, imageurl, }: cardProps): JSX.Element { - const dispatchQueue = useQueueDispatch() + const dispatchQueue = useQueueDispatch(); + const queue = useQueue(); + + const navigate = useNavigate(); + + const handleNavigate = () => { + navigate(`/podcast/${idpodcast}`); + }; + + const handleAddToQueue = async (e: React.MouseEvent<HTMLElement>) => { + e.stopPropagation(); - const handleAddToQueue = async () => { const axiosInstance = axios.create({ headers: { Authorization: `Bearer ${localStorage.getItem("token")}`, }, }); + await axiosInstance.delete(`${import.meta.env.VITE_REST_URL}/queue`); + await axiosInstance.post(`${import.meta.env.VITE_REST_URL}/queue/podcast`, { idPodcast: idpodcast, }); + if (queue.current) { + await axiosInstance.post( + `${import.meta.env.VITE_REST_URL}/queue/forward` + ); + } + const [current, next, prev] = await Promise.all([ axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/current`), axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/next`), @@ -46,7 +64,10 @@ export default function PodcastCard({ }; return ( - <div className="w-[160px] h-[230px] rounded-xl overflow-hidden border-NAVY-5 border-2 shadow-[-2px_2px_4px_0_#5C67DE,2px_-2px_4px_0_#5C67DE,-2px_-2px_4px_0_#5C67DE,2px_2px_4px_0_#5C67DE] hover:shadow-[-2px_2px_4px_0_#F5D049,2px_-2px_4px_0_#F5D049,-2px_-2px_4px_0_#F5D049,2px_2px_4px_0_#F5D049] hover:bg-YELLOW-5 hover:border-YELLOW-1 group shrink-0 xl:w-[200px] xl:h-[288px]"> + <div + onClick={handleNavigate} + className="cursor-pointer w-[160px] h-[230px] rounded-xl overflow-hidden border-NAVY-5 border-2 shadow-[-2px_2px_4px_0_#5C67DE,2px_-2px_4px_0_#5C67DE,-2px_-2px_4px_0_#5C67DE,2px_2px_4px_0_#5C67DE] hover:shadow-[-2px_2px_4px_0_#F5D049,2px_-2px_4px_0_#F5D049,-2px_-2px_4px_0_#F5D049,2px_2px_4px_0_#F5D049] hover:bg-YELLOW-5 hover:border-YELLOW-1 group shrink-0 xl:w-[200px] xl:h-[288px]" + > <img className="w-[160px] h-[140px] object-cover object-center xl:w-[200px] xl:h-[175px]" src={ @@ -62,7 +83,7 @@ export default function PodcastCard({ <div className="pt-3 pb-5 px-1.5 w-full group-hover:text-NAVY-2 relative xl:pt-4 xl:pb-6 xl:px-2.5"> <button onClick={handleAddToQueue} - className="invisible group-hover:visible flex absolute right-2.5 top-0 -translate-y-[10px] group-hover:-translate-y-[32px] items-center justify-center rounded-full bg-BLACK py-4 pl-[18px] pr-3.5 transition-transform duration-500" + className="invisible hover:scale-100 scale-90 group-hover:visible flex absolute right-2.5 top-0 -translate-y-[10px] group-hover:-translate-y-[32px] items-center justify-center rounded-full bg-BLACK py-4 pl-[18px] pr-3.5 transition-transform duration-500" > <img src={PlayIcon} width={16} height={16} alt="pause-episode" /> </button> diff --git a/src/components/PodcastHeader.tsx b/src/components/PodcastHeader.tsx index 272cb55..bd571a3 100644 --- a/src/components/PodcastHeader.tsx +++ b/src/components/PodcastHeader.tsx @@ -1,6 +1,8 @@ +import axios from "axios"; import Placeholder from "../assets/placeholder_image.jpg"; import PlayIcon from "../assets/play-icon.svg"; import PlusIcon from "../assets/plus-icon.svg"; +import { Queue, useQueue, useQueueDispatch } from "../contexts/QueueContext"; export type headerProps = { category: string; @@ -8,6 +10,7 @@ export type headerProps = { title: string; description: string; url_thumbnail: string; + id_podcast: number; }; export default function PodcastHeader({ @@ -16,8 +19,49 @@ export default function PodcastHeader({ title, description, url_thumbnail, + id_podcast, }: headerProps): JSX.Element { const urlPrefix = "http://localhost:3000/images/"; + + const queue = useQueue(); + const dispatchQueue = useQueueDispatch(); + + const handleAddToQueue = async (e: React.MouseEvent<HTMLElement>) => { + e.stopPropagation(); + + const axiosInstance = axios.create({ + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + await axiosInstance.delete(`${import.meta.env.VITE_REST_URL}/queue`); + + await axiosInstance.post(`${import.meta.env.VITE_REST_URL}/queue/podcast`, { + idPodcast: id_podcast, + }); + + if (queue.current) { + await axiosInstance.post( + `${import.meta.env.VITE_REST_URL}/queue/forward` + ); + } + + const [current, next, prev] = await Promise.all([ + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/current`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/next`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/previous`), + ]); + + const tempQueue: Queue = { + prev: prev.data.result, + current: current.data.result, + next: next.data.result, + }; + + dispatchQueue({ type: "SET_QUEUE", payload: tempQueue }); + }; + return ( <div className="block"> <div className="w-[950px] inline-flex mt-[20px]"> @@ -71,6 +115,7 @@ export default function PodcastHeader({ </button> <button + onClick={handleAddToQueue} data-te-toggle="tooltip" title="play episode" className=" w-[48px] h-[48px] bg-black text-white rounded-[32px] h4 leading-4 ml-[30px] hover:bg-gray-600" diff --git a/src/components/layouts/Player.tsx b/src/components/layouts/Player.tsx index 02af69b..6032b13 100644 --- a/src/components/layouts/Player.tsx +++ b/src/components/layouts/Player.tsx @@ -33,6 +33,7 @@ export default function Player() { // Component states const [isPlaying, setPlaying] = useState(false); const [currProgress, setCurrProgress] = useState(0); + const [isInitialized, setInitialized] = useState(false); useEffect(() => { (async function () { @@ -59,19 +60,36 @@ export default function Player() { }, [dispatchQueue]); useEffect(() => { - if (playerRef.current) { - playerRef.current.src = queue.current?.url_audio - ? `${import.meta.env.VITE_REST_URL}/audio/${queue.current?.url_audio}` - : ""; - } - }, [queue]); + (async () => { + try { + if (playerRef.current) { + playerRef.current.src = queue.current?.url_audio + ? `${import.meta.env.VITE_REST_URL}/audio/${ + queue.current?.url_audio + }` + : ""; + + if (!isInitialized) { + console.log("oke"); + setInitialized(true); + setPlaying(false); + return; + } + + playerRef.current.pause(); + await playerRef.current.play(); + setPlaying(true); + } + } catch (err) { + console.log(err); + } + })(); + }, [queue, isInitialized]); const handlePlay = () => { setPlaying(true); if (playerRef.current) { - console.log(playerRef.current.src); - playerRef.current.play(); } }; @@ -135,7 +153,7 @@ export default function Player() { } ); - if (res.status === 200) { + if (res.data.messsage === "success") { const prev = await axios.get( `${import.meta.env.VITE_REST_URL}/queue/previous`, { @@ -154,6 +172,7 @@ export default function Player() { dispatchQueue({ type: "SET_QUEUE", payload: tempQueue }); revalidator.revalidate(); } + // ! Add hot toast here for error handling }; return ( diff --git a/src/lib/handleAddToQueue.ts b/src/lib/handleAddToQueue.ts new file mode 100644 index 0000000..26d606d --- /dev/null +++ b/src/lib/handleAddToQueue.ts @@ -0,0 +1,33 @@ +import axios from "axios"; + +export const handleAddToQueue = async (idpodcast: number) => { + const axiosInstance = axios.create({ + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + }); + + await axiosInstance.delete(`${import.meta.env.VITE_REST_URL}/queue`); + + await axiosInstance.post(`${import.meta.env.VITE_REST_URL}/queue/podcast`, { + idPodcast: idpodcast, + }); + + if (queue.current) { + await axiosInstance.post(`${import.meta.env.VITE_REST_URL}/queue/forward`); + } + + const [current, next, prev] = await Promise.all([ + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/current`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/next`), + axiosInstance.get(`${import.meta.env.VITE_REST_URL}/queue/previous`), + ]); + + const tempQueue: Queue = { + prev: prev.data.result, + current: current.data.result, + next: next.data.result, + }; + + dispatchQueue({ type: "SET_QUEUE", payload: tempQueue }); +}; diff --git a/src/pages/episode/index.tsx b/src/pages/episode/index.tsx index 8b24cf9..b8e2799 100644 --- a/src/pages/episode/index.tsx +++ b/src/pages/episode/index.tsx @@ -1,27 +1,34 @@ import EpisodeHeader, { headerProps } from "../../components/EpisodeHeader"; -import { useEffect, useState } from 'react' -import axios from 'axios' -import { useParams } from 'react-router-dom' +import { useEffect, useState } from "react"; +import axios from "axios"; +import { useParams } from "react-router-dom"; -export default function PodcastPage() : JSX.Element{ - const { episodeId } = useParams(); +export default function PodcastPage(): JSX.Element { + const { episodeId } = useParams(); - const [episodeHeader, setEpisodeHeader] = useState<headerProps>(); + const [episodeHeader, setEpisodeHeader] = useState<headerProps>(); - useEffect(() => { - (async () => { - const resEpisodeHeader = await axios.get( - `http://localhost:3000/episode/${episodeId}` - ); - setEpisodeHeader(resEpisodeHeader.data.episode[0]); - })(); - }, [episodeId]) + useEffect(() => { + (async () => { + const resEpisodeHeader = await axios.get( + `http://localhost:3000/episode/${episodeId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + } + ); + setEpisodeHeader(resEpisodeHeader.data.episode); + })(); + }, [episodeId]); - return( - <div className="ml-[100px]"> - {episodeHeader? (<EpisodeHeader {...episodeHeader}/>) : <h1 className="h1"></h1>} - </div> - - ); - -} \ No newline at end of file + return ( + <div className="ml-[100px]"> + {episodeHeader ? ( + <EpisodeHeader {...episodeHeader} /> + ) : ( + <h1 className="h1"></h1> + )} + </div> + ); +} diff --git a/src/pages/podcast/index.tsx b/src/pages/podcast/index.tsx index ac1febe..be02d37 100644 --- a/src/pages/podcast/index.tsx +++ b/src/pages/podcast/index.tsx @@ -27,14 +27,7 @@ export default function PodcastPage(): JSX.Element { ), ]); - // const resPodcastHeader = await axiosInstance.get( - // `${import.meta.env.VITE_REST_URL}/podcast/${podcastId}` - // ); setPodcastHeader(resPodcastHeader.data.podcast); - - // const resEpisodes = await axiosInstance.get( - // `${import.meta.env.VITE_REST_URL}/podcast/episode/${podcastId}` - // ); setEpisodes(resEpisodes.data.episodes); })(); }, [podcastId]); @@ -54,6 +47,7 @@ export default function PodcastPage(): JSX.Element { {episodes?.map((episode: episodeProps, idx: number) => ( <Episode key={idx} + id_episode={episode.id_episode} title={episode.title} description={episode.description} url_thumbnail={episode.url_thumbnail} -- GitLab