diff options
| author | Factiven <[email protected]> | 2023-09-13 00:45:53 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-09-13 00:45:53 +0700 |
| commit | 7327a69b55a20b99b14ee0803d6cf5f8b88c45ef (patch) | |
| tree | cbcca777593a8cc4b0282e7d85a6fc51ba517e25 /pages/en/anime | |
| parent | Update issue templates (diff) | |
| download | moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.tar.xz moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.zip | |
Update v4 - Merge pre-push to main (#71)
* Create build-test.yml
* initial v4 commit
* update: github workflow
* update: push on branch
* Update .github/ISSUE_TEMPLATE/bug_report.md
* configuring next.config.js file
Diffstat (limited to 'pages/en/anime')
| -rw-r--r-- | pages/en/anime/[...id].js | 252 | ||||
| -rw-r--r-- | pages/en/anime/popular.js | 21 | ||||
| -rw-r--r-- | pages/en/anime/recent.js | 163 | ||||
| -rw-r--r-- | pages/en/anime/recently-watched.js | 130 | ||||
| -rw-r--r-- | pages/en/anime/trending.js | 21 | ||||
| -rw-r--r-- | pages/en/anime/watch/[...info].js | 240 |
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, }, |