diff options
Diffstat (limited to 'pages')
| -rw-r--r-- | pages/404.js | 22 | ||||
| -rw-r--r-- | pages/_app.js | 66 | ||||
| -rw-r--r-- | pages/api/auth/[...nextauth].js | 20 | ||||
| -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.js | 576 | ||||
| -rw-r--r-- | pages/en/manga/[id].js | 172 | ||||
| -rw-r--r-- | pages/en/manga/read/[...params].js | 262 | ||||
| -rw-r--r-- | pages/en/profile/[user].js (renamed from pages/profile/[user].js) | 8 | ||||
| -rw-r--r-- | pages/en/search/[param].js | 493 | ||||
| -rw-r--r-- | pages/id/about.js | 57 | ||||
| -rw-r--r-- | pages/id/anime/[...id].js | 850 | ||||
| -rw-r--r-- | pages/id/anime/watch/[...info].js | 488 | ||||
| -rw-r--r-- | pages/id/contact.js | 19 | ||||
| -rw-r--r-- | pages/id/dmca.js | 109 | ||||
| -rw-r--r-- | pages/id/index.js | 633 | ||||
| -rw-r--r-- | pages/id/profile/[user].js | 423 | ||||
| -rw-r--r-- | pages/id/search/[param].js (renamed from pages/search/[param].js) | 13 | ||||
| -rw-r--r-- | pages/index.js | 626 |
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>} ·{" "} + {anime.status || <p>-</p>} ·{" "} + {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, + }, + }; } } |