aboutsummaryrefslogtreecommitdiff
path: root/pages/en/anime
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-09-13 00:45:53 +0700
committerGitHub <[email protected]>2023-09-13 00:45:53 +0700
commit7327a69b55a20b99b14ee0803d6cf5f8b88c45ef (patch)
treecbcca777593a8cc4b0282e7d85a6fc51ba517e25 /pages/en/anime
parentUpdate issue templates (diff)
downloadmoopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.tar.xz
moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.zip
Update v4 - Merge pre-push to main (#71)
* Create build-test.yml * initial v4 commit * update: github workflow * update: push on branch * Update .github/ISSUE_TEMPLATE/bug_report.md * configuring next.config.js file
Diffstat (limited to 'pages/en/anime')
-rw-r--r--pages/en/anime/[...id].js252
-rw-r--r--pages/en/anime/popular.js21
-rw-r--r--pages/en/anime/recent.js163
-rw-r--r--pages/en/anime/recently-watched.js130
-rw-r--r--pages/en/anime/trending.js21
-rw-r--r--pages/en/anime/watch/[...info].js240
6 files changed, 545 insertions, 282 deletions
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
index 534aa17..71dae56 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -2,7 +2,6 @@ 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 Content from "../../../components/home/content";
import Modal from "../../../components/modal";
@@ -10,22 +9,26 @@ 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 DetailTop from "../../../components/anime/mobile/topSection";
-import DesktopDetails from "../../../components/anime/infoDetails";
import AnimeEpisode from "../../../components/anime/episode";
+import { useAniList } from "../../../lib/anilist/useAnilist";
+import Footer from "../../../components/footer";
+import { mediaInfoQuery } from "../../../lib/graphql/query";
+import MobileNav from "../../../components/shared/MobileNav";
+import redis from "../../../lib/redis";
export default function Info({ info, color }) {
const { data: session } = useSession();
+ const { getUserLists } = useAniList(session);
+
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 [watch, setWatch] = useState();
+
const [open, setOpen] = useState(false);
const { id } = useRouter().query;
@@ -45,40 +48,20 @@ export default function Info({ info, color }) {
setStatuses(null);
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 res = await getUserLists(info.id);
+ const user = res?.data?.Media?.mediaListEntry;
- 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 (user) {
+ setProgress(user.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[user.status]);
}
}
} catch (error) {
@@ -109,6 +92,14 @@ export default function Info({ info, color }) {
? info?.title?.romaji || info?.title?.english
: "Retrieving Data..."}
</title>
+ <meta
+ name="title"
+ content={info?.title?.romaji}
+ data-title-romaji={info?.title?.romaji}
+ data-title-english={info?.title?.english}
+ data-title-native={info?.title?.native}
+ />
+ <meta name="description" content={info.description} />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
@@ -159,62 +150,43 @@ export default function Info({ info, color }) {
)}
</div>
</Modal>
- <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 ? (
- <>
- {info?.bannerImage && (
- <Image
- src={info?.bannerImage}
- priority={true}
- alt="banner anime"
- height={1000}
- width={1000}
- className="hidden md:block object-cover bg-image w-screen absolute top-0 left-0 h-[300px] brightness-[70%] z-0"
- />
- )}
- <Image
- src={info?.coverImage.extraLarge || info?.coverImage.large}
- priority={true}
- alt="banner anime"
- height={1000}
- width={1000}
- className="md:hidden 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 Anime Information */}
-
- <DetailTop
- info={info}
- handleOpen={handleOpen}
- loading={loading}
- statuses={statuses}
- />
-
- {/* PC Anime Information*/}
- <DesktopDetails
- info={info}
- color={color}
- handleOpen={handleOpen}
- loading={loading}
- statuses={statuses}
- setShowAll={setShowAll}
- showAll={showAll}
+ <MobileNav sessions={session} hideProfile={true} />
+ <main className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5">
+ <div className="w-screen absolute">
+ <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" />
+ {info?.bannerImage && (
+ <Image
+ src={info?.bannerImage}
+ priority={true}
+ alt="banner anime"
+ height={1000}
+ width={1000}
+ className="object-cover blur-[2px] bg-image w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0"
/>
+ )}
+ </div>
+ <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5">
+ <DetailTop
+ info={info}
+ session={session}
+ handleOpen={handleOpen}
+ loading={loading}
+ statuses={statuses}
+ watchUrl={watch}
+ progress={progress}
+ color={color}
+ />
- {/* Episodes */}
+ <AnimeEpisode
+ info={info}
+ session={session}
+ progress={progress}
+ setProgress={setProgress}
+ setWatch={setWatch}
+ />
- <AnimeEpisode info={info} progress={progress} />
- </div>
{info && rec?.length !== 0 && (
- <div className="w-screen lg:w-[90%] xl:w-[85%]">
+ <div className="w-full">
<Content
ids="recommendAnime"
section="Recommendations"
@@ -223,51 +195,85 @@ export default function Info({ info, color }) {
</div>
)}
</div>
- </Layout>
+ </main>
+ <Footer />
</>
);
}
-export async function getServerSideProps(context) {
- const { id } = context.query;
+export async function getServerSideProps(ctx) {
+ const { id } = ctx.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],
- },
- }),
- });
+ let cache;
- const json = await res.json();
- const data = json?.data?.Media;
+ if (redis) {
+ cache = await redis.get(`anime:${id}`);
+ }
- if (!data) {
+ if (cache) {
+ const { info, color } = JSON.parse(cache);
return {
- notFound: true,
+ props: {
+ info,
+ color,
+ api: API_URI,
+ },
};
- }
+ } else {
+ const resp = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ query: mediaInfoQuery,
+ variables: {
+ id: id?.[0],
+ },
+ }),
+ });
+
+ const json = await resp.json();
+ const data = json?.data?.Media;
+
+ const cacheTime = data.nextAiringEpisode?.episode
+ ? 60 * 10
+ : 60 * 60 * 24 * 30;
+
+ if (!data) {
+ return {
+ notFound: true,
+ };
+ }
- const textColor = setTxtColor(data?.coverImage?.color);
+ const textColor = setTxtColor(data?.coverImage?.color);
- const color = {
- backgroundColor: `${data?.coverImage?.color || "#ffff"}`,
- color: textColor,
- };
+ const color = {
+ backgroundColor: `${data?.coverImage?.color || "#ffff"}`,
+ color: textColor,
+ };
+
+ if (redis) {
+ await redis.set(
+ `anime:${id}`,
+ JSON.stringify({
+ info: data,
+ color: color,
+ }),
+ "EX",
+ cacheTime
+ );
+ }
- return {
- props: {
- info: data,
- color: color,
- api: API_URI,
- },
- };
+ return {
+ props: {
+ info: data,
+ color: color,
+ api: API_URI,
+ },
+ };
+ }
}
function getBrightness(hexColor) {
diff --git a/pages/en/anime/popular.js b/pages/en/anime/popular.js
index 8cbbeab..7b40a0e 100644
--- a/pages/en/anime/popular.js
+++ b/pages/en/anime/popular.js
@@ -1,12 +1,13 @@
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
-import { useEffect, useState } from "react";
+import { Fragment, useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import MobileNav from "../../../components/home/mobileNav";
+import Head from "next/head";
+import MobileNav from "../../../components/shared/MobileNav";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
@@ -94,9 +95,17 @@ export default function PopularAnime({ sessions }) {
}, [page, nextPage]);
return (
- <>
+ <Fragment>
+ <Head>
+ <title>Moopa - Popular Anime</title>
+ <meta name="title" content="Popular Anime" />
+ <meta
+ name="description"
+ content="Explore Beloved Classics and Favorites - Dive into a curated collection of timeless anime on Moopa's Popular Anime Page. From iconic classics to all-time favorites, experience the stories that have captured hearts worldwide. Start streaming now and relive the magic of anime!"
+ />
+ </Head>
<MobileNav sessions={sessions} />
- <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
+ <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
<div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
<ChevronLeftIcon className="w-5 h-5" />
@@ -165,9 +174,9 @@ export default function PopularAnime({ sessions }) {
Load More
</button>
)}
- </div>
+ </main>
<Footer />
- </>
+ </Fragment>
);
}
diff --git a/pages/en/anime/recent.js b/pages/en/anime/recent.js
new file mode 100644
index 0000000..89a868a
--- /dev/null
+++ b/pages/en/anime/recent.js
@@ -0,0 +1,163 @@
+import Head from "next/head";
+import { Fragment, useEffect, useState } from "react";
+import Link from "next/link";
+import { ChevronLeftIcon } from "@heroicons/react/24/outline";
+import Skeleton from "react-loading-skeleton";
+import Footer from "../../../components/footer";
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../api/auth/[...nextauth]";
+import Image from "next/image";
+import MobileNav from "../../../components/shared/MobileNav";
+
+export async function getServerSideProps(context) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ return {
+ props: {
+ sessions: session,
+ },
+ };
+}
+
+export default function Recent({ sessions }) {
+ const [data, setData] = useState(null);
+ const [page, setPage] = useState(1);
+ const [nextPage, setNextPage] = useState(true);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setLoading(true);
+ async function getRecent() {
+ const data = await fetch(`/api/v2/etc/recent/${page}`).then((res) =>
+ res.json()
+ );
+ if (data?.results?.length === 0) {
+ setNextPage(false);
+ } else if (data !== null && page > 1) {
+ setData((prevData) => {
+ return [...(prevData ?? []), ...data?.results];
+ });
+ setNextPage(data?.hasNextPage);
+ } else {
+ setData(data?.results);
+ }
+ setNextPage(data?.hasNextPage);
+ setLoading(false);
+ }
+ getRecent();
+ }, [page]);
+
+ useEffect(() => {
+ function handleScroll() {
+ if (page > 5 || !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]);
+
+ return (
+ <Fragment>
+ <Head>
+ <title>Moopa - New Episodes</title>
+ <meta name="title" content="New Episodes" />
+ <meta
+ name="description"
+ content="Explore Beloved Classics and Favorites - Dive into a curated collection of timeless anime on Moopa's New Episodes Page. From iconic classics to all-time favorites, experience the stories that have captured hearts worldwide. Start streaming now and relive the magic of anime!"
+ />
+ </Head>
+ <MobileNav sessions={sessions} />
+ <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
+ <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
+ <Link href="/en" className="flex gap-2 items-center font-karla">
+ <ChevronLeftIcon className="w-5 h-5" />
+ <h1 className="text-xl">New Episodes</h1>
+ </Link>
+ </div>
+ <div className="grid grid-cols-2 xs:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-6 gap-5 max-w-6xl pt-20">
+ {data?.map((i, index) => (
+ <div
+ key={index}
+ className="flex flex-col items-center w-[150px] lg:w-[180px]"
+ >
+ <Link
+ href={`/en/anime/${i.id}`}
+ className=" relative hover:scale-105 scale-100 transition-all duration-200 ease-out"
+ title={i.title.romaji}
+ >
+ <div className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded opacity-90 z-20">
+ <div className="absolute bg-gradient-to-b from-black/30 to-transparent from-5% to-30% top-0 z-30 w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] rounded" />
+ <Image
+ src={i.image}
+ alt={i.title.romaji}
+ width={500}
+ height={500}
+ className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded opacity-90 z-20"
+ />
+ </div>
+ <Image
+ src="/svg/episode-badge.svg"
+ alt="episode-bade"
+ width={200}
+ height={100}
+ className="w-24 lg:w-28 absolute top-1 -right-[13px] lg:-right-[15px] z-40"
+ />
+ <p className="absolute z-40 text-center w-[80px] lg:w-[100px] top-[5px] -right-2 lg:top-[4px] lg:-right-3 font-karla text-sm lg:text-base">
+ Episode <span className="text-white">{i?.episodeNumber}</span>
+ </p>
+ </Link>
+ <Link
+ href={`/en/anime/${i.id}`}
+ className="w-full px-1 py-2"
+ title={i.title.romaji}
+ >
+ <h1 className="font-karla font-bold xl:text-base text-[15px] line-clamp-2">
+ <span className="dots bg-green-500" />
+ {i.title.romaji}
+ </h1>
+ </Link>
+ </div>
+ ))}
+
+ {loading && (
+ <>
+ {[1, 2, 4, 5, 6, 7, 8].map((item) => (
+ <div
+ key={item}
+ className="flex flex-col items-center w-[150px] lg:w-[180px]"
+ >
+ <div className="w-full p-2">
+ <Skeleton className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] rounded" />
+ </div>
+ <div className="w-full px-2">
+ <Skeleton width={80} height={20} />
+ </div>
+ </div>
+ ))}
+ </>
+ )}
+ </div>
+ {!loading && page > 5 && nextPage && (
+ <button
+ onClick={() => setPage((p) => p + 1)}
+ className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md"
+ >
+ Load More
+ </button>
+ )}
+ </main>
+ <Footer />
+ </Fragment>
+ );
+}
diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js
index 1cc713a..9d3b6cf 100644
--- a/pages/en/anime/recently-watched.js
+++ b/pages/en/anime/recently-watched.js
@@ -6,14 +6,18 @@ import Skeleton from "react-loading-skeleton";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import MobileNav from "../../../components/home/mobileNav";
import { ToastContainer, toast } from "react-toastify";
-import { XMarkIcon } from "@heroicons/react/24/outline";
+import { ChevronRightIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/router";
+import HistoryOptions from "../../../components/home/content/historyOptions";
+import Head from "next/head";
+import MobileNav from "../../../components/shared/MobileNav";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [remove, setRemoved] = useState();
+ const router = useRouter();
useEffect(() => {
setLoading(true);
@@ -49,9 +53,9 @@ export default function PopularAnime({ sessions }) {
}
};
fetchData();
- }, [remove]);
+ }, [sessions?.user?.name, remove]);
- const removeItem = async (id) => {
+ const removeItem = async (id, aniId) => {
if (sessions?.user?.name) {
// remove from database
const res = await fetch(`/api/user/update/episode`, {
@@ -61,24 +65,42 @@ export default function PopularAnime({ sessions }) {
},
body: JSON.stringify({
name: sessions?.user?.name,
- id: id,
+ id,
+ aniId,
}),
});
const data = await res.json();
- // remove from local storage
- const artplayerSettings =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
- if (artplayerSettings[id]) {
- delete artplayerSettings[id];
- localStorage.setItem(
- "artplayer_settings",
- JSON.stringify(artplayerSettings)
- );
+ if (id) {
+ // remove from local storage
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+ }
+ if (aniId) {
+ const currentData =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+
+ const updatedData = {};
+
+ for (const key in currentData) {
+ const item = currentData[key];
+ if (item.aniId !== aniId) {
+ updatedData[key] = item;
+ }
+ }
+
+ localStorage.setItem("artplayer_settings", JSON.stringify(updatedData));
}
// update client
- setRemoved(id);
+ setRemoved(id || aniId);
if (data?.message === "Episode deleted") {
toast.success("Episode removed from history", {
@@ -91,22 +113,46 @@ export default function PopularAnime({ sessions }) {
});
}
} else {
- const artplayerSettings =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
- if (artplayerSettings[id]) {
- delete artplayerSettings[id];
- localStorage.setItem(
- "artplayer_settings",
- JSON.stringify(artplayerSettings)
- );
+ if (id) {
+ // remove from local storage
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+ setRemoved(id);
}
+ if (aniId) {
+ const currentData =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+
+ // Create a new object to store the updated data
+ const updatedData = {};
- setRemoved(id);
+ // Iterate through the current data and copy items with different aniId to the updated object
+ for (const key in currentData) {
+ const item = currentData[key];
+ if (item.aniId !== aniId) {
+ updatedData[key] = item;
+ }
+ }
+
+ // Update localStorage with the filtered data
+ localStorage.setItem("artplayer_settings", JSON.stringify(updatedData));
+ setRemoved(aniId);
+ }
}
};
return (
<>
+ <Head>
+ <title>Moopa - Recently Watched Episodes</title>
+ </Head>
<MobileNav sessions={sessions} />
<ToastContainer pauseOnHover={false} />
<div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
@@ -130,16 +176,32 @@ export default function PopularAnime({ sessions }) {
key={i.watchId}
className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item"
>
- <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action">
- <div
- className="flex flex-col items-center group/delete"
- onClick={() => removeItem(i.watchId)}
- >
- <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" />
- <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible">
- Remove from history
- </span>
- </div>
+ <div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible">
+ <HistoryOptions
+ remove={removeItem}
+ watchId={i.watchId}
+ aniId={i.aniId}
+ />
+ {i?.nextId && (
+ <button
+ type="button"
+ className="flex flex-col items-center group/next relative"
+ onClick={() => {
+ router.push(
+ `/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i?.nextId)}&num=${
+ i?.nextNumber
+ }`
+ );
+ }}
+ >
+ <ChevronRightIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" />
+ <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/next:visible group-hover/next:scale-100 group-hover/next:translate-x-0 group-hover/next:opacity-100 opacity-0 translate-x-10 scale-50 invisible">
+ Play Next Episode
+ </span>
+ </button>
+ )}
</div>
<Link
className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group"
diff --git a/pages/en/anime/trending.js b/pages/en/anime/trending.js
index 9f8a187..18eadf9 100644
--- a/pages/en/anime/trending.js
+++ b/pages/en/anime/trending.js
@@ -1,12 +1,13 @@
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
-import { useEffect, useState } from "react";
+import { Fragment, useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import MobileNav from "../../../components/home/mobileNav";
+import Head from "next/head";
+import MobileNav from "../../../components/shared/MobileNav";
export default function TrendingAnime({ sessions }) {
const [data, setData] = useState(null);
@@ -94,9 +95,17 @@ export default function TrendingAnime({ sessions }) {
}, [page, nextPage]);
return (
- <>
+ <Fragment>
+ <Head>
+ <title>Moopa - Trending Anime</title>
+ <meta name="title" content="Trending Anime" />
+ <meta
+ name="description"
+ content="Explore Top Trending Anime - Dive into the latest and most popular anime series on Moopa. From thrilling action to heartwarming romance, discover the buzzworthy shows that have everyone talking. Stream now and stay up-to-date with the hottest anime trends!"
+ />
+ </Head>
<MobileNav sessions={sessions} />
- <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
+ <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
<div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
<ChevronLeftIcon className="w-5 h-5" />
@@ -165,9 +174,9 @@ export default function TrendingAnime({ sessions }) {
Load More
</button>
)}
- </div>
+ </main>
<Footer />
- </>
+ </Fragment>
);
}
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index c17d9c5..aa0b672 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -4,156 +4,90 @@ import { useEffect, useState } from "react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../api/auth/[...nextauth]";
-import dotenv from "dotenv";
import Navigasi from "../../../../components/home/staticNav";
import PrimarySide from "../../../../components/anime/watch/primarySide";
import SecondarySide from "../../../../components/anime/watch/secondarySide";
-import { GET_MEDIA_USER } from "../../../../queries";
import { createList, createUser, getEpisode } from "../../../../prisma/user";
-// import { updateUser } from "../../../../prisma/user";
export default function Info({
sessions,
- aniId,
watchId,
provider,
epiNumber,
dub,
+ info,
userData,
proxy,
disqus,
}) {
- const [info, setInfo] = useState(null);
const [currentEpisode, setCurrentEpisode] = useState(null);
const [loading, setLoading] = useState(false);
- const [progress, setProgress] = useState(0);
- const [statuses, setStatuses] = useState("CURRENT");
const [artStorage, setArtStorage] = useState(null);
const [episodesList, setepisodesList] = useState();
+ const [mapProviders, setMapProviders] = useState(null);
+
const [onList, setOnList] = useState(false);
+ const [origin, setOrigin] = useState(null);
useEffect(() => {
setLoading(true);
+ setOrigin(window.location.origin);
async function getInfo() {
- const ress = await fetch(`https://graphql.anilist.co`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: `query ($id: Int) {
- Media (id: $id) {
- id
- idMal
- title {
- romaji
- english
- native
- }
- status
- genres
- episodes
- studios {
- edges {
- node {
- id
- name
- }
- }
- }
- bannerImage
- description
- coverImage {
- extraLarge
- color
- }
- synonyms
-
- }
- }
- `,
- variables: {
- id: aniId,
- },
- }),
- });
- const data = await ress.json();
-
- if (sessions?.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: sessions?.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(aniId));
+ if (info.mediaListEntry) {
+ setOnList(true);
+ }
- if (gut) {
- setProgress(gut.progress);
- setOnList(true);
- }
+ const response = await fetch(
+ `/api/v2/episode/${info.id}?releasing=${
+ info.status === "RELEASING" ? "true" : "false"
+ }${dub ? "&dub=true" : ""}`
+ ).then((res) => res.json());
+ const getMap = response.find((i) => i?.map === true) || response[0];
+ let episodes = response;
- if (gut?.status === "COMPLETED") {
- setStatuses("REPEATING");
- } else if (
- gut?.status === "REPEATING" &&
- gut?.media?.episodes === parseInt(epiNumber)
- ) {
- setStatuses("COMPLETED");
- } else if (gut?.status === "REPEATING") {
- setStatuses("REPEATING");
- } else if (gut?.media?.episodes === parseInt(epiNumber)) {
- setStatuses("COMPLETED");
- } else if (
- gut?.media?.episodes !== null &&
- data?.data?.Media.episodes === parseInt(epiNumber)
- ) {
- setStatuses("COMPLETED");
- setLoading(false);
- }
+ if (getMap) {
+ if (provider === "gogoanime" && !watchId.startsWith("/")) {
+ episodes = episodes.filter((i) => {
+ if (i?.providerId === "gogoanime" && i?.map !== true) {
+ return null;
+ }
+ return i;
+ });
}
- }
-
- setInfo(data.data.Media);
- const response = await fetch(
- `/api/consumet/episode/${aniId}${dub ? `?dub=${dub}` : ""}`
- );
- const episodes = await response.json();
+ setMapProviders(getMap?.episodes);
+ }
if (episodes) {
- const getProvider = episodes.data?.find(
- (i) => i.providerId === provider
+ const getProvider = episodes?.find((i) => i.providerId === provider);
+ const episodeList = dub
+ ? getProvider?.episodes?.filter((x) => x.hasDub === true)
+ : getProvider?.episodes.slice(0, getMap?.episodes.length);
+ const playingData = getMap?.episodes.find(
+ (i) => i.number === Number(epiNumber)
);
+
if (getProvider) {
- setepisodesList(getProvider.episodes);
- const currentEpisode = getProvider.episodes?.find(
+ setepisodesList(episodeList);
+ const currentEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber)
);
- const nextEpisode = getProvider.episodes?.find(
+ const nextEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber) + 1
);
- const previousEpisode = getProvider.episodes?.find(
+ const previousEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber) - 1
);
setCurrentEpisode({
prev: previousEpisode,
- playing: currentEpisode,
+ playing: {
+ id: currentEpisode.id,
+ title: playingData?.title,
+ description: playingData?.description,
+ image: playingData?.image,
+ number: currentEpisode.number,
+ },
next: nextEpisode,
});
} else {
@@ -176,6 +110,36 @@ export default function Info({
<>
<Head>
<title>{info?.title?.romaji || "Retrieving data..."}</title>
+ <meta
+ name="title"
+ data-title-romaji={info?.title?.romaji}
+ data-title-english={info?.title?.english}
+ data-title-native={info?.title?.native}
+ />
+ <meta
+ name="description"
+ content={currentEpisode?.playing?.description || info?.description}
+ />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta
+ name="twitter:title"
+ content={`Episode ${epiNumber} - ${
+ info.title.romaji || info.title.english
+ }`}
+ />
+ <meta
+ name="twitter:description"
+ content={`${
+ currentEpisode?.playing?.description?.slice(0, 180) ||
+ info?.description?.slice(0, 180)
+ }...`}
+ />
+ <meta
+ name="twitter:image"
+ content={`${origin}/api/og?title=${
+ info.title.romaji || info.title.english
+ }&image=${info.bannerImage || info.coverImage.extraLarge}`}
+ />
</Head>
<Navigasi />
@@ -189,7 +153,6 @@ export default function Info({
epiNumber={epiNumber}
providerId={provider}
watchId={watchId}
- status={statuses}
onList={onList}
proxy={proxy}
disqus={disqus}
@@ -201,10 +164,10 @@ export default function Info({
/>
<SecondarySide
info={info}
+ map={mapProviders}
providerId={provider}
watchId={watchId}
episode={episodesList}
- progress={progress}
artStorage={artStorage}
dub={dub}
/>
@@ -215,9 +178,8 @@ export default function Info({
}
export async function getServerSideProps(context) {
- dotenv.config();
-
const session = await getServerSession(context.req, context.res, authOptions);
+ const accessToken = session?.user?.token || null;
const query = context.query;
if (!query) {
@@ -236,6 +198,57 @@ export async function getServerSideProps(context) {
let userData = null;
+ const ress = await fetch(`https://graphql.anilist.co`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
+ },
+ body: JSON.stringify({
+ query: `query ($id: Int) {
+ Media (id: $id) {
+ mediaListEntry {
+ progress
+ status
+ customLists
+ repeat
+ }
+ id
+ idMal
+ title {
+ romaji
+ english
+ native
+ }
+ status
+ genres
+ episodes
+ studios {
+ edges {
+ node {
+ id
+ name
+ }
+ }
+ }
+ bannerImage
+ description
+ coverImage {
+ extraLarge
+ color
+ }
+ synonyms
+
+ }
+ }
+ `,
+ variables: {
+ id: aniId,
+ },
+ }),
+ });
+ const data = await ress.json();
+
try {
if (session) {
await createUser(session.user.name);
@@ -264,6 +277,7 @@ export async function getServerSideProps(context) {
epiNumber: epiNumber || null,
dub: dub || null,
userData: userData?.[0] || null,
+ info: data.data.Media || null,
proxy,
disqus,
},