aboutsummaryrefslogtreecommitdiff
path: root/pages/en
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-09-13 00:45:53 +0700
committerGitHub <[email protected]>2023-09-13 00:45:53 +0700
commit7327a69b55a20b99b14ee0803d6cf5f8b88c45ef (patch)
treecbcca777593a8cc4b0282e7d85a6fc51ba517e25 /pages/en
parentUpdate issue templates (diff)
downloadmoopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.tar.xz
moopa-7327a69b55a20b99b14ee0803d6cf5f8b88c45ef.zip
Update v4 - Merge pre-push to main (#71)
* Create build-test.yml * initial v4 commit * update: github workflow * update: push on branch * Update .github/ISSUE_TEMPLATE/bug_report.md * configuring next.config.js file
Diffstat (limited to 'pages/en')
-rw-r--r--pages/en/about.js10
-rw-r--r--pages/en/anime/[...id].js252
-rw-r--r--pages/en/anime/popular.js21
-rw-r--r--pages/en/anime/recent.js163
-rw-r--r--pages/en/anime/recently-watched.js130
-rw-r--r--pages/en/anime/trending.js21
-rw-r--r--pages/en/anime/watch/[...info].js240
-rw-r--r--pages/en/dmca.js2
-rw-r--r--pages/en/index.js361
-rw-r--r--pages/en/manga/[id].js72
-rw-r--r--pages/en/manga/read/[...params].js9
-rw-r--r--pages/en/profile/[user].js116
-rw-r--r--pages/en/schedule/index.js523
-rw-r--r--pages/en/search/[...param].js433
-rw-r--r--pages/en/search/[param].js496
15 files changed, 1860 insertions, 989 deletions
diff --git a/pages/en/about.js b/pages/en/about.js
index 9bd32ed..cfbee6b 100644
--- a/pages/en/about.js
+++ b/pages/en/about.js
@@ -8,9 +8,17 @@ export default function About() {
<>
<Head>
<title>Moopa - About</title>
+ <meta name="title" content="About" />
+ <meta
+ name="description"
+ content="Moopa is a platform where you can watch and stream anime or read
+ manga for free, without any ads or VPNs. Our mission is to provide
+ a convenient and enjoyable experience for anime and manga
+ enthusiasts all around the world."
+ />
<meta name="about" content="About this web" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <link rel="icon" href="/c.svg" />
+ <link rel="icon" href="/svg/c.svg" />
</Head>
<Layout>
<motion.div
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
index 534aa17..71dae56 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -2,7 +2,6 @@ import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
-import Layout from "../../../components/layout";
import Content from "../../../components/home/content";
import Modal from "../../../components/modal";
@@ -10,22 +9,26 @@ import { signIn, useSession } from "next-auth/react";
import AniList from "../../../components/media/aniList";
import ListEditor from "../../../components/listEditor";
-import { GET_MEDIA_USER } from "../../../queries";
-import { GET_MEDIA_INFO } from "../../../queries";
-
import { ToastContainer } from "react-toastify";
import DetailTop from "../../../components/anime/mobile/topSection";
-import DesktopDetails from "../../../components/anime/infoDetails";
import AnimeEpisode from "../../../components/anime/episode";
+import { useAniList } from "../../../lib/anilist/useAnilist";
+import Footer from "../../../components/footer";
+import { mediaInfoQuery } from "../../../lib/graphql/query";
+import MobileNav from "../../../components/shared/MobileNav";
+import redis from "../../../lib/redis";
export default function Info({ info, color }) {
const { data: session } = useSession();
+ const { getUserLists } = useAniList(session);
+
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
const [statuses, setStatuses] = useState(null);
const [domainUrl, setDomainUrl] = useState("");
- const [showAll, setShowAll] = useState(false);
+ const [watch, setWatch] = useState();
+
const [open, setOpen] = useState(false);
const { id } = useRouter().query;
@@ -45,40 +48,20 @@ export default function Info({ info, color }) {
setStatuses(null);
if (session?.user?.name) {
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: GET_MEDIA_USER,
- variables: {
- username: session?.user?.name,
- },
- }),
- });
-
- const responseData = await response.json();
+ const res = await getUserLists(info.id);
+ const user = res?.data?.Media?.mediaListEntry;
- const prog = responseData?.data?.MediaListCollection;
-
- if (prog && prog.lists.length > 0) {
- const gut = prog.lists
- .flatMap((item) => item.entries)
- .find((item) => item.mediaId === parseInt(id[0]));
-
- if (gut) {
- setProgress(gut.progress);
- const statusMapping = {
- CURRENT: { name: "Watching", value: "CURRENT" },
- PLANNING: { name: "Plan to watch", value: "PLANNING" },
- COMPLETED: { name: "Completed", value: "COMPLETED" },
- DROPPED: { name: "Dropped", value: "DROPPED" },
- PAUSED: { name: "Paused", value: "PAUSED" },
- REPEATING: { name: "Rewatching", value: "REPEATING" },
- };
- setStatuses(statusMapping[gut.status]);
- }
+ if (user) {
+ setProgress(user.progress);
+ const statusMapping = {
+ CURRENT: { name: "Watching", value: "CURRENT" },
+ PLANNING: { name: "Plan to watch", value: "PLANNING" },
+ COMPLETED: { name: "Completed", value: "COMPLETED" },
+ DROPPED: { name: "Dropped", value: "DROPPED" },
+ PAUSED: { name: "Paused", value: "PAUSED" },
+ REPEATING: { name: "Rewatching", value: "REPEATING" },
+ };
+ setStatuses(statusMapping[user.status]);
}
}
} catch (error) {
@@ -109,6 +92,14 @@ export default function Info({ info, color }) {
? info?.title?.romaji || info?.title?.english
: "Retrieving Data..."}
</title>
+ <meta
+ name="title"
+ content={info?.title?.romaji}
+ data-title-romaji={info?.title?.romaji}
+ data-title-english={info?.title?.english}
+ data-title-native={info?.title?.native}
+ />
+ <meta name="description" content={info.description} />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
@@ -159,62 +150,43 @@ export default function Info({ info, color }) {
)}
</div>
</Modal>
- <Layout navTop="text-white bg-primary lg:pt-0 lg:px-0 bg-slate bg-opacity-40 z-50">
- <div className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5">
- <div className="bg-image w-screen">
- <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[300px] w-screen z-10 inset-0" />
- {info ? (
- <>
- {info?.bannerImage && (
- <Image
- src={info?.bannerImage}
- priority={true}
- alt="banner anime"
- height={1000}
- width={1000}
- className="hidden md:block object-cover bg-image w-screen absolute top-0 left-0 h-[300px] brightness-[70%] z-0"
- />
- )}
- <Image
- src={info?.coverImage.extraLarge || info?.coverImage.large}
- priority={true}
- alt="banner anime"
- height={1000}
- width={1000}
- className="md:hidden object-cover bg-image w-screen absolute top-0 left-0 h-[300px] brightness-[70%] z-0"
- />
- </>
- ) : (
- <div className="bg-image w-screen absolute top-0 left-0 h-[300px]" />
- )}
- </div>
- <div className="lg:w-[90%] xl:w-[75%] lg:pt-[10rem] z-30 flex flex-col gap-5">
- {/* Mobile Anime Information */}
-
- <DetailTop
- info={info}
- handleOpen={handleOpen}
- loading={loading}
- statuses={statuses}
- />
-
- {/* PC Anime Information*/}
- <DesktopDetails
- info={info}
- color={color}
- handleOpen={handleOpen}
- loading={loading}
- statuses={statuses}
- setShowAll={setShowAll}
- showAll={showAll}
+ <MobileNav sessions={session} hideProfile={true} />
+ <main className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5">
+ <div className="w-screen absolute">
+ <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" />
+ {info?.bannerImage && (
+ <Image
+ src={info?.bannerImage}
+ priority={true}
+ alt="banner anime"
+ height={1000}
+ width={1000}
+ className="object-cover blur-[2px] bg-image w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0"
/>
+ )}
+ </div>
+ <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5">
+ <DetailTop
+ info={info}
+ session={session}
+ handleOpen={handleOpen}
+ loading={loading}
+ statuses={statuses}
+ watchUrl={watch}
+ progress={progress}
+ color={color}
+ />
- {/* Episodes */}
+ <AnimeEpisode
+ info={info}
+ session={session}
+ progress={progress}
+ setProgress={setProgress}
+ setWatch={setWatch}
+ />
- <AnimeEpisode info={info} progress={progress} />
- </div>
{info && rec?.length !== 0 && (
- <div className="w-screen lg:w-[90%] xl:w-[85%]">
+ <div className="w-full">
<Content
ids="recommendAnime"
section="Recommendations"
@@ -223,51 +195,85 @@ export default function Info({ info, color }) {
</div>
)}
</div>
- </Layout>
+ </main>
+ <Footer />
</>
);
}
-export async function getServerSideProps(context) {
- const { id } = context.query;
+export async function getServerSideProps(ctx) {
+ const { id } = ctx.query;
const API_URI = process.env.API_URI;
- const res = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: GET_MEDIA_INFO,
- variables: {
- id: id?.[0],
- },
- }),
- });
+ let cache;
- const json = await res.json();
- const data = json?.data?.Media;
+ if (redis) {
+ cache = await redis.get(`anime:${id}`);
+ }
- if (!data) {
+ if (cache) {
+ const { info, color } = JSON.parse(cache);
return {
- notFound: true,
+ props: {
+ info,
+ color,
+ api: API_URI,
+ },
};
- }
+ } else {
+ const resp = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ query: mediaInfoQuery,
+ variables: {
+ id: id?.[0],
+ },
+ }),
+ });
+
+ const json = await resp.json();
+ const data = json?.data?.Media;
+
+ const cacheTime = data.nextAiringEpisode?.episode
+ ? 60 * 10
+ : 60 * 60 * 24 * 30;
+
+ if (!data) {
+ return {
+ notFound: true,
+ };
+ }
- const textColor = setTxtColor(data?.coverImage?.color);
+ const textColor = setTxtColor(data?.coverImage?.color);
- const color = {
- backgroundColor: `${data?.coverImage?.color || "#ffff"}`,
- color: textColor,
- };
+ const color = {
+ backgroundColor: `${data?.coverImage?.color || "#ffff"}`,
+ color: textColor,
+ };
+
+ if (redis) {
+ await redis.set(
+ `anime:${id}`,
+ JSON.stringify({
+ info: data,
+ color: color,
+ }),
+ "EX",
+ cacheTime
+ );
+ }
- return {
- props: {
- info: data,
- color: color,
- api: API_URI,
- },
- };
+ return {
+ props: {
+ info: data,
+ color: color,
+ api: API_URI,
+ },
+ };
+ }
}
function getBrightness(hexColor) {
diff --git a/pages/en/anime/popular.js b/pages/en/anime/popular.js
index 8cbbeab..7b40a0e 100644
--- a/pages/en/anime/popular.js
+++ b/pages/en/anime/popular.js
@@ -1,12 +1,13 @@
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
-import { useEffect, useState } from "react";
+import { Fragment, useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import MobileNav from "../../../components/home/mobileNav";
+import Head from "next/head";
+import MobileNav from "../../../components/shared/MobileNav";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
@@ -94,9 +95,17 @@ export default function PopularAnime({ sessions }) {
}, [page, nextPage]);
return (
- <>
+ <Fragment>
+ <Head>
+ <title>Moopa - Popular Anime</title>
+ <meta name="title" content="Popular Anime" />
+ <meta
+ name="description"
+ content="Explore Beloved Classics and Favorites - Dive into a curated collection of timeless anime on Moopa's Popular Anime Page. From iconic classics to all-time favorites, experience the stories that have captured hearts worldwide. Start streaming now and relive the magic of anime!"
+ />
+ </Head>
<MobileNav sessions={sessions} />
- <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
+ <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
<div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
<ChevronLeftIcon className="w-5 h-5" />
@@ -165,9 +174,9 @@ export default function PopularAnime({ sessions }) {
Load More
</button>
)}
- </div>
+ </main>
<Footer />
- </>
+ </Fragment>
);
}
diff --git a/pages/en/anime/recent.js b/pages/en/anime/recent.js
new file mode 100644
index 0000000..89a868a
--- /dev/null
+++ b/pages/en/anime/recent.js
@@ -0,0 +1,163 @@
+import Head from "next/head";
+import { Fragment, useEffect, useState } from "react";
+import Link from "next/link";
+import { ChevronLeftIcon } from "@heroicons/react/24/outline";
+import Skeleton from "react-loading-skeleton";
+import Footer from "../../../components/footer";
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../api/auth/[...nextauth]";
+import Image from "next/image";
+import MobileNav from "../../../components/shared/MobileNav";
+
+export async function getServerSideProps(context) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ return {
+ props: {
+ sessions: session,
+ },
+ };
+}
+
+export default function Recent({ sessions }) {
+ const [data, setData] = useState(null);
+ const [page, setPage] = useState(1);
+ const [nextPage, setNextPage] = useState(true);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setLoading(true);
+ async function getRecent() {
+ const data = await fetch(`/api/v2/etc/recent/${page}`).then((res) =>
+ res.json()
+ );
+ if (data?.results?.length === 0) {
+ setNextPage(false);
+ } else if (data !== null && page > 1) {
+ setData((prevData) => {
+ return [...(prevData ?? []), ...data?.results];
+ });
+ setNextPage(data?.hasNextPage);
+ } else {
+ setData(data?.results);
+ }
+ setNextPage(data?.hasNextPage);
+ setLoading(false);
+ }
+ getRecent();
+ }, [page]);
+
+ useEffect(() => {
+ function handleScroll() {
+ if (page > 5 || !nextPage) {
+ window.removeEventListener("scroll", handleScroll);
+ return;
+ }
+
+ if (
+ window.innerHeight + window.pageYOffset >=
+ document.body.offsetHeight - 3
+ ) {
+ setPage((prevPage) => prevPage + 1);
+ }
+ }
+
+ window.addEventListener("scroll", handleScroll);
+
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, [page, nextPage]);
+
+ return (
+ <Fragment>
+ <Head>
+ <title>Moopa - New Episodes</title>
+ <meta name="title" content="New Episodes" />
+ <meta
+ name="description"
+ content="Explore Beloved Classics and Favorites - Dive into a curated collection of timeless anime on Moopa's New Episodes Page. From iconic classics to all-time favorites, experience the stories that have captured hearts worldwide. Start streaming now and relive the magic of anime!"
+ />
+ </Head>
+ <MobileNav sessions={sessions} />
+ <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
+ <div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
+ <Link href="/en" className="flex gap-2 items-center font-karla">
+ <ChevronLeftIcon className="w-5 h-5" />
+ <h1 className="text-xl">New Episodes</h1>
+ </Link>
+ </div>
+ <div className="grid grid-cols-2 xs:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-6 gap-5 max-w-6xl pt-20">
+ {data?.map((i, index) => (
+ <div
+ key={index}
+ className="flex flex-col items-center w-[150px] lg:w-[180px]"
+ >
+ <Link
+ href={`/en/anime/${i.id}`}
+ className=" relative hover:scale-105 scale-100 transition-all duration-200 ease-out"
+ title={i.title.romaji}
+ >
+ <div className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded opacity-90 z-20">
+ <div className="absolute bg-gradient-to-b from-black/30 to-transparent from-5% to-30% top-0 z-30 w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] rounded" />
+ <Image
+ src={i.image}
+ alt={i.title.romaji}
+ width={500}
+ height={500}
+ className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] object-cover rounded opacity-90 z-20"
+ />
+ </div>
+ <Image
+ src="/svg/episode-badge.svg"
+ alt="episode-bade"
+ width={200}
+ height={100}
+ className="w-24 lg:w-28 absolute top-1 -right-[13px] lg:-right-[15px] z-40"
+ />
+ <p className="absolute z-40 text-center w-[80px] lg:w-[100px] top-[5px] -right-2 lg:top-[4px] lg:-right-3 font-karla text-sm lg:text-base">
+ Episode <span className="text-white">{i?.episodeNumber}</span>
+ </p>
+ </Link>
+ <Link
+ href={`/en/anime/${i.id}`}
+ className="w-full px-1 py-2"
+ title={i.title.romaji}
+ >
+ <h1 className="font-karla font-bold xl:text-base text-[15px] line-clamp-2">
+ <span className="dots bg-green-500" />
+ {i.title.romaji}
+ </h1>
+ </Link>
+ </div>
+ ))}
+
+ {loading && (
+ <>
+ {[1, 2, 4, 5, 6, 7, 8].map((item) => (
+ <div
+ key={item}
+ className="flex flex-col items-center w-[150px] lg:w-[180px]"
+ >
+ <div className="w-full p-2">
+ <Skeleton className="w-[140px] h-[190px] lg:w-[170px] lg:h-[230px] rounded" />
+ </div>
+ <div className="w-full px-2">
+ <Skeleton width={80} height={20} />
+ </div>
+ </div>
+ ))}
+ </>
+ )}
+ </div>
+ {!loading && page > 5 && nextPage && (
+ <button
+ onClick={() => setPage((p) => p + 1)}
+ className="bg-secondary xl:w-[30%] w-[80%] h-10 rounded-md"
+ >
+ Load More
+ </button>
+ )}
+ </main>
+ <Footer />
+ </Fragment>
+ );
+}
diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js
index 1cc713a..9d3b6cf 100644
--- a/pages/en/anime/recently-watched.js
+++ b/pages/en/anime/recently-watched.js
@@ -6,14 +6,18 @@ import Skeleton from "react-loading-skeleton";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import MobileNav from "../../../components/home/mobileNav";
import { ToastContainer, toast } from "react-toastify";
-import { XMarkIcon } from "@heroicons/react/24/outline";
+import { ChevronRightIcon } from "@heroicons/react/24/outline";
+import { useRouter } from "next/router";
+import HistoryOptions from "../../../components/home/content/historyOptions";
+import Head from "next/head";
+import MobileNav from "../../../components/shared/MobileNav";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [remove, setRemoved] = useState();
+ const router = useRouter();
useEffect(() => {
setLoading(true);
@@ -49,9 +53,9 @@ export default function PopularAnime({ sessions }) {
}
};
fetchData();
- }, [remove]);
+ }, [sessions?.user?.name, remove]);
- const removeItem = async (id) => {
+ const removeItem = async (id, aniId) => {
if (sessions?.user?.name) {
// remove from database
const res = await fetch(`/api/user/update/episode`, {
@@ -61,24 +65,42 @@ export default function PopularAnime({ sessions }) {
},
body: JSON.stringify({
name: sessions?.user?.name,
- id: id,
+ id,
+ aniId,
}),
});
const data = await res.json();
- // remove from local storage
- const artplayerSettings =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
- if (artplayerSettings[id]) {
- delete artplayerSettings[id];
- localStorage.setItem(
- "artplayer_settings",
- JSON.stringify(artplayerSettings)
- );
+ if (id) {
+ // remove from local storage
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+ }
+ if (aniId) {
+ const currentData =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+
+ const updatedData = {};
+
+ for (const key in currentData) {
+ const item = currentData[key];
+ if (item.aniId !== aniId) {
+ updatedData[key] = item;
+ }
+ }
+
+ localStorage.setItem("artplayer_settings", JSON.stringify(updatedData));
}
// update client
- setRemoved(id);
+ setRemoved(id || aniId);
if (data?.message === "Episode deleted") {
toast.success("Episode removed from history", {
@@ -91,22 +113,46 @@ export default function PopularAnime({ sessions }) {
});
}
} else {
- const artplayerSettings =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
- if (artplayerSettings[id]) {
- delete artplayerSettings[id];
- localStorage.setItem(
- "artplayer_settings",
- JSON.stringify(artplayerSettings)
- );
+ if (id) {
+ // remove from local storage
+ const artplayerSettings =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ if (artplayerSettings[id]) {
+ delete artplayerSettings[id];
+ localStorage.setItem(
+ "artplayer_settings",
+ JSON.stringify(artplayerSettings)
+ );
+ }
+ setRemoved(id);
}
+ if (aniId) {
+ const currentData =
+ JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+
+ // Create a new object to store the updated data
+ const updatedData = {};
- setRemoved(id);
+ // Iterate through the current data and copy items with different aniId to the updated object
+ for (const key in currentData) {
+ const item = currentData[key];
+ if (item.aniId !== aniId) {
+ updatedData[key] = item;
+ }
+ }
+
+ // Update localStorage with the filtered data
+ localStorage.setItem("artplayer_settings", JSON.stringify(updatedData));
+ setRemoved(aniId);
+ }
}
};
return (
<>
+ <Head>
+ <title>Moopa - Recently Watched Episodes</title>
+ </Head>
<MobileNav sessions={sessions} />
<ToastContainer pauseOnHover={false} />
<div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
@@ -130,16 +176,32 @@ export default function PopularAnime({ sessions }) {
key={i.watchId}
className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item"
>
- <div className="absolute z-40 top-1 right-1 group-hover/item:visible invisible hover:text-action">
- <div
- className="flex flex-col items-center group/delete"
- onClick={() => removeItem(i.watchId)}
- >
- <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full" />
- <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible">
- Remove from history
- </span>
- </div>
+ <div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible">
+ <HistoryOptions
+ remove={removeItem}
+ watchId={i.watchId}
+ aniId={i.aniId}
+ />
+ {i?.nextId && (
+ <button
+ type="button"
+ className="flex flex-col items-center group/next relative"
+ onClick={() => {
+ router.push(
+ `/en/anime/watch/${i.aniId}/${
+ i.provider
+ }?id=${encodeURIComponent(i?.nextId)}&num=${
+ i?.nextNumber
+ }`
+ );
+ }}
+ >
+ <ChevronRightIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" />
+ <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/next:visible group-hover/next:scale-100 group-hover/next:translate-x-0 group-hover/next:opacity-100 opacity-0 translate-x-10 scale-50 invisible">
+ Play Next Episode
+ </span>
+ </button>
+ )}
</div>
<Link
className="relative md:w-[320px] aspect-video rounded-md overflow-hidden group"
diff --git a/pages/en/anime/trending.js b/pages/en/anime/trending.js
index 9f8a187..18eadf9 100644
--- a/pages/en/anime/trending.js
+++ b/pages/en/anime/trending.js
@@ -1,12 +1,13 @@
import { ChevronLeftIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
-import { useEffect, useState } from "react";
+import { Fragment, useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
import Footer from "../../../components/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import MobileNav from "../../../components/home/mobileNav";
+import Head from "next/head";
+import MobileNav from "../../../components/shared/MobileNav";
export default function TrendingAnime({ sessions }) {
const [data, setData] = useState(null);
@@ -94,9 +95,17 @@ export default function TrendingAnime({ sessions }) {
}, [page, nextPage]);
return (
- <>
+ <Fragment>
+ <Head>
+ <title>Moopa - Trending Anime</title>
+ <meta name="title" content="Trending Anime" />
+ <meta
+ name="description"
+ content="Explore Top Trending Anime - Dive into the latest and most popular anime series on Moopa. From thrilling action to heartwarming romance, discover the buzzworthy shows that have everyone talking. Stream now and stay up-to-date with the hottest anime trends!"
+ />
+ </Head>
<MobileNav sessions={sessions} />
- <div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
+ <main className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
<div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
<ChevronLeftIcon className="w-5 h-5" />
@@ -165,9 +174,9 @@ export default function TrendingAnime({ sessions }) {
Load More
</button>
)}
- </div>
+ </main>
<Footer />
- </>
+ </Fragment>
);
}
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index c17d9c5..aa0b672 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -4,156 +4,90 @@ import { useEffect, useState } from "react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../../api/auth/[...nextauth]";
-import dotenv from "dotenv";
import Navigasi from "../../../../components/home/staticNav";
import PrimarySide from "../../../../components/anime/watch/primarySide";
import SecondarySide from "../../../../components/anime/watch/secondarySide";
-import { GET_MEDIA_USER } from "../../../../queries";
import { createList, createUser, getEpisode } from "../../../../prisma/user";
-// import { updateUser } from "../../../../prisma/user";
export default function Info({
sessions,
- aniId,
watchId,
provider,
epiNumber,
dub,
+ info,
userData,
proxy,
disqus,
}) {
- const [info, setInfo] = useState(null);
const [currentEpisode, setCurrentEpisode] = useState(null);
const [loading, setLoading] = useState(false);
- const [progress, setProgress] = useState(0);
- const [statuses, setStatuses] = useState("CURRENT");
const [artStorage, setArtStorage] = useState(null);
const [episodesList, setepisodesList] = useState();
+ const [mapProviders, setMapProviders] = useState(null);
+
const [onList, setOnList] = useState(false);
+ const [origin, setOrigin] = useState(null);
useEffect(() => {
setLoading(true);
+ setOrigin(window.location.origin);
async function getInfo() {
- const ress = await fetch(`https://graphql.anilist.co`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: `query ($id: Int) {
- Media (id: $id) {
- id
- idMal
- title {
- romaji
- english
- native
- }
- status
- genres
- episodes
- studios {
- edges {
- node {
- id
- name
- }
- }
- }
- bannerImage
- description
- coverImage {
- extraLarge
- color
- }
- synonyms
-
- }
- }
- `,
- variables: {
- id: aniId,
- },
- }),
- });
- const data = await ress.json();
-
- if (sessions?.user?.name) {
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: GET_MEDIA_USER,
- variables: {
- username: sessions?.user?.name,
- },
- }),
- });
-
- const responseData = await response.json();
-
- const prog = responseData?.data?.MediaListCollection;
-
- if (prog && prog.lists.length > 0) {
- const gut = prog.lists
- .flatMap((item) => item.entries)
- .find((item) => item.mediaId === parseInt(aniId));
+ if (info.mediaListEntry) {
+ setOnList(true);
+ }
- if (gut) {
- setProgress(gut.progress);
- setOnList(true);
- }
+ const response = await fetch(
+ `/api/v2/episode/${info.id}?releasing=${
+ info.status === "RELEASING" ? "true" : "false"
+ }${dub ? "&dub=true" : ""}`
+ ).then((res) => res.json());
+ const getMap = response.find((i) => i?.map === true) || response[0];
+ let episodes = response;
- if (gut?.status === "COMPLETED") {
- setStatuses("REPEATING");
- } else if (
- gut?.status === "REPEATING" &&
- gut?.media?.episodes === parseInt(epiNumber)
- ) {
- setStatuses("COMPLETED");
- } else if (gut?.status === "REPEATING") {
- setStatuses("REPEATING");
- } else if (gut?.media?.episodes === parseInt(epiNumber)) {
- setStatuses("COMPLETED");
- } else if (
- gut?.media?.episodes !== null &&
- data?.data?.Media.episodes === parseInt(epiNumber)
- ) {
- setStatuses("COMPLETED");
- setLoading(false);
- }
+ if (getMap) {
+ if (provider === "gogoanime" && !watchId.startsWith("/")) {
+ episodes = episodes.filter((i) => {
+ if (i?.providerId === "gogoanime" && i?.map !== true) {
+ return null;
+ }
+ return i;
+ });
}
- }
-
- setInfo(data.data.Media);
- const response = await fetch(
- `/api/consumet/episode/${aniId}${dub ? `?dub=${dub}` : ""}`
- );
- const episodes = await response.json();
+ setMapProviders(getMap?.episodes);
+ }
if (episodes) {
- const getProvider = episodes.data?.find(
- (i) => i.providerId === provider
+ const getProvider = episodes?.find((i) => i.providerId === provider);
+ const episodeList = dub
+ ? getProvider?.episodes?.filter((x) => x.hasDub === true)
+ : getProvider?.episodes.slice(0, getMap?.episodes.length);
+ const playingData = getMap?.episodes.find(
+ (i) => i.number === Number(epiNumber)
);
+
if (getProvider) {
- setepisodesList(getProvider.episodes);
- const currentEpisode = getProvider.episodes?.find(
+ setepisodesList(episodeList);
+ const currentEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber)
);
- const nextEpisode = getProvider.episodes?.find(
+ const nextEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber) + 1
);
- const previousEpisode = getProvider.episodes?.find(
+ const previousEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber) - 1
);
setCurrentEpisode({
prev: previousEpisode,
- playing: currentEpisode,
+ playing: {
+ id: currentEpisode.id,
+ title: playingData?.title,
+ description: playingData?.description,
+ image: playingData?.image,
+ number: currentEpisode.number,
+ },
next: nextEpisode,
});
} else {
@@ -176,6 +110,36 @@ export default function Info({
<>
<Head>
<title>{info?.title?.romaji || "Retrieving data..."}</title>
+ <meta
+ name="title"
+ data-title-romaji={info?.title?.romaji}
+ data-title-english={info?.title?.english}
+ data-title-native={info?.title?.native}
+ />
+ <meta
+ name="description"
+ content={currentEpisode?.playing?.description || info?.description}
+ />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta
+ name="twitter:title"
+ content={`Episode ${epiNumber} - ${
+ info.title.romaji || info.title.english
+ }`}
+ />
+ <meta
+ name="twitter:description"
+ content={`${
+ currentEpisode?.playing?.description?.slice(0, 180) ||
+ info?.description?.slice(0, 180)
+ }...`}
+ />
+ <meta
+ name="twitter:image"
+ content={`${origin}/api/og?title=${
+ info.title.romaji || info.title.english
+ }&image=${info.bannerImage || info.coverImage.extraLarge}`}
+ />
</Head>
<Navigasi />
@@ -189,7 +153,6 @@ export default function Info({
epiNumber={epiNumber}
providerId={provider}
watchId={watchId}
- status={statuses}
onList={onList}
proxy={proxy}
disqus={disqus}
@@ -201,10 +164,10 @@ export default function Info({
/>
<SecondarySide
info={info}
+ map={mapProviders}
providerId={provider}
watchId={watchId}
episode={episodesList}
- progress={progress}
artStorage={artStorage}
dub={dub}
/>
@@ -215,9 +178,8 @@ export default function Info({
}
export async function getServerSideProps(context) {
- dotenv.config();
-
const session = await getServerSession(context.req, context.res, authOptions);
+ const accessToken = session?.user?.token || null;
const query = context.query;
if (!query) {
@@ -236,6 +198,57 @@ export async function getServerSideProps(context) {
let userData = null;
+ const ress = await fetch(`https://graphql.anilist.co`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
+ },
+ body: JSON.stringify({
+ query: `query ($id: Int) {
+ Media (id: $id) {
+ mediaListEntry {
+ progress
+ status
+ customLists
+ repeat
+ }
+ id
+ idMal
+ title {
+ romaji
+ english
+ native
+ }
+ status
+ genres
+ episodes
+ studios {
+ edges {
+ node {
+ id
+ name
+ }
+ }
+ }
+ bannerImage
+ description
+ coverImage {
+ extraLarge
+ color
+ }
+ synonyms
+
+ }
+ }
+ `,
+ variables: {
+ id: aniId,
+ },
+ }),
+ });
+ const data = await ress.json();
+
try {
if (session) {
await createUser(session.user.name);
@@ -264,6 +277,7 @@ export async function getServerSideProps(context) {
epiNumber: epiNumber || null,
dub: dub || null,
userData: userData?.[0] || null,
+ info: data.data.Media || null,
proxy,
disqus,
},
diff --git a/pages/en/dmca.js b/pages/en/dmca.js
index fd93811..d6d7ccf 100644
--- a/pages/en/dmca.js
+++ b/pages/en/dmca.js
@@ -16,7 +16,7 @@ export default function DMCA() {
/>
<meta property="og:image" content="/icon-512x512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <link rel="icon" href="/c.svg" />
+ <link rel="icon" href="/svg/c.svg" />
</Head>
<Layout>
<div className="min-h-screen z-20 flex w-screen justify-center items-center">
diff --git a/pages/en/index.js b/pages/en/index.js
index 73b4e94..5577fc4 100644
--- a/pages/en/index.js
+++ b/pages/en/index.js
@@ -1,5 +1,5 @@
import { aniListData } from "../../lib/anilist/AniList";
-import React, { useState, useEffect } from "react";
+import { useState, useEffect, Fragment } from "react";
import Head from "next/head";
import Link from "next/link";
import Footer from "../../components/footer";
@@ -8,97 +8,108 @@ import Content from "../../components/home/content";
import { motion } from "framer-motion";
-import { signOut } from "next-auth/react";
-import { getServerSession } from "next-auth/next";
-import { authOptions } from "../api/auth/[...nextauth]";
-import SearchBar from "../../components/searchBar";
+import { signOut, useSession } from "next-auth/react";
import Genres from "../../components/home/genres";
import Schedule from "../../components/home/schedule";
import getUpcomingAnime from "../../lib/anilist/getUpcomingAnime";
-import { useCountdown } from "../../utils/useCountdownSeconds";
import Navigasi from "../../components/home/staticNav";
-import MobileNav from "../../components/home/mobileNav";
-import axios from "axios";
-import { createUser } from "../../prisma/user";
-import { checkAdBlock } from "adblock-checker";
-import { ToastContainer, toast } from "react-toastify";
-import { useAniList } from "../../lib/anilist/useAnilist";
+import { ToastContainer } from "react-toastify";
+import getMedia from "../../lib/anilist/getMedia";
+// import UserRecommendation from "../../components/home/recommendation";
+import MobileNav from "../../components/shared/MobileNav";
+import { getGreetings } from "../../utils/getGreetings";
+import redis from "../../lib/redis";
-export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
+export async function getServerSideProps() {
+ let cachedData;
- try {
- if (session) {
- await createUser(session.user.name);
- }
- } catch (error) {
- console.error(error);
+ if (redis) {
+ cachedData = await redis.get("index_server");
}
- const trendingDetail = await aniListData({
- sort: "TRENDING_DESC",
- page: 1,
- });
- const popularDetail = await aniListData({
- sort: "POPULARITY_DESC",
- page: 1,
- });
- const genreDetail = await aniListData({ sort: "TYPE", page: 1 });
-
- const upComing = await getUpcomingAnime();
-
- return {
- props: {
- genre: genreDetail.props,
- detail: trendingDetail.props,
- populars: popularDetail.props,
- sessions: session,
- upComing,
- },
- };
+ if (cachedData) {
+ const { genre, detail, populars } = JSON.parse(cachedData);
+ const upComing = await getUpcomingAnime();
+ return {
+ props: {
+ genre,
+ detail,
+ populars,
+ upComing,
+ },
+ };
+ } else {
+ const trendingDetail = await aniListData({
+ sort: "TRENDING_DESC",
+ page: 1,
+ });
+ const popularDetail = await aniListData({
+ sort: "POPULARITY_DESC",
+ page: 1,
+ });
+ const genreDetail = await aniListData({ sort: "TYPE", page: 1 });
+
+ if (redis) {
+ await redis.set(
+ "index_server",
+ JSON.stringify({
+ genre: genreDetail.props,
+ detail: trendingDetail.props,
+ populars: popularDetail.props,
+ }), // set cache for 2 hours
+ "EX",
+ 60 * 60 * 2
+ );
+ }
+
+ const upComing = await getUpcomingAnime();
+
+ return {
+ props: {
+ genre: genreDetail.props,
+ detail: trendingDetail.props,
+ populars: popularDetail.props,
+ upComing,
+ },
+ };
+ }
}
-export default function Home({ detail, populars, sessions, upComing }) {
- const { media: current } = useAniList(sessions, { stats: "CURRENT" });
- const { media: plan } = useAniList(sessions, { stats: "PLANNING" });
- const { media: release } = useAniList(sessions);
+export default function Home({ detail, populars, upComing }) {
+ const { data: sessions } = useSession();
+ const { media: current } = getMedia(sessions, { stats: "CURRENT" });
+ const { media: plan } = getMedia(sessions, { stats: "PLANNING" });
+ const { media: release, recommendations } = getMedia(sessions);
const [schedules, setSchedules] = useState(null);
-
const [anime, setAnime] = useState([]);
+ const [recentAdded, setRecentAdded] = useState([]);
+
+ async function getRecent() {
+ const data = await fetch(`/api/v2/etc/recent/1`).then((res) => res.json());
+
+ setRecentAdded(data?.results);
+ }
+
useEffect(() => {
- async function adBlock() {
- const ad = await checkAdBlock();
- if (ad) {
- toast.dark(
- "Please disable your adblock for better experience, we don't have any ads on our site.",
- {
- position: "top-center",
- autoClose: false,
- hideProgressBar: true,
- closeOnClick: true,
- pauseOnHover: true,
- draggable: true,
- theme: "dark",
- }
- );
+ if (sessions?.user?.version) {
+ if (sessions.user.version !== "1.0.1") {
+ signOut("AniListProvider");
}
}
- adBlock();
+ }, [sessions?.user?.version]);
+
+ useEffect(() => {
+ getRecent();
}, []);
const update = () => {
setAnime((prevAnime) => prevAnime.slice(1));
};
- const [days, hours, minutes, seconds] = useCountdown(
- anime[0]?.nextAiringEpisode?.airingAt * 1000 || Date.now(),
- update
- );
-
useEffect(() => {
if (upComing && upComing.length > 0) {
setAnime(upComing);
@@ -107,7 +118,7 @@ export default function Home({ detail, populars, sessions, upComing }) {
useEffect(() => {
const getSchedule = async () => {
- const res = await fetch(`/api/anify/schedule`);
+ const res = await fetch(`/api/v2/etc/schedule`);
const data = await res.json();
if (!res.ok) {
@@ -146,7 +157,6 @@ export default function Home({ detail, populars, sessions, upComing }) {
const [list, setList] = useState(null);
const [planned, setPlanned] = useState(null);
- const [greeting, setGreeting] = useState("");
const [user, setUser] = useState(null);
const [removed, setRemoved] = useState();
@@ -157,6 +167,21 @@ export default function Home({ detail, populars, sessions, upComing }) {
useEffect(() => {
async function userData() {
+ try {
+ if (sessions?.user?.name) {
+ await fetch(`/api/user/profile`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: sessions.user.name,
+ }),
+ });
+ }
+ } catch (error) {
+ console.log(error);
+ }
let data;
try {
if (sessions?.user?.name) {
@@ -194,10 +219,33 @@ export default function Home({ detail, populars, sessions, upComing }) {
const newFirst = arr?.sort((a, b) => {
return new Date(b?.createdAt) - new Date(a?.createdAt);
});
- setUser(newFirst);
+
+ const uniqueTitles = new Set();
+
+ // Filter out duplicates and store unique entries
+ const filteredData = newFirst.filter((entry) => {
+ if (uniqueTitles.has(entry.aniTitle)) {
+ return false;
+ }
+ uniqueTitles.add(entry.aniTitle);
+ return true;
+ });
+
+ setUser(filteredData);
}
} else {
- setUser(data?.WatchListEpisode);
+ // Create a Set to store unique aniTitles
+ const uniqueTitles = new Set();
+
+ // Filter out duplicates and store unique entries
+ const filteredData = data?.WatchListEpisode.filter((entry) => {
+ if (uniqueTitles.has(entry.aniTitle)) {
+ return false;
+ }
+ uniqueTitles.add(entry.aniTitle);
+ return true;
+ });
+ setUser(filteredData);
}
// const data = await res.json();
}
@@ -205,21 +253,6 @@ export default function Home({ detail, populars, sessions, upComing }) {
}, [sessions?.user?.name, removed]);
useEffect(() => {
- const time = new Date().getHours();
- let greeting = "";
-
- if (time >= 5 && time < 12) {
- greeting = "Good morning";
- } else if (time >= 12 && time < 18) {
- greeting = "Good afternoon";
- } else if (time >= 18 && time < 22) {
- greeting = "Good evening";
- } else if (time >= 22 || time < 5) {
- greeting = "Good night";
- }
-
- setGreeting(greeting);
-
async function userData() {
if (!sessions?.user?.name) return;
@@ -234,45 +267,62 @@ export default function Home({ detail, populars, sessions, upComing }) {
.filter((media) => media);
if (list) {
- setList(list.reverse());
+ setList(list);
}
if (planned) {
- setPlanned(planned.reverse());
+ setPlanned(planned);
}
}
userData();
}, [sessions?.user?.name, current, plan]);
return (
- <>
+ <Fragment>
<Head>
<title>Moopa</title>
<meta charSet="UTF-8"></meta>
+ <link rel="icon" href="/svg/c.svg" />
+ <link rel="canonical" href="https://moopa.live/en/" />
<meta name="twitter:card" content="summary_large_image" />
+ {/* Write the best SEO for this homepage */}
<meta
- name="twitter:title"
+ name="description"
+ content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
+ />
+ <meta
+ name="keywords"
+ content="anime, anime streaming, anime streaming website, anime streaming free, anime streaming website free, anime streaming website free english subbed, anime streaming website free english dubbed, anime streaming website free english subbed and dubbed, anime streaming webs
+ ite free english subbed and dubbed download, anime streaming website free english subbed and dubbed"
+ />
+ <meta name="robots" content="index, follow" />
+
+ <meta property="og:type" content="website" />
+ <meta property="og:url" content="https://moopa.live/" />
+ <meta
+ property="og:title"
content="Moopa - Free Anime and Manga Streaming"
/>
<meta
- name="twitter:description"
+ property="og:description"
content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
/>
+ <meta property="og:image" content="/preview.png" />
+ <meta property="og:site_name" content="Moopa" />
+ <meta name="twitter:card" content="summary_large_image" />
<meta
- name="twitter:image"
- content="https://beta.moopa.live/preview.png"
+ name="twitter:title"
+ content="Moopa - Free Anime and Manga Streaming"
/>
<meta
- name="description"
+ name="twitter:description"
content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
/>
- <link rel="icon" href="/c.svg" />
+ <meta name="twitter:image" content="/preview.png" />
</Head>
+ <MobileNav sessions={sessions} hideProfile={true} />
- <MobileNav sessions={sessions} />
-
- <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] ">
+ <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd]">
<Navigasi />
- <SearchBar />
<ToastContainer
pauseOnHover={false}
style={{
@@ -292,15 +342,12 @@ export default function Home({ detail, populars, sessions, upComing }) {
dangerouslySetInnerHTML={{ __html: data?.description }}
/>
- <div className="lg:pt-5">
+ <div className="lg:pt-5 flex">
<Link
href={`/en/anime/${data.id}`}
- legacyBehavior
- className="flex"
+ className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]"
>
- <a className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]">
- START WATCHING
- </a>
+ START WATCHING
</Link>
</div>
</div>
@@ -311,9 +358,9 @@ export default function Home({ detail, populars, sessions, upComing }) {
<Image
draggable={false}
src={data.coverImage?.extraLarge || data.image}
- alt={`alt for ${data.title.english || data.title.romaji}`}
- width={460}
- height={662}
+ alt={`cover ${data.title.english || data.title.romaji}`}
+ width="0"
+ height="0"
priority
className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]"
/>
@@ -321,15 +368,16 @@ export default function Home({ detail, populars, sessions, upComing }) {
</div>
</div>
</div>
- {/* {!sessions && (
- <h1 className="font-bold font-karla mx-5 text-[32px] mt-2 lg:mx-24 xl:mx-36">
- {greeting}!
- </h1>
- )} */}
+
{sessions && (
<div className="flex items-center justify-center lg:bg-none mt-4 lg:mt-0 w-screen">
<div className="lg:w-[85%] w-screen px-5 lg:px-0 lg:text-4xl flex items-center gap-3 text-2xl font-bold font-karla">
- {greeting},<h1 className="lg:hidden">{sessions?.user.name}</h1>
+ {getGreetings() && (
+ <>
+ {getGreetings()},
+ <h1 className="lg:hidden">{sessions?.user.name}</h1>
+ </>
+ )}
<button
onClick={() => signOut()}
className="hidden text-center relative lg:flex justify-center group"
@@ -343,7 +391,7 @@ export default function Home({ detail, populars, sessions, upComing }) {
</div>
)}
- <div className="lg:mt-16 mt-5 flex flex-col items-center">
+ <div className="lg:mt-16 mt-5 flex flex-col gap-5 items-center">
<motion.div
className="w-screen flex-none lg:w-[87%]"
initial={{ opacity: 0 }}
@@ -351,7 +399,7 @@ export default function Home({ detail, populars, sessions, upComing }) {
transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop
>
{user?.length > 0 && (
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="recentlyWatched"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
@@ -365,11 +413,11 @@ export default function Home({ detail, populars, sessions, upComing }) {
userName={sessions?.user?.name}
setRemoved={setRemoved}
/>
- </motion.div>
+ </motion.section>
)}
{sessions && releaseData?.length > 0 && (
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="onGoing"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
@@ -383,11 +431,11 @@ export default function Home({ detail, populars, sessions, upComing }) {
og={prog}
userName={sessions?.user?.name}
/>
- </motion.div>
+ </motion.section>
)}
{sessions && list?.length > 0 && (
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="listAnime"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
@@ -401,12 +449,27 @@ export default function Home({ detail, populars, sessions, upComing }) {
og={prog}
userName={sessions?.user?.name}
/>
- </motion.div>
+ </motion.section>
)}
+ {/* {recommendations.length > 0 && (
+ <div className="space-y-5 mb-10">
+ <div className="px-5">
+ <p className="text-sm lg:text-base">
+ Based on Your List
+ <br />
+ <span className="font-karla text-[20px] lg:text-3xl font-bold">
+ Recommendations
+ </span>
+ </p>
+ </div>
+ <UserRecommendation data={recommendations} />
+ </div>
+ )} */}
+
{/* SECTION 2 */}
{sessions && planned?.length > 0 && (
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="plannedAnime"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
@@ -419,12 +482,36 @@ export default function Home({ detail, populars, sessions, upComing }) {
data={planned}
userName={sessions?.user?.name}
/>
- </motion.div>
+ </motion.section>
)}
+ </motion.div>
+ <motion.div
+ className="w-screen flex-none lg:w-[87%]"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop
+ >
{/* SECTION 3 */}
+ {recentAdded.length > 0 && (
+ <motion.section // Add motion.div to each child component
+ key="recentAdded"
+ initial={{ y: 20, opacity: 0 }}
+ transition={{ duration: 0.5 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="recentAdded"
+ section="New Episodes"
+ data={recentAdded}
+ />
+ </motion.section>
+ )}
+
+ {/* SECTION 4 */}
{detail && (
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="trendingAnime"
initial={{ y: 20, opacity: 0 }}
transition={{ duration: 0.5 }}
@@ -436,12 +523,12 @@ export default function Home({ detail, populars, sessions, upComing }) {
section="Trending Now"
data={detail.data}
/>
- </motion.div>
+ </motion.section>
)}
{/* Schedule */}
{anime.length > 0 && (
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="schedule"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
@@ -450,20 +537,16 @@ export default function Home({ detail, populars, sessions, upComing }) {
>
<Schedule
data={anime[0]}
- time={{
- days: days || 0,
- hours: hours || 0,
- minutes: minutes || 0,
- seconds: seconds || 0,
- }}
+ anime={anime}
+ update={update}
scheduleData={schedules}
/>
- </motion.div>
+ </motion.section>
)}
- {/* SECTION 4 */}
+ {/* SECTION 5 */}
{popular && (
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="popularAnime"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
@@ -475,10 +558,10 @@ export default function Home({ detail, populars, sessions, upComing }) {
section="Popular Anime"
data={popular}
/>
- </motion.div>
+ </motion.section>
)}
- <motion.div // Add motion.div to each child component
+ <motion.section // Add motion.div to each child component
key="Genres"
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
@@ -486,11 +569,11 @@ export default function Home({ detail, populars, sessions, upComing }) {
viewport={{ once: true }}
>
<Genres />
- </motion.div>
+ </motion.section>
</motion.div>
</div>
</div>
<Footer />
- </>
+ </Fragment>
);
}
diff --git a/pages/en/manga/[id].js b/pages/en/manga/[id].js
index bb3cbc2..e928bd4 100644
--- a/pages/en/manga/[id].js
+++ b/pages/en/manga/[id].js
@@ -1,4 +1,3 @@
-import dotenv from "dotenv";
import ChapterSelector from "../../../components/manga/chapters";
import HamburgerMenu from "../../../components/manga/mobile/hamburgerMenu";
import Navbar from "../../../components/navbar";
@@ -11,7 +10,7 @@ import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
import getAnifyInfo from "../../../lib/anify/info";
-export default function Manga({ info, userManga, chapters }) {
+export default function Manga({ info, userManga }) {
const [domainUrl, setDomainUrl] = useState("");
const [firstEp, setFirstEp] = useState();
const chaptersData = info.chapters.data;
@@ -45,6 +44,12 @@ export default function Manga({ info, userManga, chapters }) {
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 />
@@ -78,9 +83,8 @@ export default function Manga({ info, userManga, chapters }) {
}
export async function getServerSideProps(context) {
- dotenv.config();
-
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;
@@ -93,55 +97,37 @@ export async function getServerSideProps(context) {
method: "POST",
headers: {
"Content-Type": "application/json",
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
body: JSON.stringify({
query: `
- query ($username: String, $status: MediaListStatus) {
- MediaListCollection(userName: $username, type: MANGA, status: $status, sort: SCORE_DESC) {
- user {
- id
- name
- }
- lists {
- status
- name
- entries {
- id
- mediaId
- status
- progress
- score
- progressVolumes
- media {
- id
- status
- title {
- english
- romaji
+ query ($id: Int) {
+ Media (id: $id) {
+ mediaListEntry {
+ status
+ progress
+ progressVolumes
+ status
+ }
+ id
+ idMal
+ title {
+ romaji
+ english
+ native
+ }
+ }
}
- episodes
- coverImage {
- large
- }
- }
- }
- }
- }
- }
`,
variables: {
- username: session?.user?.name,
+ id: parseInt(id),
},
}),
});
const data = await response.json();
- const user = data?.data?.MediaListCollection;
- const userListsCurrent = user?.lists.find((X) => X.status === "CURRENT");
- const matched = userListsCurrent?.entries.find(
- (x) => x.mediaId === parseInt(id)
- );
- if (matched) {
- userManga = matched;
+ const user = data?.data?.Media?.mediaListEntry;
+ if (user) {
+ userManga = user;
}
}
diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js
index 301b646..faebcd6 100644
--- a/pages/en/manga/read/[...params].js
+++ b/pages/en/manga/read/[...params].js
@@ -1,4 +1,3 @@
-import dotenv from "dotenv";
import { useEffect, useRef, useState } from "react";
import { LeftBar } from "../../../../components/manga/leftBar";
import { useRouter } from "next/router";
@@ -115,6 +114,12 @@ export default function Read({ data, currentId, sessions }) {
}`
: "Getting Info..."}
</title>
+ <meta
+ name="title"
+ data-title-romaji={info?.title?.romaji}
+ data-title-english={info?.title?.english}
+ data-title-native={info?.title?.native}
+ />
<meta id="CoverImage" data-manga-cover={info?.coverImage} />
</Head>
<div className="w-screen flex justify-evenly relative">
@@ -226,8 +231,6 @@ export default function Read({ data, currentId, sessions }) {
}
export async function getServerSideProps(context) {
- dotenv.config();
-
const cookies = nookies.get(context);
const key = process.env.API_KEY;
diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].js
index b66699b..fc06236 100644
--- a/pages/en/profile/[user].js
+++ b/pages/en/profile/[user].js
@@ -4,11 +4,47 @@ import Navbar from "../../../components/navbar";
import Image from "next/image";
import Link from "next/link";
import Head from "next/head";
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import { getUser } from "../../../prisma/user";
+import { ToastContainer, toast } from "react-toastify";
-export default function MyList({ media, sessions, user, time }) {
+export default function MyList({ media, sessions, user, time, userSettings }) {
const [listFilter, setListFilter] = useState("all");
const [visible, setVisible] = useState(false);
+ const [useCustomList, setUseCustomList] = useState(true);
+
+ useEffect(() => {
+ if (userSettings) {
+ localStorage.setItem("customList", userSettings.CustomLists);
+ setUseCustomList(userSettings.CustomLists);
+ }
+ }, [userSettings]);
+
+ // Function to handle checkbox state changes
+ const handleCheckboxChange = async () => {
+ setUseCustomList(!useCustomList); // Toggle the checkbox state
+ try {
+ const res = await fetch("/api/user/profile", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: sessions?.user?.name,
+ settings: {
+ CustomLists: !useCustomList,
+ },
+ }),
+ });
+ const data = await res.json();
+ if (data) {
+ toast.success(`Custom List is now ${!useCustomList ? "on" : "off"}`);
+ }
+ localStorage.setItem("customList", !useCustomList);
+ } catch (error) {
+ console.error(error);
+ }
+ };
const filterMedia = (status) => {
if (status === "all") {
@@ -22,6 +58,8 @@ export default function MyList({ media, sessions, user, time }) {
<title>My Lists</title>
</Head>
<Navbar />
+ <ToastContainer pauseOnHover={false} />
+
<div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 relative">
<div className="lg:w-[30%] h-full mt-12 lg:mr-10 grid gap-5 mx-3 lg:mx-0 antialiased">
<div className="flex items-center gap-5">
@@ -51,28 +89,30 @@ export default function MyList({ media, sessions, user, time }) {
Created At :
<UnixTimeConverter unixTime={user.createdAt} />
</div>
- {sessions && user.name === sessions?.user.name ? (
- <Link
- href={"https://anilist.co/settings/"}
- className="flex items-center gap-2 p-1 px-2 ring-[1px] antialiased ring-txt rounded-lg text-xs font-karla hover:bg-txt hover:shadow-lg group"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-4 h-4 group-hover:stroke-black"
+ <div className="flex items-center gap-2">
+ {sessions && user.name === sessions?.user.name ? (
+ <Link
+ href={"https://anilist.co/settings/"}
+ className="flex items-center gap-2 p-1 px-2 ring-[1px] antialiased ring-txt rounded-lg text-xs font-karla hover:bg-txt hover:shadow-lg group"
>
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"
- />
- </svg>
- <span className="group-hover:text-black">Edit Profile</span>
- </Link>
- ) : null}
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth={1.5}
+ stroke="currentColor"
+ className="w-4 h-4 group-hover:stroke-black"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"
+ />
+ </svg>
+ <span className="group-hover:text-black">Edit Profile</span>
+ </Link>
+ ) : null}
+ </div>
</div>
<div className="bg-secondary lg:min-h-[160px] text-xs rounded-md p-4 font-karla">
<div>
@@ -109,6 +149,27 @@ export default function MyList({ media, sessions, user, time }) {
</div>
)}
</div>
+ {sessions && user.name === sessions?.user.name && (
+ <div className="font-karla flex flex-col gap-4">
+ <h1>User Settings</h1>
+ <div className="flex p-2 items-center justify-between">
+ <h2
+ className="text-sm text-white/70"
+ title="Disabling this will stop adding your Anime to 'Watched using Moopa' list."
+ >
+ Custom Lists
+ </h2>
+ <div className="w-5 h-5">
+ <input
+ type="checkbox"
+ checked={useCustomList}
+ onChange={handleCheckboxChange}
+ className="accent-action"
+ />
+ </div>
+ </div>
+ </div>
+ )}
{media.length !== 0 && (
<div className="font-karla grid gap-4">
<div className="flex md:justify-normal justify-between items-center">
@@ -183,7 +244,7 @@ export default function MyList({ media, sessions, user, time }) {
)}
</div>
- <div className="lg:w-[75%] grid gap-10 my-12 lg:pt-16">
+ <div className="lg:w-[75%] grid gap-10 my-5 lg:my-12 lg:pt-16">
{media.length !== 0 ? (
filterMedia(listFilter).map((item, index) => {
return (
@@ -381,6 +442,12 @@ export async function getServerSideProps(context) {
};
}
+ let userData;
+
+ if (session) {
+ userData = await getUser(session.user.name, false);
+ }
+
const prog = get.lists;
function getIndex(status) {
@@ -400,6 +467,7 @@ export async function getServerSideProps(context) {
sessions: session,
user: user,
time: time,
+ userSettings: userData?.setting || null,
},
};
}
diff --git a/pages/en/schedule/index.js b/pages/en/schedule/index.js
new file mode 100644
index 0000000..0a49037
--- /dev/null
+++ b/pages/en/schedule/index.js
@@ -0,0 +1,523 @@
+import Image from "next/image";
+import { useEffect, useRef, useState } from "react";
+import { NewNavbar } from "../../../components/anime/mobile/topSection";
+import Link from "next/link";
+import { CalendarIcon } from "@heroicons/react/24/solid";
+import { ClockIcon } from "@heroicons/react/24/outline";
+import Loading from "../../../components/shared/loading";
+import { timeStamptoAMPM, timeStamptoHour } from "../../../utils/getTimes";
+import {
+ filterFormattedSchedule,
+ filterScheduleByDay,
+ sortScheduleByDay,
+ transformSchedule,
+} from "../../../utils/schedulesUtils";
+
+import { scheduleQuery } from "../../../lib/graphql/query";
+import MobileNav from "../../../components/shared/MobileNav";
+
+import { useSession } from "next-auth/react";
+import redis from "../../../lib/redis";
+import Head from "next/head";
+
+const day = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+];
+
+const isAired = (timestamp) => {
+ const currentTime = new Date().getTime() / 1000;
+ return timestamp <= currentTime;
+};
+
+export async function getServerSideProps() {
+ const now = new Date();
+ // Adjust for Japan timezone (add 9 hours)
+ const nowJapan = new Date(now.getTime() + 9 * 60 * 60 * 1000);
+
+ // Calculate the time until midnight of the next day in Japan timezone
+ const midnightTomorrowJapan = new Date(
+ nowJapan.getFullYear(),
+ nowJapan.getMonth(),
+ nowJapan.getDate() + 1,
+ 0,
+ 0,
+ 0,
+ 0
+ );
+ const timeUntilMidnightJapan = Math.round(
+ (midnightTomorrowJapan - nowJapan) / 1000
+ );
+
+ let cachedData;
+
+ // Check if the data is already in Redis
+ if (redis) {
+ cachedData = await redis.get("new_schedule");
+ }
+
+ if (cachedData) {
+ const scheduleByDay = JSON.parse(cachedData);
+
+ // const today = now.getDay();
+ // const todaySchedule = day[today];
+
+ return {
+ props: {
+ schedule: scheduleByDay,
+ // today: todaySchedule,
+ },
+ };
+ } else {
+ now.setHours(0, 0, 0, 0); // Set the time to 00:00:00.000
+ const dayInSeconds = 86400; // Number of seconds in a day
+ const yesterdayStart = Math.floor(now.getTime() / 1000) - dayInSeconds;
+ // Calculate weekStart from yesterday's 00:00:00
+ const weekStart = yesterdayStart;
+ const weekEnd = weekStart + 604800;
+
+ // const today = now.getDay();
+ // const todaySchedule = day[today];
+
+ // const now = new Date();
+ // const currentDayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
+
+ // // Calculate the number of seconds until the current Saturday at 00:00:00
+ // const secondsUntilSaturday = (6 - currentDayOfWeek) * 24 * 60 * 60;
+
+ // // Calculate weekStart as the current time minus secondsUntilSaturday
+ // const weekStart = Math.floor(now.getTime() / 1000) - secondsUntilSaturday;
+
+ // // Calculate weekEnd as one week from weekStart
+ // const weekEnd = weekStart + 604800; // One week in seconds
+
+ let page = 1;
+ const airingSchedules = [];
+
+ while (true) {
+ const res = await fetch("https://graphql.anilist.co", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({
+ query: scheduleQuery,
+ variables: {
+ weekStart,
+ weekEnd,
+ page,
+ },
+ }),
+ });
+
+ const json = await res.json();
+ const schedules = json.data.Page.airingSchedules;
+
+ if (schedules.length === 0) {
+ break; // No more data to fetch
+ }
+
+ airingSchedules.push(...schedules);
+ page++;
+ }
+
+ const timestampToDay = (timestamp) => {
+ const options = { weekday: "long" };
+ return new Date(timestamp * 1000).toLocaleDateString(undefined, options);
+ };
+
+ const scheduleByDay = {};
+ airingSchedules.forEach((schedule) => {
+ const day = timestampToDay(schedule.airingAt);
+ if (!scheduleByDay[day]) {
+ scheduleByDay[day] = [];
+ }
+ scheduleByDay[day].push(schedule);
+ });
+
+ if (redis) {
+ await redis.set(
+ "new_schedule",
+ JSON.stringify(scheduleByDay),
+ "EX",
+ timeUntilMidnightJapan
+ );
+ }
+
+ return {
+ props: {
+ schedule: scheduleByDay,
+ // today: todaySchedule,
+ },
+ };
+ }
+ // setSchedule(scheduleByDay);
+}
+
+export default function Schedule({ schedule }) {
+ const { data: session } = useSession();
+
+ // const [schedule, setSchedule] = useState({});
+ const [filterDay, setFilterDay] = useState("All");
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setLoading(true);
+ async function setDay() {
+ const now = new Date();
+ const today = day[now.getDay()];
+ setFilterDay(today);
+ setLoading(false);
+ }
+ setDay();
+ }, []);
+ // Sort the schedule object by day, placing today's schedule first
+ const sortedSchedule = sortScheduleByDay(schedule);
+ const formattedSchedule = transformSchedule(schedule);
+
+ // State to keep track of the next airing anime
+ const [nextAiringAnime, setNextAiringAnime] = useState(null);
+ // const [nextAiringBanner, setNextAiringBanner] = useState(null);
+
+ // State to keep track of the currently airing anime
+ const [currentlyAiringAnime, setCurrentlyAiringAnime] = useState(null);
+
+ const [layout, setLayout] = useState(1);
+
+ // Effect to update the next and currently airing anime
+ useEffect(() => {
+ const now = new Date().getTime() / 1000; // Current time in seconds
+ let nextAiring = null;
+ let currentlyAiring = null;
+
+ for (const [, schedules] of Object.entries(sortedSchedule)) {
+ for (const s of schedules) {
+ if (s.airingAt > now) {
+ if (!nextAiring) {
+ nextAiring = s.id;
+ // setNextAiringBanner(s.media.bannerImage);
+ }
+ } else if (s.airingAt + 1440 > now) {
+ currentlyAiring = s.id;
+ }
+ }
+ if (nextAiring && currentlyAiring) break;
+ }
+
+ setNextAiringAnime(nextAiring);
+ setCurrentlyAiringAnime(currentlyAiring);
+ }, [sortedSchedule]);
+
+ const scrollContainerRef = useRef(null);
+
+ useEffect(() => {
+ // Scroll to center the active button when it changes
+ if (scrollContainerRef.current) {
+ const activeButton =
+ scrollContainerRef.current.querySelector(".text-action");
+ if (activeButton) {
+ const containerWidth = scrollContainerRef.current.clientWidth;
+ const buttonLeft = activeButton.offsetLeft;
+ const buttonWidth = activeButton.clientWidth;
+ const scrollLeft = buttonLeft - containerWidth / 2 + buttonWidth / 2;
+ scrollContainerRef.current.scrollLeft = scrollLeft;
+ }
+ }
+ }, [filterDay]);
+
+ return (
+ <>
+ <Head>
+ <title>Moopa - Schedule</title>
+ {/* write a meta with good seo for this page */}
+ <meta
+ name="description"
+ content="Moopa is a website where you can find all the information about your favorite anime and manga."
+ />
+ <meta
+ name="keywords"
+ content="anime, manga, moopa, anilist, information, schedule, airing, next, currently, airing, anime, manga"
+ />
+ <meta name="robots" content="index, follow" />
+ <meta name="author" content="Moopa Team" />
+ <meta name="url" content="https://moopa.live/en/schedule" />
+ <meta name="og:title" property="og:title" content="Moopa - Schedule" />
+ <meta
+ name="og:description"
+ property="og:description"
+ content="Moopa is a website where you can find all the information about your favorite anime and manga."
+ />
+ <meta property="og:type" content="website" />
+ <meta property="og:url" content="https://moopa.live/en/schedule" />
+ <meta
+ property="og:image"
+ content="https://beta.moopa.live/preview.png"
+ />
+ <meta
+ property="og:image:alt"
+ content="Moopa is a website where you can find all the information about your favorite anime and manga."
+ />
+ <meta property="og:locale" content="en_US" />
+ <meta property="og:site_name" content="Moopa" />
+ <meta name="twitter:card" content="summary_large_image" />
+ {/* <meta name="twitter:site" content="@moopa_anime" />
+ <meta name="twitter:creator" content="@moopa_anime" /> */}
+ <meta
+ name="twitter:image"
+ content="https://beta.moopa.live/preview.png"
+ />
+ <meta
+ name="twitter:image:alt"
+ content="Moopa is a website where you can find all the information about your favorite anime and manga."
+ />
+ <meta name="twitter:title" content="Moopa - Schedule" />
+ <meta
+ name="twitter:description"
+ content="Moopa is a website where you can find all the information about your favorite anime and manga."
+ />
+ <link rel="canonical" href="https://moopa.live/en/schedule" />
+ </Head>
+ <MobileNav sessions={session} hideProfile={true} />
+ <div className="w-screen">
+ <NewNavbar scrollP={10} session={session} toTop={true} />
+ <span className="absolute z-20 top-0 left-0 w-screen h-[190px] lg:h-[250px] bg-secondary overflow-hidden">
+ <div className="absolute top-40 lg:top-36 w-full h-full bg-primary rounded-t-3xl xl:rounded-t-[50px]" />
+ </span>
+ <div className="flex flex-col mx-auto my-10 w-full mt-16 lg:mt-24 max-w-screen-2xl gap-5 md:gap-10 z-30">
+ <div className="flex flex-col lg:flex-row gap-2 justify-between z-20 px-3">
+ <ul
+ ref={scrollContainerRef}
+ className="flex overflow-x-scroll cust-scroll items-center gap-5 font-karla text-2xl font-semibold"
+ >
+ <button
+ type="button"
+ onClick={() => setFilterDay("All")}
+ className={`hover:text-action transition-all duration-200 ease-out cursor-pointer ${
+ filterDay === "All" ? "text-action" : ""
+ }`}
+ >
+ All
+ </button>
+ {day.map((i) => (
+ <button
+ key={i}
+ // id={`same_${i}`}
+ type="button"
+ onClick={() => {
+ setLoading(true);
+ setFilterDay(i);
+ setLoading(false);
+ }}
+ className={`py-2 lg:py-0 outline-none hover:text-action transition-all duration-200 ease-out cursor-pointer ${
+ filterDay === i ? "text-action" : ""
+ }`}
+ >
+ {i}
+ </button>
+ ))}
+ </ul>
+ <div className="flex gap-3">
+ <ClockIcon
+ className={`w-6 h-6 cursor-pointer ${
+ layout === 1 ? "text-action" : "text-white"
+ }`}
+ onClick={() => setLayout(1)}
+ />
+ <CalendarIcon
+ className={`w-6 h-6 cursor-pointer ${
+ layout === 2 ? "text-action" : "text-white"
+ }`}
+ onClick={() => setLayout(2)}
+ />
+ </div>
+ </div>
+
+ {layout === 1 ? (
+ !loading ? (
+ Object.entries(
+ filterFormattedSchedule(formattedSchedule, filterDay)
+ ).map(([day, timeSlots], index) => (
+ <div
+ key={`section_${day}`}
+ // id={`same_${day}`}
+ className="flex flex-col gap-5 z-50 px-3"
+ >
+ <h2 className="font-bold font-outfit text-white text-2xl z-[250]">
+ {day}
+ </h2>
+ {Object.entries(timeSlots).map(([time, animeList]) => (
+ <div
+ key={time}
+ // id={`same_${time}`}
+ className="relative space-y-2"
+ >
+ <div className="ml-4 flex items-center gap-2">
+ <h3 className="text-lg text-gray-200 font-semibold">
+ {timeStamptoAMPM(time)}
+ </h3>
+ {/* {!isAired(time) && <p>Airing Next</p>} */}
+ <p
+ className={`absolute left-0 h-1.5 w-1.5 rounded-full ${
+ isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime
+ }`}
+ ></p>
+ </div>
+ <div className="w-full grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5 md:gap-7 grid-flow-row relative">
+ {animeList.map((s, index) => {
+ const m = s.media;
+ return (
+ <>
+ <Link
+ key={m.id}
+ // id={`same_${m.id}`}
+ href={`/en/${m.type.toLowerCase()}/${m.id}`}
+ className={`flex bg-secondary rounded group cursor-pointer ml-4 z-50`}
+ >
+ <Image
+ src={m.coverImage.extraLarge}
+ alt="image"
+ width="0"
+ height="0"
+ className="w-[50px] h-[65px] object-cover shrink-0"
+ />
+ <div className="flex flex-col justify-center font-karla p-2">
+ <h1 className="font-semibold line-clamp-1 text-sm group-hover:text-action transition-all duration-200 ease-out">
+ {m.title.romaji}
+ </h1>
+ <p className="text-gray-400 group-hover:text-action/80 transition-all duration-200 ease-out">
+ Ep {s.episode} {timeStamptoHour(s.airingAt)}
+ </p>
+ </div>
+ </Link>
+ <p
+ key={`p_${s.id}_${index}`}
+ className={`absolute translate-x-full top-1/2 -translate-y-1/2 h-full w-0.5 ${
+ isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime
+ }`}
+ ></p>
+ </>
+ );
+ })}
+ </div>
+ </div>
+ ))}
+ </div>
+ ))
+ ) : (
+ <div className="z-[500] pt-10 lg:pt-0">
+ <Loading />
+ </div>
+ )
+ ) : !loading ? (
+ Object.entries(filterScheduleByDay(sortedSchedule, filterDay)).map(
+ ([day, schedules]) => (
+ <div
+ key={`section2_${day}`}
+ // id={`same_${day}`}
+ className="flex flex-col gap-5 px-3 z-50"
+ >
+ <h2
+ // id={day}
+ className="font-bold font-outfit text-white text-2xl"
+ >
+ {day}
+ </h2>
+ <div className="w-full grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-5 md:gap-7 grid-flow-row">
+ {schedules.map((s) => {
+ const m = s.media;
+
+ return (
+ <Link
+ key={m.id}
+ // id={`same_${m.id}`}
+ href={`/en/${m.type?.toLowerCase()}/${m.id}`}
+ className={`flex bg-secondary rounded group cursor-pointer relative ${
+ s.id === nextAiringAnime
+ ? "ring-1 ring-sky-500"
+ : "" // Add a class for next airing anime
+ } ${
+ s.id === currentlyAiringAnime
+ ? "ring-1 ring-action"
+ : "" // Add a class for currently airing anime
+ }`}
+ >
+ {/* <p className={``}> */}
+ <p className="absolute flex top-0 right-0 -mt-1 -mr-1 justify-center items-center">
+ <span
+ className={`relative flex justify-center h-3 w-3 tooltip-container ${
+ s.id === nextAiringAnime ? "" : "hidden" // Add a className for next airing anime
+ }`}
+ >
+ {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span> */}
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500"></span>
+ <span className="tooltip">Next Airing</span>
+ </span>
+ </p>
+ <p className="absolute flex top-0 right-0 -mt-1 -mr-1 justify-center items-center">
+ <span
+ className={`relative flex justify-center h-3 w-3 tooltip-container ${
+ s.id === currentlyAiringAnime ? "" : "hidden" // Add a className for currently airing anime
+ }`}
+ >
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-orange-400 opacity-75"></span>
+ <span className="relative inline-flex rounded-full h-3 w-3 bg-orange-500"></span>
+ <span className="tooltip">Airing Now</span>
+ </span>
+ </p>
+ {/* <span
+ className={`${
+ s.id === nextAiringAnime
+ ? "bg-orange-700 text-sm px-3 py-1 rounded-full font-bold text-white"
+ : ""
+ } mx-auto`}
+ >
+ Airing Next
+ </span> */}
+ {/* </p> */}
+ {/* {s.media?.bannerImage && (
+ <Image
+ src={s.media?.bannerImage}
+ alt="banner"
+ width="0"
+ height="0"
+ className="absolute pointer-events-none top-0 opacity-0 group-hover:opacity-10 transition-all duration-500 ease-linear -z-10 left-0 rounded-l w-full h-[250px] object-cover"
+ />
+ )} */}
+ <Image
+ src={m.coverImage.extraLarge}
+ alt="image"
+ width="0"
+ height="0"
+ className="w-[50px] h-[65px] object-cover shrink-0"
+ />
+ <div className="flex flex-col justify-center font-karla p-2">
+ <h1 className="font-semibold line-clamp-1 text-sm group-hover:text-action transition-all duration-200 ease-out">
+ {m.title.romaji}
+ </h1>
+ <p className="text-gray-400 group-hover:text-action/80 transition-all duration-200 ease-out">
+ Ep {s.episode} {timeStamptoHour(s.airingAt)}
+ </p>
+ </div>
+ </Link>
+ );
+ })}
+ </div>
+ </div>
+ )
+ )
+ ) : (
+ <div className="z-[500] pt-10 lg:pt-0">
+ <Loading />
+ </div>
+ )}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].js
new file mode 100644
index 0000000..2ec7681
--- /dev/null
+++ b/pages/en/search/[...param].js
@@ -0,0 +1,433 @@
+import { useEffect, useRef, useState } from "react";
+import { AnimatePresence, motion as m } from "framer-motion";
+import Skeleton from "react-loading-skeleton";
+import { useRouter } from "next/router";
+import Link from "next/link";
+import Navbar from "../../../components/navbar";
+import Head from "next/head";
+import Footer from "../../../components/footer";
+
+import Image from "next/image";
+import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch";
+import MultiSelector from "../../../components/search/dropdown/multiSelector";
+import SingleSelector from "../../../components/search/dropdown/singleSelector";
+import {
+ animeFormatOptions,
+ formatOptions,
+ genreOptions,
+ mangaFormatOptions,
+ mediaType,
+ seasonOptions,
+ tagsOption,
+ yearOptions,
+} from "../../../components/search/selection";
+import InputSelect from "../../../components/search/dropdown/inputSelect";
+import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid";
+import useDebounce from "../../../lib/hooks/useDebounce";
+// import { NewNavbar } from "../../../components/anime/mobile/topSection";
+// import { useSession } from "next-auth/react";
+
+export async function getServerSideProps(context) {
+ const { param } = context.query;
+
+ const { search, format, genres, season, year } = context.query;
+
+ let getFormat;
+ let getSeason;
+ let getYear;
+ let getGenres = [];
+
+ if (genres) {
+ const gr = genreOptions.find(
+ (i) => i.value.toLowerCase() === genres.toLowerCase()
+ );
+ getGenres.push(gr);
+ }
+
+ if (season) {
+ getSeason = seasonOptions.find(
+ (i) => i.value.toLowerCase() === season.toLowerCase()
+ );
+ if (!year) {
+ const now = new Date().getFullYear();
+ getYear = yearOptions.find((i) => i.value === now.toString());
+ } else {
+ getYear = yearOptions.find((i) => i.value === year);
+ }
+ }
+
+ if (format) {
+ getFormat = formatOptions.find(
+ (i) => i.value.toLowerCase() === format.toLowerCase()
+ );
+ }
+
+ if (!param && param.length !== 1) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const typeIndex = param[0] === "anime" ? 0 : 1;
+
+ return {
+ props: {
+ index: typeIndex,
+ query: search || null,
+ formats: getFormat || null,
+ seasons: getSeason || null,
+ years: getYear || null,
+ genres: getGenres || null,
+ },
+ };
+}
+
+export default function Card({
+ index,
+ query,
+ genres,
+ formats,
+ seasons,
+ years,
+}) {
+ const inputRef = useRef(null);
+ const router = useRouter();
+ // const { data: session } = useSession();
+
+ const [data, setData] = useState();
+ const [loading, setLoading] = useState(true);
+
+ const [search, setQuery] = useState(query);
+ const debounceSearch = useDebounce(search, 500);
+
+ const [type, setSelectedType] = useState(mediaType[index]);
+ const [year, setYear] = useState(years);
+ const [season, setSeason] = useState(seasons);
+ const [sort, setSelectedSort] = useState();
+ const [genre, setGenre] = useState(genres);
+ const [format, setFormat] = useState(formats);
+
+ const [isVisible, setIsVisible] = useState(false);
+
+ const [page, setPage] = useState(1);
+ const [nextPage, setNextPage] = useState(true);
+
+ async function advance() {
+ setLoading(true);
+ const data = await aniAdvanceSearch({
+ search: debounceSearch,
+ type: type?.value,
+ genres: genre,
+ page: page,
+ sort: sort?.value,
+ format: format?.value,
+ season: season?.value,
+ seasonYear: year?.value,
+ });
+ if (data?.media?.length === 0) {
+ setNextPage(false);
+ } else if (data !== null && page > 1) {
+ setData((prevData) => {
+ return [...(prevData ?? []), ...data?.media];
+ });
+ setNextPage(data?.pageInfo.hasNextPage);
+ } else {
+ setData(data?.media);
+ }
+ setNextPage(data?.pageInfo.hasNextPage);
+ setLoading(false);
+ }
+
+ useEffect(() => {
+ setData(null);
+ setPage(1);
+ setNextPage(true);
+ advance();
+ }, [
+ debounceSearch,
+ type?.value,
+ sort?.value,
+ genre,
+ format?.value,
+ season?.value,
+ year?.value,
+ ]);
+
+ useEffect(() => {
+ advance();
+ }, [page]);
+
+ useEffect(() => {
+ function handleScroll() {
+ if (page > 10 || !nextPage) {
+ window.removeEventListener("scroll", handleScroll);
+ return;
+ }
+
+ if (
+ window.innerHeight + window.pageYOffset >=
+ document.body.offsetHeight - 3
+ ) {
+ setPage((prevPage) => prevPage + 1);
+ }
+ }
+
+ window.addEventListener("scroll", handleScroll);
+
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, [page, nextPage]);
+
+ const handleKeyDown = async (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ const inputValue = event.target.value;
+ if (inputValue === "") {
+ setQuery(null);
+ } else {
+ setQuery(inputValue);
+ }
+ }
+ };
+
+ function trash() {
+ setQuery();
+ setGenre();
+ setFormat();
+ setSelectedSort();
+ setSeason();
+ setYear();
+ router.push(`/en/search/${mediaType[index]?.value?.toLowerCase()}`);
+ }
+
+ function handleVisible() {
+ setIsVisible(!isVisible);
+ }
+
+ return (
+ <>
+ <Head>
+ <title>Moopa - search</title>
+ <meta name="title" content="Search" />
+ <meta name="description" content="Search your favourites Anime/Manga" />
+ <link rel="icon" href="/svg/c.svg" />
+ </Head>
+ <Navbar />
+ {/* <NewNavbar session={session} /> */}
+ <main className="w-screen min-h-screen z-40">
+ <div className="max-w-screen-xl flex flex-col gap-3 mx-auto">
+ <div className="w-full flex justify-between items-end gap-2 my-3 lg:gap-10 px-5 xl:px-0 relative">
+ <div className="hidden lg:flex items-end w-full gap-5 z-50">
+ <InputSelect
+ inputRef={inputRef}
+ data={mediaType}
+ label="Search"
+ keyDown={handleKeyDown}
+ query={search}
+ setQuery={setQuery}
+ selected={type}
+ setSelected={setSelectedType}
+ />
+ {/* GENRES */}
+ <MultiSelector
+ data={genreOptions}
+ other={tagsOption}
+ selected={genre}
+ setSelected={setGenre}
+ label="Genres"
+ inputRef={inputRef}
+ />
+ {/* SORT */}
+ {/* <SingleSelector
+ data={sortOptions}
+ selected={sort}
+ setSelected={setSelectedSort}
+ label="Sort"
+ /> */}
+ {/* FORMAT */}
+ <SingleSelector
+ data={index === 0 ? animeFormatOptions : mangaFormatOptions}
+ selected={format}
+ setSelected={setFormat}
+ label="Format"
+ />
+ {/* SEASON */}
+ <SingleSelector
+ data={seasonOptions}
+ selected={season}
+ setSelected={setSeason}
+ label="Season"
+ />
+ {/* YEAR */}
+ <SingleSelector
+ data={yearOptions}
+ selected={year}
+ setSelected={setYear}
+ label="Year"
+ />
+ </div>
+ <div className="w-full lg:hidden">
+ <InputSelect
+ inputRef={inputRef}
+ data={mediaType}
+ label="Search"
+ keyDown={handleKeyDown}
+ query={search}
+ setQuery={setQuery}
+ selected={type}
+ setSelected={setSelectedType}
+ />
+ </div>
+
+ <div className="flex gap-2">
+ <div
+ className="lg:hidden py-2 px-2 bg-secondary rounded flex justify-center items-center cursor-pointer hover:bg-opacity-75 transition-all duration-100 group"
+ onClick={handleVisible}
+ >
+ <Cog6ToothIcon className="w-5 h-5" />
+ </div>
+ <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}
+ >
+ <TrashIcon className="w-5 h-5" />
+ </div>
+ </div>
+ </div>
+ {isVisible && (
+ <div className="lg:hidden w-full flex justify-center z-40">
+ <div className="grid grid-cols-2 grid-rows-2 place-items-center w-full px-5 z-30 gap-4">
+ {/* GENRES */}
+ <MultiSelector
+ data={genreOptions}
+ other={tagsOption}
+ selected={genre}
+ setSelected={setGenre}
+ label="Genres"
+ inputRef={inputRef}
+ />
+ {/* SORT */}
+ {/* <SingleSelector
+ data={sortOptions}
+ selected={sort}
+ setSelected={setSelectedSort}
+ label="Sort"
+ /> */}
+ {/* FORMAT */}
+ <SingleSelector
+ data={index === 0 ? animeFormatOptions : mangaFormatOptions}
+ selected={format}
+ setSelected={setFormat}
+ label="Format"
+ />
+ {/* SEASON */}
+ <SingleSelector
+ data={seasonOptions}
+ selected={season}
+ setSelected={setSeason}
+ label="Season"
+ />
+ {/* YEAR */}
+ <SingleSelector
+ data={yearOptions}
+ selected={year}
+ setSelected={setYear}
+ label="Year"
+ />
+ </div>
+ </div>
+ )}
+ {/* <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>
+ )}
+ {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}
+ >
+ <Link
+ href={
+ anime.format === "MANGA" || anime.format === "NOVEL"
+ ? `/en/manga/${anime.id}`
+ : `/en/anime/${anime.id}`
+ }
+ title={anime.title.userPreferred}
+ >
+ <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}
+ />
+ </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>} &#183;{" "}
+ {anime.status || <p>-</p>} &#183;{" "}
+ {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 }}
+ >
+ <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" />
+ <Skeleton width={110} height={30} />
+ </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>
+ </div>
+ {/* </div> */}
+ </div>
+ </main>
+ <Footer />
+ </>
+ );
+}
diff --git a/pages/en/search/[param].js b/pages/en/search/[param].js
deleted file mode 100644
index abd4f04..0000000
--- a/pages/en/search/[param].js
+++ /dev/null
@@ -1,496 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { AnimatePresence, motion as m } from "framer-motion";
-import Skeleton from "react-loading-skeleton";
-import { useRouter } from "next/router";
-import Link from "next/link";
-import Navbar from "../../../components/navbar";
-import Head from "next/head";
-import Footer from "../../../components/footer";
-
-import Image from "next/image";
-import { ChevronDownIcon } from "@heroicons/react/24/outline";
-import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch";
-
-const genre = [
- "Action",
- "Adventure",
- "Comedy",
- "Drama",
- "Ecchi",
- "Fantasy",
- "Horror",
- "Mahou Shoujo",
- "Mecha",
- "Music",
- "Mystery",
- "Psychological",
- "Romance",
- "Sci-Fi",
- "Slice of Life",
- "Sports",
- "Supernatural",
- "Thriller",
-];
-
-const types = ["ANIME", "MANGA"];
-
-const sorts = [
- { name: "Title", value: "TITLE_ROMAJI" },
- { name: "Popularity", value: "POPULARITY_DESC" },
- { name: "Trending", value: "TRENDING_DESC" },
- { name: "Favourites", value: "FAVOURITES_DESC" },
- { name: "Average Score", value: "SCORE_DESC" },
- { name: "Date Added", value: "ID_DESC" },
- { name: "Release Date", value: "START_DATE_DESC" },
-];
-
-export default function Card() {
- const router = useRouter();
-
- const [data, setData] = useState();
- const [loading, setLoading] = useState(true);
-
- let hasil = null;
- let tipe = "ANIME";
- let s = undefined;
- let y = NaN;
- let gr = undefined;
-
- const query = router.query;
- gr = query.genres;
-
- if (query.param !== "anime" && query.param !== "manga") {
- hasil = query.param;
- } else if (query.param === "anime") {
- hasil = null;
- tipe = "ANIME";
- if (
- query.season !== "WINTER" &&
- query.season !== "SPRING" &&
- query.season !== "SUMMER" &&
- query.season !== "FALL"
- ) {
- s = undefined;
- y = NaN;
- } else {
- s = query.season;
- y = parseInt(query.seasonYear);
- }
- } else if (query.param === "manga") {
- hasil = null;
- tipe = "MANGA";
- if (
- query.season !== "WINTER" &&
- query.season !== "SPRING" &&
- query.season !== "SUMMER" &&
- query.season !== "FALL"
- ) {
- s = undefined;
- y = NaN;
- } else {
- s = query.season;
- y = parseInt(query.seasonYear);
- }
- }
-
- // console.log(tags);
-
- const [search, setQuery] = useState(hasil);
- const [type, setSelectedType] = useState(tipe);
- // const [genres, setSelectedGenre] = useState();
- const [sort, setSelectedSort] = useState();
-
- const [isVisible, setIsVisible] = useState(false);
-
- const inputRef = useRef(null);
-
- const [page, setPage] = useState(1);
- const [nextPage, setNextPage] = useState(true);
-
- async function advance() {
- setLoading(true);
- const data = await aniAdvanceSearch({
- search: search,
- type: type,
- genres: gr,
- page: page,
- sort: sort,
- season: s,
- seasonYear: y,
- });
- if (data?.media?.length === 0) {
- setNextPage(false);
- } else if (data !== null && page > 1) {
- setData((prevData) => {
- return [...(prevData ?? []), ...data?.media];
- });
- setNextPage(data?.pageInfo.hasNextPage);
- } else {
- setData(data?.media);
- }
- setNextPage(data?.pageInfo.hasNextPage);
- setLoading(false);
- }
-
- useEffect(() => {
- setData(null);
- setPage(1);
- setNextPage(true);
- advance();
- }, [search, type, sort, s, y, gr]);
-
- useEffect(() => {
- advance();
- }, [page]);
-
- useEffect(() => {
- function handleScroll() {
- if (page > 10 || !nextPage) {
- window.removeEventListener("scroll", handleScroll);
- return;
- }
-
- if (
- window.innerHeight + window.pageYOffset >=
- document.body.offsetHeight - 3
- ) {
- setPage((prevPage) => prevPage + 1);
- }
- }
-
- window.addEventListener("scroll", handleScroll);
-
- return () => window.removeEventListener("scroll", handleScroll);
- }, [page, nextPage]);
-
- const handleKeyDown = async (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- const inputValue = event.target.value;
- if (inputValue === "") {
- setQuery(null);
- } else {
- setQuery(inputValue);
- }
- }
- };
-
- function trash() {
- setQuery(null);
- inputRef.current.value = "";
- // setSelectedGenre(null);
- setSelectedSort(["POPULARITY_DESC"]);
- router.push(`/en/search/${tipe.toLocaleLowerCase()}`);
- }
-
- function handleVisible() {
- setIsVisible(!isVisible);
- }
-
- function handleTipe(e) {
- setSelectedType(e.target.value);
- router.push(`/en/search/${e.target.value.toLowerCase()}`);
- }
-
- // );
-
- return (
- <>
- <Head>
- <title>Moopa - search</title>
- <link rel="icon" href="/c.svg" />
- </Head>
- <div className="bg-primary">
- <Navbar />
- <div className="min-h-screen mt-10 mb-14 text-white items-center gap-5 xl:gap-0 flex flex-col">
- <div className="w-screen px-10 xl:w-[80%] xl:h-[10rem] flex text-center xl:items-end xl:pb-10 justify-center lg:gap-7 xl:gap-10 gap-3 font-karla font-light">
- <div className="text-start">
- <h1 className="font-bold xl:pb-5 pb-3 hidden lg:block text-md pl-1 font-outfit">
- TITLE
- </h1>
- <input
- className="xl:w-[297px] md:w-[297px] lg:w-[230px] xl:h-[46px] h-[35px] xxs:w-[230px] xs:w-[280px] bg-secondary rounded-[10px] font-karla font-light text-[#ffffff89] text-center"
- placeholder="search here..."
- type="text"
- onKeyDown={handleKeyDown}
- ref={inputRef}
- />
- </div>
-
- {/* TYPE */}
- <div className="hidden lg:block text-start">
- <h1 className="font-bold xl:pb-5 pb-3 text-md pl-1 font-outfit">
- TYPE
- </h1>
- <div className="relative">
- <select
- className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] justify-between flex items-center text-center appearance-none"
- value={type}
- onChange={(e) => handleTipe(e)}
- >
- {types.map((option) => (
- <option key={option} value={option}>
- {option}
- </option>
- ))}
- </select>
- <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
- </div>
- </div>
-
- {/* SORT */}
- <div className="hidden lg:block text-start">
- <h1 className="font-bold xl:pb-5 lg:pb-3 text-md pl-1 font-outfit">
- SORT
- </h1>
- <div className="relative">
- <select
- className="xl:w-[297px] xl:h-[46px] lg:h-[35px] lg:w-[230px] bg-secondary rounded-[10px] flex items-center text-center appearance-none"
- onChange={(e) => {
- setSelectedSort(e.target.value);
- setData(null);
- }}
- >
- <option value={["POPULARITY_DESC"]}>Sort By</option>
- {sorts.map((sort) => (
- <option key={sort.value} value={sort.value}>
- {sort.name}
- </option>
- ))}
- </select>
- <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
- </div>
- </div>
-
- {/* OPTIONS */}
- <div className="flex lg:gap-7 text-center gap-3 items-end">
- <div
- className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group"
- onClick={handleVisible}
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"
- />
- </svg>
- </div>
-
- {/* TRASH ICON */}
- <div
- className="xl:w-[73px] w-[50px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] justify-center flex items-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 group"
- onClick={trash}
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
- />
- </svg>
- </div>
- </div>
- </div>
-
- <div className="w-screen xl:w-[64%] flex xl:justify-end xl:pl-0">
- <AnimatePresence>
- {isVisible && (
- <m.div
- key="imagine"
- initial={{ opacity: 0, y: -10 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0, y: -10 }}
- className="xl:pb-16"
- >
- <div className="text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 lg:pb-0 ">
- <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit">
- GENRE
- </h1>
- <div className="relative">
- <select
- className="w-[195px] xl:w-[297px] xl:h-[46px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none"
- onChange={(e) => {
- // setSelectedGenre(
- // e.target.value === "undefined"
- // ? undefined
- // : e.target.value
- // );
- router.push(
- `/en/search/${tipe.toLocaleLowerCase()}/?genres=${
- e.target.value
- }`
- );
- }}
- >
- <option value="undefined">Select a Genre</option>
- {genre.map((option) => {
- return (
- <option key={option} value={option}>
- {option}
- </option>
- );
- })}
- </select>
- <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
- </div>
- </div>
- <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col pb-5 ">
- <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit">
- TYPE
- </h1>
- <div className="relative">
- <select
- className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none"
- value={type}
- onChange={(e) => setSelectedType(e.target.value)}
- >
- {types.map((option) => (
- <option key={option} value={option}>
- {option}
- </option>
- ))}
- </select>
- <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
- </div>
- </div>
-
- <div className="xl:hidden text-start items-center xl:items-start flex w-screen xl:w-auto px-8 xl:px-0 flex-row justify-between xl:flex-col ">
- <h1 className="font-bold xl:pb-5 text-md pl-1 font-outfit">
- SORT
- </h1>
- <div className="relative">
- <select
- className="w-[195px] h-[35px] bg-secondary rounded-[10px] flex items-center text-center cursor-pointer hover:bg-[#272b35] transition-all duration-300 appearance-none"
- onChange={(e) => {
- setSelectedSort(e.target.value);
- }}
- >
- <option value={["POPULARITY_DESC"]}>Sort By</option>
- {sorts.map((sort) => (
- <option key={sort.value} value={sort.value}>
- {sort.name}
- </option>
- ))}
- </select>
- <ChevronDownIcon className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
- </div>
- </div>
- </m.div>
- )}
- </AnimatePresence>
- </div>
- {gr && (
- <div className="lg:w-[70%] px-5 lg:px-4 w-screen lg:mb-6">
- <h1 className="font-bold text-[25px] font-karla">
- Looking for : {gr}
- </h1>
- </div>
- )}
- <div className="flex flex-col gap-14 items-center">
- <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>
- )}
- {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}
- >
- <Link
- href={
- anime.format === "MANGA" || anime.format === "NOVEL"
- ? `/en/manga/${anime.id}`
- : `/en/anime/${anime.id}`
- }
- title={anime.title.userPreferred}
- >
- <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}
- />
- </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>} &#183;{" "}
- {anime.status || <p>-</p>} &#183;{" "}
- {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 }}
- >
- <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" />
- <Skeleton width={110} height={30} />
- </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>
- </div>
- </div>
- <Footer />
- </div>
- </>
- );
-}