diff options
| author | Factiven <[email protected]> | 2023-09-13 00:45:53 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-09-13 00:45:53 +0700 |
| commit | 7327a69b55a20b99b14ee0803d6cf5f8b88c45ef (patch) | |
| tree | cbcca777593a8cc4b0282e7d85a6fc51ba517e25 /components/anime | |
| parent | Update issue templates (diff) | |
| download | moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.tar.xz moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.zip | |
Update v4 - Merge pre-push to main (#71)
* Create build-test.yml
* initial v4 commit
* update: github workflow
* update: push on branch
* Update .github/ISSUE_TEMPLATE/bug_report.md
* configuring next.config.js file
Diffstat (limited to 'components/anime')
| -rw-r--r-- | components/anime/changeView.js | 30 | ||||
| -rw-r--r-- | components/anime/episode.js | 185 | ||||
| -rw-r--r-- | components/anime/infoDetails.js | 6 | ||||
| -rw-r--r-- | components/anime/mobile/reused/description.js | 44 | ||||
| -rw-r--r-- | components/anime/mobile/reused/infoChip.js | 43 | ||||
| -rw-r--r-- | components/anime/mobile/topSection.js | 504 | ||||
| -rw-r--r-- | components/anime/viewMode/listMode.js | 58 | ||||
| -rw-r--r-- | components/anime/viewMode/thumbnailDetail.js | 25 | ||||
| -rw-r--r-- | components/anime/viewMode/thumbnailOnly.js | 30 | ||||
| -rw-r--r-- | components/anime/watch/primarySide.js | 45 | ||||
| -rw-r--r-- | components/anime/watch/secondarySide.js | 19 |
11 files changed, 781 insertions, 208 deletions
diff --git a/components/anime/changeView.js b/components/anime/changeView.js index cab9054..75ebdff 100644 --- a/components/anime/changeView.js +++ b/components/anime/changeView.js @@ -1,14 +1,14 @@ -import { useEffect, useState } from "react"; - -export default function ChangeView({ view, setView, episode }) { - // const [view, setView] = useState(1); - // const episode = null; +export default function ChangeView({ view, setView, episode, map }) { return ( <div className="flex gap-3 rounded-sm items-center p-2"> <div className={ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -30,7 +30,11 @@ export default function ChangeView({ view, setView, episode }) { height="20" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 1 ? "fill-action" @@ -44,7 +48,11 @@ export default function ChangeView({ view, setView, episode }) { <div className={ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -61,7 +69,11 @@ export default function ChangeView({ view, setView, episode }) { fill="none" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 2 ? "fill-action" diff --git a/components/anime/episode.js b/components/anime/episode.js index 5d3451b..b2f4bd7 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -1,13 +1,18 @@ import { useEffect, useState, Fragment } from "react"; -import { ChevronDownIcon, ClockIcon } from "@heroicons/react/20/solid"; -import { convertSecondsToTime } from "../../utils/getTimes"; +import { ChevronDownIcon } from "@heroicons/react/20/solid"; import ChangeView from "./changeView"; import ThumbnailOnly from "./viewMode/thumbnailOnly"; import ThumbnailDetail from "./viewMode/thumbnailDetail"; import ListMode from "./viewMode/listMode"; -import axios from "axios"; +import { convertSecondsToTime } from "../../utils/getTimes"; -export default function AnimeEpisode({ info, progress }) { +export default function AnimeEpisode({ + info, + session, + progress, + setProgress, + setWatch, +}) { const [providerId, setProviderId] = useState(); // default provider const [currentPage, setCurrentPage] = useState(1); // for pagination const [visible, setVisible] = useState(false); // for mobile view @@ -19,42 +24,60 @@ export default function AnimeEpisode({ info, progress }) { const [isDub, setIsDub] = useState(false); const [providers, setProviders] = useState(null); + const [mapProviders, setMapProviders] = useState(null); useEffect(() => { setLoading(true); - setProviders(null); const fetchData = async () => { - try { - const { data: firstResponse } = await axios.get( - `/api/consumet/episode/${info.id}${isDub === true ? "?dub=true" : ""}` - ); - if (firstResponse.data.length > 0) { - const defaultProvider = firstResponse.data?.find( - (x) => x.providerId === "gogoanime" - ); - setProviderId( - defaultProvider?.providerId || firstResponse.data[0].providerId - ); // set to first provider id - } + const response = await fetch( + `/api/v2/episode/${info.id}?releasing=${ + info.status === "RELEASING" ? "true" : "false" + }${isDub ? "&dub=true" : ""}` + ).then((res) => res.json()); + const getMap = response.find((i) => i?.map === true); + let allProvider = response; - setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); - setProviders(firstResponse.data); - setLoading(false); - } catch (error) { - setLoading(false); - setProviders([]); + if (getMap) { + allProvider = response.filter((i) => { + if (i?.providerId === "gogoanime" && i?.map !== true) { + return null; + } + return i; + }); + setMapProviders(getMap?.episodes); } + + if (allProvider.length > 0) { + const defaultProvider = allProvider.find( + (x) => x.providerId === "gogoanime" || x.providerId === "9anime" + ); + setProviderId(defaultProvider?.providerId || allProvider[0].providerId); // set to first provider id + } + + setView(Number(localStorage.getItem("view")) || 3); + setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); + setProviders(allProvider); + setLoading(false); }; fetchData(); + + return () => { + setProviders(null); + setMapProviders(null); + }; }, [info.id, isDub]); const episodes = - providers?.find((provider) => provider.providerId === providerId) - ?.episodes || []; + providers + ?.find((provider) => provider.providerId === providerId) + ?.episodes?.slice(0, mapProviders?.length) || []; const lastEpisodeIndex = currentPage * itemsPerPage; const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage; - const currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + let currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + if (isDub) { + currentEpisodes = currentEpisodes.filter((i) => i.hasDub === true); + } const totalPages = Math.ceil(episodes.length / itemsPerPage); const handleChange = (event) => { @@ -66,36 +89,90 @@ export default function AnimeEpisode({ info, progress }) { }; useEffect(() => { - if (episodes?.some((item) => item?.title === null)) { + if ( + !mapProviders || + mapProviders?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item?.image === null + ) + ) { setView(3); } }, [providerId, episodes]); + useEffect(() => { + if (episodes) { + const getEpi = info?.nextAiringEpisode + ? episodes.find((i) => i.number === progress + 1) + : episodes[0]; + if (getEpi) { + const watchUrl = `/en/anime/watch/${ + info.id + }/${providerId}?id=${encodeURIComponent(getEpi.id)}&num=${ + getEpi.number + }${isDub ? `&dub=${isDub}` : ""}`; + setWatch(watchUrl); + } else { + setWatch(null); + } + } + }, [episodes]); + + useEffect(() => { + if (artStorage) { + // console.log({ artStorage }); + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + // Create a new object to store the updated data + const updatedData = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (Number(item.aniId) === info.id && item.provider === providerId) { + updatedData[key] = item; + } + } + + if (!session?.user?.name) { + setProgress( + Object.keys(updatedData).length > 0 + ? Math.max( + ...Object.keys(updatedData).map( + (key) => updatedData[key].episode + ) + ) + : 0 + ); + } else { + return; + } + } + }, [providerId, artStorage, info.id, session?.user?.name]); + return ( <> - <div className="flex flex-col gap-5 px-3"> + <div className="flex flex-col gap-5 px-3"> <div className="flex lg:flex-row flex-col gap-5 lg:gap-0 justify-between "> <div className="flex justify-between"> - <div className="flex items-center lg:gap-10 sm:gap-7 gap-3"> + <div className="flex items-center md:gap-5"> {info && ( <h1 className="text-[20px] lg:text-2xl font-bold font-karla"> Episodes </h1> )} - {info?.nextAiringEpisode && ( - <div className="flex items-center gap-2"> - <div className="flex items-center gap-4 text-[10px] xxs:text-sm lg:text-base"> - <h1>Next :</h1> - <div className="px-4 rounded-sm font-karla font-bold bg-white text-black"> - {convertSecondsToTime( - info.nextAiringEpisode.timeUntilAiring - )} - </div> - </div> - <div className="h-6 w-6"> - <ClockIcon /> - </div> - </div> + {info.nextAiringEpisode?.timeUntilAiring && ( + <p className="hidden md:block bg-gray-100 text-gray-900 rounded-md px-2 font-karla font-medium"> + Ep {info.nextAiringEpisode.episode}{" "} + <span className="animate-pulse">{">>"}</span>{" "} + <span className="font-bold"> + {convertSecondsToTime( + info.nextAiringEpisode.timeUntilAiring + )}{" "} + </span> + </p> )} </div> @@ -165,9 +242,6 @@ export default function AnimeEpisode({ info, progress }) { </option> ))} </select> - {/* <span className="absolute invisible opacity-0 group-hover:opacity-100 group-hover:scale-100 scale-0 group-hover:-translate-y-7 translate-y-0 group-hover:visible rounded-sm shadow top-0 w-32 bg-secondary text-center transition-all transform duration-200 ease-out"> - Select Providers - </span> */} <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> </div> @@ -197,6 +271,7 @@ export default function AnimeEpisode({ info, progress }) { view={view} setView={setView} episode={currentEpisodes} + map={mapProviders} /> </div> </div> @@ -204,15 +279,21 @@ export default function AnimeEpisode({ info, progress }) { {/* Episodes */} {!loading ? ( <div - className={ + className={`${ view === 1 ? "grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-5 lg:gap-8 place-items-center" - : `flex flex-col gap-3` - } + : view === 2 + ? "flex flex-col gap-3" + : `flex flex-col odd:bg-secondary even:bg-primary` + } py-2`} > {Array.isArray(providers) ? ( providers.length > 0 ? ( currentEpisodes.map((episode, index) => { + const mapData = mapProviders?.find( + (i) => i.number === episode.number + ); + return ( <Fragment key={index}> {view === 1 && ( @@ -220,17 +301,20 @@ export default function AnimeEpisode({ info, progress }) { key={index} index={index} info={info} + image={mapData?.image} providerId={providerId} episode={episode} artStorage={artStorage} progress={progress} dub={isDub} - // image={thumbnail} /> )} {view === 2 && ( <ThumbnailDetail key={index} + image={mapData?.image} + title={mapData?.title} + description={mapData?.description} index={index} epi={episode} provider={providerId} @@ -245,7 +329,6 @@ export default function AnimeEpisode({ info, progress }) { key={index} info={info} episode={episode} - index={index} artStorage={artStorage} providerId={providerId} progress={progress} diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js index 814e49b..8200bfa 100644 --- a/components/anime/infoDetails.js +++ b/components/anime/infoDetails.js @@ -165,9 +165,7 @@ export default function DesktopDetails({ > <div className="w-[90px] bg-image rounded-l-md shrink-0"> <Image - src={ - rel.coverImage.extraLarge || rel.coverImage.large - } + src={rel.coverImage.extraLarge} alt={rel.id} height={500} width={500} @@ -179,7 +177,7 @@ export default function DesktopDetails({ {r.relationType} </div> <div className="font-outfit font-thin line-clamp-2"> - {rel.title.userPreferred || rel.title.romaji} + {rel.title.userPreferred} </div> <div className={``}>{rel.type}</div> </div> diff --git a/components/anime/mobile/reused/description.js b/components/anime/mobile/reused/description.js new file mode 100644 index 0000000..99973d3 --- /dev/null +++ b/components/anime/mobile/reused/description.js @@ -0,0 +1,44 @@ +export default function Description({ + info, + readMore, + setReadMore, + className, +}) { + return ( + <div className={`${className} relative md:py-2 z-40`}> + <div + className={`${ + info?.description?.replace(/<[^>]*>/g, "").length > 240 + ? "" + : "pointer-events-none" + } ${ + readMore ? "hidden" : "" + } absolute z-30 flex items-end justify-center top-0 w-full h-full transition-all duration-200 ease-linear md:opacity-0 md:hover:opacity-100 bg-gradient-to-b from-transparent to-primary to-95%`} + > + <button + type="button" + disabled={readMore} + onClick={() => setReadMore(!readMore)} + className="text-center font-bold text-gray-200 py-1 w-full" + > + Read {readMore ? "Less" : "More"} + </button> + </div> + <p + className={`${ + readMore + ? "text-start md:h-[90px] md:overflow-y-scroll md:scrollbar-thin md:scrollbar-thumb-secondary md:scrollbar-thumb-rounded" + : "md:line-clamp-2 line-clamp-3 md:text-start text-center" + } text-sm md:text-base font-light antialiased font-karla leading-6`} + style={{ + scrollbarGutter: "stable", + }} + dangerouslySetInnerHTML={{ + __html: readMore + ? info?.description + : info?.description?.replace(/<[^>]*>/g, ""), + }} + /> + </div> + ); +} diff --git a/components/anime/mobile/reused/infoChip.js b/components/anime/mobile/reused/infoChip.js new file mode 100644 index 0000000..41def85 --- /dev/null +++ b/components/anime/mobile/reused/infoChip.js @@ -0,0 +1,43 @@ +import React from "react"; +import { getFormat } from "../../../../utils/getFormat"; + +export default function InfoChip({ info, color, className }) { + return ( + <div + className={`flex-wrap w-full justify-start md:pt-1 gap-4 ${className}`} + > + {info?.episodes && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.episodes} Episodes + </div> + )} + {info?.averageScore && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.averageScore}% + </div> + )} + {info?.format && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {getFormat(info?.format)} + </div> + )} + {info?.status && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.status} + </div> + )} + </div> + ); +} diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js index e9c9c7d..25d387f 100644 --- a/components/anime/mobile/topSection.js +++ b/components/anime/mobile/topSection.js @@ -1,81 +1,459 @@ -import { HeartIcon } from "@heroicons/react/20/solid"; +import { + ArrowUpCircleIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/solid"; import { - TvIcon, - ArrowTrendingUpIcon, - RectangleStackIcon, -} from "@heroicons/react/24/outline"; + ArrowLeftIcon, + PlayIcon, + PlusIcon, + ShareIcon, + UserIcon, +} from "@heroicons/react/24/solid"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useSearch } from "../../../lib/hooks/isOpenState"; +import { useEffect, useState } from "react"; +import { convertSecondsToTime } from "../../../utils/getTimes"; +import Link from "next/link"; +import { signIn } from "next-auth/react"; +import InfoChip from "./reused/infoChip"; +import Description from "./reused/description"; + +const getScrollPosition = (el = window) => ({ + x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, + y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop, +}); + +export function NewNavbar({ info, session, scrollP = 200, toTop = false }) { + const router = useRouter(); + const [scrollPosition, setScrollPosition] = useState(); + const { isOpen, setIsOpen } = useSearch(); + + useEffect(() => { + const handleScroll = () => { + setScrollPosition(getScrollPosition()); + }; -export default function DetailTop({ info, statuses, handleOpen, loading }) { + // Add a scroll event listener when the component mounts + window.addEventListener("scroll", handleScroll); + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); return ( - <div className="lg:hidden pt-5 w-screen px-5 flex flex-col"> - <div className="h-[250px] flex flex-col gap-1 justify-center"> - <h1 className="font-karla font-extrabold text-lg line-clamp-1 w-[70%]"> - {info?.title?.romaji || info?.title?.english} - </h1> - <p - className="line-clamp-2 text-sm font-light antialiased w-[56%]" - dangerouslySetInnerHTML={{ __html: info?.description }} - /> - <div className="font-light flex gap-1 py-1 flex-wrap font-outfit text-[10px] text-[#ffffff] w-[70%]"> - {info?.genres - ?.slice(0, info?.genres?.length > 3 ? info?.genres?.length : 3) - .map((item, index) => ( - <span - key={index} - className="px-2 py-1 bg-secondary shadow-lg font-outfit font-light rounded-full" + <> + <nav + className={`fixed z-[200] top-0 py-3 px-5 w-full ${ + scrollPosition?.y >= scrollP + ? "bg-tersier shadow-tersier shadow-sm" + : "" + } transition-all duration-200 ease-linear`} + > + <div className="flex items-center justify-between max-w-screen-2xl mx-auto"> + <div className="flex w-full items-center gap-4"> + {info ? ( + <> + <button + type="button" + className="flex-center w-7 h-7 text-white" + onClick={() => { + // router.back(); + router.push("/en"); + }} + > + <ArrowLeftIcon className="w-full h-full" /> + </button> + <span + className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${ + scrollPosition?.y >= scrollP + 80 + ? "opacity-100" + : "opacity-0" + } transition-all duration-200 ease-linear`} + > + {info.title.romaji} + </span> + </> + ) : ( + // <></> + <Link + href={"/en"} + className="flex-center text-white font-outfit text-2xl font-semibold" > - <span>{item}</span> - </span> - ))} - </div> - {info && ( - <div className="flex items-center gap-5 pt-3 text-center"> - <div className="flex items-center gap-2 text-center"> + moopa + </Link> + )} + </div> + <div className="flex items-center gap-4"> + <button + type="button" + onClick={() => setIsOpen(true)} + className="flex-center w-[26px] h-[26px]" + > + <svg + xmlns="http://www.w3.org/2000/svg" + width="32" + height="32" + viewBox="0 0 24 24" + > + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="2" + d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z" + ></path> + </svg> + </button> + {/* <div + className="bg-white" + // title={sessions ? "Go to Profile" : "Login With AniList"} + > */} + {session ? ( <button type="button" - className="bg-action px-10 rounded-sm font-karla font-bold" - onClick={() => handleOpen()} + onClick={() => router.push(`/en/profile/${session?.user.name}`)} + className="w-7 h-7 relative flex flex-col items-center group" > - {!loading - ? statuses - ? statuses.name - : "Add to List" - : "Loading..."} + <Image + src={session?.user.image.large} + alt="avatar" + width={50} + height={50} + className="w-full h-full object-cover" + /> + <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1"> + <Link + href={`/en/profile/${session?.user.name}`} + className="hover:text-action" + > + Profile + </Link> + <div + onClick={() => signOut({ callbackUrl: "/" })} + className="hover:text-action" + > + Log out + </div> + </div> </button> - <div className="h-6 w-6"> - <HeartIcon /> - </div> - </div> + ) : ( + <button + type="button" + onClick={() => signIn("AniListProvider")} + title="Login With AniList" + className="w-7 h-7 bg-white/30 rounded-full overflow-hidden" + > + <UserIcon className="w-full h-full translate-y-2" /> + </button> + )} + {/* </div> */} </div> - )} + </div> + </nav> + {toTop && ( + <button + type="button" + onClick={() => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }} + className={`${ + scrollPosition?.y >= 180 + ? "-translate-x-6 opacity-100" + : "translate-x-[100%] opacity-0" + } transform transition-all duration-300 ease-in-out fixed bottom-24 lg:bottom-14 right-0 z-[500]`} + > + <ArrowUpCircleIcon className="w-10 h-10 text-white" /> + </button> + )} + </> + ); +} + +export default function DetailTop({ + info, + session, + statuses, + handleOpen, + watchUrl, + progress, + color, +}) { + const router = useRouter(); + const [readMore, setReadMore] = useState(false); + + const [showAll, setShowAll] = useState(false); + + useEffect(() => { + setReadMore(false); + }, [info.id]); + + const handleShareClick = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: `Watch Now - ${info?.title?.english}`, + // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, + url: window.location.href, + }); + } else { + // Web Share API is not supported, provide a fallback or show a message + alert("Web Share API is not supported in this browser."); + } + } catch (error) { + console.error("Error sharing:", error); + } + }; + + return ( + <div className="gap-6 w-full px-3 pt-4 md:pt-10 flex flex-col items-center justify-center"> + <NewNavbar info={info} session={session} /> + + {/* MAIN */} + <div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12"> + <div className="shrink-0 w-[180px] h-[250px] rounded overflow-hidden"> + <Image + src={info?.coverImage?.extraLarge} + // alt="coverImage" + alt="poster anime" + width={300} + height={300} + className="w-full h-full object-cover" + /> + </div> + <div className="flex flex-col gap-4 items-center md:items-start justify-end w-full"> + <div className="flex flex-col gap-1 text-center md:text-start"> + <h3 className="font-karla text-lg capitalize leading-none"> + {info?.season?.toLowerCase()} {info.seasonYear} + </h3> + <h1 className="font-outfit font-extrabold text-2xl md:text-4xl line-clamp-2 text-white"> + {info?.title?.romaji || info?.title?.english} + </h1> + <h2 className="font-karla line-clamp-1 text-sm md:text-lg md:pb-2 font-light text-white/70"> + {info.title?.english} + </h2> + <InfoChip info={info} color={color} className="hidden md:flex" /> + {info?.description && ( + <Description + info={info} + readMore={readMore} + setReadMore={setReadMore} + className="md:block hidden" + /> + )} + </div> + </div> </div> - <div className="bg-secondary rounded-sm xs:h-[30px]"> - <div className="grid grid-cols-3 place-content-center xxs:flex items-center justify-center h-full xxs:gap-10 p-2 text-sm"> - {info && info.status !== "NOT_YET_RELEASED" ? ( - <> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <TvIcon className="w-5 h-5 text-action" /> - <h4 className="font-karla">{info?.type}</h4> - </div> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <ArrowTrendingUpIcon className="w-5 h-5 text-action" /> - <h4>{info?.averageScore ? `${info?.averageScore}%` : "N/A"}</h4> - </div> - <div className="flex-center flex-col xxs:flex-row gap-2"> - <RectangleStackIcon className="w-5 h-5 text-action" /> - {info?.episodes ? ( - <h1>{info?.episodes} Episodes</h1> - ) : ( - <h1>N/A</h1> - )} - </div> - </> + + <div className="hidden md:flex gap-5 items-center justify-start w-full"> + <button + type="button" + onClick={() => router.push(watchUrl)} + className={`${ + !watchUrl ? "opacity-30 pointer-events-none" : "" + } w-[180px] flex-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`} + > + <PlayIcon className="w-5 h-5" /> + {progress > 0 ? ( + statuses?.value === "COMPLETED" ? ( + "Rewatch" + ) : !watchUrl && info?.nextAiringEpisode ? ( + <span> + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + </span> + ) : ( + "Continue" + ) ) : ( - <div>{info && "Not Yet Released"}</div> + "Watch Now" )} + </button> + <div className="flex gap-2"> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={() => handleOpen()} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Add to List + </span> + <PlusIcon className="w-5 h-5" /> + </button> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={handleShareClick} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Share Anime + </span> + <ShareIcon className="w-5 h-5" /> + </button> + <a + target="_blank" + rel="noopener noreferrer" + href={`https://anilist.co/anime/${info.id}`} + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + See on AniList + </span> + <Image + src="/svg/anilist-icon.svg" + alt="anilist_icon" + width={20} + height={20} + /> + </a> </div> </div> + + <div className="md:hidden flex gap-2 items-center justify-center w-[90%]"> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={() => handleOpen()} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Add to List + </span> + <PlusIcon className="w-5 h-5" /> + </button> + <button + // href={watchUrl || ""} + type="button" + // disabled={!watchUrl || info?.nextAiringEpisode} + onClick={() => router.push(watchUrl)} + className={`${ + !watchUrl ? "opacity-30 pointer-events-none" : "" + } flex items-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-2 px-4 bg-white`} + > + <PlayIcon className="w-5 h-5" /> + {progress > 0 ? ( + statuses?.value === "COMPLETED" ? ( + "Rewatch" + ) : !watchUrl && info?.nextAiringEpisode ? ( + <span> + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + </span> + ) : ( + "Continue" + ) + ) : ( + "Watch Now" + )} + </button> + <button + type="button" + className="flex-center group relative w-10 h-10 bg-secondary rounded-full" + onClick={handleShareClick} + > + <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> + Share Anime + </span> + <ShareIcon className="w-5 h-5" /> + </button> + </div> + + {info.nextAiringEpisode?.timeUntilAiring && ( + <p className="md:hidden"> + Episode {info.nextAiringEpisode.episode} in{" "} + <span className="font-bold"> + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + </span> + </p> + )} + + {info?.description && ( + <Description + info={info} + readMore={readMore} + setReadMore={setReadMore} + className="md:hidden" + /> + )} + + <InfoChip + info={info} + color={color} + className={`${readMore ? "flex" : "hidden"} md:hidden`} + /> + + {info?.relations?.edges?.length > 0 && ( + <div className="w-screen md:w-full"> + <div className="flex justify-between items-center p-3 md:p-0"> + {info?.relations?.edges?.length > 0 && ( + <div className="text-[20px] md:text-2xl font-bold font-karla"> + Relations + </div> + )} + {info?.relations?.edges?.length > 3 && ( + <div + className="cursor-pointer font-karla" + onClick={() => setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} + </div> + )} + </div> + <div + className={` md:w-full flex gap-5 overflow-x-scroll snap-x scroll-px-5 scrollbar-none md:grid md:grid-cols-3 justify-items-center md:pt-7 md:pb-5 px-3 md:px-4 pt-4 rounded-xl`} + > + {info?.relations?.edges + .slice(0, showAll ? info?.relations?.edges.length : 3) + .map((r, index) => { + const rel = r.node; + return ( + <Link + key={rel.id} + href={ + rel.type === "ANIME" || + rel.type === "OVA" || + rel.type === "MOVIE" || + rel.type === "SPECIAL" || + rel.type === "ONA" + ? `/en/anime/${rel.id}` + : `/en/manga/${rel.id}` + } + className={`md:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full ${ + rel.type === "MUSIC" ? "pointer-events-none" : "" + }`} + > + <div + key={rel.id} + className="w-[400px] md:w-full h-[126px] bg-secondary flex rounded-md" + > + <div className="w-[90px] bg-image rounded-l-md shrink-0"> + <Image + src={rel.coverImage.extraLarge} + alt={rel.id} + height={500} + width={500} + className="object-cover h-full w-full shrink-0 rounded-l-md" + /> + </div> + <div className="h-full grid px-3 items-center"> + <div className="text-action font-outfit font-bold capitalize"> + {r.relationType.replace(/_/g, " ")} + </div> + <div className="font-outfit line-clamp-2"> + {rel.title.userPreferred} + </div> + <div className="font-thin">{rel.format}</div> + </div> + </div> + </Link> + ); + })} + </div> + </div> + )} </div> ); } diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js index f3bcf05..5beded1 100644 --- a/components/anime/viewMode/listMode.js +++ b/components/anime/viewMode/listMode.js @@ -3,7 +3,6 @@ import Link from "next/link"; export default function ListMode({ info, episode, - index, artStorage, providerId, progress, @@ -15,39 +14,32 @@ export default function ListMode({ if (prog > 90) prog = 100; return ( - <div key={episode.number} className="flex flex-col gap-3 px-2"> - <Link - href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent( - episode.id - )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`} - className={`text-start text-sm lg:text-lg ${ - progress - ? progress && episode.number <= progress + <Link + key={episode.number} + href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent( + episode.id + )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`} + className={`flex gap-3 py-4 hover:bg-secondary/10 odd:bg-secondary/30 even:bg-primary`} + > + <div className="flex w-full"> + <span className="shrink-0 px-4 text-center text-white/50"> + {episode.number} + </span> + <p + className={`w-full line-clamp-1 ${ + progress + ? progress && episode.number <= progress + ? "text-[#5f5f5f]" + : "text-white" + : prog === 100 ? "text-[#5f5f5f]" : "text-white" - : prog === 100 - ? "text-[#5f5f5f]" - : "text-white" - }`} - > - <p>Episode {episode.number}</p> - {episode.title && ( - <p - className={`text-xs lg:text-sm ${ - progress - ? progress && episode.number <= progress - ? "text-[#5f5f5f]" - : "text-[#b1b1b1]" - : prog === 100 - ? "text-[#5f5f5f]" - : "text-[#b1b1b1]" - } italic`} - > - "{episode.title}" - </p> - )} - </Link> - {index !== episode?.length - 1 && <span className="h-[1px] bg-white" />} - </div> + }`} + > + {episode?.title || `Episode ${episode.number}`} + </p> + <p className="capitalize text-sm text-white/50 px-4">{providerId}</p> + </div> + </Link> ); } diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js index 6efeb77..296e0d2 100644 --- a/components/anime/viewMode/thumbnailDetail.js +++ b/components/anime/viewMode/thumbnailDetail.js @@ -5,6 +5,9 @@ export default function ThumbnailDetail({ index, epi, info, + image, + title, + description, provider, artStorage, progress, @@ -25,13 +28,15 @@ export default function ThumbnailDetail({ > <div className="w-[43%] lg:w-[30%] relative shrink-0 z-40 rounded-lg overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> <div className="relative"> - <Image - src={epi?.image} - alt="Anime Cover" - width={1000} - height={1000} - className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]" - /> + {image && ( + <Image + src={image || ""} + alt="Anime Cover" + width={1000} + height={1000} + className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]" + /> + )} <span className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} style={{ @@ -63,11 +68,11 @@ export default function ThumbnailDetail({ className={`w-[70%] h-full select-none p-4 flex flex-col justify-center gap-3`} > <h1 className="font-karla font-bold text-base lg:text-lg xl:text-xl italic line-clamp-1"> - {epi?.title} + {title} </h1> - {epi?.description && ( + {description && ( <p className="line-clamp-2 text-xs lg:text-md xl:text-lg italic font-outfit font-extralight"> - {epi?.description} + {description} </p> )} </div> diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js index 99f02bd..69cd8c3 100644 --- a/components/anime/viewMode/thumbnailOnly.js +++ b/components/anime/viewMode/thumbnailOnly.js @@ -3,6 +3,7 @@ import Link from "next/link"; export default function ThumbnailOnly({ info, + image, providerId, episode, artStorage, @@ -35,25 +36,16 @@ export default function ThumbnailOnly({ : "0%", }} /> - <div className="absolute inset-0 bg-black z-30 opacity-20" /> - <Image - // src={ - // providerId === "animepahe" - // ? `https://img.moopa.live/image-proxy?url=${encodeURIComponent( - // episode.img - // )}&headers=${encodeURIComponent( - // JSON.stringify({ Referer: "https://animepahe.com/" }) - // )}` - // : thumbnail?.img.includes("null") - // ? info.coverImage.large - // : thumbnail?.img || info.coverImage.large - // } - src={episode?.image} - alt="epi image" - width={500} - height={500} - className="object-cover w-full h-[150px] sm:h-[100px] z-20" - /> + {/* <div className="absolute inset-0 bg-black z-30 opacity-20" /> */} + {image && ( + <Image + src={image || ""} + alt="epi image" + width={500} + height={500} + className="object-cover w-full h-[150px] sm:h-[100px] z-20 brightness-75" + /> + )} </Link> ); } diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js index b032fd6..a3d9f4f 100644 --- a/components/anime/watch/primarySide.js +++ b/components/anime/watch/primarySide.js @@ -9,18 +9,14 @@ import Link from "next/link"; import Skeleton from "react-loading-skeleton"; import Modal from "../../modal"; import AniList from "../../media/aniList"; -import axios from "axios"; export default function PrimarySide({ info, session, epiNumber, - setLoading, navigation, - loading, providerId, watchId, - status, onList, proxy, disqus, @@ -33,15 +29,31 @@ export default function PrimarySide({ const [open, setOpen] = useState(false); const [skip, setSkip] = useState(); + const [loading, setLoading] = useState(true); + const router = useRouter(); useEffect(() => { setLoading(true); async function fetchData() { if (info) { - const { data } = await axios.get( - `/api/consumet/source/${providerId}/${watchId}` - ); + const anify = await fetch("/api/v2/source", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + source: + providerId === "gogoanime" && !watchId.startsWith("/") + ? "consumet" + : "anify", + providerId: providerId, + watchId: watchId, + episode: epiNumber, + id: info.id, + sub: dub ? "dub" : "sub", + }), + }).then((res) => res.json()); const skip = await fetch( `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt( @@ -65,10 +77,9 @@ export default function PrimarySide({ setSkip({ op, ed }); - setEpisodeData(data); + setEpisodeData(anify); setLoading(false); } - // setMal(malId); } fetchData(); @@ -134,7 +145,7 @@ export default function PrimarySide({ <div className="w-full h-full"> <div key={watchId} className="w-full aspect-video bg-black"> {!loading ? ( - episodeData && ( + navigation && episodeData?.sources?.length !== 0 ? ( <VideoPlayer session={session} info={info} @@ -142,7 +153,6 @@ export default function PrimarySide({ provider={providerId} id={watchId} progress={epiNumber} - stats={status} skip={skip} proxy={proxy} aniId={info.id} @@ -151,9 +161,20 @@ export default function PrimarySide({ timeWatched={timeWatched} dub={dub} /> + ) : ( + <p className="h-full flex-center"> + Video is not available, please try other providers + </p> ) ) : ( - <div className="aspect-video bg-black" /> + <div className="flex-center aspect-video bg-black"> + <div className="lds-ellipsis"> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + </div> )} </div> <div className="flex flex-col divide-y divide-white/20"> diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js index 5d9b8f9..c9ef684 100644 --- a/components/anime/watch/secondarySide.js +++ b/components/anime/watch/secondarySide.js @@ -4,24 +4,27 @@ import Link from "next/link"; export default function SecondarySide({ info, + map, providerId, watchId, episode, - progress, artStorage, dub, }) { + const progress = info.mediaListEntry?.progress; return ( <div className="lg:w-[35%] shrink-0 w-screen"> <h1 className="text-xl font-karla pl-4 pb-5 font-semibold">Up Next</h1> <div className="flex flex-col gap-5 lg:pl-5 py-2 scrollbar-thin px-2 scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full"> {episode && episode.length > 0 ? ( - episode.some((item) => item.title && item.description) > 0 ? ( + map?.some((item) => item.title && item.description) > 0 ? ( episode.map((item) => { const time = artStorage?.[item.id]?.timeWatched; const duration = artStorage?.[item.id]?.duration; let prog = (time / duration) * 100; if (prog > 90) prog = 100; + + const mapData = map?.find((i) => i.number === item.number); return ( <Link href={`/en/anime/watch/${ @@ -38,8 +41,9 @@ export default function SecondarySide({ > <div className="w-[43%] lg:w-[40%] h-[110px] relative rounded-lg z-40 shrink-0 overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> <div className="relative"> + {/* {mapData?.image && ( */} <Image - src={item.image} + src={mapData?.image || info?.coverImage?.extraLarge} alt="Anime Cover" width={1000} height={1000} @@ -49,6 +53,7 @@ export default function SecondarySide({ : "brightness-75" }`} /> + {/* )} */} <span className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} style={{ @@ -61,7 +66,7 @@ export default function SecondarySide({ }} /> <span className="absolute bottom-2 left-2 font-karla font-bold text-sm"> - Episode {item.number} + Episode {item?.number} </span> {item.id == watchId && ( <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]"> @@ -78,15 +83,15 @@ export default function SecondarySide({ </div> </div> <div - className={`w-[70%] h-full select-none p-4 flex flex-col gap-2 ${ + className={`w-full h-full overflow-x-hidden select-none p-4 flex flex-col gap-2 ${ item.id == watchId ? "text-[#7a7a7a]" : "" }`} > <h1 className="font-karla font-bold italic line-clamp-1"> - {item.title} + {mapData?.title} </h1> <p className="line-clamp-2 text-xs italic font-outfit font-extralight"> - {item?.description} + {mapData?.description} </p> </div> </Link> |