aboutsummaryrefslogtreecommitdiff
path: root/components/anime
diff options
context:
space:
mode:
Diffstat (limited to 'components/anime')
-rw-r--r--components/anime/changeView.js107
-rw-r--r--components/anime/episode.js281
-rw-r--r--components/anime/infoDetails.js203
-rw-r--r--components/anime/mobile/topSection.js81
-rw-r--r--components/anime/viewMode/listMode.js39
-rw-r--r--components/anime/viewMode/thumbnailDetail.js76
-rw-r--r--components/anime/viewMode/thumbnailOnly.js59
-rw-r--r--components/anime/watch/primary/details.js177
-rw-r--r--components/anime/watch/primarySide.js213
-rw-r--r--components/anime/watch/secondarySide.js129
10 files changed, 1365 insertions, 0 deletions
diff --git a/components/anime/changeView.js b/components/anime/changeView.js
new file mode 100644
index 0000000..cab9054
--- /dev/null
+++ b/components/anime/changeView.js
@@ -0,0 +1,107 @@
+import { useEffect, useState } from "react";
+
+export default function ChangeView({ view, setView, episode }) {
+ // const [view, setView] = useState(1);
+ // const episode = null;
+ return (
+ <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={() => {
+ setView(1);
+ localStorage.setItem("view", 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]"
+ : view === 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={() => {
+ setView(2);
+ localStorage.setItem("view", 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]"
+ : view === 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={() => {
+ setView(3);
+ localStorage.setItem("view", 3);
+ }}
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="33"
+ height="20"
+ fill="none"
+ className={`${
+ episode?.length > 0
+ ? view === 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>
+ );
+}
diff --git a/components/anime/episode.js b/components/anime/episode.js
new file mode 100644
index 0000000..c889c25
--- /dev/null
+++ b/components/anime/episode.js
@@ -0,0 +1,281 @@
+import { useEffect, useState, Fragment } from "react";
+import { ChevronDownIcon, ClockIcon } from "@heroicons/react/20/solid";
+import { convertSecondsToTime } from "../../utils/getTimes";
+import ChangeView from "./changeView";
+import ThumbnailOnly from "./viewMode/thumbnailOnly";
+import ThumbnailDetail from "./viewMode/thumbnailDetail";
+import ListMode from "./viewMode/listMode";
+import axios from "axios";
+
+export default function AnimeEpisode({ info, progress }) {
+ const [providerId, setProviderId] = useState(); // default provider
+ const [currentPage, setCurrentPage] = useState(1); // for pagination
+ const [visible, setVisible] = useState(false); // for mobile view
+ const itemsPerPage = 13; // choose your number of items per page
+
+ const [loading, setLoading] = useState(true);
+ const [artStorage, setArtStorage] = useState(null);
+ const [view, setView] = useState(3);
+ const [isDub, setIsDub] = useState(false);
+
+ const [providers, setProviders] = useState(null);
+
+ useEffect(() => {
+ setLoading(true);
+ setProviders(null);
+ const fetchData = async () => {
+ try {
+ const { data: firstResponse } = await axios.get(
+ `/api/consumet/episode/${info.id}${isDub === true ? "?dub=true" : ""}`
+ );
+ if (firstResponse.data.length > 0) {
+ const defaultProvider = firstResponse.data?.find(
+ (x) => x.providerId === "gogoanime"
+ );
+ setProviderId(
+ defaultProvider?.providerId || firstResponse.data[0].providerId
+ ); // set to first provider id
+ }
+
+ setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
+ setProviders(firstResponse.data);
+ setLoading(false);
+ } catch (error) {
+ setLoading(false);
+ setProviders([]);
+ }
+ };
+ fetchData();
+ }, [info.id, isDub]);
+
+ const episodes =
+ providers?.find((provider) => provider.providerId === providerId)
+ ?.episodes || [];
+
+ const lastEpisodeIndex = currentPage * itemsPerPage;
+ const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage;
+ const currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex);
+ const totalPages = Math.ceil(episodes.length / itemsPerPage);
+
+ const handleChange = (event) => {
+ setProviderId(event.target.value);
+ };
+
+ const handlePageChange = (pageNumber) => {
+ setCurrentPage(pageNumber);
+ };
+
+ useEffect(() => {
+ if (episodes?.some((item) => item?.title === null)) {
+ setView(3);
+ }
+ }, [providerId, episodes]);
+
+ return (
+ <>
+ <div className="flex flex-col gap-5 px-3">
+ <div className="flex lg:flex-row flex-col gap-5 lg:gap-0 justify-between ">
+ <div className="flex justify-between">
+ <div className="flex items-center lg:gap-10 sm:gap-7 gap-3">
+ {info && (
+ <h1 className="text-[20px] lg:text-2xl font-bold font-karla">
+ Episodes
+ </h1>
+ )}
+ {info?.nextAiringEpisode && (
+ <div className="flex items-center gap-2">
+ <div className="flex items-center gap-4 text-[10px] xxs:text-sm lg:text-base">
+ <h1>Next :</h1>
+ <div className="px-4 rounded-sm font-karla font-bold bg-white text-black">
+ {convertSecondsToTime(
+ info.nextAiringEpisode.timeUntilAiring
+ )}
+ </div>
+ </div>
+ <div className="h-6 w-6">
+ <ClockIcon />
+ </div>
+ </div>
+ )}
+ </div>
+
+ <div className="flex items-center gap-2">
+ <div
+ onClick={() => setIsDub((prev) => !prev)}
+ className="flex lg:hidden flex-col items-center relative rounded-md bg-secondary py-1.5 px-3 font-karla text-sm hover:ring-1 ring-action cursor-pointer group"
+ >
+ {isDub ? "Dub" : "Sub"}
+ <span className="absolute invisible opacity-0 group-hover:opacity-100 group-hover:scale-100 scale-0 group-hover:-translate-y-7 translate-y-0 group-hover:visible rounded-sm shadow top-0 w-28 bg-secondary text-center transition-all transform duration-200 ease-out">
+ Switch to {isDub ? "Sub" : "Dub"}
+ </span>
+ </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>
+ <div
+ className={`flex lg:flex gap-3 items-center justify-between ${
+ visible ? "" : "hidden"
+ }`}
+ >
+ {providers && (
+ <div
+ onClick={() => setIsDub((prev) => !prev)}
+ className="hidden lg:flex flex-col items-center relative rounded-[3px] bg-secondary py-1 px-3 font-karla text-sm hover:ring-1 ring-action cursor-pointer group"
+ >
+ {isDub ? "Dub" : "Sub"}
+ <span className="absolute invisible opacity-0 group-hover:opacity-100 group-hover:scale-100 scale-0 group-hover:-translate-y-7 translate-y-0 group-hover:visible rounded-sm shadow top-0 w-28 bg-secondary text-center transition-all transform duration-200 ease-out">
+ Switch to {isDub ? "Sub" : "Dub"}
+ </span>
+ </div>
+ )}
+ {providers && providers.length > 0 && (
+ <>
+ <div className="flex gap-3">
+ <div className="relative flex gap-2 items-center group">
+ <select
+ title="Providers"
+ onChange={handleChange}
+ value={providerId}
+ 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 group-hover:ring-1 group-hover:ring-action"
+ >
+ {providers.map((provider) => (
+ <option
+ key={provider.providerId}
+ value={provider.providerId}
+ >
+ {provider.providerId}
+ </option>
+ ))}
+ </select>
+ {/* <span className="absolute invisible opacity-0 group-hover:opacity-100 group-hover:scale-100 scale-0 group-hover:-translate-y-7 translate-y-0 group-hover:visible rounded-sm shadow top-0 w-32 bg-secondary text-center transition-all transform duration-200 ease-out">
+ Select Providers
+ </span> */}
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
+ </div>
+
+ {totalPages > 1 && (
+ <div className="relative flex gap-2 items-center">
+ <select
+ title="Pages"
+ onChange={(e) =>
+ handlePageChange(Number(e.target.value))
+ }
+ 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 hover:ring-1 hover:ring-action"
+ >
+ {[...Array(totalPages)].map((_, i) => (
+ <option key={i} value={i + 1}>
+ {i + 1}
+ </option>
+ ))}
+ </select>
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
+ </div>
+ )}
+ </div>
+ </>
+ )}
+
+ <ChangeView
+ view={view}
+ setView={setView}
+ episode={currentEpisodes}
+ />
+ </div>
+ </div>
+
+ {/* Episodes */}
+ {!loading ? (
+ <div
+ className={
+ view === 1
+ ? "grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-5 lg:gap-8 place-items-center"
+ : `flex flex-col gap-3`
+ }
+ >
+ {Array.isArray(providers) ? (
+ providers.length > 0 ? (
+ currentEpisodes.map((episode, index) => {
+ return (
+ <Fragment key={index}>
+ {view === 1 && (
+ <ThumbnailOnly
+ key={index}
+ index={index}
+ info={info}
+ providerId={providerId}
+ episode={episode}
+ artStorage={artStorage}
+ progress={progress}
+ dub={isDub}
+ // image={thumbnail}
+ />
+ )}
+ {view === 2 && (
+ <ThumbnailDetail
+ key={index}
+ index={index}
+ epi={episode}
+ provider={providerId}
+ info={info}
+ artStorage={artStorage}
+ progress={progress}
+ dub={isDub}
+ />
+ )}
+ {view === 3 && (
+ <ListMode
+ key={index}
+ info={info}
+ episode={episode}
+ index={index}
+ providerId={providerId}
+ progress={progress}
+ dub={isDub}
+ />
+ )}
+ </Fragment>
+ );
+ })
+ ) : (
+ <div className="h-[20vh] lg:w-full flex-center flex-col gap-5">
+ <p className="text-center font-karla font-bold lg:text-lg">
+ Oops!<br></br> It looks like this anime is not available.
+ </p>
+ </div>
+ )
+ ) : (
+ <p>{providers.message}</p>
+ )}
+ </div>
+ ) : (
+ <div className="flex justify-center">
+ <div className="lds-ellipsis">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+ </div>
+ )}
+ </div>
+ </>
+ );
+}
diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js
new file mode 100644
index 0000000..0cf233c
--- /dev/null
+++ b/components/anime/infoDetails.js
@@ -0,0 +1,203 @@
+import Image from "next/image";
+import Link from "next/link";
+import Skeleton from "react-loading-skeleton";
+
+export default function DesktopDetails({
+ info,
+ statuses,
+ handleOpen,
+ loading,
+ color,
+ setShowAll,
+ showAll,
+}) {
+ return (
+ <>
+ <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>
+
+ <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>
+ </>
+ );
+}
diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js
new file mode 100644
index 0000000..4f7c4b3
--- /dev/null
+++ b/components/anime/mobile/topSection.js
@@ -0,0 +1,81 @@
+import { HeartIcon } from "@heroicons/react/20/solid";
+
+import {
+ TvIcon,
+ ArrowTrendingUpIcon,
+ RectangleStackIcon,
+} from "@heroicons/react/24/outline";
+
+export default function DetailTop({ info, statuses, handleOpen, loading }) {
+ return (
+ <div className="lg:hidden pt-5 w-screen px-5 flex flex-col">
+ <div className="h-[250px] flex flex-col gap-1 justify-center">
+ <h1 className="font-karla font-extrabold text-lg line-clamp-1 w-[70%]">
+ {info?.title?.romaji || info?.title?.english}
+ </h1>
+ <p
+ className="line-clamp-2 text-sm font-light antialiased w-[56%]"
+ dangerouslySetInnerHTML={{ __html: info?.description }}
+ />
+ <div className="font-light flex gap-1 py-1 flex-wrap font-outfit text-[10px] text-[#ffffff] w-[70%]">
+ {info?.genres
+ ?.slice(0, info?.genres?.length > 3 ? info?.genres?.length : 3)
+ .map((item, index) => (
+ <span
+ key={index}
+ className="px-2 py-1 bg-secondary shadow-lg font-outfit font-light rounded-full"
+ >
+ <span>{item}</span>
+ </span>
+ ))}
+ </div>
+ {info && (
+ <div className="flex items-center gap-5 pt-3 text-center">
+ <div className="flex items-center gap-2 text-center">
+ <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>
+ );
+}
diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js
new file mode 100644
index 0000000..2016262
--- /dev/null
+++ b/components/anime/viewMode/listMode.js
@@ -0,0 +1,39 @@
+import Link from "next/link";
+
+export default function ListMode({
+ info,
+ episode,
+ index,
+ providerId,
+ progress,
+ dub,
+}) {
+ return (
+ <div key={episode.number} className="flex flex-col gap-3 px-2">
+ <Link
+ href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent(
+ episode.id
+ )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`}
+ className={`text-start text-sm lg:text-lg ${
+ progress && episode.number <= progress
+ ? "text-[#5f5f5f]"
+ : "text-white"
+ }`}
+ >
+ <p>Episode {episode.number}</p>
+ {episode.title && (
+ <p
+ className={`text-xs lg:text-sm ${
+ progress && episode.number <= progress
+ ? "text-[#5f5f5f]"
+ : "text-[#b1b1b1]"
+ } italic`}
+ >
+ "{episode.title}"
+ </p>
+ )}
+ </Link>
+ {index !== episode?.length - 1 && <span className="h-[1px] bg-white" />}
+ </div>
+ );
+}
diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js
new file mode 100644
index 0000000..a085bc7
--- /dev/null
+++ b/components/anime/viewMode/thumbnailDetail.js
@@ -0,0 +1,76 @@
+import Image from "next/image";
+import Link from "next/link";
+
+export default function ThumbnailDetail({
+ index,
+ epi,
+ info,
+ provider,
+ artStorage,
+ progress,
+ dub,
+}) {
+ 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/${info.id}/${provider}?id=${encodeURIComponent(
+ epi.id
+ )}&num=${epi.number}${dub ? `&dub=${dub}` : ""}`}
+ 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-3`}
+ >
+ <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>
+ );
+}
diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js
new file mode 100644
index 0000000..6063dfc
--- /dev/null
+++ b/components/anime/viewMode/thumbnailOnly.js
@@ -0,0 +1,59 @@
+import Image from "next/image";
+import Link from "next/link";
+
+export default function ThumbnailOnly({
+ info,
+ providerId,
+ episode,
+ artStorage,
+ progress,
+ dub,
+}) {
+ const time = artStorage?.[episode?.id]?.time;
+ const duration = artStorage?.[episode?.id]?.duration;
+ let prog = (time / duration) * 100;
+ if (prog > 90) prog = 100;
+ return (
+ <Link
+ // key={index}
+ href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent(
+ episode.id
+ )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`}
+ 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 {episode?.number}
+ </span>
+ <span
+ className={`absolute bottom-7 left-0 h-1 bg-red-600`}
+ style={{
+ width:
+ progress && artStorage && episode?.number <= progress
+ ? "100%"
+ : artStorage?.[episode?.id]
+ ? `${prog}%`
+ : "0%",
+ }}
+ />
+ <div className="absolute inset-0 bg-black z-30 opacity-20" />
+ <Image
+ // src={
+ // providerId === "animepahe"
+ // ? `https://img.moopa.live/image-proxy?url=${encodeURIComponent(
+ // episode.img
+ // )}&headers=${encodeURIComponent(
+ // JSON.stringify({ Referer: "https://animepahe.com/" })
+ // )}`
+ // : thumbnail?.img.includes("null")
+ // ? info.coverImage.large
+ // : thumbnail?.img || info.coverImage.large
+ // }
+ src={episode?.image}
+ alt="epi image"
+ width={500}
+ height={500}
+ className="object-cover w-full h-[150px] sm:h-[100px] z-20"
+ />
+ </Link>
+ );
+}
diff --git a/components/anime/watch/primary/details.js b/components/anime/watch/primary/details.js
new file mode 100644
index 0000000..94c3360
--- /dev/null
+++ b/components/anime/watch/primary/details.js
@@ -0,0 +1,177 @@
+import { useEffect, useState } from "react";
+import { useAniList } from "../../../../lib/anilist/useAnilist";
+import Skeleton from "react-loading-skeleton";
+import DisqusComments from "../../../disqus";
+import Image from "next/image";
+
+export default function Details({
+ info,
+ session,
+ epiNumber,
+ id,
+ onList,
+ setOnList,
+ handleOpen,
+ disqus,
+}) {
+ const [showComments, setShowComments] = useState(false);
+ const { markPlanning } = useAniList(session);
+ const [url, setUrl] = useState(null);
+
+ function handlePlan() {
+ if (onList === false) {
+ markPlanning(info.id);
+ setOnList(true);
+ }
+ }
+
+ useEffect(() => {
+ const url = window.location.href;
+ setShowComments(false);
+ setUrl(url);
+ }, [id]);
+
+ return (
+ <div className="flex flex-col gap-2">
+ <div className="px-4 pt-7 pb-4 h-full flex">
+ <div className="aspect-[9/13] h-[240px]">
+ {info ? (
+ <Image
+ src={info.coverImage.extraLarge}
+ 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 pl-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">
+ {info ? info.studios.edges[0].node.name : <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"
+ onClick={() => {
+ session ? handlePlan() : handleOpen();
+ }}
+ className={`w-8 h-8 hover:fill-white text-white hover:cursor-pointer ${
+ onList ? "fill-white" : ""
+ }`}
+ >
+ <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>{info ? info.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">
+ {info ? (
+ <>
+ <div className="line-clamp-3">{info.title?.romaji || ""}</div>
+ <div className="line-clamp-3">
+ {info.title?.english || ""}
+ </div>
+ <div className="line-clamp-3">{info.title?.native || ""}</div>
+ </>
+ ) : (
+ <Skeleton width={200} height={50} />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="flex flex-wrap gap-3 px-4 pt-3">
+ {info &&
+ info.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`}>
+ {info && (
+ <p
+ dangerouslySetInnerHTML={{ __html: info?.description }}
+ className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `}
+ />
+ )}
+ </div>
+ {/* {<div className="mt-5 px-5"></div>} */}
+ {!showComments && (
+ <div className="w-full flex justify-center py-2 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>
+ {info && url && (
+ <div className="mt-5 px-5">
+ <DisqusComments
+ key={id}
+ post={{
+ id: id,
+ title: info.title.romaji,
+ url: url,
+ episode: epiNumber,
+ name: disqus,
+ }}
+ />
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js
new file mode 100644
index 0000000..49bb1b6
--- /dev/null
+++ b/components/anime/watch/primarySide.js
@@ -0,0 +1,213 @@
+import { useEffect, useState } from "react";
+import { ChevronDownIcon } from "@heroicons/react/20/solid";
+import { ForwardIcon } from "@heroicons/react/24/solid";
+import { useRouter } from "next/router";
+import { signIn } from "next-auth/react";
+import Details from "./primary/details";
+import VideoPlayer from "../../videoPlayer";
+import Link from "next/link";
+import Skeleton from "react-loading-skeleton";
+import Modal from "../../modal";
+import AniList from "../../media/aniList";
+import axios from "axios";
+
+export default function PrimarySide({
+ info,
+ session,
+ epiNumber,
+ setLoading,
+ navigation,
+ loading,
+ providerId,
+ watchId,
+ status,
+ onList,
+ proxy,
+ disqus,
+ setOnList,
+ episodeList,
+}) {
+ const [episodeData, setEpisodeData] = useState();
+ const [open, setOpen] = useState(false);
+ const [skip, setSkip] = useState();
+
+ const router = useRouter();
+
+ useEffect(() => {
+ setLoading(true);
+ setEpisodeData();
+ setSkip();
+ async function fetchData() {
+ if (info) {
+ const { data } = await axios.get(
+ `/api/consumet/source/${providerId}/${watchId}`
+ );
+
+ const skip = await fetch(
+ `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt(
+ epiNumber
+ )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=`
+ ).then((r) => {
+ if (!r.ok) {
+ switch (r.status) {
+ case 404: {
+ return null;
+ }
+ }
+ }
+ return r.json();
+ });
+
+ const op =
+ skip?.results?.find((item) => item.skipType === "op") || null;
+ const ed =
+ skip?.results?.find((item) => item.skipType === "ed") || null;
+
+ setSkip({ op, ed });
+
+ setEpisodeData(data);
+ setLoading(false);
+ }
+ // setMal(malId);
+ }
+
+ fetchData();
+ }, [providerId, watchId, info]);
+
+ function handleOpen() {
+ setOpen(true);
+ document.body.style.overflow = "hidden";
+ }
+
+ function handleClose() {
+ setOpen(false);
+ document.body.style.overflow = "auto";
+ }
+
+ return (
+ <>
+ <Modal open={open} onClose={() => handleClose()}>
+ {!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>
+ )}
+ </Modal>
+ <div className="w-full h-full">
+ <div key={watchId} className="w-full aspect-video bg-black">
+ {!loading ? (
+ episodeData && (
+ <VideoPlayer
+ session={session}
+ data={episodeData}
+ provider={providerId}
+ id={watchId}
+ progress={epiNumber}
+ stats={status}
+ skip={skip}
+ proxy={proxy}
+ aniId={info.id}
+ />
+ )
+ ) : (
+ <div className="aspect-video bg-black" />
+ )}
+ </div>
+ <div className="flex flex-col divide-y divide-white/20">
+ {info && episodeList ? (
+ <div className="flex items-center justify-between py-3 px-3">
+ <div className="flex flex-col gap-2 w-[60%]">
+ <h1 className="text-xl font-outfit font-semibold line-clamp-1">
+ <Link
+ href={`/en/anime/${info.id}`}
+ className="hover:underline"
+ >
+ {navigation?.playing?.title || info.title?.romaji}
+ </Link>
+ </h1>
+ <h1 className="text-sm font-karla font-light">
+ Episode {epiNumber}
+ </h1>
+ </div>
+ <div className="flex gap-4 items-center justify-end">
+ <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={epiNumber}
+ onChange={(e) => {
+ const selectedEpisode = episodeList.find(
+ (episode) => episode.number === parseInt(e.target.value)
+ );
+ router.push(
+ `/en/anime/watch/${info.id}/${providerId}?id=${selectedEpisode.id}&num=${selectedEpisode.number}`
+ );
+ }}
+ >
+ {episodeList.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
+ disabled={!navigation?.next}
+ className={`${
+ !navigation?.next ? "pointer-events-none" : ""
+ }relative group`}
+ onClick={() => {
+ router.push(
+ `/en/anime/watch/${info.id}/${providerId}?id=${navigation?.next.id}&num=${navigation?.next.number}`
+ );
+ }}
+ >
+ <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 ${
+ !navigation?.next ? "text-[#282828]" : ""
+ }`}
+ />
+ </button>
+ </div>
+ </div>
+ ) : (
+ <div className="py-3 px-4">
+ <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>
+ )}
+ <Details
+ info={info}
+ session={session}
+ epiNumber={epiNumber}
+ id={watchId}
+ onList={onList}
+ setOnList={setOnList}
+ handleOpen={handleOpen}
+ disqus={disqus}
+ />
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js
new file mode 100644
index 0000000..e3f0224
--- /dev/null
+++ b/components/anime/watch/secondarySide.js
@@ -0,0 +1,129 @@
+import Skeleton from "react-loading-skeleton";
+import Image from "next/image";
+import Link from "next/link";
+
+export default function SecondarySide({
+ info,
+ providerId,
+ watchId,
+ episode,
+ progress,
+ artStorage,
+ dub,
+}) {
+ return (
+ <div className="lg:w-[35%] shrink-0 w-screen">
+ <h1 className="text-xl font-karla pl-4 pb-5 font-semibold">Up Next</h1>
+ <div className="flex flex-col gap-5 lg:pl-5 py-2 scrollbar-thin px-2 scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full">
+ {episode && episode.length > 0 ? (
+ episode.some((item) => item.title && item.description) > 0 ? (
+ episode.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/${
+ info.id
+ }/${providerId}?id=${encodeURIComponent(item.id)}&num=${
+ item.number
+ }${dub ? `&dub=${dub}` : ""}`}
+ key={item.id}
+ className={`bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out ${
+ item.id == watchId
+ ? "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 == watchId
+ ? "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 == watchId && (
+ <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 == watchId ? "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>
+ );
+ })
+ ) : (
+ episode.map((item) => {
+ return (
+ <Link
+ href={`/en/anime/watch/${
+ info.id
+ }/${providerId}?id=${encodeURIComponent(item.id)}&num=${
+ item.number
+ }${dub ? `&dub=${dub}` : ""}`}
+ key={item.id}
+ className={`bg-secondary flex-center w-full h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${
+ item.id == watchId
+ ? "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>
+ );
+}