diff options
Diffstat (limited to 'pages/en')
| -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 |
9 files changed, 859 insertions, 300 deletions
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> |