diff options
Diffstat (limited to 'pages/en')
| -rw-r--r-- | pages/en/about.tsx (renamed from pages/en/about.js) | 4 | ||||
| -rw-r--r-- | pages/en/anime/[...id].tsx (renamed from pages/en/anime/[...id].js) | 79 | ||||
| -rw-r--r-- | pages/en/anime/recent.js | 2 | ||||
| -rw-r--r-- | pages/en/anime/watch/[...info].js | 213 | ||||
| -rw-r--r-- | pages/en/contact.tsx (renamed from pages/en/contact.js) | 4 | ||||
| -rw-r--r-- | pages/en/dmca.tsx (renamed from pages/en/dmca.js) | 4 | ||||
| -rw-r--r-- | pages/en/index.tsx (renamed from pages/en/index.js) | 242 | ||||
| -rw-r--r-- | pages/en/manga/[...id].js | 427 | ||||
| -rw-r--r-- | pages/en/manga/[...id].tsx | 456 | ||||
| -rw-r--r-- | pages/en/manga/read/[...params].js | 1 | ||||
| -rw-r--r-- | pages/en/profile/[user].tsx (renamed from pages/en/profile/[user].js) | 83 | ||||
| -rw-r--r-- | pages/en/schedule/index.tsx (renamed from pages/en/schedule/index.js) | 35 | ||||
| -rw-r--r-- | pages/en/search/[...param].tsx (renamed from pages/en/search/[...param].js) | 211 |
13 files changed, 1029 insertions, 732 deletions
diff --git a/pages/en/about.js b/pages/en/about.tsx index aa0ba30..c5e9c51 100644 --- a/pages/en/about.js +++ b/pages/en/about.tsx @@ -1,7 +1,7 @@ import Head from "next/head"; import { motion } from "framer-motion"; import Link from "next/link"; -import { NewNavbar } from "@/components/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; import Footer from "@/components/shared/footer"; export default function About() { @@ -21,7 +21,7 @@ export default function About() { <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/svg/c.svg" /> </Head> - <NewNavbar withNav={true} scrollP={5} shrink={true} /> + <Navbar withNav={true} scrollP={5} shrink={true} /> <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].tsx index 25cc4d6..42cae38 100644 --- a/pages/en/anime/[...id].js +++ b/pages/en/anime/[...id].tsx @@ -16,18 +16,30 @@ import Footer from "@/components/shared/footer"; import { mediaInfoQuery } from "@/lib/graphql/query"; import MobileNav from "@/components/shared/MobileNav"; +import pls from "@/utils/request/index"; + import Characters from "@/components/anime/charactersCard"; import { redis } from "@/lib/redis"; - -export default function Info({ info, color }) { - const { data: session } = useSession(); +import { toast } from "sonner"; +import { Navbar } from "@/components/shared/NavBar"; +import { AniListInfoTypes } from "types/info/AnilistInfoTypes"; + +type InfoTypes = { + info: AniListInfoTypes; + color: string; + api: string; + chapterNotFound: string; +}; + +export default function Info({ info, color, chapterNotFound }: InfoTypes) { + const { data: session }: any = useSession(); const { getUserLists } = useAniList(session); const [loading, setLoading] = useState(false); - const [progress, setProgress] = useState(0); - const [statuses, setStatuses] = useState(null); + const [progress, setProgress] = useState<number>(0); + const [statuses, setStatuses] = useState<any>(null); const [domainUrl, setDomainUrl] = useState(""); - const [watch, setWatch] = useState(); + const [watch, setWatch] = useState<string>(); const [open, setOpen] = useState(false); const { id } = useRouter().query; @@ -37,6 +49,14 @@ export default function Info({ info, color }) { ); useEffect(() => { + if (chapterNotFound) { + toast.error("Source not found"); + const cleanUrl = window.location.origin + window.location.pathname; + window.history.replaceState(null, "", cleanUrl); + } + }, [chapterNotFound]); + + useEffect(() => { handleClose(); async function fetchData() { setLoading(true); @@ -53,7 +73,9 @@ export default function Info({ info, color }) { if (user) { setProgress(user.progress); - const statusMapping = { + const statusMapping: { + [key: string]: { name: string; value: string }; + } = { CURRENT: { name: "Watching", value: "CURRENT" }, PLANNING: { name: "Plan to watch", value: "PLANNING" }, COMPLETED: { name: "Completed", value: "COMPLETED" }, @@ -118,6 +140,7 @@ export default function Info({ info, color }) { }&image=${info.bannerImage || info.coverImage.extraLarge}`} /> </Head> + <Navbar info={info} /> <Modal open={open} onClose={() => handleClose()}> <div> {!session && ( @@ -151,7 +174,7 @@ export default function Info({ info, color }) { )} </div> </Modal> - <MobileNav sessions={session} hideProfile={true} /> + <MobileNav 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" /> @@ -169,12 +192,10 @@ export default function Info({ info, color }) { <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} + progress={progress || 0} color={color} /> @@ -188,6 +209,9 @@ export default function Info({ info, color }) { {info?.characters?.edges && ( <div className="w-full"> + {/* <div className="w-full h-[150px] bg-white flex-center text-black"> + ad banner + </div> */} <Characters info={info?.characters?.edges} /> </div> )} @@ -208,8 +232,8 @@ export default function Info({ info, color }) { ); } -export async function getServerSideProps(ctx) { - const { id } = ctx.query; +export async function getServerSideProps(ctx: any) { + const { id, notfound } = ctx.query; let API_URI; API_URI = process.env.API_URI || null || null; @@ -217,7 +241,12 @@ export async function getServerSideProps(ctx) { API_URI = API_URI.slice(0, -1); } - let cache; + let cache, chapterNotFound; + + if (notfound) { + // create random id string + chapterNotFound = Math.random().toString(36).substring(7); + } if (redis) { cache = await redis.get(`anime:${id}`); @@ -230,14 +259,15 @@ export async function getServerSideProps(ctx) { info, color, api: API_URI, + chapterNotFound: chapterNotFound || null, }, }; } else { - const resp = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, + const [resp] = await pls.post("https://graphql.anilist.co/", { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, body: JSON.stringify({ query: mediaInfoQuery, variables: { @@ -246,10 +276,10 @@ export async function getServerSideProps(ctx) { }), }); - const json = await resp.json(); - const data = json?.data?.Media; + // const json = await resp.json(); + const data = resp?.data?.Media; - const cacheTime = data.nextAiringEpisode?.episode + const cacheTime = data?.nextAiringEpisode?.episode ? 60 * 10 : 60 * 60 * 24 * 30; @@ -283,12 +313,13 @@ export async function getServerSideProps(ctx) { info: data, color: color, api: API_URI, + chapterNotFound: chapterNotFound || null, }, }; } } -function getBrightness(hexColor) { +function getBrightness(hexColor: { match: (arg0: RegExp) => any[] }) { if (!hexColor) { return 200; } @@ -299,7 +330,7 @@ function getBrightness(hexColor) { return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000; } -function setTxtColor(hexColor) { +function setTxtColor(hexColor: { match: (arg0: RegExp) => any[] }) { const brightness = getBrightness(hexColor); return brightness < 150 ? "#fff" : "#000"; } diff --git a/pages/en/anime/recent.js b/pages/en/anime/recent.js index 4a8111d..240ed1d 100644 --- a/pages/en/anime/recent.js +++ b/pages/en/anime/recent.js @@ -83,7 +83,7 @@ export default function Recent({ sessions }) { <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> + <h1 className="text-xl">Freshly Added</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"> diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js index beab366..dc1f412 100644 --- a/pages/en/anime/watch/[...info].js +++ b/pages/en/anime/watch/[...info].js @@ -1,5 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; -import PlayerComponent from "@/components/watch/player/playerComponent"; +import { useEffect, useState } from "react"; import { FlagIcon, ShareIcon } from "@heroicons/react/24/solid"; import Details from "@/components/watch/primary/details"; import EpisodeLists from "@/components/watch/secondary/episodeLists"; @@ -9,13 +8,16 @@ import { authOptions } from "../../../api/auth/[...nextauth]"; import { createList, createUser, getEpisode } from "@/prisma/user"; import Link from "next/link"; import MobileNav from "@/components/shared/MobileNav"; -import { NewNavbar } from "@/components/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; import Modal from "@/components/modal"; import AniList from "@/components/media/aniList"; import { signIn } from "next-auth/react"; import BugReportForm from "@/components/shared/bugReport"; import Skeleton from "react-loading-skeleton"; import Head from "next/head"; +import VidStack from "@/components/watch/new-player/player"; +import { useRouter } from "next/router"; +import { Spinner } from "@vidstack/react"; export async function getServerSideProps(context) { let userData = null; @@ -81,7 +83,7 @@ export async function getServerSideProps(context) { color } synonyms - + } } `, @@ -91,6 +93,8 @@ export async function getServerSideProps(context) { }), }); const data = await ress.json(); + // const variables = { id: aniId }; + // const data = await getAnilistMediaInfo(variables, context.req); try { if (session) { @@ -142,17 +146,24 @@ export default function Watch({ const [episodesList, setepisodesList] = useState(); const [mapEpisode, setMapEpisode] = useState(null); - const [episodeSource, setEpisodeSource] = useState(null); - const [open, setOpen] = useState(false); const [isOpen, setIsOpen] = useState(false); + const { setAutoNext } = useWatchProvider(); + const [onList, setOnList] = useState(false); - const { theaterMode, setPlayerState, setAutoPlay, setMarked } = - useWatchProvider(); + const router = useRouter(); - const playerRef = useRef(null); + const { + theaterMode, + setPlayerState, + setAutoPlay, + setMarked, + setTrack, + aspectRatio, + setDataMedia, + } = useWatchProvider(); useEffect(() => { async function getInfo() { @@ -160,6 +171,8 @@ export default function Watch({ setOnList(true); } + setDataMedia(info); + const response = await fetch( `/api/v2/episode/${info.id}?releasing=${ info.status === "RELEASING" ? "true" : "false" @@ -202,17 +215,18 @@ export default function Watch({ const previousEpisode = episodeList?.find( (i) => i.number === parseInt(epiNumber) - 1 ); - setEpisodeNavigation({ + const vidNav = { prev: previousEpisode, playing: { id: currentEpisode.id, - title: playingData?.title, + title: playingData?.title || info?.title?.romaji, description: playingData?.description, img: playingData?.img || playingData?.image, number: currentEpisode.number, }, next: nextEpisode, - }); + }; + setEpisodeNavigation(vidNav); } } @@ -228,12 +242,17 @@ export default function Watch({ }, [sessions?.user?.name, epiNumber, dub]); useEffect(() => { + const autoNext = localStorage.getItem("autoNext"), + autoPlay = localStorage.getItem("autoplay"); + if (autoNext) { + setAutoNext(autoNext); + } + if (autoPlay) { + setAutoPlay(autoPlay); + } + async function fetchData() { if (info) { - const autoplay = - localStorage.getItem("autoplay_video") === "true" ? true : false; - setAutoPlay(autoplay); - const anify = await fetch("/api/v2/source", { method: "POST", headers: { @@ -252,6 +271,11 @@ export default function Watch({ }), }).then((res) => res.json()); + if (!anify?.sources?.length > 0) { + router.push(`/en/anime/${info.id}?notfound=true`); + return; + } + const skip = await fetch( `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt( epiNumber @@ -267,31 +291,77 @@ export default function Watch({ return res.json(); }); - const op = - skip?.results?.find((item) => item.skipType === "op") || null; - const ed = - skip?.results?.find((item) => item.skipType === "ed") || null; + let getOp = + skip?.results?.find((item) => item.skipType === "op") || null, + getEd = skip?.results?.find((item) => item.skipType === "ed") || null; + + const op = getOp + ? { + startTime: + anify?.intro?.start ?? Math.round(getOp?.interval.startTime), + endTime: + anify?.intro?.end ?? Math.round(getOp?.interval.endTime), + text: "Opening", + } + : null, + ed = { + startTime: + anify?.outro?.start ?? Math.round(getEd?.interval.startTime), + endTime: anify?.outro?.end ?? Math.round(getEd?.interval.endTime), + text: "Ending", + }; + const skipData = [op, ed].filter((i) => i !== null); + + const quality = + anify?.sources?.find( + (i) => i.quality === "default" || i.quality === "auto" + ) || anify?.sources[0]; + + const reFormSubtitles = anify?.subtitles?.map((i) => { + return { + src: proxy + "/" + i.url, + label: i.lang, + kind: i.lang === "Thumbnails" ? "thumbnails" : "subtitles", + ...(i.lang === "English" && { default: true }), + }; + }); + + const thumbnails = reFormSubtitles?.find( + (i) => i.kind === "thumbnails" + ); + + const subtitles = reFormSubtitles?.filter( + (i) => i.kind !== "thumbnails" + ); const episode = { - epiData: anify, - skip: { - op, - ed, + provider, + isDub: dub, + defaultQuality: { + // url: quality?.url, + url: `${proxy}/proxy/m3u8/${encodeURIComponent( + String(quality?.url) + )}/${encodeURIComponent(JSON.stringify(anify?.headers))}`, + headers: anify?.headers, }, + subtitles: subtitles, + thumbnails: thumbnails?.src, + epiData: anify, + skip: skipData, }; - setEpisodeSource(episode); + setTrack(episode); } } fetchData(); return () => { - setEpisodeSource(); setPlayerState({ currentTime: 0, isPlaying: false, }); setMarked(0); + setTrack(null); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -424,7 +494,7 @@ export default function Watch({ </Modal> <BugReportForm isOpen={isOpen} setIsOpen={setIsOpen} /> <main className="w-screen h-full"> - <NewNavbar + <Navbar scrollP={20} withNav={true} shrink={true} @@ -435,21 +505,23 @@ export default function Watch({ className={`mx-auto pt-16 ${theaterMode ? "lg:pt-16" : "lg:pt-20"}`} > {theaterMode && ( - <PlayerComponent - id={"cinematic"} - session={sessions} - playerRef={playerRef} - dub={dub} - info={info} - watchId={watchId} - proxy={proxy} - track={episodeNavigation} - data={episodeSource?.epiData} - skip={episodeSource?.skip} - timeWatched={userData?.timeWatched} - provider={provider} - className="w-screen max-h-[85dvh]" - /> + <div + className={`bg-black w-full max-h-[84dvh] h-full flex-center rounded-md`} + style={{ aspectRatio: aspectRatio }} + > + {episodeNavigation ? ( + <VidStack + id={`${watchId}-theater`} + navigation={episodeNavigation} + sessions={sessions} + userData={userData} + /> + ) : ( + <div className="flex-center aspect-video w-full h-full relative"> + <SpinLoader /> + </div> + )} + </div> )} <div id="default" @@ -459,20 +531,25 @@ export default function Watch({ > <div id="primary" className="w-full"> {!theaterMode && ( - <PlayerComponent - id={"default"} - session={sessions} - playerRef={playerRef} - dub={dub} - info={info} - watchId={watchId} - proxy={proxy} - track={episodeNavigation} - data={episodeSource?.epiData} - skip={episodeSource?.skip} - timeWatched={userData?.timeWatched} - provider={provider} - /> + <div + className={`bg-black w-full flex-center rounded-md overflow-hidden ${ + aspectRatio === "4/3" ? "aspect-video" : "" + }`} + // style={{ aspectRatio: aspectRatio }} + > + {episodeNavigation ? ( + <VidStack + id={`${watchId}-default`} + navigation={episodeNavigation} + sessions={sessions} + userData={userData} + /> + ) : ( + <div className="flex-center aspect-video w-full h-full relative"> + <SpinLoader /> + </div> + )} + </div> )} <div id="details" @@ -506,7 +583,7 @@ export default function Watch({ className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden" > <ShareIcon className="w-5 h-5" /> - share + <span className="hidden lg:block">share</span> </button> <button type="button" @@ -514,11 +591,10 @@ export default function Watch({ className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden" > <FlagIcon className="w-5 h-5" /> - report + <span className="hidden lg:block">report</span> </button> </div> </div> - {/* <div>right</div> */} </div> <Details @@ -538,6 +614,11 @@ export default function Watch({ id="secondary" className={`relative ${theaterMode ? "pt-5" : "pt-4 lg:pt-0"}`} > + {/* <div className="w-full h-[150px] text-black p-3"> + <span className="bg-white w-full h-full flex-center"> + ad banner + </span> + </div> */} <EpisodeLists info={info} session={sessions} @@ -556,3 +637,17 @@ export default function Watch({ </> ); } + +function SpinLoader() { + return ( + <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center"> + <Spinner.Root + className="text-white animate-spin opacity-100" + size={84} + > + <Spinner.Track className="opacity-25" width={8} /> + <Spinner.TrackFill className="opacity-75" width={8} /> + </Spinner.Root> + </div> + ); +} diff --git a/pages/en/contact.js b/pages/en/contact.tsx index 385bdb1..9954f95 100644 --- a/pages/en/contact.js +++ b/pages/en/contact.tsx @@ -1,10 +1,10 @@ -import { NewNavbar } from "@/components/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; import Footer from "@/components/shared/footer"; const Contact = () => { return ( <> - <NewNavbar withNav={true} scrollP={5} shrink={true} /> + <Navbar withNav={true} scrollP={5} shrink={true} /> <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> diff --git a/pages/en/dmca.js b/pages/en/dmca.tsx index e559829..eba28fe 100644 --- a/pages/en/dmca.js +++ b/pages/en/dmca.tsx @@ -1,5 +1,5 @@ import MobileNav from "@/components/shared/MobileNav"; -import { NewNavbar } from "@/components/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; import Footer from "@/components/shared/footer"; import Head from "next/head"; @@ -21,7 +21,7 @@ export default function DMCA() { <link rel="icon" href="/svg/c.svg" /> </Head> <> - <NewNavbar withNav={true} scrollP={5} shrink={true} /> + <Navbar withNav={true} scrollP={5} shrink={true} /> <MobileNav hideProfile={true} /> <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.tsx index 29b0778..4141015 100644 --- a/pages/en/index.js +++ b/pages/en/index.tsx @@ -14,11 +14,11 @@ import Schedule from "@/components/home/schedule"; import getUpcomingAnime from "@/lib/anilist/getUpcomingAnime"; 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"; -import { NewNavbar } from "@/components/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; +import UserRecommendation from "@/components/home/recommendation"; export async function getServerSideProps() { let cachedData; @@ -75,12 +75,55 @@ export async function getServerSideProps() { } } -export default function Home({ detail, populars, upComing }) { - const { data: sessions } = useSession(); - const { anime: currentAnime, manga: currentManga } = GetMedia(sessions, { +type HomeProps = { + genre: any; + detail: any; + populars: any; + upComing: any; +}; + +export interface SessionTypes { + name: string; + picture: Picture; + sub: string; + token: string; + id: number; + image: Image; + list: string[]; + version: string; + iat: number; + exp: number; + jti: string; +} + +interface Picture { + large: string; + medium: string; +} + +interface Image { + large: string; + medium: string; +} + +export default function Home({ detail, populars, upComing }: HomeProps) { + const { data: sessions }: any = useSession(); + const userSession: SessionTypes = sessions?.user; + + const { + anime: currentAnime, + manga: currentManga, + recommendations, + }: { + anime: CurrentMediaTypes[]; + manga: CurrentMediaTypes[]; + recommendations: CurrentMediaTypes[]; + } = GetMedia(sessions, { stats: "CURRENT", }); - const { anime: plan } = GetMedia(sessions, { stats: "PLANNING" }); + const { anime: plan }: { anime: CurrentMediaTypes[] } = GetMedia(sessions, { + stats: "PLANNING", + }); const { anime: release } = GetMedia(sessions); const [schedules, setSchedules] = useState(null); @@ -97,12 +140,12 @@ export default function Home({ detail, populars, upComing }) { } useEffect(() => { - if (sessions?.user?.version) { - if (sessions.user.version !== "1.0.1") { - signOut("AniListProvider"); + if (userSession?.version) { + if (userSession?.version !== "1.0.1") { + signOut({ redirect: true }); } } - }, [sessions?.user?.version]); + }, [userSession?.version]); useEffect(() => { getRecent(); @@ -118,33 +161,15 @@ export default function Home({ detail, populars, upComing }) { } }, [upComing]); - // useEffect(() => { - // const getSchedule = async () => { - // try { - // const res = await fetch(`/api/v2/etc/schedule`); - // const data = await res.json(); - - // if (!res.ok) { - // setSchedules(null); - // } else { - // setSchedules(data); - // } - // } catch (err) { - // console.log(err); - // } - // }; - // getSchedule(); - // }, []); - - const [releaseData, setReleaseData] = useState([]); + const [releaseData, setReleaseData] = useState<any[]>([]); useEffect(() => { function getRelease() { - let releasingAnime = []; - let progress = []; - let seenIds = new Set(); // Create a Set to store the IDs of seen anime - release.map((list) => { - list.entries.map((entry) => { + let releasingAnime: any[] = []; + let progress: any[] = []; + let seenIds = new Set<number>(); // Create a Set to store the IDs of seen anime + (release as any[]).forEach((list: any) => { + list.entries.forEach((entry: any) => { if ( entry.media.status === "RELEASING" && !seenIds.has(entry.media.id) @@ -156,18 +181,18 @@ export default function Home({ detail, populars, upComing }) { }); }); setReleaseData(releasingAnime); - setProg(progress); + if (progress.length > 0) setProg(progress); } getRelease(); }, [release]); - const [listAnime, setListAnime] = useState(null); - const [listManga, setListManga] = useState(null); - const [planned, setPlanned] = useState(null); - const [user, setUser] = useState(null); + const [listAnime, setListAnime] = useState<any[] | null>(); + const [listManga, setListManga] = useState<any[] | null>(null); + const [planned, setPlanned] = useState<any[] | null>(null); + const [user, setUser] = useState<any[] | null>(null); const [removed, setRemoved] = useState(); - const [prog, setProg] = useState(null); + const [prog, setProg] = useState<any[] | null>(); const popular = populars?.data; const data = detail.data[0]; @@ -175,7 +200,7 @@ export default function Home({ detail, populars, upComing }) { useEffect(() => { async function userData() { try { - if (sessions?.user?.name) { + if (userSession?.name) { await fetch(`/api/user/profile`, { method: "POST", headers: { @@ -189,9 +214,9 @@ export default function Home({ detail, populars, upComing }) { } catch (error) { console.log(error); } - let data; + let data: UserDataType | null = null; try { - if (sessions?.user?.name) { + if (userSession?.name) { const res = await fetch( `/api/user/profile?name=${sessions.user.name}` ); @@ -220,17 +245,20 @@ export default function Home({ detail, populars, upComing }) { // Handle the error here } if (!data) { - const dat = JSON.parse(localStorage.getItem("artplayer_settings")); + const dat: any = localStorage.getItem("artplayer_settings"); if (dat) { - const arr = Object.keys(dat).map((key) => dat[key]); - const newFirst = arr?.sort((a, b) => { - return new Date(b?.createdAt) - new Date(a?.createdAt); + const arr = Object.keys(dat).map((key: string) => dat[key] as any); + const newFirst = arr?.sort((a: any, b: any) => { + return ( + new Date(b?.createdAt).getTime() - + new Date(a?.createdAt).getTime() + ); }); const uniqueTitles = new Set(); // Filter out duplicates and store unique entries - const filteredData = newFirst.filter((entry) => { + const filteredData = newFirst.filter((entry: any) => { if (uniqueTitles.has(entry.aniTitle)) { return false; } @@ -238,7 +266,9 @@ export default function Home({ detail, populars, upComing }) { return true; }); - setUser(filteredData); + if (filteredData) { + setUser(filteredData); + } } } else { // Create a Set to store unique aniTitles @@ -257,11 +287,11 @@ export default function Home({ detail, populars, upComing }) { // const data = await res.json(); } userData(); - }, [sessions?.user?.name, removed]); + }, [userSession?.name, removed]); useEffect(() => { async function userData() { - if (!sessions?.user?.name) return; + if (!userSession?.name) return; const getMedia = currentAnime.find((item) => item.status === "CURRENT") || null; @@ -292,9 +322,7 @@ export default function Home({ detail, populars, upComing }) { userData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessions?.user?.name, currentAnime, plan]); - - // console.log({ recentAdded }); + }, [userSession?.name, currentAnime, plan]); return ( <Fragment> @@ -304,7 +332,6 @@ export default function Home({ detail, populars, upComing }) { <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="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!" @@ -339,9 +366,9 @@ export default function Home({ detail, populars, upComing }) { /> <meta name="twitter:image" content="/preview.png" /> </Head> - <MobileNav sessions={sessions} hideProfile={true} /> + <MobileNav hideProfile={true} /> - <NewNavbar paddingY="pt-2 lg:pt-10" withNav={true} home={true} /> + <Navbar paddingY="pt-2 lg:pt-10" withNav={true} home={true} /> <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd]"> {/* PC / TABLET */} <div className=" hidden justify-center lg:flex my-16"> @@ -381,6 +408,16 @@ export default function Home({ detail, populars, upComing }) { </div> </div> </div> + {/* <div className="relative w-screen h-screen overflow-hidden"> + <iframe + width="560" + height="315" + src="https://www.youtube.com/embed/VVfdqw-qvNE?autoplay=1&controls=0&rel=0&mute=1" + frameborder="0" + allowfullscreen + className="absolute w-screen h-screen top-0 scale-[115%] left-0 z-0" + /> + </div> */} {sessions && ( <div className="flex items-center justify-center lg:bg-none mt-4 lg:mt-0 w-screen"> @@ -411,7 +448,7 @@ export default function Home({ detail, populars, upComing }) { animate={{ opacity: 1 }} transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop > - {user?.length > 0 && user?.some((i) => i?.watchId) && ( + {user && user?.length > 0 && user?.some((i) => i?.watchId) && ( <motion.section // Add motion.div to each child component key="recentlyWatched" initial={{ y: 20, opacity: 0 }} @@ -423,7 +460,7 @@ export default function Home({ detail, populars, upComing }) { ids="recentlyWatched" section="Recently Watched" userData={user} - userName={sessions?.user?.name} + userName={userSession?.name} setRemoved={setRemoved} /> </motion.section> @@ -442,12 +479,12 @@ export default function Home({ detail, populars, upComing }) { section="On-Going Anime" data={releaseData} og={prog} - userName={sessions?.user?.name} + userName={userSession?.name} /> </motion.section> )} - {sessions && listAnime?.length > 0 && ( + {sessions && listAnime && listAnime?.length > 0 && ( <motion.section // Add motion.div to each child component key="listAnime" initial={{ y: 20, opacity: 0 }} @@ -460,12 +497,12 @@ export default function Home({ detail, populars, upComing }) { section="Your Watch List" data={listAnime} og={prog} - userName={sessions?.user?.name} + userName={userSession?.name} /> </motion.section> )} - {sessions && listManga?.length > 0 && ( + {sessions && listManga && listManga?.length > 0 && ( <motion.section // Add motion.div to each child component key="listManga" initial={{ y: 20, opacity: 0 }} @@ -478,13 +515,13 @@ export default function Home({ detail, populars, upComing }) { section="Your Manga List" data={listManga} // og={prog} - userName={sessions?.user?.name} + userName={userSession?.name} /> </motion.section> )} - {/* {recommendations.length > 0 && ( - <div className="space-y-5 mb-10"> + {recommendations.length > 0 && ( + <div className="space-y-4 lg:space-y-5 mb-5 lg:mb-10"> <div className="px-5"> <p className="text-sm lg:text-base"> Based on Your List @@ -496,10 +533,10 @@ export default function Home({ detail, populars, upComing }) { </div> <UserRecommendation data={recommendations} /> </div> - )} */} + )} {/* SECTION 2 */} - {sessions && planned?.length > 0 && ( + {sessions && planned && planned?.length > 0 && ( <motion.section // Add motion.div to each child component key="plannedAnime" initial={{ y: 20, opacity: 0 }} @@ -511,7 +548,7 @@ export default function Home({ detail, populars, upComing }) { ids="plannedAnime" section="Your Plan" data={planned} - userName={sessions?.user?.name} + userName={userSession?.name} /> </motion.section> )} @@ -534,7 +571,7 @@ export default function Home({ detail, populars, upComing }) { > <Content ids="recentAdded" - section="New Episodes" + section="Freshly Added" data={recentAdded} /> </motion.section> @@ -556,6 +593,9 @@ export default function Home({ detail, populars, upComing }) { /> </motion.section> )} + {/* <div className="w-full h-[150px] bg-white flex-center my-5 text-black"> + ad banner + </div> */} {/* Schedule */} {anime.length > 0 && ( @@ -608,3 +648,65 @@ export default function Home({ detail, populars, upComing }) { </Fragment> ); } + +export interface CurrentMediaTypes { + status?: string; + name: string; + entries: Entry[]; +} + +export interface Entry { + id: number; + mediaId: number; + status: string; + progress: number; + score: number; + media: Media; +} + +export interface Media { + id: number; + status: string; + nextAiringEpisode: any; + title: Title; + episodes: number; + coverImage: CoverImage; +} + +export interface Title { + english: string; + romaji: string; +} + +export interface CoverImage { + large: string; +} + +export interface UserDataType { + id: string; + name: string; + setting: Setting; + WatchListEpisode: WatchListEpisode[]; +} + +export interface Setting { + CustomLists: boolean; +} + +export interface WatchListEpisode { + id: string; + aniId?: string; + title?: string; + aniTitle?: string; + image?: string; + episode?: number; + timeWatched?: number; + duration?: number; + provider?: string; + nextId?: string; + nextNumber?: number; + dub?: boolean; + createdDate: string; + userProfileId: string; + watchId: string; +} diff --git a/pages/en/manga/[...id].js b/pages/en/manga/[...id].js deleted file mode 100644 index 5648b2c..0000000 --- a/pages/en/manga/[...id].js +++ /dev/null @@ -1,427 +0,0 @@ -import ChapterSelector from "@/components/manga/chapters"; -import Footer from "@/components/shared/footer"; -import Head from "next/head"; -import { useEffect, useState } from "react"; -import { getServerSession } from "next-auth"; -import { authOptions } from "../../api/auth/[...nextauth]"; -import { mediaInfoQuery } from "@/lib/graphql/query"; -import Modal from "@/components/modal"; -import { signIn, useSession } from "next-auth/react"; -import AniList from "@/components/media/aniList"; -import ListEditor from "@/components/listEditor"; -import MobileNav from "@/components/shared/MobileNav"; -import Image from "next/image"; -import DetailTop from "@/components/anime/mobile/topSection"; -import Characters from "@/components/anime/charactersCard"; -import Content from "@/components/home/content"; -import { toast } from "sonner"; -import axios from "axios"; -import getAnifyInfo from "@/lib/anify/info"; -import { redis } from "@/lib/redis"; -import getMangaId from "@/lib/anify/getMangaId"; - -export default function Manga({ info, anifyData, color, chapterNotFound }) { - const [domainUrl, setDomainUrl] = useState(""); - const { data: session } = useSession(); - - const [loading, setLoading] = useState(false); - const [progress, setProgress] = useState(0); - const [statuses, setStatuses] = useState(null); - const [watch, setWatch] = useState(); - - const [chapter, setChapter] = useState(null); - - const [open, setOpen] = useState(false); - - const rec = info?.recommendations?.nodes?.map( - (data) => data.mediaRecommendation - ); - - useEffect(() => { - setDomainUrl(window.location.origin); - }, []); - - useEffect(() => { - if (chapterNotFound) { - toast.error("Chapter not found"); - const cleanUrl = window.location.origin + window.location.pathname; - window.history.replaceState(null, null, cleanUrl); - } - }, [chapterNotFound]); - - useEffect(() => { - async function fetchData() { - try { - setLoading(true); - - const { data } = await axios.get(`/api/v2/info?id=${anifyData.id}`); - - if (!data.chapters) { - setLoading(false); - return; - } - - setChapter(data); - setLoading(false); - } catch (error) { - console.error(error); - } - } - fetchData(); - - return () => { - setChapter(null); - }; - }, [info?.id]); - - function handleOpen() { - setOpen(true); - document.body.style.overflow = "hidden"; - } - - function handleClose() { - setOpen(false); - document.body.style.overflow = "auto"; - } - - return ( - <> - <Head> - <title> - {info - ? `Manga - ${ - info.title.romaji || info.title.english || info.title.native - }` - : "Getting Info..."} - </title> - <meta name="twitter:card" content="summary_large_image" /> - <meta - name="twitter:title" - content={`Moopa - ${info.title.romaji || info.title.english}`} - /> - <meta - name="twitter:description" - content={`${info.description?.slice(0, 180)}...`} - /> - <meta - name="twitter:image" - content={`${domainUrl}/api/og?title=${ - info.title.romaji || info.title.english - }&image=${info.bannerImage || info.coverImage}`} - /> - <meta - name="title" - data-title-romaji={info?.title?.romaji} - data-title-english={info?.title?.english} - data-title-native={info?.title?.native} - /> - </Head> - <Modal open={open} onClose={() => handleClose()}> - <div> - {!session && ( - <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> - <div className="text-md font-extrabold font-karla"> - Edit your list - </div> - <button - className="flex items-center bg-[#363642] rounded-md text-white p-1" - onClick={() => signIn("AniListProvider")} - > - <h1 className="px-1 font-bold font-karla"> - Login with AniList - </h1> - <div className="scale-[60%] pb-[1px]"> - <AniList /> - </div> - </button> - </div> - )} - {session && info && ( - <ListEditor - animeId={info?.id} - session={session} - stats={statuses?.value} - prg={progress} - max={info?.episodes} - info={info} - close={handleClose} - /> - )} - </div> - </Modal> - <MobileNav sessions={session} hideProfile={true} /> - <main className="w-screen min-h-screen overflow-hidden relative flex flex-col items-center gap-5"> - {/* <div className="absolute bg-gradient-to-t from-primary from-85% to-100% to-transparent w-screen h-full z-10" /> */} - <div className="w-screen absolute"> - <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" /> - {info?.bannerImage && ( - <Image - src={info?.bannerImage} - alt="banner anime" - height={1000} - width={1000} - blurDataURL={info?.bannerImage} - className="object-cover bg-image blur-[2px] w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0" - /> - )} - </div> - <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5 pb-10"> - <DetailTop - info={info} - session={session} - handleOpen={handleOpen} - loading={loading} - statuses={statuses} - watchUrl={watch} - progress={progress} - color={color} - /> - - {!loading ? ( - chapter?.chapters?.length > 0 ? ( - <ChapterSelector - chaptersData={chapter.chapters} - mangaId={chapter.id} - data={info} - setWatch={setWatch} - /> - ) : ( - <div className="h-[20vh] lg:w-full flex-center flex-col gap-5"> - <p className="text-center font-karla font-bold lg:text-lg"> - Oops!<br></br> It looks like this manga is not available. - </p> - </div> - ) - ) : ( - <div className="flex justify-center"> - <div className="lds-ellipsis"> - <div></div> - <div></div> - <div></div> - <div></div> - </div> - </div> - )} - - {info?.characters?.edges?.length > 0 && ( - <div className="w-full"> - <Characters info={info?.characters?.edges} /> - </div> - )} - - {info && rec && rec?.length !== 0 && ( - <div className="w-full"> - <Content - ids="recommendAnime" - section="Recommendations" - type="manga" - data={rec} - /> - </div> - )} - </div> - </main> - <Footer /> - </> - ); -} - -export async function getServerSideProps(context) { - const session = await getServerSession(context.req, context.res, authOptions); - const accessToken = session?.user?.token || null; - - const { chapter } = context.query; - const [id1, id2] = context.query.id; - - let cached; - let aniId, mangadexId; - let info, data, color, chapterNotFound; - - if (String(id1).length > 6) { - aniId = id2; - mangadexId = id1; - } else { - aniId = id1; - mangadexId = id2; - } - - if (chapter) { - // create random id string - chapterNotFound = Math.random().toString(36).substring(7); - } - - if (aniId === "na" && mangadexId) { - const datas = await getAnifyInfo(mangadexId); - - aniId = - datas.mappings?.filter((i) => i.providerId === "anilist")[0]?.id || null; - - if (!aniId) { - info = datas; - data = datas; - color = { - backgroundColor: `${"#ffff"}`, - color: "#000", - }; - // return { - // redirect: { - // destination: "/404", - // permanent: false, - // }, - // }; - } - } else if (aniId && !mangadexId) { - // console.log({ aniId }); - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(accessToken && { Authorization: `Bearer ${accessToken}` }), - }, - body: JSON.stringify({ - query: `query ($id: Int, $type: MediaType) { - Media (id: $id, type: $type) { - id - title { - romaji - english - native - } - } - }`, - variables: { - id: parseInt(aniId), - type: "MANGA", - }, - }), - }); - const aniListData = await response.json(); - const info = aniListData?.data?.Media; - - const mangaId = await getMangaId( - info?.title?.romaji, - info?.title?.english, - info?.title?.native - ); - mangadexId = mangaId?.id; - - if (!mangadexId) { - return { - redirect: { - destination: "/404", - permanent: false, - }, - }; - } - - return { - redirect: { - destination: `/en/manga/${aniId}/${mangadexId}${ - chapter ? "?chapter=404" : "" - }`, - permanent: true, - }, - }; - } else if (!aniId && mangadexId) { - const data = await getAnifyInfo(mangadexId); - - aniId = - data.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null; - - if (!aniId) { - info = data; - // return { - // redirect: { - // destination: "/404", - // permanent: false, - // }, - // }; - } - - return { - redirect: { - destination: `/en/manga/${aniId ? aniId : "na"}${`/${mangadexId}`}${ - chapter ? "?chapter=404" : "" - }`, - permanent: true, - }, - }; - } else { - if (redis) { - const getCached = await redis.get(`mangaPage:${mangadexId}`); - - if (getCached) { - cached = JSON.parse(getCached); - } - } - // let chapters; - if (cached) { - data = cached.data; - info = cached.info; - color = cached.color; - } else { - data = await getAnifyInfo(mangadexId); - - const aniListId = - data.mappings?.filter((i) => i.providerId === "anilist")[0]?.id || null; - - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(accessToken && { Authorization: `Bearer ${accessToken}` }), - }, - body: JSON.stringify({ - query: mediaInfoQuery, - variables: { - id: parseInt(aniListId), - type: "MANGA", - }, - }), - }); - const aniListData = await response.json(); - if (aniListData?.data?.Media) info = aniListData?.data?.Media; - - const textColor = setTxtColor(info?.color); - - color = { - backgroundColor: `${info?.color || "#ffff"}`, - color: textColor, - }; - - if (redis) { - await redis.set( - `mangaPage:${mangadexId}`, - JSON.stringify({ data, info, color }), - "ex", - 60 * 60 * 24 - ); - } - } - } - - return { - props: { - info: info || null, - anifyData: data || null, - chapterNotFound: chapterNotFound || null, - color: color || null, - }, - }; -} - -function getBrightness(hexColor) { - if (!hexColor) { - return 200; - } - const rgb = hexColor - .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) - .slice(1) - .map((x) => parseInt(x, 16)); - return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000; -} - -function setTxtColor(hexColor) { - const brightness = getBrightness(hexColor); - return brightness < 150 ? "#fff" : "#000"; -} diff --git a/pages/en/manga/[...id].tsx b/pages/en/manga/[...id].tsx new file mode 100644 index 0000000..d1c10a4 --- /dev/null +++ b/pages/en/manga/[...id].tsx @@ -0,0 +1,456 @@ +import Footer from "@/components/shared/footer"; +import Head from "next/head"; +import { useEffect, useState } from "react"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../api/auth/[...nextauth]"; +import { mediaInfoQuery } from "@/lib/graphql/query"; +import Modal from "@/components/modal"; +import { signIn } from "next-auth/react"; +import AniList from "@/components/media/aniList"; +import ListEditor from "@/components/listEditor"; +import MobileNav from "@/components/shared/MobileNav"; +import Image from "next/image"; +import DetailTop from "@/components/anime/mobile/topSection"; +import Characters from "@/components/anime/charactersCard"; +import Content from "@/components/home/content"; +import { toast } from "sonner"; +import getAnifyInfo from "@/lib/anify/info"; +import getMangaId from "@/lib/anify/getMangaId"; +import { useRouter } from "next/router"; +import ChaptersComponent from "@/components/manga/ChaptersComponent"; +import pls from "@/utils/request/index"; +import { AniListInfoTypes } from "types/info/AnilistInfoTypes"; +import { Navbar } from "@/components/shared/NavBar"; + +type MangaProps = { + aniId: string; + mangadexId: string; + sessions: any; + metaData: any; + chapterNotFound: string; +}; + +export default function Manga({ + aniId, + mangadexId, + sessions: session, + chapterNotFound, + metaData, +}: MangaProps) { + const [domainUrl, setDomainUrl] = useState(""); + + const [loading, setLoading] = useState(false); + const [watch, setWatch] = useState(); + + const [mangaId, setMangaId] = useState<string | null>(mangadexId); + const [chapters, setChapters] = useState(null); + const [notFound, setNotFound] = useState(false); + + const [info, setInfo] = useState<AniListInfoTypes | null>(null); + const [color, setColor] = useState(null); + + const [open, setOpen] = useState(false); + + const router = useRouter(); + + const rec = info?.recommendations?.nodes?.map( + (data) => data.mediaRecommendation + ); + + useEffect(() => { + setDomainUrl(window.location.origin); + }, []); + + useEffect(() => { + if (chapterNotFound) { + toast.error("Chapter not found"); + const cleanUrl = window.location.origin + window.location.pathname; + window.history.replaceState(null, "", cleanUrl); + } + }, [chapterNotFound]); + + useEffect(() => { + setMangaId(null); + }, [aniId]); + + useEffect(() => { + async function fetchData() { + try { + let info, data, color: any; + setChapters(null); + setNotFound(false); + + if (aniId && mangadexId) { + const [aniListData] = await pls.post("https://graphql.anilist.co/", { + body: JSON.stringify({ + query: mediaInfoQuery, + variables: { + id: parseInt(aniId), + type: "MANGA", + }, + }), + }); + // const aniListData = await response.json(); + info = aniListData?.data?.Media; + const textColor = setTxtColor(info?.color); + + color = { + backgroundColor: `${info?.color || "#ffff"}`, + color: textColor, + }; + + setInfo(info); + setColor(color); + setMangaId(mangadexId); + // console.log("wow two of them here"); + } else if (aniId && !mangadexId) { + const [aniListData] = await pls.post("https://graphql.anilist.co/", { + body: JSON.stringify({ + query: mediaInfoQuery, + variables: { + id: parseInt(aniId), + type: "MANGA", + }, + }), + }); + // const aniListData = await response.json(); + info = aniListData?.data?.Media; + const textColor = setTxtColor(info?.color); + + color = { + backgroundColor: `${info?.color || "#ffff"}`, + color: textColor, + }; + + setInfo(info); + setColor(color); + + const mangaId = await getMangaId( + info?.title?.romaji, + info?.title?.english, + info?.title?.native + ); + + mangadexId = (mangaId as { id: string }).id; + + if (mangadexId) { + setMangaId(mangadexId); + // console.log("mangadex is here", mangadexId); + router.push("/en/manga/" + aniId + "/" + mangadexId, undefined, { + shallow: true, + }); + } else { + // console.log("why is this running?"); + setMangaId(null); + setLoading(false); + setNotFound(true); + // router.push("/en/manga/" + aniId, undefined, { shallow: true }); + } + } else if (!aniId && mangadexId) { + data = await getAnifyInfo(mangadexId); + + const aniListId = + data.mappings?.filter((i: any) => i.providerId === "anilist")[0] + ?.id || null; + + if (aniListId) { + const [aniListData] = await pls.post( + "https://graphql.anilist.co/", + { + body: JSON.stringify({ + query: mediaInfoQuery, + variables: { + id: parseInt(aniListId), + type: "MANGA", + }, + }), + } + ); + // const aniListData = await response.json(); + info = aniListData?.data?.Media; + + router.push( + "/en/manga/" + aniListId + "/" + mangadexId, + undefined, + { shallow: true } + ); + } + + const textColor = setTxtColor(data?.color); + + color = { + backgroundColor: `${data?.color || "#ffff"}`, + color: textColor, + }; + + setInfo(aniListId ? info : data); + setColor(color); + setMangaId(mangadexId); + } + } catch (error) { + console.log(error); + } + } + fetchData(); + + return () => { + setInfo(null); + }; + }, [session?.user?.token, aniId, mangadexId]); + + function handleOpen() { + setOpen(true); + document.body.style.overflow = "hidden"; + } + + function handleClose() { + setOpen(false); + document.body.style.overflow = "auto"; + } + + return ( + <> + <Head> + <title> + {metaData + ? `Manga - ${ + metaData.title.romaji || + metaData.title.english || + metaData.title.native + }` + : "Getting Info..."} + </title> + <meta + name="description" + content={`${metaData?.description?.slice(0, 180)}...`} + /> + <meta + name="keywords" + content={`${metaData?.genres}, ${metaData?.author} `} + /> + <meta + property="og:title" + content={`Moopa - ${ + metaData?.title.romaji || metaData?.title.english + }`} + /> + <meta + property="og:description" + content={`${metaData?.description?.slice(0, 180)}...`} + /> + <meta + property="og:image" + content={`${domainUrl}/api/og?title=${ + metaData?.title.romaji || metaData?.title.english + }&image=${metaData?.bannerImage || metaData?.coverImage}`} + /> + <meta + property="og:url" + content={`${domainUrl}/en/manga/${metaData?.id}`} + /> + <meta property="og:type" content="book" /> + <meta property="og:locale" content="en_US" /> + <meta name="twitter:card" content="summary_large_image" /> + <meta name="twitter:site" content="@yourTwitterHandle" /> + <meta + name="twitter:title" + content={`Moopa - ${ + metaData?.title.romaji || metaData?.title.english + }`} + /> + <meta + name="twitter:description" + content={`${metaData?.description?.slice(0, 180)}...`} + /> + <meta name="robots" content="noindex" /> + <meta + name="twitter:image" + content={`${domainUrl}/api/og?title=${ + metaData?.title.romaji || metaData?.title.english + }&image=${metaData?.bannerImage || metaData?.coverImage}`} + /> + </Head> + <Navbar info={info} manga /> + <Modal open={open} onClose={() => handleClose()}> + <div> + {!session && ( + <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md"> + <div className="text-md font-extrabold font-karla"> + Edit your list + </div> + <button + className="flex items-center bg-[#363642] rounded-md text-white p-1" + onClick={() => signIn("AniListProvider")} + > + <h1 className="px-1 font-bold font-karla"> + Login with AniList + </h1> + <div className="scale-[60%] pb-[1px]"> + <AniList /> + </div> + </button> + </div> + )} + {session && info && ( + <ListEditor + animeId={info?.id} + session={session} + // stats={statuses?.value} + // prg={progress} + max={info?.episodes} + info={info} + close={handleClose} + /> + )} + </div> + </Modal> + <MobileNav hideProfile={true} /> + <main className="w-screen min-h-screen overflow-hidden relative flex flex-col items-center 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} + alt="banner anime" + height={1000} + width={1000} + blurDataURL={info?.bannerImage} + className="object-cover bg-image blur-[2px] w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0" + /> + )} + </div> + <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5 pb-10"> + {/* {info && ( */} + <DetailTop + info={info} + handleOpen={handleOpen} + // statuses={statuses} + watchUrl={watch} + // progress={progress} + color={color} + /> + {/* )} */} + + <ChaptersComponent + info={info} + mangaId={mangaId} + aniId={aniId} + setWatch={setWatch} + chapter={chapters} + setChapter={setChapters} + loading={loading} + setLoading={setLoading} + notFound={notFound} + setNotFound={setNotFound} + /> + + {info && info.characters.edges.length > 0 && ( + <div className="w-full"> + <Characters info={info?.characters?.edges} /> + </div> + )} + + {info && rec && rec?.length !== 0 && ( + <div className="w-full"> + <Content + ids="recommendAnime" + section="Recommendations" + type="manga" + data={rec} + /> + </div> + )} + </div> + </main> + <Footer /> + </> + ); +} + +export async function getServerSideProps(context: any) { + const session: any = await getServerSession( + context.req, + context.res, + authOptions + ); + const accessToken = session?.user?.token || null; + + const { chapter } = context.query; + const [id1, id2] = context.query.id; + + let aniId, mangadexId; + let chapterNotFound; + + if (String(id1).length > 6) { + aniId = id2; + mangadexId = id1; + } else { + aniId = id1; + mangadexId = id2; + } + + if (chapter) { + // create random id string + chapterNotFound = Math.random().toString(36).substring(7); + } + + const [aniListData] = await pls.post("https://graphql.anilist.co/", { + body: JSON.stringify({ + query: `query ($id: Int, $type: MediaType) { + Media(id: $id, type: $type) { + id + title { + romaji + english + native + } + bannerImage + genres + coverImage { + extraLarge + large + medium + color + } + status + description + } + }`, + variables: { + id: parseInt(aniId), + type: "MANGA", + }, + }), + }); + const info = aniListData?.data?.Media; + + return { + props: { + aniId: aniId || null, + mangadexId: mangadexId || null, + accessToken: accessToken || null, + sessions: session || null, + metaData: info || null, + // info: info || null, + // anifyData: data || null, + chapterNotFound: chapterNotFound || null, + // color: color || null, + }, + }; +} + +function getBrightness(hexColor: { match: (arg0: RegExp) => any[] }) { + 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: { match: (arg0: RegExp) => any[] }) { + const brightness = getBrightness(hexColor); + return brightness < 150 ? "#fff" : "#000"; +} diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js index a7fa78b..036b999 100644 --- a/pages/en/manga/read/[...params].js +++ b/pages/en/manga/read/[...params].js @@ -150,6 +150,7 @@ export default function Read({ data-title-native={info?.title?.native} /> <meta id="CoverImage" data-manga-cover={info?.coverImage} /> + <meta name="robots" content="noindex" /> </Head> <div className="w-screen flex justify-evenly relative"> <ShortCutModal isOpen={isKeyOpen} setIsOpen={setIsKeyOpen} /> diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].tsx index 7ef5de3..82b88af 100644 --- a/pages/en/profile/[user].js +++ b/pages/en/profile/[user].tsx @@ -1,14 +1,28 @@ -import { getServerSession } from "next-auth"; -import { authOptions } from "../../api/auth/[...nextauth]"; import Image from "next/image"; import Link from "next/link"; import Head from "next/head"; import { useEffect, useState } from "react"; import { getUser } from "@/prisma/user"; -import { NewNavbar } from "@/components/shared/NavBar"; import { toast } from "sonner"; +import { Navbar } from "@/components/shared/NavBar"; +import pls from "@/utils/request"; +import { CurrentMediaTypes } from ".."; -export default function MyList({ media, sessions, user, time, userSettings }) { +type MyListProps = { + media: CurrentMediaTypes[]; + sessions: any; + user: any; + time: any; + userSettings: any; +}; + +export default function MyList({ + media, + sessions, + user, + time, + userSettings, +}: MyListProps) { const [listFilter, setListFilter] = useState("all"); const [visible, setVisible] = useState(false); const [useCustomList, setUseCustomList] = useState(true); @@ -40,26 +54,27 @@ export default function MyList({ media, sessions, user, time, userSettings }) { if (data) { toast.success(`Custom List is now ${!useCustomList ? "on" : "off"}`); } - localStorage.setItem("customList", !useCustomList); + localStorage.setItem("customList", String(!useCustomList)); } catch (error) { console.error(error); } }; - const filterMedia = (status) => { + const filterMedia = (status: string) => { if (status === "all") { return media; } - return media.filter((m) => m.name === status); + return media.filter((m: { name: string }) => m.name === status); }; return ( <> <Head> <title>My Lists</title> </Head> - <NewNavbar /> - <div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 relative"> + <Navbar withNav toTop shrink bgHover scrollP={110} paddingY={"py-1"} /> + + <div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 mt-10 xl:mt-16 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 @@ -289,7 +304,7 @@ export default function MyList({ media, sessions, user, time, userSettings }) { <div className="absolute -top-10 -left-40 invisible lg:group-hover:visible"> <Image src={item.media.coverImage.large} - alt={item.media.id} + alt={String(item.media.id)} width={1000} height={1000} className="object-cover h-[186px] w-[140px] shrink-0 rounded-md" @@ -362,19 +377,14 @@ export default function MyList({ media, sessions, user, time, userSettings }) { ); } -export async function getServerSideProps(context) { - const session = await getServerSession(context.req, context.res, authOptions); - const accessToken = session?.user?.token || null; +export async function getServerSideProps(context: any) { const query = context.query; - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(accessToken && { Authorization: `Bearer ${accessToken}` }), - }, - body: JSON.stringify({ - query: ` + const [data, session] = await pls.post( + "https://graphql.anilist.co/", + { + body: JSON.stringify({ + query: ` query ($username: String, $status: MediaListStatus) { MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) { user { @@ -426,15 +436,15 @@ export async function getServerSideProps(context) { } } `, - variables: { - username: query.user, - }, - }), - }); - - const data = await response.json(); + variables: { + username: query.user, + }, + }), + }, + context + ); - const get = data.data.MediaListCollection; + const get = data?.data?.MediaListCollection; const sectionOrder = get?.user.mediaListOptions.animeList.sectionOrder; if (!sectionOrder) { @@ -451,12 +461,15 @@ export async function getServerSideProps(context) { const prog = get.lists; - function getIndex(status) { + function getIndex(status: string) { const index = sectionOrder.indexOf(status); return index === -1 ? sectionOrder.length : index; } - prog.sort((a, b) => getIndex(a.name) - getIndex(b.name)); + prog.sort( + (a: { name: string }, b: { name: string }) => + getIndex(a.name) - getIndex(b.name) + ); const user = get.user; @@ -473,24 +486,24 @@ export async function getServerSideProps(context) { }; } -function UnixTimeConverter({ unixTime }) { +function UnixTimeConverter({ unixTime }: { unixTime: number }) { 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) { +function convertMinutesToDays(minutes: number) { const hours = minutes / 60; const days = hours / 24; if (days >= 1) { return days % 1 === 0 - ? { days: `${parseInt(days)}` } + ? { days: `${days}` } : { days: `${days.toFixed(1)}` }; } else { return hours % 1 === 0 - ? { hours: `${parseInt(hours)}` } + ? { hours: `${hours}` } : { hours: `${hours.toFixed(1)}` }; } } diff --git a/pages/en/schedule/index.js b/pages/en/schedule/index.tsx index f1e6730..aa30259 100644 --- a/pages/en/schedule/index.js +++ b/pages/en/schedule/index.tsx @@ -18,7 +18,7 @@ import MobileNav from "@/components/shared/MobileNav"; import { useSession } from "next-auth/react"; import { redis } from "@/lib/redis"; import Head from "next/head"; -import { NewNavbar } from "@/components/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; const day = [ "Sunday", @@ -30,7 +30,8 @@ const day = [ "Saturday", ]; -const isAired = (timestamp) => { +const isAired = (timestamp: number | null) => { + if (!timestamp) return false; const currentTime = new Date().getTime() / 1000; return timestamp <= currentTime; }; @@ -51,7 +52,7 @@ export async function getServerSideProps() { 0 ); const timeUntilMidnightJapan = Math.round( - (midnightTomorrowJapan - nowJapan) / 1000 + (midnightTomorrowJapan.getTime() - nowJapan.getTime()) / 1000 ); let cachedData; @@ -109,12 +110,13 @@ export async function getServerSideProps() { page++; } - const timestampToDay = (timestamp) => { - const options = { weekday: "long" }; - return new Date(timestamp * 1000).toLocaleDateString(undefined, options); + const timestampToDay = (timestamp: number) => { + return new Date(timestamp * 1000).toLocaleDateString(undefined, { + weekday: "long", + }); }; - const scheduleByDay = {}; + const scheduleByDay: { [key: string]: any } = {}; airingSchedules.forEach((schedule) => { const day = timestampToDay(schedule.airingAt); if (!scheduleByDay[day]) { @@ -142,10 +144,7 @@ export async function getServerSideProps() { // setSchedule(scheduleByDay); } -export default function Schedule({ schedule }) { - const { data: session } = useSession(); - - // const [schedule, setSchedule] = useState({}); +export default function Schedule({ schedule }: any) { const [filterDay, setFilterDay] = useState("All"); const [loading, setLoading] = useState(true); @@ -178,7 +177,7 @@ export default function Schedule({ schedule }) { let nextAiring = null; let currentlyAiring = null; - for (const [, schedules] of Object.entries(sortedSchedule)) { + for (const [, schedules] of Object.entries(sortedSchedule as object)) { for (const s of schedules) { if (s.airingAt > now) { if (!nextAiring) { @@ -196,16 +195,16 @@ export default function Schedule({ schedule }) { setCurrentlyAiringAnime(currentlyAiring); }, [sortedSchedule]); - const scrollContainerRef = useRef(null); + const scrollContainerRef = useRef<HTMLUListElement>(null); useEffect(() => { // Scroll to center the active button when it changes if (scrollContainerRef.current) { const activeButton = - scrollContainerRef.current.querySelector(".text-action"); + scrollContainerRef.current?.querySelector(".text-action"); if (activeButton) { const containerWidth = scrollContainerRef.current.clientWidth; - const buttonLeft = activeButton.offsetLeft; + const buttonLeft = (activeButton as HTMLElement).offsetLeft; const buttonWidth = activeButton.clientWidth; const scrollLeft = buttonLeft - containerWidth / 2 + buttonWidth / 2; scrollContainerRef.current.scrollLeft = scrollLeft; @@ -264,8 +263,8 @@ export default function Schedule({ schedule }) { content="Moopa is a website where you can find all the information about your favorite anime and manga." /> </Head> - <MobileNav sessions={session} hideProfile={true} /> - <NewNavbar scrollP={10} toTop={true} /> + <MobileNav hideProfile={true} /> + <Navbar scrollP={10} toTop={true} /> <div className="w-screen"> <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]" /> @@ -340,7 +339,7 @@ export default function Schedule({ schedule }) { > <div className="ml-4 flex items-center gap-2"> <h3 className="text-lg text-gray-200 font-semibold"> - {timeStamptoAMPM(time)} + {time && timeStamptoAMPM(time)} </h3> {/* {!isAired(time) && <p>Airing Next</p>} */} <p diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].tsx index c1fd94c..5a34ff5 100644 --- a/pages/en/search/[...param].js +++ b/pages/en/search/[...param].tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { Key, useEffect, useRef, useState } from "react"; import { motion as m } from "framer-motion"; import Skeleton from "react-loading-skeleton"; import { useRouter } from "next/router"; @@ -23,12 +23,15 @@ import { 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/shared/NavBar"; +import { Navbar } from "@/components/shared/NavBar"; import MobileNav from "@/components/shared/MobileNav"; -import SearchByImage from "@/components/search/searchByImage"; +import SearchByImage, { + TraceMoeResultTypes, +} from "@/components/search/searchByImage"; import { PlayIcon } from "@heroicons/react/24/outline"; +import { StaticImport } from "next/dist/shared/lib/get-img-props"; -export async function getServerSideProps(context) { +export async function getServerSideProps(context: any) { const { param } = context.query; const { search, format, genres, season, year } = context.query; @@ -81,6 +84,15 @@ export async function getServerSideProps(context) { }; } +type CardProps = { + index: number; + query: string; + genres: any; + formats: any; + seasons: any; + years: any; +}; + export default function Card({ index, query, @@ -88,22 +100,25 @@ export default function Card({ formats, seasons, years, -}) { +}: CardProps) { const inputRef = useRef(null); const router = useRouter(); - const [data, setData] = useState(); - const [imageSearch, setImageSearch] = useState(); + const [data, setData] = useState<any>(); + const [imageSearch, setImageSearch] = useState<TraceMoeResultTypes[]>(); const [loading, setLoading] = useState(true); - const [search, setQuery] = useState(query); + const [search, setQuery] = useState<string | null | undefined>(query); const debounceSearch = useDebounce(search, 500); - const [type, setSelectedType] = useState(mediaType[index]); + const [type, setSelectedType] = useState<{ + name: string; + value: string; + } | null>(mediaType[index]); const [year, setYear] = useState(years); const [season, setSeason] = useState(seasons); - const [sort, setSelectedSort] = useState(); + const [sort, setSelectedSort] = useState<{ name: string; value: string }>(); const [genre, setGenre] = useState(genres); const [format, setFormat] = useState(formats); @@ -116,7 +131,7 @@ export default function Card({ setLoading(true); const data = await aniAdvanceSearch({ search: debounceSearch, - type: type?.value, + type: type?.value as "ANIME" | "MANGA" | undefined, genres: genre, page: page, sort: sort?.value, @@ -128,7 +143,7 @@ export default function Card({ setNextPage(false); setLoading(false); } else if (data !== null && page > 1) { - setData((prevData) => { + setData((prevData: any) => { return [...(prevData ?? []), ...data?.media]; }); setNextPage(data?.pageInfo.hasNextPage); @@ -144,7 +159,9 @@ export default function Card({ setData(null); setPage(1); setNextPage(true); - advance(); + if (page === 1) { + advance(); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ debounceSearch, @@ -158,7 +175,9 @@ export default function Card({ useEffect(() => { if (imageSearch) return; - advance(); + if (page > 1) { + advance(); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [page, imageSearch]); @@ -177,21 +196,23 @@ export default function Card({ window.innerHeight + window.pageYOffset >= document.body.offsetHeight - 3 ) { - setPage((prevPage) => prevPage + 1); + if (!loading) { + setPage((prevPage) => prevPage + 1); + } } } window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); - }, [page, nextPage, imageSearch]); + }, [page, nextPage, imageSearch, loading]); - const handleKeyDown = async (event) => { + const handleKeyDown = async (event: any) => { if (event.key === "Enter") { event.preventDefault(); const inputValue = event.target.value; if (inputValue === "") { - setQuery(null); + setQuery(undefined); } else { setQuery(inputValue); } @@ -199,13 +220,13 @@ export default function Card({ }; function trash() { - setImageSearch(); - setQuery(); - setGenre(); - setFormat(); - setSelectedSort(); - setSeason(); - setYear(); + setImageSearch(undefined); + setQuery(undefined); + setGenre(undefined); + setFormat(undefined); + setSelectedSort(undefined); + setSeason(undefined); + setYear(undefined); router.push(`/en/search/${mediaType[index]?.value?.toLowerCase()}`); } @@ -213,8 +234,8 @@ export default function Card({ setIsVisible(!isVisible); } - const handleVideoHover = (hovered, id) => { - const updatedImageSearch = imageSearch?.map((item) => { + const handleVideoHover = (hovered: boolean, id: any) => { + const updatedImageSearch = imageSearch?.map((item: any) => { if (item.filename === id) { return { ...item, hovered }; } @@ -234,7 +255,7 @@ export default function Card({ <link rel="icon" href="/svg/c.svg" /> </Head> - <NewNavbar + <Navbar scrollP={10} withNav={true} shrink={true} @@ -366,7 +387,7 @@ export default function Card({ </div> )} {/* <div> */} - <div className="flex flex-col gap-14 items-center z-30"> + <div className="flex flex-col gap-14 items-center z-30 overflow-x-hidden"> <div key="card-keys" className={`${ @@ -384,69 +405,75 @@ export default function Card({ {data && data?.length > 0 && !imageSearch && - data?.map((anime, index) => { - const anilistId = anime?.mappings?.find( - (x) => x.providerId === "anilist" - )?.id; - return ( - <m.div - initial={{ scale: 0.98 }} - animate={{ scale: 1, transition: { duration: 0.35 } }} - className="w-full" - key={index} - > - <Link - href={ - anime.format === "MANGA" || anime.format === "NOVEL" - ? `/en/manga/${anilistId ? `${anilistId}/` : ""}${ - anime.id - }` - : `/en/anime/${anime.id}` - } - title={anime.title.userPreferred} - className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded" - style={{ - paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%) - }} - > - <Image - className="object-cover" - src={anime.coverImage.extraLarge} - alt={anime.title.userPreferred} - sizes="(min-width: 808px) 50vw, 100vw" - quality={100} - fill - /> - </Link> - <Link - href={ - anime.format === "MANGA" || anime.format === "NOVEL" - ? `/en/manga/${anilistId ? `${anilistId}/` : ""}${ - anime.id - }` - : `/en/anime/${anime.id}` - } - title={anime.title.userPreferred} + data?.map( + ( + anime: { + format: string; + id: any; + title: { userPreferred: string }; + coverImage: { extraLarge: string | StaticImport }; + status: string; + episodes: any; + chapters: any; + }, + index: Key | null | undefined + ) => { + return ( + <m.div + initial={{ scale: 0.98 }} + animate={{ scale: 1, transition: { duration: 0.35 } }} + className="w-full" + key={index} > - <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> - ); - })} + <Link + href={ + anime.format === "MANGA" || anime.format === "NOVEL" + ? `/en/manga/${anime.id}` + : `/en/anime/${anime.id}` + } + title={anime.title.userPreferred} + className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded" + style={{ + paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%) + }} + > + <Image + className="object-cover" + src={anime.coverImage.extraLarge} + alt={anime.title.userPreferred} + sizes="(min-width: 808px) 50vw, 100vw" + quality={100} + fill + /> + </Link> + <Link + href={ + anime.format === "MANGA" || anime.format === "NOVEL" + ? `/en/manga/${anime.id}` + : `/en/anime/${anime.id}` + } + title={anime.title.userPreferred} + > + <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> + {anime.status === "RELEASING" ? ( + <span className="dots bg-green-500" /> + ) : anime.status === "NOT_YET_RELEASED" ? ( + <span className="dots bg-red-500" /> + ) : null} + {anime.title.userPreferred} + </h1> + </Link> + <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]"> + {anime.format || <p>-</p>} ·{" "} + {anime.status || <p>-</p>} ·{" "} + {anime.episodes + ? `${anime.episodes || "N/A"} Episodes` + : `${anime.chapters || "N/A"} Chapters`} + </h2> + </m.div> + ); + } + )} {loading && ( <> @@ -532,7 +559,7 @@ export default function Card({ href={`/en/anime/${a.anilist.id}`} > {/* <h1 className="font-semibold">{a.title}</h1> */} - <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]"> + <p className="flex items-center gap-1 text-sm text-gray-400 max-w-[320px]"> <span className="text-white max-w-[120px] md:max-w-[200px] lg:max-w-[220px]" style={{ |