aboutsummaryrefslogtreecommitdiff
path: root/pages/en/anime
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-07-16 22:35:39 +0700
committerFactiven <[email protected]>2023-07-16 22:35:39 +0700
commit1eee181e219dfd993d396ac3169e7aad3dd285eb (patch)
tree23fe54e9c3f8810f3ac9ab6b29070b4f0d4b9d20 /pages/en/anime
parentremoved console.log (diff)
downloadmoopa-1eee181e219dfd993d396ac3169e7aad3dd285eb.tar.xz
moopa-1eee181e219dfd993d396ac3169e7aad3dd285eb.zip
Update v3.6.4
- Added Manga page with a working tracker for AniList user - Added schedule component to home page - Added disqus comment section so you can fight on each other (not recommended) - Added /id and /en route for english and indonesian subs (id route still work in progress)
Diffstat (limited to 'pages/en/anime')
-rw-r--r--pages/en/anime/[...id].js1162
-rw-r--r--pages/en/anime/watch/[...info].js642
2 files changed, 1804 insertions, 0 deletions
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
new file mode 100644
index 0000000..b6393d4
--- /dev/null
+++ b/pages/en/anime/[...id].js
@@ -0,0 +1,1162 @@
+import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
+import "react-loading-skeleton/dist/skeleton.css";
+
+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, 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 { ToastContainer } from "react-toastify";
+
+// 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 [epiView, setEpiView] = useState("3");
+
+ const [artStorage, setArtStorage] = useState(null);
+
+ const rec = info?.recommendations?.nodes?.map(
+ (data) => data.mediaRecommendation
+ );
+
+ const [provider, setProvider] = useState();
+ const [prvValue, setPrvValue] = useState("gogoanime");
+
+ const [availableProviders, setAvailableProviders] = useState([]);
+ // const [err, setErr] = useState('');
+
+ function handleProvider(e) {
+ setEpisode(
+ Array.isArray(provider[e.target.value])
+ ? provider[e.target.value]?.reverse()
+ : provider[e.target.value]
+ );
+ setPrvValue(e.target.value);
+ localStorage.setItem("provider", e.target.value);
+ }
+
+ //for episodes dropdown
+ useEffect(() => {
+ setFirstEpisodeIndex(0);
+ setLastEpisodeIndex();
+ setSelectedRange("All");
+ }, [info, prvValue]);
+
+ useEffect(() => {
+ handleClose();
+ async function fetchData() {
+ setLoading(true);
+ if (id) {
+ try {
+ const { protocol, host } = window.location;
+ const prv = localStorage.getItem("provider");
+ const url = `${protocol}//${host}`;
+
+ const view = localStorage.getItem("epiView");
+
+ if (prv) {
+ setPrvValue(prv);
+ } else {
+ setPrvValue("gogoanime");
+ }
+
+ setDomainUrl(url);
+
+ setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
+
+ setEpisode(null);
+ setProgress(0);
+ setStatuses(null);
+
+ let reloadCount = 0;
+
+ try {
+ const fetchPromises = [
+ fetch(`${api}/meta/anilist/info/${info.id}?provider=enime`),
+ fetch(`${api}/meta/anilist/info/${info.id}?provider=zoro`),
+ fetch(`${api}/meta/anilist/info/${info.id}?provider=gogoanime`),
+ ];
+
+ const results = await Promise.allSettled(fetchPromises);
+ const successfulResponses = [];
+ let errorCount = 0;
+
+ results.forEach((result) => {
+ if (result.status === "fulfilled") {
+ successfulResponses.push(result.value);
+ } else {
+ errorCount++;
+ }
+ });
+
+ if (errorCount === fetchPromises.length) {
+ // All fetch requests failed, handle the error here
+ setEpisode([]);
+ } else {
+ // Process the successfulResponses here
+ const responsesData = await Promise.all(
+ successfulResponses.map((response) => response.json())
+ );
+ const [enime, zoro, gogoanime] = responsesData;
+
+ const prov = {
+ enime: enime?.episodes || enime,
+ zoro: zoro?.episodes || zoro,
+ gogoanime: gogoanime?.episodes || gogoanime,
+ };
+
+ const aPrv = [
+ {
+ name: "enime",
+ available:
+ enime?.episodes && enime?.episodes.length > 0
+ ? true
+ : false,
+ },
+ {
+ name: "zoro",
+ available:
+ zoro?.episodes && zoro?.episodes.length > 0 ? true : false,
+ },
+ {
+ name: "gogoanime",
+ available:
+ gogoanime?.episodes && gogoanime?.episodes.length > 0
+ ? true
+ : false,
+ },
+ ];
+
+ setAvailableProviders(aPrv);
+
+ const infProv = {
+ enime: enime,
+ zoro: zoro,
+ gogoanime: gogoanime,
+ };
+
+ if (prv) {
+ setEpisode(
+ Array.isArray(prov[prv]) ? prov[prv]?.reverse() : prov[prv]
+ );
+ } else {
+ setEpisode(
+ Array.isArray(prov["gogoanime"])
+ ? prov["gogoanime"]?.reverse()
+ : prov["gogoanime"]
+ );
+ }
+
+ const data = infProv[prv] || infProv["gogoanime"];
+ // const data = aniInfo;
+ if (!data || data?.episodes?.length === 0) {
+ setEpisode([]);
+ } else {
+ if (data.episodes?.some((i) => i.title === null)) {
+ setEpiView("3");
+ } else if (view) {
+ setEpiView(view);
+ } else {
+ setEpiView("3");
+ }
+ }
+
+ 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]);
+ }
+ }
+ }
+
+ if (data.nextAiringEpisode) {
+ setTime(
+ convertSecondsToTime(data.nextAiringEpisode.timeUntilAiring)
+ );
+ }
+
+ setProvider(prov);
+ }
+ } catch (error) {
+ console.error(error);
+ if (reloadCount < 2) {
+ reloadCount++;
+ setTimeout(() => {
+ window.location.reload();
+ }, 1000);
+ } else {
+ setEpisode([]);
+ }
+ }
+ } catch (error) {
+ console.error(error);
+ setTimeout(() => {
+ window.location.reload();
+ }, 1000);
+ } finally {
+ 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";
+ }
+
+ const filterProviders = availableProviders?.filter((x) => x.available);
+
+ 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>
+ <ToastContainer pauseOnFocusLoss={false} />
+ <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 flex gap-5 overflow-x-scroll snap-x scroll-px-5 scrollbar-none lg:grid lg:grid-cols-3 justify-items-center 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"
+ ? `/en/anime/${rel.id}`
+ : `/en/manga/${rel.id}`
+ }
+ className={`lg: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] lg: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 ||
+ 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>
+ <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>
+ <div
+ className={`flex lg:flex items-center gap-0 lg:gap-5 justify-between ${
+ visible ? "" : "hidden"
+ }`}
+ >
+ <div className="flex items-end gap-3">
+ {filterProviders?.length > 0 && (
+ <div className="relative flex gap-2 items-center">
+ <p className="hidden md:block">Provider</p>
+ <select
+ onChange={handleProvider}
+ value={prvValue}
+ 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"
+ >
+ {availableProviders
+ ?.filter((p) => p.available === true)
+ .map((p) => {
+ return (
+ <option key={p.name} value={p.name}>
+ {p.name}
+ </option>
+ );
+ })}
+ </select>
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
+ </div>
+ )}
+ {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 className="flex gap-3 rounded-sm items-center p-2">
+ <div
+ className={
+ episode?.length > 0
+ ? episode?.some((item) => item?.title === null)
+ ? "pointer-events-none"
+ : "cursor-pointer"
+ : "pointer-events-none"
+ }
+ onClick={() => {
+ setEpiView("1");
+ localStorage.setItem("epiView", "1");
+ }}
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="31"
+ height="20"
+ fill="none"
+ viewBox="0 0 31 20"
+ >
+ <rect
+ width="31"
+ height="20"
+ className={`${
+ episode?.length > 0
+ ? episode?.some((item) => item?.title === null)
+ ? "fill-[#1c1c22]"
+ : epiView === "1"
+ ? "fill-action"
+ : "fill-[#3A3A44]"
+ : "fill-[#1c1c22]"
+ }`}
+ rx="3"
+ ></rect>
+ </svg>
+ </div>
+ <div
+ className={
+ episode?.length > 0
+ ? episode?.some((item) => item?.title === null)
+ ? "pointer-events-none"
+ : "cursor-pointer"
+ : "pointer-events-none"
+ }
+ onClick={() => {
+ setEpiView("2");
+ localStorage.setItem("epiView", "2");
+ }}
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="33"
+ height="20"
+ fill="none"
+ className={`${
+ episode?.length > 0
+ ? episode?.some((item) => item?.title === null)
+ ? "fill-[#1c1c22]"
+ : epiView === "2"
+ ? "fill-action"
+ : "fill-[#3A3A44]"
+ : "fill-[#1c1c22]"
+ }`}
+ viewBox="0 0 33 20"
+ >
+ <rect width="33" height="7" y="1" rx="3"></rect>
+ <rect width="33" height="7" y="12" rx="3"></rect>
+ </svg>
+ </div>
+ <div
+ className={
+ episode?.length > 0
+ ? `cursor-pointer`
+ : "pointer-events-none"
+ }
+ onClick={() => {
+ setEpiView("3");
+ localStorage.setItem("epiView", "3");
+ }}
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="33"
+ height="20"
+ fill="none"
+ className={`${
+ episode?.length > 0
+ ? epiView === "3"
+ ? "fill-action"
+ : "fill-[#3A3A44]"
+ : "fill-[#1c1c22]"
+ }`}
+ viewBox="0 0 33 20"
+ >
+ <rect width="29" height="4" x="2" y="2" rx="2"></rect>
+ <rect width="29" height="4" x="2" y="8" rx="2"></rect>
+ <rect
+ width="16"
+ height="4"
+ x="2"
+ y="14"
+ rx="2"
+ ></rect>
+ </svg>
+ </div>
+ </div>
+ </div>
+ </div>
+ {!loading ? (
+ Array.isArray(episode) ? (
+ episode && (
+ <div
+ className={`${
+ epiView === "3" &&
+ "scrollbar-thin scrollbar-thumb-[#1b1c21] scrollbar-thumb-rounded-full overflow-y-scroll hover:scrollbar-thumb-[#2e2f37] h-[640px]"
+ }`}
+ >
+ {episode?.length !== 0 && episode ? (
+ <div
+ className={`grid ${
+ epiView === "1"
+ ? "grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-5 lg:gap-8"
+ : "flex flex-col gap-5"
+ } pb-5 pt-2 lg:pt-0 ${
+ epiView === "3" ? "" : "place-items-center"
+ }`}
+ >
+ {epiView === "1"
+ ? episode
+ .slice(firstEpisodeIndex, lastEpisodeIndex)
+ ?.map((epi, index) => {
+ const time = artStorage?.[epi?.id]?.time;
+ const duration =
+ artStorage?.[epi?.id]?.duration;
+ let prog = (time / duration) * 100;
+ if (prog > 90) prog = 100;
+ return (
+ <Link
+ key={index}
+ href={`/en/anime/watch/${epi.id}/${info.id}/${prvValue}`}
+ className="transition-all duration-200 ease-out lg:hover:scale-105 hover:ring-1 hover:ring-white cursor-pointer bg-secondary shrink-0 relative w-full h-[180px] sm:h-[130px] subpixel-antialiased rounded-md overflow-hidden"
+ >
+ <span className="absolute text-sm z-40 bottom-1 left-2 font-karla font-semibold text-white">
+ Episode {epi?.number}
+ </span>
+ <span
+ className={`absolute bottom-7 left-0 h-1 bg-red-600`}
+ style={{
+ width:
+ progress &&
+ artStorage &&
+ epi?.number <= progress
+ ? "100%"
+ : artStorage?.[epi?.id]
+ ? `${prog}%`
+ : "0%",
+ }}
+ />
+ <div className="absolute inset-0 bg-black z-30 opacity-20" />
+ <Image
+ src={epi?.image}
+ alt="epi image"
+ width={500}
+ height={500}
+ className="object-cover w-full h-[150px] sm:h-[100px] z-20"
+ />
+ </Link>
+ );
+ })
+ : ""}
+ {epiView === "2" &&
+ episode
+ .slice(firstEpisodeIndex, lastEpisodeIndex)
+ .map((epi, index) => {
+ const time = artStorage?.[epi?.id]?.time;
+ const duration =
+ artStorage?.[epi?.id]?.duration;
+ let prog = (time / duration) * 100;
+ if (prog > 90) prog = 100;
+ return (
+ <Link
+ key={index}
+ href={`/en/anime/watch/${epi.id}/${info.id}/${prvValue}`}
+ className="flex group h-[110px] lg:h-[160px] w-full rounded-lg transition-all duration-300 ease-out bg-secondary cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white"
+ >
+ <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%]"
+ />
+ <span
+ className={`absolute bottom-0 left-0 h-[3px] bg-red-700`}
+ style={{
+ width:
+ progress &&
+ artStorage &&
+ epi?.number <= progress
+ ? "100%"
+ : artStorage?.[epi?.id]
+ ? `${prog}%`
+ : "0",
+ }}
+ />
+ <span className="absolute bottom-2 left-2 font-karla font-semibold text-sm lg:text-lg">
+ Episode {epi?.number}
+ </span>
+ <div className="z-[9999] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ className="w-5 h-5 invisible group-hover:visible"
+ >
+ <path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
+ </svg>
+ </div>
+ </div>
+ </div>
+
+ <div
+ className={`w-[70%] h-full select-none p-4 flex flex-col justify-center gap-5 ${
+ epi?.id == id ? "text-[#7a7a7a]" : ""
+ }`}
+ >
+ <h1 className="font-karla font-bold text-base lg:text-lg xl:text-xl italic line-clamp-1">
+ {epi?.title}
+ </h1>
+ {epi?.description && (
+ <p className="line-clamp-2 text-xs lg:text-md xl:text-lg italic font-outfit font-extralight">
+ {epi?.description}
+ </p>
+ )}
+ </div>
+ </Link>
+ );
+ })}
+ {epiView === "3" &&
+ episode
+ .slice(firstEpisodeIndex, lastEpisodeIndex)
+ .map((epi, index) => {
+ return (
+ <div
+ key={index}
+ className="flex flex-col gap-3 px-2"
+ >
+ <Link
+ href={`/en/anime/watch/${epi.id}/${info.id}/${prvValue}`}
+ className={`text-start text-sm lg:text-lg ${
+ progress && epi.number <= progress
+ ? "text-[#5f5f5f]"
+ : "text-white"
+ }`}
+ >
+ <p>Episode {epi.number}</p>
+ {epi.title && (
+ <p
+ className={`text-xs lg:text-sm ${
+ progress && epi.number <= progress
+ ? "text-[#5f5f5f]"
+ : "text-[#b1b1b1]"
+ } italic`}
+ >
+ "{epi.title}"
+ </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}</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/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
new file mode 100644
index 0000000..d6e40e1
--- /dev/null
+++ b/pages/en/anime/watch/[...info].js
@@ -0,0 +1,642 @@
+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 { ChevronDownIcon, ForwardIcon } from "@heroicons/react/24/solid";
+import { useRouter } from "next/router";
+
+import { GET_MEDIA_USER } from "../../../../queries";
+
+import dotenv from "dotenv";
+import Navigasi from "../../../../components/home/staticNav";
+import DisqusComments from "../../../../components/disqus";
+
+const VideoPlayer = dynamic(() =>
+ import("../../../../components/videoPlayer", { ssr: false })
+);
+
+export default function Info({ sessions, id, aniId, provider, proxy, api }) {
+ const [epiData, setEpiData] = useState(null);
+ const [data, setAniData] = useState(null);
+ const [skip, setSkip] = useState({ op: null, ed: null });
+ const [statusWatch, setStatusWatch] = useState("CURRENT");
+ const [loading, setLoading] = useState(false);
+ const [showComments, setShowComments] = useState(false);
+
+ const [playing, setPlaying] = useState(null);
+ const [playingEpisode, setPlayingEpisode] = useState(null);
+ const [playingTitle, setPlayingTitle] = useState(null);
+
+ const [poster, setPoster] = useState(null);
+ const [progress, setProgress] = useState(0);
+
+ const [episodes, setEpisodes] = useState([]);
+ const [artStorage, setArtStorage] = useState(null);
+
+ const [url, setUrl] = useState(null);
+
+ const router = useRouter();
+
+ // console.log({ playing });
+
+ useEffect(() => {
+ const defaultState = {
+ epiData: null,
+ skip: { op: null, ed: null },
+ statusWatch: "CURRENT",
+ playingEpisode: null,
+ loading: false,
+ showComments: 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 url = window.location.href;
+ setUrl(url);
+
+ const fetchData = async () => {
+ try {
+ if (provider) {
+ const res = await fetch(
+ `${api}/meta/anilist/watch/${id}?provider=${provider}`
+ );
+ const epiData = await res.json();
+ setEpiData(epiData);
+ } else {
+ const res = await fetch(`${api}/meta/anilist/watch/${id}`);
+ const epiData = await res.json();
+ setEpiData(epiData);
+ }
+ } catch (error) {
+ setTimeout(() => {
+ window.location.reload();
+ }, 3000);
+ }
+
+ let aniData = null;
+ setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
+
+ if (provider) {
+ const res = await fetch(
+ `${api}/meta/anilist/info/${aniId}?provider=${provider}`
+ );
+ aniData = await res.json();
+ setEpisodes(aniData.episodes?.reverse());
+ setAniData(aniData);
+ } else {
+ const res2 = await fetch(`${api}/meta/anilist/info/${aniId}`);
+ aniData = await res2.json();
+ setEpisodes(aniData.episodes?.reverse());
+ setAniData(aniData);
+ }
+
+ let playingEpisode = aniData.episodes
+ .filter((item) => item.id == id)
+ .map((item) => item.number);
+
+ setPlayingEpisode(playingEpisode);
+
+ const playing = aniData.episodes.find((item) => item.id === id);
+
+ setPoster(playing?.image);
+ setPlaying(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
+ ? [{ src: poster, 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?.sources) ? (
+ <div className="aspect-video z-20 bg-black">
+ <VideoPlayer
+ key={id}
+ data={epiData}
+ id={id}
+ progress={parseInt(playingEpisode)}
+ session={sessions}
+ aniId={parseInt(data?.id)}
+ stats={statusWatch}
+ op={skip.op}
+ ed={skip.ed}
+ title={playingTitle}
+ poster={poster}
+ proxy={proxy}
+ provider={provider}
+ />
+ </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.id == id)
+ .map((item, index) => (
+ <div className="flex justify-between" key={index}>
+ <div key={item.id} className="p-3 grid gap-2 w-[60%]">
+ <div className="text-xl font-outfit font-semibold line-clamp-1">
+ <Link
+ href={`/en/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(
+ `/en/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(
+ `/en/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>
+ {!showComments && loading && (
+ <div className="w-full flex justify-center py-5 font-karla px-3 lg:px-0">
+ <button
+ onClick={() => setShowComments(true)}
+ className={
+ showComments
+ ? "hidden"
+ : "flex-center gap-2 h-10 bg-secondary rounded w-full lg:w-[50%]"
+ }
+ >
+ Load Disqus{" "}
+ <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="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
+ />
+ </svg>
+ </button>
+ </div>
+ )}
+ {showComments && (
+ <div>
+ {data && url && playing && (
+ <div className="mt-5 px-5">
+ <DisqusComments
+ key={id}
+ post={{
+ id: id,
+ title: data.title.romaji,
+ url: url,
+ episode: playing.number,
+ }}
+ />
+ </div>
+ )}
+ </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 ? (
+ data.episodes.some(
+ (item) => item.title && item.description
+ ) ? (
+ episodes.map((item) => {
+ const time = artStorage?.[item.id]?.time;
+ const duration = artStorage?.[item.id]?.duration;
+ let prog = (time / duration) * 100;
+ if (prog > 90) prog = 100;
+ return (
+ <Link
+ href={`/en/anime/watch/${item.id}/${data.id}${
+ provider ? `/${provider}` : ""
+ }`}
+ key={item.id}
+ className={`bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out ${
+ item.id == id
+ ? "pointer-events-none ring-1 ring-action"
+ : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white"
+ }`}
+ >
+ <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">
+ <Image
+ src={item.image}
+ alt="Anime Cover"
+ width={1000}
+ height={1000}
+ className={`object-cover z-30 rounded-lg h-[110px] ${
+ item.id == id
+ ? "brightness-[30%]"
+ : "brightness-75"
+ }`}
+ />
+ <span
+ className={`absolute bottom-0 left-0 h-[3px] bg-red-700`}
+ style={{
+ width:
+ progress &&
+ artStorage &&
+ item?.number <= progress
+ ? "100%"
+ : artStorage?.[item?.id]
+ ? `${prog}%`
+ : "0",
+ }}
+ />
+ <span className="absolute bottom-2 left-2 font-karla font-bold text-sm">
+ Episode {item.number}
+ </span>
+ {item.id == id && (
+ <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ className="w-5 h-5"
+ >
+ <path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
+ </svg>
+ </div>
+ )}
+ </div>
+ </div>
+ <div
+ className={`w-[70%] h-full select-none p-4 flex flex-col gap-2 ${
+ item.id == id ? "text-[#7a7a7a]" : ""
+ }`}
+ >
+ <h1 className="font-karla font-bold italic line-clamp-1">
+ {item.title}
+ </h1>
+ <p className="line-clamp-2 text-xs italic font-outfit font-extralight">
+ {item.description}
+ </p>
+ </div>
+ </Link>
+ );
+ })
+ ) : (
+ data.episodes.map((item) => {
+ return (
+ <Link
+ href={`/en/anime/watch/${item.id}/${data.id}${
+ provider ? "/9anime" : ""
+ }`}
+ key={item.id}
+ className={`bg-secondary flex-center w-full h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${
+ item.id == id
+ ? "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 {item.number}
+ </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];
+ const provider = info[2] || null;
+
+ return {
+ props: {
+ sessions: session,
+ id,
+ aniId,
+ provider,
+ proxy,
+ api: API_URI,
+ },
+ };
+}