diff options
Diffstat (limited to 'pages/id')
| -rw-r--r-- | pages/id/about.js | 57 | ||||
| -rw-r--r-- | pages/id/anime/[...id].js | 850 | ||||
| -rw-r--r-- | pages/id/anime/watch/[...info].js | 488 | ||||
| -rw-r--r-- | pages/id/contact.js | 19 | ||||
| -rw-r--r-- | pages/id/dmca.js | 109 | ||||
| -rw-r--r-- | pages/id/index.js | 633 | ||||
| -rw-r--r-- | pages/id/profile/[user].js | 423 | ||||
| -rw-r--r-- | pages/id/search/[param].js | 493 |
8 files changed, 3072 insertions, 0 deletions
diff --git a/pages/id/about.js b/pages/id/about.js new file mode 100644 index 0000000..9bd32ed --- /dev/null +++ b/pages/id/about.js @@ -0,0 +1,57 @@ +import Head from "next/head"; +import Layout from "../../components/layout"; +import { motion } from "framer-motion"; +import Link from "next/link"; + +export default function About() { + return ( + <> + <Head> + <title>Moopa - About</title> + <meta name="about" content="About this web" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" href="/c.svg" /> + </Head> + <Layout> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + className="flex flex-col justify-center items-center min-h-screen md:py-0 py-16" + > + <div className="max-w-screen-lg w-full px-4 py-10"> + <h1 className="text-4xl font-bold mb-6">About Us</h1> + <p className="text-lg mb-8"> + Moopa is a platform where you can watch and stream anime or read + manga for free, without any ads or VPNs. Our mission is to provide + a convenient and enjoyable experience for anime and manga + enthusiasts all around the world. + </p> + <p className="text-lg mb-8"> + At our site, you will find a vast collection of anime and manga + titles from different genres, including action, adventure, comedy, + romance, and more. We take pride in our fast and reliable servers, + which ensure smooth streaming and reading for all our users. + </p> + <p className="text-lg mb-8"> + We believe that anime and manga have the power to inspire and + entertain people of all ages and backgrounds. Our service is + designed to make it easy for fans to access the content they love, + whether they are casual viewers or die-hard fans. + </p> + <p className="text-lg mb-8"> + Thank you for choosing our website as your go-to platform for + anime and manga. We hope you enjoy your stay here, and feel free + to contact us if you have any feedback or suggestions. + </p> + <Link href="/en/contact"> + <div className="bg-[#ffffff] text-black font-medium py-3 px-6 rounded-lg hover:bg-action transition duration-300 ease-in-out"> + Contact Us + </div> + </Link> + </div> + </motion.div> + </Layout> + </> + ); +} diff --git a/pages/id/anime/[...id].js b/pages/id/anime/[...id].js new file mode 100644 index 0000000..8a52e4b --- /dev/null +++ b/pages/id/anime/[...id].js @@ -0,0 +1,850 @@ +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; + +import { + ChevronDownIcon, + ClockIcon, + HeartIcon, +} from "@heroicons/react/20/solid"; +import { + TvIcon, + ArrowTrendingUpIcon, + RectangleStackIcon, +} from "@heroicons/react/24/outline"; + +import Head from "next/head"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useEffect, useRef, useState } from "react"; +import Layout from "../../../components/layout"; +import Link from "next/link"; +import Content from "../../../components/home/content"; +import Modal from "../../../components/modal"; + +import { signIn, useSession } from "next-auth/react"; +import AniList from "../../../components/media/aniList"; +import ListEditor from "../../../components/listEditor"; + +import { GET_MEDIA_USER } from "../../../queries"; +import { GET_MEDIA_INFO } from "../../../queries"; +import { closestMatch } from "closest-match"; + +// import { aniInfo } from "../../components/devComp/data"; +// console.log(GET_MEDIA_USER); + +export default function Info({ info, color, api }) { + // Episodes dropdown + const [firstEpisodeIndex, setFirstEpisodeIndex] = useState(0); + const [lastEpisodeIndex, setLastEpisodeIndex] = useState(); + const [selectedRange, setSelectedRange] = useState("All"); + function onEpisodeIndexChange(e) { + if (e.target.value === "All") { + setFirstEpisodeIndex(0); + setLastEpisodeIndex(); + setSelectedRange("All"); + return; + } + setFirstEpisodeIndex(e.target.value.split("-")[0] - 1); + setLastEpisodeIndex(e.target.value.split("-")[1]); + setSelectedRange(e.target.value); + } + + const { data: session } = useSession(); + const [episode, setEpisode] = useState(null); + const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState(0); + const [statuses, setStatuses] = useState(null); + const [domainUrl, setDomainUrl] = useState(""); + const [showAll, setShowAll] = useState(false); + const [visible, setVisible] = useState(false); + const [open, setOpen] = useState(false); + const [time, setTime] = useState(0); + const { id } = useRouter().query; + + const [fetchFailed, setFetchFailed] = useState(false); + const failedAttempts = useRef(0); + + const [artStorage, setArtStorage] = useState(null); + + const rec = info?.recommendations?.nodes?.map( + (data) => data.mediaRecommendation + ); + + const [log, setLog] = useState(); + + //for episodes dropdown + useEffect(() => { + setFirstEpisodeIndex(0); + setLastEpisodeIndex(); + setSelectedRange("All"); + }, [info]); + + useEffect(() => { + handleClose(); + async function fetchData() { + setLoading(true); + if (id) { + const { protocol, host } = window.location; + const url = `${protocol}//${host}`; + + setDomainUrl(url); + + setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); + + setEpisode(null); + setProgress(0); + setStatuses(null); + + try { + const res1 = await Promise.race([ + fetch( + `https://ani-indo.vercel.app/get/search?q=${encodeURIComponent( + info.title.romaji + )}` + ), + new Promise((_, reject) => + setTimeout(() => reject(new Error("timeout")), 10000) + ), + ]); + + const data1 = await res1.json(); + if (data1.data.length === 0) { + let text = info.title.romaji; + let words = text.split(" "); + let firstTwoWords = words.slice(0, 2).join(" "); + + setLog(firstTwoWords); + const anotherRes = await Promise.race([ + fetch( + `https://ani-indo.vercel.app/get/search?q=${firstTwoWords}` + ), + new Promise((_, reject) => + setTimeout(() => reject(new Error("timeout")), 10000) + ), + ]); + const fallbackData = await anotherRes.json(); + + const title = fallbackData.data.map((i) => i.title); + const match = closestMatch(info.title.romaji, title); + if (match) { + const getAnime = fallbackData.data.find((i) => i.title === match); + const res2 = await fetch( + `https://ani-indo.vercel.app/get/info/${getAnime.animeId}` + ); + const data2 = await res2.json(); + if (data2.status === "success") { + setEpisode(data2.data[0].episode); + } + // setLog(data2); + } else { + setLoading(false); + } + } + if (data1.status === "success") { + const title = data1.data.map((i) => i.title); + const match = closestMatch(info.title.romaji, title); + if (match) { + const getAnime = data1.data.find((i) => i.title === match); + const res2 = await fetch( + `https://ani-indo.vercel.app/get/info/${getAnime.animeId}` + ); + const data2 = await res2.json(); + if (data2.status === "success") { + setEpisode(data2.data[0].episode); + } + // setLog(data2); + } else { + setLoading(false); + } + // setLog(match); + } + // setLog(data1); + + if (session?.user?.name) { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: GET_MEDIA_USER, + variables: { + username: session?.user?.name, + }, + }), + }); + + const responseData = await response.json(); + + const prog = responseData?.data?.MediaListCollection; + + if (prog && prog.lists.length > 0) { + const gut = prog.lists + .flatMap((item) => item.entries) + .find((item) => item.mediaId === parseInt(id[0])); + + if (gut) { + setProgress(gut.progress); + const statusMapping = { + CURRENT: { name: "Watching", value: "CURRENT" }, + PLANNING: { name: "Plan to watch", value: "PLANNING" }, + COMPLETED: { name: "Completed", value: "COMPLETED" }, + DROPPED: { name: "Dropped", value: "DROPPED" }, + PAUSED: { name: "Paused", value: "PAUSED" }, + REPEATING: { name: "Rewatching", value: "REPEATING" }, + }; + setStatuses(statusMapping[gut.status]); + } + } + setLoading(false); + } + + if (info.nextAiringEpisode) { + setTime( + convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring) + ); + } + } catch (error) { + if (error.message === "timeout") { + const currentAttempts = + parseInt(localStorage.getItem("failedAttempts") || "0", 10) + 1; + localStorage.setItem("failedAttempts", currentAttempts.toString()); + + if (currentAttempts < 3) { + window.location.reload(); + } else { + localStorage.removeItem("failedAttempts"); + setFetchFailed(true); + } + } else { + console.error(error); + } + } + } + setLoading(false); + } + fetchData(); + }, [id, info, session?.user?.name]); + + function handleOpen() { + setOpen(true); + document.body.style.overflow = "hidden"; + } + + function handleClose() { + setOpen(false); + document.body.style.overflow = "auto"; + } + + return ( + <> + <Head> + <title> + {info + ? info?.title?.romaji || info?.title?.english + : "Retrieving Data..."} + </title> + <meta name="twitter:card" content="summary_large_image" /> + <meta + name="twitter:title" + content={`Moopa - ${info.title.romaji || info.title.english}`} + /> + <meta + name="twitter:description" + content={`${info.description?.slice(0, 180)}...`} + /> + <meta + name="twitter:image" + content={`${domainUrl}/api/og?title=${ + info.title.romaji || info.title.english + }&image=${info.bannerImage || info.coverImage.extraLarge}`} + /> + </Head> + <Modal open={open} onClose={() => handleClose()}> + <div> + {!session && ( + <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> + <h1 className="text-md font-extrabold font-karla"> + Edit your list + </h1> + <button + className="flex items-center bg-[#363642] rounded-md text-white p-1" + onClick={() => signIn("AniListProvider")} + > + <h1 className="px-1 font-bold font-karla"> + Login with AniList + </h1> + <div className="scale-[60%] pb-[1px]"> + <AniList /> + </div> + </button> + </div> + )} + {session && info && ( + <ListEditor + animeId={info?.id} + session={session} + stats={statuses} + prg={progress} + max={info?.episodes} + image={info} + /> + )} + </div> + </Modal> + <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> + <Layout navTop="text-white bg-primary lg:pt-0 lg:px-0 bg-slate bg-opacity-40 z-50"> + <div className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5"> + <div className="bg-image w-screen"> + <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[300px] w-screen z-10 inset-0" /> + {info ? ( + <Image + src={ + info?.bannerImage || + info?.coverImage?.extraLarge || + info?.coverImage.large + } + priority={true} + alt="banner anime" + height={1000} + width={1000} + className="object-cover bg-image w-screen absolute top-0 left-0 h-[300px] brightness-[70%] z-0" + /> + ) : ( + <div className="bg-image w-screen absolute top-0 left-0 h-[300px]" /> + )} + </div> + <div className="lg:w-[90%] xl:w-[75%] lg:pt-[10rem] z-30 flex flex-col gap-5"> + {/* Mobile */} + + <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" + > + <span className="">{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"> + <button + type="button" + className="bg-action px-10 rounded-sm font-karla font-bold" + onClick={() => handleOpen()} + > + {!loading + ? statuses + ? statuses.name + : "Add to List" + : "Loading..."} + </button> + <div className="h-6 w-6"> + <HeartIcon /> + </div> + </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}%</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>TBA</h1> + )} + </div> + </> + ) : ( + <div>{info && "Not Yet Released"}</div> + )} + </div> + </div> + </div> + + {/* PC */} + <div className="hidden lg:flex gap-8 w-full flex-nowrap"> + <div className="shrink-0 lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] relative"> + {info ? ( + <> + <div className="bg-image lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] bg-opacity-30 absolute backdrop-blur-lg z-10 -top-7" /> + <Image + src={ + info.coverImage.extraLarge || info.coverImage.large + } + priority={true} + alt="poster anime" + height={700} + width={700} + className="object-cover lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] z-20 absolute rounded-md -top-7" + /> + <button + type="button" + className="bg-action flex-center z-20 h-[20px] w-[180px] absolute bottom-0 rounded-sm font-karla font-bold" + onClick={() => handleOpen()} + > + {!loading + ? statuses + ? statuses.name + : "Add to List" + : "Loading..."} + </button> + </> + ) : ( + <Skeleton className="h-[250px] w-[180px]" /> + )} + </div> + + {/* PC */} + <div className="hidden lg:flex w-full flex-col gap-5 h-[250px]"> + <div className="flex flex-col gap-2"> + <h1 className=" font-inter font-bold text-[36px] text-white line-clamp-1"> + {info ? ( + info?.title?.romaji || info?.title?.english + ) : ( + <Skeleton width={450} /> + )} + </h1> + {info ? ( + <div className="flex gap-6"> + {info?.episodes && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.episodes} Episodes + </div> + )} + {info?.startDate?.year && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.startDate?.year} + </div> + )} + {info?.averageScore && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.averageScore}% + </div> + )} + {info?.type && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.type} + </div> + )} + {info?.status && ( + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + {info?.status} + </div> + )} + <div + className={`dynamic-text rounded-md px-2 font-karla font-bold`} + style={color} + > + Sub | EN + </div> + </div> + ) : ( + <Skeleton width={240} height={32} /> + )} + </div> + {info ? ( + <p + dangerouslySetInnerHTML={{ __html: info?.description }} + className="overflow-y-scroll scrollbar-thin pr-2 scrollbar-thumb-secondary scrollbar-thumb-rounded-lg h-[140px]" + /> + ) : ( + <Skeleton className="h-[130px]" /> + )} + </div> + </div> + + <div> + <div className="flex gap-5 items-center"> + {info?.relations?.edges?.length > 0 && ( + <div className="p-3 lg:p-0 text-[20px] lg:text-2xl font-bold font-karla"> + Relations + </div> + )} + {info?.relations?.edges?.length > 3 && ( + <div + className="cursor-pointer" + onClick={() => setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} + </div> + )} + </div> + <div + className={`w-screen lg:w-full grid lg:grid-cols-3 justify-items-center gap-7 lg:pt-7 lg:pb-5 px-3 lg:px-4 pt-4 rounded-xl`} + > + {info?.relations?.edges ? ( + 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" + ? `/id/anime/${rel.id}` + : `/manga/detail/id?aniId=${ + rel.id + }&aniTitle=${encodeURIComponent( + info?.title?.english || + info?.title.romaji || + info?.title.native + )}` + } + className={`hover:scale-[1.02] hover:shadow-lg lg:px-0 px-4 scale-100 transition-transform duration-200 ease-out w-full ${ + rel.type === "MUSIC" ? "pointer-events-none" : "" + }`} + > + <div + key={rel.id} + className="w-full shrink h-[126px] bg-secondary flex rounded-md" + > + <div className="w-[90px] bg-image rounded-l-md shrink-0"> + <Image + src={ + rel.coverImage.extraLarge || + rel.coverImage.large + } + 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"> + {r.relationType} + </div> + <div className="font-outfit font-thin line-clamp-2"> + {rel.title.userPreferred || rel.title.romaji} + </div> + <div className={``}>{rel.type}</div> + </div> + </div> + </Link> + ); + }) + ) : ( + <> + {[1, 2, 3].map((item) => ( + <div key={item} className="w-full hidden lg:block"> + <Skeleton className="h-[126px]" /> + </div> + ))} + <div className="w-full lg:hidden"> + <Skeleton className="h-[126px]" /> + </div> + </> + )} + </div> + </div> + <div className="flex flex-col gap-5 lg:gap-10 p-3 lg:p-0"> + <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"> + {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"> + {time} + </div> + </div> + <div className="h-6 w-6"> + <ClockIcon /> + </div> + </div> + )} + </div> + {episode?.length > 50 && ( + <div + className="lg:hidden bg-secondary p-1 rounded-md cursor-pointer" + onClick={() => setVisible(!visible)} + > + <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" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" + /> + </svg> + </div> + )} + </div> + {episode?.length > 50 && ( + <div + className={`flex lg:flex items-center gap-0 lg:gap-5 justify-between ${ + visible ? "" : "hidden" + }`} + > + <div className="flex items-end gap-3"> + {episode?.length > 50 && ( + <div className="relative flex gap-2 items-center"> + <p className="hidden md:block">Episodes</p> + <select + onChange={onEpisodeIndexChange} + value={selectedRange} + className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus:ring-1 focus:ring-action scrollbar-thin scrollbar-thumb-secondary scrollbar-thumb-rounded-lg" + > + <option value="All">All</option> + {[...Array(Math.ceil(episode?.length / 50))].map( + (_, index) => { + const start = index * 50 + 1; + const end = Math.min( + start + 50 - 1, + episode?.length + ); + const optionLabel = `${start} to ${end}`; + if (episode[0]?.number !== 1) { + var valueLabel = `${ + episode.length - end + 1 + }-${episode.length - start + 1}`; + } else { + var valueLabel = `${start}-${end}`; + } + return ( + <option key={valueLabel} value={valueLabel}> + {optionLabel} + </option> + ); + } + )} + </select> + <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + )} + </div> + </div> + )} + </div> + {!loading ? ( + Array.isArray(episode) ? ( + episode && ( + <div className="scrollbar-thin scrollbar-thumb-[#1b1c21] scrollbar-thumb-rounded-full overflow-y-scroll hover:scrollbar-thumb-[#2e2f37] h-[640px]"> + {episode?.length !== 0 && episode ? ( + <div + className={`flex flex-col gap-5 pb-5 pt-2 lg:pt-0`} + > + {episode + .slice(firstEpisodeIndex, lastEpisodeIndex) + .map((epi, index) => { + return ( + <div + key={index} + className="flex flex-col gap-3 px-2" + > + <Link + href={`/id/anime/watch/${info.id}/${epi.episodeId}`} + className={`text-start text-sm lg:text-lg ${ + progress && index <= progress - 1 + ? "text-[#5f5f5f]" + : "text-white" + }`} + > + <p>{epi.epsTitle}</p> + </Link> + {index !== episode?.length - 1 && ( + <span className="h-[1px] bg-white" /> + )} + </div> + ); + })} + </div> + ) : ( + <p>No Episodes Available</p> + )} + </div> + ) + ) : ( + <div className="flex flex-col"> + <pre + className={`rounded-md overflow-hidden ${getLanguageClassName( + "bash" + )}`} + > + <code> + {episode?.message || "Anime tidak tersedia :/"} + </code> + </pre> + </div> + ) + ) : ( + <div className="flex justify-center"> + <div className="lds-ellipsis"> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + </div> + )} + </div> + </div> + {info && rec?.length !== 0 && ( + <div className="w-screen lg:w-[90%] xl:w-[85%]"> + <Content + ids="recommendAnime" + section="Recommendations" + data={rec} + /> + </div> + )} + </div> + </Layout> + </SkeletonTheme> + </> + ); +} + +export async function getServerSideProps(context) { + const { id } = context.query; + const API_URI = process.env.API_URI; + + const res = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: GET_MEDIA_INFO, + variables: { + id: id?.[0], + }, + }), + }); + + const json = await res.json(); + const data = json?.data?.Media; + + if (!data) { + return { + notFound: true, + }; + } + + const textColor = setTxtColor(data?.coverImage?.color); + + const color = { + backgroundColor: `${data?.coverImage?.color || "#ffff"}`, + color: textColor, + }; + + return { + props: { + info: data, + color: color, + api: API_URI, + }, + }; +} + +function convertSecondsToTime(sec) { + let days = Math.floor(sec / (3600 * 24)); + let hours = Math.floor((sec % (3600 * 24)) / 3600); + let minutes = Math.floor((sec % 3600) / 60); + + let time = ""; + + if (days > 0) { + time += `${days}d `; + } + + if (hours > 0) { + time += `${hours}h `; + } + + if (minutes > 0) { + time += `${minutes}m `; + } + + return time.trim(); +} + +function getBrightness(hexColor) { + if (!hexColor) { + return 200; + } + const rgb = hexColor + .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) + .slice(1) + .map((x) => parseInt(x, 16)); + return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000; +} + +function setTxtColor(hexColor) { + const brightness = getBrightness(hexColor); + return brightness < 150 ? "#fff" : "#000"; +} + +const getLanguageClassName = (language) => { + switch (language) { + case "javascript": + return "language-javascript"; + case "html": + return "language-html"; + case "bash": + return "language-bash"; + // add more languages here as needed + default: + return ""; + } +}; diff --git a/pages/id/anime/watch/[...info].js b/pages/id/anime/watch/[...info].js new file mode 100644 index 0000000..89fd3a6 --- /dev/null +++ b/pages/id/anime/watch/[...info].js @@ -0,0 +1,488 @@ +import Image from "next/image"; +import Link from "next/link"; +import Head from "next/head"; +import { useEffect, useState } from "react"; +import dynamic from "next/dynamic"; + +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../../../api/auth/[...nextauth]"; + +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; + +import { Navigasi } from "../.."; +import { ChevronDownIcon, ForwardIcon } from "@heroicons/react/24/solid"; +import { useRouter } from "next/router"; + +import { GET_MEDIA_USER } from "../../../../queries"; + +import dotenv from "dotenv"; + +import VideoPlayer from "../../../../components/id-components/player/VideoPlayerId"; + +export default function Info({ sessions, id, aniId, provider, api, proxy }) { + const [epiData, setEpiData] = useState(null); + const [data, setAniData] = useState(null); + const [episode, setEpisode] = useState(null); + const [skip, setSkip] = useState({ op: null, ed: null }); + const [statusWatch, setStatusWatch] = useState("CURRENT"); + const [playingEpisode, setPlayingEpisode] = useState(null); + const [loading, setLoading] = useState(false); + const [playingTitle, setPlayingTitle] = useState(null); + const [poster, setPoster] = useState(null); + const [progress, setProgress] = useState(0); + const [currentNumber, setCurrentNumber] = useState(null); + + const [episodes, setEpisodes] = useState([]); + const [artStorage, setArtStorage] = useState(null); + + const router = useRouter(); + + useEffect(() => { + const defaultState = { + epiData: null, + skip: { op: null, ed: null }, + statusWatch: "CURRENT", + playingEpisode: null, + loading: false, + }; + + // Reset all state variables to their default values + Object.keys(defaultState).forEach((key) => { + const value = defaultState[key]; + if (Array.isArray(value)) { + value.length + ? eval( + `set${ + key.charAt(0).toUpperCase() + key.slice(1) + }(${JSON.stringify(value)})` + ) + : eval(`set${key.charAt(0).toUpperCase() + key.slice(1)}([])`); + } else { + eval( + `set${key.charAt(0).toUpperCase() + key.slice(1)}(${JSON.stringify( + value + )})` + ); + } + }); + + const fetchData = async () => { + let currentNumber = null; + try { + const res = await fetch( + `https://ani-indo.vercel.app/get/watch/${aniId}` + ); + const epiData = await res.json(); + currentNumber = epiData.episodeActive; + setCurrentNumber(currentNumber); + setEpisode(epiData.data); + setEpiData(epiData.episodeUrl); + } catch (error) { + setTimeout(() => { + window.location.reload(); + }, 3000); + } + + let aniData = null; + setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); + + const res2 = await fetch(`${api}/meta/anilist/info/${id}`); + aniData = await res2.json(); + setEpisodes(aniData.episodes?.reverse()); + setAniData(aniData); + + let playingEpisode = aniData.episodes + .filter((item) => item.number == currentNumber) + .map((item) => item.number); + + setPlayingEpisode(playingEpisode); + + const playing = aniData.episodes.filter((item) => item.id == id); + + setPoster(playing); + + const title = aniData.episodes + .filter((item) => item.id == id) + .find((item) => item.title !== null); + setPlayingTitle( + title?.title || aniData.title?.romaji || aniData.title?.english + ); + + const res4 = await fetch( + `https://api.aniskip.com/v2/skip-times/${aniData.malId}/${parseInt( + playingEpisode + )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=` + ); + const skip = await res4.json(); + + const op = skip.results?.find((item) => item.skipType === "op") || null; + const ed = skip.results?.find((item) => item.skipType === "ed") || null; + + setSkip({ op, ed }); + + if (sessions) { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: GET_MEDIA_USER, + variables: { + username: sessions?.user.name, + }, + }), + }); + + const dat = await response.json(); + + const prog = dat.data.MediaListCollection; + + const gat = prog?.lists.map((item) => item.entries); + const git = gat?.map((item) => + item?.find((item) => item.media.id === parseInt(aniId)) + ); + const gut = git?.find((item) => item?.media.id === parseInt(aniId)); + + if (gut) { + setProgress(gut.progress); + } + + if (gut?.status === "COMPLETED") { + setStatusWatch("REPEATING"); + } else if ( + gut?.status === "REPEATING" && + gut?.media?.episodes === parseInt(playingEpisode) + ) { + setStatusWatch("COMPLETED"); + } else if (gut?.status === "REPEATING") { + setStatusWatch("REPEATING"); + } else if (gut?.media?.episodes === parseInt(playingEpisode)) { + setStatusWatch("COMPLETED"); + } else if ( + gut?.media?.episodes !== null && + aniData.totalEpisodes === parseInt(playingEpisode) + ) { + setStatusWatch("COMPLETED"); + setLoading(true); + } + } + setLoading(true); + }; + fetchData(); + }, [id, aniId, provider, sessions]); + + useEffect(() => { + const mediaSession = navigator.mediaSession; + if (!mediaSession) return; + + const artwork = + poster && poster.length > 0 + ? [{ src: poster[0].image, sizes: "512x512", type: "image/jpeg" }] + : undefined; + + mediaSession.metadata = new MediaMetadata({ + title: playingTitle, + artist: `Moopa ${ + playingTitle === data?.title?.romaji + ? "- Episode " + playingEpisode + : `- ${data?.title?.romaji || data?.title?.english}` + }`, + artwork, + }); + }, [poster, playingTitle, playingEpisode, data]); + + return ( + <> + <Head> + <title>{playingTitle || "Loading..."}</title> + </Head> + + <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> + <div className="bg-primary"> + <Navigasi /> + <div className="min-h-screen mt-3 md:mt-0 flex flex-col lg:gap-0 gap-5 lg:flex-row lg:py-10 lg:px-10 justify-start w-screen"> + <div className="w-screen lg:w-[67%]"> + {loading ? ( + Array.isArray(epiData) ? ( + <div className="aspect-video z-20 bg-black"> + <VideoPlayer + key={id} + data={epiData} + id={aniId} + progress={parseInt(playingEpisode)} + session={sessions} + aniId={parseInt(data?.id)} + stats={statusWatch} + op={skip.op} + ed={skip.ed} + title={playingTitle} + poster={poster[0]?.image} + proxy={proxy} + /> + </div> + ) : ( + <div className="aspect-video bg-black flex-center select-none"> + <p className="lg:p-0 p-5 text-center"> + Whoops! Something went wrong. Please reload the page or + try other sources. {`:(`} + </p> + </div> + ) + ) : ( + <div className="aspect-video bg-black" /> + )} + <div> + {data && data?.episodes.length > 0 ? ( + data.episodes + .filter((items) => items.number == currentNumber) + .map((item, index) => ( + <div className="flex justify-between" key={item.id}> + <div className="p-3 grid gap-2 w-[60%]"> + <div className="text-xl font-outfit font-semibold line-clamp-1"> + <Link + href={`/id/anime/${data.id}`} + className="inline hover:underline" + > + {item.title || + data.title.romaji || + data.title.english} + </Link> + </div> + <h4 className="text-sm font-karla font-light"> + Episode {item.number} + </h4> + </div> + <div className="w-[50%] flex gap-4 items-center justify-end px-4"> + <div className="relative"> + <select + className="flex items-center gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer" + value={item.number} + onChange={(e) => { + const selectedEpisode = data.episodes.find( + (episode) => + episode.number === parseInt(e.target.value) + ); + router.push( + `/id/anime/watch/${selectedEpisode.id}/${data.id}` + ); + }} + > + {data.episodes.map((episode) => ( + <option + key={episode.number} + value={episode.number} + > + Episode {episode.number} + </option> + ))} + </select> + <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + <button + className={`${ + item.number === data.episodes.length + ? "pointer-events-none" + : "" + } relative group`} + onClick={() => { + const currentEpisodeIndex = + data.episodes.findIndex( + (episode) => episode.number === item.number + ); + if ( + currentEpisodeIndex !== -1 && + currentEpisodeIndex < data.episodes.length - 1 + ) { + const nextEpisode = + data.episodes[currentEpisodeIndex + 1]; + router.push( + `/id/anime/watch/${nextEpisode.id}/${data.id}` + ); + } + }} + > + <span className="absolute z-[9999] -left-11 -top-14 p-2 shadow-xl rounded-md transform transition-all whitespace-nowrap bg-secondary lg:group-hover:block group-hover:opacity-1 hidden font-karla font-bold"> + Next Episode + </span> + <ForwardIcon className="w-6 h-6" /> + </button> + </div> + </div> + )) + ) : ( + <div className="p-3 grid gap-2"> + <div className="text-xl font-outfit font-semibold line-clamp-2"> + <div className="inline hover:underline"> + <Skeleton width={240} /> + </div> + </div> + <h4 className="text-sm font-karla font-light"> + <Skeleton width={75} /> + </h4> + </div> + )} + <div className="h-[1px] bg-[#3b3b3b]" /> + + <div className="px-4 pt-7 pb-4 h-full flex"> + <div className="aspect-[9/13] h-[240px]"> + {data ? ( + <Image + src={data.image} + alt="Anime Cover" + width={1000} + height={1000} + priority + className="object-cover aspect-[9/13] h-[240px] rounded-md" + /> + ) : ( + <Skeleton height={240} /> + )} + </div> + <div className="grid w-full px-5 gap-3 h-[240px]"> + <div className="grid grid-cols-2 gap-1 items-center"> + <h2 className="text-sm font-light font-roboto text-[#878787]"> + Studios + </h2> + <div className="row-start-2"> + {data ? data.studios : <Skeleton width={80} />} + </div> + <div className="hidden xxs:grid col-start-2 place-content-end relative"> + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-8 h-8 hover:fill-white hover:cursor-pointer" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" + /> + </svg> + </div> + </div> + </div> + <div className="grid gap-1 items-center"> + <h2 className="text-sm font-light font-roboto text-[#878787]"> + Status + </h2> + <div>{data ? data.status : <Skeleton width={75} />}</div> + </div> + <div className="grid gap-1 items-center overflow-y-hidden"> + <h2 className="text-sm font-light font-roboto text-[#878787]"> + Titles + </h2> + <div className="grid grid-flow-dense grid-cols-2 gap-2 h-full w-full"> + {data ? ( + <> + <div className="line-clamp-3"> + {data.title.romaji || ""} + </div> + <div className="line-clamp-3"> + {data.title.english || ""} + </div> + <div className="line-clamp-3"> + {data.title.native || ""} + </div> + </> + ) : ( + <Skeleton width={200} height={50} /> + )} + </div> + </div> + </div> + </div> + <div className="flex flex-wrap gap-3 px-4 pt-3"> + {data && + data.genres.map((item, index) => ( + <div + key={index} + className="border border-action text-gray-100 py-1 px-2 rounded-md font-karla text-sm" + > + {item} + </div> + ))} + </div> + <div className={`bg-secondary rounded-md mt-3 mx-3`}> + {data && ( + <p + dangerouslySetInnerHTML={{ __html: data.description }} + className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `} + /> + )} + </div> + </div> + </div> + <div className="flex flex-col w-screen lg:w-[35%] "> + <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 px-2 py-2 scrollbar-thin scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full"> + {data && data?.episodes.length > 0 ? ( + episode.map((item, index) => { + return ( + <Link + href={`/id/anime/watch/${data.id}/${item.episodeId}`} + key={item.id} + className={`bg-secondary flex-center w-full h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${ + index === currentNumber - 1 + ? "pointer-events-none ring-1 ring-action text-[#5d5d5d]" + : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" + }`} + > + Episode {index + 1} + </Link> + ); + }) + ) : ( + <> + {[1].map((item) => ( + <Skeleton + key={item} + className="bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out" + /> + ))} + </> + )} + </div> + </div> + </div> + </div> + </SkeletonTheme> + </> + ); +} + +export async function getServerSideProps(context) { + dotenv.config(); + + const API_URI = process.env.API_URI; + + const session = await getServerSession(context.req, context.res, authOptions); + + const proxy = process.env.PROXY_URI; + + const { info } = context.query; + if (!info) { + return { + notFound: true, + }; + } + + const id = info[0]; + const aniId = [info[1], info[2], info[3]]; + + return { + props: { + sessions: session, + id, + aniId: aniId.join("/"), + proxy, + api: API_URI, + }, + }; +} diff --git a/pages/id/contact.js b/pages/id/contact.js new file mode 100644 index 0000000..400a9e8 --- /dev/null +++ b/pages/id/contact.js @@ -0,0 +1,19 @@ +import Layout from "../../components/layout"; + +const Contact = () => { + return ( + <Layout className=""> + <div className=" flex h-screen w-screen flex-col items-center justify-center font-karla font-bold"> + <h1>Contact Us</h1> + <p>If you have any questions or comments, please email us at:</p> + <p> + <a href="mailto:[email protected]?subject=[Moopa]%20-%20Your%20Subject"> + </a> + </p> + </div> + </Layout> + ); +}; + +export default Contact; diff --git a/pages/id/dmca.js b/pages/id/dmca.js new file mode 100644 index 0000000..8dad7d7 --- /dev/null +++ b/pages/id/dmca.js @@ -0,0 +1,109 @@ +import Head from "next/head"; +import Layout from "../../components/layout"; + +export default function DMCA() { + return ( + <> + <Head> + <title>Moopa - DMCA</title> + <meta name="DMCA" content="DMCA" /> + <meta property="og:title" content="DMCA" /> + <meta + property="og:description" + content="Moopa.live is committed to respecting the intellectual + property rights of others and complying with the Digital + Millennium Copyright Act (DMCA)." + /> + <meta + property="og:image" + content="https://cdn.discordapp.com/attachments/1068758633464201268/1081591948705546330/logo.png" + /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="icon" href="/c.svg" /> + </Head> + <Layout> + <div className="min-h-screen z-20 flex w-screen justify-center items-center"> + <div className="w-[75%] text-2xl gap-7 flex flex-col my-[10rem]"> + <div className="flex"> + <h1 className="text-4xl font-bold font-karla rounded-md bg-[#212121] p-3"> + DMCA - Disclaimer + </h1> + </div> + <div className="flex flex-col gap-10"> + <div className="flex flex-col gap-3 text-[#cdcdcd]"> + <p> + Moopa.live is committed to respecting the intellectual + property rights of others and complying with the Digital + Millennium Copyright Act (DMCA). We take copyright + infringement seriously and will respond to notices of alleged + copyright infringement that comply with the DMCA and any other + applicable laws. + </p> + <p> + If you believe that any content on our website is infringing + upon your copyrights, please send us an email. Please allow up + to 2-5 business days for a response. Please note that emailing + your complaint to other parties such as our Internet Service + Provider, Hosting Provider, and other third parties will not + expedite your request and may result in a delayed response due + to the complaint not being filed properly. + </p> + </div> + <p className="text-white"> + In order for us to process your complaint, please provide the + following information: + </p> + <div className="text-xl ml-5 text-[#cdcdcd]"> + <ul className="flex flex-col gap-1"> + <li> + · Your name, address, and telephone number. We reserve the + right to verify this information. + </li> + <li> + · Identification of the copyrighted work claimed to have + been infringed. + </li> + <li> + · The exact and complete URL link where the infringing + material is located. + </li> + <li> + · The exact and complete URL link where the infringing + material is located. + </li> + <li> + · The exact and complete URL link where the infringing + material is located. + </li> + <li>· Please write to us in English or Indonesian.</li> + </ul> + </div> + <p className="text-[#cdcdcd]"> + Please note that anonymous or incomplete messages will not be + dealt with. Thank you for your understanding. + </p> + <h1 className="text-white font-karla">DISCLAIMER:</h1> + <p className="text-[#cdcdcd]"> + None of the files listed on Moopa.live are hosted on our + servers. All links point to content hosted on third-party + websites. Moopa.live does not accept responsibility for content + hosted on third-party websites and has no involvement in the + downloading/uploading of movies. We only post links that are + available on the internet. If you believe that any content on + our website infringes upon your intellectual property rights and + you hold the copyright for that content, please report it to{" "} + <a + href="mailto:[email protected]?subject=[Moopa]%20-%20Your%20Subject" + className="font-semibold" + > + </a>{" "} + and the content will be immediately removed. + </p> + </div> + </div> + </div> + </Layout> + </> + ); +} diff --git a/pages/id/index.js b/pages/id/index.js new file mode 100644 index 0000000..1d42ce3 --- /dev/null +++ b/pages/id/index.js @@ -0,0 +1,633 @@ +import { aniListData } from "../../lib/anilist/AniList"; +import React, { useState, useEffect } from "react"; +import Head from "next/head"; +import Link from "next/link"; +import Footer from "../../components/footer"; +import Image from "next/image"; +import Content from "../../components/home/content"; +import { useRouter } from "next/router"; + +import { motion } from "framer-motion"; + +import { useSession, signIn, signOut } from "next-auth/react"; +import { useAniList } from "../../lib/anilist/useAnilist"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "../api/auth/[...nextauth]"; +import SearchBar from "../../components/searchBar"; +import Genres from "../../components/home/genres"; +import { ToastContainer, toast, cssTransition } from "react-toastify"; + +export function Navigasi() { + const { data: sessions, status } = useSession(); + const [year, setYear] = useState(new Date().getFullYear()); + const [season, setSeason] = useState(getCurrentSeason()); + + const router = useRouter(); + + const handleFormSubmission = (inputValue) => { + router.push(`/id/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"> + <Link + href="/id/" + className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]" + > + moopa + </Link> + <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex"> + <li> + <Link + href={`/id/search/anime?season=${season}&seasonYear=${year}`} + > + This Season + </Link> + </li> + <li> + <Link href="/id/search/manga">Manga</Link> + </li> + <li> + <Link href="/id/search/anime">Anime</Link> + </li> + + {status === "loading" ? ( + <li>Loading...</li> + ) : ( + <> + {!sessions && ( + <li> + <button + onClick={() => signIn("AniListProvider")} + className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md" + > + Sign in + </button> + </li> + )} + {sessions && ( + <li className="text-center"> + <Link href={`/id/profile/${sessions?.user.name}`}> + My List + </Link> + </li> + )} + </> + )} + </ul> + </div> + <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-0"> + <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> + </div> + </div> + </> + ); +} + +export default function Home({ detail, populars, sessions }) { + const { media: current } = useAniList(sessions, { stats: "CURRENT" }); + const { media: plan } = useAniList(sessions, { stats: "PLANNING" }); + + const [isVisible, setIsVisible] = useState(false); + const [list, setList] = useState(null); + const [planned, setPlanned] = useState(null); + const [greeting, setGreeting] = useState(""); + const [onGoing, setOnGoing] = useState(null); + + const [prog, setProg] = useState(null); + + const popular = populars?.data; + const data = detail.data[0]; + + const handleShowClick = () => { + setIsVisible(true); + }; + + const handleHideClick = () => { + setIsVisible(false); + }; + + useEffect(() => { + const time = new Date().getHours(); + let greeting = ""; + + if (time >= 5 && time < 12) { + greeting = "Good morning"; + } else if (time >= 12 && time < 18) { + greeting = "Good afternoon"; + } else if (time >= 18 && time < 22) { + greeting = "Good evening"; + } else if (time >= 22 || time < 5) { + greeting = "Good night"; + } + + setGreeting(greeting); + + async function userData() { + if (!sessions) return; + const getMedia = + current.filter((item) => item.status === "CURRENT")[0] || null; + const list = getMedia?.entries + .map(({ media }) => media) + .filter((media) => media); + + const prog = getMedia?.entries.filter( + (item) => item.media.nextAiringEpisode !== null + ); + + setProg(prog); + + const planned = plan?.[0]?.entries + .map(({ media }) => media) + .filter((media) => media); + + const onGoing = list?.filter((item) => item.nextAiringEpisode !== null); + setOnGoing(onGoing); + + if (list) { + setList(list.reverse()); + } + if (planned) { + setPlanned(planned.reverse()); + } + } + userData(); + }, [sessions, current, plan]); + + const blurSlide = cssTransition({ + enter: "slide-in-blurred-right", + exit: "slide-out-blurred-right", + }); + + useEffect(() => { + function Toast() { + toast.warn( + "This site is still in development, some features may not work properly.", + { + position: "bottom-right", + autoClose: false, + hideProgressBar: true, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + theme: "dark", + transition: blurSlide, + } + ); + } + Toast(); + }, []); + + // console.log(log); + + return ( + <> + <Head> + <title>Moopa</title> + <meta charSet="UTF-8"></meta> + <meta name="twitter:card" content="summary_large_image" /> + <meta + name="twitter:title" + content="Moopa - Free Anime and Manga Streaming" + /> + <meta + name="twitter:description" + content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!" + /> + <meta + name="twitter:image" + content="https://cdn.discordapp.com/attachments/1084446049986420786/1093300833422168094/image.png" + /> + <link rel="icon" href="/c.svg" /> + </Head> + + <ToastContainer pauseOnFocusLoss={false} style={{ width: "420px" }} /> + + {/* 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={`/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]" + > + <img + src={sessions?.user.image.large} + alt="user avatar" + 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="/id/" 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="/id/" + className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" + > + home + </Link> + </button> + <button className="group flex flex-col items-center"> + <Link href="/id/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="/id/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="/id/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="/id/search/anime" + className="font-karla font-bold text-[#8BA0B2] 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-[#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> + + <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] "> + <Navigasi /> + <SearchBar /> + {/* PC / TABLET */} + <div className=" hidden justify-center lg:flex my-16"> + <div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between"> + <div className="row-start-2 flex h-full flex-col gap-7 lg:w-[55%] lg:justify-center"> + <h1 className="w-[85%] font-outfit font-extrabold lg:text-[34px] line-clamp-2"> + {data.title.english || data.title.romaji || data.title.native} + </h1> + <p + className="font-roboto font-light lg:text-[18px] line-clamp-5" + dangerouslySetInnerHTML={{ __html: data?.description }} + /> + + <div className="lg:pt-5"> + <Link + href={`/id/anime/${data.id}`} + legacyBehavior + className="flex" + > + <a className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]"> + START WATCHING + </a> + </Link> + </div> + </div> + <div className="z-10 row-start-1 flex justify-center "> + <div className="relative lg:h-[467px] lg:w-[322px] lg:scale-100"> + <div className="absolute bg-gradient-to-t from-[#141519] to-transparent lg:h-[467px] lg:w-[322px]" /> + + <Image + draggable={false} + src={data.coverImage?.extraLarge || data.image} + alt={`alt for ${data.title.english || data.title.romaji}`} + width={460} + height={662} + priority + className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]" + /> + </div> + </div> + </div> + </div> + {/* {!sessions && ( + <h1 className="font-bold font-karla mx-5 text-[32px] mt-2 lg:mx-24 xl:mx-36"> + {greeting}! + </h1> + )} */} + {sessions && ( + <div className="flex items-center justify-center lg:bg-none mt-4 lg:mt-0 w-screen"> + <div className="lg:w-[85%] w-screen px-5 lg:px-0 lg:text-4xl flex items-center gap-3 text-2xl font-bold font-karla"> + {greeting},<h1 className="lg:hidden">{sessions?.user.name}</h1> + <button + onClick={() => signOut()} + className="hidden text-center relative lg:flex justify-center group" + > + {sessions?.user.name} + <span className="absolute text-sm z-50 w-20 text-center bottom-11 text-white shadow-lg opacity-0 bg-secondary p-1 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all"> + Sign Out + </span> + </button> + </div> + </div> + )} + + <div className="lg:mt-16 mt-5 flex flex-col items-center"> + <motion.div + className="w-screen flex-none lg:w-[87%]" + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop + > + {sessions && onGoing?.length > 0 && ( + <motion.div // Add motion.div to each child component + key="onGoing" + initial={{ y: 20, opacity: 0 }} + whileInView={{ y: 0, opacity: 1 }} + transition={{ duration: 0.5 }} + viewport={{ once: true }} + > + <Content + ids="onGoing" + section="On-Going Anime" + data={onGoing} + og={prog} + /> + </motion.div> + )} + + {sessions && list?.length > 0 && ( + <motion.div // Add motion.div to each child component + key="listAnime" + initial={{ y: 20, opacity: 0 }} + whileInView={{ y: 0, opacity: 1 }} + transition={{ duration: 0.5 }} + viewport={{ once: true }} + > + <Content + ids="listAnime" + section="Your Watch List" + data={list} + /> + </motion.div> + )} + + {/* SECTION 2 */} + {sessions && planned?.length > 0 && ( + <motion.div // Add motion.div to each child component + key="plannedAnime" + initial={{ y: 20, opacity: 0 }} + whileInView={{ y: 0, opacity: 1 }} + transition={{ duration: 0.5 }} + viewport={{ once: true }} + > + <Content + ids="plannedAnime" + section="Your Plan" + data={planned} + /> + </motion.div> + )} + + {/* SECTION 3 */} + {detail && ( + <motion.div // Add motion.div to each child component + key="trendingAnime" + initial={{ y: 20, opacity: 0 }} + transition={{ duration: 0.5 }} + whileInView={{ y: 0, opacity: 1 }} + viewport={{ once: true }} + > + <Content + ids="trendingAnime" + section="Trending Now" + data={detail.data} + /> + </motion.div> + )} + + {/* SECTION 4 */} + {popular && ( + <motion.div // Add motion.div to each child component + key="popularAnime" + initial={{ y: 20, opacity: 0 }} + whileInView={{ y: 0, opacity: 1 }} + transition={{ duration: 0.5 }} + viewport={{ once: true }} + > + <Content + ids="popularAnime" + section="Popular Anime" + data={popular} + /> + </motion.div> + )} + + <motion.div // Add motion.div to each child component + key="Genres" + initial={{ y: 20, opacity: 0 }} + whileInView={{ y: 0, opacity: 1 }} + transition={{ duration: 0.5 }} + viewport={{ once: true }} + > + <Genres /> + </motion.div> + </motion.div> + </div> + </div> + <Footer /> + </> + ); +} + +export async function getServerSideProps(context) { + const session = await getServerSession(context.req, context.res, authOptions); + + const trendingDetail = await aniListData({ + sort: "TRENDING_DESC", + page: 1, + }); + const popularDetail = await aniListData({ + sort: "POPULARITY_DESC", + page: 1, + }); + const genreDetail = await aniListData({ sort: "TYPE", page: 1 }); + + return { + props: { + genre: genreDetail.props, + detail: trendingDetail.props, + populars: popularDetail.props, + sessions: session, + }, + }; +} + +function getCurrentSeason() { + const now = new Date(); + const month = now.getMonth() + 1; // getMonth() returns 0-based index + + switch (month) { + case 12: + case 1: + case 2: + return "WINTER"; + case 3: + case 4: + case 5: + return "SPRING"; + case 6: + case 7: + case 8: + return "SUMMER"; + case 9: + case 10: + case 11: + return "FALL"; + default: + return "UNKNOWN SEASON"; + } +} diff --git a/pages/id/profile/[user].js b/pages/id/profile/[user].js new file mode 100644 index 0000000..6bc804e --- /dev/null +++ b/pages/id/profile/[user].js @@ -0,0 +1,423 @@ +import { getServerSession } from "next-auth"; +import { authOptions } from "../../api/auth/[...nextauth]"; +import Navbar from "../../../components/navbar"; +import Image from "next/image"; +import Link from "next/link"; +import Head from "next/head"; +import { useState } from "react"; + +export default function MyList({ media, sessions, user, time }) { + const [listFilter, setListFilter] = useState("all"); + const [visible, setVisible] = useState(false); + + const filterMedia = (status) => { + if (status === "all") { + return media; + } + return media.filter((m) => m.name === status); + }; + return ( + <> + <Head> + <title>My Lists</title> + </Head> + <Navbar /> + <div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 relative"> + <div className="lg:w-[30%] h-full mt-12 lg:mr-10 grid gap-5 mx-3 lg:mx-0 antialiased"> + <div className="flex items-center gap-5"> + <Image + src={user.avatar.large} + alt="user avatar" + width={1000} + height={1000} + className="object-cover h-28 w-28 rounded-lg" + /> + {user.bannerImage ? ( + <Image + src={user.bannerImage} + alt="image" + width={1000} + height={1000} + priority + className="absolute w-screen h-[240px] object-cover -top-[7.75rem] left-0 -z-50 brightness-[65%]" + /> + ) : ( + <div className="absolute w-screen h-[240px] object-cover -top-[7.75rem] left-0 -z-50 brightness-[65%] bg-image" /> + )} + <h1 className="font-karla font-bold text-2xl pt-7">{user.name}</h1> + </div> + <div className="flex items-center justify-between"> + <div className="flex gap-2 text-sm font-karla"> + Created At : + <UnixTimeConverter unixTime={user.createdAt} /> + </div> + {sessions && user.name === sessions?.user.name ? ( + <Link + href={"https://anilist.co/settings/"} + className="flex items-center gap-2 p-1 px-2 ring-[1px] antialiased ring-txt rounded-lg text-xs font-karla hover:bg-txt hover:shadow-lg group" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-4 h-4 group-hover:stroke-black" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42" + /> + </svg> + <span className="group-hover:text-black">Edit Profile</span> + </Link> + ) : null} + </div> + <div className="bg-secondary lg:min-h-[160px] text-xs rounded-md p-4 font-karla"> + <div> + {user.about ? ( + <div dangerouslySetInnerHTML={{ __html: user.about }} /> + ) : ( + "No description created." + )} + </div> + </div> + + <div className="bg-secondary font-karla rounded-md h-20 p-1 grid grid-cols-3 place-items-center text-center text-txt"> + <div> + <h1 className="text-action font-bold"> + {user.statistics.anime.episodesWatched} + </h1> + <h2 className="text-sm">Total Episodes</h2> + </div> + <div> + <h1 className="text-action font-bold"> + {user.statistics.anime.count} + </h1> + <h2 className="text-sm">Total Anime</h2> + </div> + {time?.days ? ( + <div> + <h1 className="text-action font-bold">{time.days}</h1> + <h2 className="text-sm">Days Watched</h2> + </div> + ) : ( + <div> + <h1 className="text-action font-bold">{time.hours}</h1> + <h2 className="text-sm">hours</h2> + </div> + )} + </div> + {media.length !== 0 && ( + <div className="font-karla grid gap-4"> + <div className="flex md:justify-normal justify-between items-center"> + <div className="flex items-center gap-3"> + <h1>Lists Filter</h1> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-[20px] h-[20px]" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" + /> + </svg> + </div> + <div + className="md:hidden bg-secondary p-1 rounded-md cursor-pointer" + onClick={() => setVisible(!visible)} + > + <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" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" + /> + </svg> + </div> + </div> + <ul + className={`group md:grid gap-1 text-sm ${ + visible ? "" : "hidden" + }`} + > + <li + onClick={() => setListFilter("all")} + className={`p-2 cursor-pointer hover:text-action ${ + listFilter === "all" && "bg-secondary text-action" + }`} + > + <h1 className={`cursor-pointer hover:text-action`}> + Show All + </h1> + </li> + {media.map((item) => ( + <li + key={item.name} + onClick={() => setListFilter(item.name)} + className={`cursor-pointer hover:text-action flex gap-2 p-2 duration-200 ${ + item.name === listFilter && "bg-secondary text-action" + }`} + > + <h1 className="">{item.name}</h1> + <div className="text-gray-400 opacity-0 invisible duration-200 transition-all group-hover:visible group-hover:opacity-100"> + ({item.entries.length}) + </div> + </li> + ))} + </ul> + </div> + )} + </div> + + <div className="lg:w-[75%] grid gap-10 my-12 lg:pt-16"> + {media.length !== 0 ? ( + filterMedia(listFilter).map((item, index) => { + return ( + <div key={index} className="flex flex-col gap-5 mx-3"> + <h1 className="font-karla font-bold text-xl">{item.name}</h1> + <table className="bg-secondary rounded-lg"> + <thead> + <tr> + <th className="font-bold text-xs py-3 text-start pl-10 lg:w-[75%] w-[65%]"> + Title + </th> + <th className="font-bold text-xs py-3">Score</th> + <th className="font-bold text-xs py-3">Progress</th> + </tr> + </thead> + <tbody className=""> + {item.entries.map((item) => { + return ( + <tr + key={item.mediaId} + className="hover:bg-orange-400 duration-150 ease-in-out group relative" + > + <td className="font-medium py-2 pl-2 rounded-l-lg"> + <div className="flex items-center gap-2"> + {item.media.status === "RELEASING" ? ( + <span className="dot group-hover:invisible bg-green-500 shrink-0" /> + ) : item.media.status === "NOT_YET_RELEASED" ? ( + <span className="dot group-hover:invisible bg-red-500 shrink-0" /> + ) : ( + <span className="dot group-hover:invisible shrink-0" /> + )} + <Image + src={item.media.coverImage.large} + alt="Cover Image" + width={500} + height={500} + className="object-cover rounded-md w-10 h-10 shrink-0" + /> + <div className="absolute -top-10 -left-40 invisible lg:group-hover:visible"> + <Image + src={item.media.coverImage.large} + alt={item.media.id} + width={1000} + height={1000} + className="object-cover h-[186px] w-[140px] shrink-0 rounded-md" + /> + </div> + <Link + href={`/en/anime/${item.media.id}`} + className="font-semibold font-karla pl-2 text-sm line-clamp-1" + title={item.media.title.romaji} + > + {item.media.title.romaji} + </Link> + </div> + </td> + <td className="text-center text-xs text-txt"> + {item.score === 0 ? null : item.score} + </td> + <td className="text-center text-xs text-txt rounded-r-lg"> + {item.progress === item.media.episodes + ? item.progress + : item.media.episodes === null + ? item.progress + : `${item.progress}/${item.media.episodes}`} + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + }) + ) : ( + <div className="w-screen lg:w-full flex-center flex-col gap-5"> + {user.name === sessions?.user.name ? ( + <p className="text-center font-karla font-bold lg:text-lg"> + Oops!<br></br> Looks like you haven't watch anything yet. + </p> + ) : ( + <p className="text-center font-karla font-bold lg:text-lg"> + Oops!<br></br> It looks like this user haven't watch anything + yet. + </p> + )} + <Link + href="/en/search/anime" + className="flex gap-2 text-sm ring-1 ring-action p-2 rounded-lg font-karla" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + className="w-5 h-5" + > + <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> + <span>Start Watching</span> + </Link> + </div> + )} + </div> + </div> + </> + ); +} + +export async function getServerSideProps(context) { + const session = await getServerSession(context.req, context.res, authOptions); + const query = context.query; + + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: ` + query ($username: String, $status: MediaListStatus) { + MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) { + user { + id + name + about (asHtml: true) + createdAt + avatar { + large + } + statistics { + anime { + count + episodesWatched + meanScore + minutesWatched + } + } + bannerImage + mediaListOptions { + animeList { + sectionOrder + } + } + } + lists { + status + name + entries { + id + mediaId + status + progress + score + media { + id + status + title { + english + romaji + } + episodes + coverImage { + large + } + } + } + } + } + } + `, + variables: { + username: query.user, + }, + }), + }); + + const data = await response.json(); + + const get = data.data.MediaListCollection; + const sectionOrder = get?.user.mediaListOptions.animeList.sectionOrder; + + if (!sectionOrder) { + return { + notFound: true, + }; + } + + const prog = get.lists; + + function getIndex(status) { + const index = sectionOrder.indexOf(status); + return index === -1 ? sectionOrder.length : index; + } + + prog.sort((a, b) => getIndex(a.name) - getIndex(b.name)); + + const user = get.user; + + const time = convertMinutesToDays(user.statistics.anime.minutesWatched); + + return { + props: { + media: prog, + sessions: session, + user: user, + time: time, + }, + }; +} + +function UnixTimeConverter({ unixTime }) { + const date = new Date(unixTime * 1000); // multiply by 1000 to convert to milliseconds + const formattedDate = date.toISOString().slice(0, 10); // format date to YYYY-MM-DD + + return <p>{formattedDate}</p>; +} + +function convertMinutesToDays(minutes) { + const hours = minutes / 60; + const days = hours / 24; + + if (days >= 1) { + return days % 1 === 0 + ? { days: `${parseInt(days)}` } + : { days: `${days.toFixed(1)}` }; + } else { + return hours % 1 === 0 + ? { hours: `${parseInt(hours)}` } + : { hours: `${hours.toFixed(1)}` }; + } +} diff --git a/pages/id/search/[param].js b/pages/id/search/[param].js new file mode 100644 index 0000000..00fab64 --- /dev/null +++ b/pages/id/search/[param].js @@ -0,0 +1,493 @@ +import { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion as m } from "framer-motion"; +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import { useRouter } from "next/router"; +import Link from "next/link"; +import Navbar from "../../../components/navbar"; +import Head from "next/head"; +import Footer from "../../../components/footer"; + +import Image from "next/image"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch"; + +const genre = [ + "Action", + "Adventure", + "Comedy", + "Drama", + "Ecchi", + "Fantasy", + "Horror", + "Mahou Shoujo", + "Mecha", + "Music", + "Mystery", + "Psychological", + "Romance", + "Sci-Fi", + "Slice of Life", + "Sports", + "Supernatural", + "Thriller", +]; + +const types = ["ANIME", "MANGA"]; + +const sorts = [ + { name: "Title", value: "TITLE_ROMAJI" }, + { name: "Popularity", value: "POPULARITY_DESC" }, + { name: "Trending", value: "TRENDING_DESC" }, + { name: "Favourites", value: "FAVOURITES_DESC" }, + { name: "Average Score", value: "SCORE_DESC" }, + { name: "Date Added", value: "ID_DESC" }, + { name: "Release Date", value: "START_DATE_DESC" }, +]; + +export default function Card() { + const router = useRouter(); + + const [data, setData] = useState(); + const [loading, setLoading] = useState(true); + + let hasil = null; + let tipe = "ANIME"; + let s = undefined; + let y = NaN; + let gr = undefined; + + const query = router.query; + gr = query.genres; + + if (query.param !== "anime" && query.param !== "manga") { + hasil = query.param; + } else if (query.param === "anime") { + hasil = null; + tipe = "ANIME"; + if ( + query.season !== "WINTER" && + query.season !== "SPRING" && + query.season !== "SUMMER" && + query.season !== "FALL" + ) { + s = undefined; + y = NaN; + } else { + s = query.season; + y = parseInt(query.seasonYear); + } + } else if (query.param === "manga") { + hasil = null; + tipe = "MANGA"; + if ( + query.season !== "WINTER" && + query.season !== "SPRING" && + query.season !== "SUMMER" && + query.season !== "FALL" + ) { + s = undefined; + y = NaN; + } else { + s = query.season; + y = parseInt(query.seasonYear); + } + } + + // console.log(tags); + + const [search, setQuery] = useState(hasil); + const [type, setSelectedType] = useState(tipe); + // const [genres, setSelectedGenre] = useState(); + const [sort, setSelectedSort] = useState(); + + const [isVisible, setIsVisible] = useState(false); + + const inputRef = useRef(null); + + const [page, setPage] = useState(1); + const [nextPage, setNextPage] = useState(true); + + async function advance() { + setLoading(true); + const data = await aniAdvanceSearch({ + search: search, + type: type, + genres: gr, + page: page, + sort: sort, + season: s, + seasonYear: y, + }); + if (data.media.length === 0) { + setNextPage(false); + } else if (data !== null && page > 1) { + setData((prevData) => { + return [...(prevData ?? []), ...data.media]; + }); + setNextPage(data.pageInfo.hasNextPage); + } else { + setData(data.media); + } + setNextPage(data.pageInfo.hasNextPage); + setLoading(false); + } + + useEffect(() => { + setData(null); + setPage(1); + setNextPage(true); + advance(); + }, [search, type, sort, s, y, gr]); + + useEffect(() => { + advance(); + }, [page]); + + useEffect(() => { + function handleScroll() { + if (page > 10 || !nextPage) { + window.removeEventListener("scroll", handleScroll); + return; + } + + if ( + window.innerHeight + window.pageYOffset >= + document.body.offsetHeight - 3 + ) { + setPage((prevPage) => prevPage + 1); + } + } + + window.addEventListener("scroll", handleScroll); + + return () => window.removeEventListener("scroll", handleScroll); + }, [page, nextPage]); + + const handleKeyDown = async (event) => { + if (event.key === "Enter") { + event.preventDefault(); + const inputValue = event.target.value; + if (inputValue === "") { + setQuery(null); + } else { + setQuery(inputValue); + } + } + }; + + function trash() { + setQuery(null); + inputRef.current.value = ""; + // setSelectedGenre(null); + setSelectedSort(["POPULARITY_DESC"]); + router.push(`/search/${tipe.toLocaleLowerCase()}`); + } + + function handleVisible() { + setIsVisible(!isVisible); + } + + function handleTipe(e) { + setSelectedType(e.target.value); + router.push(`/search/${e.target.value.toLowerCase()}`); + } + + // ); + + return ( + <> + <Head> + <title>Moopa - search</title> + <link rel="icon" href="/c.svg" /> + </Head> + <div className="bg-primary"> + <Navbar /> + <div className="min-h-screen mt-10 mb-14 text-white items-center gap-5 xl:gap-0 flex flex-col"> + <div className="w-screen px-10 xl:w-[80%] xl:h-[10rem] flex text-center xl:items-end xl:pb-10 justify-center lg:gap-7 xl:gap-10 gap-3 font-karla font-light"> + <div className="text-start"> + <h1 className="font-bold xl:pb-5 pb-3 hidden lg:block text-md pl-1 font-outfit"> + TITLE + </h1> + <input + className="xl:w-[297px] md:w-[297px] lg:w-[230px] xl:h-[46px] h-[35px] xxs:w-[230px] xs:w-[280px] bg-secondary rounded-[10px] font-karla font-light text-[#ffffff89] text-center" + placeholder="search here..." + type="text" + onKeyDown={handleKeyDown} + ref={inputRef} + /> + </div> + + {/* TYPE */} + <div className="hidden lg:block text-start"> + <h1 className="font-bold xl:pb-5 pb-3 text-md pl-1 font-outfit"> + TYPE + </h1> + <div className="relative"> + <select + className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] justify-between flex items-center text-center appearance-none" + value={type} + onChange={(e) => handleTipe(e)} + > + {types.map((option) => ( + <option key={option} value={option}> + {option} + </option> + ))} + </select> + <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + </div> + + {/* SORT */} + <div className="hidden lg:block text-start"> + <h1 className="font-bold xl:pb-5 lg:pb-3 text-md pl-1 font-outfit"> + SORT + </h1> + <div className="relative"> + <select + className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] flex items-center text-center appearance-none" + onChange={(e) => { + setSelectedSort(e.target.value); + setData(null); + }} + > + <option value={["POPULARITY_DESC"]}>Sort By</option> + {sorts.map((sort) => ( + <option key={sort.value} value={sort.value}> + {sort.name} + </option> + ))} + </select> + <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + </div> + + {/* OPTIONS */} + <div className="flex lg:gap-7 text-center gap-3 items-end"> + <div + className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group" + onClick={handleVisible} + > + <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="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" + /> + </svg> + </div> + + {/* TRASH ICON */} + <div + className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group" + onClick={trash} + > + <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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" + /> + </svg> + </div> + </div> + </div> + + <div className="w-screen xl:w-[64%] flex xl:justify-end xl:pl-0"> + <AnimatePresence> + {isVisible && ( + <m.div + key="imagine" + initial={{ opacity: 0, y: -10 }} + animate={{ opacity: 1, y: 0 }} + exit={{ opacity: 0, y: -10 }} + className="xl:pb-16" + > + <div className="text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 lg:pb-0 "> + <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> + GENRE + </h1> + <div className="relative"> + <select + className="w-[195px] xl:w-[297px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" + onChange={(e) => { + // setSelectedGenre( + // e.target.value === "undefined" + // ? undefined + // : e.target.value + // ); + router.push( + `/search/${tipe.toLocaleLowerCase()}/?genres=${ + e.target.value + }` + ); + }} + > + <option value="undefined">Select a Genre</option> + {genre.map((option) => { + return ( + <option key={option} value={option}> + {option} + </option> + ); + })} + </select> + <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + </div> + <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 "> + <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> + TYPE + </h1> + <div className="relative"> + <select + className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" + value={type} + onChange={(e) => setSelectedType(e.target.value)} + > + {types.map((option) => ( + <option key={option} value={option}> + {option} + </option> + ))} + </select> + <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + </div> + + <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col "> + <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit"> + SORT + </h1> + <div className="relative"> + <select + className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none" + onChange={(e) => { + setSelectedSort(e.target.value); + }} + > + <option value={["POPULARITY_DESC"]}>Sort By</option> + {sorts.map((sort) => ( + <option key={sort.value} value={sort.value}> + {sort.name} + </option> + ))} + </select> + <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + </div> + </m.div> + )} + </AnimatePresence> + </div> + {gr && ( + <div className="lg:w-[70%] px-5 lg:px-4 w-screen lg:mb-6"> + <h1 className="font-bold text-[25px] font-karla"> + Looking for : {gr} + </h1> + </div> + )} + <div className="flex flex-col gap-14 items-center"> + <AnimatePresence> + <div + key="card-keys" + className="grid pt-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 xxs:grid-cols-3 w-screen px-2 xl:w-auto xl:gap-10 gap-2 xl:gap-y-24 gap-y-12 overflow-hidden" + > + {loading + ? "" + : !data?.length && ( + <div className="w-screen text-[#ff7f57] xl:col-start-3 col-start-2 items-center flex justify-center text-center font-bold font-karla xl:text-2xl"> + Oops!<br></br> Nothing's Found... + </div> + )} + {data && + data?.map((anime, index) => { + return ( + <m.div + initial={{ scale: 0.9 }} + animate={{ scale: 1, transition: { duration: 0.35 } }} + className="w-[146px] xxs:w-[115px] xs:w-[135px] xl:w-[185px]" + key={index} + > + <Link + href={ + anime.format === "MANGA" || anime.format === "NOVEL" + ? `/manga/detail/id?aniId=${anime.id}&aniTitle=${anime.title.userPreferred}` + : `/en/anime/${anime.id}` + } + className="" + > + <Image + className="object-cover bg-[#3B3C41] w-[146px] h-[208px] xxs:w-[115px] xxs:h-[163px] xs:w-[135px] xs:h-[192px] xl:w-[185px] xl:h-[265px] hover:scale-105 scale-100 transition-all cursor-pointer duration-200 ease-out rounded-[10px]" + src={anime.coverImage.extraLarge} + alt={anime.title.userPreferred} + width={500} + height={500} + /> + </Link> + <Link href={`/en/anime/${anime.id}`}> + <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> + {anime.status === "RELEASING" ? ( + <span className="dots bg-green-500" /> + ) : anime.status === "NOT_YET_RELEASED" ? ( + <span className="dots bg-red-500" /> + ) : null} + {anime.title.userPreferred} + </h1> + </Link> + <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]"> + {anime.format || <p>-</p>} ·{" "} + {anime.status || <p>-</p>} ·{" "} + {anime.episodes || 0} Episodes + </h2> + </m.div> + ); + })} + + {loading && ( + <> + <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> + {[1, 2, 4, 5, 6, 7, 8].map((item) => ( + <div + key={item} + className="flex flex-col w-[135px] xl:w-[185px] gap-5" + style={{ scale: 0.98 }} + > + <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" /> + <Skeleton width={110} height={30} /> + </div> + ))} + </SkeletonTheme> + </> + )} + </div> + {!loading && page > 10 && nextPage && ( + <button + onClick={() => setPage((p) => p + 1)} + className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md" + > + Load More + </button> + )} + </AnimatePresence> + </div> + </div> + <Footer /> + </div> + </> + ); +} |