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 | |
| 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')
31 files changed, 2661 insertions, 871 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> diff --git a/components/footer.js b/components/footer.js index d658172..ca5a21f 100644 --- a/components/footer.js +++ b/components/footer.js @@ -1,13 +1,11 @@ import Link from "next/link"; -import { signIn, useSession } from "next-auth/react"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import { parseCookies, setCookie } from "nookies"; function Footer() { - const { data: session, status } = useSession(); - const [year, setYear] = useState(new Date().getFullYear()); - const [season, setSeason] = useState(getCurrentSeason()); + const [year] = useState(new Date().getFullYear()); + const [season] = useState(getCurrentSeason()); const [lang, setLang] = useState("en"); const [checked, setChecked] = useState(false); @@ -41,118 +39,160 @@ function Footer() { }); router.push("/en"); } else { - console.log("switching to id"); - setCookie(null, "lang", "id", { - maxAge: 365 * 24 * 60 * 60, - path: "/", - }); router.push("/id"); } } return ( - <section className="text-[#dbdcdd] z-40 bg-[#0c0d10] lg:flex lg:h-[12rem] w-full lg:items-center lg:justify-between"> - <div className="mx-auto flex w-[80%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 pb-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> - <div className="flex items-center gap-24"> - <div className="lg:flex grid items-center lg:gap-10 gap-3"> - {/* <h1 className="font-outfit text-[2.56rem]">moopa</h1> */} - <h1 className="font-outfit text-[40px]">moopa</h1> - <div className="flex flex-col gap-5"> - <div className="flex flex-col gap-1"> - <p className="flex items-center gap-1 font-karla lg:text-[0.81rem] text-[0.7rem] text-[#CCCCCC]"> - © {new Date().getFullYear()} moopa.live | Website Made by - Factiven - </p> - <p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic"> - This site does not store any files on our server, we only - linked to the media which is hosted on 3rd party services. - </p> - </div> - - <label className="flex items-center relative w-max cursor-pointer select-none text-txt"> - <span className="text-base text-[#cccccc] font-inter font-semibold mr-3"> - Lang - </span> - <input - type="checkbox" - checked={checked} - onChange={() => switchLang()} - className="appearance-none transition-colors cursor-pointer w-14 h-5 rounded-full focus:outline-none focus:ring-offset-2 focus:ring-offset-black focus:ring-action bg-secondary" - /> - <span className="absolute font-medium text-xs uppercase right-2 text-action"> - {" "} - EN{" "} - </span> - <span className="absolute font-medium text-xs uppercase right-[2.1rem] text-action"> - {" "} - ID{" "} - </span> - <span className="w-6 h-6 right-[2.1rem] absolute rounded-full transform transition-transform bg-gray-200" /> - </label> - </div> + <footer className="flex-col w-full"> + <div className="text-[#dbdcdd] z-40 bg-[#0c0d10] lg:flex lg:h-[12rem] w-full lg:items-center lg:justify-between"> + <div className="mx-auto flex w-[85%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 py-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> + <div className="flex flex-col gap-2"> + {/* <div className="flex items-center gap-2"> */} + {/* <Image + src="/svg/c.svg" + alt="Website Logo" + width={100} + height={100} + className="w-10 h-10" + /> */} + <p className="font-outfit text-4xl">moopa</p> + <p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic"> + This site does not store any files on our server, we only linked + to the media which is hosted on 3rd party services. + </p> + {/* </div> */} </div> - {/* <div className="lg:hidden lg:block"> - <Image - src="https://i1210.photobucket.com/albums/cc417/kusanagiblog/NarutoVSSasuke.gif" - alt="gambar" - title="request nya rapip yulistian" - width={210} - height={85} - /> - </div> */} - </div> - <div className="flex flex-col gap-10 lg:flex-row lg:items-end lg:gap-[9.06rem] text-[#a7a7a7] text-sm lg:text-end"> - <div className="flex flex-col gap-10 font-karla font-bold lg:flex-row lg:gap-[5.94rem]"> - <ul className="flex flex-col gap-y-[0.7rem] "> - <li className="cursor-pointer hover:text-action"> - <Link - href={`/${lang}/search/anime?season=${season}&seasonYear=${year}`} - > - This Season - </Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/anime`}>Popular Anime</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/manga`}>Popular Manga</Link> - </li> - {status === "loading" ? ( - <p>Loading...</p> - ) : session ? ( + <div className="flex flex-col gap-10 lg:flex-row lg:items-end lg:gap-[9.06rem] text-[#a7a7a7] text-sm lg:text-end"> + <div className="flex flex-col gap-10 font-karla font-bold lg:flex-row lg:gap-[5.94rem]"> + <ul className="flex flex-col gap-y-[0.7rem] "> <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/profile/${session?.user?.name}`}> - My List + <Link + href={`/${lang}/search/anime?season=${season}&year=${year}`} + > + This Season </Link> </li> - ) : ( - <li className="hover:text-action"> - <button onClick={() => signIn("AniListProvider")}> - Login - </button> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/anime`}>Popular Anime</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/manga`}>Popular Manga</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href={`https://ko-fi.com/factiven`}>Donate</Link> + </li> + </ul> + <ul className="flex flex-col gap-y-[0.7rem]"> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/anime?format=MOVIE`}> + Movies + </Link> </li> - )} - </ul> - <ul className="flex flex-col gap-y-[0.7rem]"> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/anime`}>Movies</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/search/anime`}>TV Shows</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href={`/${lang}/dmca`}>DMCA</Link> - </li> - <li className="cursor-pointer hover:text-action"> - <Link href="https://github.com/DevanAbinaya/Ani-Moopa"> - Github - </Link> - </li> - </ul> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/search/anime?format=TV`}>TV Shows</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href={`/${lang}/dmca`}>DMCA</Link> + </li> + <li className="cursor-pointer hover:text-action"> + <Link href="https://github.com/DevanAbinaya/Ani-Moopa"> + Github + </Link> + </li> + </ul> + </div> + </div> + </div> + </div> + <div className="bg-tersier border-t border-white/5"> + <div className="mx-auto flex w-[90%] lg:w-[95%] xl:w-[80%] flex-col pb-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0"> + <p className="flex items-center gap-1 font-karla lg:text-[0.81rem] text-[0.7rem] text-[#CCCCCC] py-3"> + © {new Date().getFullYear()} moopa.live | Website Made by{" "} + <span className="text-white font-bold">Factiven</span> + </p> + <div className="flex items-center gap-5"> + {/* Github Icon */} + <Link + href="https://github.com/Ani-Moopa/Moopa" + className="w-5 h-5 hover:opacity-75" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="#fff" + viewBox="0 0 20 20" + > + <g> + <g + fill="none" + fillRule="evenodd" + stroke="none" + strokeWidth="1" + > + <g fill="#fff" transform="translate(-140 -7559)"> + <g transform="translate(56 160)"> + <path d="M94 7399c5.523 0 10 4.59 10 10.253 0 4.529-2.862 8.371-6.833 9.728-.507.101-.687-.219-.687-.492 0-.338.012-1.442.012-2.814 0-.956-.32-1.58-.679-1.898 2.227-.254 4.567-1.121 4.567-5.059 0-1.12-.388-2.034-1.03-2.752.104-.259.447-1.302-.098-2.714 0 0-.838-.275-2.747 1.051a9.396 9.396 0 00-2.505-.345 9.375 9.375 0 00-2.503.345c-1.911-1.326-2.751-1.051-2.751-1.051-.543 1.412-.2 2.455-.097 2.714-.639.718-1.03 1.632-1.03 2.752 0 3.928 2.335 4.808 4.556 5.067-.286.256-.545.708-.635 1.371-.57.262-2.018.715-2.91-.852 0 0-.529-.985-1.533-1.057 0 0-.975-.013-.068.623 0 0 .655.315 1.11 1.5 0 0 .587 1.83 3.369 1.21.005.857.014 1.665.014 1.909 0 .271-.184.588-.683.493-3.974-1.355-6.839-5.199-6.839-9.729 0-5.663 4.478-10.253 10-10.253"></path> + </g> + </g> + </g> + </g> + </svg> + </Link> + {/* Discord Icon */} + <Link + href="https://discord.gg/v5fjSdKwr2" + className="w-6 h-6 hover:opacity-75" + > + <svg + xmlns="http://www.w3.org/2000/svg" + preserveAspectRatio="xMidYMid" + viewBox="0 -28.5 256 256" + > + <path + fill="#fff" + d="M216.856 16.597A208.502 208.502 0 00164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 00-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0079.735 175.3a136.413 136.413 0 01-21.846-10.632 108.636 108.636 0 005.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 005.355 4.237 136.07 136.07 0 01-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36zM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18z" + ></path> + </svg> + </Link> + + {/* Kofi */} + <Link + href="https://ko-fi.com/factiven" + className="w-6 h-6 hover:opacity-75" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="#fff" + viewBox="0 0 24 24" + > + <path d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-1.246 1.453-4.011 3.976-4.011 3.976s-.121.119-.31.023c-.076-.057-.108-.09-.108-.09-.443-.441-3.368-3.049-4.034-3.954-.709-.965-1.041-2.7-.091-3.71.951-1.01 3.005-1.086 4.363.407 0 0 1.565-1.782 3.468-.963 1.904.82 1.832 3.011.723 4.311zm6.173.478c-.928.116-1.682.028-1.682.028V7.284h1.77s1.971.551 1.971 2.638c0 1.913-.985 2.667-2.059 3.015z"></path> + </svg> + </Link> + + <label + className="flex items-center relative w-max cursor-pointer select-none text-txt" + title="Switch to ID" + > + <input + type="checkbox" + checked={checked} + onChange={() => switchLang()} + className="appearance-none transition-colors cursor-pointer w-14 h-5 rounded-full focus:outline-none focus:ring-offset-2 focus:ring-offset-black focus:ring-action bg-secondary" + /> + <span className="absolute font-medium text-xs uppercase right-2 text-action"> + {" "} + EN{" "} + </span> + <span className="absolute font-medium text-xs uppercase right-[2.1rem] text-action"> + {" "} + ID{" "} + </span> + <span className="w-6 h-6 right-[2.1rem] absolute rounded-full transform transition-transform bg-gray-200" /> + </label> </div> </div> </div> - </section> + </footer> ); } diff --git a/components/home/content.js b/components/home/content.js index 70f0e3f..e18e5d8 100644 --- a/components/home/content.js +++ b/components/home/content.js @@ -1,5 +1,6 @@ import Link from "next/link"; -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, Fragment } from "react"; +import { useDraggable } from "react-use-draggable-scroll"; import Image from "next/image"; import { MdChevronRight } from "react-icons/md"; import { @@ -14,6 +15,7 @@ import { ChevronLeftIcon } from "@heroicons/react/20/solid"; import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; import { useRouter } from "next/router"; import { toast } from "react-toastify"; +import HistoryOptions from "./content/historyOptions"; export default function Content({ ids, @@ -26,11 +28,10 @@ export default function Content({ }) { const router = useRouter(); - const [startX, setStartX] = useState(null); - const containerRef = useRef(null); + const ref = useRef(); + const { events } = useDraggable(ref); const [cookie, setCookie] = useState(null); - const [isDragging, setIsDragging] = useState(false); const [clicked, setClicked] = useState(false); const [lang, setLang] = useState("en"); @@ -55,39 +56,20 @@ export default function Content({ } }, []); - const handleMouseDown = (e) => { - setIsDragging(true); - setStartX(e.pageX - containerRef.current.offsetLeft); - }; - - const handleMouseUp = () => { - setIsDragging(false); - }; - - const handleMouseMove = (e) => { - if (!isDragging) return; - e.preventDefault(); - const x = e.pageX - containerRef.current.offsetLeft; - const walk = (x - startX) * 3; - containerRef.current.scrollLeft = scrollLeft - walk; - }; - - const handleClick = (e) => { - if (isDragging) { - e.preventDefault(); - } - }; - const [scrollLeft, setScrollLeft] = useState(false); const [scrollRight, setScrollRight] = useState(true); const slideLeft = () => { + ref.current.classList.add("scroll-smooth"); var slider = document.getElementById(ids); slider.scrollLeft = slider.scrollLeft - 500; + ref.current.classList.remove("scroll-smooth"); }; const slideRight = () => { + ref.current.classList.add("scroll-smooth"); var slider = document.getElementById(ids); slider.scrollLeft = slider.scrollLeft + 500; + ref.current.classList.remove("scroll-smooth"); }; const handleScroll = (e) => { @@ -128,6 +110,9 @@ export default function Content({ if (section === "Recently Watched") { router.push(`/${lang}/anime/recently-watched`); } + if (section === "New Episodes") { + router.push(`/${lang}/anime/recent`); + } if (section === "Trending Now") { router.push(`/${lang}/anime/trending`); } @@ -142,7 +127,7 @@ export default function Content({ } }; - const removeItem = async (id) => { + const removeItem = async (id, aniId) => { if (userName) { // remove from database const res = await fetch(`/api/user/update/episode`, { @@ -152,24 +137,42 @@ export default function Content({ }, body: JSON.stringify({ name: userName, - id: id, + id, + aniId, }), }); const data = await res.json(); - // remove from local storage - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + const updatedData = {}; + + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); } // update client - setRemoved(id); + setRemoved(id || aniId); if (data?.message === "Episode deleted") { toast.success("Episode removed from history", { @@ -182,17 +185,38 @@ export default function Content({ }); } } else { - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + setRemoved(id); + } + if (aniId) { + 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 (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + // Update localStorage with the filtered data + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + setRemoved(aniId); } - - setRemoved(id); } }; @@ -218,13 +242,10 @@ export default function Content({ </div> <div id={ids} - className="scroll flex h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide lg:gap-8 gap-4 lg:p-10 py-8 px-5 z-30 scroll-smooth" + className="flex h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide lg:gap-8 gap-4 lg:p-10 py-8 px-5 z-30" onScroll={handleScroll} - onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} - onMouseMove={handleMouseMove} - onClick={handleClick} - ref={containerRef} + {...events} + ref={ref} > {ids !== "recentlyWatched" ? slicedData?.map((anime) => { @@ -241,14 +262,14 @@ export default function Content({ title={anime.title.romaji} > {ids === "onGoing" && ( - <div className="h-[190px] lg:h-[265px] w-[135px] lg:w-[185px] bg-gradient-to-b from-transparent to-black absolute z-40 rounded-md whitespace-normal font-karla group"> + <div className="h-[190px] lg:h-[265px] w-[135px] lg:w-[185px] bg-gradient-to-b from-transparent to-black/90 absolute z-40 rounded-md whitespace-normal font-karla group"> <div className="flex flex-col items-center h-full justify-end text-center pb-5"> <h1 className="line-clamp-1 w-[70%] text-[10px]"> {anime.title.romaji || anime.title.english} </h1> {checkProgress(progress) && !clicked?.hasOwnProperty(anime.id) && ( - <ExclamationCircleIcon className="w-7 h-7 absolute z-40 -top-3 -right-3" /> + <ExclamationCircleIcon className="w-7 h-7 absolute z-40 text-white -top-3 -right-3" /> )} {checkProgress(progress) && ( <div @@ -275,30 +296,52 @@ export default function Content({ </div> </div> )} - <Image - draggable={false} - src={ - anime.image || - anime.coverImage?.extraLarge || - anime.coverImage?.large || - "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" - } - alt={ - anime.title.romaji || - anime.title.english || - "coverImage" - } - width={500} - height={300} - placeholder="blur" - blurDataURL={ - anime.image || - anime.coverImage?.extraLarge || - anime.coverImage?.large || - "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" - } - className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90" - /> + <div className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded-md z-30"> + {ids === "recentAdded" && ( + <div className="absolute bg-gradient-to-b from-black/30 to-transparent from-5% to-30% top-0 z-30 w-full h-full rounded" /> + )} + <Image + draggable={false} + src={ + anime.image || + anime.coverImage?.extraLarge || + anime.coverImage?.large || + "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" + } + alt={ + anime.title.romaji || + anime.title.english || + "coverImage" + } + width={500} + height={300} + placeholder="blur" + blurDataURL={ + anime.image || + anime.coverImage?.extraLarge || + anime.coverImage?.large || + "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png" + } + className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90" + /> + </div> + {ids === "recentAdded" && ( + <Fragment> + <Image + src="/svg/episode-badge.svg" + alt="episode-bade" + width={200} + height={100} + className="w-24 lg:w-32 absolute top-1 -right-[12px] lg:-right-[17px] z-40" + /> + <p className="absolute z-40 text-center w-[86px] lg:w-[110px] top-1 -right-2 lg:top-[5.5px] lg:-right-2 font-karla text-sm lg:text-base"> + Episode{" "} + <span className="text-white"> + {anime?.episodeNumber} + </span> + </p> + </Fragment> + )} </Link> {ids !== "onGoing" && ( <Link @@ -307,7 +350,8 @@ export default function Content({ title={anime.title.romaji} > <h1 className="font-karla font-semibold xl:text-base text-[15px]"> - {anime.status === "RELEASING" ? ( + {anime.status === "RELEASING" || + ids === "recentAdded" ? ( <span className="dots bg-green-500" /> ) : anime.status === "NOT_YET_RELEASED" ? ( <span className="dots bg-red-500" /> @@ -333,22 +377,50 @@ export default function Content({ key={i.watchId} className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" > - <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action"> - <div - className="flex flex-col items-center group/delete" + <div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible "> + {/* <button + type="button" + className="flex flex-col items-center group/delete relative" onClick={() => removeItem(i.watchId)} > - <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" /> + <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" /> <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> Remove from history </span> - </div> + </button> */} + <HistoryOptions + remove={removeItem} + watchId={i.watchId} + aniId={i.aniId} + /> + {i?.nextId && ( + <button + type="button" + className="flex flex-col items-center group/next relative" + onClick={() => { + router.push( + `/en/anime/watch/${i.aniId}/${ + i.provider + }?id=${encodeURIComponent(i?.nextId)}&num=${ + i?.nextNumber + }${i?.dub ? `&dub=${i?.dub}` : ""}` + ); + }} + > + <ChevronRightIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" /> + <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/next:visible group-hover/next:scale-100 group-hover/next:translate-x-0 group-hover/next:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> + Play Next Episode + </span> + </button> + )} </div> <Link className="relative w-[320px] aspect-video rounded-md overflow-hidden group" href={`/en/anime/watch/${i.aniId}/${ i.provider - }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}`} + }?id=${encodeURIComponent(i.watchId)}&num=${i.episode}${ + i?.dub ? `&dub=${i?.dub}` : "" + }`} > <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" /> <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30"> @@ -372,8 +444,8 @@ export default function Content({ {i?.image && ( <Image src={i?.image} - width={200} - height={200} + width="0" + height="0" alt="Episode Thumbnail" className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10" /> @@ -411,7 +483,7 @@ export default function Content({ section !== "Recommendations" && ( <div key={section} - className="flex cursor-pointer" + className="flex flex-col cursor-pointer" onClick={goToPage} > <div className="w-[320px] aspect-video overflow-hidden object-cover rounded-md border-secondary border-2 flex flex-col gap-2 items-center text-center justify-center text-[#6a6a6a] hover:text-[#9f9f9f] hover:border-[#757575] transition-colors duration-200"> diff --git a/components/home/content/historyOptions.js b/components/home/content/historyOptions.js new file mode 100644 index 0000000..1b9c5ed --- /dev/null +++ b/components/home/content/historyOptions.js @@ -0,0 +1,56 @@ +import { Menu, Transition } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import React, { Fragment } from "react"; + +export default function HistoryOptions({ remove, watchId, aniId }) { + return ( + <Menu as="div" className="relative inline-block text-left"> + <div> + <Menu.Button className="group/delete w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out"> + <XMarkIcon /> + <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible"> + Remove from history + </span> + </Menu.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <Menu.Items className="absolute z-50 right-0 mt-1 w-56 origin-top-right rounded-md bg-secondary shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> + <div className="px-1 py-1 "> + <Menu.Item> + {({ active }) => ( + <button + className={`${ + active ? "bg-white/10 text-white" : "text-gray-100" + } group flex w-full items-center rounded-md px-2 py-2 text-sm`} + onClick={() => remove(null, aniId)} + > + Delete All Episodes + </button> + )} + </Menu.Item> + <Menu.Item> + {({ active }) => ( + <button + className={`${ + active ? "bg-white/10 text-white" : "text-gray-100" + } group flex w-full items-center rounded-md px-2 py-2 text-sm`} + onClick={() => remove(watchId, null)} + > + Delete Just This Episode + </button> + )} + </Menu.Item> + </div> + </Menu.Items> + </Transition> + </Menu> + ); +} diff --git a/components/home/genres.js b/components/home/genres.js index 3eefecd..f054fc9 100644 --- a/components/home/genres.js +++ b/components/home/genres.js @@ -55,7 +55,7 @@ export default function Genres() { <ChevronRightIcon className="w-5 h-5" /> </div> <div className="flex xl:justify-center items-center relative"> - <div className="bg-gradient-to-r from-primary to-transparent z-40 absolute w-7 h-[200px] left-0" /> + <div className="bg-gradient-to-r from-primary to-transparent z-40 absolute w-7 h-full left-0" /> <div className="flex lg:gap-8 gap-3 lg:p-10 py-8 px-5 z-30 overflow-y-hidden overflow-x-scroll snap-x snap-proximity scrollbar-none relative"> <div className="flex lg:gap-10 gap-4"> {g.map((a, index) => ( @@ -80,7 +80,7 @@ export default function Genres() { ))} </div> </div> - <div className="bg-gradient-to-l from-primary to-transparent z-40 absolute w-7 h-[200px] lg:h-[300px] right-0" /> + <div className="bg-gradient-to-l from-primary to-transparent z-40 absolute w-7 h-full right-0" /> </div> </div> ); diff --git a/components/home/recommendation.js b/components/home/recommendation.js new file mode 100644 index 0000000..842932c --- /dev/null +++ b/components/home/recommendation.js @@ -0,0 +1,91 @@ +import Image from "next/image"; +// import data from "../../assets/dummyData.json"; +import { BookOpenIcon, PlayIcon } from "@heroicons/react/24/solid"; +import { useDraggable } from "react-use-draggable-scroll"; +import { useRef } from "react"; +import Link from "next/link"; + +export default function UserRecommendation({ data }) { + const ref = useRef(null); + const { events } = useDraggable(ref); + + const uniqueRecommendationIds = new Set(); + + // Filter out duplicates from the recommendations array + const filteredData = data.filter((recommendation) => { + // Check if the ID is already in the set + if (uniqueRecommendationIds.has(recommendation.id)) { + // If it's a duplicate, return false to exclude it from the filtered array + return false; + } + + // If it's not a duplicate, add the ID to the set and return true + uniqueRecommendationIds.add(recommendation.id); + return true; + }); + + return ( + <div className="flex flex-col bg-tersier relative rounded overflow-hidden"> + <div className="flex lg:gap-5 z-50"> + <div className="flex flex-col items-start justify-center gap-3 lg:gap-7 lg:w-[50%] pl-5 lg:px-10"> + <h2 className="font-bold text-3xl text-white"> + {data[0].title.userPreferred} + </h2> + <p + dangerouslySetInnerHTML={{ + __html: data[0].description?.replace(/<[^>]*>/g, ""), + }} + className="font-roboto font-light line-clamp-3 lg:line-clamp-3" + /> + <button + type="button" + className="border border-white/70 py-1 px-2 lg:py-2 lg:px-4 rounded-full flex items-center gap-2 text-white font-bold" + > + {data[0].type === "ANIME" ? ( + <PlayIcon className="w-5 h-5 text-white" /> + ) : ( + <BookOpenIcon className="w-5 h-5 text-white" /> + )} + {data[0].type === "ANIME" ? "Watch" : "Read"} Now + </button> + </div> + <div + id="recommendation-list" + className="flex gap-5 overflow-x-scroll scrollbar-none px-5 py-7 lg:py-10" + ref={ref} + {...events} + > + {filteredData.slice(0, 9).map((i) => ( + <Link + key={i.id} + href={`/en/${i.type.toLowerCase()}/${i.id}`} + className="relative snap-start shrink-0 group hover:bg-white/20 p-1 rounded" + > + <Image + src={i.coverImage.extraLarge} + alt={i.title.userPreferred} + width={190} + height={256} + className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded-md object-cover overflow-hidden transition-all duration-150 ease-in-out" + /> + <span className="absolute rounded pointer-events-none w-[240px] h-[50%] transition-all duration-150 ease-in transform translate-x-[75%] group-hover:translate-x-[80%] top-0 left-0 bg-secondary opacity-0 group-hover:opacity-100 flex flex-col z-50"> + <div className="">{i.title.userPreferred}</div> + <div>a</div> + </span> + </Link> + ))} + </div> + </div> + <div className="absolute top-0 left-0 z-40 bg-gradient-to-r from-transparent from-30% to-80% to-tersier w-[80%] lg:w-[60%] h-full" /> + {data[0]?.bannerImage && ( + <Image + src={data[0]?.bannerImage} + alt={data[0].title.userPreferred} + width={500} + height={500} + className="absolute top-0 left-0 z-30 w-[60%] h-full object-cover opacity-30" + /> + )} + </div> + ); +} diff --git a/components/home/schedule.js b/components/home/schedule.js index 4043c5e..a9846a7 100644 --- a/components/home/schedule.js +++ b/components/home/schedule.js @@ -1,17 +1,23 @@ import Image from "next/image"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { convertUnixToTime } from "../../utils/getTimes"; import { PlayIcon } from "@heroicons/react/20/solid"; import { BackwardIcon, ForwardIcon } from "@heroicons/react/24/solid"; import Link from "next/link"; +import { useCountdown } from "../../utils/useCountdownSeconds"; -export default function Schedule({ data, scheduleData, time }) { +export default function Schedule({ data, scheduleData, anime, update }) { let now = new Date(); let currentDay = now.toLocaleString("default", { weekday: "long" }).toLowerCase() + "Schedule"; currentDay = currentDay.replace("Schedule", ""); + const [day, hours, minutes, seconds] = useCountdown( + anime[0]?.airingSchedule.nodes[0]?.airingAt * 1000 || Date.now(), + update + ); + const [currentPage, setCurrentPage] = useState(0); const [days, setDay] = useState(); @@ -37,8 +43,6 @@ export default function Schedule({ data, scheduleData, time }) { setCurrentPage(todayIndex >= 0 ? todayIndex : 0); }, [currentDay, days]); - // console.log({ scheduleData }); - return ( <div className="flex flex-col gap-5 px-4 lg:px-0"> <h1 className="font-bold font-karla text-[20px] lg:px-5"> @@ -46,7 +50,7 @@ export default function Schedule({ data, scheduleData, time }) { </h1> <div className="rounded mb-5 shadow-md shadow-black"> <div className="overflow-hidden w-full h-[96px] lg:h-[10rem] rounded relative"> - <div className="absolute flex flex-col justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-[#0c0c0c] to-transparent w-full h-full"> + <div className="absolute flex flex-col justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-tersier to-transparent w-full h-full"> <h1 className="text-xs lg:text-lg">Coming Up Next!</h1> <div className="w-1/2 lg:w-2/5 hidden lg:block font-medium font-karla leading-[2.9rem] text-white line-clamp-1"> <Link @@ -62,15 +66,15 @@ export default function Schedule({ data, scheduleData, time }) { </div> {data.bannerImage ? ( <Image - src={data.bannerImage || data.coverImage.large} + src={data.bannerImage || data.coverImage.extraLarge} width={500} height={500} alt="banner next anime" - className="absolute z-10 top-0 right-0 w-3/4 h-full object-cover brightness-[30%]" + className="absolute z-10 top-0 right-0 w-3/4 h-full object-cover opacity-30" /> ) : ( <Image - src={data.coverImage.large} + src={data.coverImage.extraLarge} width={500} height={500} sizes="100vw" @@ -87,22 +91,22 @@ export default function Schedule({ data, scheduleData, time }) { <div className="flex items-center gap-2 md:gap-5 font-bold font-karla text-sm md:text-xl"> {/* Countdown Timer */} <div className="flex flex-col items-center"> - <span className="text-action/80">{time.days}</span> + <span className="text-action/80">{day}</span> <span className="text-sm lg:text-base font-medium">Days</span> </div> <span></span> <div className="flex flex-col items-center"> - <span className="text-action/80">{time.hours}</span> + <span className="text-action/80">{hours}</span> <span className="text-sm lg:text-base font-medium">Hours</span> </div> <span></span> <div className="flex flex-col items-center"> - <span className="text-action/80">{time.minutes}</span> + <span className="text-action/80">{minutes}</span> <span className="text-sm lg:text-base font-medium">Mins</span> </div> <span></span> <div className="flex flex-col items-center"> - <span className="text-action/80">{time.seconds}</span> + <span className="text-action/80">{seconds}</span> <span className="text-sm lg:text-base font-medium">Secs</span> </div> </div> diff --git a/components/home/staticNav.js b/components/home/staticNav.js index 93f7b26..b22a9e3 100644 --- a/components/home/staticNav.js +++ b/components/home/staticNav.js @@ -1,51 +1,27 @@ -import { signIn, useSession } from "next-auth/react"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { signIn, signOut, useSession } from "next-auth/react"; import { getCurrentSeason } from "../../utils/getTimes"; import Link from "next/link"; -import { parseCookies } from "nookies"; +// import { } from "@heroicons/react/24/solid"; +import { useSearch } from "../../lib/hooks/isOpenState"; +import Image from "next/image"; +import { UserIcon } from "@heroicons/react/20/solid"; +import { useRouter } from "next/router"; export default function Navigasi() { const { data: sessions, status } = useSession(); - const [year, setYear] = useState(new Date().getFullYear()); - const [season, setSeason] = useState(getCurrentSeason()); - - const [lang, setLang] = useState("en"); - const [cookie, setCookies] = useState(null); + const year = new Date().getFullYear(); + const season = getCurrentSeason(); const router = useRouter(); - useEffect(() => { - let lang = null; - if (!cookie) { - const cookie = parseCookies(); - lang = cookie.lang || null; - setCookies(cookie); - } - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); + const { setIsOpen } = useSearch(); - const handleFormSubmission = (inputValue) => { - router.push(`/${lang}/search/${encodeURIComponent(inputValue)}`); - }; - - const handleKeyDown = async (event) => { - if (event.key === "Enter") { - event.preventDefault(); - const inputValue = event.target.value; - handleFormSubmission(inputValue); - } - }; return ( <> {/* NAVBAR PC */} <div className="flex items-center justify-center"> - <div className="flex w-full items-center justify-between px-5 lg:mx-[94px]"> - <div className="flex items-center lg:gap-16 lg:pt-7"> + <div className="flex w-full items-center justify-between px-5 lg:mx-[94px] lg:pt-7"> + <div className="flex items-center lg:gap-16"> <Link href="/en/" className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]" @@ -55,16 +31,35 @@ export default function Navigasi() { <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex"> <li> <Link - href={`/en/search/anime?season=${season}&seasonYear=${year}`} + href={`/en/search/anime?season=${season}&year=${year}`} + className="hover:text-action/80 transition-all duration-150 ease-linear" > This Season </Link> </li> <li> - <Link href="/en/search/manga">Manga</Link> + <Link + href="/en/search/manga" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Manga + </Link> </li> <li> - <Link href="/en/search/anime">Anime</Link> + <Link + href="/en/search/anime" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Anime + </Link> + </li> + <li> + <Link + href="/en/schedule" + className="hover:text-action/80 transition-all duration-150 ease-linear" + > + Schedule + </Link> </li> {status === "loading" ? ( @@ -75,15 +70,19 @@ export default function Navigasi() { <li> <button onClick={() => signIn("AniListProvider")} - className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md" + className="hover:text-action/80 transition-all duration-150 ease-linear" + // className="px-2 py-1 ring-1 ring-action font-bold font-karla rounded-md" > - Sign in + Sign In </button> </li> )} {sessions && ( <li className="text-center"> - <Link href={`/en/profile/${sessions?.user.name}`}> + <Link + href={`/en/profile/${sessions?.user.name}`} + className="hover:text-action/80 transition-all duration-150 ease-linear" + > My List </Link> </li> @@ -92,18 +91,73 @@ export default function Navigasi() { )} </ul> </div> - <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-1"> - <div className="search-box "> - <input - className="search-text" - type="text" - placeholder="Search Anime" - onKeyDown={handleKeyDown} - /> - <div className="search-btn"> - <i className="fas fa-search"></i> - </div> - </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"} + > */} + {sessions ? ( + <button + type="button" + onClick={() => + router.push(`/en/profile/${sessions?.user.name}`) + } + className="w-7 h-7 relative flex flex-col items-center group" + > + <Image + src={sessions?.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/${sessions?.user.name}`} + className="hover:text-action" + > + Profile + </Link> + <div + onClick={() => signOut({ callbackUrl: "/" })} + className="hover:text-action cursor-pointer" + > + Log out + </div> + </div> + </button> + ) : ( + <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 text-white/50" /> + </button> + )} + {/* </div> */} </div> </div> </div> diff --git a/components/id-components/player/Artplayer.js b/components/id/player/Artplayer.js index e209433..e209433 100644 --- a/components/id-components/player/Artplayer.js +++ b/components/id/player/Artplayer.js diff --git a/components/id-components/player/VideoPlayerId.js b/components/id/player/VideoPlayerId.js index 1168313..1168313 100644 --- a/components/id-components/player/VideoPlayerId.js +++ b/components/id/player/VideoPlayerId.js diff --git a/components/navbar.js b/components/navbar.js index e148b09..7edd6c1 100644 --- a/components/navbar.js +++ b/components/navbar.js @@ -3,6 +3,7 @@ import Link from "next/link"; import { useSession, signIn, signOut } from "next-auth/react"; import Image from "next/image"; import { parseCookies } from "nookies"; +import MobileNav from "./shared/MobileNav"; function Navbar(props) { const { data: session, status } = useSession(); @@ -45,193 +46,7 @@ function Navbar(props) { <Link href={`/${lang}/`}>moopa</Link> </div> - {/* Mobile Hamburger */} - {!isVisible && ( - <button - onClick={handleShowClick} - className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" - id="bars" - > - <svg - xmlns="http://www.w3.org/2000/svg" - className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500" - viewBox="0 0 20 20" - fill="currentColor" - > - <path - fillRule="evenodd" - d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" - clipRule="evenodd" - /> - </svg> - </button> - )} - - {/* Mobile Menu */} - <div - className={`transition-all duration-150 ${ - fade ? "opacity-100" : "opacity-0" - } z-50`} - > - {isVisible && session && ( - <Link - href={`/${lang}/profile/${session?.user?.name}`} - className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]" - > - <Image - src={session?.user.image.large} - alt="user avatar" - height={500} - width={500} - className="object-cover w-[60px] h-[60px] rounded-full" - /> - </Link> - )} - {isVisible && ( - <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden"> - <div className="grid grid-cols-4 place-items-center gap-6"> - <button className="group flex flex-col items-center"> - <Link href={`/${lang}/`} className=""> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" - /> - </svg> - </Link> - <Link - href={`/${lang}/`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - home - </Link> - </button> - <button className="group flex flex-col items-center"> - <Link href={`/${lang}/about`}> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" - /> - </svg> - </Link> - <Link - href={`/${lang}/about`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - about - </Link> - </button> - <button className="group flex gap-[1.5px] flex-col items-center "> - <div> - <Link href={`/${lang}/search/anime`}> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="w-6 h-6 group-hover:stroke-action" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" - /> - </svg> - </Link> - </div> - <Link - href={`/${lang}/search/anime`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - search - </Link> - </button> - {session ? ( - <button - onClick={() => signOut("AniListProvider")} - className="group flex gap-[1.5px] flex-col items-center " - > - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 96 960 960" - className="group-hover:fill-action w-6 h-6 fill-txt" - > - <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path> - </svg> - </div> - <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"> - logout - </h1> - </button> - ) : ( - <button - onClick={() => signIn("AniListProvider")} - className="group flex gap-[1.5px] flex-col items-center " - > - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 96 960 960" - className="group-hover:fill-action w-6 h-6 fill-txt mr-2" - > - <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path> - </svg> - </div> - <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"> - login - </h1> - </button> - )} - </div> - <button onClick={handleHideClick}> - <svg - width="20" - height="21" - className="fill-orange-500" - viewBox="0 0 20 21" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <rect - x="2.44043" - y="0.941467" - width="23.5842" - height="3.45134" - rx="1.72567" - transform="rotate(45 2.44043 0.941467)" - /> - <rect - x="19.1172" - y="3.38196" - width="23.5842" - height="3.45134" - rx="1.72567" - transform="rotate(135 19.1172 3.38196)" - /> - </svg> - </button> - </div> - )} - </div> + <MobileNav sessions={session} /> <nav className="left-0 top-[-100%] hidden w-auto items-center gap-10 px-5 lg:flex"> <ul className="hidden gap-10 font-roboto text-md lg:flex items-center relative"> diff --git a/components/search/dropdown/inputSelect.js b/components/search/dropdown/inputSelect.js new file mode 100644 index 0000000..d36ee6e --- /dev/null +++ b/components/search/dropdown/inputSelect.js @@ -0,0 +1,111 @@ +import { Fragment } from "react"; +import { Combobox, Transition } from "@headlessui/react"; +import { + CheckIcon, + ChevronDownIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/20/solid"; +import React from "react"; +import { useRouter } from "next/router"; + +export default function InputSelect({ + data, + label, + keyDown, + selected, + setSelected, + query, + setQuery, + inputRef, +}) { + const router = useRouter(); + + function handleChange(event) { + setSelected(event); + router.push(`/en/search/${event.value.toLowerCase()}`); + } + + return ( + <Combobox value={selected} onChange={(e) => handleChange(e)}> + <div className="relative mt-1 z-[55] w-full"> + <div className="flex items-center gap-2 mb-2 relative"> + <span className="font-bold text-lg">{label}</span> + <Combobox.Button className="py-[2px] bg-secondary/70 rounded text-sm font-karla flex items-center px-2"> + {selected.name} + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </Combobox.Button> + </div> + <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-secondary text-left shadow-md focus:outline-none sm:text-sm"> + <input + type="text" + value={query || ""} + className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 bg-secondary text-gray-300 focus:ring-0 outline-none" + onKeyDown={keyDown} + onChange={(e) => setQuery(e.target.value)} + ref={inputRef} + /> + <div className="absolute inset-y-0 right-0 flex items-center pr-2"> + <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" /> + </div> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95 translate-y-5" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95 translate-y-5" + afterLeave={() => setQuery("")} + > + <Combobox.Options + className="absolute z-[55] mt-1 max-h-60 w-full rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + style={{ scrollbarGutter: "stable" }} + > + {data.length === 0 && query !== "" ? ( + <div className="relative cursor-default select-none py-2 px-4 text-gray-300"> + Nothing found. + </div> + ) : ( + data.map((item) => ( + <Combobox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 mx-2 rounded-md ${ + active ? "bg-white/5 text-white" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected ? "font-medium text-white" : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon className="h-5 w-5" aria-hidden="true" /> + </span> + ) : null} + </React.Fragment> + )} + </Combobox.Option> + )) + )} + </Combobox.Options> + </Transition> + </div> + </Combobox> + ); +} diff --git a/components/search/dropdown/multiSelector.js b/components/search/dropdown/multiSelector.js new file mode 100644 index 0000000..8eea547 --- /dev/null +++ b/components/search/dropdown/multiSelector.js @@ -0,0 +1,168 @@ +import { Fragment, useState } from "react"; +import { Combobox, Transition } from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import React from "react"; + +export default function MultiSelector({ + data, + other, + label, + selected, + setSelected, + inputRef, +}) { + // const [selected, setSelected] = useState(); + const [query, setQuery] = useState(""); + + const filteredMain = + query === "" + ? data + : data.filter((item) => + item.name + .toLowerCase() + .replace(/\s+/g, "") + .includes(query.toLowerCase().replace(/\s+/g, "")) + ); + + const filteredOther = + query === "" + ? other + : other.filter((item) => + item.name + .toLowerCase() + .replace(/\s+/g, "") + .includes(query.toLowerCase().replace(/\s+/g, "")) + ); + + return ( + <Combobox value={selected} onChange={setSelected} multiple> + <div className="relative mt-1 min-w-full lg:min-w-[160px] w-full"> + <div className="font-bold text-lg mb-2">{label}</div> + <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-secondary text-left shadow-md focus:outline-none sm:text-sm"> + <Combobox.Input + className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 bg-secondary text-gray-300 focus:ring-0 outline-none" + displayValue={(item) => item?.map((item) => item?.name).join(", ")} + placeholder="Any" + onChange={(event) => setQuery(event.target.value)} + /> + <Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2"> + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </Combobox.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95 translate-y-5" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95 translate-y-5" + afterLeave={() => setQuery("")} + > + <Combobox.Options + className="absolute z-50 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-white/10 scrollbar-thumb-rounded-lg mt-1 max-h-60 w-full overflow-auto rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + // style={{ scrollbarGutter: "stable" }} + > + {filteredOther.length === 0 && + filteredMain.length === 0 && + query !== "" ? ( + <div className="relative cursor-default select-none py-2 px-4 text-gray-300"> + Nothing found. + </div> + ) : ( + <div className="space-y-1"> + <span className="px-3 font-karla font-bold text-sm text-gray-200"> + GENRES + </span> + <div> + {filteredMain.map((item) => ( + <Combobox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 ml-2 mr-1 rounded-md ${ + active ? "bg-white/5 text-action" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected + ? "font-medium text-white" + : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon + className="h-5 w-5" + aria-hidden="true" + /> + </span> + ) : null} + </React.Fragment> + )} + </Combobox.Option> + ))} + </div> + <span className="px-3 font-karla font-bold text-sm text-gray-200"> + TAGS + </span> + <div> + {filteredOther.map((item) => ( + <Combobox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 ml-2 mr-1 rounded-md ${ + active ? "bg-white/5 text-white" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected + ? "font-medium text-white" + : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon + className="h-5 w-5" + aria-hidden="true" + /> + </span> + ) : null} + </React.Fragment> + )} + </Combobox.Option> + ))} + </div> + </div> + )} + </Combobox.Options> + </Transition> + </div> + </Combobox> + ); +} diff --git a/components/search/dropdown/singleSelector.js b/components/search/dropdown/singleSelector.js new file mode 100644 index 0000000..ec8afe0 --- /dev/null +++ b/components/search/dropdown/singleSelector.js @@ -0,0 +1,98 @@ +import { Fragment, useState } from "react"; +import { Combobox, Listbox, Transition } from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import React from "react"; + +export default function SingleSelector({ data, label, selected, setSelected }) { + // const [selected, setSelected] = useState(); + const [query, setQuery] = useState(""); + + const filteredData = + query === "" + ? data + : data.filter((item) => + item.name + .toLowerCase() + .replace(/\s+/g, "") + .includes(query.toLowerCase().replace(/\s+/g, "")) + ); + + return ( + <Listbox value={selected} onChange={setSelected}> + <div className="relative mt-1 min-w-full lg:min-w-[160px] w-full"> + <div className="font-bold text-lg mb-2">{label}</div> + <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-secondary text-left shadow-md focus:outline-none sm:text-sm"> + {/* <Combobox.Input + className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 bg-secondary text-gray-300 focus:ring-0 outline-none" + displayValue={(item) => item.name} + placeholder="Any" + onChange={(event) => setQuery(event.target.value)} + /> */} + <Listbox.Button className="w-full border-none py-2 text-start pl-3 text-sm leading-5 bg-secondary text-gray-400"> + <span>{selected?.name || "Any"}</span> + <div className="absolute inset-y-0 right-0 flex items-center pr-2"> + <ChevronDownIcon + className="h-5 w-5 text-gray-400" + aria-hidden="true" + /> + </div> + </Listbox.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95 translate-y-5" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95 translate-y-5" + afterLeave={() => setQuery("")} + > + <Listbox.Options + className="absolute z-50 scrollbar-thin scrollbar-thumb-white/10 scrollbar-thumb-rounded-lg mt-1 max-h-80 w-full overflow-auto rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + style={{ scrollbarGutter: "stable" }} + > + {filteredData.length === 0 && query !== "" ? ( + <div className="relative cursor-default select-none py-2 px-4 text-gray-300"> + Nothing found. + </div> + ) : ( + filteredData.map((item) => ( + <Listbox.Option + key={item.value} + className={({ active }) => + `relative cursor-pointer select-none py-2 px-2 ml-2 mr-1 rounded-md ${ + active ? "bg-white/5 text-action" : "text-gray-300" + }` + } + value={item} + > + {({ selected, active }) => ( + <React.Fragment> + <span + className={`block truncate ${ + selected ? "font-medium text-white" : "font-normal" + }`} + > + {item.name} + </span> + {selected ? ( + <span + className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${ + active ? "text-white" : "text-action" + }`} + > + <CheckIcon className="h-5 w-5" aria-hidden="true" /> + </span> + ) : null} + </React.Fragment> + )} + </Listbox.Option> + )) + )} + </Listbox.Options> + </Transition> + </div> + </Listbox> + ); +} diff --git a/components/search/selection.js b/components/search/selection.js new file mode 100644 index 0000000..767361d --- /dev/null +++ b/components/search/selection.js @@ -0,0 +1,415 @@ +export const mediaType = [ + { name: "Anime", value: "ANIME" }, + { name: "Manga", value: "MANGA" }, +]; +export const genreOptions = [ + { + name: "Action", + value: "Action", + type: "genres", + }, + { + name: "Adventure", + value: "Adventure", + type: "genres", + }, + { + name: "Comedy", + value: "Comedy", + type: "genres", + }, + { + name: "Drama", + value: "Drama", + type: "genres", + }, + { + name: "Ecchi", + value: "Ecchi", + type: "genres", + }, + { + name: "Fantasy", + value: "Fantasy", + type: "genres", + }, + { + name: "Horror", + value: "Horror", + type: "genres", + }, + { + name: "Mahou Shoujo", + value: "Mahou Shoujo", + type: "genres", + }, + { + name: "Mecha", + value: "Mecha", + type: "genres", + }, + { + name: "Music", + value: "Music", + type: "genres", + }, + { + name: "Mystery", + value: "Mystery", + type: "genres", + }, + { + name: "Psychological", + value: "Psychological", + type: "genres", + }, + { + name: "Romance", + value: "Romance", + type: "genres", + }, + { + name: "Sci-Fi", + value: "Sci-Fi", + type: "genres", + }, + { + name: "Slice of Life", + value: "Slice of Life", + type: "genres", + }, + { + name: "Sports", + value: "Sports", + type: "genres", + }, + { + name: "Supernatural", + value: "Supernatural", + type: "genres", + }, + { + name: "Thriller", + value: "Thriller", + type: "genres", + }, +]; +export const tagsOption = [ + { name: "4-koma", value: "4-koma", type: "tags" }, + { name: "Achronological Order", value: "Achronological Order", type: "tags" }, + { name: "Afterlife", value: "Afterlife", type: "tags" }, + { name: "Age Gap", value: "Age Gap", type: "tags" }, + { name: "Airsoft", value: "Airsoft", type: "tags" }, + { name: "Aliens", value: "Aliens", type: "tags" }, + { name: "Alternate Universe", value: "Alternate Universe", type: "tags" }, + { name: "American Football", value: "American Football", type: "tags" }, + { name: "Amnesia", value: "Amnesia", type: "tags" }, + { name: "Anti-Hero", value: "Anti-Hero", type: "tags" }, + { name: "Archery", value: "Archery", type: "tags" }, + { name: "Assassins", value: "Assassins", type: "tags" }, + { name: "Athletics", value: "Athletics", type: "tags" }, + { name: "Augmented Reality", value: "Augmented Reality", type: "tags" }, + { name: "Aviation", value: "Aviation", type: "tags" }, + { name: "Badminton", value: "Badminton", type: "tags" }, + { name: "Band", value: "Band", type: "tags" }, + { name: "Bar", value: "Bar", type: "tags" }, + { name: "Baseball", value: "Baseball", type: "tags" }, + { name: "Basketball", value: "Basketball", type: "tags" }, + { name: "Battle Royale", value: "Battle Royale", type: "tags" }, + { name: "Biographical", value: "Biographical", type: "tags" }, + { name: "Bisexual", value: "Bisexual", type: "tags" }, + { name: "Body Swapping", value: "Body Swapping", type: "tags" }, + { name: "Boxing", value: "Boxing", type: "tags" }, + { name: "Bullying", value: "Bullying", type: "tags" }, + { name: "Calligraphy", value: "Calligraphy", type: "tags" }, + { name: "Card Battle", value: "Card Battle", type: "tags" }, + { name: "Cars", value: "Cars", type: "tags" }, + { name: "CGI", value: "CGI", type: "tags" }, + { name: "Chibi", value: "Chibi", type: "tags" }, + { name: "Chuunibyou", value: "Chuunibyou", type: "tags" }, + { name: "Classic Literature", value: "Classic Literature", type: "tags" }, + { name: "College", value: "College", type: "tags" }, + { name: "Coming of Age", value: "Coming of Age", type: "tags" }, + { name: "Cosplay", value: "Cosplay", type: "tags" }, + { name: "Crossdressing", value: "Crossdressing", type: "tags" }, + { name: "Crossover", value: "Crossover", type: "tags" }, + { name: "Cultivation", value: "Cultivation", type: "tags" }, + { + name: "Cute Girls Doing Cute Things", + value: "Cute Girls Doing Cute Things", + type: "tags", + }, + { name: "Cyberpunk", value: "Cyberpunk", type: "tags" }, + { name: "Cycling", value: "Cycling", type: "tags" }, + { name: "Dancing", value: "Dancing", type: "tags" }, + { name: "Delinquents", value: "Delinquents", type: "tags" }, + { name: "Demons", value: "Demons", type: "tags" }, + { name: "Development", value: "Development", type: "tags" }, + { name: "Dragons", value: "Dragons", type: "tags" }, + { name: "Drawing", value: "Drawing", type: "tags" }, + { name: "Dystopian", value: "Dystopian", type: "tags" }, + { name: "Economics", value: "Economics", type: "tags" }, + { name: "Educational", value: "Educational", type: "tags" }, + { name: "Ensemble Cast", value: "Ensemble Cast", type: "tags" }, + { name: "Environmental", value: "Environmental", type: "tags" }, + { name: "Episodic", value: "Episodic", type: "tags" }, + { name: "Espionage", value: "Espionage", type: "tags" }, + { name: "Fairy Tale", value: "Fairy Tale", type: "tags" }, + { name: "Family Life", value: "Family Life", type: "tags" }, + { name: "Fashion", value: "Fashion", type: "tags" }, + { name: "Female Protagonist", value: "Female Protagonist", type: "tags" }, + { name: "Fishing", value: "Fishing", type: "tags" }, + { name: "Fitness", value: "Fitness", type: "tags" }, + { name: "Flash", value: "Flash", type: "tags" }, + { name: "Food", value: "Food", type: "tags" }, + { name: "Football", value: "Football", type: "tags" }, + { name: "Foreign", value: "Foreign", type: "tags" }, + { name: "Fugitive", value: "Fugitive", type: "tags" }, + { name: "Full CGI", value: "Full CGI", type: "tags" }, + { name: "Full Colour", value: "Full Colour", type: "tags" }, + { name: "Gambling", value: "Gambling", type: "tags" }, + { name: "Gangs", value: "Gangs", type: "tags" }, + { name: "Gender Bending", value: "Gender Bending", type: "tags" }, + { name: "Gender Neutral", value: "Gender Neutral", type: "tags" }, + { name: "Ghost", value: "Ghost", type: "tags" }, + { name: "Gods", value: "Gods", type: "tags" }, + { name: "Gore", value: "Gore", type: "tags" }, + { name: "Guns", value: "Guns", type: "tags" }, + { name: "Gyaru", value: "Gyaru", type: "tags" }, + { name: "Harem", value: "Harem", type: "tags" }, + { name: "Henshin", value: "Henshin", type: "tags" }, + { name: "Hikikomori", value: "Hikikomori", type: "tags" }, + { name: "Historical", value: "Historical", type: "tags" }, + { name: "Ice Skating", value: "Ice Skating", type: "tags" }, + { name: "Idol", value: "Idol", type: "tags" }, + { name: "Isekai", value: "Isekai", type: "tags" }, + { name: "Iyashikei", value: "Iyashikei", type: "tags" }, + { name: "Josei", value: "Josei", type: "tags" }, + { name: "Kaiju", value: "Kaiju", type: "tags" }, + { name: "Karuta", value: "Karuta", type: "tags" }, + { name: "Kemonomimi", value: "Kemonomimi", type: "tags" }, + { name: "Kids", value: "Kids", type: "tags" }, + { name: "Love Triangle", value: "Love Triangle", type: "tags" }, + { name: "Mafia", value: "Mafia", type: "tags" }, + { name: "Magic", value: "Magic", type: "tags" }, + { name: "Mahjong", value: "Mahjong", type: "tags" }, + { name: "Maids", value: "Maids", type: "tags" }, + { name: "Male Protagonist", value: "Male Protagonist", type: "tags" }, + { name: "Martial Arts", value: "Martial Arts", type: "tags" }, + { name: "Memory Manipulation", value: "Memory Manipulation", type: "tags" }, + { name: "Meta", value: "Meta", type: "tags" }, + { name: "Military", value: "Military", type: "tags" }, + { name: "Monster Girl", value: "Monster Girl", type: "tags" }, + { name: "Mopeds", value: "Mopeds", type: "tags" }, + { name: "Motorcycles", value: "Motorcycles", type: "tags" }, + { name: "Musical", value: "Musical", type: "tags" }, + { name: "Mythology", value: "Mythology", type: "tags" }, + { name: "Nekomimi", value: "Nekomimi", type: "tags" }, + { name: "Ninja", value: "Ninja", type: "tags" }, + { name: "No Dialogue", value: "No Dialogue", type: "tags" }, + { name: "Noir", value: "Noir", type: "tags" }, + { name: "Nudity", value: "Nudity", type: "tags" }, + { name: "Otaku Culture", value: "Otaku Culture", type: "tags" }, + { name: "Outdoor", value: "Outdoor", type: "tags" }, + { name: "Parody", value: "Parody", type: "tags" }, + { name: "Philosophy", value: "Philosophy", type: "tags" }, + { name: "Photography", value: "Photography", type: "tags" }, + { name: "Pirates", value: "Pirates", type: "tags" }, + { name: "Poker", value: "Poker", type: "tags" }, + { name: "Police", value: "Police", type: "tags" }, + { name: "Politics", value: "Politics", type: "tags" }, + { name: "Post-Apocalyptic", value: "Post-Apocalyptic", type: "tags" }, + { name: "Primarily Adult Cast", value: "Primarily Adult Cast", type: "tags" }, + { + name: "Primarily Female Cast", + value: "Primarily Female Cast", + type: "tags", + }, + { name: "Primarily Male Cast", value: "Primarily Male Cast", type: "tags" }, + { name: "Puppetry", value: "Puppetry", type: "tags" }, + { name: "Real Robot", value: "Real Robot", type: "tags" }, + { name: "Rehabilitation", value: "Rehabilitation", type: "tags" }, + { name: "Reincarnation", value: "Reincarnation", type: "tags" }, + { name: "Revenge", value: "Revenge", type: "tags" }, + { name: "Reverse Harem", value: "Reverse Harem", type: "tags" }, + { name: "Robots", value: "Robots", type: "tags" }, + { name: "Rugby", value: "Rugby", type: "tags" }, + { name: "Rural", value: "Rural", type: "tags" }, + { name: "Samurai", value: "Samurai", type: "tags" }, + { name: "Satire", value: "Satire", type: "tags" }, + { name: "School", value: "School", type: "tags" }, + { name: "School Club", value: "School Club", type: "tags" }, + { name: "Seinen", value: "Seinen", type: "tags" }, + { name: "Ships", value: "Ships", type: "tags" }, + { name: "Shogi", value: "Shogi", type: "tags" }, + { name: "Shoujo", value: "Shoujo", type: "tags" }, + { name: "Shoujo Ai", value: "Shoujo Ai", type: "tags" }, + { name: "Shounen", value: "Shounen", type: "tags" }, + { name: "Shounen Ai", value: "Shounen Ai", type: "tags" }, + { name: "Slapstick", value: "Slapstick", type: "tags" }, + { name: "Slavery", value: "Slavery", type: "tags" }, + { name: "Space", value: "Space", type: "tags" }, + { name: "Space Opera", value: "Space Opera", type: "tags" }, + { name: "Steampunk", value: "Steampunk", type: "tags" }, + { name: "Stop Motion", value: "Stop Motion", type: "tags" }, + { name: "Super Power", value: "Super Power", type: "tags" }, + { name: "Super Robot", value: "Super Robot", type: "tags" }, + { name: "Superhero", value: "Superhero", type: "tags" }, + { name: "Surreal Comedy", value: "Surreal Comedy", type: "tags" }, + { name: "Survival", value: "Survival", type: "tags" }, + { name: "Swimming", value: "Swimming", type: "tags" }, + { name: "Swordplay", value: "Swordplay", type: "tags" }, + { name: "Table Tennis", value: "Table Tennis", type: "tags" }, + { name: "Tanks", value: "Tanks", type: "tags" }, + { name: "Teacher", value: "Teacher", type: "tags" }, + { name: "Tennis", value: "Tennis", type: "tags" }, + { name: "Terrorism", value: "Terrorism", type: "tags" }, + { name: "Time Manipulation", value: "Time Manipulation", type: "tags" }, + { name: "Time Skip", value: "Time Skip", type: "tags" }, + { name: "Tragedy", value: "Tragedy", type: "tags" }, + { name: "Trains", value: "Trains", type: "tags" }, + { name: "Triads", value: "Triads", type: "tags" }, + { name: "Tsundere", value: "Tsundere", type: "tags" }, + { name: "Urban Fantasy", value: "Urban Fantasy", type: "tags" }, + { name: "Vampire", value: "Vampire", type: "tags" }, + { name: "Video Games", value: "Video Games", type: "tags" }, + { name: "Virtual World", value: "Virtual World", type: "tags" }, + { name: "Volleyball", value: "Volleyball", type: "tags" }, + { name: "War", value: "War", type: "tags" }, + { name: "Witch", value: "Witch", type: "tags" }, + { name: "Work", value: "Work", type: "tags" }, + { name: "Wrestling", value: "Wrestling", type: "tags" }, + { name: "Writing", value: "Writing", type: "tags" }, + { name: "Wuxia", value: "Wuxia", type: "tags" }, + { name: "Yakuza", value: "Yakuza", type: "tags" }, + { name: "Yandere", value: "Yandere", type: "tags" }, + { name: "Youkai", value: "Youkai", type: "tags" }, + { name: "Zombie", value: "Zombie", type: "tags" }, +]; +export const formatOptions = [ + { name: "TV", value: "TV" }, + { name: "TV Short", value: "TV_SHORT" }, + { name: "Movie", value: "MOVIE" }, + { name: "Special", value: "SPECIAL" }, + { name: "OVA", value: "OVA" }, + { name: "ONA", value: "ONA" }, + { name: "Music", value: "MUSIC" }, + { name: "Manga", value: "MANGA" }, + { name: "Novel", value: "NOVEL" }, + { name: "One Shot", value: "ONE_SHOT" }, +]; +export const animeFormatOptions = [ + { name: "TV", value: "TV" }, + { name: "TV Short", value: "TV_SHORT" }, + { name: "Movie", value: "MOVIE" }, + { name: "Special", value: "SPECIAL" }, + { name: "OVA", value: "OVA" }, + { name: "ONA", value: "ONA" }, +]; +export const mangaFormatOptions = [ + { name: "Manga", value: "MANGA" }, + { name: "Novel", value: "NOVEL" }, + { name: "One Shot", value: "ONE_SHOT" }, +]; +export const sortOptions = [ + { name: "Date Added", value: "ID_DESC" }, + { name: "Title", value: "TITLE_ROMAJI" }, + { name: "Release Date", value: "START_DATE_DESC" }, + { name: "Average Score", value: "SCORE_DESC" }, + { name: "Popularity", value: "POPULARITY_DESC" }, + { name: "Trending", value: ["TRENDING_DESC", "POPULARITY_DESC"] }, + { name: "Favorites", value: "FAVOURITES_DESC" }, +]; +export const yearOptions = [ + { name: "1940", value: "1940" }, + { name: "1941", value: "1941" }, + { name: "1942", value: "1942" }, + { name: "1943", value: "1943" }, + { name: "1944", value: "1944" }, + { name: "1945", value: "1945" }, + { name: "1946", value: "1946" }, + { name: "1947", value: "1947" }, + { name: "1948", value: "1948" }, + { name: "1949", value: "1949" }, + { name: "1950", value: "1950" }, + { name: "1951", value: "1951" }, + { name: "1952", value: "1952" }, + { name: "1953", value: "1953" }, + { name: "1954", value: "1954" }, + { name: "1955", value: "1955" }, + { name: "1956", value: "1956" }, + { name: "1957", value: "1957" }, + { name: "1958", value: "1958" }, + { name: "1959", value: "1959" }, + { name: "1960", value: "1960" }, + { name: "1961", value: "1961" }, + { name: "1962", value: "1962" }, + { name: "1963", value: "1963" }, + { name: "1964", value: "1964" }, + { name: "1965", value: "1965" }, + { name: "1966", value: "1966" }, + { name: "1967", value: "1967" }, + { name: "1968", value: "1968" }, + { name: "1969", value: "1969" }, + { name: "1970", value: "1970" }, + { name: "1971", value: "1971" }, + { name: "1972", value: "1972" }, + { name: "1973", value: "1973" }, + { name: "1974", value: "1974" }, + { name: "1975", value: "1975" }, + { name: "1976", value: "1976" }, + { name: "1977", value: "1977" }, + { name: "1978", value: "1978" }, + { name: "1979", value: "1979" }, + { name: "1980", value: "1980" }, + { name: "1981", value: "1981" }, + { name: "1982", value: "1982" }, + { name: "1983", value: "1983" }, + { name: "1984", value: "1984" }, + { name: "1985", value: "1985" }, + { name: "1986", value: "1986" }, + { name: "1987", value: "1987" }, + { name: "1988", value: "1988" }, + { name: "1989", value: "1989" }, + { name: "1990", value: "1990" }, + { name: "1991", value: "1991" }, + { name: "1992", value: "1992" }, + { name: "1993", value: "1993" }, + { name: "1994", value: "1994" }, + { name: "1995", value: "1995" }, + { name: "1996", value: "1996" }, + { name: "1997", value: "1997" }, + { name: "1998", value: "1998" }, + { name: "1999", value: "1999" }, + { name: "2000", value: "2000" }, + { name: "2001", value: "2001" }, + { name: "2002", value: "2002" }, + { name: "2003", value: "2003" }, + { name: "2004", value: "2004" }, + { name: "2005", value: "2005" }, + { name: "2006", value: "2006" }, + { name: "2007", value: "2007" }, + { name: "2008", value: "2008" }, + { name: "2009", value: "2009" }, + { name: "2010", value: "2010" }, + { name: "2011", value: "2011" }, + { name: "2012", value: "2012" }, + { name: "2013", value: "2013" }, + { name: "2014", value: "2014" }, + { name: "2015", value: "2015" }, + { name: "2016", value: "2016" }, + { name: "2017", value: "2017" }, + { name: "2018", value: "2018" }, + { name: "2019", value: "2019" }, + { name: "2020", value: "2020" }, + { name: "2021", value: "2021" }, + { name: "2022", value: "2022" }, + { name: "2023", value: "2023" }, + { name: "2024", value: "2024" }, +]; +export const seasonOptions = [ + { name: "Winter", value: "WINTER" }, + { name: "Spring", value: "SPRING" }, + { name: "Summer", value: "SUMMER" }, + { name: "Fall", value: "FALL" }, +]; diff --git a/components/searchBar.js b/components/searchBar.js deleted file mode 100644 index 20d2d7c..0000000 --- a/components/searchBar.js +++ /dev/null @@ -1,155 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { motion as m, AnimatePresence } from "framer-motion"; -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -import { useAniList } from "../lib/anilist/useAnilist"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; - -const SearchBar = () => { - const [isOpen, setIsOpen] = useState(false); - const searchBoxRef = useRef(null); - - const router = useRouter(); - - const { aniAdvanceSearch } = useAniList(); - const [data, setData] = useState(null); - const [query, setQuery] = useState(""); - - const [lang, setLang] = useState("en"); - - useEffect(() => { - if (isOpen) { - searchBoxRef.current.querySelector("input").focus(); - } - const handleKeyDown = (e) => { - if (e.ctrlKey && e.code === "Space") { - setIsOpen((prev) => !prev); - setData(null); - setQuery(""); - } - }; - - document.addEventListener("keydown", handleKeyDown); - - const handleClick = (e) => { - if (searchBoxRef.current && !searchBoxRef.current.contains(e.target)) { - setIsOpen(false); - } - }; - document.addEventListener("click", handleClick); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("click", handleClick); - }; - }, [isOpen]); - - async function search() { - const data = await aniAdvanceSearch({ - search: query, - type: "ANIME", - perPage: 10, - }); - setData(data); - } - - useEffect(() => { - if (query) { - search(); - } - }, [query]); - - useEffect(() => { - const lang = localStorage.getItem("lang") || "id"; - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); - - function handleSubmit(e) { - e.preventDefault(); - if (data?.media.length) { - router.push(`${lang}/anime/${data?.media[0].id}`); - } - } - - return ( - <AnimatePresence> - {isOpen && ( - <m.div - initial={{ opacity: 0, y: -100 }} - animate={{ opacity: 1, y: 0 }} - exit={{ opacity: 0, y: -100 }} - className="fixed top-0 w-screen flex justify-center z-50" - > - <div - ref={searchBoxRef} - className={` bg-[#1c1c1fef] text-white p-4 ${ - isOpen ? "flex" : "hidden" - } flex-col w-[80%] backdrop-blur-sm rounded-b-lg`} - > - <form onSubmit={handleSubmit}> - <input - type="text" - className="w-full rounded-lg px-4 py-2 mb-2 bg-[#474747]" - placeholder="Search..." - onChange={(e) => setQuery(e.target.value)} - /> - </form> - <div className="flex flex-col gap-2 p-2 font-karla"> - {data?.media.map((i) => ( - <Link - key={i.id} - href={i.type === "ANIME" ? `${lang}/anime/${i.id}` : `/`} - className="flex hover:bg-[#3e3e3e] rounded-md" - > - <Image - src={i.coverImage.extraLarge} - alt="search results" - width={500} - height={500} - className="object-cover w-14 h-14 rounded-md" - /> - <div className="flex items-center justify-between w-full px-5"> - <div> - <h1>{i.title.userPreferred}</h1> - <h5 className="text-sm font-light text-[#878787] flex gap-2"> - {i.status - ?.toLowerCase() - .replace(/^\w/, (c) => c.toUpperCase())}{" "} - {i.status && i.season && <>·</>}{" "} - {i.season - ?.toLowerCase() - .replace(/^\w/, (c) => c.toUpperCase())}{" "} - {(i.status || i.season) && i.episodes && <>·</>}{" "} - {i.episodes || 0} Episodes - </h5> - </div> - <div className="text-sm text-[#b5b5b5] "> - <h1> - {i.type - ?.toLowerCase() - .replace(/^\w/, (c) => c.toUpperCase())} - </h1> - </div> - </div> - </Link> - ))} - </div> - {query && ( - <button className="flex items-center gap-2 justify-center"> - <MagnifyingGlassIcon className="h-5 w-5" /> - <Link href={`${lang}/search/${query}`}>More Results...</Link> - </button> - )} - </div> - </m.div> - )} - </AnimatePresence> - ); -}; - -export default SearchBar; diff --git a/components/searchPalette.js b/components/searchPalette.js new file mode 100644 index 0000000..07c8f89 --- /dev/null +++ b/components/searchPalette.js @@ -0,0 +1,265 @@ +import { Fragment, useEffect, useState } from "react"; +import { Combobox, Dialog, Menu, Transition } from "@headlessui/react"; +import useDebounce from "../lib/hooks/useDebounce"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useSearch } from "../lib/hooks/isOpenState"; +import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; +import { BookOpenIcon, PlayIcon } from "@heroicons/react/20/solid"; +import { useAniList } from "../lib/anilist/useAnilist"; +import { getFormat } from "../utils/getFormat"; + +export default function SearchPalette() { + const { isOpen, setIsOpen } = useSearch(); + const { quickSearch } = useAniList(); + + const [query, setQuery] = useState(""); + const [data, setData] = useState(null); + const debounceSearch = useDebounce(query, 500); + const [loading, setLoading] = useState(false); + const [type, setType] = useState("ANIME"); + + const [nextPage, setNextPage] = useState(false); + + const router = useRouter(); + + function closeModal() { + setIsOpen(false); + } + + function handleChange(event) { + router.push(`/en/${type.toLowerCase()}/${event}`); + } + + async function advance() { + setLoading(true); + const res = await quickSearch({ + search: debounceSearch, + type, + }); + setData(res?.data?.Page?.results); + setNextPage(res?.data?.Page?.pageInfo?.hasNextPage); + setLoading(false); + } + + useEffect(() => { + advance(); + }, [debounceSearch, type]); + + useEffect(() => { + const handleKeyDown = (e) => { + if (e.code === "KeyS" && e.ctrlKey) { + // do your stuff + e.preventDefault(); + setIsOpen((prev) => !prev); + setData(null); + setQuery(""); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + return ( + <Transition appear show={isOpen} as={Fragment}> + <Dialog as="div" className="relative z-[6969]" onClose={closeModal}> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black/90" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-4 text-center"> + <Transition.Child + as={Fragment} + enter="ease-out duration-200" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-100" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-2xl max-h-[68dvh] transform text-left transition-all"> + <Combobox + as="div" + className="max-w-2xl mx-auto rounded-lg shadow-2xl relative flex flex-col" + onChange={(e) => { + handleChange(e); + setData(null); + setIsOpen(false); + setQuery(""); + }} + > + <div className="flex justify-between py-1 font-karla"> + <div className="flex items-center px-2 gap-2"> + <p>For quick access :</p> + <div className="bg-secondary text-white text-xs font-bold px-2 py-1 rounded-md"> + <span>CTRL</span> + </div> + <span>+</span> + <div className="bg-secondary text-white text-xs font-bold px-2 py-1 rounded-md"> + <span>S</span> + </div> + </div> + <div> + <Menu + as="div" + className="relative inline-block text-left" + > + <div> + <Menu.Button className="capitalize bg-secondary inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-medium text-white hover:bg-opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"> + {type.toLowerCase()} + <ChevronDownIcon + className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100" + // aria-hidden="true" + /> + </Menu.Button> + </div> + <Transition + as={Fragment} + enter="transition ease-out duration-100" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + > + <Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-primary shadow ring-1 ring-black ring-opacity-5 focus:outline-none"> + <div className="px-1 py-1"> + <Menu.Item> + {({ active }) => ( + <button + onClick={() => setType("ANIME")} + className={`${ + active + ? "bg-secondary text-white" + : "text-white" + } group flex w-full items-center gap-3 rounded-md px-2 py-2 text-sm`} + > + <PlayIcon className="w-6 h-6" /> + <span>Anime</span> + </button> + )} + </Menu.Item> + <Menu.Item> + {({ active }) => ( + <button + onClick={() => setType("MANGA")} + className={`${ + active + ? "bg-secondary text-white" + : "text-white" + } group flex w-full items-center gap-3 rounded-md px-2 py-2 text-sm`} + > + <BookOpenIcon className="w-6 h-6" /> + <span>Manga</span> + </button> + )} + </Menu.Item> + </div> + </Menu.Items> + </Transition> + </Menu> + </div> + </div> + <div className="flex items-center text-base font-medium rounded bg-secondary"> + <Combobox.Input + className="p-5 text-white w-full bg-transparent border-0 outline-none" + placeholder="Search something..." + onChange={(event) => setQuery(event.target.value)} + /> + </div> + <Combobox.Options + static + className="bg-secondary rounded mt-2 max-h-[50dvh] overflow-y-auto flex flex-col scrollbar-thin scrollbar-thumb-primary scrollbar-thumb-rounded" + > + {!loading ? ( + <Fragment> + {data?.length > 0 + ? data?.map((i) => ( + <Combobox.Option + key={i.id} + value={i.id} + className={({ active }) => + `flex items-center gap-3 p-5 ${ + active ? "bg-primary/40 cursor-pointer" : "" + }` + } + > + <div className="shrink-0"> + <Image + src={i.coverImage.medium} + alt="coverImage" + width={100} + height={100} + className="w-16 h-16 object-cover rounded" + /> + </div> + <div className="flex flex-col w-full h-full"> + <h3 className="font-karla font-semibold"> + {i.title.userPreferred} + </h3> + <p className="text-sm text-white/50"> + {i.startDate.year} {getFormat(i.format)} + </p> + </div> + </Combobox.Option> + )) + : !loading && + debounceSearch !== "" && ( + <p className="flex-center font-karla gap-3 p-5"> + No results found. + </p> + )} + {nextPage && ( + <button + type="button" + onClick={() => { + router.push( + `/en/search/${type.toLowerCase()}/${ + query !== "" ? `?search=${query}` : "" + }` + ); + setIsOpen(false); + setQuery(""); + }} + className="flex-center font-karla gap-2 py-4 hover:bg-primary/30 cursor-pointer" + > + <span>View More</span> + <ChevronRightIcon className="w-4 h-4" /> + </button> + )} + </Fragment> + ) : ( + <div className="flex-center gap-3 p-5"> + <div className="flex justify-center"> + <div className="lds-ellipsis"> + <span></span> + <span></span> + <span></span> + <span></span> + </div> + </div> + </div> + )} + </Combobox.Options> + </Combobox> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + ); +} diff --git a/components/shared/MobileNav.js b/components/shared/MobileNav.js new file mode 100644 index 0000000..6dd1e64 --- /dev/null +++ b/components/shared/MobileNav.js @@ -0,0 +1,170 @@ +import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; +import { CalendarIcon, ClockIcon, HomeIcon } from "@heroicons/react/24/outline"; +import { signIn, signOut } from "next-auth/react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useState } from "react"; + +export default function MobileNav({ sessions, hideProfile = false }) { + const [isVisible, setIsVisible] = useState(false); + + const handleShowClick = () => { + setIsVisible(true); + }; + + const handleHideClick = () => { + setIsVisible(false); + }; + return ( + <> + {/* NAVBAR */} + <div className="z-[1000]"> + {!isVisible && ( + <button + onClick={handleShowClick} + className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" + id="bars" + > + <svg + xmlns="http://www.w3.org/2000/svg" + className="h-[42px] w-[61.5px] text-white/60 fill-orange-500" + viewBox="0 0 20 20" + fill="currentColor" + > + <path + fillRule="evenodd" + d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" + clipRule="evenodd" + /> + </svg> + </button> + )} + </div> + + {/* Mobile Menu */} + <div + className={`transition-all duration-150 subpixel-antialiased z-[500]`} + > + {isVisible && sessions && !hideProfile && ( + <Link + href={`/en/profile/${sessions?.user.name}`} + className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]" + > + <Image + src={sessions?.user.image.large} + alt="user avatar" + width={60} + height={60} + className="object-cover w-[60px] h-[60px] rounded-full" + /> + </Link> + )} + {isVisible && ( + <div className="fixed bottom-[30px] right-[20px] z-[500] flex h-[51px] px-5 items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden"> + <div className="flex items-center gap-5"> + <button className="group flex flex-col items-center"> + <Link href="/en/"> + <HomeIcon className="w-6 h-6 group-hover:text-action" /> + </Link> + <Link + href="/en/" + className="font-karla font-bold text-white/60 group-hover:text-action" + > + home + </Link> + </button> + <button className="group flex flex-col items-center gap-[1px]"> + <Link href="/en/schedule"> + <CalendarIcon className="w-6 h-6 group-hover:text-action" /> + </Link> + <Link + href="/en/schedule" + className="font-karla font-bold text-white/60 group-hover:text-action" + > + schedule + </Link> + </button> + <button className="group flex gap-[1px] flex-col items-center"> + <Link href="/en/search/anime"> + <MagnifyingGlassIcon className="w-6 h-6 group-hover:text-action" /> + </Link> + + <Link + href="/en/search/anime" + className="font-karla font-bold text-white/60 group-hover:text-action" + > + search + </Link> + </button> + {sessions ? ( + <button + onClick={() => signOut("AniListProvider")} + className="group flex gap-[1.5px] flex-col items-center " + > + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 96 960 960" + className="group-hover:fill-action w-6 h-6 fill-txt" + > + <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path> + </svg> + </div> + <h1 className="font-karla font-bold text-white/60 group-hover:text-action"> + logout + </h1> + </button> + ) : ( + <button + onClick={() => signIn("AniListProvider")} + className="group flex gap-[1.5px] flex-col items-center " + > + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 96 960 960" + className="group-hover:fill-action w-6 h-6 fill-txt mr-2" + > + <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path> + </svg> + </div> + <h1 className="font-karla font-bold text-white/60 group-hover:text-action"> + login + </h1> + </button> + )} + </div> + <button onClick={handleHideClick}> + <svg + width="20" + height="21" + className="fill-orange-500" + viewBox="0 0 20 21" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <rect + x="2.44043" + y="0.941467" + width="23.5842" + height="3.45134" + rx="1.72567" + transform="rotate(45 2.44043 0.941467)" + /> + <rect + x="19.1172" + y="3.38196" + width="23.5842" + height="3.45134" + rx="1.72567" + transform="rotate(135 19.1172 3.38196)" + /> + </svg> + </button> + </div> + )} + </div> + </> + ); +} diff --git a/components/home/mobileNav.js b/components/shared/hamburgerMenu.js index 52c9d52..7e4bdf1 100644 --- a/components/home/mobileNav.js +++ b/components/shared/hamburgerMenu.js @@ -1,62 +1,52 @@ -import { signIn, signOut } from "next-auth/react"; +import { signIn, signOut, useSession } from "next-auth/react"; +import Image from "next/image"; import Link from "next/link"; -import { useState } from "react"; +import React, { useState } from "react"; -export default function MobileNav({ sessions }) { +export default function HamburgerMenu() { + const { data: session } = useSession(); const [isVisible, setIsVisible] = useState(false); + const [fade, setFade] = useState(false); const handleShowClick = () => { setIsVisible(true); + setFade(true); }; const handleHideClick = () => { setIsVisible(false); + setFade(false); }; - return ( - <> - {/* NAVBAR */} - <div className="z-50"> - {!isVisible && ( - <button - onClick={handleShowClick} - className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" - id="bars" - > - <svg - xmlns="http://www.w3.org/2000/svg" - className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500" - viewBox="0 0 20 20" - fill="currentColor" - > - <path - fillRule="evenodd" - d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" - clipRule="evenodd" - /> - </svg> - </button> - )} - </div> - {/* Mobile Menu */} - <div className={`transition-all duration-150 subpixel-antialiased z-50`}> - {isVisible && sessions && ( - <Link - href={`/en/profile/${sessions?.user.name}`} - className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]" + return ( + <React.Fragment> + {/* Mobile Hamburger */} + {!isVisible && ( + <button + onClick={handleShowClick} + className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden" + id="bars" + > + <svg + xmlns="http://www.w3.org/2000/svg" + className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500" + viewBox="0 0 20 20" + fill="currentColor" > - <img - src={sessions?.user.image.large} - alt="user avatar" - className="object-cover w-[60px] h-[60px] rounded-full" + <path + fillRule="evenodd" + d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" + clipRule="evenodd" /> - </Link> - )} + </svg> + </button> + )} + <div className={`z-50`}> {isVisible && ( <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden"> <div className="grid grid-cols-4 place-items-center gap-6"> <button className="group flex flex-col items-center"> - <Link href="/en/"> + <Link href={`/en/`} className=""> <svg xmlns="http://www.w3.org/2000/svg" fill="none" @@ -73,14 +63,14 @@ export default function MobileNav({ sessions }) { </svg> </Link> <Link - href="/en/" + href={`/en/`} className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" > home </Link> </button> <button className="group flex flex-col items-center"> - <Link href="/en/about"> + <Link href={`/en/about`}> <svg xmlns="http://www.w3.org/2000/svg" fill="none" @@ -97,7 +87,7 @@ export default function MobileNav({ sessions }) { </svg> </Link> <Link - href="/en/about" + href={`/en/about`} className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" > about @@ -105,7 +95,7 @@ export default function MobileNav({ sessions }) { </button> <button className="group flex gap-[1.5px] flex-col items-center "> <div> - <Link href="/en/search/anime"> + <Link href={`/en/search/anime`}> <svg xmlns="http://www.w3.org/2000/svg" fill="none" @@ -123,13 +113,13 @@ export default function MobileNav({ sessions }) { </Link> </div> <Link - href="/en/search/anime" + href={`/en/search/anime`} className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" > search </Link> </button> - {sessions ? ( + {session ? ( <button onClick={() => signOut("AniListProvider")} className="group flex gap-[1.5px] flex-col items-center " @@ -197,6 +187,6 @@ export default function MobileNav({ sessions }) { </div> )} </div> - </> + </React.Fragment> ); } diff --git a/components/shared/loading.js b/components/shared/loading.js new file mode 100644 index 0000000..4620645 --- /dev/null +++ b/components/shared/loading.js @@ -0,0 +1,20 @@ +import Image from "next/image"; + +export default function Loading() { + return ( + <> + <div className="flex flex-col gap-5 items-center justify-center w-full z-[800]"> + {/* <Image + src="/wait-animation.gif" + width="0" + height="0" + className="w-[30%] h-[30%]" + /> */} + <div className="flex flex-col items-center font-karla gap-2"> + <p>Please Wait...</p> + <div className="loader"></div> + </div> + </div> + </> + ); +} diff --git a/components/videoPlayer.js b/components/videoPlayer.js index dcde703..a961c1b 100644 --- a/components/videoPlayer.js +++ b/components/videoPlayer.js @@ -26,7 +26,6 @@ export default function VideoPlayer({ progress, session, aniId, - stats, skip, title, poster, @@ -86,12 +85,10 @@ export default function VideoPlayer({ return { ...(isDefault && { default: true }), html: items.quality === "default" ? "adaptive" : items.quality, - url: - provider === "gogoanime" - ? `https://cors.moopa.workers.dev/?url=${encodeURIComponent( - items.url - )}${referer ? `&referer=${encodeURIComponent(referer)}` : ""}` - : `${proxy}${items.url}`, + // url: `https://cors.moopa.live/${items.url}`, + url: `${proxy}?url=${encodeURIComponent(items.url)}${ + referer ? `&referer=${encodeURIComponent(referer)}` : "" + }`, }; }); @@ -136,7 +133,7 @@ export default function VideoPlayer({ option={{ url: `${url}`, title: `${title}`, - autoplay: false, + autoplay: true, screenshot: true, moreVideoAttr: { crossOrigin: "anonymous", @@ -225,16 +222,19 @@ export default function VideoPlayer({ name: session?.user?.name, id: String(aniId), watchId: id, - title: track?.playing?.title || aniTitle, + title: track.playing?.title || aniTitle, aniTitle: aniTitle, - image: track?.playing?.image || info?.coverImage?.extraLarge, + image: track.playing?.image || info?.coverImage?.extraLarge, number: Number(progress), duration: art.duration, timeWatched: art.currentTime, provider: provider, + nextId: track.next?.id, + nextNumber: Number(track.next?.number), + dub: dub ? true : false, }), }); - // console.log("updating db"); + // console.log("updating db", { track }); }, 5000); art.on("video:pause", () => { @@ -263,6 +263,9 @@ export default function VideoPlayer({ duration: art.duration, timeWatched: art.currentTime, provider: provider, + nextId: track?.next?.id, + nextNumber: track?.next?.number, + dub: dub ? true : false, createdAt: new Date().toISOString(), }); }, 5000); @@ -297,7 +300,7 @@ export default function VideoPlayer({ // use >= instead of > if (marked < 1) { marked = 1; - markProgress(aniId, progress, stats); + markProgress(aniId, progress); } } }); |