diff options
| author | Factiven <[email protected]> | 2023-10-22 19:43:17 +0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-10-22 19:43:17 +0700 |
| commit | f801f8f422954b884a6541321dba0669ee9d6173 (patch) | |
| tree | e0d5e106b99e9b4e0a4c4bf7bb0464617db85b8d /pages | |
| parent | Bump @babel/traverse from 7.22.8 to 7.23.2 (#90) (diff) | |
| download | moopa-4.2.0.tar.xz moopa-4.2.0.zip | |
Update v4.2.0 (#93)v4.2.0
Diffstat (limited to 'pages')
| -rw-r--r-- | pages/404.js | 51 | ||||
| -rw-r--r-- | pages/_app.js | 68 | ||||
| -rw-r--r-- | pages/_error.js | 41 | ||||
| -rw-r--r-- | pages/_offline.js | 45 | ||||
| -rw-r--r-- | pages/admin/index.js | 7 | ||||
| -rw-r--r-- | pages/api/v2/admin/broadcast/index.js | 54 | ||||
| -rw-r--r-- | pages/api/v2/admin/bug-report/index.js | 30 | ||||
| -rw-r--r-- | pages/api/v2/episode/[id].js | 171 | ||||
| -rw-r--r-- | pages/api/v2/etc/recent/[page].js | 6 | ||||
| -rw-r--r-- | pages/api/v2/info/[id].js | 47 | ||||
| -rw-r--r-- | pages/api/v2/info/index.js | 60 | ||||
| -rw-r--r-- | pages/api/v2/pages/[...id].js | 34 | ||||
| -rw-r--r-- | pages/api/v2/source/index.js | 8 | ||||
| -rw-r--r-- | pages/en/anime/[...id].js | 11 | ||||
| -rw-r--r-- | pages/en/anime/recently-watched.js | 7 | ||||
| -rw-r--r-- | pages/en/anime/watch/[...info].js | 23 | ||||
| -rw-r--r-- | pages/en/index.js | 38 | ||||
| -rw-r--r-- | pages/en/manga/[...id].js | 425 | ||||
| -rw-r--r-- | pages/en/manga/[id].js | 146 | ||||
| -rw-r--r-- | pages/en/manga/read/[...params].js | 202 | ||||
| -rw-r--r-- | pages/en/profile/[user].js | 2 | ||||
| -rw-r--r-- | pages/en/search/[...param].js | 305 |
22 files changed, 1311 insertions, 470 deletions
diff --git a/pages/404.js b/pages/404.js index f6e609f..085d984 100644 --- a/pages/404.js +++ b/pages/404.js @@ -1,27 +1,13 @@ import Head from "next/head"; import Link from "next/link"; -import { useEffect, useState } from "react"; -import { parseCookies } from "nookies"; import Image from "next/image"; import Footer from "@/components/shared/footer"; +import { NewNavbar } from "@/components/shared/NavBar"; +import { useRouter } from "next/router"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; export default function Custom404() { - const [lang, setLang] = useState("en"); - const [cookie, setCookies] = useState(null); - - useEffect(() => { - let lang = null; - if (!cookie) { - const cookie = parseCookies(); - lang = cookie.lang || null; - setCookies(cookie); - } - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); + const router = useRouter(); return ( <> <Head> @@ -30,6 +16,7 @@ export default function Custom404() { <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/svg/c.svg" /> </Head> + <NewNavbar withNav shrink /> <div className="min-h-screen w-screen flex flex-col items-center justify-center "> <Image width={500} @@ -44,11 +31,29 @@ export default function Custom404() { <p className="text-base sm:text-lg xl:text-xl text-gray-300 mb-6 text-center"> The page you're looking for doesn't seem to exist. </p> - <Link href={`/${lang}/`}> - <div className="bg-[#fa7d56] xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-[#fb6f44]"> - Go back home - </div> - </Link> + <div className="flex gap-5 font-karla"> + <button + type="button" + onClick={() => { + router.back(); + }} + className="flex items-center gap-2 py-2 px-4 ring-1 ring-action/70 rounded hover:text-white transition-all duration-200 ease-out" + > + <span> + <ArrowLeftIcon className="w-5 h-5" /> + </span> + Go back + </button> + <button + type="button" + onClick={() => { + router.push("/en"); + }} + className="bg-action xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-opacity-80 hover:text-white transition-all duration-200 ease-out" + > + Home Page + </button> + </div> </div> <Footer /> </> diff --git a/pages/_app.js b/pages/_app.js index f553a98..e2f780d 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -3,22 +3,23 @@ import { AnimatePresence, motion as m } from "framer-motion"; import NextNProgress from "nextjs-progressbar"; import { SessionProvider } from "next-auth/react"; import "../styles/globals.css"; -import "react-toastify/dist/ReactToastify.css"; import "react-loading-skeleton/dist/skeleton.css"; import { SkeletonTheme } from "react-loading-skeleton"; import SearchPalette from "@/components/searchPalette"; import { SearchProvider } from "@/lib/context/isOpenState"; import Head from "next/head"; import { WatchPageProvider } from "@/lib/context/watchPageProvider"; -import { ToastContainer, toast } from "react-toastify"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { unixTimestampToRelativeTime } from "@/utils/getTimes"; +import SecretPage from "@/components/secret"; +import { Toaster, toast } from "sonner"; export default function App({ Component, pageProps: { session, ...pageProps }, }) { const router = useRouter(); + const [info, setInfo] = useState(null); useEffect(() => { async function getBroadcast() { @@ -31,29 +32,31 @@ export default function App({ }, }); const data = await res.json(); - if ( - data && - data?.message !== "No broadcast" && - data?.message !== "unauthorized" - ) { - toast( - `${data.message} ${ + if (data?.show === true) { + toast.message( + `🚧${data.message} ${ data?.startAt ? unixTimestampToRelativeTime(data.startAt) : "" - }`, + }🚧`, { - position: "top-center", - autoClose: false, - closeOnClick: true, - draggable: true, - theme: "colored", - className: "toaster", - style: { - background: "#232329", - color: "#fff", - }, + position: "bottom-right", + important: true, + duration: 100000, + className: "flex-center font-karla text-white", + // description: `🚧${info}🚧`, } ); + // toast.message(`Announcement`, { + // position: "top-center", + // important: true, + // // duration: 10000, + // description: `🚧${info}🚧`, + // }); } + setInfo( + `${data.message} ${ + data?.startAt ? unixTimestampToRelativeTime(data.startAt) : "" + }` + ); } catch (err) { console.log(err); } @@ -61,12 +64,16 @@ export default function App({ getBroadcast(); }, []); + const handleCheatCodeEntered = () => { + alert("Cheat code entered!"); // You can replace this with your desired action + }; + return ( <> <Head> <meta name="viewport" - content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover" + content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover" /> </Head> <SessionProvider session={session}> @@ -74,7 +81,22 @@ export default function App({ <WatchPageProvider> <AnimatePresence mode="wait"> <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> - <ToastContainer pauseOnFocusLoss={false} pauseOnHover={false} /> + <Toaster richColors theme="dark" closeButton /> + <SecretPage + cheatCode={"aofienaef"} + onCheatCodeEntered={handleCheatCodeEntered} + /> + {/* {info && ( + <div className="relative px-3 flex items-center justify-center font-karla w-full py-2 bg-secondary/80 text-white text-center"> + <span className="line-clamp-1 mr-5">🚧{info}🚧</span> + <span + onClick={() => setInfo()} + className="absolute right-3 cursor-pointer" + > + <XMarkIcon className="w-6 h-6" /> + </span> + </div> + )} */} <m.div key={`route-${router.route}`} transition={{ duration: 0.5 }} diff --git a/pages/_error.js b/pages/_error.js new file mode 100644 index 0000000..19dfcff --- /dev/null +++ b/pages/_error.js @@ -0,0 +1,41 @@ +import MobileNav from "@/components/shared/MobileNav"; +import { NewNavbar } from "@/components/shared/NavBar"; +import Footer from "@/components/shared/footer"; +import Head from "next/head"; +import Link from "next/link"; + +function Error({ statusCode }) { + return ( + <> + <Head> + <title>An Error Has Occurred</title> + </Head> + <NewNavbar withNav shrink /> + <MobileNav hideProfile /> + <div className="w-screen h-screen flex-center flex-col gap-5"> + <div className="relative text-3xl">(╯°□°)╯︵ ┻━┻</div> + <div className="flex items-center gap-2 text-xl"> + <span> + {statusCode + ? `An error ${statusCode} occurred on server.` + : "An error occurred on client."} + </span> + </div> + <Link + href="/en" + className="rounded ring-action/50 ring-1 p-2 font-karla bg-action bg-opacity-0 hover:bg-opacity-20 hover:scale-105 text-white transition-all duration-300" + > + Back to home + </Link> + </div> + <Footer /> + </> + ); +} + +Error.getInitialProps = ({ res, err }) => { + const statusCode = res ? res.statusCode : err ? err.statusCode : 404; + return { statusCode }; +}; + +export default Error; diff --git a/pages/_offline.js b/pages/_offline.js new file mode 100644 index 0000000..f440b39 --- /dev/null +++ b/pages/_offline.js @@ -0,0 +1,45 @@ +import Image from "next/image"; +import React from "react"; + +export default function Fallback() { + return ( + <div className="w-screen h-screen flex-center flex-col gap-5"> + <div className="relative"> + <Image + src="/svg/c.svg" + alt="logo" + height={160} + width={160} + quality={100} + className="object-cover" + /> + </div> + <p className="flex items-center gap-2 text-2xl"> + <svg + xmlns="http://www.w3.org/2000/svg" + width="1em" + height="1em" + viewBox="0 0 512 512" + > + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="32" + d="M93.72 183.25C49.49 198.05 16 233.1 16 288c0 66 54 112 120 112h184.37m147.45-22.26C485.24 363.3 496 341.61 496 312c0-59.82-53-85.76-96-88c-8.89-89.54-71-144-144-144c-26.16 0-48.79 6.93-67.6 18.14" + ></path> + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeMiterlimit="10" + strokeWidth="32" + d="M448 448L64 64" + ></path> + </svg> + <span>You are Offline :\</span> + </p> + </div> + ); +} diff --git a/pages/admin/index.js b/pages/admin/index.js index cbb5086..2a73fc1 100644 --- a/pages/admin/index.js +++ b/pages/admin/index.js @@ -27,7 +27,12 @@ export async function getServerSideProps(context) { } const admin = sessions?.user?.name === process.env.ADMIN_USERNAME; - const api = process.env.API_URI; + + let api; + api = process.env.API_URI; + if (api.endsWith("/")) { + api = api.slice(0, -1); + } if (!admin) { return { diff --git a/pages/api/v2/admin/broadcast/index.js b/pages/api/v2/admin/broadcast/index.js index d3d3af0..470d61d 100644 --- a/pages/api/v2/admin/broadcast/index.js +++ b/pages/api/v2/admin/broadcast/index.js @@ -1,9 +1,17 @@ import { rateLimitStrict, redis } from "@/lib/redis"; -// import { getServerSession } from "next-auth"; -// import { authOptions } from "pages/api/auth/[...nextauth]"; +import { getServerSession } from "next-auth"; +import { authOptions } from "pages/api/auth/[...nextauth]"; export default async function handler(req, res) { // Check if the custom header "X-Your-Custom-Header" is present and has a specific value + const sessions = await getServerSession(req, res, authOptions); + + const admin = sessions?.user?.name === process.env.ADMIN_USERNAME; + // if req.method === POST and admin === false return 401 + if (!admin && req.method === "DELETE") { + return res.status(401).json({ message: "Unauthorized" }); + } + const customHeaderValue = req.headers["x-broadcast-key"]; if (customHeaderValue !== "get-broadcast") { @@ -21,14 +29,40 @@ export default async function handler(req, res) { }); } - const getId = await redis.get(`broadcast`); - if (getId) { - const broadcast = JSON.parse(getId); - return res - .status(200) - .json({ message: broadcast.message, startAt: broadcast.startAt }); - } else { - return res.status(200).json({ message: "No broadcast" }); + if (req.method === "POST") { + const { message, startAt = undefined, show = false } = req.body; + if (!message) { + return res.status(400).json({ message: "Message is required" }); + } + + const broadcastContent = { + message, + startAt, + show, + }; + await redis.set(`broadcasts`, JSON.stringify(broadcastContent)); + return res.status(200).json({ message: "Broadcast created" }); + } else if (req.method === "DELETE") { + const br = await redis.get(`broadcasts`); + // set broadcast show as false + if (br) { + const broadcast = JSON.parse(br); + broadcast.show = false; + await redis.set(`broadcasts`, JSON.stringify(broadcast)); + } + return res.status(200).json({ message: "Broadcast deleted" }); + } else if (req.method === "GET") { + const getId = await redis.get(`broadcasts`); + if (getId) { + const broadcast = JSON.parse(getId); + return res.status(200).json({ + message: broadcast.message, + startAt: broadcast.startAt, + show: broadcast.show, + }); + } else { + return res.status(200).json({ message: "No broadcast" }); + } } } diff --git a/pages/api/v2/admin/bug-report/index.js b/pages/api/v2/admin/bug-report/index.js index fc5ee77..508e6cd 100644 --- a/pages/api/v2/admin/bug-report/index.js +++ b/pages/api/v2/admin/bug-report/index.js @@ -8,16 +8,6 @@ export default async function handler(req, res) { // create random id each time the endpoint is called const id = Math.random().toString(36).substr(2, 9); - // if (!admin) { - // return res.status(401).json({ message: "Unauthorized" }); - // } - const { data } = req.body; - - // if method is not POST return message "Method not allowed" - if (req.method !== "POST") { - return res.status(405).json({ message: "Method not allowed" }); - } - try { if (redis) { try { @@ -29,16 +19,22 @@ export default async function handler(req, res) { }); } - const getId = await redis.get(`report:${id}`); - if (getId) { + if (req.method === "POST") { + const { data } = req.body; + + data.id = id; + + await redis.set(`report:${id}`, JSON.stringify(data)); return res .status(200) - .json({ message: `Data already exist for id: ${id}` }); + .json({ message: `Report has successfully sent, with Id of ${id}` }); + } else if (req.method === "DELETE") { + const { reportId } = req.body; + await redis.del(`report:${reportId}`); + return res.status(200).json({ message: `Report has been deleted` }); + } else { + return res.status(405).json({ message: "Method not allowed" }); } - await redis.set(`report:${id}`, JSON.stringify(data)); - return res - .status(200) - .json({ message: `Report has successfully sent, with Id of ${id}` }); } return res.status(200).json({ message: "redis is not defined" }); diff --git a/pages/api/v2/episode/[id].js b/pages/api/v2/episode/[id].js index c1fac8b..3f1372b 100644 --- a/pages/api/v2/episode/[id].js +++ b/pages/api/v2/episode/[id].js @@ -3,7 +3,13 @@ import { rateLimitStrict, rateLimiterRedis, redis } from "@/lib/redis"; import appendImagesToEpisodes from "@/utils/combineImages"; import appendMetaToEpisodes from "@/utils/appendMetaToEpisodes"; -const CONSUMET_URI = process.env.API_URI; +let CONSUMET_URI; + +CONSUMET_URI = process.env.API_URI; +if (CONSUMET_URI.endsWith("/")) { + CONSUMET_URI = CONSUMET_URI.slice(0, -1); +} + const API_KEY = process.env.API_KEY; const isAscending = (data) => { @@ -15,37 +21,70 @@ const isAscending = (data) => { return true; }; -async function fetchConsumet(id, dub) { - try { - if (dub) { - return []; +function filterData(data, type) { + // Filter the data based on the type (sub or dub) and providerId + const filteredData = data.map((item) => { + if (item?.map === true) { + if (item.episodes[type].length === 0) { + return null; + } else { + return { + ...item, + episodes: Object?.entries(item.episodes[type]).map( + ([id, episode]) => ({ + ...episode, + }) + ), + }; + } } + return item; + }); - const { data } = await axios.get( - `${CONSUMET_URI}/meta/anilist/episodes/${id}` - ); + const noEmpty = filteredData.filter((i) => i !== null); + return noEmpty; +} - if (data?.message === "Anime not found" && data?.length < 1) { - return []; +async function fetchConsumet(id) { + try { + async function fetchData(dub) { + const { data } = await axios.get( + `${CONSUMET_URI}/meta/anilist/episodes/${id}${dub ? "?dub=true" : ""}` + ); + if (data?.message === "Anime not found" && data?.length < 1) { + return []; + } + + if (dub) { + if (!data?.some((i) => i.id.includes("dub"))) return []; + } + + const reformatted = data.map((item) => ({ + id: item?.id || null, + title: item?.title || null, + img: item?.image || null, + number: item?.number || null, + createdAt: item?.createdAt || null, + description: item?.description || null, + url: item?.url || null, + })); + + return reformatted; } - const reformatted = data.map((item) => ({ - id: item?.id || null, - title: item?.title || null, - img: item?.image || null, - number: item?.number || null, - createdAt: item?.createdAt || null, - description: item?.description || null, - url: item?.url || null, - })); + const [subData, dubData] = await Promise.all([ + fetchData(), + fetchData(true), + ]); const array = [ { map: true, providerId: "gogoanime", - episodes: isAscending(reformatted) - ? reformatted - : reformatted.reverse(), + episodes: { + sub: isAscending(subData) ? subData : subData.reverse(), + dub: isAscending(dubData) ? dubData : dubData.reverse(), + }, }, ]; @@ -73,7 +112,15 @@ async function fetchAnify(id) { const filtered = data.filter( (item) => item.providerId !== "animepahe" && item.providerId !== "kass" ); - + // const modifiedData = filtered.map((provider) => { + // if (provider.providerId === "gogoanime") { + // const reversedEpisodes = [...provider.episodes].reverse(); + // return { ...provider, episodes: reversedEpisodes }; + // } + // return provider; + // }); + + // return modifiedData; return filtered; } catch (error) { console.error("Error fetching and processing data:", error.message); @@ -81,12 +128,16 @@ async function fetchAnify(id) { } } -async function fetchCoverImage(id) { +async function fetchCoverImage(id, available = false) { try { if (!process.env.API_KEY) { return []; } + if (available) { + return null; + } + const { data } = await axios.get( `https://api.anify.tv/content-metadata/${id}?apikey=${API_KEY}` ); @@ -95,7 +146,9 @@ async function fetchCoverImage(id) { return []; } - return data; + const getData = data[0].data; + + return getData; } catch (error) { console.error("Error fetching and processing data:", error.message); return []; @@ -124,10 +177,10 @@ export default async function handler(req, res) { } if (refresh) { - await redis.del(id); + await redis.del(`episode:${id}`); console.log("deleted cache"); } else { - cached = await redis.get(id); + cached = await redis.get(`episode:${id}`); console.log("using redis"); } @@ -136,49 +189,75 @@ export default async function handler(req, res) { if (cached && !refresh) { if (dub) { - const filtered = JSON.parse(cached).filter((item) => - item.episodes.some((epi) => epi.hasDub === true) + const filteredData = filterData(JSON.parse(cached), "dub"); + + let filtered = filteredData.filter((item) => + item?.episodes?.some((epi) => epi.hasDub !== false) ); + + if (meta) { + filtered = await appendMetaToEpisodes(filtered, JSON.parse(meta)); + } + return res.status(200).json(filtered); } else { - return res.status(200).json(JSON.parse(cached)); + const filteredData = filterData(JSON.parse(cached), "sub"); + + let filtered = filteredData; + + if (meta) { + filtered = await appendMetaToEpisodes(filteredData, JSON.parse(meta)); + } + + return res.status(200).json(filtered); } } else { const [consumet, anify, cover] = await Promise.all([ fetchConsumet(id, dub), fetchAnify(id), - fetchCoverImage(id), + fetchCoverImage(id, meta), ]); - const hasImage = consumet.map((i) => - i.episodes.some( - (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/") - ) - ); + // const hasImage = consumet.map((i) => + // i.episodes?.sub?.some( + // (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/") + // ) + // ); + + let subDub = "sub"; + if (dub) { + subDub = "dub"; + } - const rawData = [...consumet, ...(anify[0]?.data ?? [])]; + const rawData = [...consumet, ...anify]; - let data = rawData; + const filteredData = filterData(rawData, subDub); + + let data = filteredData; if (meta) { - data = await appendMetaToEpisodes(rawData, JSON.parse(meta)); - } else if (cover && cover?.length > 0 && !hasImage.includes(true)) - data = await appendImagesToEpisodes(rawData, cover); + data = await appendMetaToEpisodes(filteredData, JSON.parse(meta)); + } else if (cover && !cover.some((e) => e.img === null)) { + await redis.set(`meta:${id}`, JSON.stringify(cover)); + data = await appendMetaToEpisodes(filteredData, cover); + } if (redis && cacheTime !== null) { await redis.set( - id, - JSON.stringify(data.filter((i) => i.episodes.length > 0)), + `episode:${id}`, + JSON.stringify(rawData), "EX", cacheTime ); } if (dub) { - const filtered = data.filter((item) => - item.episodes.some((epi) => epi.hasDub === true) + const filtered = data.filter( + (item) => !item.episodes.some((epi) => epi.hasDub === false) ); - return res.status(200).json(filtered); + return res + .status(200) + .json(filtered.filter((i) => i.episodes.length > 0)); } console.log("fresh data"); diff --git a/pages/api/v2/etc/recent/[page].js b/pages/api/v2/etc/recent/[page].js index 6727787..b1bda0f 100644 --- a/pages/api/v2/etc/recent/[page].js +++ b/pages/api/v2/etc/recent/[page].js @@ -1,6 +1,10 @@ import { rateLimiterRedis, redis } from "@/lib/redis"; -const API_URL = process.env.API_URI; +let API_URL; +API_URL = process.env.API_URI; +if (API_URL.endsWith("/")) { + API_URL = API_URL.slice(0, -1); +} export default async function handler(req, res) { try { diff --git a/pages/api/v2/info/[id].js b/pages/api/v2/info/[id].js deleted file mode 100644 index 243756c..0000000 --- a/pages/api/v2/info/[id].js +++ /dev/null @@ -1,47 +0,0 @@ -import axios from "axios"; -import { rateLimiterRedis, redis } from "@/lib/redis"; - -const API_KEY = process.env.API_KEY; - -export async function fetchInfo(id) { - try { - const { data } = await axios.get( - `https://api.anify.tv/info/${id}?apikey=${API_KEY}` - ); - return data; - } catch (error) { - console.error("Error fetching data:", error); - return null; - } -} - -export default async function handler(req, res) { - const id = req.query.id; - let cached; - if (redis) { - try { - const ipAddress = req.socket.remoteAddress; - await rateLimiterRedis.consume(ipAddress); - } catch (error) { - return res.status(429).json({ - error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`, - }); - } - cached = await redis.get(id); - } - if (cached) { - // console.log("Using cached data"); - return res.status(200).json(JSON.parse(cached)); - } else { - const data = await fetchInfo(id); - if (data) { - // console.log("Setting cache"); - if (redis) { - await redis.set(id, JSON.stringify(data), "EX", 60 * 10); - } - return res.status(200).json(data); - } else { - return res.status(404).json({ message: "Schedule not found" }); - } - } -} diff --git a/pages/api/v2/info/index.js b/pages/api/v2/info/index.js new file mode 100644 index 0000000..95770bd --- /dev/null +++ b/pages/api/v2/info/index.js @@ -0,0 +1,60 @@ +import { redis } from "@/lib/redis"; +import axios from "axios"; + +const API_KEY = process.env.API_KEY; + +export async function fetchInfo(id) { + try { + // console.log(id); + const { data } = await axios + .get(`https://api.anify.tv/info/${id}?apikey=${API_KEY}`) + .catch((err) => { + return { + data: null, + }; + }); + + if (!data) { + return null; + } + + const { data: Chapters } = await axios.get( + `https://api.anify.tv/chapters/${data.id}?apikey=${API_KEY}` + ); + + if (!Chapters) { + return null; + } + + return { id: data.id, chapters: Chapters }; + } catch (error) { + console.error("Error fetching data:", error); + return null; + } +} + +export default async function handler(req, res) { + //const [romaji, english, native] = req.query.title; + const { id } = req.query; + try { + let cached; + // const data = await fetchInfo(id); + cached = await redis.get(`manga:${id}`); + + if (cached) { + return res.status(200).json(JSON.parse(cached)); + } + + const manga = await fetchInfo(id); + + if (!manga) { + return res.status(404).json({ error: "Manga not found" }); + } + + await redis.set(`manga:${id}`, JSON.stringify(manga), "ex", 60 * 60 * 24); + + res.status(200).json(manga); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} diff --git a/pages/api/v2/pages/[...id].js b/pages/api/v2/pages/[...id].js new file mode 100644 index 0000000..a9fe0f9 --- /dev/null +++ b/pages/api/v2/pages/[...id].js @@ -0,0 +1,34 @@ +import axios from "axios"; + +async function fetchData(id, number, provider, readId) { + try { + const { data } = await axios.get( + `https://api.anify.tv/pages?id=${id}&chapterNumber=${number}&providerId=${provider}&readId=${encodeURIComponent( + readId + )}` + ); + + if (!data) { + return null; + } + + return data; + } catch (error) { + return null; + } +} + +export default async function handler(req, res) { + const [id, number, provider, readId] = req.query.id; + + try { + const data = await fetchData(id, number, provider, readId); + // if (!data) { + // return res.status(400).json({ error: "Invalid query" }); + // } + + return res.status(200).json(data); + } catch (error) { + return res.status(500).json({ error: error.message }); + } +} diff --git a/pages/api/v2/source/index.js b/pages/api/v2/source/index.js index f15e47d..9ec6082 100644 --- a/pages/api/v2/source/index.js +++ b/pages/api/v2/source/index.js @@ -1,7 +1,11 @@ import { rateLimiterRedis, redis } from "@/lib/redis"; import axios from "axios"; -const CONSUMET_URI = process.env.API_URI; +let CONSUMET_URI; +CONSUMET_URI = process.env.API_URI; +if (CONSUMET_URI.endsWith("/")) { + CONSUMET_URI = CONSUMET_URI.slice(0, -1); +} const API_KEY = process.env.API_KEY; async function consumetSource(id) { @@ -25,7 +29,7 @@ async function anifySource(providerId, watchId, episode, id, sub) { ); return data; } catch (error) { - return null; + return { error: error.message, status: error.response.status }; } } diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js index 910bbc6..e2c0039 100644 --- a/pages/en/anime/[...id].js +++ b/pages/en/anime/[...id].js @@ -72,6 +72,8 @@ export default function Info({ info, color }) { } } fetchData(); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, info, session?.user?.name]); function handleOpen() { @@ -143,7 +145,7 @@ export default function Info({ info, color }) { stats={statuses?.value} prg={progress} max={info?.episodes} - image={info} + info={info} close={handleClose} /> )} @@ -208,7 +210,12 @@ export default function Info({ info, color }) { export async function getServerSideProps(ctx) { const { id } = ctx.query; - const API_URI = process.env.API_URI; + + let API_URI; + API_URI = process.env.API_URI; + if (API_URI.endsWith("/")) { + API_URI = API_URI.slice(0, -1); + } let cache; diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js index c723394..6abf09d 100644 --- a/pages/en/anime/recently-watched.js +++ b/pages/en/anime/recently-watched.js @@ -6,12 +6,12 @@ import Skeleton from "react-loading-skeleton"; import Footer from "@/components/shared/footer"; import { getServerSession } from "next-auth"; import { authOptions } from "../../api/auth/[...nextauth]"; -import { toast } from "react-toastify"; 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"; +import { toast } from "sonner"; export default function PopularAnime({ sessions }) { const [data, setData] = useState(null); @@ -105,11 +105,6 @@ export default function PopularAnime({ sessions }) { if (data?.message === "Episode deleted") { toast.success("Episode removed from history", { position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", }); } } else { diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js index f918f86..a838b7f 100644 --- a/pages/en/anime/watch/[...info].js +++ b/pages/en/anime/watch/[...info].js @@ -29,8 +29,12 @@ export async function getServerSideProps(context) { }; } - const proxy = process.env.PROXY_URI; - const disqus = process.env.DISQUS_SHORTNAME || null; + let proxy; + proxy = process.env.PROXY_URI; + if (proxy.endsWith("/")) { + proxy = proxy.slice(0, -1); + } + const disqus = process.env.DISQUS_SHORTNAME; const [aniId, provider] = query?.info; const watchId = query?.id; @@ -114,7 +118,7 @@ export async function getServerSideProps(context) { epiNumber: epiNumber || null, dub: dub || null, userData: userData?.[0] || null, - info: data.data.Media || null, + info: data?.data?.Media || null, proxy, disqus, }, @@ -179,9 +183,10 @@ export default function Watch({ if (episodes) { 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 episodeList = getProvider?.episodes.slice( + 0, + getMap?.episodes.length + ); const playingData = getMap?.episodes.find( (i) => i.number === Number(epiNumber) ); @@ -219,6 +224,7 @@ export default function Watch({ return () => { setEpisodeNavigation(null); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessions?.user?.name, epiNumber, dub]); useEffect(() => { @@ -287,6 +293,8 @@ export default function Watch({ }); setMarked(0); }; + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [provider, watchId, info?.id]); useEffect(() => { @@ -524,7 +532,7 @@ export default function Watch({ </div> <div id="secondary" - className={`relative ${theaterMode ? "pt-2" : ""}`} + className={`relative ${theaterMode ? "pt-5" : "pt-4 lg:pt-0"}`} > <EpisodeLists info={info} @@ -534,6 +542,7 @@ export default function Watch({ watchId={watchId} episode={episodesList} artStorage={artStorage} + track={episodeNavigation} dub={dub} /> </div> diff --git a/pages/en/index.js b/pages/en/index.js index 9be3c2c..29b0778 100644 --- a/pages/en/index.js +++ b/pages/en/index.js @@ -118,23 +118,23 @@ export default function Home({ detail, populars, upComing }) { } }, [upComing]); - useEffect(() => { - const getSchedule = async () => { - try { - const res = await fetch(`/api/v2/etc/schedule`); - const data = await res.json(); - - if (!res.ok) { - setSchedules(null); - } else { - setSchedules(data); - } - } catch (err) { - console.log(err); - } - }; - getSchedule(); - }, []); + // useEffect(() => { + // const getSchedule = async () => { + // try { + // const res = await fetch(`/api/v2/etc/schedule`); + // const data = await res.json(); + + // if (!res.ok) { + // setSchedules(null); + // } else { + // setSchedules(data); + // } + // } catch (err) { + // console.log(err); + // } + // }; + // getSchedule(); + // }, []); const [releaseData, setReleaseData] = useState([]); @@ -290,6 +290,8 @@ export default function Home({ detail, populars, upComing }) { } } userData(); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessions?.user?.name, currentAnime, plan]); // console.log({ recentAdded }); @@ -402,7 +404,7 @@ export default function Home({ detail, populars, upComing }) { </div> )} - <div className="lg:mt-16 mt-5 flex flex-col gap-5 items-center"> + <div className="lg:mt-16 mt-5 flex flex-col items-center"> <motion.div className="w-screen flex-none lg:w-[95%] xl:w-[87%]" initial={{ opacity: 0 }} diff --git a/pages/en/manga/[...id].js b/pages/en/manga/[...id].js new file mode 100644 index 0000000..106bce2 --- /dev/null +++ b/pages/en/manga/[...id].js @@ -0,0 +1,425 @@ +import ChapterSelector from "@/components/manga/chapters"; +import Footer from "@/components/shared/footer"; +import Head from "next/head"; +import { useEffect, useState } from "react"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../api/auth/[...nextauth]"; +import { mediaInfoQuery } from "@/lib/graphql/query"; +import Modal from "@/components/modal"; +import { signIn, useSession } from "next-auth/react"; +import AniList from "@/components/media/aniList"; +import ListEditor from "@/components/listEditor"; +import MobileNav from "@/components/shared/MobileNav"; +import Image from "next/image"; +import DetailTop from "@/components/anime/mobile/topSection"; +import Characters from "@/components/anime/charactersCard"; +import Content from "@/components/home/content"; +import { toast } from "sonner"; +import axios from "axios"; +import getAnifyInfo from "@/lib/anify/info"; +import { redis } from "@/lib/redis"; +import getMangaId from "@/lib/anify/getMangaId"; + +export default function Manga({ info, anifyData, color, chapterNotFound }) { + const [domainUrl, setDomainUrl] = useState(""); + const { data: session } = useSession(); + + const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState(0); + const [statuses, setStatuses] = useState(null); + const [watch, setWatch] = useState(); + + const [chapter, setChapter] = useState(null); + + const [open, setOpen] = useState(false); + + const rec = info?.recommendations?.nodes?.map( + (data) => data.mediaRecommendation + ); + + useEffect(() => { + setDomainUrl(window.location.origin); + }, []); + + useEffect(() => { + if (chapterNotFound) { + toast.error("Chapter not found"); + const cleanUrl = window.location.origin + window.location.pathname; + window.history.replaceState(null, null, cleanUrl); + } + }, [chapterNotFound]); + + useEffect(() => { + async function fetchData() { + try { + setLoading(true); + + const { data } = await axios.get(`/api/v2/info?id=${anifyData.id}`); + + if (!data.chapters) { + setLoading(false); + return; + } + + setChapter(data); + setLoading(false); + } catch (error) { + console.error(error); + } + } + fetchData(); + + return () => { + setChapter(null); + }; + }, [info?.id]); + + function handleOpen() { + setOpen(true); + document.body.style.overflow = "hidden"; + } + + function handleClose() { + setOpen(false); + document.body.style.overflow = "auto"; + } + + return ( + <> + <Head> + <title> + {info + ? `Manga - ${ + info.title.romaji || info.title.english || info.title.native + }` + : "Getting Info..."} + </title> + <meta name="twitter:card" content="summary_large_image" /> + <meta + name="twitter:title" + content={`Moopa - ${info.title.romaji || info.title.english}`} + /> + <meta + name="twitter:description" + content={`${info.description?.slice(0, 180)}...`} + /> + <meta + name="twitter:image" + content={`${domainUrl}/api/og?title=${ + info.title.romaji || info.title.english + }&image=${info.bannerImage || info.coverImage}`} + /> + <meta + name="title" + data-title-romaji={info?.title?.romaji} + data-title-english={info?.title?.english} + data-title-native={info?.title?.native} + /> + </Head> + <Modal open={open} onClose={() => handleClose()}> + <div> + {!session && ( + <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> + <div className="text-md font-extrabold font-karla"> + Edit your list + </div> + <button + className="flex items-center bg-[#363642] rounded-md text-white p-1" + onClick={() => signIn("AniListProvider")} + > + <h1 className="px-1 font-bold font-karla"> + Login with AniList + </h1> + <div className="scale-[60%] pb-[1px]"> + <AniList /> + </div> + </button> + </div> + )} + {session && info && ( + <ListEditor + animeId={info?.id} + session={session} + stats={statuses?.value} + prg={progress} + max={info?.episodes} + info={info} + close={handleClose} + /> + )} + </div> + </Modal> + <MobileNav sessions={session} hideProfile={true} /> + <main className="w-screen min-h-screen overflow-hidden relative flex flex-col items-center gap-5"> + {/* <div className="absolute bg-gradient-to-t from-primary from-85% to-100% to-transparent w-screen h-full z-10" /> */} + <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} + alt="banner anime" + height={1000} + width={1000} + blurDataURL={info?.bannerImage} + className="object-cover bg-image blur-[2px] 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 pb-10"> + <DetailTop + info={info} + session={session} + handleOpen={handleOpen} + loading={loading} + statuses={statuses} + watchUrl={watch} + progress={progress} + color={color} + /> + + {!loading ? ( + chapter?.chapters?.length > 0 ? ( + <ChapterSelector + chaptersData={chapter.chapters} + mangaId={chapter.id} + data={info} + setWatch={setWatch} + /> + ) : ( + <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 manga is not available. + </p> + </div> + ) + ) : ( + <div className="flex justify-center"> + <div className="lds-ellipsis"> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + </div> + )} + + {info?.characters?.edges?.length > 0 && ( + <div className="w-full"> + <Characters info={info?.characters?.edges} /> + </div> + )} + + {info && rec && rec?.length !== 0 && ( + <div className="w-full"> + <Content + ids="recommendAnime" + section="Recommendations" + type="manga" + data={rec} + /> + </div> + )} + </div> + </main> + <Footer /> + </> + ); +} + +export async function getServerSideProps(context) { + const session = await getServerSession(context.req, context.res, authOptions); + const accessToken = session?.user?.token || null; + + const { chapter } = context.query; + const [id1, id2] = context.query.id; + + let cached; + let aniId, mangadexId; + let info, data, color, chapterNotFound; + + if (String(id1).length > 6) { + aniId = id2; + mangadexId = id1; + } else { + aniId = id1; + mangadexId = id2; + } + + if (chapter) { + // create random id string + chapterNotFound = Math.random().toString(36).substring(7); + } + + if (aniId === "na" && mangadexId) { + const datas = await getAnifyInfo(mangadexId); + + aniId = + datas.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null; + + if (!aniId) { + info = datas; + data = datas; + color = { + backgroundColor: `${"#ffff"}`, + color: "#000", + }; + // return { + // redirect: { + // destination: "/404", + // permanent: false, + // }, + // }; + } + } else if (aniId && !mangadexId) { + // console.log({ aniId }); + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + body: JSON.stringify({ + query: `query ($id: Int, $type: MediaType) { + Media (id: $id, type: $type) { + id + title { + romaji + english + native + } + } + }`, + variables: { + id: parseInt(aniId), + type: "MANGA", + }, + }), + }); + const aniListData = await response.json(); + const info = aniListData?.data?.Media; + + const mangaId = await getMangaId( + info?.title?.romaji, + info?.title?.english, + info?.title?.native + ); + mangadexId = mangaId?.id; + + if (!mangadexId) { + return { + redirect: { + destination: "/404", + permanent: false, + }, + }; + } + + return { + redirect: { + destination: `/en/manga/${aniId}/${mangadexId}${ + chapter ? "?chapter=404" : "" + }`, + permanent: true, + }, + }; + } else if (!aniId && mangadexId) { + const data = await getAnifyInfo(mangadexId); + + aniId = + data.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null; + + if (!aniId) { + info = data; + // return { + // redirect: { + // destination: "/404", + // permanent: false, + // }, + // }; + } + + return { + redirect: { + destination: `/en/manga/${aniId ? aniId : "na"}${`/${mangadexId}`}${ + chapter ? "?chapter=404" : "" + }`, + permanent: true, + }, + }; + } else { + const getCached = await redis.get(`mangaPage:${mangadexId}`); + + if (getCached) { + cached = JSON.parse(getCached); + } + + // let chapters; + + if (cached) { + data = cached.data; + info = cached.info; + color = cached.color; + } else { + data = await getAnifyInfo(mangadexId); + + const aniListId = + data.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null; + + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + body: JSON.stringify({ + query: mediaInfoQuery, + variables: { + id: parseInt(aniListId), + type: "MANGA", + }, + }), + }); + const aniListData = await response.json(); + if (aniListData?.data?.Media) info = aniListData?.data?.Media; + + const textColor = setTxtColor(info?.color); + + color = { + backgroundColor: `${info?.color || "#ffff"}`, + color: textColor, + }; + + await redis.set( + `mangaPage:${mangadexId}`, + JSON.stringify({ data, info, color }), + "ex", + 60 * 60 * 24 + ); + } + } + + return { + props: { + info: info || null, + anifyData: data || null, + chapterNotFound: chapterNotFound || null, + color: color || null, + }, + }; +} + +function getBrightness(hexColor) { + if (!hexColor) { + return 200; + } + const rgb = hexColor + .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) + .slice(1) + .map((x) => parseInt(x, 16)); + return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000; +} + +function setTxtColor(hexColor) { + const brightness = getBrightness(hexColor); + return brightness < 150 ? "#fff" : "#000"; +} diff --git a/pages/en/manga/[id].js b/pages/en/manga/[id].js deleted file mode 100644 index 6f25532..0000000 --- a/pages/en/manga/[id].js +++ /dev/null @@ -1,146 +0,0 @@ -import ChapterSelector from "@/components/manga/chapters"; -import HamburgerMenu from "@/components/manga/mobile/hamburgerMenu"; -import TopSection from "@/components/manga/info/topSection"; -import Footer from "@/components/shared/footer"; -import Head from "next/head"; -import { useEffect, useState } from "react"; -import { setCookie } from "nookies"; -import { getServerSession } from "next-auth"; -import { authOptions } from "../../api/auth/[...nextauth]"; -import getAnifyInfo from "@/lib/anify/info"; -import { NewNavbar } from "@/components/shared/NavBar"; - -export default function Manga({ info, userManga }) { - const [domainUrl, setDomainUrl] = useState(""); - const [firstEp, setFirstEp] = useState(); - const chaptersData = info.chapters.data; - - useEffect(() => { - setDomainUrl(window.location.origin); - }, []); - - return ( - <> - <Head> - <title> - {info - ? `Manga - ${ - info.title.romaji || info.title.english || info.title.native - }` - : "Getting Info..."} - </title> - <meta name="twitter:card" content="summary_large_image" /> - <meta - name="twitter:title" - content={`Moopa - ${info.title.romaji || info.title.english}`} - /> - <meta - name="twitter:description" - content={`${info.description?.slice(0, 180)}...`} - /> - <meta - name="twitter:image" - content={`${domainUrl}/api/og?title=${ - info.title.romaji || info.title.english - }&image=${info.bannerImage || info.coverImage}`} - /> - <meta - name="title" - data-title-romaji={info?.title?.romaji} - data-title-english={info?.title?.english} - data-title-native={info?.title?.native} - /> - </Head> - <div className="min-h-screen w-screen flex flex-col items-center relative"> - <HamburgerMenu /> - <NewNavbar info={info} manga={true} /> - <div className="flex flex-col w-screen items-center gap-5 md:gap-10 py-10 pt-nav"> - <div className="flex-center w-full relative z-30"> - <TopSection info={info} firstEp={firstEp} setCookie={setCookie} /> - <> - <div className="absolute hidden md:block z-20 bottom-0 h-1/2 w-full bg-secondary" /> - <div className="absolute hidden md:block z-20 top-0 h-1/2 w-full bg-transparent" /> - </> - </div> - <div className="w-[90%] xl:w-[70%] min-h-[35vh] z-40"> - {chaptersData.length > 0 ? ( - <ChapterSelector - chaptersData={chaptersData} - data={info} - setFirstEp={setFirstEp} - setCookie={setCookie} - userManga={userManga} - /> - ) : ( - <p>No Chapter Available :(</p> - )} - </div> - </div> - <Footer /> - </div> - </> - ); -} - -export async function getServerSideProps(context) { - const session = await getServerSession(context.req, context.res, authOptions); - const accessToken = session?.user?.token || null; - - const { id } = context.query; - const key = process.env.API_KEY; - const data = await getAnifyInfo(id, key); - - let userManga = null; - - if (session) { - const response = 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 { - status - progress - progressVolumes - status - } - id - idMal - title { - romaji - english - native - } - } - } - `, - variables: { - id: parseInt(id), - }, - }), - }); - const data = await response.json(); - const user = data?.data?.Media?.mediaListEntry; - if (user) { - userManga = user; - } - } - - if (!data?.chapters) { - return { - notFound: true, - }; - } - - return { - props: { - info: data, - userManga, - }, - }; -} diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js index a7769e2..1076601 100644 --- a/pages/en/manga/read/[...params].js +++ b/pages/en/manga/read/[...params].js @@ -10,13 +10,27 @@ import { authOptions } from "../../../api/auth/[...nextauth]"; import BottomBar from "@/components/manga/mobile/bottomBar"; import TopBar from "@/components/manga/mobile/topBar"; import Head from "next/head"; -import nookies from "nookies"; import ShortCutModal from "@/components/manga/modals/shortcutModal"; import ChapterModal from "@/components/manga/modals/chapterModal"; -import getAnifyPage from "@/lib/anify/page"; +// import getConsumetPages from "@/lib/consumet/manga/getPage"; +import { mediaInfoQuery } from "@/lib/graphql/query"; +// import { redis } from "@/lib/redis"; +// import getConsumetChapters from "@/lib/consumet/manga/getChapters"; +import { toast } from "sonner"; +import axios from "axios"; +import { redis } from "@/lib/redis"; +import getAnifyInfo from "@/lib/anify/info"; -export default function Read({ data, currentId, sessions }) { - const [info, setInfo] = useState(); +export default function Read({ + data, + info, + chaptersData, + currentId, + sessions, + provider, + mangaDexId, + number, +}) { const [chapter, setChapter] = useState([]); const [layout, setLayout] = useState(1); @@ -30,8 +44,8 @@ export default function Read({ data, currentId, sessions }) { const [paddingX, setPaddingX] = useState(208); const [scaleImg, setScaleImg] = useState(1); - const [nextChapterId, setNextChapterId] = useState(null); - const [prevChapterId, setPrevChapterId] = useState(null); + const [nextChapter, setNextChapter] = useState(null); + const [prevChapter, setPrevChapter] = useState(null); const [currentChapter, setCurrentChapter] = useState(null); const [currentPage, setCurrentPage] = useState(0); @@ -40,17 +54,22 @@ export default function Read({ data, currentId, sessions }) { const router = useRouter(); + // console.log({ info }); + useEffect(() => { - hasRun.current = false; - }, [currentId]); + toast.message("This page is still under development", { + description: "If you found any bugs, please report it to us!", + position: "top-center", + duration: 10000, + }); + }, []); useEffect(() => { - const get = JSON.parse(localStorage.getItem("manga")); - const chapters = get.manga; + hasRun.current = false; + const chapters = chaptersData.find((x) => x.providerId === provider); const currentChapter = chapters.chapters?.find((x) => x.id === currentId); setCurrentChapter(currentChapter); - setInfo(get.data); setChapter(chapters); if (Array.isArray(chapters?.chapters)) { @@ -60,25 +79,36 @@ export default function Read({ data, currentId, sessions }) { if (currentIndex !== -1) { const nextChapter = chapters.chapters[currentIndex - 1]; const prevChapter = chapters.chapters[currentIndex + 1]; - setNextChapterId(nextChapter ? nextChapter.id : null); - setPrevChapterId(prevChapter ? prevChapter.id : null); + setNextChapter(nextChapter ? nextChapter : null); + setPrevChapter(prevChapter ? prevChapter : null); } } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentId]); useEffect(() => { const handleKeyDown = (event) => { - if (event.key === "ArrowRight" && event.ctrlKey && nextChapterId) { + event.preventDefault(); + if (event.key === "ArrowRight" && event.ctrlKey && nextChapter?.id) { router.push( - `/en/manga/read/${chapter.providerId}?id=${ - info.id - }&chapterId=${encodeURIComponent(nextChapterId)}` + `/en/manga/read/${ + chapter.providerId + }?id=${mangaDexId}&chapterId=${encodeURIComponent(nextChapter?.id)}${ + info?.id?.length > 6 ? "" : `&anilist=${info?.id}` + }&num=${nextChapter?.number}` ); - } else if (event.key === "ArrowLeft" && event.ctrlKey && prevChapterId) { + } else if ( + event.key === "ArrowLeft" && + event.ctrlKey && + prevChapter?.id + ) { router.push( - `/en/manga/read/${chapter.providerId}?id=${ - info.id - }&chapterId=${encodeURIComponent(prevChapterId)}` + `/en/manga/read/${ + chapter.providerId + }?id=${mangaDexId}&chapterId=${encodeURIComponent(prevChapter?.id)}${ + info?.id?.length > 6 ? "" : `&anilist=${info?.id}` + }&num=${prevChapter?.number}` ); } if (event.code === "Slash" && event.ctrlKey) { @@ -99,7 +129,9 @@ export default function Read({ data, currentId, sessions }) { return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, [nextChapterId, prevChapterId, visible, isKeyOpen, paddingX]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nextChapter?.id, prevChapter?.id, visible, isKeyOpen, paddingX]); return ( <> @@ -134,13 +166,15 @@ export default function Read({ data, currentId, sessions }) { <TopBar info={info} /> <BottomBar id={info?.id} - prevChapter={prevChapterId} - nextChapter={nextChapterId} + prevChapter={prevChapter} + nextChapter={nextChapter} currentPage={currentPage} chapter={chapter} - page={data} + data={data} setSeekPage={setSeekPage} setIsOpen={setIsChapOpen} + number={number} + mangadexId={mangaDexId} /> </> )} @@ -149,13 +183,17 @@ export default function Read({ data, currentId, sessions }) { data={chapter} page={data} info={info} + number={number} + mediaId={mangaDexId} currentId={currentId} setSeekPage={setSeekPage} + providerId={provider} /> )} {layout === 1 && ( <FirstPanel aniId={info?.id} + providerId={provider} data={data} hasRun={hasRun} currentId={currentId} @@ -164,19 +202,22 @@ export default function Read({ data, currentId, sessions }) { visible={visible} setVisible={setVisible} chapter={chapter} - nextChapter={nextChapterId} - prevChapter={prevChapterId} + nextChapter={nextChapter} + prevChapter={prevChapter} paddingX={paddingX} session={sessions} mobileVisible={mobileVisible} setMobileVisible={setMobileVisible} setCurrentPage={setCurrentPage} + mangadexId={mangaDexId} + number={number} /> )} {layout === 2 && ( <SecondPanel aniId={info?.id} data={data} + chapterData={chapter} hasRun={hasRun} currentChapter={currentChapter} currentId={currentId} @@ -185,12 +226,14 @@ export default function Read({ data, currentId, sessions }) { visible={visible} setVisible={setVisible} session={sessions} + providerId={provider} /> )} {layout === 3 && ( <ThirdPanel aniId={info?.id} data={data} + chapterData={chapter} hasRun={hasRun} currentId={currentId} currentChapter={currentChapter} @@ -202,6 +245,7 @@ export default function Read({ data, currentId, sessions }) { scaleImg={scaleImg} setMobileVisible={setMobileVisible} mobileVisible={mobileVisible} + providerId={provider} /> )} {visible && ( @@ -224,42 +268,130 @@ export default function Read({ data, currentId, sessions }) { )} </div> </> + // <p></p> ); } -export async function getServerSideProps(context) { - const cookies = nookies.get(context); +async function fetchAnifyPages(id, number, provider, readId, key) { + try { + let cached; + cached = await redis.get(`pages:${readId}`); + + if (cached) { + return JSON.parse(cached); + } + + const url = `https://api.anify.tv/pages?id=${id}&chapterNumber=${number}&providerId=${provider}&readId=${encodeURIComponent( + readId + )}`; + + const { data } = await axios.get(url); + + if (!data) { + return null; + } + + await redis.set( + `pages:${readId}`, + JSON.stringify(data), + "EX", + 60 * 60 * 24 * 7 + ); + + return data; + } catch (error) { + return { error: "Error fetching data" }; + } +} + +export async function getServerSideProps(context) { const key = process.env.API_KEY; const query = context.query; const providerId = query.params[0]; const chapterId = query.chapterId; const mediaId = query.id; + const number = query.num; + const anilistId = query.anilist; + + const session = await getServerSession(context.req, context.res, authOptions); + const accessToken = session?.user?.token || null; + + // const data = await getConsumetPages(mediaId, providerId, chapterId, key); + // const chapters = await getConsumetChapters(mediaId, redis); + + const dataManga = await fetchAnifyPages( + mediaId, + number, + providerId, + chapterId, + mediaId, + key + ); + + let info; - if (!cookies.manga || cookies.manga !== mediaId) { + if (anilistId) { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + body: JSON.stringify({ + query: mediaInfoQuery, + variables: { + id: parseInt(anilistId), + type: "MANGA", + }, + }), + }); + const json = await response.json(); + info = json?.data?.Media; + } else { + const datas = await getAnifyInfo(mediaId); + if (datas) { + info = datas; + } + } + + const chapters = await ( + await fetch("https://api.anify.tv/chapters/" + mediaId + "?apikey=" + key) + ).json(); + + if ((dataManga && dataManga?.error) || dataManga?.length === 0) { return { redirect: { - destination: `/en/manga/${mediaId}`, + destination: `/en/manga/${anilistId}?chapter=404`, }, }; } - const session = await getServerSession(context.req, context.res, authOptions); - - const data = await getAnifyPage(mediaId, providerId, chapterId, key); + /* + const { data } = await axios.get( + `https://beta.moopa.live/api/v2/info/${romaji}${ + english ? `/${english}` : "" + }${native ? `/${native}` : ""}?id=${anilistId}` + ); if (data.error) { return { notFound: true, }; } + */ return { props: { - data: data, + data: dataManga, + mangaDexId: mediaId, + info: info, + number: number, + chaptersData: chapters, currentId: chapterId, sessions: session, + provider: providerId, }, }; } diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].js index b931597..7ef5de3 100644 --- a/pages/en/profile/[user].js +++ b/pages/en/profile/[user].js @@ -5,8 +5,8 @@ import Link from "next/link"; import Head from "next/head"; import { useEffect, useState } from "react"; import { getUser } from "@/prisma/user"; -import { toast } from "react-toastify"; import { NewNavbar } from "@/components/shared/NavBar"; +import { toast } from "sonner"; export default function MyList({ media, sessions, user, time, userSettings }) { const [listFilter, setListFilter] = useState("all"); diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].js index 603cd17..2cb609f 100644 --- a/pages/en/search/[...param].js +++ b/pages/en/search/[...param].js @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { AnimatePresence, motion as m } from "framer-motion"; +import { motion as m } from "framer-motion"; import Skeleton from "react-loading-skeleton"; import { useRouter } from "next/router"; import Link from "next/link"; @@ -25,6 +25,8 @@ import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid"; import useDebounce from "@/lib/hooks/useDebounce"; import { NewNavbar } from "@/components/shared/NavBar"; import MobileNav from "@/components/shared/MobileNav"; +import SearchByImage from "@/components/search/searchByImage"; +import { PlayIcon } from "@heroicons/react/24/outline"; export async function getServerSideProps(context) { const { param } = context.query; @@ -91,9 +93,10 @@ export default function Card({ }) { const inputRef = useRef(null); const router = useRouter(); - // const { data: session } = useSession(); const [data, setData] = useState(); + const [imageSearch, setImageSearch] = useState(); + const [loading, setLoading] = useState(true); const [search, setQuery] = useState(query); @@ -125,16 +128,18 @@ export default function Card({ }); if (data?.media?.length === 0) { setNextPage(false); + setLoading(false); } else if (data !== null && page > 1) { setData((prevData) => { return [...(prevData ?? []), ...data?.media]; }); setNextPage(data?.pageInfo.hasNextPage); + setLoading(false); } else { setData(data?.media); + setNextPage(data?.pageInfo.hasNextPage); + setLoading(false); } - setNextPage(data?.pageInfo.hasNextPage); - setLoading(false); } useEffect(() => { @@ -142,6 +147,7 @@ export default function Card({ setPage(1); setNextPage(true); advance(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ debounceSearch, type?.value, @@ -153,11 +159,17 @@ export default function Card({ ]); useEffect(() => { + if (imageSearch) return; advance(); - }, [page]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page, imageSearch]); useEffect(() => { function handleScroll() { + if (imageSearch) { + window.removeEventListener("scroll", handleScroll); + return; + } if (page > 10 || !nextPage) { window.removeEventListener("scroll", handleScroll); return; @@ -174,7 +186,7 @@ export default function Card({ window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); - }, [page, nextPage]); + }, [page, nextPage, imageSearch]); const handleKeyDown = async (event) => { if (event.key === "Enter") { @@ -189,6 +201,7 @@ export default function Card({ }; function trash() { + setImageSearch(); setQuery(); setGenre(); setFormat(); @@ -202,6 +215,18 @@ export default function Card({ setIsVisible(!isVisible); } + const handleVideoHover = (hovered, id) => { + const updatedImageSearch = imageSearch?.map((item) => { + if (item.filename === id) { + return { ...item, hovered }; + } + return item; + }); + setImageSearch(updatedImageSearch); + }; + + // console.log({ loading, data }); + return ( <> <Head> @@ -290,6 +315,7 @@ export default function Card({ > <Cog6ToothIcon className="w-5 h-5" /> </div> + <SearchByImage setMedia={setData} setData={setImageSearch} /> <div className="py-2 px-2 bg-secondary rounded flex justify-center items-center cursor-pointer hover:bg-opacity-75 transition-all duration-100 group" onClick={trash} @@ -343,91 +369,200 @@ export default function Card({ )} {/* <div> */} <div className="flex flex-col gap-14 items-center z-30"> - <AnimatePresence> - <div - key="card-keys" - className="grid pt-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 xxs:grid-cols-3 w-screen px-2 xl:w-auto xl:gap-10 gap-2 xl:gap-y-24 gap-y-12 overflow-hidden" - > - {loading - ? "" - : !data?.length && ( - <div className="w-screen text-[#ff7f57] xl:col-start-3 col-start-2 items-center flex justify-center text-center font-bold font-karla xl:text-2xl"> - Oops!<br></br> Nothing's Found... + <div + key="card-keys" + className={`${ + imageSearch ? "hidden" : "" + } grid pt-3 px-5 xl:px-0 xxs:grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 w-screen xl:w-auto xl:gap-7 gap-5 gap-y-10`} + > + {loading + ? "" + : !data && ( + <div className="w-full text-[#ff7f57] col-span-6 items-center flex justify-center text-center font-bold font-karla xl:text-2xl"> + Oops!<br></br> Nothing's Found... + </div> + )} + + {data && + data?.length > 0 && + !imageSearch && + data?.map((anime, index) => { + const anilistId = anime?.mappings?.find( + (x) => x.providerId === "anilist" + )?.id; + return ( + <m.div + initial={{ scale: 0.98 }} + animate={{ scale: 1, transition: { duration: 0.35 } }} + className="w-full" + key={index} + > + <Link + href={ + anime.format === "MANGA" || anime.format === "NOVEL" + ? `/en/manga/${ + anilistId ? anilistId : "" + }${`/${anime.id}`}` + : `/en/anime/${anime.id}` + } + title={anime.title.userPreferred} + className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded" + style={{ + paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%) + }} + > + <Image + className="object-cover" + src={anime.coverImage.extraLarge} + alt={anime.title.userPreferred} + sizes="(min-width: 808px) 50vw, 100vw" + quality={100} + fill + /> + </Link> + <Link + href={ + anime.format === "MANGA" || anime.format === "NOVEL" + ? `/en/manga/${ + anilistId ? anilistId : "" + }${`/${anime.id}`}` + : `/en/anime/${anime.id}` + } + title={anime.title.userPreferred} + > + <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> + {anime.status === "RELEASING" ? ( + <span className="dots bg-green-500" /> + ) : anime.status === "NOT_YET_RELEASED" ? ( + <span className="dots bg-red-500" /> + ) : null} + {anime.title.userPreferred} + </h1> + </Link> + <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]"> + {anime.format || <p>-</p>} ·{" "} + {anime.status || <p>-</p>} ·{" "} + {anime.episodes + ? `${anime.episodes || "N/A"} Episodes` + : `${anime.chapters || "N/A"} Chapters`} + </h2> + </m.div> + ); + })} + + {loading && ( + <> + {[1, 2, 4, 5, 6, 7, 8].map((item) => ( + <div className="w-full" key={item}> + <div className="w-full"> + <Skeleton + className="w-full rounded" + style={{ + paddingTop: "140%", // 2:3 aspect ratio (3/2 * 100%) + width: "(min-width: 808px) 50vw, 100vw", + lineHeight: 1, + }} + /> </div> - )} - {data && - data?.map((anime, index) => { - return ( - <m.div - initial={{ scale: 0.9 }} - animate={{ scale: 1, transition: { duration: 0.35 } }} - className="w-[146px] xxs:w-[115px] xs:w-[135px] xl:w-[185px]" - key={index} + <div> + <h1 className="font-outfit w-[320px] font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> + <Skeleton width={120} height={26} /> + </h1> + </div> + </div> + ))} + </> + )} + </div> + + {imageSearch && ( + <div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 gap-3 md:gap-7 px-5 lg:px-0"> + {imageSearch.map((a, index) => { + return ( + <m.div + key={index} + initial={{ scale: 0.9 }} + animate={{ scale: 1, transition: { duration: 0.35 } }} + className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" + > + <Link + className="relative aspect-video rounded-md overflow-hidden group" + href={`/en/anime/${a.anilist.id}`} + onMouseEnter={() => { + handleVideoHover(true, a.filename); + }} + onMouseLeave={() => handleVideoHover(false, a.filename)} > - <Link - href={ - anime.format === "MANGA" || anime.format === "NOVEL" - ? `/en/manga/${anime.id}` - : `/en/anime/${anime.id}` - } - title={anime.title.userPreferred} - > + <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" /> + <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30"> + <PlayIcon className="w-5 h-5 shrink-0" /> + <h1 + className="font-semibold font-karla line-clamp-1" + title={a?.anilist.title.romaji} + > + {`Episode ${a.episode}`} + </h1> + </div> + + {a?.image && ( <Image - className="object-cover bg-[#3B3C41] w-[146px] h-[208px] xxs:w-[115px] xxs:h-[163px] xs:w-[135px] xs:h-[192px] xl:w-[185px] xl:h-[265px] hover:scale-105 scale-100 transition-all cursor-pointer duration-200 ease-out rounded-[10px]" - src={anime.coverImage.extraLarge} - alt={anime.title.userPreferred} - width={500} - height={500} + src={a?.image} + width={200} + height={200} + alt="Episode Thumbnail" + className={`w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10 ${ + !a.hovered ? "visible" : "hidden" + }`} /> - </Link> - <Link - href={`/en/anime/${anime.id}`} - title={anime.title.userPreferred} - > - <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> - {anime.status === "RELEASING" ? ( - <span className="dots bg-green-500" /> - ) : anime.status === "NOT_YET_RELEASED" ? ( - <span className="dots bg-red-500" /> - ) : null} - {anime.title.userPreferred} - </h1> - </Link> - <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]"> - {anime.format || <p>-</p>} ·{" "} - {anime.status || <p>-</p>} ·{" "} - {anime.episodes - ? `${anime.episodes || "N/A"} Episodes` - : `${anime.chapters || "N/A"} Chapters`} - </h2> - </m.div> - ); - })} - - {loading && ( - <> - {[1, 2, 4, 5, 6, 7, 8].map((item) => ( - <div - key={item} - className="flex flex-col w-[135px] xl:w-[185px] gap-5" - style={{ scale: 0.98 }} + )} + {a?.video && ( + <video + src={a.video} + className={`w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10 ${ + a.hovered ? "visible" : "hidden" + }`} + autoPlay + muted + loop + playsInline + /> + )} + </Link> + + <Link + className="flex flex-col font-karla w-full" + href={`/en/anime/${a.anilist.id}`} > - <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" /> - <Skeleton width={110} height={30} /> - </div> - ))} - </> - )} + {/* <h1 className="font-semibold">{a.title}</h1> */} + <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]"> + <span + className="text-white max-w-[120px] md:max-w-[200px] lg:max-w-[220px]" + style={{ + display: "inline-block", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }} + title={a?.anilist.title.romaji} + > + {a?.anilist.title.romaji} + </span>{" "} + | Episode {a.episode} + </p> + </Link> + </m.div> + ); + })} </div> - {!loading && page > 10 && nextPage && ( - <button - onClick={() => setPage((p) => p + 1)} - className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md" - > - Load More - </button> - )} - </AnimatePresence> + )} + {!loading && page > 10 && nextPage && ( + <button + onClick={() => setPage((p) => p + 1)} + className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md" + > + Load More + </button> + )} </div> {/* </div> */} </div> |