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