aboutsummaryrefslogtreecommitdiff
path: root/pages
diff options
context:
space:
mode:
Diffstat (limited to 'pages')
-rw-r--r--pages/404.js22
-rw-r--r--pages/_app.js66
-rw-r--r--pages/api/auth/[...nextauth].js20
-rw-r--r--pages/en/about.js (renamed from pages/about.js)4
-rw-r--r--pages/en/anime/[...id].js (renamed from pages/anime/[...id].js)102
-rw-r--r--pages/en/anime/watch/[...info].js (renamed from pages/anime/watch/[...info].js)93
-rw-r--r--pages/en/contact.js (renamed from pages/contact.js)2
-rw-r--r--pages/en/dmca.js (renamed from pages/dmca.js)2
-rw-r--r--pages/en/index.js576
-rw-r--r--pages/en/manga/[id].js172
-rw-r--r--pages/en/manga/read/[...params].js262
-rw-r--r--pages/en/profile/[user].js (renamed from pages/profile/[user].js)8
-rw-r--r--pages/en/search/[param].js493
-rw-r--r--pages/id/about.js57
-rw-r--r--pages/id/anime/[...id].js850
-rw-r--r--pages/id/anime/watch/[...info].js488
-rw-r--r--pages/id/contact.js19
-rw-r--r--pages/id/dmca.js109
-rw-r--r--pages/id/index.js633
-rw-r--r--pages/id/profile/[user].js423
-rw-r--r--pages/id/search/[param].js (renamed from pages/search/[param].js)13
-rw-r--r--pages/index.js626
22 files changed, 4326 insertions, 714 deletions
diff --git a/pages/404.js b/pages/404.js
index 8cead6b..746e8fa 100644
--- a/pages/404.js
+++ b/pages/404.js
@@ -2,8 +2,26 @@ import Head from "next/head";
import Footer from "../components/footer";
import Navbar from "../components/navbar";
import Link from "next/link";
+import { useEffect, useState } from "react";
+import { parseCookies } from "nookies";
export default function Custom404() {
+ const [lang, setLang] = useState("en");
+ const [cookie, setCookies] = useState(null);
+
+ useEffect(() => {
+ let lang = null;
+ if (!cookie) {
+ const cookie = parseCookies();
+ lang = cookie.lang || null;
+ setCookies(cookie);
+ }
+ if (lang === "en" || lang === null) {
+ setLang("en");
+ } else if (lang === "id") {
+ setLang("id");
+ }
+ }, []);
return (
<>
<Head>
@@ -13,7 +31,7 @@ export default function Custom404() {
<link rel="icon" href="/c.svg" />
</Head>
<Navbar className="bg-[#0c0d10]" />
- <div className="min-h-screen flex flex-col items-center justify-center ">
+ <div className="min-h-screen w-screen flex flex-col items-center justify-center ">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
@@ -88,7 +106,7 @@ export default function Custom404() {
<p className="text-base sm:text-lg xl:text-xl text-gray-300 mb-6 text-center">
The page you're looking for doesn't seem to exist.
</p>
- <Link href="/">
+ <Link href={`/${lang}/`}>
<div className="bg-[#fa7d56] xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-[#fb6f44]">
Go back home
</div>
diff --git a/pages/_app.js b/pages/_app.js
index 869f66e..6334bd9 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -1,9 +1,11 @@
-import { ThemeProvider } from "next-themes";
-import "../styles/globals.css";
+// import { ThemeProvider } from "next-themes";
import { useRouter } from "next/router";
import { AnimatePresence, motion as m } from "framer-motion";
import NextNProgress from "nextjs-progressbar";
import { SessionProvider } from "next-auth/react";
+import "../styles/globals.css";
+import "react-toastify/dist/ReactToastify.css";
+import "react-loading-skeleton/dist/skeleton.css";
export default function App({
Component,
@@ -13,36 +15,36 @@ export default function App({
return (
<SessionProvider session={session}>
- <ThemeProvider attribute="class">
- <AnimatePresence mode="wait">
- <m.div
- key={`route-${router.route}`}
- transition={{ duration: 0.5 }}
- initial="initialState"
- animate="animateState"
- exit="exitState"
- variants={{
- initialState: {
- opacity: 0,
- },
- animateState: {
- opacity: 1,
- },
- exitState: {},
- }}
- className="z-50 w-screen"
- >
- <NextNProgress
- color="#FF7E2C"
- startPosition={0.3}
- stopDelayMs={200}
- height={3}
- showOnShallow={true}
- />
- <Component {...pageProps} />
- </m.div>
- </AnimatePresence>
- </ThemeProvider>
+ {/* <ThemeProvider attribute="class"> */}
+ <AnimatePresence mode="wait">
+ <m.div
+ key={`route-${router.route}`}
+ transition={{ duration: 0.5 }}
+ initial="initialState"
+ animate="animateState"
+ exit="exitState"
+ variants={{
+ initialState: {
+ opacity: 0,
+ },
+ animateState: {
+ opacity: 1,
+ },
+ exitState: {},
+ }}
+ className="z-50 w-screen"
+ >
+ <NextNProgress
+ color="#FF7E2C"
+ startPosition={0.3}
+ stopDelayMs={200}
+ height={3}
+ showOnShallow={true}
+ />
+ <Component {...pageProps} />
+ </m.div>
+ </AnimatePresence>
+ {/* </ThemeProvider> */}
</SessionProvider>
);
}
diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js
index 713b5f7..f270e7a 100644
--- a/pages/api/auth/[...nextauth].js
+++ b/pages/api/auth/[...nextauth].js
@@ -1,6 +1,24 @@
import NextAuth from "next-auth";
import { GET_CURRENT_USER } from "../../../queries";
-import { client } from "../../../lib/apolloClient";
+import { ApolloClient, InMemoryCache } from "@apollo/client";
+
+const defaultOptions = {
+ watchQuery: {
+ fetchPolicy: "no-cache",
+ errorPolicy: "ignore",
+ },
+ query: {
+ fetchPolicy: "no-cache",
+ errorPolicy: "all",
+ },
+};
+
+const client = new ApolloClient({
+ uri: "https://graphql.anilist.co",
+ cache: new InMemoryCache(),
+ defaultOptions: defaultOptions,
+});
+
// import clientPromise from "../../../lib/mongodb";
// import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
diff --git a/pages/about.js b/pages/en/about.js
index 55d7914..9bd32ed 100644
--- a/pages/about.js
+++ b/pages/en/about.js
@@ -1,5 +1,5 @@
import Head from "next/head";
-import Layout from "../components/layout";
+import Layout from "../../components/layout";
import { motion } from "framer-motion";
import Link from "next/link";
@@ -44,7 +44,7 @@ export default function About() {
anime and manga. We hope you enjoy your stay here, and feel free
to contact us if you have any feedback or suggestions.
</p>
- <Link href="/contact">
+ <Link href="/en/contact">
<div className="bg-[#ffffff] text-black font-medium py-3 px-6 rounded-lg hover:bg-action transition duration-300 ease-in-out">
Contact Us
</div>
diff --git a/pages/anime/[...id].js b/pages/en/anime/[...id].js
index a5429ed..b6393d4 100644
--- a/pages/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -16,17 +16,19 @@ 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 Layout from "../../../components/layout";
import Link from "next/link";
-import Content from "../../components/hero/content";
-import Modal from "../../components/modal";
+import Content from "../../../components/home/content";
+import Modal from "../../../components/modal";
import { signIn, useSession } from "next-auth/react";
-import AniList from "../../components/media/aniList";
-import ListEditor from "../../components/listEditor";
+import AniList from "../../../components/media/aniList";
+import ListEditor from "../../../components/listEditor";
-import { GET_MEDIA_USER } from "../../queries";
-import { GET_MEDIA_INFO } from "../../queries";
+import { GET_MEDIA_USER } from "../../../queries";
+import { GET_MEDIA_INFO } from "../../../queries";
+
+import { ToastContainer } from "react-toastify";
// import { aniInfo } from "../../components/devComp/data";
// console.log(GET_MEDIA_USER);
@@ -155,11 +157,24 @@ export default function Info({ info, color, api }) {
};
const aPrv = [
- { name: "enime", available: enime?.episodes ? true : false },
- { name: "zoro", available: zoro?.episodes ? true : false },
+ {
+ name: "enime",
+ available:
+ enime?.episodes && enime?.episodes.length > 0
+ ? true
+ : false,
+ },
+ {
+ name: "zoro",
+ available:
+ zoro?.episodes && zoro?.episodes.length > 0 ? true : false,
+ },
{
name: "gogoanime",
- available: gogoanime?.episodes ? true : false,
+ available:
+ gogoanime?.episodes && gogoanime?.episodes.length > 0
+ ? true
+ : false,
},
];
@@ -277,6 +292,8 @@ export default function Info({ info, color, api }) {
document.body.style.overflow = "auto";
}
+ const filterProviders = availableProviders?.filter((x) => x.available);
+
return (
<>
<Head>
@@ -301,6 +318,7 @@ export default function Info({ info, color, api }) {
}&image=${info.bannerImage || info.coverImage.extraLarge}`}
/>
</Head>
+ <ToastContainer pauseOnFocusLoss={false} />
<Modal open={open} onClose={() => handleClose()}>
<div>
{!session && (
@@ -555,7 +573,7 @@ export default function Info({ info, color, api }) {
)}
</div>
<div
- className={`w-screen lg:w-full grid lg:grid-cols-3 justify-items-center gap-7 lg:pt-7 lg:pb-5 px-3 lg:px-4 pt-4 rounded-xl`}
+ className={`w-screen lg:w-full flex gap-5 overflow-x-scroll snap-x scroll-px-5 scrollbar-none lg:grid lg:grid-cols-3 justify-items-center lg:pt-7 lg:pb-5 px-3 lg:px-4 pt-4 rounded-xl`}
>
{info?.relations?.edges ? (
info?.relations?.edges
@@ -571,22 +589,16 @@ export default function Info({ info, color, api }) {
rel.type === "MOVIE" ||
rel.type === "SPECIAL" ||
rel.type === "ONA"
- ? `/anime/${rel.id}`
- : `/manga/detail/id?aniId=${
- rel.id
- }&aniTitle=${encodeURIComponent(
- info?.title?.english ||
- info?.title.romaji ||
- info?.title.native
- )}`
+ ? `/en/anime/${rel.id}`
+ : `/en/manga/${rel.id}`
}
- className={`hover:scale-[1.02] hover:shadow-lg lg:px-0 px-4 scale-100 transition-transform duration-200 ease-out w-full ${
+ className={`lg:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full ${
rel.type === "MUSIC" ? "pointer-events-none" : ""
}`}
>
<div
key={rel.id}
- className="w-full shrink h-[126px] bg-secondary flex rounded-md"
+ className="w-[400px] lg:w-full h-[126px] bg-secondary flex rounded-md"
>
<div className="w-[90px] bg-image rounded-l-md shrink-0">
<Image
@@ -676,25 +688,27 @@ export default function Info({ info, color, api }) {
}`}
>
<div className="flex items-end gap-3">
- <div className="relative flex gap-2 items-center">
- <p className="hidden md:block">Provider</p>
- <select
- onChange={handleProvider}
- value={prvValue}
- className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus:ring-1 focus:ring-action"
- >
- {availableProviders
- ?.filter((p) => p.available === true)
- .map((p) => {
- return (
- <option key={p.name} value={p.name}>
- {p.name}
- </option>
- );
- })}
- </select>
- <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
- </div>
+ {filterProviders?.length > 0 && (
+ <div className="relative flex gap-2 items-center">
+ <p className="hidden md:block">Provider</p>
+ <select
+ onChange={handleProvider}
+ value={prvValue}
+ className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus:ring-1 focus:ring-action"
+ >
+ {availableProviders
+ ?.filter((p) => p.available === true)
+ .map((p) => {
+ return (
+ <option key={p.name} value={p.name}>
+ {p.name}
+ </option>
+ );
+ })}
+ </select>
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
+ </div>
+ )}
{episode?.length > 50 && (
<div className="relative flex gap-2 items-center">
<p className="hidden md:block">Episodes</p>
@@ -853,7 +867,7 @@ export default function Info({ info, color, api }) {
<div
className={`grid ${
epiView === "1"
- ? "grid-auto-fit gap-5 lg:gap-8"
+ ? "grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-5 lg:gap-8"
: "flex flex-col gap-5"
} pb-5 pt-2 lg:pt-0 ${
epiView === "3" ? "" : "place-items-center"
@@ -871,7 +885,7 @@ export default function Info({ info, color, api }) {
return (
<Link
key={index}
- href={`/anime/watch/${epi.id}/${info.id}/${prvValue}`}
+ href={`/en/anime/watch/${epi.id}/${info.id}/${prvValue}`}
className="transition-all duration-200 ease-out lg:hover:scale-105 hover:ring-1 hover:ring-white cursor-pointer bg-secondary shrink-0 relative w-full h-[180px] sm:h-[130px] subpixel-antialiased rounded-md overflow-hidden"
>
<span className="absolute text-sm z-40 bottom-1 left-2 font-karla font-semibold text-white">
@@ -914,7 +928,7 @@ export default function Info({ info, color, api }) {
return (
<Link
key={index}
- href={`/anime/watch/${epi.id}/${info.id}/${prvValue}`}
+ href={`/en/anime/watch/${epi.id}/${info.id}/${prvValue}`}
className="flex group h-[110px] lg:h-[160px] w-full rounded-lg transition-all duration-300 ease-out bg-secondary cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white"
>
<div className="w-[43%] lg:w-[30%] relative shrink-0 z-40 rounded-lg overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]">
@@ -982,7 +996,7 @@ export default function Info({ info, color, api }) {
className="flex flex-col gap-3 px-2"
>
<Link
- href={`/anime/watch/${epi.id}/${info.id}/${prvValue}`}
+ href={`/en/anime/watch/${epi.id}/${info.id}/${prvValue}`}
className={`text-start text-sm lg:text-lg ${
progress && epi.number <= progress
? "text-[#5f5f5f]"
diff --git a/pages/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index d47071e..d6e40e1 100644
--- a/pages/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -5,21 +5,21 @@ import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { getServerSession } from "next-auth/next";
-import { authOptions } from "../../api/auth/[...nextauth]";
+import { authOptions } from "../../../api/auth/[...nextauth]";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
-import "react-loading-skeleton/dist/skeleton.css";
-import { Navigasi } from "../..";
import { ChevronDownIcon, ForwardIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/router";
-import { GET_MEDIA_USER } from "../../../queries";
+import { GET_MEDIA_USER } from "../../../../queries";
import dotenv from "dotenv";
+import Navigasi from "../../../../components/home/staticNav";
+import DisqusComments from "../../../../components/disqus";
const VideoPlayer = dynamic(() =>
- import("../../../components/videoPlayer", { ssr: false })
+ import("../../../../components/videoPlayer", { ssr: false })
);
export default function Info({ sessions, id, aniId, provider, proxy, api }) {
@@ -27,18 +27,24 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
const [data, setAniData] = useState(null);
const [skip, setSkip] = useState({ op: null, ed: null });
const [statusWatch, setStatusWatch] = useState("CURRENT");
- const [playingEpisode, setPlayingEpisode] = useState(null);
const [loading, setLoading] = useState(false);
+ const [showComments, setShowComments] = useState(false);
+
+ const [playing, setPlaying] = useState(null);
+ const [playingEpisode, setPlayingEpisode] = useState(null);
const [playingTitle, setPlayingTitle] = useState(null);
+
const [poster, setPoster] = useState(null);
const [progress, setProgress] = useState(0);
const [episodes, setEpisodes] = useState([]);
const [artStorage, setArtStorage] = useState(null);
+ const [url, setUrl] = useState(null);
+
const router = useRouter();
- // console.log(data);
+ // console.log({ playing });
useEffect(() => {
const defaultState = {
@@ -47,6 +53,7 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
statusWatch: "CURRENT",
playingEpisode: null,
loading: false,
+ showComments: false,
};
// Reset all state variables to their default values
@@ -69,6 +76,9 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
}
});
+ const url = window.location.href;
+ setUrl(url);
+
const fetchData = async () => {
try {
if (provider) {
@@ -111,9 +121,10 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
setPlayingEpisode(playingEpisode);
- const playing = aniData.episodes.filter((item) => item.id == id);
+ const playing = aniData.episodes.find((item) => item.id === id);
- setPoster(playing);
+ setPoster(playing?.image);
+ setPlaying(playing);
const title = aniData.episodes
.filter((item) => item.id == id)
@@ -190,10 +201,9 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
const mediaSession = navigator.mediaSession;
if (!mediaSession) return;
- const artwork =
- poster && poster.length > 0
- ? [{ src: poster[0].image, sizes: "512x512", type: "image/jpeg" }]
- : undefined;
+ const artwork = poster
+ ? [{ src: poster, sizes: "512x512", type: "image/jpeg" }]
+ : undefined;
mediaSession.metadata = new MediaMetadata({
title: playingTitle,
@@ -231,7 +241,7 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
op={skip.op}
ed={skip.ed}
title={playingTitle}
- poster={poster[0]?.image}
+ poster={poster}
proxy={proxy}
provider={provider}
/>
@@ -256,7 +266,7 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
<div key={item.id} className="p-3 grid gap-2 w-[60%]">
<div className="text-xl font-outfit font-semibold line-clamp-1">
<Link
- href={`/anime/${data.id}`}
+ href={`/en/anime/${data.id}`}
className="inline hover:underline"
>
{item.title ||
@@ -279,7 +289,7 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
episode.number === parseInt(e.target.value)
);
router.push(
- `/anime/watch/${selectedEpisode.id}/${data.id}`
+ `/en/anime/watch/${selectedEpisode.id}/${data.id}`
);
}}
>
@@ -312,7 +322,7 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
const nextEpisode =
data.episodes[currentEpisodeIndex + 1];
router.push(
- `/anime/watch/${nextEpisode.id}/${data.id}`
+ `/en/anime/watch/${nextEpisode.id}/${data.id}`
);
}
}}
@@ -430,6 +440,51 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
/>
)}
</div>
+ {!showComments && loading && (
+ <div className="w-full flex justify-center py-5 font-karla px-3 lg:px-0">
+ <button
+ onClick={() => setShowComments(true)}
+ className={
+ showComments
+ ? "hidden"
+ : "flex-center gap-2 h-10 bg-secondary rounded w-full lg:w-[50%]"
+ }
+ >
+ Load Disqus{" "}
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth="1.5"
+ stroke="currentColor"
+ className="w-5 h-5"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
+ />
+ </svg>
+ </button>
+ </div>
+ )}
+ {showComments && (
+ <div>
+ {data && url && playing && (
+ <div className="mt-5 px-5">
+ <DisqusComments
+ key={id}
+ post={{
+ id: id,
+ title: data.title.romaji,
+ url: url,
+ episode: playing.number,
+ }}
+ />
+ </div>
+ )}
+ </div>
+ )}
</div>
</div>
<div className="flex flex-col w-screen lg:w-[35%] ">
@@ -448,7 +503,7 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
if (prog > 90) prog = 100;
return (
<Link
- href={`/anime/watch/${item.id}/${data.id}${
+ href={`/en/anime/watch/${item.id}/${data.id}${
provider ? `/${provider}` : ""
}`}
key={item.id}
@@ -520,7 +575,7 @@ export default function Info({ sessions, id, aniId, provider, proxy, api }) {
data.episodes.map((item) => {
return (
<Link
- href={`/anime/watch/${item.id}/${data.id}${
+ href={`/en/anime/watch/${item.id}/${data.id}${
provider ? "/9anime" : ""
}`}
key={item.id}
diff --git a/pages/contact.js b/pages/en/contact.js
index c7da878..400a9e8 100644
--- a/pages/contact.js
+++ b/pages/en/contact.js
@@ -1,4 +1,4 @@
-import Layout from "../components/layout";
+import Layout from "../../components/layout";
const Contact = () => {
return (
diff --git a/pages/dmca.js b/pages/en/dmca.js
index f306054..8dad7d7 100644
--- a/pages/dmca.js
+++ b/pages/en/dmca.js
@@ -1,5 +1,5 @@
import Head from "next/head";
-import Layout from "../components/layout";
+import Layout from "../../components/layout";
export default function DMCA() {
return (
diff --git a/pages/en/index.js b/pages/en/index.js
new file mode 100644
index 0000000..d13f182
--- /dev/null
+++ b/pages/en/index.js
@@ -0,0 +1,576 @@
+import { aniListData } from "../../lib/anilist/AniList";
+import React, { useState, useEffect } from "react";
+import Head from "next/head";
+import Link from "next/link";
+import Footer from "../../components/footer";
+import Image from "next/image";
+import Content from "../../components/home/content";
+
+import { motion } from "framer-motion";
+
+import { signIn, signOut } from "next-auth/react";
+import { useAniList } from "../../lib/anilist/useAnilist";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "../api/auth/[...nextauth]";
+import SearchBar from "../../components/searchBar";
+import Genres from "../../components/home/genres";
+import Schedule from "../../components/home/schedule";
+import getUpcomingAnime from "../../lib/anilist/getUpcomingAnime";
+import { useCountdown } from "../../lib/useCountdownSeconds";
+
+import dotenv from "dotenv";
+import Navigasi from "../../components/home/staticNav";
+
+// Filter schedules for each day
+const filterByCountryOfOrigin = (schedule, country) => {
+ const filteredSchedule = {};
+ for (const day in schedule) {
+ filteredSchedule[day] = schedule[day].filter(
+ (anime) => anime.countryOfOrigin === country
+ );
+ }
+ return filteredSchedule;
+};
+
+export default function Home({
+ detail,
+ populars,
+ sessions,
+ upComing,
+ schedules,
+}) {
+ const { media: current } = useAniList(sessions, { stats: "CURRENT" });
+ const { media: plan } = useAniList(sessions, { stats: "PLANNING" });
+ const { media: release } = useAniList(sessions);
+
+ const [anime, setAnime] = useState([]);
+ let scheduleData = null;
+
+ 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);
+ }
+ }, [upComing]);
+
+ const [releaseData, setReleaseData] = useState([]);
+
+ // console.log(schedules);
+
+ useEffect(() => {
+ function getRelease() {
+ let releasingAnime = [];
+ let progress = [];
+ release.map((list) => {
+ list.entries.map((entry) => {
+ if (entry.media.status === "RELEASING") {
+ releasingAnime.push(entry.media);
+ }
+
+ progress.push(entry);
+ });
+ });
+ setReleaseData(releasingAnime);
+ setProg(progress);
+ }
+ getRelease();
+ }, [release]);
+
+ const [isVisible, setIsVisible] = useState(false);
+ const [list, setList] = useState(null);
+ const [planned, setPlanned] = useState(null);
+ const [greeting, setGreeting] = useState("");
+
+ const [prog, setProg] = useState(null);
+
+ const popular = populars?.data;
+ const data = detail.data[0];
+
+ const handleShowClick = () => {
+ setIsVisible(true);
+ };
+
+ const handleHideClick = () => {
+ setIsVisible(false);
+ };
+
+ 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) return;
+ const getMedia =
+ current.filter((item) => item.status === "CURRENT")[0] || null;
+ const list = getMedia?.entries
+ .map(({ media }) => media)
+ .filter((media) => media);
+
+ const planned = plan?.[0]?.entries
+ .map(({ media }) => media)
+ .filter((media) => media);
+
+ if (list) {
+ setList(list.reverse());
+ }
+ if (planned) {
+ setPlanned(planned.reverse());
+ }
+ }
+ userData();
+ }, [sessions, current, plan]);
+
+ return (
+ <>
+ <Head>
+ <title>Moopa</title>
+ <meta charSet="UTF-8"></meta>
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta
+ name="twitter:title"
+ content="Moopa - Free Anime and Manga Streaming"
+ />
+ <meta
+ 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!"
+ />
+ <meta
+ name="twitter:image"
+ content="https://cdn.discordapp.com/attachments/1084446049986420786/1093300833422168094/image.png"
+ />
+ <link rel="icon" href="/c.svg" />
+ </Head>
+
+ {/* NAVBAR */}
+ <div className="z-50">
+ {!isVisible && (
+ <button
+ onClick={handleShowClick}
+ className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden"
+ id="bars"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ >
+ <path
+ fillRule="evenodd"
+ d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
+ clipRule="evenodd"
+ />
+ </svg>
+ </button>
+ )}
+ </div>
+
+ {/* Mobile Menu */}
+ <div className={`transition-all duration-150 subpixel-antialiased z-50`}>
+ {isVisible && sessions && (
+ <Link
+ href={`/profile/${sessions?.user.name}`}
+ className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]"
+ >
+ <img
+ src={sessions?.user.image.large}
+ alt="user avatar"
+ className="object-cover w-[60px] h-[60px] rounded-full"
+ />
+ </Link>
+ )}
+ {isVisible && (
+ <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden">
+ <div className="grid grid-cols-4 place-items-center gap-6">
+ <button className="group flex flex-col items-center">
+ <Link href="/en/" className="">
+ <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="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
+ />
+ </svg>
+ </Link>
+ <Link
+ href="/en/"
+ className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
+ >
+ home
+ </Link>
+ </button>
+ <button className="group flex flex-col items-center">
+ <Link href="/en/about">
+ <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="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
+ />
+ </svg>
+ </Link>
+ <Link
+ href="/en/about"
+ className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
+ >
+ about
+ </Link>
+ </button>
+ <button className="group flex gap-[1.5px] flex-col items-center ">
+ <div>
+ <Link href="/en/search/anime">
+ <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="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
+ />
+ </svg>
+ </Link>
+ </div>
+ <Link
+ href="/en/search/anime"
+ className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
+ >
+ search
+ </Link>
+ </button>
+ {sessions ? (
+ <button
+ onClick={() => signOut("AniListProvider")}
+ className="group flex gap-[1.5px] flex-col items-center "
+ >
+ <div>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 96 960 960"
+ className="group-hover:fill-action w-6 h-6 fill-txt"
+ >
+ <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path>
+ </svg>
+ </div>
+ <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
+ logout
+ </h1>
+ </button>
+ ) : (
+ <button
+ onClick={() => signIn("AniListProvider")}
+ className="group flex gap-[1.5px] flex-col items-center "
+ >
+ <div>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 96 960 960"
+ className="group-hover:fill-action w-6 h-6 fill-txt mr-2"
+ >
+ <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path>
+ </svg>
+ </div>
+ <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
+ login
+ </h1>
+ </button>
+ )}
+ </div>
+ <button onClick={handleHideClick}>
+ <svg
+ width="20"
+ height="21"
+ className="fill-orange-500"
+ viewBox="0 0 20 21"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <rect
+ x="2.44043"
+ y="0.941467"
+ width="23.5842"
+ height="3.45134"
+ rx="1.72567"
+ transform="rotate(45 2.44043 0.941467)"
+ />
+ <rect
+ x="19.1172"
+ y="3.38196"
+ width="23.5842"
+ height="3.45134"
+ rx="1.72567"
+ transform="rotate(135 19.1172 3.38196)"
+ />
+ </svg>
+ </button>
+ </div>
+ )}
+ </div>
+
+ <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] ">
+ <Navigasi />
+ <SearchBar />
+ {/* PC / TABLET */}
+ <div className=" hidden justify-center lg:flex my-16">
+ <div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between">
+ <div className="row-start-2 flex h-full flex-col gap-7 lg:w-[55%] lg:justify-center">
+ <h1 className="w-[85%] font-outfit font-extrabold lg:text-[34px] line-clamp-2">
+ {data.title.english || data.title.romaji || data.title.native}
+ </h1>
+ <p
+ className="font-roboto font-light lg:text-[18px] line-clamp-5"
+ dangerouslySetInnerHTML={{ __html: data?.description }}
+ />
+
+ <div className="lg:pt-5">
+ <Link
+ href={`/en/anime/${data.id}`}
+ legacyBehavior
+ className="flex"
+ >
+ <a className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]">
+ START WATCHING
+ </a>
+ </Link>
+ </div>
+ </div>
+ <div className="z-10 row-start-1 flex justify-center ">
+ <div className="relative lg:h-[467px] lg:w-[322px] lg:scale-100">
+ <div className="absolute bg-gradient-to-t from-[#141519] to-transparent lg:h-[467px] lg:w-[322px]" />
+
+ <Image
+ draggable={false}
+ src={data.coverImage?.extraLarge || data.image}
+ alt={`alt for ${data.title.english || data.title.romaji}`}
+ width={460}
+ height={662}
+ priority
+ className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]"
+ />
+ </div>
+ </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>
+ <button
+ onClick={() => signOut()}
+ className="hidden text-center relative lg:flex justify-center group"
+ >
+ {sessions?.user.name}
+ <span className="absolute text-sm z-50 w-20 text-center bottom-11 text-white shadow-lg opacity-0 bg-secondary p-1 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all">
+ Sign Out
+ </span>
+ </button>
+ </div>
+ </div>
+ )}
+
+ <div className="lg:mt-16 mt-5 flex flex-col items-center">
+ <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
+ >
+ {sessions && releaseData?.length > 0 && (
+ <motion.div // Add motion.div to each child component
+ key="onGoing"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="onGoing"
+ section="On-Going Anime"
+ data={releaseData}
+ og={prog}
+ />
+ </motion.div>
+ )}
+
+ {sessions && list?.length > 0 && (
+ <motion.div // Add motion.div to each child component
+ key="listAnime"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="listAnime"
+ section="Your Watch List"
+ data={list}
+ og={prog}
+ />
+ </motion.div>
+ )}
+
+ {/* SECTION 2 */}
+ {sessions && planned?.length > 0 && (
+ <motion.div // Add motion.div to each child component
+ key="plannedAnime"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="plannedAnime"
+ section="Your Plan"
+ data={planned}
+ />
+ </motion.div>
+ )}
+
+ {/* SECTION 3 */}
+ {detail && (
+ <motion.div // Add motion.div to each child component
+ key="trendingAnime"
+ initial={{ y: 20, opacity: 0 }}
+ transition={{ duration: 0.5 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="trendingAnime"
+ section="Trending Now"
+ data={detail.data}
+ />
+ </motion.div>
+ )}
+
+ {/* Schedule */}
+ {anime.length > 0 && schedules && (
+ <motion.div // Add motion.div to each child component
+ key="schedule"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Schedule
+ data={anime[0]}
+ time={{
+ days: days || 0,
+ hours: hours || 0,
+ minutes: minutes || 0,
+ seconds: seconds || 0,
+ }}
+ scheduleData={schedules}
+ />
+ </motion.div>
+ )}
+
+ {/* SECTION 4 */}
+ {popular && (
+ <motion.div // Add motion.div to each child component
+ key="popularAnime"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="popularAnime"
+ section="Popular Anime"
+ data={popular}
+ />
+ </motion.div>
+ )}
+
+ <motion.div // Add motion.div to each child component
+ key="Genres"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Genres />
+ </motion.div>
+ </motion.div>
+ </div>
+ </div>
+ <Footer />
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ dotenv.config();
+
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ 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 apikey = process.env.API_KEY;
+ const res = await fetch(`https://api.anify.tv/schedule?apikey=${apikey}`);
+ const schedules = await res.json();
+
+ const upComing = await getUpcomingAnime();
+
+ return {
+ props: {
+ genre: genreDetail.props,
+ detail: trendingDetail.props,
+ populars: popularDetail.props,
+ sessions: session,
+ upComing,
+ schedules,
+ },
+ };
+}
diff --git a/pages/en/manga/[id].js b/pages/en/manga/[id].js
new file mode 100644
index 0000000..5e46599
--- /dev/null
+++ b/pages/en/manga/[id].js
@@ -0,0 +1,172 @@
+import dotenv from "dotenv";
+import ChapterSelector from "../../../components/manga/chapters";
+import HamburgerMenu from "../../../components/manga/mobile/hamburgerMenu";
+import Navbar from "../../../components/navbar";
+import TopSection from "../../../components/manga/info/topSection";
+import Footer from "../../../components/footer";
+import Head from "next/head";
+import { useEffect, useState } from "react";
+import { setCookie } from "nookies";
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../api/auth/[...nextauth]";
+
+export default function Manga({ info, userManga, chapters }) {
+ const [domainUrl, setDomainUrl] = useState("");
+ const [firstEp, setFirstEp] = useState();
+ const chaptersData =
+ info.chapters.data.length === 0 ? chapters : info.chapters.data;
+
+ useEffect(() => {
+ setDomainUrl(window.location.origin);
+ }, []);
+
+ return (
+ <>
+ <Head>
+ <title>
+ {info
+ ? `Manga - ${
+ info.title.romaji || info.title.english || info.title.native
+ }`
+ : "Getting Info..."}
+ </title>
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta
+ name="twitter:title"
+ content={`Moopa - ${info.title.romaji || info.title.english}`}
+ />
+ <meta
+ name="twitter:description"
+ content={`${info.description?.slice(0, 180)}...`}
+ />
+ <meta
+ name="twitter:image"
+ content={`${domainUrl}/api/og?title=${
+ info.title.romaji || info.title.english
+ }&image=${info.bannerImage || info.coverImage}`}
+ />
+ </Head>
+ <div className="min-h-screen w-screen flex flex-col items-center relative">
+ <HamburgerMenu />
+ <Navbar className="absolute top-0 w-full z-40" />
+ <div className="flex flex-col w-screen items-center gap-5 md:gap-10 py-10 pt-nav">
+ <div className="flex-center w-full relative z-30">
+ <TopSection info={info} firstEp={firstEp} setCookie={setCookie} />
+ <>
+ <div className="absolute hidden md:block z-20 bottom-0 h-1/2 w-full bg-secondary" />
+ <div className="absolute hidden md:block z-20 top-0 h-1/2 w-full bg-transparent" />
+ </>
+ </div>
+ <div className="w-[90%] xl:w-[70%] min-h-[35vh] z-40">
+ {chaptersData.length > 0 ? (
+ <ChapterSelector
+ chaptersData={chaptersData}
+ data={info}
+ setFirstEp={setFirstEp}
+ setCookie={setCookie}
+ userManga={userManga}
+ />
+ ) : (
+ <p>No Chapter Available :(</p>
+ )}
+ </div>
+ </div>
+ <Footer />
+ </div>
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ dotenv.config();
+
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ const { id } = context.query;
+ const key = process.env.API_KEY;
+ const res = await fetch(`https://api.anify.tv/info/${id}?apikey=${key}`);
+ const data = await res.json();
+
+ let userManga = null;
+
+ if (session) {
+ const response = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ 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
+ }
+ episodes
+ coverImage {
+ large
+ }
+ }
+ }
+ }
+ }
+ }
+ `,
+ variables: {
+ username: session?.user?.name,
+ },
+ }),
+ });
+ 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;
+ }
+ }
+
+ if (!data?.chapters) {
+ return {
+ notFound: true,
+ };
+ }
+
+ let chapter = null;
+
+ if (data?.chapters?.data.length === 0) {
+ const res2 = await fetch(
+ `https://api.anify.tv/chapters/${id}?apikey=${key}`
+ );
+ const data2 = await res2.json();
+ chapter = data2;
+ }
+
+ return {
+ props: {
+ info: data,
+ userManga,
+ chapters: chapter || null,
+ },
+ };
+}
diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js
new file mode 100644
index 0000000..0c6372d
--- /dev/null
+++ b/pages/en/manga/read/[...params].js
@@ -0,0 +1,262 @@
+import dotenv from "dotenv";
+import { useEffect, useRef, useState } from "react";
+import { LeftBar } from "../../../../components/manga/leftBar";
+import { useRouter } from "next/router";
+import RightBar from "../../../../components/manga/rightBar";
+import FirstPanel from "../../../../components/manga/panels/firstPanel";
+import SecondPanel from "../../../../components/manga/panels/secondPanel";
+import ThirdPanel from "../../../../components/manga/panels/thirdPanel";
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../../api/auth/[...nextauth]";
+import BottomBar from "../../../../components/manga/mobile/bottomBar";
+import TopBar from "../../../../components/manga/mobile/topBar";
+import { ToastContainer } from "react-toastify";
+import Head from "next/head";
+import nookies from "nookies";
+import ShortCutModal from "../../../../components/manga/modals/shortcutModal";
+import ChapterModal from "../../../../components/manga/modals/chapterModal";
+
+export default function Read({ data, currentId, sessions }) {
+ const [info, setInfo] = useState();
+ const [chapter, setChapter] = useState([]);
+ const [layout, setLayout] = useState(1);
+
+ const [visible, setVisible] = useState(true);
+ const [mobileVisible, setMobileVisible] = useState(true);
+ const [isKeyOpen, setIsKeyOpen] = useState(false);
+ const [isChapOpen, setIsChapOpen] = useState(false);
+
+ const [seekPage, setSeekPage] = useState(0);
+
+ const [paddingX, setPaddingX] = useState(208);
+ const [scaleImg, setScaleImg] = useState(1);
+
+ const [nextChapterId, setNextChapterId] = useState(null);
+ const [prevChapterId, setPrevChapterId] = useState(null);
+
+ const [currentChapter, setCurrentChapter] = useState(null);
+ const [currentPage, setCurrentPage] = useState(0);
+
+ const hasRun = useRef(false);
+
+ const router = useRouter();
+
+ // console.log(cookies);
+
+ useEffect(() => {
+ hasRun.current = false;
+ }, [currentId]);
+
+ useEffect(() => {
+ const get = JSON.parse(localStorage.getItem("manga"));
+ const chapters = get.manga;
+ const currentChapter = chapters.chapters?.find((x) => x.id === currentId);
+
+ setCurrentChapter(currentChapter);
+ setInfo(get.data);
+ setChapter(chapters);
+
+ if (Array.isArray(chapters?.chapters)) {
+ const currentIndex = chapters.chapters.findIndex(
+ (chapter) => chapter.id === currentId
+ );
+ if (currentIndex !== -1) {
+ const nextChapter = chapters.chapters[currentIndex - 1];
+ const prevChapter = chapters.chapters[currentIndex + 1];
+ setNextChapterId(nextChapter ? nextChapter.id : null);
+ setPrevChapterId(prevChapter ? prevChapter.id : null);
+ }
+ }
+ }, [currentId]);
+
+ useEffect(() => {
+ const handleKeyDown = (event) => {
+ if (event.key === "ArrowRight" && event.ctrlKey && nextChapterId) {
+ router.push(
+ `/en/manga/read/${chapter.providerId}?id=${
+ info.id
+ }&chapterId=${encodeURIComponent(nextChapterId)}`
+ );
+ } else if (event.key === "ArrowLeft" && event.ctrlKey && prevChapterId) {
+ router.push(
+ `/en/manga/read/${chapter.providerId}?id=${
+ info.id
+ }&chapterId=${encodeURIComponent(prevChapterId)}`
+ );
+ }
+ if (event.code === "Slash" && event.ctrlKey) {
+ setIsKeyOpen(!isKeyOpen);
+ }
+ if (event.key === "f" || event.key === "F") {
+ setVisible(!visible);
+ }
+ if (event.code === "ArrowUp" && event.shiftKey) {
+ setPaddingX(paddingX - 50);
+ } else if (event.code === "ArrowDown" && event.shiftKey) {
+ setPaddingX(paddingX + 50);
+ }
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown);
+ };
+ }, [nextChapterId, prevChapterId, visible, isKeyOpen, paddingX]);
+
+ return (
+ <>
+ <Head>
+ <title>
+ {info
+ ? `Manga - ${
+ info.title.romaji || info.title.english || info.title.native
+ }`
+ : "Getting Info..."}
+ </title>
+ </Head>
+ <div className="w-screen flex justify-evenly relative">
+ <ToastContainer pauseOnFocusLoss={false} />
+ <ShortCutModal isOpen={isKeyOpen} setIsOpen={setIsKeyOpen} />
+ <ChapterModal
+ id={info?.id}
+ currentId={currentId}
+ data={chapter}
+ isOpen={isChapOpen}
+ setIsOpen={setIsChapOpen}
+ />
+
+ {mobileVisible && (
+ <>
+ <TopBar info={info} />
+ <BottomBar
+ id={info?.id}
+ prevChapter={prevChapterId}
+ nextChapter={nextChapterId}
+ currentPage={currentPage}
+ chapter={chapter}
+ page={data}
+ setSeekPage={setSeekPage}
+ setIsOpen={setIsChapOpen}
+ />
+ </>
+ )}
+ {visible && (
+ <LeftBar
+ data={chapter}
+ page={data}
+ info={info}
+ currentId={currentId}
+ setSeekPage={setSeekPage}
+ />
+ )}
+ {layout === 1 && (
+ <FirstPanel
+ aniId={info?.id}
+ data={data}
+ hasRun={hasRun}
+ currentId={currentId}
+ seekPage={seekPage}
+ setSeekPage={setSeekPage}
+ visible={visible}
+ setVisible={setVisible}
+ chapter={chapter}
+ nextChapter={nextChapterId}
+ prevChapter={prevChapterId}
+ paddingX={paddingX}
+ session={sessions}
+ mobileVisible={mobileVisible}
+ setMobileVisible={setMobileVisible}
+ setCurrentPage={setCurrentPage}
+ />
+ )}
+ {layout === 2 && (
+ <SecondPanel
+ aniId={info?.id}
+ data={data}
+ hasRun={hasRun}
+ currentChapter={currentChapter}
+ currentId={currentId}
+ seekPage={seekPage}
+ setSeekPage={setSeekPage}
+ visible={visible}
+ setVisible={setVisible}
+ session={sessions}
+ />
+ )}
+ {layout === 3 && (
+ <ThirdPanel
+ aniId={info?.id}
+ data={data}
+ hasRun={hasRun}
+ currentId={currentId}
+ currentChapter={currentChapter}
+ seekPage={seekPage}
+ setSeekPage={setSeekPage}
+ visible={visible}
+ setVisible={setVisible}
+ session={sessions}
+ scaleImg={scaleImg}
+ setMobileVisible={setMobileVisible}
+ mobileVisible={mobileVisible}
+ />
+ )}
+ {visible && (
+ <RightBar
+ id={info?.id}
+ hasRun={hasRun}
+ session={sessions}
+ data={chapter}
+ currentId={currentId}
+ currentChapter={currentChapter}
+ layout={layout}
+ setLayout={setLayout}
+ paddingX={paddingX}
+ setPaddingX={setPaddingX}
+ setIsKeyOpen={setIsKeyOpen}
+ scaleImg={scaleImg}
+ setScaleImg={setScaleImg}
+ />
+ )}
+ </div>
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ dotenv.config();
+
+ const cookies = nookies.get(context);
+
+ const query = context.query;
+ const providerId = query.params[0];
+ const chapterId = query.chapterId;
+ const mediaId = query.id;
+
+ if (!cookies.manga || cookies.manga !== mediaId) {
+ return {
+ redirect: {
+ destination: `/en/manga/${mediaId}`,
+ },
+ };
+ }
+
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ const key = process.env.API_KEY;
+ const res = await fetch(
+ `https://api.anify.tv/pages?providerId=${providerId}&readId=${encodeURIComponent(
+ chapterId
+ )}&apikey=${key}`
+ );
+
+ const data = await res.json();
+
+ return {
+ props: {
+ data: data,
+ currentId: chapterId,
+ sessions: session,
+ },
+ };
+}
diff --git a/pages/profile/[user].js b/pages/en/profile/[user].js
index d5e316f..6bc804e 100644
--- a/pages/profile/[user].js
+++ b/pages/en/profile/[user].js
@@ -1,6 +1,6 @@
import { getServerSession } from "next-auth";
-import { authOptions } from "../api/auth/[...nextauth]";
-import Navbar from "../../components/navbar";
+import { authOptions } from "../../api/auth/[...nextauth]";
+import Navbar from "../../../components/navbar";
import Image from "next/image";
import Link from "next/link";
import Head from "next/head";
@@ -232,7 +232,7 @@ export default function MyList({ media, sessions, user, time }) {
/>
</div>
<Link
- href={`/anime/${item.media.id}`}
+ href={`/en/anime/${item.media.id}`}
className="font-semibold font-karla pl-2 text-sm line-clamp-1"
title={item.media.title.romaji}
>
@@ -271,7 +271,7 @@ export default function MyList({ media, sessions, user, time }) {
</p>
)}
<Link
- href="/search/anime"
+ href="/en/search/anime"
className="flex gap-2 text-sm ring-1 ring-action p-2 rounded-lg font-karla"
>
<svg
diff --git a/pages/en/search/[param].js b/pages/en/search/[param].js
new file mode 100644
index 0000000..480cebe
--- /dev/null
+++ b/pages/en/search/[param].js
@@ -0,0 +1,493 @@
+import { useEffect, useRef, useState } from "react";
+import { AnimatePresence, motion as m } from "framer-motion";
+import Skeleton, { SkeletonTheme } 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}`
+ }
+ className=""
+ >
+ <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}`}>
+ <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 || 0} Episodes
+ </h2>
+ </m.div>
+ );
+ })}
+
+ {loading && (
+ <>
+ <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32">
+ {[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>
+ ))}
+ </SkeletonTheme>
+ </>
+ )}
+ </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>
+ </>
+ );
+}
diff --git a/pages/id/about.js b/pages/id/about.js
new file mode 100644
index 0000000..9bd32ed
--- /dev/null
+++ b/pages/id/about.js
@@ -0,0 +1,57 @@
+import Head from "next/head";
+import Layout from "../../components/layout";
+import { motion } from "framer-motion";
+import Link from "next/link";
+
+export default function About() {
+ return (
+ <>
+ <Head>
+ <title>Moopa - About</title>
+ <meta name="about" content="About this web" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="/c.svg" />
+ </Head>
+ <Layout>
+ <motion.div
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ className="flex flex-col justify-center items-center min-h-screen md:py-0 py-16"
+ >
+ <div className="max-w-screen-lg w-full px-4 py-10">
+ <h1 className="text-4xl font-bold mb-6">About Us</h1>
+ <p className="text-lg mb-8">
+ 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.
+ </p>
+ <p className="text-lg mb-8">
+ At our site, you will find a vast collection of anime and manga
+ titles from different genres, including action, adventure, comedy,
+ romance, and more. We take pride in our fast and reliable servers,
+ which ensure smooth streaming and reading for all our users.
+ </p>
+ <p className="text-lg mb-8">
+ We believe that anime and manga have the power to inspire and
+ entertain people of all ages and backgrounds. Our service is
+ designed to make it easy for fans to access the content they love,
+ whether they are casual viewers or die-hard fans.
+ </p>
+ <p className="text-lg mb-8">
+ Thank you for choosing our website as your go-to platform for
+ anime and manga. We hope you enjoy your stay here, and feel free
+ to contact us if you have any feedback or suggestions.
+ </p>
+ <Link href="/en/contact">
+ <div className="bg-[#ffffff] text-black font-medium py-3 px-6 rounded-lg hover:bg-action transition duration-300 ease-in-out">
+ Contact Us
+ </div>
+ </Link>
+ </div>
+ </motion.div>
+ </Layout>
+ </>
+ );
+}
diff --git a/pages/id/anime/[...id].js b/pages/id/anime/[...id].js
new file mode 100644
index 0000000..8a52e4b
--- /dev/null
+++ b/pages/id/anime/[...id].js
@@ -0,0 +1,850 @@
+import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
+
+import {
+ ChevronDownIcon,
+ ClockIcon,
+ HeartIcon,
+} from "@heroicons/react/20/solid";
+import {
+ TvIcon,
+ ArrowTrendingUpIcon,
+ RectangleStackIcon,
+} from "@heroicons/react/24/outline";
+
+import Head from "next/head";
+import Image from "next/image";
+import { useRouter } from "next/router";
+import { useEffect, useRef, useState } from "react";
+import Layout from "../../../components/layout";
+import Link from "next/link";
+import Content from "../../../components/home/content";
+import Modal from "../../../components/modal";
+
+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 { closestMatch } from "closest-match";
+
+// import { aniInfo } from "../../components/devComp/data";
+// console.log(GET_MEDIA_USER);
+
+export default function Info({ info, color, api }) {
+ // Episodes dropdown
+ const [firstEpisodeIndex, setFirstEpisodeIndex] = useState(0);
+ const [lastEpisodeIndex, setLastEpisodeIndex] = useState();
+ const [selectedRange, setSelectedRange] = useState("All");
+ function onEpisodeIndexChange(e) {
+ if (e.target.value === "All") {
+ setFirstEpisodeIndex(0);
+ setLastEpisodeIndex();
+ setSelectedRange("All");
+ return;
+ }
+ setFirstEpisodeIndex(e.target.value.split("-")[0] - 1);
+ setLastEpisodeIndex(e.target.value.split("-")[1]);
+ setSelectedRange(e.target.value);
+ }
+
+ const { data: session } = useSession();
+ const [episode, setEpisode] = useState(null);
+ 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 [visible, setVisible] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [time, setTime] = useState(0);
+ const { id } = useRouter().query;
+
+ const [fetchFailed, setFetchFailed] = useState(false);
+ const failedAttempts = useRef(0);
+
+ const [artStorage, setArtStorage] = useState(null);
+
+ const rec = info?.recommendations?.nodes?.map(
+ (data) => data.mediaRecommendation
+ );
+
+ const [log, setLog] = useState();
+
+ //for episodes dropdown
+ useEffect(() => {
+ setFirstEpisodeIndex(0);
+ setLastEpisodeIndex();
+ setSelectedRange("All");
+ }, [info]);
+
+ useEffect(() => {
+ handleClose();
+ async function fetchData() {
+ setLoading(true);
+ if (id) {
+ const { protocol, host } = window.location;
+ const url = `${protocol}//${host}`;
+
+ setDomainUrl(url);
+
+ setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
+
+ setEpisode(null);
+ setProgress(0);
+ setStatuses(null);
+
+ try {
+ const res1 = await Promise.race([
+ fetch(
+ `https://ani-indo.vercel.app/get/search?q=${encodeURIComponent(
+ info.title.romaji
+ )}`
+ ),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("timeout")), 10000)
+ ),
+ ]);
+
+ const data1 = await res1.json();
+ if (data1.data.length === 0) {
+ let text = info.title.romaji;
+ let words = text.split(" ");
+ let firstTwoWords = words.slice(0, 2).join(" ");
+
+ setLog(firstTwoWords);
+ const anotherRes = await Promise.race([
+ fetch(
+ `https://ani-indo.vercel.app/get/search?q=${firstTwoWords}`
+ ),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("timeout")), 10000)
+ ),
+ ]);
+ const fallbackData = await anotherRes.json();
+
+ const title = fallbackData.data.map((i) => i.title);
+ const match = closestMatch(info.title.romaji, title);
+ if (match) {
+ const getAnime = fallbackData.data.find((i) => i.title === match);
+ const res2 = await fetch(
+ `https://ani-indo.vercel.app/get/info/${getAnime.animeId}`
+ );
+ const data2 = await res2.json();
+ if (data2.status === "success") {
+ setEpisode(data2.data[0].episode);
+ }
+ // setLog(data2);
+ } else {
+ setLoading(false);
+ }
+ }
+ if (data1.status === "success") {
+ const title = data1.data.map((i) => i.title);
+ const match = closestMatch(info.title.romaji, title);
+ if (match) {
+ const getAnime = data1.data.find((i) => i.title === match);
+ const res2 = await fetch(
+ `https://ani-indo.vercel.app/get/info/${getAnime.animeId}`
+ );
+ const data2 = await res2.json();
+ if (data2.status === "success") {
+ setEpisode(data2.data[0].episode);
+ }
+ // setLog(data2);
+ } else {
+ setLoading(false);
+ }
+ // setLog(match);
+ }
+ // setLog(data1);
+
+ 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 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]);
+ }
+ }
+ setLoading(false);
+ }
+
+ if (info.nextAiringEpisode) {
+ setTime(
+ convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)
+ );
+ }
+ } catch (error) {
+ if (error.message === "timeout") {
+ const currentAttempts =
+ parseInt(localStorage.getItem("failedAttempts") || "0", 10) + 1;
+ localStorage.setItem("failedAttempts", currentAttempts.toString());
+
+ if (currentAttempts < 3) {
+ window.location.reload();
+ } else {
+ localStorage.removeItem("failedAttempts");
+ setFetchFailed(true);
+ }
+ } else {
+ console.error(error);
+ }
+ }
+ }
+ setLoading(false);
+ }
+ fetchData();
+ }, [id, info, session?.user?.name]);
+
+ function handleOpen() {
+ setOpen(true);
+ document.body.style.overflow = "hidden";
+ }
+
+ function handleClose() {
+ setOpen(false);
+ document.body.style.overflow = "auto";
+ }
+
+ return (
+ <>
+ <Head>
+ <title>
+ {info
+ ? info?.title?.romaji || info?.title?.english
+ : "Retrieving Data..."}
+ </title>
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta
+ name="twitter:title"
+ content={`Moopa - ${info.title.romaji || info.title.english}`}
+ />
+ <meta
+ name="twitter:description"
+ content={`${info.description?.slice(0, 180)}...`}
+ />
+ <meta
+ name="twitter:image"
+ content={`${domainUrl}/api/og?title=${
+ info.title.romaji || info.title.english
+ }&image=${info.bannerImage || info.coverImage.extraLarge}`}
+ />
+ </Head>
+ <Modal open={open} onClose={() => handleClose()}>
+ <div>
+ {!session && (
+ <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md">
+ <h1 className="text-md font-extrabold font-karla">
+ Edit your list
+ </h1>
+ <button
+ className="flex items-center bg-[#363642] rounded-md text-white p-1"
+ onClick={() => signIn("AniListProvider")}
+ >
+ <h1 className="px-1 font-bold font-karla">
+ Login with AniList
+ </h1>
+ <div className="scale-[60%] pb-[1px]">
+ <AniList />
+ </div>
+ </button>
+ </div>
+ )}
+ {session && info && (
+ <ListEditor
+ animeId={info?.id}
+ session={session}
+ stats={statuses}
+ prg={progress}
+ max={info?.episodes}
+ image={info}
+ />
+ )}
+ </div>
+ </Modal>
+ <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32">
+ <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 ? (
+ <Image
+ src={
+ info?.bannerImage ||
+ info?.coverImage?.extraLarge ||
+ info?.coverImage.large
+ }
+ priority={true}
+ alt="banner anime"
+ height={1000}
+ width={1000}
+ className="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 */}
+
+ <div className="lg:hidden pt-5 w-screen px-5 flex flex-col">
+ <div className="h-[250px] flex flex-col gap-1 justify-center">
+ <h1 className="font-karla font-extrabold text-lg line-clamp-1 w-[70%]">
+ {info?.title?.romaji || info?.title?.english}
+ </h1>
+ <p
+ className="line-clamp-2 text-sm font-light antialiased w-[56%]"
+ dangerouslySetInnerHTML={{ __html: info?.description }}
+ />
+ <div className="font-light flex gap-1 py-1 flex-wrap font-outfit text-[10px] text-[#ffffff] w-[70%]">
+ {info?.genres
+ ?.slice(
+ 0,
+ info?.genres?.length > 3 ? info?.genres?.length : 3
+ )
+ .map((item, index) => (
+ <span
+ key={index}
+ className="px-2 py-1 bg-secondary shadow-lg font-outfit font-light rounded-full"
+ >
+ <span className="">{item}</span>
+ </span>
+ ))}
+ </div>
+ {info && (
+ <div className="flex items-center gap-5 pt-3 text-center">
+ <div className="flex items-center gap-2 text-center">
+ <button
+ type="button"
+ className="bg-action px-10 rounded-sm font-karla font-bold"
+ onClick={() => handleOpen()}
+ >
+ {!loading
+ ? statuses
+ ? statuses.name
+ : "Add to List"
+ : "Loading..."}
+ </button>
+ <div className="h-6 w-6">
+ <HeartIcon />
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ <div className="bg-secondary rounded-sm xs:h-[30px]">
+ <div className="grid grid-cols-3 place-content-center xxs:flex items-center justify-center h-full xxs:gap-10 p-2 text-sm">
+ {info && info.status !== "NOT_YET_RELEASED" ? (
+ <>
+ <div className="flex-center flex-col xxs:flex-row gap-2">
+ <TvIcon className="w-5 h-5 text-action" />
+ <h4 className="font-karla">{info?.type}</h4>
+ </div>
+ <div className="flex-center flex-col xxs:flex-row gap-2">
+ <ArrowTrendingUpIcon className="w-5 h-5 text-action" />
+ <h4>{info?.averageScore}%</h4>
+ </div>
+ <div className="flex-center flex-col xxs:flex-row gap-2">
+ <RectangleStackIcon className="w-5 h-5 text-action" />
+ {info?.episodes ? (
+ <h1>{info?.episodes} Episodes</h1>
+ ) : (
+ <h1>TBA</h1>
+ )}
+ </div>
+ </>
+ ) : (
+ <div>{info && "Not Yet Released"}</div>
+ )}
+ </div>
+ </div>
+ </div>
+
+ {/* PC */}
+ <div className="hidden lg:flex gap-8 w-full flex-nowrap">
+ <div className="shrink-0 lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] relative">
+ {info ? (
+ <>
+ <div className="bg-image lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] bg-opacity-30 absolute backdrop-blur-lg z-10 -top-7" />
+ <Image
+ src={
+ info.coverImage.extraLarge || info.coverImage.large
+ }
+ priority={true}
+ alt="poster anime"
+ height={700}
+ width={700}
+ className="object-cover lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] z-20 absolute rounded-md -top-7"
+ />
+ <button
+ type="button"
+ className="bg-action flex-center z-20 h-[20px] w-[180px] absolute bottom-0 rounded-sm font-karla font-bold"
+ onClick={() => handleOpen()}
+ >
+ {!loading
+ ? statuses
+ ? statuses.name
+ : "Add to List"
+ : "Loading..."}
+ </button>
+ </>
+ ) : (
+ <Skeleton className="h-[250px] w-[180px]" />
+ )}
+ </div>
+
+ {/* PC */}
+ <div className="hidden lg:flex w-full flex-col gap-5 h-[250px]">
+ <div className="flex flex-col gap-2">
+ <h1 className=" font-inter font-bold text-[36px] text-white line-clamp-1">
+ {info ? (
+ info?.title?.romaji || info?.title?.english
+ ) : (
+ <Skeleton width={450} />
+ )}
+ </h1>
+ {info ? (
+ <div className="flex gap-6">
+ {info?.episodes && (
+ <div
+ className={`dynamic-text rounded-md px-2 font-karla font-bold`}
+ style={color}
+ >
+ {info?.episodes} Episodes
+ </div>
+ )}
+ {info?.startDate?.year && (
+ <div
+ className={`dynamic-text rounded-md px-2 font-karla font-bold`}
+ style={color}
+ >
+ {info?.startDate?.year}
+ </div>
+ )}
+ {info?.averageScore && (
+ <div
+ className={`dynamic-text rounded-md px-2 font-karla font-bold`}
+ style={color}
+ >
+ {info?.averageScore}%
+ </div>
+ )}
+ {info?.type && (
+ <div
+ className={`dynamic-text rounded-md px-2 font-karla font-bold`}
+ style={color}
+ >
+ {info?.type}
+ </div>
+ )}
+ {info?.status && (
+ <div
+ className={`dynamic-text rounded-md px-2 font-karla font-bold`}
+ style={color}
+ >
+ {info?.status}
+ </div>
+ )}
+ <div
+ className={`dynamic-text rounded-md px-2 font-karla font-bold`}
+ style={color}
+ >
+ Sub | EN
+ </div>
+ </div>
+ ) : (
+ <Skeleton width={240} height={32} />
+ )}
+ </div>
+ {info ? (
+ <p
+ dangerouslySetInnerHTML={{ __html: info?.description }}
+ className="overflow-y-scroll scrollbar-thin pr-2 scrollbar-thumb-secondary scrollbar-thumb-rounded-lg h-[140px]"
+ />
+ ) : (
+ <Skeleton className="h-[130px]" />
+ )}
+ </div>
+ </div>
+
+ <div>
+ <div className="flex gap-5 items-center">
+ {info?.relations?.edges?.length > 0 && (
+ <div className="p-3 lg:p-0 text-[20px] lg:text-2xl font-bold font-karla">
+ Relations
+ </div>
+ )}
+ {info?.relations?.edges?.length > 3 && (
+ <div
+ className="cursor-pointer"
+ onClick={() => setShowAll(!showAll)}
+ >
+ {showAll ? "show less" : "show more"}
+ </div>
+ )}
+ </div>
+ <div
+ className={`w-screen lg:w-full grid lg:grid-cols-3 justify-items-center gap-7 lg:pt-7 lg:pb-5 px-3 lg:px-4 pt-4 rounded-xl`}
+ >
+ {info?.relations?.edges ? (
+ info?.relations?.edges
+ .slice(0, showAll ? info?.relations?.edges.length : 3)
+ .map((r, index) => {
+ const rel = r.node;
+ return (
+ <Link
+ key={rel.id}
+ href={
+ rel.type === "ANIME" ||
+ rel.type === "OVA" ||
+ rel.type === "MOVIE" ||
+ rel.type === "SPECIAL" ||
+ rel.type === "ONA"
+ ? `/id/anime/${rel.id}`
+ : `/manga/detail/id?aniId=${
+ rel.id
+ }&aniTitle=${encodeURIComponent(
+ info?.title?.english ||
+ info?.title.romaji ||
+ info?.title.native
+ )}`
+ }
+ className={`hover:scale-[1.02] hover:shadow-lg lg:px-0 px-4 scale-100 transition-transform duration-200 ease-out w-full ${
+ rel.type === "MUSIC" ? "pointer-events-none" : ""
+ }`}
+ >
+ <div
+ key={rel.id}
+ className="w-full shrink h-[126px] bg-secondary flex rounded-md"
+ >
+ <div className="w-[90px] bg-image rounded-l-md shrink-0">
+ <Image
+ src={
+ rel.coverImage.extraLarge ||
+ rel.coverImage.large
+ }
+ alt={rel.id}
+ height={500}
+ width={500}
+ className="object-cover h-full w-full shrink-0 rounded-l-md"
+ />
+ </div>
+ <div className="h-full grid px-3 items-center">
+ <div className="text-action font-outfit font-bold">
+ {r.relationType}
+ </div>
+ <div className="font-outfit font-thin line-clamp-2">
+ {rel.title.userPreferred || rel.title.romaji}
+ </div>
+ <div className={``}>{rel.type}</div>
+ </div>
+ </div>
+ </Link>
+ );
+ })
+ ) : (
+ <>
+ {[1, 2, 3].map((item) => (
+ <div key={item} className="w-full hidden lg:block">
+ <Skeleton className="h-[126px]" />
+ </div>
+ ))}
+ <div className="w-full lg:hidden">
+ <Skeleton className="h-[126px]" />
+ </div>
+ </>
+ )}
+ </div>
+ </div>
+ <div className="flex flex-col gap-5 lg:gap-10 p-3 lg:p-0">
+ <div className="flex lg:flex-row flex-col gap-5 lg:gap-0 justify-between ">
+ <div className="flex justify-between">
+ <div className="flex items-center lg:gap-10 sm:gap-7 gap-3">
+ {info && (
+ <h1 className="text-[20px] lg:text-2xl font-bold font-karla">
+ Episodes
+ </h1>
+ )}
+ {info?.nextAiringEpisode && (
+ <div className="flex items-center gap-2">
+ <div className="flex items-center gap-4 text-[10px] xxs:text-sm lg:text-base">
+ <h1>Next :</h1>
+ <div className="px-4 rounded-sm font-karla font-bold bg-white text-black">
+ {time}
+ </div>
+ </div>
+ <div className="h-6 w-6">
+ <ClockIcon />
+ </div>
+ </div>
+ )}
+ </div>
+ {episode?.length > 50 && (
+ <div
+ className="lg:hidden bg-secondary p-1 rounded-md cursor-pointer"
+ onClick={() => setVisible(!visible)}
+ >
+ <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"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
+ />
+ </svg>
+ </div>
+ )}
+ </div>
+ {episode?.length > 50 && (
+ <div
+ className={`flex lg:flex items-center gap-0 lg:gap-5 justify-between ${
+ visible ? "" : "hidden"
+ }`}
+ >
+ <div className="flex items-end gap-3">
+ {episode?.length > 50 && (
+ <div className="relative flex gap-2 items-center">
+ <p className="hidden md:block">Episodes</p>
+ <select
+ onChange={onEpisodeIndexChange}
+ value={selectedRange}
+ className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus:ring-1 focus:ring-action scrollbar-thin scrollbar-thumb-secondary scrollbar-thumb-rounded-lg"
+ >
+ <option value="All">All</option>
+ {[...Array(Math.ceil(episode?.length / 50))].map(
+ (_, index) => {
+ const start = index * 50 + 1;
+ const end = Math.min(
+ start + 50 - 1,
+ episode?.length
+ );
+ const optionLabel = `${start} to ${end}`;
+ if (episode[0]?.number !== 1) {
+ var valueLabel = `${
+ episode.length - end + 1
+ }-${episode.length - start + 1}`;
+ } else {
+ var valueLabel = `${start}-${end}`;
+ }
+ return (
+ <option key={valueLabel} value={valueLabel}>
+ {optionLabel}
+ </option>
+ );
+ }
+ )}
+ </select>
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+ </div>
+ {!loading ? (
+ Array.isArray(episode) ? (
+ episode && (
+ <div className="scrollbar-thin scrollbar-thumb-[#1b1c21] scrollbar-thumb-rounded-full overflow-y-scroll hover:scrollbar-thumb-[#2e2f37] h-[640px]">
+ {episode?.length !== 0 && episode ? (
+ <div
+ className={`flex flex-col gap-5 pb-5 pt-2 lg:pt-0`}
+ >
+ {episode
+ .slice(firstEpisodeIndex, lastEpisodeIndex)
+ .map((epi, index) => {
+ return (
+ <div
+ key={index}
+ className="flex flex-col gap-3 px-2"
+ >
+ <Link
+ href={`/id/anime/watch/${info.id}/${epi.episodeId}`}
+ className={`text-start text-sm lg:text-lg ${
+ progress && index <= progress - 1
+ ? "text-[#5f5f5f]"
+ : "text-white"
+ }`}
+ >
+ <p>{epi.epsTitle}</p>
+ </Link>
+ {index !== episode?.length - 1 && (
+ <span className="h-[1px] bg-white" />
+ )}
+ </div>
+ );
+ })}
+ </div>
+ ) : (
+ <p>No Episodes Available</p>
+ )}
+ </div>
+ )
+ ) : (
+ <div className="flex flex-col">
+ <pre
+ className={`rounded-md overflow-hidden ${getLanguageClassName(
+ "bash"
+ )}`}
+ >
+ <code>
+ {episode?.message || "Anime tidak tersedia :/"}
+ </code>
+ </pre>
+ </div>
+ )
+ ) : (
+ <div className="flex justify-center">
+ <div className="lds-ellipsis">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ {info && rec?.length !== 0 && (
+ <div className="w-screen lg:w-[90%] xl:w-[85%]">
+ <Content
+ ids="recommendAnime"
+ section="Recommendations"
+ data={rec}
+ />
+ </div>
+ )}
+ </div>
+ </Layout>
+ </SkeletonTheme>
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ const { id } = context.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],
+ },
+ }),
+ });
+
+ const json = await res.json();
+ const data = json?.data?.Media;
+
+ if (!data) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const textColor = setTxtColor(data?.coverImage?.color);
+
+ const color = {
+ backgroundColor: `${data?.coverImage?.color || "#ffff"}`,
+ color: textColor,
+ };
+
+ return {
+ props: {
+ info: data,
+ color: color,
+ api: API_URI,
+ },
+ };
+}
+
+function convertSecondsToTime(sec) {
+ let days = Math.floor(sec / (3600 * 24));
+ let hours = Math.floor((sec % (3600 * 24)) / 3600);
+ let minutes = Math.floor((sec % 3600) / 60);
+
+ let time = "";
+
+ if (days > 0) {
+ time += `${days}d `;
+ }
+
+ if (hours > 0) {
+ time += `${hours}h `;
+ }
+
+ if (minutes > 0) {
+ time += `${minutes}m `;
+ }
+
+ return time.trim();
+}
+
+function getBrightness(hexColor) {
+ if (!hexColor) {
+ return 200;
+ }
+ const rgb = hexColor
+ .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
+ .slice(1)
+ .map((x) => parseInt(x, 16));
+ return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000;
+}
+
+function setTxtColor(hexColor) {
+ const brightness = getBrightness(hexColor);
+ return brightness < 150 ? "#fff" : "#000";
+}
+
+const getLanguageClassName = (language) => {
+ switch (language) {
+ case "javascript":
+ return "language-javascript";
+ case "html":
+ return "language-html";
+ case "bash":
+ return "language-bash";
+ // add more languages here as needed
+ default:
+ return "";
+ }
+};
diff --git a/pages/id/anime/watch/[...info].js b/pages/id/anime/watch/[...info].js
new file mode 100644
index 0000000..89fd3a6
--- /dev/null
+++ b/pages/id/anime/watch/[...info].js
@@ -0,0 +1,488 @@
+import Image from "next/image";
+import Link from "next/link";
+import Head from "next/head";
+import { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
+
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "../../../api/auth/[...nextauth]";
+
+import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
+
+import { Navigasi } from "../..";
+import { ChevronDownIcon, ForwardIcon } from "@heroicons/react/24/solid";
+import { useRouter } from "next/router";
+
+import { GET_MEDIA_USER } from "../../../../queries";
+
+import dotenv from "dotenv";
+
+import VideoPlayer from "../../../../components/id-components/player/VideoPlayerId";
+
+export default function Info({ sessions, id, aniId, provider, api, proxy }) {
+ const [epiData, setEpiData] = useState(null);
+ const [data, setAniData] = useState(null);
+ const [episode, setEpisode] = useState(null);
+ const [skip, setSkip] = useState({ op: null, ed: null });
+ const [statusWatch, setStatusWatch] = useState("CURRENT");
+ const [playingEpisode, setPlayingEpisode] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [playingTitle, setPlayingTitle] = useState(null);
+ const [poster, setPoster] = useState(null);
+ const [progress, setProgress] = useState(0);
+ const [currentNumber, setCurrentNumber] = useState(null);
+
+ const [episodes, setEpisodes] = useState([]);
+ const [artStorage, setArtStorage] = useState(null);
+
+ const router = useRouter();
+
+ useEffect(() => {
+ const defaultState = {
+ epiData: null,
+ skip: { op: null, ed: null },
+ statusWatch: "CURRENT",
+ playingEpisode: null,
+ loading: false,
+ };
+
+ // Reset all state variables to their default values
+ Object.keys(defaultState).forEach((key) => {
+ const value = defaultState[key];
+ if (Array.isArray(value)) {
+ value.length
+ ? eval(
+ `set${
+ key.charAt(0).toUpperCase() + key.slice(1)
+ }(${JSON.stringify(value)})`
+ )
+ : eval(`set${key.charAt(0).toUpperCase() + key.slice(1)}([])`);
+ } else {
+ eval(
+ `set${key.charAt(0).toUpperCase() + key.slice(1)}(${JSON.stringify(
+ value
+ )})`
+ );
+ }
+ });
+
+ const fetchData = async () => {
+ let currentNumber = null;
+ try {
+ const res = await fetch(
+ `https://ani-indo.vercel.app/get/watch/${aniId}`
+ );
+ const epiData = await res.json();
+ currentNumber = epiData.episodeActive;
+ setCurrentNumber(currentNumber);
+ setEpisode(epiData.data);
+ setEpiData(epiData.episodeUrl);
+ } catch (error) {
+ setTimeout(() => {
+ window.location.reload();
+ }, 3000);
+ }
+
+ let aniData = null;
+ setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
+
+ const res2 = await fetch(`${api}/meta/anilist/info/${id}`);
+ aniData = await res2.json();
+ setEpisodes(aniData.episodes?.reverse());
+ setAniData(aniData);
+
+ let playingEpisode = aniData.episodes
+ .filter((item) => item.number == currentNumber)
+ .map((item) => item.number);
+
+ setPlayingEpisode(playingEpisode);
+
+ const playing = aniData.episodes.filter((item) => item.id == id);
+
+ setPoster(playing);
+
+ const title = aniData.episodes
+ .filter((item) => item.id == id)
+ .find((item) => item.title !== null);
+ setPlayingTitle(
+ title?.title || aniData.title?.romaji || aniData.title?.english
+ );
+
+ const res4 = await fetch(
+ `https://api.aniskip.com/v2/skip-times/${aniData.malId}/${parseInt(
+ playingEpisode
+ )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=`
+ );
+ const skip = await res4.json();
+
+ const op = skip.results?.find((item) => item.skipType === "op") || null;
+ const ed = skip.results?.find((item) => item.skipType === "ed") || null;
+
+ setSkip({ op, ed });
+
+ if (sessions) {
+ 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 dat = await response.json();
+
+ const prog = dat.data.MediaListCollection;
+
+ const gat = prog?.lists.map((item) => item.entries);
+ const git = gat?.map((item) =>
+ item?.find((item) => item.media.id === parseInt(aniId))
+ );
+ const gut = git?.find((item) => item?.media.id === parseInt(aniId));
+
+ if (gut) {
+ setProgress(gut.progress);
+ }
+
+ if (gut?.status === "COMPLETED") {
+ setStatusWatch("REPEATING");
+ } else if (
+ gut?.status === "REPEATING" &&
+ gut?.media?.episodes === parseInt(playingEpisode)
+ ) {
+ setStatusWatch("COMPLETED");
+ } else if (gut?.status === "REPEATING") {
+ setStatusWatch("REPEATING");
+ } else if (gut?.media?.episodes === parseInt(playingEpisode)) {
+ setStatusWatch("COMPLETED");
+ } else if (
+ gut?.media?.episodes !== null &&
+ aniData.totalEpisodes === parseInt(playingEpisode)
+ ) {
+ setStatusWatch("COMPLETED");
+ setLoading(true);
+ }
+ }
+ setLoading(true);
+ };
+ fetchData();
+ }, [id, aniId, provider, sessions]);
+
+ useEffect(() => {
+ const mediaSession = navigator.mediaSession;
+ if (!mediaSession) return;
+
+ const artwork =
+ poster && poster.length > 0
+ ? [{ src: poster[0].image, sizes: "512x512", type: "image/jpeg" }]
+ : undefined;
+
+ mediaSession.metadata = new MediaMetadata({
+ title: playingTitle,
+ artist: `Moopa ${
+ playingTitle === data?.title?.romaji
+ ? "- Episode " + playingEpisode
+ : `- ${data?.title?.romaji || data?.title?.english}`
+ }`,
+ artwork,
+ });
+ }, [poster, playingTitle, playingEpisode, data]);
+
+ return (
+ <>
+ <Head>
+ <title>{playingTitle || "Loading..."}</title>
+ </Head>
+
+ <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32">
+ <div className="bg-primary">
+ <Navigasi />
+ <div className="min-h-screen mt-3 md:mt-0 flex flex-col lg:gap-0 gap-5 lg:flex-row lg:py-10 lg:px-10 justify-start w-screen">
+ <div className="w-screen lg:w-[67%]">
+ {loading ? (
+ Array.isArray(epiData) ? (
+ <div className="aspect-video z-20 bg-black">
+ <VideoPlayer
+ key={id}
+ data={epiData}
+ id={aniId}
+ progress={parseInt(playingEpisode)}
+ session={sessions}
+ aniId={parseInt(data?.id)}
+ stats={statusWatch}
+ op={skip.op}
+ ed={skip.ed}
+ title={playingTitle}
+ poster={poster[0]?.image}
+ proxy={proxy}
+ />
+ </div>
+ ) : (
+ <div className="aspect-video bg-black flex-center select-none">
+ <p className="lg:p-0 p-5 text-center">
+ Whoops! Something went wrong. Please reload the page or
+ try other sources. {`:(`}
+ </p>
+ </div>
+ )
+ ) : (
+ <div className="aspect-video bg-black" />
+ )}
+ <div>
+ {data && data?.episodes.length > 0 ? (
+ data.episodes
+ .filter((items) => items.number == currentNumber)
+ .map((item, index) => (
+ <div className="flex justify-between" key={item.id}>
+ <div className="p-3 grid gap-2 w-[60%]">
+ <div className="text-xl font-outfit font-semibold line-clamp-1">
+ <Link
+ href={`/id/anime/${data.id}`}
+ className="inline hover:underline"
+ >
+ {item.title ||
+ data.title.romaji ||
+ data.title.english}
+ </Link>
+ </div>
+ <h4 className="text-sm font-karla font-light">
+ Episode {item.number}
+ </h4>
+ </div>
+ <div className="w-[50%] flex gap-4 items-center justify-end px-4">
+ <div className="relative">
+ <select
+ className="flex items-center gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer"
+ value={item.number}
+ onChange={(e) => {
+ const selectedEpisode = data.episodes.find(
+ (episode) =>
+ episode.number === parseInt(e.target.value)
+ );
+ router.push(
+ `/id/anime/watch/${selectedEpisode.id}/${data.id}`
+ );
+ }}
+ >
+ {data.episodes.map((episode) => (
+ <option
+ key={episode.number}
+ value={episode.number}
+ >
+ Episode {episode.number}
+ </option>
+ ))}
+ </select>
+ <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
+ </div>
+ <button
+ className={`${
+ item.number === data.episodes.length
+ ? "pointer-events-none"
+ : ""
+ } relative group`}
+ onClick={() => {
+ const currentEpisodeIndex =
+ data.episodes.findIndex(
+ (episode) => episode.number === item.number
+ );
+ if (
+ currentEpisodeIndex !== -1 &&
+ currentEpisodeIndex < data.episodes.length - 1
+ ) {
+ const nextEpisode =
+ data.episodes[currentEpisodeIndex + 1];
+ router.push(
+ `/id/anime/watch/${nextEpisode.id}/${data.id}`
+ );
+ }
+ }}
+ >
+ <span className="absolute z-[9999] -left-11 -top-14 p-2 shadow-xl rounded-md transform transition-all whitespace-nowrap bg-secondary lg:group-hover:block group-hover:opacity-1 hidden font-karla font-bold">
+ Next Episode
+ </span>
+ <ForwardIcon className="w-6 h-6" />
+ </button>
+ </div>
+ </div>
+ ))
+ ) : (
+ <div className="p-3 grid gap-2">
+ <div className="text-xl font-outfit font-semibold line-clamp-2">
+ <div className="inline hover:underline">
+ <Skeleton width={240} />
+ </div>
+ </div>
+ <h4 className="text-sm font-karla font-light">
+ <Skeleton width={75} />
+ </h4>
+ </div>
+ )}
+ <div className="h-[1px] bg-[#3b3b3b]" />
+
+ <div className="px-4 pt-7 pb-4 h-full flex">
+ <div className="aspect-[9/13] h-[240px]">
+ {data ? (
+ <Image
+ src={data.image}
+ alt="Anime Cover"
+ width={1000}
+ height={1000}
+ priority
+ className="object-cover aspect-[9/13] h-[240px] rounded-md"
+ />
+ ) : (
+ <Skeleton height={240} />
+ )}
+ </div>
+ <div className="grid w-full px-5 gap-3 h-[240px]">
+ <div className="grid grid-cols-2 gap-1 items-center">
+ <h2 className="text-sm font-light font-roboto text-[#878787]">
+ Studios
+ </h2>
+ <div className="row-start-2">
+ {data ? data.studios : <Skeleton width={80} />}
+ </div>
+ <div className="hidden xxs:grid col-start-2 place-content-end relative">
+ <div>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth={1.5}
+ stroke="currentColor"
+ className="w-8 h-8 hover:fill-white hover:cursor-pointer"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z"
+ />
+ </svg>
+ </div>
+ </div>
+ </div>
+ <div className="grid gap-1 items-center">
+ <h2 className="text-sm font-light font-roboto text-[#878787]">
+ Status
+ </h2>
+ <div>{data ? data.status : <Skeleton width={75} />}</div>
+ </div>
+ <div className="grid gap-1 items-center overflow-y-hidden">
+ <h2 className="text-sm font-light font-roboto text-[#878787]">
+ Titles
+ </h2>
+ <div className="grid grid-flow-dense grid-cols-2 gap-2 h-full w-full">
+ {data ? (
+ <>
+ <div className="line-clamp-3">
+ {data.title.romaji || ""}
+ </div>
+ <div className="line-clamp-3">
+ {data.title.english || ""}
+ </div>
+ <div className="line-clamp-3">
+ {data.title.native || ""}
+ </div>
+ </>
+ ) : (
+ <Skeleton width={200} height={50} />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="flex flex-wrap gap-3 px-4 pt-3">
+ {data &&
+ data.genres.map((item, index) => (
+ <div
+ key={index}
+ className="border border-action text-gray-100 py-1 px-2 rounded-md font-karla text-sm"
+ >
+ {item}
+ </div>
+ ))}
+ </div>
+ <div className={`bg-secondary rounded-md mt-3 mx-3`}>
+ {data && (
+ <p
+ dangerouslySetInnerHTML={{ __html: data.description }}
+ className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ <div className="flex flex-col w-screen lg:w-[35%] ">
+ <h1 className="text-xl font-karla pl-4 pb-5 font-semibold">
+ Up Next
+ </h1>
+ <div className="flex flex-col gap-5 lg:pl-5 px-2 py-2 scrollbar-thin scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full">
+ {data && data?.episodes.length > 0 ? (
+ episode.map((item, index) => {
+ return (
+ <Link
+ href={`/id/anime/watch/${data.id}/${item.episodeId}`}
+ key={item.id}
+ className={`bg-secondary flex-center w-full h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${
+ index === currentNumber - 1
+ ? "pointer-events-none ring-1 ring-action text-[#5d5d5d]"
+ : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white"
+ }`}
+ >
+ Episode {index + 1}
+ </Link>
+ );
+ })
+ ) : (
+ <>
+ {[1].map((item) => (
+ <Skeleton
+ key={item}
+ className="bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out"
+ />
+ ))}
+ </>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </SkeletonTheme>
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ dotenv.config();
+
+ const API_URI = process.env.API_URI;
+
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ const proxy = process.env.PROXY_URI;
+
+ const { info } = context.query;
+ if (!info) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const id = info[0];
+ const aniId = [info[1], info[2], info[3]];
+
+ return {
+ props: {
+ sessions: session,
+ id,
+ aniId: aniId.join("/"),
+ proxy,
+ api: API_URI,
+ },
+ };
+}
diff --git a/pages/id/contact.js b/pages/id/contact.js
new file mode 100644
index 0000000..400a9e8
--- /dev/null
+++ b/pages/id/contact.js
@@ -0,0 +1,19 @@
+import Layout from "../../components/layout";
+
+const Contact = () => {
+ return (
+ <Layout className="">
+ <div className=" flex h-screen w-screen flex-col items-center justify-center font-karla font-bold">
+ <h1>Contact Us</h1>
+ <p>If you have any questions or comments, please email us at:</p>
+ <p>
+ <a href="mailto:[email protected]?subject=[Moopa]%20-%20Your%20Subject">
+ </a>
+ </p>
+ </div>
+ </Layout>
+ );
+};
+
+export default Contact;
diff --git a/pages/id/dmca.js b/pages/id/dmca.js
new file mode 100644
index 0000000..8dad7d7
--- /dev/null
+++ b/pages/id/dmca.js
@@ -0,0 +1,109 @@
+import Head from "next/head";
+import Layout from "../../components/layout";
+
+export default function DMCA() {
+ return (
+ <>
+ <Head>
+ <title>Moopa - DMCA</title>
+ <meta name="DMCA" content="DMCA" />
+ <meta property="og:title" content="DMCA" />
+ <meta
+ property="og:description"
+ content="Moopa.live is committed to respecting the intellectual
+ property rights of others and complying with the Digital
+ Millennium Copyright Act (DMCA)."
+ />
+ <meta
+ property="og:image"
+ content="https://cdn.discordapp.com/attachments/1068758633464201268/1081591948705546330/logo.png"
+ />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="/c.svg" />
+ </Head>
+ <Layout>
+ <div className="min-h-screen z-20 flex w-screen justify-center items-center">
+ <div className="w-[75%] text-2xl gap-7 flex flex-col my-[10rem]">
+ <div className="flex">
+ <h1 className="text-4xl font-bold font-karla rounded-md bg-[#212121] p-3">
+ DMCA - Disclaimer
+ </h1>
+ </div>
+ <div className="flex flex-col gap-10">
+ <div className="flex flex-col gap-3 text-[#cdcdcd]">
+ <p>
+ Moopa.live is committed to respecting the intellectual
+ property rights of others and complying with the Digital
+ Millennium Copyright Act (DMCA). We take copyright
+ infringement seriously and will respond to notices of alleged
+ copyright infringement that comply with the DMCA and any other
+ applicable laws.
+ </p>
+ <p>
+ If you believe that any content on our website is infringing
+ upon your copyrights, please send us an email. Please allow up
+ to 2-5 business days for a response. Please note that emailing
+ your complaint to other parties such as our Internet Service
+ Provider, Hosting Provider, and other third parties will not
+ expedite your request and may result in a delayed response due
+ to the complaint not being filed properly.
+ </p>
+ </div>
+ <p className="text-white">
+ In order for us to process your complaint, please provide the
+ following information:
+ </p>
+ <div className="text-xl ml-5 text-[#cdcdcd]">
+ <ul className="flex flex-col gap-1">
+ <li>
+ · Your name, address, and telephone number. We reserve the
+ right to verify this information.
+ </li>
+ <li>
+ · Identification of the copyrighted work claimed to have
+ been infringed.
+ </li>
+ <li>
+ · The exact and complete URL link where the infringing
+ material is located.
+ </li>
+ <li>
+ · The exact and complete URL link where the infringing
+ material is located.
+ </li>
+ <li>
+ · The exact and complete URL link where the infringing
+ material is located.
+ </li>
+ <li>· Please write to us in English or Indonesian.</li>
+ </ul>
+ </div>
+ <p className="text-[#cdcdcd]">
+ Please note that anonymous or incomplete messages will not be
+ dealt with. Thank you for your understanding.
+ </p>
+ <h1 className="text-white font-karla">DISCLAIMER:</h1>
+ <p className="text-[#cdcdcd]">
+ None of the files listed on Moopa.live are hosted on our
+ servers. All links point to content hosted on third-party
+ websites. Moopa.live does not accept responsibility for content
+ hosted on third-party websites and has no involvement in the
+ downloading/uploading of movies. We only post links that are
+ available on the internet. If you believe that any content on
+ our website infringes upon your intellectual property rights and
+ you hold the copyright for that content, please report it to{" "}
+ <a
+ href="mailto:[email protected]?subject=[Moopa]%20-%20Your%20Subject"
+ className="font-semibold"
+ >
+ </a>{" "}
+ and the content will be immediately removed.
+ </p>
+ </div>
+ </div>
+ </div>
+ </Layout>
+ </>
+ );
+}
diff --git a/pages/id/index.js b/pages/id/index.js
new file mode 100644
index 0000000..1d42ce3
--- /dev/null
+++ b/pages/id/index.js
@@ -0,0 +1,633 @@
+import { aniListData } from "../../lib/anilist/AniList";
+import React, { useState, useEffect } from "react";
+import Head from "next/head";
+import Link from "next/link";
+import Footer from "../../components/footer";
+import Image from "next/image";
+import Content from "../../components/home/content";
+import { useRouter } from "next/router";
+
+import { motion } from "framer-motion";
+
+import { useSession, signIn, signOut } from "next-auth/react";
+import { useAniList } from "../../lib/anilist/useAnilist";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "../api/auth/[...nextauth]";
+import SearchBar from "../../components/searchBar";
+import Genres from "../../components/home/genres";
+import { ToastContainer, toast, cssTransition } from "react-toastify";
+
+export function Navigasi() {
+ const { data: sessions, status } = useSession();
+ const [year, setYear] = useState(new Date().getFullYear());
+ const [season, setSeason] = useState(getCurrentSeason());
+
+ const router = useRouter();
+
+ const handleFormSubmission = (inputValue) => {
+ router.push(`/id/search/${encodeURIComponent(inputValue)}`);
+ };
+
+ const handleKeyDown = async (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ const inputValue = event.target.value;
+ handleFormSubmission(inputValue);
+ }
+ };
+ return (
+ <>
+ {/* NAVBAR PC */}
+ <div className="flex items-center justify-center">
+ <div className="flex w-full items-center justify-between px-5 lg:mx-[94px]">
+ <div className="flex items-center lg:gap-16 lg:pt-7">
+ <Link
+ href="/id/"
+ className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]"
+ >
+ moopa
+ </Link>
+ <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex">
+ <li>
+ <Link
+ href={`/id/search/anime?season=${season}&seasonYear=${year}`}
+ >
+ This Season
+ </Link>
+ </li>
+ <li>
+ <Link href="/id/search/manga">Manga</Link>
+ </li>
+ <li>
+ <Link href="/id/search/anime">Anime</Link>
+ </li>
+
+ {status === "loading" ? (
+ <li>Loading...</li>
+ ) : (
+ <>
+ {!sessions && (
+ <li>
+ <button
+ onClick={() => signIn("AniListProvider")}
+ className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md"
+ >
+ Sign in
+ </button>
+ </li>
+ )}
+ {sessions && (
+ <li className="text-center">
+ <Link href={`/id/profile/${sessions?.user.name}`}>
+ My List
+ </Link>
+ </li>
+ )}
+ </>
+ )}
+ </ul>
+ </div>
+ <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-0">
+ <div className="search-box ">
+ <input
+ className="search-text"
+ type="text"
+ placeholder="Search Anime"
+ onKeyDown={handleKeyDown}
+ />
+ <div className="search-btn">
+ <i className="fas fa-search"></i>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+}
+
+export default function Home({ detail, populars, sessions }) {
+ const { media: current } = useAniList(sessions, { stats: "CURRENT" });
+ const { media: plan } = useAniList(sessions, { stats: "PLANNING" });
+
+ const [isVisible, setIsVisible] = useState(false);
+ const [list, setList] = useState(null);
+ const [planned, setPlanned] = useState(null);
+ const [greeting, setGreeting] = useState("");
+ const [onGoing, setOnGoing] = useState(null);
+
+ const [prog, setProg] = useState(null);
+
+ const popular = populars?.data;
+ const data = detail.data[0];
+
+ const handleShowClick = () => {
+ setIsVisible(true);
+ };
+
+ const handleHideClick = () => {
+ setIsVisible(false);
+ };
+
+ 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) return;
+ const getMedia =
+ current.filter((item) => item.status === "CURRENT")[0] || null;
+ const list = getMedia?.entries
+ .map(({ media }) => media)
+ .filter((media) => media);
+
+ const prog = getMedia?.entries.filter(
+ (item) => item.media.nextAiringEpisode !== null
+ );
+
+ setProg(prog);
+
+ const planned = plan?.[0]?.entries
+ .map(({ media }) => media)
+ .filter((media) => media);
+
+ const onGoing = list?.filter((item) => item.nextAiringEpisode !== null);
+ setOnGoing(onGoing);
+
+ if (list) {
+ setList(list.reverse());
+ }
+ if (planned) {
+ setPlanned(planned.reverse());
+ }
+ }
+ userData();
+ }, [sessions, current, plan]);
+
+ const blurSlide = cssTransition({
+ enter: "slide-in-blurred-right",
+ exit: "slide-out-blurred-right",
+ });
+
+ useEffect(() => {
+ function Toast() {
+ toast.warn(
+ "This site is still in development, some features may not work properly.",
+ {
+ position: "bottom-right",
+ autoClose: false,
+ hideProgressBar: true,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ theme: "dark",
+ transition: blurSlide,
+ }
+ );
+ }
+ Toast();
+ }, []);
+
+ // console.log(log);
+
+ return (
+ <>
+ <Head>
+ <title>Moopa</title>
+ <meta charSet="UTF-8"></meta>
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta
+ name="twitter:title"
+ content="Moopa - Free Anime and Manga Streaming"
+ />
+ <meta
+ 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!"
+ />
+ <meta
+ name="twitter:image"
+ content="https://cdn.discordapp.com/attachments/1084446049986420786/1093300833422168094/image.png"
+ />
+ <link rel="icon" href="/c.svg" />
+ </Head>
+
+ <ToastContainer pauseOnFocusLoss={false} style={{ width: "420px" }} />
+
+ {/* NAVBAR */}
+ <div className="z-50">
+ {!isVisible && (
+ <button
+ onClick={handleShowClick}
+ className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden"
+ id="bars"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ >
+ <path
+ fillRule="evenodd"
+ d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
+ clipRule="evenodd"
+ />
+ </svg>
+ </button>
+ )}
+ </div>
+
+ {/* Mobile Menu */}
+ <div className={`transition-all duration-150 subpixel-antialiased z-50`}>
+ {isVisible && sessions && (
+ <Link
+ href={`/profile/${sessions?.user.name}`}
+ className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]"
+ >
+ <img
+ src={sessions?.user.image.large}
+ alt="user avatar"
+ className="object-cover w-[60px] h-[60px] rounded-full"
+ />
+ </Link>
+ )}
+ {isVisible && (
+ <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden">
+ <div className="grid grid-cols-4 place-items-center gap-6">
+ <button className="group flex flex-col items-center">
+ <Link href="/id/" className="">
+ <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="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
+ />
+ </svg>
+ </Link>
+ <Link
+ href="/id/"
+ className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
+ >
+ home
+ </Link>
+ </button>
+ <button className="group flex flex-col items-center">
+ <Link href="/id/about">
+ <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="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
+ />
+ </svg>
+ </Link>
+ <Link
+ href="/id/about"
+ className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
+ >
+ about
+ </Link>
+ </button>
+ <button className="group flex gap-[1.5px] flex-col items-center ">
+ <div>
+ <Link href="/id/search/anime">
+ <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="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
+ />
+ </svg>
+ </Link>
+ </div>
+ <Link
+ href="/id/search/anime"
+ className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
+ >
+ search
+ </Link>
+ </button>
+ {sessions ? (
+ <button
+ onClick={() => signOut("AniListProvider")}
+ className="group flex gap-[1.5px] flex-col items-center "
+ >
+ <div>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 96 960 960"
+ className="group-hover:fill-action w-6 h-6 fill-txt"
+ >
+ <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path>
+ </svg>
+ </div>
+ <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
+ logout
+ </h1>
+ </button>
+ ) : (
+ <button
+ onClick={() => signIn("AniListProvider")}
+ className="group flex gap-[1.5px] flex-col items-center "
+ >
+ <div>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 96 960 960"
+ className="group-hover:fill-action w-6 h-6 fill-txt mr-2"
+ >
+ <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path>
+ </svg>
+ </div>
+ <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
+ login
+ </h1>
+ </button>
+ )}
+ </div>
+ <button onClick={handleHideClick}>
+ <svg
+ width="20"
+ height="21"
+ className="fill-orange-500"
+ viewBox="0 0 20 21"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <rect
+ x="2.44043"
+ y="0.941467"
+ width="23.5842"
+ height="3.45134"
+ rx="1.72567"
+ transform="rotate(45 2.44043 0.941467)"
+ />
+ <rect
+ x="19.1172"
+ y="3.38196"
+ width="23.5842"
+ height="3.45134"
+ rx="1.72567"
+ transform="rotate(135 19.1172 3.38196)"
+ />
+ </svg>
+ </button>
+ </div>
+ )}
+ </div>
+
+ <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] ">
+ <Navigasi />
+ <SearchBar />
+ {/* PC / TABLET */}
+ <div className=" hidden justify-center lg:flex my-16">
+ <div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between">
+ <div className="row-start-2 flex h-full flex-col gap-7 lg:w-[55%] lg:justify-center">
+ <h1 className="w-[85%] font-outfit font-extrabold lg:text-[34px] line-clamp-2">
+ {data.title.english || data.title.romaji || data.title.native}
+ </h1>
+ <p
+ className="font-roboto font-light lg:text-[18px] line-clamp-5"
+ dangerouslySetInnerHTML={{ __html: data?.description }}
+ />
+
+ <div className="lg:pt-5">
+ <Link
+ href={`/id/anime/${data.id}`}
+ legacyBehavior
+ className="flex"
+ >
+ <a className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]">
+ START WATCHING
+ </a>
+ </Link>
+ </div>
+ </div>
+ <div className="z-10 row-start-1 flex justify-center ">
+ <div className="relative lg:h-[467px] lg:w-[322px] lg:scale-100">
+ <div className="absolute bg-gradient-to-t from-[#141519] to-transparent lg:h-[467px] lg:w-[322px]" />
+
+ <Image
+ draggable={false}
+ src={data.coverImage?.extraLarge || data.image}
+ alt={`alt for ${data.title.english || data.title.romaji}`}
+ width={460}
+ height={662}
+ priority
+ className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]"
+ />
+ </div>
+ </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>
+ <button
+ onClick={() => signOut()}
+ className="hidden text-center relative lg:flex justify-center group"
+ >
+ {sessions?.user.name}
+ <span className="absolute text-sm z-50 w-20 text-center bottom-11 text-white shadow-lg opacity-0 bg-secondary p-1 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all">
+ Sign Out
+ </span>
+ </button>
+ </div>
+ </div>
+ )}
+
+ <div className="lg:mt-16 mt-5 flex flex-col items-center">
+ <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
+ >
+ {sessions && onGoing?.length > 0 && (
+ <motion.div // Add motion.div to each child component
+ key="onGoing"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="onGoing"
+ section="On-Going Anime"
+ data={onGoing}
+ og={prog}
+ />
+ </motion.div>
+ )}
+
+ {sessions && list?.length > 0 && (
+ <motion.div // Add motion.div to each child component
+ key="listAnime"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="listAnime"
+ section="Your Watch List"
+ data={list}
+ />
+ </motion.div>
+ )}
+
+ {/* SECTION 2 */}
+ {sessions && planned?.length > 0 && (
+ <motion.div // Add motion.div to each child component
+ key="plannedAnime"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="plannedAnime"
+ section="Your Plan"
+ data={planned}
+ />
+ </motion.div>
+ )}
+
+ {/* SECTION 3 */}
+ {detail && (
+ <motion.div // Add motion.div to each child component
+ key="trendingAnime"
+ initial={{ y: 20, opacity: 0 }}
+ transition={{ duration: 0.5 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="trendingAnime"
+ section="Trending Now"
+ data={detail.data}
+ />
+ </motion.div>
+ )}
+
+ {/* SECTION 4 */}
+ {popular && (
+ <motion.div // Add motion.div to each child component
+ key="popularAnime"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="popularAnime"
+ section="Popular Anime"
+ data={popular}
+ />
+ </motion.div>
+ )}
+
+ <motion.div // Add motion.div to each child component
+ key="Genres"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Genres />
+ </motion.div>
+ </motion.div>
+ </div>
+ </div>
+ <Footer />
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+
+ 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 });
+
+ return {
+ props: {
+ genre: genreDetail.props,
+ detail: trendingDetail.props,
+ populars: popularDetail.props,
+ sessions: session,
+ },
+ };
+}
+
+function getCurrentSeason() {
+ const now = new Date();
+ const month = now.getMonth() + 1; // getMonth() returns 0-based index
+
+ switch (month) {
+ case 12:
+ case 1:
+ case 2:
+ return "WINTER";
+ case 3:
+ case 4:
+ case 5:
+ return "SPRING";
+ case 6:
+ case 7:
+ case 8:
+ return "SUMMER";
+ case 9:
+ case 10:
+ case 11:
+ return "FALL";
+ default:
+ return "UNKNOWN SEASON";
+ }
+}
diff --git a/pages/id/profile/[user].js b/pages/id/profile/[user].js
new file mode 100644
index 0000000..6bc804e
--- /dev/null
+++ b/pages/id/profile/[user].js
@@ -0,0 +1,423 @@
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../api/auth/[...nextauth]";
+import Navbar from "../../../components/navbar";
+import Image from "next/image";
+import Link from "next/link";
+import Head from "next/head";
+import { useState } from "react";
+
+export default function MyList({ media, sessions, user, time }) {
+ const [listFilter, setListFilter] = useState("all");
+ const [visible, setVisible] = useState(false);
+
+ const filterMedia = (status) => {
+ if (status === "all") {
+ return media;
+ }
+ return media.filter((m) => m.name === status);
+ };
+ return (
+ <>
+ <Head>
+ <title>My Lists</title>
+ </Head>
+ <Navbar />
+ <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">
+ <Image
+ src={user.avatar.large}
+ alt="user avatar"
+ width={1000}
+ height={1000}
+ className="object-cover h-28 w-28 rounded-lg"
+ />
+ {user.bannerImage ? (
+ <Image
+ src={user.bannerImage}
+ alt="image"
+ width={1000}
+ height={1000}
+ priority
+ className="absolute w-screen h-[240px] object-cover -top-[7.75rem] left-0 -z-50 brightness-[65%]"
+ />
+ ) : (
+ <div className="absolute w-screen h-[240px] object-cover -top-[7.75rem] left-0 -z-50 brightness-[65%] bg-image" />
+ )}
+ <h1 className="font-karla font-bold text-2xl pt-7">{user.name}</h1>
+ </div>
+ <div className="flex items-center justify-between">
+ <div className="flex gap-2 text-sm font-karla">
+ 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"
+ >
+ <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 className="bg-secondary lg:min-h-[160px] text-xs rounded-md p-4 font-karla">
+ <div>
+ {user.about ? (
+ <div dangerouslySetInnerHTML={{ __html: user.about }} />
+ ) : (
+ "No description created."
+ )}
+ </div>
+ </div>
+
+ <div className="bg-secondary font-karla rounded-md h-20 p-1 grid grid-cols-3 place-items-center text-center text-txt">
+ <div>
+ <h1 className="text-action font-bold">
+ {user.statistics.anime.episodesWatched}
+ </h1>
+ <h2 className="text-sm">Total Episodes</h2>
+ </div>
+ <div>
+ <h1 className="text-action font-bold">
+ {user.statistics.anime.count}
+ </h1>
+ <h2 className="text-sm">Total Anime</h2>
+ </div>
+ {time?.days ? (
+ <div>
+ <h1 className="text-action font-bold">{time.days}</h1>
+ <h2 className="text-sm">Days Watched</h2>
+ </div>
+ ) : (
+ <div>
+ <h1 className="text-action font-bold">{time.hours}</h1>
+ <h2 className="text-sm">hours</h2>
+ </div>
+ )}
+ </div>
+ {media.length !== 0 && (
+ <div className="font-karla grid gap-4">
+ <div className="flex md:justify-normal justify-between items-center">
+ <div className="flex items-center gap-3">
+ <h1>Lists Filter</h1>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth={1.5}
+ stroke="currentColor"
+ className="w-[20px] h-[20px]"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"
+ />
+ </svg>
+ </div>
+ <div
+ className="md:hidden bg-secondary p-1 rounded-md cursor-pointer"
+ onClick={() => setVisible(!visible)}
+ >
+ <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"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M6.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM18.75 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
+ />
+ </svg>
+ </div>
+ </div>
+ <ul
+ className={`group md:grid gap-1 text-sm ${
+ visible ? "" : "hidden"
+ }`}
+ >
+ <li
+ onClick={() => setListFilter("all")}
+ className={`p-2 cursor-pointer hover:text-action ${
+ listFilter === "all" && "bg-secondary text-action"
+ }`}
+ >
+ <h1 className={`cursor-pointer hover:text-action`}>
+ Show All
+ </h1>
+ </li>
+ {media.map((item) => (
+ <li
+ key={item.name}
+ onClick={() => setListFilter(item.name)}
+ className={`cursor-pointer hover:text-action flex gap-2 p-2 duration-200 ${
+ item.name === listFilter && "bg-secondary text-action"
+ }`}
+ >
+ <h1 className="">{item.name}</h1>
+ <div className="text-gray-400 opacity-0 invisible duration-200 transition-all group-hover:visible group-hover:opacity-100">
+ ({item.entries.length})
+ </div>
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+ </div>
+
+ <div className="lg:w-[75%] grid gap-10 my-12 lg:pt-16">
+ {media.length !== 0 ? (
+ filterMedia(listFilter).map((item, index) => {
+ return (
+ <div key={index} className="flex flex-col gap-5 mx-3">
+ <h1 className="font-karla font-bold text-xl">{item.name}</h1>
+ <table className="bg-secondary rounded-lg">
+ <thead>
+ <tr>
+ <th className="font-bold text-xs py-3 text-start pl-10 lg:w-[75%] w-[65%]">
+ Title
+ </th>
+ <th className="font-bold text-xs py-3">Score</th>
+ <th className="font-bold text-xs py-3">Progress</th>
+ </tr>
+ </thead>
+ <tbody className="">
+ {item.entries.map((item) => {
+ return (
+ <tr
+ key={item.mediaId}
+ className="hover:bg-orange-400 duration-150 ease-in-out group relative"
+ >
+ <td className="font-medium py-2 pl-2 rounded-l-lg">
+ <div className="flex items-center gap-2">
+ {item.media.status === "RELEASING" ? (
+ <span className="dot group-hover:invisible bg-green-500 shrink-0" />
+ ) : item.media.status === "NOT_YET_RELEASED" ? (
+ <span className="dot group-hover:invisible bg-red-500 shrink-0" />
+ ) : (
+ <span className="dot group-hover:invisible shrink-0" />
+ )}
+ <Image
+ src={item.media.coverImage.large}
+ alt="Cover Image"
+ width={500}
+ height={500}
+ className="object-cover rounded-md w-10 h-10 shrink-0"
+ />
+ <div className="absolute -top-10 -left-40 invisible lg:group-hover:visible">
+ <Image
+ src={item.media.coverImage.large}
+ alt={item.media.id}
+ width={1000}
+ height={1000}
+ className="object-cover h-[186px] w-[140px] shrink-0 rounded-md"
+ />
+ </div>
+ <Link
+ href={`/en/anime/${item.media.id}`}
+ className="font-semibold font-karla pl-2 text-sm line-clamp-1"
+ title={item.media.title.romaji}
+ >
+ {item.media.title.romaji}
+ </Link>
+ </div>
+ </td>
+ <td className="text-center text-xs text-txt">
+ {item.score === 0 ? null : item.score}
+ </td>
+ <td className="text-center text-xs text-txt rounded-r-lg">
+ {item.progress === item.media.episodes
+ ? item.progress
+ : item.media.episodes === null
+ ? item.progress
+ : `${item.progress}/${item.media.episodes}`}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+ })
+ ) : (
+ <div className="w-screen lg:w-full flex-center flex-col gap-5">
+ {user.name === sessions?.user.name ? (
+ <p className="text-center font-karla font-bold lg:text-lg">
+ Oops!<br></br> Looks like you haven't watch anything yet.
+ </p>
+ ) : (
+ <p className="text-center font-karla font-bold lg:text-lg">
+ Oops!<br></br> It looks like this user haven't watch anything
+ yet.
+ </p>
+ )}
+ <Link
+ href="/en/search/anime"
+ className="flex gap-2 text-sm ring-1 ring-action p-2 rounded-lg font-karla"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ strokeWidth={1.5}
+ stroke="currentColor"
+ className="w-5 h-5"
+ >
+ <path
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
+ />
+ </svg>
+ <span>Start Watching</span>
+ </Link>
+ </div>
+ )}
+ </div>
+ </div>
+ </>
+ );
+}
+
+export async function getServerSideProps(context) {
+ const session = await getServerSession(context.req, context.res, authOptions);
+ const query = context.query;
+
+ const response = await fetch("https://graphql.anilist.co/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ query: `
+ query ($username: String, $status: MediaListStatus) {
+ MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) {
+ user {
+ id
+ name
+ about (asHtml: true)
+ createdAt
+ avatar {
+ large
+ }
+ statistics {
+ anime {
+ count
+ episodesWatched
+ meanScore
+ minutesWatched
+ }
+ }
+ bannerImage
+ mediaListOptions {
+ animeList {
+ sectionOrder
+ }
+ }
+ }
+ lists {
+ status
+ name
+ entries {
+ id
+ mediaId
+ status
+ progress
+ score
+ media {
+ id
+ status
+ title {
+ english
+ romaji
+ }
+ episodes
+ coverImage {
+ large
+ }
+ }
+ }
+ }
+ }
+ }
+ `,
+ variables: {
+ username: query.user,
+ },
+ }),
+ });
+
+ const data = await response.json();
+
+ const get = data.data.MediaListCollection;
+ const sectionOrder = get?.user.mediaListOptions.animeList.sectionOrder;
+
+ if (!sectionOrder) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const prog = get.lists;
+
+ function getIndex(status) {
+ const index = sectionOrder.indexOf(status);
+ return index === -1 ? sectionOrder.length : index;
+ }
+
+ prog.sort((a, b) => getIndex(a.name) - getIndex(b.name));
+
+ const user = get.user;
+
+ const time = convertMinutesToDays(user.statistics.anime.minutesWatched);
+
+ return {
+ props: {
+ media: prog,
+ sessions: session,
+ user: user,
+ time: time,
+ },
+ };
+}
+
+function UnixTimeConverter({ unixTime }) {
+ const date = new Date(unixTime * 1000); // multiply by 1000 to convert to milliseconds
+ const formattedDate = date.toISOString().slice(0, 10); // format date to YYYY-MM-DD
+
+ return <p>{formattedDate}</p>;
+}
+
+function convertMinutesToDays(minutes) {
+ const hours = minutes / 60;
+ const days = hours / 24;
+
+ if (days >= 1) {
+ return days % 1 === 0
+ ? { days: `${parseInt(days)}` }
+ : { days: `${days.toFixed(1)}` };
+ } else {
+ return hours % 1 === 0
+ ? { hours: `${parseInt(hours)}` }
+ : { hours: `${hours.toFixed(1)}` };
+ }
+}
diff --git a/pages/search/[param].js b/pages/id/search/[param].js
index 130ca03..00fab64 100644
--- a/pages/search/[param].js
+++ b/pages/id/search/[param].js
@@ -1,16 +1,15 @@
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion as m } from "framer-motion";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
-import "react-loading-skeleton/dist/skeleton.css";
import { useRouter } from "next/router";
import Link from "next/link";
-import Navbar from "../../components/navbar";
+import Navbar from "../../../components/navbar";
import Head from "next/head";
-import Footer from "../../components/footer";
+import Footer from "../../../components/footer";
-import { useAniList } from "../../lib/useAnilist";
import Image from "next/image";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
+import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch";
const genre = [
"Action",
@@ -48,8 +47,6 @@ const sorts = [
export default function Card() {
const router = useRouter();
- const { aniAdvanceSearch } = useAniList();
-
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
@@ -430,7 +427,7 @@ export default function Card() {
href={
anime.format === "MANGA" || anime.format === "NOVEL"
? `/manga/detail/id?aniId=${anime.id}&aniTitle=${anime.title.userPreferred}`
- : `/anime/${anime.id}`
+ : `/en/anime/${anime.id}`
}
className=""
>
@@ -442,7 +439,7 @@ export default function Card() {
height={500}
/>
</Link>
- <Link href={`/anime/${anime.id}`}>
+ <Link href={`/en/anime/${anime.id}`}>
<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" />
diff --git a/pages/index.js b/pages/index.js
index 1c65970..6f020fb 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -1,606 +1,32 @@
-import { aniListData } from "../lib/AniList";
-import React, { useState, useEffect } from "react";
-import Head from "next/head";
-import Link from "next/link";
-import Footer from "../components/footer";
-import Image from "next/image";
-import Content from "../components/hero/content";
-import { useRouter } from "next/router";
+import { parseCookies } from "nookies";
-import { motion } from "framer-motion";
-
-import { useSession, signIn, signOut } from "next-auth/react";
-import { useAniList } from "../lib/useAnilist";
-import { getServerSession } from "next-auth/next";
-import { authOptions } from "./api/auth/[...nextauth]";
-import SearchBar from "../components/searchBar";
-import Genres from "../components/hero/genres";
-
-export function Navigasi() {
- const { data: sessions, status } = useSession();
- const [year, setYear] = useState(new Date().getFullYear());
- const [season, setSeason] = useState(getCurrentSeason());
-
- const router = useRouter();
-
- const handleFormSubmission = (inputValue) => {
- router.push(`/search/${encodeURIComponent(inputValue)}`);
- };
-
- const handleKeyDown = async (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- const inputValue = event.target.value;
- handleFormSubmission(inputValue);
- }
- };
- return (
- <>
- {/* NAVBAR PC */}
- <div className="flex items-center justify-center">
- <div className="flex w-full items-center justify-between px-5 lg:mx-[94px]">
- <div className="flex items-center lg:gap-16 lg:pt-7">
- <Link
- href="/"
- className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]"
- >
- moopa
- </Link>
- <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex">
- <li>
- <Link
- href={`/search/anime?season=${season}&seasonYear=${year}`}
- >
- This Season
- </Link>
- </li>
- <li>
- <Link href="/search/manga">Manga</Link>
- </li>
- <li>
- <Link href="/search/anime">Anime</Link>
- </li>
-
- {status === "loading" ? (
- <li>Loading...</li>
- ) : (
- <>
- {!sessions && (
- <li>
- <button
- onClick={() => signIn("AniListProvider")}
- className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md"
- >
- Sign in
- </button>
- </li>
- )}
- {sessions && (
- <li className="text-center">
- <Link href={`/profile/${sessions?.user.name}`}>
- My List
- </Link>
- </li>
- )}
- </>
- )}
- </ul>
- </div>
- <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-0">
- <div className="search-box ">
- <input
- className="search-text"
- type="text"
- placeholder="Search Anime"
- onKeyDown={handleKeyDown}
- />
- <div className="search-btn">
- <i className="fas fa-search"></i>
- </div>
- </div>
- </div>
- </div>
- </div>
- </>
- );
-}
-
-export default function Home({ detail, populars, sessions }) {
- const { media: current } = useAniList(sessions, { stats: "CURRENT" });
- const { media: plan } = useAniList(sessions, { stats: "PLANNING" });
-
- const [isVisible, setIsVisible] = useState(false);
- const [list, setList] = useState(null);
- const [planned, setPlanned] = useState(null);
- const [greeting, setGreeting] = useState("");
- const [onGoing, setOnGoing] = useState(null);
-
- const [prog, setProg] = useState(null);
-
- const popular = populars?.data;
- const data = detail.data[0];
-
- const handleShowClick = () => {
- setIsVisible(true);
- };
-
- const handleHideClick = () => {
- setIsVisible(false);
- };
-
- 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) return;
- const getMedia =
- current.filter((item) => item.status === "CURRENT")[0] || null;
- const list = getMedia?.entries
- .map(({ media }) => media)
- .filter((media) => media);
-
- const prog = getMedia?.entries.filter(
- (item) => item.media.nextAiringEpisode !== null
- );
-
- setProg(prog);
-
- const planned = plan?.[0]?.entries
- .map(({ media }) => media)
- .filter((media) => media);
-
- const onGoing = list?.filter((item) => item.nextAiringEpisode !== null);
- setOnGoing(onGoing);
-
- if (list) {
- setList(list.reverse());
- }
- if (planned) {
- setPlanned(planned.reverse());
- }
- }
- userData();
- }, [sessions, current, plan]);
-
- // console.log(log);
-
- return (
- <>
- <Head>
- <title>Moopa</title>
- <meta charSet="UTF-8"></meta>
- <meta name="twitter:card" content="summary_large_image" />
- <meta
- name="twitter:title"
- content="Moopa - Free Anime and Manga Streaming"
- />
- <meta
- 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!"
- />
- <meta
- name="twitter:image"
- content="https://cdn.discordapp.com/attachments/1084446049986420786/1093300833422168094/image.png"
- />
- <link rel="icon" href="/c.svg" />
- </Head>
-
- {/* NAVBAR */}
- <div className="z-50">
- {!isVisible && (
- <button
- onClick={handleShowClick}
- className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden"
- id="bars"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500"
- viewBox="0 0 20 20"
- fill="currentColor"
- >
- <path
- fillRule="evenodd"
- d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
- clipRule="evenodd"
- />
- </svg>
- </button>
- )}
- </div>
-
- {/* Mobile Menu */}
- <div className={`transition-all duration-150 subpixel-antialiased z-50`}>
- {isVisible && sessions && (
- <Link
- href={`/profile/${sessions?.user.name}`}
- className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]"
- >
- <img
- src={sessions?.user.image.large}
- alt="user avatar"
- className="object-cover w-[60px] h-[60px] rounded-full"
- />
- </Link>
- )}
- {isVisible && (
- <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden">
- <div className="grid grid-cols-4 place-items-center gap-6">
- <button className="group flex flex-col items-center">
- <Link href="/" className="">
- <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="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
- />
- </svg>
- </Link>
- <Link
- href="/"
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- home
- </Link>
- </button>
- <button className="group flex flex-col items-center">
- <Link href="/about">
- <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="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
- />
- </svg>
- </Link>
- <Link
- href="/about"
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- about
- </Link>
- </button>
- <button className="group flex gap-[1.5px] flex-col items-center ">
- <div>
- <Link href="/search/anime">
- <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="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
- />
- </svg>
- </Link>
- </div>
- <Link
- href="/search/anime"
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- search
- </Link>
- </button>
- {sessions ? (
- <button
- onClick={() => signOut("AniListProvider")}
- className="group flex gap-[1.5px] flex-col items-center "
- >
- <div>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 96 960 960"
- className="group-hover:fill-action w-6 h-6 fill-txt"
- >
- <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path>
- </svg>
- </div>
- <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
- logout
- </h1>
- </button>
- ) : (
- <button
- onClick={() => signIn("AniListProvider")}
- className="group flex gap-[1.5px] flex-col items-center "
- >
- <div>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 96 960 960"
- className="group-hover:fill-action w-6 h-6 fill-txt mr-2"
- >
- <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path>
- </svg>
- </div>
- <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
- login
- </h1>
- </button>
- )}
- </div>
- <button onClick={handleHideClick}>
- <svg
- width="20"
- height="21"
- className="fill-orange-500"
- viewBox="0 0 20 21"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <rect
- x="2.44043"
- y="0.941467"
- width="23.5842"
- height="3.45134"
- rx="1.72567"
- transform="rotate(45 2.44043 0.941467)"
- />
- <rect
- x="19.1172"
- y="3.38196"
- width="23.5842"
- height="3.45134"
- rx="1.72567"
- transform="rotate(135 19.1172 3.38196)"
- />
- </svg>
- </button>
- </div>
- )}
- </div>
-
- <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd] ">
- <Navigasi />
- <SearchBar />
- {/* PC / TABLET */}
- <div className=" hidden justify-center lg:flex my-16">
- <div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between">
- <div className="row-start-2 flex h-full flex-col gap-7 lg:w-[55%] lg:justify-center">
- <h1 className="w-[85%] font-outfit font-extrabold lg:text-[34px] line-clamp-2">
- {data.title.english || data.title.romaji || data.title.native}
- </h1>
- <p
- className="font-roboto font-light lg:text-[18px] line-clamp-5"
- dangerouslySetInnerHTML={{ __html: data?.description }}
- />
-
- <div className="lg:pt-5">
- <Link
- href={`/anime/${data.id}`}
- legacyBehavior
- className="flex"
- >
- <a className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]">
- START WATCHING
- </a>
- </Link>
- </div>
- </div>
- <div className="z-10 row-start-1 flex justify-center ">
- <div className="relative lg:h-[467px] lg:w-[322px] lg:scale-100">
- <div className="absolute bg-gradient-to-t from-[#141519] to-transparent lg:h-[467px] lg:w-[322px]" />
-
- <Image
- draggable={false}
- src={data.coverImage?.extraLarge || data.image}
- alt={`alt for ${data.title.english || data.title.romaji}`}
- width={460}
- height={662}
- priority
- className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]"
- />
- </div>
- </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>
- <button
- onClick={() => signOut()}
- className="hidden text-center relative lg:flex justify-center group"
- >
- {sessions?.user.name}
- <span className="absolute text-sm z-50 w-20 text-center bottom-11 text-white shadow-lg opacity-0 bg-secondary p-1 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all">
- Sign Out
- </span>
- </button>
- </div>
- </div>
- )}
-
- <div className="lg:mt-16 mt-5 flex flex-col items-center">
- <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
- >
- {sessions && onGoing && (
- <motion.div // Add motion.div to each child component
- key="onGoing"
- initial={{ y: 20, opacity: 0 }}
- whileInView={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.5 }}
- viewport={{ once: true }}
- >
- <Content
- ids="onGoing"
- section="On-Going Anime"
- data={onGoing}
- og={prog}
- />
- </motion.div>
- )}
-
- {sessions && list && (
- <motion.div // Add motion.div to each child component
- key="listAnime"
- initial={{ y: 20, opacity: 0 }}
- whileInView={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.5 }}
- viewport={{ once: true }}
- >
- <Content
- ids="listAnime"
- section="Your Watch List"
- data={list}
- />
- </motion.div>
- )}
-
- {/* SECTION 2 */}
- {sessions && planned && (
- <motion.div // Add motion.div to each child component
- key="plannedAnime"
- initial={{ y: 20, opacity: 0 }}
- whileInView={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.5 }}
- viewport={{ once: true }}
- >
- <Content
- ids="plannedAnime"
- section="Your Plan"
- data={planned}
- />
- </motion.div>
- )}
-
- {/* SECTION 3 */}
- {detail && (
- <motion.div // Add motion.div to each child component
- key="trendingAnime"
- initial={{ y: 20, opacity: 0 }}
- transition={{ duration: 0.5 }}
- whileInView={{ y: 0, opacity: 1 }}
- viewport={{ once: true }}
- >
- <Content
- ids="trendingAnime"
- section="Trending Now"
- data={detail.data}
- />
- </motion.div>
- )}
-
- {/* SECTION 4 */}
- {popular && (
- <motion.div // Add motion.div to each child component
- key="popularAnime"
- initial={{ y: 20, opacity: 0 }}
- whileInView={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.5 }}
- viewport={{ once: true }}
- >
- <Content
- ids="popularAnime"
- section="Popular Anime"
- data={popular}
- />
- </motion.div>
- )}
-
- <motion.div // Add motion.div to each child component
- key="Genres"
- initial={{ y: 20, opacity: 0 }}
- whileInView={{ y: 0, opacity: 1 }}
- transition={{ duration: 0.5 }}
- viewport={{ once: true }}
- >
- <Genres />
- </motion.div>
- </motion.div>
- </div>
- </div>
- <Footer />
- </>
- );
+export default function Home() {
+ return <></>;
}
export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
-
- 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 });
-
- return {
- props: {
- genre: genreDetail.props,
- detail: trendingDetail.props,
- populars: popularDetail.props,
- sessions: session,
- },
- };
-}
-
-function getCurrentSeason() {
- const now = new Date();
- const month = now.getMonth() + 1; // getMonth() returns 0-based index
-
- switch (month) {
- case 12:
- case 1:
- case 2:
- return "WINTER";
- case 3:
- case 4:
- case 5:
- return "SPRING";
- case 6:
- case 7:
- case 8:
- return "SUMMER";
- case 9:
- case 10:
- case 11:
- return "FALL";
- default:
- return "UNKNOWN SEASON";
+ const cookie = parseCookies(context);
+
+ if (cookie.lang === "en") {
+ return {
+ redirect: {
+ destination: "/en",
+ permanent: false,
+ },
+ };
+ } else if (cookie.lang === "id") {
+ return {
+ redirect: {
+ destination: "/id",
+ permanent: false,
+ },
+ };
+ } else {
+ return {
+ redirect: {
+ destination: "/en",
+ permanent: false,
+ },
+ };
}
}