diff options
76 files changed, 3144 insertions, 1644 deletions
diff --git a/.env.example b/.env.example index b3184ba..ac71088 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,6 @@ NEXTAUTH_URL="for development use http://localhost:3000/ and for production use ## NextJS PROXY_URI="This is what I use for proxying video https://github.com/chaycee/M3U8Proxy. Don't put / at the end of the url." API_URI="host your own API from this repo https://github.com/consumet/api.consumet.org. Don't put / at the end of the url." -API_KEY="this API key is used for schedules, anime and manga page. get the key from https://anify.tv/discord" DISQUS_SHORTNAME='put your disqus shortname here (optional)' # ADMIN_USERNAME="" diff --git a/.eslintrc.json b/.eslintrc.json index dbda85f..4658cc5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,8 @@ { "extends": "next/core-web-vitals", + // ignore react-hooks/exhaustive-deps "rules": { + "react-hooks/exhaustive-deps": "off", "react/no-unescaped-entities": 0, "react/no-unknown-property": ["error", { "ignore": ["css"] }] } @@ -8,7 +8,7 @@ # testing /coverage -/pages/test.js +/pages/en/test.js /components/devComp # next.js diff --git a/.prettierrc.json b/.prettierrc.json index 0967ef4..08df606 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1 +1,4 @@ -{} +{ + "bracketSpacing": true, + "printWidth": 80 +} @@ -41,7 +41,11 @@ </p> <h3 align="center">Watch Page</h3> -<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/c654aa13-76d7-47fe-ac02-924fbbb40f76"/> +<p align="center">Normal Mode</p> +<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/03b2c9c7-eb25-4f2c-8f26-a9ae817bfbaa"/> +<br/> +<p align="center">Theater Mode</p> +<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/767a0335-f6a3-4969-b415-3c45d07cce64"/> <h3 align="center">Manga Reader</h3> <img src="https://github.com/DevanAbinaya/Ani-Moopa/assets/97084324/ccd2ee11-4ee3-411c-b634-d48c84f1a9e2"/> @@ -54,13 +58,23 @@ ## Features -- Free ad-supported streaming service -- Anime tracking through Anilist API -- Skip OP/ED buttons -- Dub Anime support -- User-friendly interface -- Mobile-responsive design -- PWA supported +- General + - Free ad-supported streaming service + - Dub Anime support + - User-friendly interface + - Auto sync with AniList + - Add Anime/Manga to your AniList + - Scene Searching powered by [trace.moe](https://trace.moe) + - PWA supported + - Mobile responsive + - Fast page load +- Watch Page + - Player + - Autoplay next episode + - Skip op/ed button + - Theater mode + - Comment section +- Profile page to see your watch list ## To Do List @@ -116,9 +130,7 @@ NEXTAUTH_URL="for development use http://localhost:3000/ and for production use ## NextJS PROXY_URI="This is what I use for proxying video https://github.com/chaycee/M3U8Proxy. Don't put / at the end of the url." API_URI="host your own API from this repo https://github.com/consumet/api.consumet.org. Don't put / at the end of the url." -API_KEY="this API key is used for schedules, anime and manga page. get the key from https://anify.tv/discord" DISQUS_SHORTNAME='put your disqus shortname here (optional)' -# ADMIN_USERNAME="" ## Prisma DATABASE_URL="Your postgresql connection url" @@ -131,7 +143,7 @@ REDIS_URL="rediss://username:password@host:port" 5. Add this endpoint as Redirect Url on AniList Developer : ```bash -https://your-website-url/api/auth/callback/AniListProvider +https://your-website-domain/api/auth/callback/AniListProvider ``` 6. Start local server : diff --git a/components/admin/dashboard/index.js b/components/admin/dashboard/index.js index 64a1d6f..d0c9963 100644 --- a/components/admin/dashboard/index.js +++ b/components/admin/dashboard/index.js @@ -1,4 +1,6 @@ -import React, { useState } from "react"; +import Link from "next/link"; +import React, { useEffect, useState } from "react"; +import { toast } from "sonner"; export default function AdminDashboard({ animeCount, @@ -10,13 +12,90 @@ export default function AdminDashboard({ const [selectedTime, setSelectedTime] = useState(""); const [unixTimestamp, setUnixTimestamp] = useState(null); - const handleSubmit = (e) => { + const [broadcast, setBroadcast] = useState(); + const [reportId, setReportId] = useState(); + + useEffect(() => { + async function getBroadcast() { + const res = await fetch("/api/v2/admin/broadcast", { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-Broadcast-Key": "get-broadcast", + }, + }); + const data = await res.json(); + if (data) { + setBroadcast(data); + } + } + getBroadcast(); + }, []); + + const handleSubmit = async (e) => { e.preventDefault(); + let unixTime; + if (selectedTime) { - const unixTime = Math.floor(new Date(selectedTime).getTime() / 1000); + unixTime = Math.floor(new Date(selectedTime).getTime() / 1000); setUnixTimestamp(unixTime); } + + const res = await fetch("/api/v2/admin/broadcast", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Broadcast-Key": "get-broadcast", + }, + body: JSON.stringify({ + message, + startAt: unixTime, + show: true, + }), + }); + + const data = await res.json(); + + console.log({ message, unixTime, data }); + }; + + const handleRemove = async () => { + try { + const res = await fetch("/api/v2/admin/broadcast", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-Broadcast-Key": "get-broadcast", + }, + }); + const data = await res.json(); + console.log(data); + } catch (error) { + console.log(error); + } + }; + + const handleResolved = async () => { + try { + console.log(reportId); + if (!reportId) return toast.error("reportId is required"); + const res = await fetch("/api/v2/admin/bug-report", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + reportId, + }), + }); + const data = await res.json(); + if (res.status === 200) { + toast.success(data.message); + } + } catch (error) { + console.log(`error while resolving ${error}`); + } }; return ( <div className="flex flex-col gap-5 px-5 py-10 h-full"> @@ -39,7 +118,21 @@ export default function AdminDashboard({ </div> <div className="grid grid-cols-2 gap-5 h-full"> <div className="flex flex-col gap-2"> - <p className="font-semibold">Broadcast</p> + <p className="flex items-center gap-2 font-semibold"> + Broadcast + <span className="relative w-5 h-5 flex-center shrink-0"> + <span + className={`absolute animate-ping inline-flex h-full w-full rounded-full ${ + broadcast?.show === true ? "bg-green-500" : "hidden" + } opacity-75`} + ></span> + <span + className={`relative inline-flex rounded-full h-3 w-3 ${ + broadcast?.show === true ? "bg-green-500" : "bg-red-500" + }`} + ></span> + </span> + </p> <div className="flex flex-col justify-between bg-secondary rounded p-5 h-full"> <form onSubmit={handleSubmit}> <div className="mb-4"> @@ -70,16 +163,24 @@ export default function AdminDashboard({ id="selectedTime" value={selectedTime} onChange={(e) => setSelectedTime(e.target.value)} - required className="w-full px-3 py-2 border rounded-md focus:outline-none text-black" /> </div> - <button - type="submit" - className="bg-image text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300" - > - Submit - </button> + <div className="flex font-karla font-semibold gap-2"> + <button + type="submit" + className="bg-image text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300" + > + Broadcast + </button> + <button + type="button" + onClick={handleRemove} + className="bg-red-700 text-white py-2 px-4 rounded-md hover:bg-opacity-80 transition duration-300" + > + Remove + </button> + </div> </form> {unixTimestamp && ( <p> @@ -95,40 +196,85 @@ export default function AdminDashboard({ {report?.map((i, index) => ( <div key={index} - className="odd:bg-primary/80 even:bg-primary/40 p-2 flex justify-between items-center" + className="odd:bg-primary/80 even:bg-primary/40 hover:odd:bg-image/20 hover:even:bg-image/20 p-2 flex justify-between items-center" > - {i.desc}{" "} - {i.severity === "Low" && ( - <span className="relative w-5 h-5 flex-center shrink-0"> - {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */} - <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span> - </span> - )} - {i.severity === "Medium" && ( - <span className="relative w-5 h-5 flex-center shrink-0"> - {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */} - <span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span> - </span> - )} - {i.severity === "High" && ( - <span className="relative w-5 h-5 flex-center shrink-0"> - {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */} - <span className="relative animate-pulse inline-flex rounded-full h-3 w-3 bg-rose-500"></span> - </span> - )} - {i.severity === "Critical" && ( - <span className="relative w-5 h-5 flex-center shrink-0"> - <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-900 opacity-75"></span> - <span className="relative inline-flex rounded-full h-3 w-3 bg-red-900"></span> + <Link + href={i.url} + className="flex font-inter items-center gap-2 group" + > + {i.desc}{" "} + <span className="w-4 h-4 text-image group-hover:text-white"> + <svg + fill="none" + stroke="currentColor" + strokeWidth={1.5} + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" + /> + </svg> </span> - )} + </Link> + <div className="flex items-center gap-2"> + {i.severity === "Low" && ( + <span className="relative w-5 h-5 flex-center shrink-0"> + {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */} + <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span> + </span> + )} + {i.severity === "Medium" && ( + <span className="relative w-5 h-5 flex-center shrink-0"> + {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */} + <span className="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span> + </span> + )} + {i.severity === "High" && ( + <span className="relative w-5 h-5 flex-center shrink-0"> + {/* <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75"></span> */} + <span className="relative animate-pulse inline-flex rounded-full h-3 w-3 bg-rose-500"></span> + </span> + )} + {i.severity === "Critical" && ( + <span className="relative w-5 h-5 flex-center shrink-0"> + <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-900 opacity-75"></span> + <span className="relative inline-flex rounded-full h-3 w-3 bg-red-900"></span> + </span> + )} + <button + type="button" + onClick={() => { + setReportId(i?.id); + handleResolved(); + }} + className="w-6 h-6 hover:text-green-500" + > + <svg + fill="none" + stroke="currentColor" + strokeWidth={1.5} + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M4.5 12.75l6 6 9-13.5" + /> + </svg> + </button> + </div> </div> ))} </div> </div> </div> </div> - <div className="w-full h-full">a</div> </div> ); } diff --git a/components/admin/meta/AppendMeta.js b/components/admin/meta/AppendMeta.js index 1707ed2..e49fcad 100644 --- a/components/admin/meta/AppendMeta.js +++ b/components/admin/meta/AppendMeta.js @@ -1,7 +1,7 @@ import Loading from "@/components/shared/loading"; import Image from "next/image"; import { useState } from "react"; -import { toast } from "react-toastify"; +import { toast } from "sonner"; // Define a function to convert the data function convertData(episodes) { @@ -217,6 +217,7 @@ export default function AppendMeta({ api }) { </p> <Image src={i.image} + alt="query-image" width={500} height={500} className="w-[160px] h-[210px] object-cover" diff --git a/components/anime/episode.js b/components/anime/episode.js index 25ed997..a42307f 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -4,7 +4,7 @@ import ViewSelector from "./viewSelector"; import ThumbnailOnly from "./viewMode/thumbnailOnly"; import ThumbnailDetail from "./viewMode/thumbnailDetail"; import ListMode from "./viewMode/listMode"; -import { toast } from "react-toastify"; +import { toast } from "sonner"; export default function AnimeEpisode({ info, @@ -34,16 +34,12 @@ export default function AnimeEpisode({ info.status === "RELEASING" ? "true" : "false" }${isDub ? "&dub=true" : ""}` ).then((res) => res.json()); - const getMap = response.find((i) => i?.map === true) || response[0]; + const getMap = response.find((i) => i?.map === true); let allProvider = response; if (getMap) { allProvider = response.filter((i) => { - if ( - i?.providerId === "gogoanime" && - i?.providerId === "9anime" && - i?.map !== true - ) { + if (i?.providerId === "gogoanime" && i?.map !== true) { return null; } return i; @@ -66,9 +62,12 @@ export default function AnimeEpisode({ fetchData(); return () => { + setCurrentPage(1); setProviders(null); setMapProviders(null); }; + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [info.id, isDub]); const episodes = @@ -79,9 +78,7 @@ export default function AnimeEpisode({ const lastEpisodeIndex = currentPage * itemsPerPage; const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage; let currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); - if (isDub) { - currentEpisodes = currentEpisodes.filter((i) => i.hasDub === true); - } + const totalPages = Math.ceil(episodes.length / itemsPerPage); const handleChange = (event) => { @@ -104,6 +101,7 @@ export default function AnimeEpisode({ ) { setView(3); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [providerId, episodes]); useEffect(() => { @@ -122,6 +120,7 @@ export default function AnimeEpisode({ setWatch(null); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [episodes]); useEffect(() => { @@ -157,6 +156,7 @@ export default function AnimeEpisode({ return; } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [providerId, artStorage, info.id, session?.user?.name]); let debounceTimeout; @@ -173,12 +173,7 @@ export default function AnimeEpisode({ ); if (!res.ok) { console.log(res); - toast.error("Something went wrong", { - position: "bottom-left", - autoClose: 3000, - hideProgressBar: true, - theme: "colored", - }); + toast.error("Something went wrong"); setProviders([]); setLoading(false); } else { @@ -213,12 +208,7 @@ export default function AnimeEpisode({ }, 1000); } catch (err) { console.log(err); - toast.error("Something went wrong", { - position: "bottom-left", - autoClose: 3000, - hideProgressBar: true, - theme: "colored", - }); + toast.error("Something went wrong"); } }; diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js index 761a9fd..e5f58da 100644 --- a/components/anime/mobile/topSection.js +++ b/components/anime/mobile/topSection.js @@ -1,4 +1,9 @@ -import { PlayIcon, PlusIcon, ShareIcon } from "@heroicons/react/24/solid"; +import { + BookOpenIcon, + PlayIcon, + PlusIcon, + ShareIcon, +} from "@heroicons/react/24/solid"; import Image from "next/image"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -21,6 +26,8 @@ export default function DetailTop({ const [showAll, setShowAll] = useState(false); + const isAnime = info.type === "ANIME"; + useEffect(() => { setReadMore(false); }, [info.id]); @@ -29,7 +36,7 @@ export default function DetailTop({ try { if (navigator.share) { await navigator.share({ - title: `Watch Now - ${info?.title?.english}`, + title: `${isAnime ? "Watch" : "Read"} Now - ${info?.title?.english}`, // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, url: window.location.href, }); @@ -50,7 +57,7 @@ export default function DetailTop({ <div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12"> <div className="shrink-0 w-[180px] h-[250px] rounded overflow-hidden"> <Image - src={info?.coverImage?.extraLarge} + src={info?.coverImage?.extraLarge || info?.coverImage} alt="poster anime" width={300} height={300} @@ -59,8 +66,9 @@ export default function DetailTop({ </div> <div className="flex flex-col gap-4 items-center md:items-start justify-end w-full"> <div className="flex flex-col gap-1 text-center md:text-start"> - <h3 className="font-karla text-lg capitalize leading-none"> - {info?.season?.toLowerCase()} {info.seasonYear} + <h3 className="font-karla text-lg capitalize leading-none"> + {info?.season?.toLowerCase() || getMonth(info?.startDate?.month)}{" "} + {info.seasonYear || info?.startDate?.year} </h3> <h1 className="font-outfit font-extrabold text-2xl md:text-4xl line-clamp-2 text-white"> {info?.title?.romaji || info?.title?.english} @@ -87,12 +95,20 @@ export default function DetailTop({ onClick={() => router.push(watchUrl)} className={`${ !watchUrl ? "opacity-30 pointer-events-none" : "" - } w-[180px] flex-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`} + } w-[180px] flex-center text-lg font-karla font-semibold gap-2 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`} > - <PlayIcon className="w-5 h-5" /> + {isAnime ? ( + <PlayIcon className="w-5 h-5" /> + ) : ( + <BookOpenIcon className="w-5 h-5" /> + )} {progress > 0 ? ( statuses?.value === "COMPLETED" ? ( - "Rewatch" + isAnime ? ( + "Rewatch" + ) : ( + "Reread" + ) ) : !watchUrl && info?.nextAiringEpisode ? ( <span> {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} @@ -100,8 +116,10 @@ export default function DetailTop({ ) : ( "Continue" ) - ) : ( + ) : isAnime ? ( "Watch Now" + ) : ( + "Read Now" )} </button> <div className="flex gap-2"> @@ -121,14 +139,14 @@ export default function DetailTop({ onClick={handleShareClick} > <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> - Share Anime + Share {isAnime ? "Anime" : "Manga"} </span> <ShareIcon className="w-5 h-5" /> </button> <a target="_blank" rel="noopener noreferrer" - href={`https://anilist.co/anime/${info.id}`} + href={`https://anilist.co/${info.type.toLowerCase()}/${info.id}`} className="flex-center group relative w-10 h-10 bg-secondary rounded-full" > <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> @@ -156,18 +174,24 @@ export default function DetailTop({ <PlusIcon className="w-5 h-5" /> </button> <button - // href={watchUrl || ""} type="button" - // disabled={!watchUrl || info?.nextAiringEpisode} onClick={() => router.push(watchUrl)} className={`${ !watchUrl ? "opacity-30 pointer-events-none" : "" } flex items-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-2 px-4 bg-white`} > - <PlayIcon className="w-5 h-5" /> + {isAnime ? ( + <PlayIcon className="w-5 h-5" /> + ) : ( + <BookOpenIcon className="w-5 h-5" /> + )} {progress > 0 ? ( statuses?.value === "COMPLETED" ? ( - "Rewatch" + isAnime ? ( + "Rewatch" + ) : ( + "Reread" + ) ) : !watchUrl && info?.nextAiringEpisode ? ( <span> {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} @@ -175,8 +199,10 @@ export default function DetailTop({ ) : ( "Continue" ) - ) : ( + ) : isAnime ? ( "Watch Now" + ) : ( + "Read Now" )} </button> <button @@ -185,7 +211,7 @@ export default function DetailTop({ onClick={handleShareClick} > <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out"> - Share Anime + Share {isAnime ? "Anime" : "Manga"} </span> <ShareIcon className="w-5 h-5" /> </button> @@ -287,3 +313,11 @@ export default function DetailTop({ </div> ); } + +function getMonth(month) { + if (!month) return ""; + const formattedMonth = new Date(0, month).toLocaleString("default", { + month: "long", + }); + return formattedMonth; +} diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js index 5beded1..a6a1cf6 100644 --- a/components/anime/viewMode/listMode.js +++ b/components/anime/viewMode/listMode.js @@ -19,7 +19,7 @@ export default function ListMode({ href={`/en/anime/watch/${info.id}/${providerId}?id=${encodeURIComponent( episode.id )}&num=${episode.number}${dub ? `&dub=${dub}` : ""}`} - className={`flex gap-3 py-4 hover:bg-secondary/10 odd:bg-secondary/30 even:bg-primary`} + className={`flex gap-3 py-4 hover:bg-secondary odd:bg-secondary/30 even:bg-primary`} > <div className="flex w-full"> <span className="shrink-0 px-4 text-center text-white/50"> diff --git a/components/home/content.js b/components/home/content.js index 651d276..678549c 100644 --- a/components/home/content.js +++ b/components/home/content.js @@ -13,8 +13,8 @@ import { parseCookies } from "nookies"; import { ChevronLeftIcon } from "@heroicons/react/20/solid"; import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; import { useRouter } from "next/router"; -import { toast } from "react-toastify"; import HistoryOptions from "./content/historyOptions"; +import { toast } from "sonner"; export default function Content({ ids, @@ -24,6 +24,7 @@ export default function Content({ og, userName, setRemoved, + type = "anime", }) { const router = useRouter(); @@ -53,6 +54,7 @@ export default function Content({ } else if (lang === "id") { setLang("id"); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const [scrollLeft, setScrollLeft] = useState(false); @@ -174,14 +176,7 @@ export default function Content({ setRemoved(id || aniId); if (data?.message === "Episode deleted") { - toast.success("Episode removed from history", { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", - }); + toast.success("Episode removed from history"); } } else { if (id) { @@ -259,7 +254,7 @@ export default function Content({ href={ ids === "listManga" ? `/en/manga/${anime.id}` - : `/${lang}/anime/${anime.id}` + : `/en/${type}/${anime.id}` } className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative" title={anime.title.romaji} @@ -352,7 +347,7 @@ export default function Content({ href={ ids === "listManga" ? `/en/manga/${anime.id}` - : `/en/anime/${anime.id}` + : `/en/${type.toLowerCase()}/${anime.id}` } className="w-[135px] lg:w-[185px] line-clamp-2" title={anime.title.romaji} diff --git a/components/home/genres.js b/components/home/genres.js index f054fc9..cd247ce 100644 --- a/components/home/genres.js +++ b/components/home/genres.js @@ -47,6 +47,7 @@ export default function Genres() { } else if (lang === "id") { setLang("id"); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <div className="antialiased"> diff --git a/components/home/schedule.js b/components/home/schedule.js index a0ab691..bb35d08 100644 --- a/components/home/schedule.js +++ b/components/home/schedule.js @@ -48,21 +48,26 @@ export default function Schedule({ data, scheduleData, anime, update }) { <h1 className="font-bold font-karla text-[20px] lg:px-5"> Don't miss out! </h1> - <div className="rounded mb-5 shadow-md shadow-black"> + <div className="rounded mb-5 shadow-tersier/50 shadow-button"> <div className="overflow-hidden w-full h-[96px] lg:h-[10rem] rounded relative"> - <div className="absolute flex flex-col lg:gap-1 justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-tersier to-transparent w-full h-full"> - <h1 className="text-xs lg:text-lg">Coming Up Next!</h1> - <div className="w-1/2 lg:w-2/5 hidden lg:block font-karla font-medium"> + <div className="absolute flex flex-col -space-y-1 lg:gap-1 justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-tersier to-transparent w-full h-full"> + <h1 className="text-xs lg:text-lg font-karla font-thin"> + Coming Up Next! + </h1> + <div className="w-1/2 lg:w-2/5 hidden lg:flex font-karla font-semibold line-clamp-2"> <Link href={`/en/anime/${data.id}`} - className="hover:underline underline-offset-4 decoration-2 leading-3 lg:text-[1.5vw]" + className="hover:underline underline-offset-4 decoration-2 leading-8 line-clamp-2 lg:text-[1.5vw]" > {data.title.romaji || data.title.english || data.title.native} </Link> </div> - <h1 className="w-1/2 lg:hidden font-medium font-karla leading-9 text-white line-clamp-1"> + <Link + href={`/en/anime/${data.id}`} + className="w-1/2 lg:hidden font-medium font-karla leading-9 text-white line-clamp-1" + > {data.title.romaji || data.title.english || data.title.native} - </h1> + </Link> </div> {data.bannerImage ? ( <Image @@ -79,16 +84,16 @@ export default function Schedule({ data, scheduleData, anime, update }) { height={500} sizes="100vw" alt="banner next anime" - className="absolute z-10 top-0 right-0 h-full object-contain object-right brightness-[90%]" + className="absolute z-10 top-0 right-0 w-3/4 lg:w-auto h-full object-cover lg:object-contain object-right opacity-30 lg:opacity-100 brightness-[90%]" /> )} <div - className={`absolute flex justify-end items-center pr-5 gap-5 md:gap-10 z-20 w-1/2 h-full right-0 ${ - data.bannerImage ? "md:pr-16" : "md:pr-48" + className={`absolute flex justify-end items-center pr-5 gap-5 lg:gap-10 z-20 w-1/2 h-full right-0 ${ + data.bannerImage ? "lg:pr-16" : "lg:pr-48" }`} > {/* Countdown Timer */} - <div className="flex items-center gap-2 md:gap-5 font-bold font-karla text-sm md:text-xl"> + <div className="flex items-center gap-2 lg:gap-5 font-bold font-karla text-sm lg:text-xl"> {/* Countdown Timer */} <div className="flex flex-col items-center"> <span className="text-action/80">{day}</span> diff --git a/components/listEditor.js b/components/listEditor.js index fa249e3..f4f46ea 100644 --- a/components/listEditor.js +++ b/components/listEditor.js @@ -1,7 +1,7 @@ import { useState } from "react"; import Image from "next/image"; -import { toast } from "react-toastify"; import { useRouter } from "next/router"; +import { toast } from "sonner"; const ListEditor = ({ animeId, @@ -9,11 +9,12 @@ const ListEditor = ({ stats, prg, max, - image = null, + info = null, close, }) => { const [status, setStatus] = useState(stats ?? "CURRENT"); const [progress, setProgress] = useState(prg ?? 0); + const isAnime = info?.type === "ANIME"; const router = useRouter(); @@ -47,27 +48,11 @@ const ListEditor = ({ }); const { data } = await response.json(); if (data.SaveMediaListEntry === null) { - toast.error("Something went wrong", { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: true, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - theme: "colored", - }); + toast.error("Something went wrong"); return; } console.log("Saved media list entry", data); - toast.success("Media list entry saved", { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: true, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - theme: "dark", - }); + toast.success("Media list entry saved"); close(); setTimeout(() => { // window.location.reload(); @@ -75,15 +60,7 @@ const ListEditor = ({ }, 1000); // showAlert("Media list entry saved", "success"); } catch (error) { - toast.error("Something went wrong", { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: true, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - theme: "colored", - }); + toast.error("Something went wrong"); console.error(error); } }; @@ -95,10 +72,10 @@ const ListEditor = ({ </div> <div className="relative bg-secondary rounded-sm w-screen md:w-auto"> <div className="md:flex"> - {image && ( + {info?.bannerImage && ( <div> <Image - src={image.coverImage.large} + src={info.coverImage.large} alt="image" height={500} width={500} @@ -106,9 +83,9 @@ const ListEditor = ({ /> <Image src={ - image.bannerImage || - image.coverImage.extraLarge || - image.coverImage.large + info.bannerImage || + info.coverImage.extraLarge || + info.coverImage.large } alt="image" height={500} @@ -136,11 +113,15 @@ const ListEditor = ({ onChange={(e) => setStatus(e.target.value)} className="rounded-sm px-2 py-1 bg-[#363642] w-[50%] sm:w-[150px] text-sm sm:text-base" > - <option value="CURRENT">Watching</option> + <option value="CURRENT"> + {isAnime ? "Watching" : "Reading"} + </option> <option value="COMPLETED">Completed</option> <option value="PAUSED">Paused</option> <option value="DROPPED">Dropped</option> - <option value="PLANNING">Plan to watch</option> + <option value="PLANNING"> + Plan to {isAnime ? "watch" : "read"} + </option> </select> </div> <div className="flex justify-between items-center mt-2"> diff --git a/components/manga/chapters.js b/components/manga/chapters.js index fd7beea..2150686 100644 --- a/components/manga/chapters.js +++ b/components/manga/chapters.js @@ -1,13 +1,16 @@ import Link from "next/link"; import { useState, useEffect } from "react"; -import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { setCookie } from "nookies"; +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "@heroicons/react/24/outline"; -const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => { +const ChapterSelector = ({ chaptersData, data, setWatch, mangaId }) => { const [selectedProvider, setSelectedProvider] = useState( chaptersData[0]?.providerId || "" ); - const [selectedChapter, setSelectedChapter] = useState(""); + // const [selectedChapter, setSelectedChapter] = useState(""); const [chapters, setChapters] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [chaptersPerPage] = useState(10); @@ -16,13 +19,15 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => { const selectedChapters = chaptersData.find( (c) => c.providerId === selectedProvider ); - if (selectedChapters) { - setSelectedChapter(selectedChapters); - setFirstEp(selectedChapters); - } setChapters(selectedChapters?.chapters || []); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedProvider, chaptersData]); + useEffect(() => { + setCurrentPage(1); + }, [data.id]); + // Get current posts const indexOfLastChapter = currentPage * chaptersPerPage; const indexOfFirstChapter = indexOfLastChapter - chaptersPerPage; @@ -31,24 +36,6 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => { indexOfLastChapter ); - // Change page - const paginate = (pageNumber) => setCurrentPage(pageNumber); - const nextPage = () => setCurrentPage((prev) => prev + 1); - const prevPage = () => setCurrentPage((prev) => prev - 1); - - function saveManga() { - localStorage.setItem( - "manga", - JSON.stringify({ manga: selectedChapter, data: data }) - ); - setCookie(null, "manga", data.id, { - maxAge: 24 * 60 * 60, - path: "/", - }); - } - - // console.log(selectedChapter); - // Create page numbers const pageNumbers = []; for (let i = 1; i <= Math.ceil(chapters.length / chaptersPerPage); i++) { @@ -59,7 +46,7 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => { const getDisplayedPageNumbers = (currentPage, totalPages, margin) => { const pageRange = [...Array(totalPages).keys()].map((i) => i + 1); - if (totalPages <= 10) { + if (totalPages <= 5) { return pageRange; } @@ -83,104 +70,147 @@ const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => { const displayedPageNumbers = getDisplayedPageNumbers( currentPage, pageNumbers.length, - 9 + 3 ); - // console.log(currentChapters); + useEffect(() => { + if (chapters) { + const getEpi = data?.nextAiringEpisode + ? chapters[data?.mediaListEntry?.progress] + : chapters[0]; + if (getEpi) { + const watchUrl = `/en/manga/read/${selectedProvider}?id=${mangaId}&chapterId=${encodeURIComponent( + getEpi.id + )}&anilist=${data.id}&num=${getEpi.number}`; + setWatch(watchUrl); + } else { + setWatch(null); + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chapters]); return ( - <div className="flex flex-col items-center z-40"> - <div className="flex flex-col w-full"> - <label htmlFor="provider" className="text-sm md:text-base font-medium"> - Select a Provider - </label> - <div className="relative w-full"> + <div className="flex flex-col gap-2 px-3"> + <div className="flex justify-between"> + <h1 className="text-[20px] lg:text-2xl font-bold font-karla"> + Chapters + </h1> + <div className="relative flex gap-2 items-center group"> <select id="provider" - className="w-full text-xs md:text-base cursor-pointer mt-2 p-2 focus:outline-none rounded-md appearance-none bg-secondary" + 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: focus:ring-action group-hover: group-hover:ring-action" value={selectedProvider} onChange={(e) => setSelectedProvider(e.target.value)} > {/* <option value="">--Select a provider--</option> */} {chaptersData.map((provider, index) => ( - <option key={index} value={provider.providerId}> + <option key={provider.providerId} value={provider.providerId}> {provider.providerId} </option> ))} </select> - <ChevronDownIcon className="absolute md:right-5 right-3 md:bottom-2 m-auto md:w-6 md:h-6 bottom-[0.5rem] h-4 w-4" /> + <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> </div> </div> - <div className="mt-4 w-full py-5 flex justify-between gap-5"> - <button - onClick={prevPage} - disabled={currentPage === 1} - className={`w-24 py-1 shrink-0 rounded-md font-karla ${ - currentPage === 1 - ? "bg-[#1D1D20] text-[#313135]" - : `bg-secondary hover:bg-[#363639]` - }`} - > - Previous - </button> - <div className="flex gap-5 overflow-x-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-thumb- w-[420px] lg:w-auto"> - {displayedPageNumbers.map((number, index) => - number === "..." ? ( - <span key={index + 2} className="w-10 py-1 text-center"> - ... - </span> - ) : ( + + <div className="flex flex-col items-center z-40"> + <div className="mt-4 w-full"> + {currentChapters.map((chapter, index) => { + const isRead = chapter.number <= data?.mediaListEntry?.progress; + return ( + <Link + key={index} + href={`/en/manga/read/${selectedProvider}?id=${mangaId}&chapterId=${encodeURIComponent( + chapter.id + )}${data?.id?.length > 6 ? "" : `&anilist=${data.id}`}&num=${ + chapter.number + }`} + className={`flex gap-3 py-4 hover:bg-secondary odd:bg-secondary/30 even:bg-primary`} + > + <div className="flex w-full"> + <span className="shrink-0 px-4 text-center text-white/50"> + {chapter.number} + </span> + <p + className={`w-full line-clamp-1 ${ + isRead ? "text-[#5f5f5f]" : "text-white" + } + `} + > + {chapter.title || `Chapter ${chapter.number}`} + </p> + <p className="capitalize text-sm text-white/50 px-4"> + {selectedProvider} + </p> + </div> + </Link> + ); + })} + </div> + + <div className="flex flex-col mt-5 md:flex-row w-full sm:items-center sm:justify-between"> + <div className="flex-center"> + <p className="text-sm text-txt"> + Showing{" "} + <span className="font-medium">{indexOfFirstChapter + 1}</span> to{" "} + <span className="font-medium"> + {indexOfLastChapter > chapters.length + ? chapters.length + : indexOfLastChapter} + </span>{" "} + of <span className="font-medium">{chapters.length}</span> chapters + </p> + </div> + <div className="flex-center"> + <nav + className="isolate inline-flex space-x-1 rounded-md shadow-sm" + aria-label="Pagination" + > <button - key={number} - onClick={() => paginate(number)} - className={`w-10 shrink-0 py-1 rounded-md hover:bg-[#363639] ${ - number === currentPage ? "bg-[#363639]" : "bg-secondary" + onClick={() => setCurrentPage((prev) => prev - 1)} + disabled={currentPage === 1} + className={`relative inline-flex items-center rounded px-2 py-2 text-gray-400 hover:bg-secondary focus:z-20 focus:outline-offset-0 ${ + currentPage === 1 + ? "opacity-50 cursor-default pointer-events-none" + : "" }`} > - {number} + <span className="sr-only">Previous</span> + <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" /> </button> - ) - )} - </div> - <button - onClick={nextPage} - disabled={currentPage === pageNumbers.length} - className={`w-24 py-1 shrink-0 rounded-md font-karla ${ - currentPage === pageNumbers.length - ? "bg-[#1D1D20] text-[#313135]" - : `bg-secondary hover:bg-[#363639]` - }`} - > - Next - </button> - </div> - <div className="mt-4 w-full"> - {currentChapters.map((chapter, index) => { - const isRead = chapter.number <= userManga?.progress; - return ( - <div key={index} className="p-2 border-b hover:bg-[#232325]"> - <Link - href={`/en/manga/read/${selectedProvider}?id=${ - data.id - }&chapterId=${encodeURIComponent(chapter.id)}`} - onClick={saveManga} + <div className="flex w-full gap-1 overflow-x-scroll scrollbar-thin scrollbar-thumb-image scrollbar-thumb-rounded"> + {displayedPageNumbers.map((pageNumber, index) => ( + <button + key={index} + onClick={() => setCurrentPage(pageNumber)} + disabled={pageNumber === "..."} + className={`relative rounded inline-flex items-center px-4 py-2 text-sm font-semibold text-txt hover:bg-secondary focus:z-20 focus:outline-offset-0 ${ + currentPage === pageNumber + ? "z-10 bg-secondary rounded text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-none" + : "" + }`} + > + {pageNumber} + </button> + ))} + </div> + <button + onClick={() => setCurrentPage((prev) => prev + 1)} + disabled={currentPage === pageNumbers.length} + className={`relative inline-flex items-center rounded px-2 py-2 text-gray-400 hover:bg-secondary focus:z-20 focus:outline-offset-0 ${ + currentPage === pageNumbers.length + ? "opacity-50 cursor-default" + : "" + }`} > - <h2 - className={`text-lg font-medium ${ - isRead ? "text-[#424245]" : "" - }`} - > - {chapter.title} - </h2> - <p - className={`text-[#59595d] ${isRead ? "text-[#313133]" : ""}`} - > - Updated At: {new Date(chapter.updatedAt).toLocaleString()} - </p> - </Link> - </div> - ); - })} + <span className="sr-only">Next</span> + <ChevronRightIcon className="h-5 w-5" aria-hidden="true" /> + </button> + </nav> + </div> + </div> </div> </div> ); diff --git a/components/manga/info/mobile/mobileButton.js b/components/manga/info/mobile/mobileButton.js deleted file mode 100644 index 0016b59..0000000 --- a/components/manga/info/mobile/mobileButton.js +++ /dev/null @@ -1,39 +0,0 @@ -import Link from "next/link"; -import AniList from "../../../media/aniList"; -import { BookOpenIcon } from "@heroicons/react/24/outline"; - -export default function MobileButton({ info, firstEp, saveManga }) { - return ( - <div className="md:hidden flex items-center gap-4 w-full pb-3"> - <button - disabled={!firstEp} - onClick={saveManga} - className={`${ - !firstEp - ? "pointer-events-none text-white/50 bg-secondary/50" - : "bg-secondary text-white" - } lg:w-full font-bold shadow-md shadow-secondary hover:bg-secondary/90 hover:text-white/50 rounded`} - > - <Link - href={`/en/manga/read/${firstEp?.providerId}?id=${ - info.id - }&chapterId=${encodeURIComponent( - firstEp?.chapters[firstEp.chapters.length - 1].id - )}`} - className="flex items-center text-xs font-karla gap-2 h-[30px] px-2" - > - <h1>Read Now</h1> - <BookOpenIcon className="w-4 h-4" /> - </Link> - </button> - <Link - href={`https://anilist.co/manga/${info.id}`} - className="flex-center rounded bg-secondary shadow-md shadow-secondary h-[30px] lg:px-4 px-2" - > - <div className="flex-center w-5 h-5"> - <AniList /> - </div> - </Link> - </div> - ); -} diff --git a/components/manga/info/mobile/topMobile.js b/components/manga/info/mobile/topMobile.js deleted file mode 100644 index 2e6b23a..0000000 --- a/components/manga/info/mobile/topMobile.js +++ /dev/null @@ -1,16 +0,0 @@ -import Image from "next/image"; - -export default function TopMobile({ info }) { - return ( - <div className="md:hidden"> - <Image - src={info.coverImage} - width={500} - height={500} - alt="cover image" - className="md:hidden absolute top-0 left-0 -translate-y-24 w-full h-[30rem] object-cover rounded-sm shadow-lg brightness-75" - /> - <div className="absolute top-0 left-0 w-full -translate-y-24 h-[32rem] bg-gradient-to-t from-primary to-transparent from-50%"></div> - </div> - ); -} diff --git a/components/manga/info/topSection.js b/components/manga/info/topSection.js deleted file mode 100644 index 45d5f11..0000000 --- a/components/manga/info/topSection.js +++ /dev/null @@ -1,107 +0,0 @@ -import Image from "next/image"; -import { BookOpenIcon } from "@heroicons/react/24/outline"; -import AniList from "../../media/aniList"; -import Link from "next/link"; -import TopMobile from "./mobile/topMobile"; -import MobileButton from "./mobile/mobileButton"; - -export default function TopSection({ info, firstEp, setCookie }) { - const slicedGenre = info.genres?.slice(0, 3); - - function saveManga() { - localStorage.setItem( - "manga", - JSON.stringify({ manga: firstEp, data: info }) - ); - - setCookie(null, "manga", info.id, { - maxAge: 24 * 60 * 60, - path: "/", - }); - } - - return ( - <div className="flex md:gap-5 w-[90%] xl:w-[70%] z-30"> - <TopMobile info={info} /> - <div className="hidden md:block w-[7rem] xs:w-[10rem] lg:w-[15rem] space-y-3 shrink-0 rounded-sm"> - <Image - src={info.coverImage} - width={500} - height={500} - priority - alt="cover image" - className="hidden md:block object-cover h-[10rem] xs:h-[14rem] lg:h-[22rem] rounded-sm shadow-lg shadow-[#1b1b1f] bg-[#34343b]/20" - /> - - <div className="hidden md:flex items-center justify-between w-full lg:gap-5 pb-3"> - <button - disabled={!firstEp} - onClick={saveManga} - className={`${ - !firstEp - ? "pointer-events-none text-white/50 bg-tersier/50" - : "bg-tersier text-white" - } lg:w-full font-bold shadow-md shadow-[#0E0E0F] hover:bg-tersier/90 hover:text-white/50 rounded-md`} - > - <Link - href={`/en/manga/read/${firstEp?.providerId}?id=${ - info.id - }&chapterId=${encodeURIComponent( - firstEp?.chapters[firstEp.chapters.length - 1].id - )}`} - className="flex items-center lg:justify-center text-sm lg:text-base font-karla gap-2 h-[35px] lg:h-[40px] px-2" - > - <h1>Read Now</h1> - <BookOpenIcon className="w-5 h-5" /> - </Link> - </button> - <Link - href={`https://anilist.co/manga/${info.id}`} - className="flex-center rounded-md bg-tersier shadow-md shadow-[#0E0E0F] h-[35px] lg:h-[40px] lg:px-4 px-2" - > - <div className="flex-center w-5 h-5"> - <AniList /> - </div> - </Link> - </div> - </div> - <div className="w-full flex flex-col justify-start z-40"> - <div className="md:h-1/2 py-2 md:py-5 flex flex-col md:gap-2 justify-end"> - <h1 className="title text-xl md:text-2xl xl:text-3xl text-white font-semibold font-karla line-clamp-1 text-start"> - {info.title?.romaji || info.title?.english || info.title?.native} - </h1> - <span className="flex flex-wrap text-xs lg:text-sm md:text-[#747478]"> - {slicedGenre && - slicedGenre.map((genre, index) => { - return ( - <div key={index} className="flex"> - {genre} - {index < slicedGenre?.length - 1 && ( - <span className="mx-2 text-sm text-[#747478]">•</span> - )} - </div> - ); - })} - </span> - </div> - - <MobileButton info={info} firstEp={firstEp} saveManga={saveManga} /> - - <div className="hidden md:block relative h-1/2"> - {/* <span className="font-semibold text-sm">Description</span> */} - <div - className={`relative group h-[8rem] lg:h-[12.5rem] text-sm lg:text-base overflow-y-scroll scrollbar-hide`} - > - <p - dangerouslySetInnerHTML={{ __html: info.description }} - className="pb-5 pt-2 leading-5" - /> - </div> - <div - className={`absolute bottom-0 w-full bg-gradient-to-b from-transparent to-secondary to-50% h-[2rem]`} - /> - </div> - </div> - </div> - ); -} diff --git a/components/manga/leftBar.js b/components/manga/leftBar.js index 17acd55..5a98115 100644 --- a/components/manga/leftBar.js +++ b/components/manga/leftBar.js @@ -1,14 +1,23 @@ +import { getHeaders, getRandomId } from "@/utils/imageUtils"; import { ArrowLeftIcon } from "@heroicons/react/24/solid"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -export function LeftBar({ data, page, info, currentId, setSeekPage }) { +export function LeftBar({ + data, + page, + info, + currentId, + setSeekPage, + number, + mediaId, + providerId, +}) { const router = useRouter(); function goBack() { router.push(`/en/manga/${info.id}`); } - // console.log(info); return ( <div className="hidden lg:block shrink-0 w-[16rem] h-screen overflow-y-auto scrollbar-none bg-secondary relative group"> <div className="grid"> @@ -37,23 +46,27 @@ export function LeftBar({ data, page, info, currentId, setSeekPage }) { <h1 className="font-bold xl:text-lg">Chapters</h1> <div className="px-2"> <div className="w-full text-sm xl:text-base px-1 h-[8rem] xl:h-[30vh] bg-[#161617] rounded-md overflow-auto scrollbar-thin scrollbar-thumb-[#363639] scrollbar-thumb-rounded-md hover:scrollbar-thumb-[#424245]"> - {data?.chapters?.map((x) => { + {data?.chapters?.map((x, index) => { return ( <div - key={x.id} + key={getRandomId()} className={`${ x.id === currentId && "text-action" } py-1 px-2 hover:bg-[#424245] rounded-sm`} > <Link - href={`/en/manga/read/${data.providerId}?id=${ - info.id - }&chapterId=${encodeURIComponent(x.id)}`} + href={`/en/manga/read/${ + data.providerId + }?id=${mediaId}&chapterId=${encodeURIComponent(x.id)}${ + info?.id?.length > 6 ? "" : `&anilist=${info?.id}` + }&num=${x.number}`} className="" > <h1 className="line-clamp-1"> - <span className="font-bold">{x.number}.</span>{" "} - {x.title} + <span className="font-bold"> + {x.number || index + 1}. + </span>{" "} + {x.title || `Chapter ${x.number || index + 1}`} </h1> </Link> </div> @@ -69,28 +82,37 @@ export function LeftBar({ data, page, info, currentId, setSeekPage }) { <div className="text-center w-full px-1 h-[30vh] bg-[#161617] rounded-md overflow-auto scrollbar-thin scrollbar-thumb-[#363639] scrollbar-thumb-rounded-md hover:scrollbar-thumb-[#424245]"> {Array.isArray(page) ? ( <div className="grid grid-cols-2 gap-5 py-4 px-2 place-items-center"> - {page?.map((x) => { + {page?.map((x, index) => { return ( <div - key={x.url} + key={getRandomId()} className="hover:bg-[#424245] cursor-pointer rounded-sm w-full" > <div className="flex flex-col items-center cursor-pointer" - onClick={() => setSeekPage(x.index)} + onClick={() => setSeekPage(index)} > <Image src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent( x.url - )}&headers=${encodeURIComponent( - JSON.stringify({ Referer: x.headers.Referer }) - )}`} + )}${ + x?.headers?.Referer + ? `&headers=${encodeURIComponent( + JSON.stringify(x?.headers) + )}` + : `&headers=${encodeURIComponent( + JSON.stringify(getHeaders(providerId)) + )}` + }`} + // &headers=${encodeURIComponent( + // JSON.stringify({ Referer: x.headers.Referer }) + // )} alt="chapter image" width={100} height={200} className="w-full h-[120px] object-contain scale-90" /> - <h1>Page {x.index + 1}</h1> + <h1>Page {index + 1}</h1> </div> </div> ); @@ -98,7 +120,7 @@ export function LeftBar({ data, page, info, currentId, setSeekPage }) { </div> ) : ( <div className="py-4"> - <p>{page.error || "No Pages."}</p> + <p>{page?.error || "No Pages."}</p> </div> )} </div> diff --git a/components/manga/mobile/bottomBar.js b/components/manga/mobile/bottomBar.js index 6493dca..5b28de4 100644 --- a/components/manga/mobile/bottomBar.js +++ b/components/manga/mobile/bottomBar.js @@ -1,3 +1,4 @@ +import { getHeaders } from "@/utils/imageUtils"; import { ChevronLeftIcon, ChevronRightIcon, @@ -14,12 +15,15 @@ export default function BottomBar({ nextChapter, currentPage, chapter, - page, + data, setSeekPage, setIsOpen, + number, + mangadexId, }) { const [openPage, setOpenPage] = useState(false); const router = useRouter(); + return ( <div className={`fixed lg:hidden flex flex-col gap-3 z-50 h-auto w-screen ${ @@ -39,7 +43,9 @@ export default function BottomBar({ router.push( `/en/manga/read/${ chapter.providerId - }?id=${id}&chapterId=${encodeURIComponent(prevChapter)}` + }?id=${mangadexId}&chapterId=${encodeURIComponent( + prevChapter.id + )}${id > 6 ? "" : `&anilist=${id}`}&num=${prevChapter.number}` ) } > @@ -56,7 +62,9 @@ export default function BottomBar({ router.push( `/en/manga/read/${ chapter.providerId - }?id=${id}&chapterId=${encodeURIComponent(nextChapter)}` + }?id=${mangadexId}&chapterId=${encodeURIComponent( + nextChapter.id + )}${id > 6 ? "" : `&anilist=${id}`}&num=${nextChapter.number}` ) } > @@ -82,13 +90,14 @@ export default function BottomBar({ <RectangleStackIcon className="w-5 h-5" /> </button> </div> - <span className="flex bg-secondary shadow-lg ring-1 ring-black ring-opacity-5 p-2 rounded-md">{`${currentPage}/${page.length}`}</span> + <span className="flex bg-secondary shadow-lg ring-1 ring-black ring-opacity-5 p-2 rounded-md">{`${currentPage}/${data?.length}`}</span> </div> {openPage && ( <div className="bg-secondary flex justify-center h-full w-screen py-2"> <div className="flex overflow-scroll"> - {Array.isArray(page) ? ( - page.map((x) => { + {Array.isArray(data) ? ( + data.map((x, index) => { + const indx = index + 1; return ( <div key={x.url} @@ -101,9 +110,18 @@ export default function BottomBar({ <Image src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent( x.url - )}&headers=${encodeURIComponent( - JSON.stringify({ Referer: x.headers.Referer }) - )}`} + )}${ + x?.headers?.Referer + ? `&headers=${encodeURIComponent( + JSON.stringify(x?.headers) + )}` + : `&headers=${encodeURIComponent( + JSON.stringify(getHeaders(chapter.providerId)) + )}` + }`} + // &headers=${encodeURIComponent( + // JSON.stringify({ Referer: x.headers.Referer }) + // )} alt="chapter image" width={100} height={200} diff --git a/components/manga/mobile/hamburgerMenu.js b/components/manga/mobile/hamburgerMenu.js deleted file mode 100644 index fcdbcce..0000000 --- a/components/manga/mobile/hamburgerMenu.js +++ /dev/null @@ -1,228 +0,0 @@ -import React, { useState, useEffect } from "react"; -import Link from "next/link"; -import { useSession, signIn, signOut } from "next-auth/react"; -import Image from "next/image"; -import { parseCookies } from "nookies"; - -export default function HamburgerMenu() { - const { data: session } = useSession(); - const [isVisible, setIsVisible] = useState(false); - const [fade, setFade] = useState(false); - - const [lang, setLang] = useState("en"); - const [cookie, setCookies] = useState(null); - - const handleShowClick = () => { - setIsVisible(true); - setFade(true); - }; - - const handleHideClick = () => { - setIsVisible(false); - setFade(false); - }; - - useEffect(() => { - let lang = null; - if (!cookie) { - const cookie = parseCookies(); - lang = cookie.lang || null; - setCookies(cookie); - } - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); - return ( - <> - {!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> - )} - - {/* Mobile Menu */} - <div - className={`transition-all duration-150 ${ - fade ? "opacity-100" : "opacity-0" - } z-50`} - > - {isVisible && session && ( - <Link - href={`/${lang}/profile/${session?.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]" - > - <Image - src={session?.user.image.large} - alt="user avatar" - height={500} - width={500} - 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={`/${lang}/`} 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={`/${lang}/`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - home - </Link> - </button> - <button className="group flex flex-col items-center"> - <Link href={`/${lang}/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={`/${lang}/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={`/${lang}/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={`/${lang}/search/anime`} - className="font-karla font-bold text-[#8BA0B2] group-hover:text-action" - > - search - </Link> - </button> - {session ? ( - <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> - </> - ); -} diff --git a/components/manga/panels/firstPanel.js b/components/manga/panels/firstPanel.js index f1ee859..596fa58 100644 --- a/components/manga/panels/firstPanel.js +++ b/components/manga/panels/firstPanel.js @@ -4,10 +4,13 @@ import { ArrowsPointingInIcon, ChevronLeftIcon, ChevronRightIcon, + PlusIcon, + MinusIcon, } from "@heroicons/react/24/outline"; import Image from "next/image"; import { useRouter } from "next/router"; import { useAniList } from "../../../lib/anilist/useAnilist"; +import { getHeaders, getRandomId } from "@/utils/imageUtils"; export default function FirstPanel({ aniId, @@ -26,14 +29,20 @@ export default function FirstPanel({ mobileVisible, setMobileVisible, setCurrentPage, + number, + mangadexId, }) { const { markProgress } = useAniList(session); const [currentImageIndex, setCurrentImageIndex] = useState(0); const imageRefs = useRef([]); const scrollContainerRef = useRef(); + const [imageQuality, setImageQuality] = useState(80); + const router = useRouter(); + // console.log({ chapter }); + useEffect(() => { const handleScroll = () => { const scrollTop = scrollContainerRef.current.scrollTop; @@ -53,13 +62,17 @@ export default function FirstPanel({ } } - if (index === data.length - 3 && !hasRun.current) { + if (index === data?.length - 3 && !hasRun.current) { if (session) { + if (aniId?.length > 6) return; const currentChapter = chapter.chapters?.find( (x) => x.id === currentId ); if (currentChapter) { - markProgress(aniId, currentChapter.number); + const chapterNumber = + currentChapter.number ?? + chapter.chapters.indexOf(currentChapter) + 1; + markProgress(aniId, chapterNumber); console.log("marking progress"); } } @@ -82,8 +95,12 @@ export default function FirstPanel({ }); } }; + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, session, chapter]); + // console.log({ imageQuality }); + useEffect(() => { if (scrollContainerRef.current && seekPage !== currentImageIndex) { const targetImageRef = imageRefs.current[seekPage]; @@ -119,19 +136,26 @@ export default function FirstPanel({ {data && Array.isArray(data) && data?.length > 0 ? ( data.map((i, index) => ( <div - key={i.url} + key={getRandomId()} className="w-screen lg:h-auto lg:w-full" ref={(el) => (imageRefs.current[index] = el)} > <Image src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent( i.url - )}&headers=${encodeURIComponent( - JSON.stringify({ Referer: i.headers.Referer }) - )}`} - alt={i.index} + )}${ + i?.headers?.Referer + ? `&headers=${encodeURIComponent( + JSON.stringify(i?.headers) + )}` + : `&headers=${encodeURIComponent( + JSON.stringify(getHeaders(chapter.providerId)) + )}` + }`} + alt={index} width={500} height={500} + quality={imageQuality} onClick={() => setMobileVisible(!mobileVisible)} className="w-screen lg:w-full h-auto bg-[#bbb]" /> @@ -145,6 +169,26 @@ export default function FirstPanel({ )} </div> <div className="absolute hidden lg:flex bottom-5 left-5 gap-5"> + {/* <button + type="button" + disabled={imageQuality >= 100} + onClick={() => { + setImageQuality((prev) => (prev <= 100 ? prev + 10 : prev)); + }} + className="flex-center p-2 bg-secondary" + > + <PlusIcon className="w-5 h-5" /> + </button> + <button + type="button" + disabled={imageQuality <= 10} + onClick={() => { + setImageQuality((prev) => (prev >= 10 ? prev - 10 : prev)); + }} + className="flex-center p-2 bg-secondary" + > + <MinusIcon className="w-5 h-5" /> + </button> */} <span className="flex bg-secondary p-2 rounded-sm"> {visible ? ( <button type="button" onClick={() => setVisible(!visible)}> @@ -168,7 +212,11 @@ export default function FirstPanel({ router.push( `/en/manga/read/${ chapter.providerId - }?id=${aniId}&chapterId=${encodeURIComponent(prevChapter)}` + }?id=${mangadexId}&chapterId=${encodeURIComponent( + prevChapter?.id + )}${aniId?.length > 6 ? "" : `&anilist=${aniId}`}&num=${ + prevChapter?.number + }` ) } > @@ -185,7 +233,11 @@ export default function FirstPanel({ router.push( `/en/manga/read/${ chapter.providerId - }?id=${aniId}&chapterId=${encodeURIComponent(nextChapter)}` + }?id=${mangadexId}&chapterId=${encodeURIComponent( + nextChapter?.id + )}${aniId?.length > 6 ? "" : `&anilist=${aniId}`}&num=${ + nextChapter?.number + }` ) } > @@ -195,7 +247,7 @@ export default function FirstPanel({ </div> <span className="hidden lg:flex bg-secondary p-2 rounded-sm absolute bottom-5 right-5">{`Page ${ currentImageIndex + 1 - }/${data.length}`}</span> + }/${data?.length}`}</span> </section> ); } diff --git a/components/manga/panels/secondPanel.js b/components/manga/panels/secondPanel.js index 9323822..fa158b2 100644 --- a/components/manga/panels/secondPanel.js +++ b/components/manga/panels/secondPanel.js @@ -5,9 +5,11 @@ import { ArrowsPointingInIcon, } from "@heroicons/react/24/outline"; import { useAniList } from "../../../lib/anilist/useAnilist"; +import { getHeaders } from "@/utils/imageUtils"; export default function SecondPanel({ aniId, + chapterData, data, hasRun, currentChapter, @@ -17,6 +19,7 @@ export default function SecondPanel({ visible, setVisible, session, + providerId, }) { const [index, setIndex] = useState(0); const [image, setImage] = useState(null); @@ -26,6 +29,7 @@ export default function SecondPanel({ useEffect(() => { setIndex(0); setSeekPage(0); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, currentId]); const seekToIndex = (newIndex) => { @@ -41,6 +45,7 @@ export default function SecondPanel({ useEffect(() => { seekToIndex(seekPage); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [seekPage]); useEffect(() => { @@ -63,13 +68,14 @@ export default function SecondPanel({ } if (index + 1 >= image.length - 4 && !hasRun.current) { - let chapterNumber = currentChapter?.number; - if (chapterNumber % 1 !== 0) { - // If it's a decimal, round it - chapterNumber = Math.round(chapterNumber); - } + const current = chapterData.chapters?.find( + (x) => x.id === currentChapter.id + ); + const chapterNumber = chapterData.chapters.indexOf(current) + 1; - markProgress(aniId, chapterNumber); + if (chapterNumber) { + markProgress(aniId, chapterNumber); + } hasRun.current = true; } } @@ -80,6 +86,7 @@ export default function SecondPanel({ return () => { window.removeEventListener("keydown", handleKeyDown); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [index, image]); const handleNext = () => { @@ -90,10 +97,13 @@ export default function SecondPanel({ if (index + 1 >= image.length - 4 && !hasRun.current) { console.log("marking progress"); - let chapterNumber = currentChapter?.number; - if (chapterNumber % 1 !== 0) { - // If it's a decimal, round it - chapterNumber = Math.round(chapterNumber); + const current = chapterData.chapters?.find( + (x) => x.id === currentChapter.id + ); + const chapterNumber = chapterData.chapters.indexOf(current) + 1; + + if (chapterNumber) { + markProgress(aniId, chapterNumber); } markProgress(aniId, chapterNumber); @@ -107,6 +117,7 @@ export default function SecondPanel({ setSeekPage(index - 2); } }; + return ( <div className="flex-grow h-screen"> <div className="flex items-center w-full relative group"> @@ -127,11 +138,17 @@ export default function SecondPanel({ className="w-1/2 h-screen object-contain" src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent( image[image.length - index - 2]?.url - )}&headers=${encodeURIComponent( - JSON.stringify({ - Referer: image[image.length - index - 2]?.headers.Referer, - }) - )}`} + )}${ + image[image.length - index - 2]?.headers?.Referer + ? `&headers=${encodeURIComponent( + JSON.stringify( + image[image.length - index - 2]?.headers + ) + )}` + : `&headers=${encodeURIComponent( + JSON.stringify(getHeaders(providerId)) + )}` + }`} alt="Manga Page" /> )} @@ -142,11 +159,15 @@ export default function SecondPanel({ className="w-1/2 h-screen object-contain" src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent( image[image.length - index - 1]?.url - )}&headers=${encodeURIComponent( - JSON.stringify({ - Referer: image[image.length - index - 1]?.headers.Referer, - }) - )}`} + )}${ + image[image.length - index - 1]?.headers?.Referer + ? `&headers=${encodeURIComponent( + JSON.stringify(image[image.length - index - 1]?.headers) + )}` + : `&headers=${encodeURIComponent( + JSON.stringify(getHeaders(providerId)) + )}` + }`} alt="Manga Page" /> </div> diff --git a/components/manga/panels/thirdPanel.js b/components/manga/panels/thirdPanel.js index d402f07..f13b49d 100644 --- a/components/manga/panels/thirdPanel.js +++ b/components/manga/panels/thirdPanel.js @@ -5,10 +5,12 @@ import { ArrowsPointingInIcon, } from "@heroicons/react/24/outline"; import { useAniList } from "../../../lib/anilist/useAnilist"; +import { getHeaders } from "@/utils/imageUtils"; export default function ThirdPanel({ aniId, data, + chapterData, hasRun, currentId, currentChapter, @@ -20,6 +22,7 @@ export default function ThirdPanel({ scaleImg, setMobileVisible, mobileVisible, + providerId, }) { const [index, setIndex] = useState(0); const [image, setImage] = useState(null); @@ -28,6 +31,7 @@ export default function ThirdPanel({ useEffect(() => { setIndex(0); setSeekPage(0); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, currentId]); const seekToIndex = (newIndex) => { @@ -39,6 +43,7 @@ export default function ThirdPanel({ useEffect(() => { seekToIndex(seekPage); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [seekPage]); useEffect(() => { @@ -60,13 +65,14 @@ export default function ThirdPanel({ setSeekPage(index + 1); } if (index + 1 >= image.length - 2 && !hasRun.current) { - let chapterNumber = currentChapter?.number; - if (chapterNumber % 1 !== 0) { - // If it's a decimal, round it - chapterNumber = Math.round(chapterNumber); - } + const current = chapterData.chapters?.find( + (x) => x.id === currentChapter.id + ); + const chapterNumber = chapterData.chapters.indexOf(current) + 1; - markProgress(aniId, chapterNumber); + if (chapterNumber) { + markProgress(aniId, chapterNumber); + } hasRun.current = true; } } @@ -77,6 +83,8 @@ export default function ThirdPanel({ return () => { window.removeEventListener("keydown", handleKeyDown); }; + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [index, image]); const handleNext = () => { @@ -85,13 +93,15 @@ export default function ThirdPanel({ setSeekPage(index + 1); } if (index + 1 >= image.length - 2 && !hasRun.current) { - let chapterNumber = currentChapter?.number; - if (chapterNumber % 1 !== 0) { - // If it's a decimal, round it - chapterNumber = Math.round(chapterNumber); + const current = chapterData.chapters?.find( + (x) => x.id === currentChapter.id + ); + const chapterNumber = chapterData.chapters.indexOf(current) + 1; + + if (chapterNumber) { + markProgress(aniId, chapterNumber); } - markProgress(aniId, chapterNumber); hasRun.current = true; } }; @@ -119,11 +129,15 @@ export default function ThirdPanel({ onClick={() => setMobileVisible(!mobileVisible)} src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent( image[image.length - index - 1]?.url - )}&headers=${encodeURIComponent( - JSON.stringify({ - Referer: image[image.length - index - 1]?.headers.Referer, - }) - )}`} + )}${ + image[image.length - index - 1]?.headers?.Referer + ? `&headers=${encodeURIComponent( + JSON.stringify(image[image.length - index - 1]?.headers) + )}` + : `&headers=${encodeURIComponent( + JSON.stringify(getHeaders(providerId)) + )}` + }`} alt="Manga Page" style={{ transform: `scale(${scaleImg})`, diff --git a/components/manga/rightBar.js b/components/manga/rightBar.js index 82d577d..9672fc4 100644 --- a/components/manga/rightBar.js +++ b/components/manga/rightBar.js @@ -4,16 +4,15 @@ import { } from "@heroicons/react/24/outline"; import { useEffect, useState } from "react"; import { useAniList } from "../../lib/anilist/useAnilist"; -import { toast } from "react-toastify"; import AniList from "../media/aniList"; import { signIn } from "next-auth/react"; +import { toast } from "sonner"; export default function RightBar({ id, hasRun, session, data, - error, currentChapter, paddingX, setPaddingX, @@ -47,19 +46,13 @@ export default function RightBar({ markProgress(id, progress, status, volumeProgress); hasRun.current = true; } else { - toast.error("Progress must be a whole number!", { - position: "bottom-right", - autoClose: 5000, - hideProgressBar: true, - closeOnClick: false, - pauseOnHover: true, - draggable: true, - theme: "colored", - }); + toast.error("Progress must be a whole number!"); } } }; + // console.log({ id }); + const changeMode = (e) => { setLayout(Number(e.target.value)); // console.log(e.target.value); @@ -129,63 +122,72 @@ export default function RightBar({ </button> </div> </div> + {/* <div className="flex flex-col gap-3 w-full"> + <h1 className="font-karla font-bold xl:text-lg">Set Quality</h1> + </div> */} <div className="flex flex-col gap-3 w-full"> <h1 className="font-karla font-bold xl:text-lg">Tracking</h1> {session ? ( - <div className="flex flex-col gap-2"> - <div className="space-y-1"> - <label className="font-karla font-semibold text-gray-500 text-xs"> - Status - </label> - <div className="relative"> - <select - onChange={(e) => setStatus(e.target.value)} - className="w-full px-2 py-1 font-karla rounded-md bg-[#161617] appearance-none text-sm" - > - <option value="CURRENT">Reading</option> - <option value="PLANNING">Plan to Read</option> - <option value="COMPLETED">Completed</option> - <option value="REPEATING">Rereading</option> - <option value="PAUSED">Paused</option> - <option value="DROPPED">Dropped</option> - </select> - <ChevronDownIcon className="w-5 h-5 text-white absolute inset-0 my-auto mx-52" /> + id?.length > 6 ? ( + <p className="flex-center w-full py-2 font-karla"> + Not available on AniList + </p> + ) : ( + <div className="flex flex-col gap-2"> + <div className="space-y-1"> + <label className="font-karla font-semibold text-gray-500 text-xs"> + Status + </label> + <div className="relative"> + <select + onChange={(e) => setStatus(e.target.value)} + className="w-full px-2 py-1 font-karla rounded-md bg-[#161617] appearance-none text-sm" + > + <option value="CURRENT">Reading</option> + <option value="PLANNING">Plan to Read</option> + <option value="COMPLETED">Completed</option> + <option value="REPEATING">Rereading</option> + <option value="PAUSED">Paused</option> + <option value="DROPPED">Dropped</option> + </select> + <ChevronDownIcon className="w-5 h-5 text-white absolute inset-0 my-auto mx-52" /> + </div> </div> + <div className="space-y-1"> + <label className="font-karla font-semibold text-gray-500 text-xs"> + Chapter Progress + </label> + <input + id="chapter-progress" + type="number" + placeholder="0" + min={0} + value={progress} + onChange={(e) => setProgress(e.target.value)} + className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm" + /> + </div> + <div className="space-y-1"> + <label className="font-karla font-semibold text-gray-500 text-xs"> + Volume Progress + </label> + <input + type="number" + placeholder="0" + min={0} + onChange={(e) => setVolumeProgress(e.target.value)} + className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm" + /> + </div> + <button + type="button" + onClick={saveProgress} + className="w-full bg-[#424245] py-1 my-5 rounded-md text-white text-sm xl:text-base shadow-md font-karla font-semibold" + > + Save Progress + </button> </div> - <div className="space-y-1"> - <label className="font-karla font-semibold text-gray-500 text-xs"> - Chapter Progress - </label> - <input - id="chapter-progress" - type="number" - placeholder="0" - min={0} - value={progress} - onChange={(e) => setProgress(e.target.value)} - className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm" - /> - </div> - <div className="space-y-1"> - <label className="font-karla font-semibold text-gray-500 text-xs"> - Volume Progress - </label> - <input - type="number" - placeholder="0" - min={0} - onChange={(e) => setVolumeProgress(e.target.value)} - className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm" - /> - </div> - <button - type="button" - onClick={saveProgress} - className="w-full bg-[#424245] py-1 my-5 rounded-md text-white text-sm xl:text-base shadow-md font-karla font-semibold" - > - Save Progress - </button> - </div> + ) ) : ( <button type="button" diff --git a/components/search/searchByImage.js b/components/search/searchByImage.js new file mode 100644 index 0000000..f95c2ad --- /dev/null +++ b/components/search/searchByImage.js @@ -0,0 +1,119 @@ +import { PhotoIcon } from "@heroicons/react/24/outline"; +import { useRouter } from "next/router"; +import React, { useEffect } from "react"; +import { toast } from "sonner"; + +export default function SearchByImage({ + searchPalette = false, + setIsOpen, + setData, + setMedia, +}) { + const router = useRouter(); + + async function findImage(formData) { + const response = new Promise((resolve, reject) => { + fetch("https://api.trace.moe/search?anilistInfo", { + method: "POST", + body: formData, + }) + .then((resp) => { + resolve(resp.json()); + }) + .catch((error) => { + reject(error); + }); + }); + + toast.promise(response, { + loading: "Finding episodes...", + success: `Episodes found!`, + error: "Error", + }); + + response + .then((data) => { + if (data?.result?.length > 0) { + const id = data.result[0].anilist.id; + const datas = data.result.filter((i) => i.anilist.isAdult === false); + if (setData) setData(datas); + if (searchPalette) router.push(`/en/anime/${id}`); + if (setIsOpen) setIsOpen(false); + if (setMedia) setMedia(); + } + }) + .catch((error) => { + console.error("Error:", error); + }); + } + + const handleImageSelect = async (e) => { + const selectedImage = e.target.files[0]; + + if (selectedImage) { + const formData = new FormData(); + formData.append("image", selectedImage); + + try { + await findImage(formData); + } catch (error) { + console.error("An error occurred:", error); + } + } + }; + + useEffect(() => { + // Add a global event listener for the paste event + const handlePaste = async (e) => { + e.preventDefault(); + + const items = e.clipboardData.items; + + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") !== -1) { + const blob = items[i].getAsFile(); + + // Create a FormData object and append the pasted image + const formData = new FormData(); + formData.append("image", blob); + + try { + // Send the pasted image to your API for processing + await findImage(formData); + } catch (error) { + console.error("An error occurred:", error); + } + break; // Stop after finding the first image + } + } + }; + + // Add the event listener to the document + document.addEventListener("paste", handlePaste); + + // Clean up the event listener when the component unmounts + return () => { + document.removeEventListener("paste", handlePaste); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <div> + <label + className={`${ + searchPalette ? "w-9 h-9" : "py-2 px-2" + } bg-secondary rounded flex justify-center items-center cursor-pointer hover:bg-opacity-75 transition-all duration-100 group`} + > + <PhotoIcon className="w-6 h-6" /> + <input + type="file" + name="image" + onChange={handleImageSelect} + className="hidden" + /> + </label> + </div> + ); +} diff --git a/components/searchPalette.js b/components/searchPalette.js index 38a0bc0..10b9003 100644 --- a/components/searchPalette.js +++ b/components/searchPalette.js @@ -1,4 +1,4 @@ -import { Fragment, useEffect, useState } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; import { Combobox, Dialog, Menu, Transition } from "@headlessui/react"; import useDebounce from "../lib/hooks/useDebounce"; import Image from "next/image"; @@ -8,6 +8,7 @@ import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; import { BookOpenIcon, PlayIcon } from "@heroicons/react/20/solid"; import { useAniList } from "../lib/anilist/useAnilist"; import { getFormat } from "../utils/getFormat"; +import SearchByImage from "./search/searchByImage"; export default function SearchPalette() { const { isOpen, setIsOpen } = useSearch(); @@ -21,6 +22,7 @@ export default function SearchPalette() { const [nextPage, setNextPage] = useState(false); + let focusInput = useRef(null); const router = useRouter(); function closeModal() { @@ -44,6 +46,7 @@ export default function SearchPalette() { useEffect(() => { advance(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [debounceSearch, type]); useEffect(() => { @@ -62,11 +65,17 @@ export default function SearchPalette() { return () => { window.removeEventListener("keydown", handleKeyDown); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <Transition appear show={isOpen} as={Fragment}> - <Dialog as="div" className="relative z-[6969]" onClose={closeModal}> + <Dialog + as="div" + className="relative z-[6969]" + initialFocus={focusInput} + onClose={closeModal} + > <Transition.Child as={Fragment} enter="ease-out duration-300" @@ -112,13 +121,13 @@ export default function SearchPalette() { <span>S</span> </div> </div> - <div> + <div className="flex gap-1 items-center"> <Menu as="div" className="relative inline-block text-left" > <div> - <Menu.Button className="capitalize bg-secondary inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-medium text-white hover:bg-opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"> + <Menu.Button className="capitalize bg-secondary inline-flex w-full justify-center rounded px-3 py-2 text-sm font-medium text-white hover:bg-opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"> {type.toLowerCase()} <ChevronDownIcon className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100" @@ -171,10 +180,15 @@ export default function SearchPalette() { </Menu.Items> </Transition> </Menu> + <SearchByImage + searchPalette={true} + setIsOpen={setIsOpen} + /> </div> </div> <div className="flex items-center text-base font-medium rounded bg-secondary"> <Combobox.Input + ref={focusInput} className="p-5 text-white w-full bg-transparent border-0 outline-none" placeholder="Search something..." onChange={(event) => setQuery(event.target.value)} diff --git a/components/secret.js b/components/secret.js new file mode 100644 index 0000000..782fcf5 --- /dev/null +++ b/components/secret.js @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; + +export default function SecretPage({ cheatCode, onCheatCodeEntered }) { + const [typedCode, setTypedCode] = useState(""); + const [timer, setTimer] = useState(null); + + const handleKeyPress = (e) => { + const newTypedCode = typedCode + e.key; + + if (newTypedCode === cheatCode) { + onCheatCodeEntered(); + setTypedCode(""); + } else { + setTypedCode(newTypedCode); + + // Reset the timer if the user stops typing for 2 seconds + clearTimeout(timer); + const newTimer = setTimeout(() => { + setTypedCode(""); + }, 2000); + setTimer(newTimer); + } + }; + + useEffect(() => { + window.addEventListener("keydown", handleKeyPress); + + return () => { + window.removeEventListener("keydown", handleKeyPress); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [typedCode]); + + return; +} diff --git a/components/shared/NavBar.js b/components/shared/NavBar.js index 7bbd617..034a06b 100644 --- a/components/shared/NavBar.js +++ b/components/shared/NavBar.js @@ -56,7 +56,7 @@ export function NewNavbar({ shrink ? "py-1" : `${paddingY}` }` : `${paddingY}` - } transition-all duration-200 ease-linear`} + } transition-all duration-200 ease-linear`} > <div className={`flex items-center justify-between mx-auto ${ @@ -83,6 +83,7 @@ export function NewNavbar({ > <ArrowLeftIcon className="w-full h-full" /> </button> + <span className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${ scrollPosition?.y >= scrollP + 80 @@ -196,7 +197,7 @@ export function NewNavbar({ // title={sessions ? "Go to Profile" : "Login With AniList"} > */} {session ? ( - <div className="w-7 h-7 relative flex flex-col items-center group"> + <div className="w-7 h-7 relative flex flex-col items-center group shrink-0"> <button type="button" onClick={() => @@ -233,7 +234,7 @@ export function NewNavbar({ type="button" onClick={() => signIn("AniListProvider")} title="Login With AniList" - className="w-7 h-7 bg-white/30 rounded-full overflow-hidden" + className="w-7 h-7 bg-white/30 rounded-full overflow-hidden shrink-0" > <UserIcon className="w-full h-full translate-y-1" /> </button> diff --git a/components/shared/bugReport.js b/components/shared/bugReport.js index 9b99016..f6bd9f1 100644 --- a/components/shared/bugReport.js +++ b/components/shared/bugReport.js @@ -1,7 +1,7 @@ import { Fragment, useState } from "react"; import { Dialog, Listbox, Transition } from "@headlessui/react"; import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; -import { toast } from "react-toastify"; +import { toast } from "sonner"; const severityOptions = [ { id: 1, name: "Low" }, @@ -42,17 +42,11 @@ const BugReportForm = ({ isOpen, setIsOpen }) => { }); const json = await res.json(); - toast.success(json.message, { - hideProgressBar: true, - theme: "colored", - }); + toast.success(json.message); closeModal(); } catch (err) { console.log(err); - toast.error("Something went wrong: " + err.message, { - hideProgressBar: true, - theme: "colored", - }); + toast.error("Something went wrong: " + err.message); } }; diff --git a/components/shared/footer.js b/components/shared/footer.js index 91af5a8..0e19f13 100644 --- a/components/shared/footer.js +++ b/components/shared/footer.js @@ -28,6 +28,8 @@ function Footer() { setLang("id"); setChecked(true); } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function switchLang() { diff --git a/components/watch/player/artplayer.js b/components/watch/player/artplayer.js index 4ae8aa1..666c103 100644 --- a/components/watch/player/artplayer.js +++ b/components/watch/player/artplayer.js @@ -46,7 +46,7 @@ export default function NewPlayer({ customType: { m3u8: playM3u8, }, - ...(provider === "zoro" && { + ...(subtitles?.length > 0 && { subtitle: { url: `${defSub}`, // type: "vtt", @@ -131,7 +131,7 @@ export default function NewPlayer({ return item.html; }, }, - provider === "zoro" && { + subtitles?.length > 0 && { html: "Subtitles", icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M4 20q-.825 0-1.413-.588T2 18V6q0-.825.588-1.413T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.588 1.413T20 20H4Zm2-4h8v-2H6v2Zm10 0h2v-2h-2v2ZM6 12h2v-2H6v2Zm4 0h8v-2h-8v2Z"></path></svg>', width: 300, @@ -261,7 +261,7 @@ export default function NewPlayer({ index: 11, position: "right", tooltip: "Theater (t)", - html: '<p class="theater"><svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M19 3H1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 12H2V5h16v10z"></path></svg></p>', + html: '<i class="theater"><svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M19 3H1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 12H2V5h16v10z"></path></svg></i>', click: function (...args) { setPlayerState((prev) => ({ ...prev, @@ -379,6 +379,8 @@ export default function NewPlayer({ art.destroy(false); } }; + + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return <div ref={artRef} {...rest}></div>; diff --git a/components/watch/player/component/controls/subtitle.js b/components/watch/player/component/controls/subtitle.js deleted file mode 100644 index 02075f7..0000000 --- a/components/watch/player/component/controls/subtitle.js +++ /dev/null @@ -1,3 +0,0 @@ -import { useState } from "react"; - -export default function getSubtitles() {} diff --git a/components/watch/player/playerComponent.js b/components/watch/player/playerComponent.js index 37c5810..665919b 100644 --- a/components/watch/player/playerComponent.js +++ b/components/watch/player/playerComponent.js @@ -4,6 +4,7 @@ import { icons } from "./component/overlay"; import { useWatchProvider } from "@/lib/context/watchPageProvider"; import { useRouter } from "next/router"; import { useAniList } from "@/lib/anilist/useAnilist"; +import Loading from "@/components/shared/loading"; export function calculateAspectRatio(width, height) { const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b)); @@ -74,20 +75,18 @@ export default function PlayerComponent({ setResolution(resol); } - if (provider === "zoro") { - const size = fontSize.map((i) => { - const isDefault = !sub ? i.html === "Small" : i.html === sub?.html; - return { - ...(isDefault && { default: true }), - html: i.html, - size: i.size, - }; - }); + const size = fontSize.map((i) => { + const isDefault = !sub ? i.html === "Small" : i.html === sub?.html; + return { + ...(isDefault && { default: true }), + html: i.html, + size: i.size, + }; + }); - const defSize = size?.find((i) => i?.default === true); - setDefSize(defSize); - setSubSize(size); - } + const defSize = size?.find((i) => i?.default === true); + setDefSize(defSize); + setSubSize(size); async function compiler() { try { @@ -114,19 +113,26 @@ export default function PlayerComponent({ setUrl(defSource.url); } - if (provider === "zoro") { - const subtitle = data?.subtitles - .filter((subtitle) => subtitle.lang !== "Thumbnails") - .map((subtitle) => { - const isEnglish = subtitle.lang === "English"; - return { - ...(isEnglish && { default: true }), - url: subtitle.url, - html: `${subtitle.lang}`, - }; - }); + const subtitle = data?.subtitles + ?.filter( + (subtitle) => + subtitle.lang !== "Thumbnails" && subtitle.lang !== "thumbnails" + ) + ?.map((subtitle) => { + const isEnglish = + subtitle.lang === "English" || + subtitle.lang === "English / English (US)"; + return { + ...(isEnglish && { default: true }), + url: subtitle.url, + html: `${subtitle.lang}`, + }; + }); - const defSub = data?.subtitles.find((i) => i.lang === "English"); + if (subtitle) { + const defSub = data?.subtitles.find( + (i) => i.lang === "English" || i.lang === "English / English (US)" + ); setDefSub(defSub?.url); @@ -162,6 +168,8 @@ export default function PlayerComponent({ setSubtitle([]); setLoading(true); }; + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [provider, data]); /** @@ -171,6 +179,17 @@ export default function PlayerComponent({ art.on("ready", () => { const autoplay = localStorage.getItem("autoplay_video") || false; + // check media queries for mobile devices + const isMobile = window.matchMedia("(max-width: 768px)").matches; + + // console.log(art.fullscreen); + + if (isMobile) { + art.controls.remove("theater-button"); + // art.controls.remove("fast-rewind"); + // art.controls.remove("fast-forward"); + } + if (autoplay === "true" || autoplay === true) { if (playerState.currentTime === 0) { art.play(); @@ -465,10 +484,13 @@ export default function PlayerComponent({ style={{ aspectRatio: aspectRatio }} > <div className="flex-center w-full h-full"> + {!data?.error && !url && ( + <div className="flex-center w-full h-full"> + <Loading /> + </div> + )} {!error ? ( - !loading && - track && - url && ( + !loading && track && url && !data?.error ? ( <NewPlayer playerRef={playerRef} res={resolution} @@ -486,6 +508,12 @@ export default function PlayerComponent({ height: "100%", }} /> + ) : ( + <p className="text-center"> + {data?.status === 404 && "Not Found"} + <br /> + {data?.error} + </p> ) ) : ( <p className="text-center"> diff --git a/components/watch/player/utils/getZoroSource.js b/components/watch/player/utils/getZoroSource.js deleted file mode 100644 index e69de29..0000000 --- a/components/watch/player/utils/getZoroSource.js +++ /dev/null diff --git a/components/watch/secondary/episodeLists.js b/components/watch/secondary/episodeLists.js index 41f1a76..485b43e 100644 --- a/components/watch/secondary/episodeLists.js +++ b/components/watch/secondary/episodeLists.js @@ -1,6 +1,8 @@ import Skeleton from "react-loading-skeleton"; import Image from "next/image"; import Link from "next/link"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { useRouter } from "next/router"; export default function EpisodeLists({ info, @@ -9,13 +11,56 @@ export default function EpisodeLists({ watchId, episode, artStorage, + track, dub, }) { const progress = info.mediaListEntry?.progress; + const router = useRouter(); + return ( <div className="w-screen lg:max-w-sm xl:max-w-lg"> - <h1 className="text-xl font-karla pl-5 pb-5 font-semibold">Up Next</h1> + <div className="flex gap-4 pl-5 pb-5"> + <button + disabled={!track?.next} + onClick={() => { + router.push( + `/en/anime/watch/${info.id}/${providerId}?id=${ + track?.next?.id + }&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}` + ); + }} + className="text-xl font-karla font-semibold" + > + Next Episode {">"} + </button> + {episode && ( + <div className="relative flex gap-2 items-center group"> + <select + value={track?.playing?.number} + onChange={(e) => { + const selectedEpisode = episode.find( + (episode) => episode.number === parseInt(e.target.value) + ); + + router.push( + `/en/anime/watch/${info.id}/${providerId}?id=${ + selectedEpisode.id + }&num=${selectedEpisode.number}${dub ? `&dub=${dub}` : ""}` + ); + }} + 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 group-hover:ring-1 group-hover:ring-action" + > + {episode?.map((x) => ( + <option key={x.id} value={x.number}> + Episode {x.number} + </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 className="flex flex-col gap-5 lg:pl-5 py-2 scrollbar-thin px-2 scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full"> {episode && episode.length > 0 ? ( map?.some( diff --git a/lib/Artplayer.js b/lib/Artplayer.js deleted file mode 100644 index 48da24d..0000000 --- a/lib/Artplayer.js +++ /dev/null @@ -1,290 +0,0 @@ -import { useEffect, useRef } from "react"; -import Artplayer from "artplayer"; -import Hls from "hls.js"; -import { useRouter } from "next/router"; - -export default function Player({ - option, - res, - quality, - subSize, - subtitles, - provider, - getInstance, - id, - track, - // socket - // isPlay, - // watchdata, - // room, - autoplay, - setautoplay, - ...rest -}) { - const artRef = useRef(); - - const router = useRouter(); - - function playM3u8(video, url, art) { - if (Hls.isSupported()) { - if (art.hls) art.hls.destroy(); - const hls = new Hls(); - hls.loadSource(url); - hls.attachMedia(video); - art.hls = hls; - art.on("destroy", () => hls.destroy()); - } else if (video.canPlayType("application/vnd.apple.mpegurl")) { - video.src = url; - } else { - art.notice.show = "Unsupported playback format: m3u8"; - } - } - - useEffect(() => { - const art = new Artplayer({ - ...option, - container: artRef.current, - type: "m3u8", - customType: { - m3u8: playM3u8, - }, - fullscreen: true, - hotkey: true, - lock: true, - setting: true, - playbackRate: true, - autoOrientation: true, - pip: true, - theme: "#f97316", - controls: [ - { - index: 10, - name: "fast-rewind", - position: "left", - html: '<svg class="hi-solid hi-rewind inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>', - tooltip: "Backward 5s", - click: function () { - art.backward = 5; - }, - }, - { - index: 11, - name: "fast-forward", - position: "left", - html: '<svg class="hi-solid hi-fast-forward inline-block w-7 h-7" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>', - tooltip: "Forward 5s", - click: function () { - art.forward = 5; - }, - }, - ], - settings: [ - { - html: "Autoplay Next", - // icon: '<img width="22" heigth="22" src="/assets/img/state.svg">', - tooltip: "ON/OFF", - switch: localStorage.getItem("autoplay") === "true" ? true : false, - onSwitch: function (item) { - setautoplay(!item.switch); - localStorage.setItem("autoplay", !item.switch); - return !item.switch; - }, - }, - provider === "zoro" && { - html: "Subtitles", - icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="28" viewBox="0 -960 960 960"><path d="M240-350h360v-60H240v60zm420 0h60v-60h-60v60zM240-470h60v-60h-60v60zm120 0h360v-60H360v60zM140-160q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42v520q0 24-18 42t-42 18H140zm0-60h680v-520H140v520zm0 0v-520 520z"></path></svg>', - width: 300, - tooltip: "Settings", - selector: [ - { - html: "Display", - icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M480.169-341.796q65.754 0 111.894-46.31 46.141-46.309 46.141-112.063t-46.31-111.894q-46.309-46.141-112.063-46.141t-111.894 46.31q-46.141 46.309-46.141 112.063t46.31 111.894q46.309 46.141 112.063 46.141zm-.371-48.307q-45.875 0-77.785-32.112-31.91-32.112-31.91-77.987 0-45.875 32.112-77.785 32.112-31.91 77.987-31.91 45.875 0 77.785 32.112 31.91 32.112 31.91 77.987 0 45.875-32.112 77.785-32.112 31.91-77.987 31.91zm.226 170.102q-130.921 0-239.6-69.821-108.679-69.82-167.556-186.476-2.687-4.574-3.892-10.811Q67.77-493.347 67.77-500t1.205-12.891q1.205-6.237 3.892-10.811Q131.745-640.358 240.4-710.178q108.655-69.821 239.576-69.821t239.6 69.821q108.679 69.82 167.556 186.476 2.687 4.574 3.892 10.811 1.205 6.238 1.205 12.891t-1.205 12.891q-1.205 6.237-3.892 10.811Q828.255-359.642 719.6-289.822q-108.655 69.821-239.576 69.821zM480-500zm-.112 229.744q117.163 0 215.048-62.347Q792.821-394.949 844.308-500q-51.487-105.051-149.26-167.397-97.772-62.347-214.936-62.347-117.163 0-215.048 62.347Q167.179-605.051 115.282-500q51.897 105.051 149.67 167.397 97.772 62.347 214.936 62.347z"></path></svg>', - tooltip: "Show", - switch: true, - onSwitch: function (item) { - item.tooltip = item.switch ? "Hide" : "Show"; - art.subtitle.show = !item.switch; - return !item.switch; - }, - }, - { - html: "Font Size", - icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M619.861-177.694q-15.655 0-26.475-10.918-10.821-10.918-10.821-26.516v-492.309H415.128q-15.598 0-26.516-10.959-10.918-10.959-10.918-26.615 0-15.655 10.918-26.475 10.918-10.82 26.516-10.82h409.744q15.598 0 26.516 10.958 10.918 10.959 10.918 26.615 0 15.656-10.918 26.476-10.918 10.82-26.516 10.82H657.435v492.309q0 15.598-10.959 26.516-10.959 10.918-26.615 10.918zm-360 0q-15.655 0-26.475-10.918-10.821-10.918-10.821-26.516v-292.309h-87.437q-15.598 0-26.516-10.959-10.918-10.959-10.918-26.615 0-15.655 10.918-26.475 10.918-10.82 26.516-10.82h249.744q15.598 0 26.516 10.958 10.918 10.959 10.918 26.615 0 15.656-10.918 26.476-10.918 10.82-26.516 10.82h-87.437v292.309q0 15.598-10.959 26.516-10.959 10.918-26.615 10.918z"></path></svg>', - selector: subSize, - onSelect: function (item) { - if (item.html === "Small") { - art.subtitle.style({ fontSize: "16px" }); - localStorage.setItem( - "subSize", - JSON.stringify({ - size: "16px", - html: "Small", - }) - ); - } else if (item.html === "Medium") { - art.subtitle.style({ fontSize: "36px" }); - localStorage.setItem( - "subSize", - JSON.stringify({ - size: "36px", - html: "Medium", - }) - ); - } else if (item.html === "Large") { - art.subtitle.style({ fontSize: "56px" }); - localStorage.setItem( - "subSize", - JSON.stringify({ - size: "56px", - html: "Large", - }) - ); - } - }, - }, - { - html: "Language", - icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M528.282-110.771q-21.744 0-31.308-14.013t-2.205-34.295l135.952-359.307q5.304-14.793 20.292-25.126 14.988-10.334 31.152-10.334 15.398 0 30.85 10.388 15.451 10.387 20.932 25.125l137.128 357.485q8.025 20.949-1.83 35.513-9.855 14.564-33.24 14.564-10.366 0-19.392-6.616-9.025-6.615-12.72-16.242l-30.997-91.808H594.769l-33.381 91.869q-3.645 9.181-13.148 15.989-9.504 6.808-19.958 6.808zm87.871-179.281h131.64l-64.615-180.717h-2.41l-64.615 180.717zM302.104-608.384q14.406 25.624 31.074 48.184 16.669 22.559 37.643 47.021 41.333-44.128 68.628-90.461t46.038-97.897H111.499q-15.674 0-26.278-10.615-10.603-10.616-10.603-26.308t10.615-26.307q10.616-10.616 26.308-10.616h221.537v-36.923q0-15.692 10.615-26.307 10.616-10.616 26.308-10.616t26.307 10.616q10.616 10.615 10.616 26.307v36.923h221.537q15.692 0 26.307 10.616 10.616 10.615 10.616 26.307 0 15.692-10.616 26.308-10.615 10.615-26.307 10.615h-69.088q-19.912 64.153-53.237 125.74-33.325 61.588-82.341 116.412l89.384 90.974-27.692 75.179-115.486-112.922-158.948 158.947q-10.615 10.616-25.667 10.616-15.051 0-25.666-11.026-11.026-10.615-11.026-25.666 0-15.052 11.026-26.077l161.614-161.358q-24.666-28.308-45.551-57.307-20.884-29-37.756-60.103-10.641-19.871-1.346-34.717t33.038-14.846q9.088 0 18.429 5.73 9.34 5.731 13.956 13.577z"></path></svg>', - tooltip: "English", - selector: [...subtitles], - onSelect: function (item) { - art.subtitle.switch(item.url, { - name: item.html, - }); - return item.html; - }, - }, - { - html: "Font Family", - tooltip: localStorage.getItem("font") - ? localStorage.getItem("font") - : "Arial", - selector: [ - { html: "Arial" }, - { html: "Comic Sans MS" }, - { html: "Verdana" }, - { html: "Tahoma" }, - { html: "Trebuchet MS" }, - { html: "Times New Roman" }, - { html: "Georgia" }, - { html: "Impact " }, - { html: "Andalé Mono" }, - { html: "Palatino" }, - { html: "Baskerville" }, - { html: "Garamond" }, - { html: "Courier New" }, - { html: "Brush Script MT" }, - ], - onSelect: function (item) { - art.subtitle.style({ fontFamily: item.html }); - localStorage.setItem("font", item.html); - return item.html; - }, - }, - { - html: "Font Shadow", - tooltip: localStorage.getItem("subShadow") - ? JSON.parse(localStorage.getItem("subShadow")).shadow - : "Default", - selector: [ - { html: "None", value: "none" }, - { - html: "Uniform", - value: - "2px 2px 0px #000, -2px -2px 0px #000, 2px -2px 0px #000, -2px 2px 0px #000", - }, - { html: "Raised", value: "-1px 2px 3px rgba(0, 0, 0, 1)" }, - { html: "Depressed", value: "-2px -3px 3px rgba(0, 0, 0, 1)" }, - { html: "Glow", value: "0 0 10px rgba(0, 0, 0, 0.8)" }, - { - html: "Block", - value: - "-3px 3px 4px rgba(0, 0, 0, 1),2px 2px 4px rgba(0, 0, 0, 1),1px -1px 3px rgba(0, 0, 0, 1),-3px -2px 4px rgba(0, 0, 0, 1)", - }, - ], - onSelect: function (item) { - art.subtitle.style({ textShadow: item.value }); - localStorage.setItem( - "subShadow", - JSON.stringify({ shadow: item.html, value: item.value }) - ); - return item.html; - }, - }, - ], - }, - provider === "gogoanime" && { - html: "Quality", - width: 150, - tooltip: `${res}`, - selector: quality, - onSelect: function (item) { - art.switchQuality(item.url, item.html); - localStorage.setItem("quality", item.html); - return item.html; - }, - }, - ].filter(Boolean), - }); - - if ("mediaSession" in navigator) { - art.on("video:timeupdate", () => { - const session = navigator.mediaSession; - if (!session) return; - session.setPositionState({ - duration: art.duration, - playbackRate: art.playbackRate, - position: art.currentTime, - }); - }); - - navigator.mediaSession.setActionHandler("play", () => { - art.play(); - }); - - navigator.mediaSession.setActionHandler("pause", () => { - art.pause(); - }); - - navigator.mediaSession.setActionHandler("previoustrack", () => { - if (track?.prev) { - router.push( - `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent( - track?.prev?.id - )}&num=${track?.prev?.number}` - ); - } - }); - - navigator.mediaSession.setActionHandler("nexttrack", () => { - if (track?.next) { - router.push( - `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent( - track?.next?.id - )}&num=${track?.next?.number}` - ); - } - }); - } - - art.events.proxy(document, "keydown", (event) => { - if (event.key === "f" || event.key === "F") { - art.fullscreen = !art.fullscreen; - } - }); - - // artInstanceRef.current = art; - - if (getInstance && typeof getInstance === "function") { - getInstance(art); - } - - return () => { - if (art && art.destroy) { - art.destroy(false); - } - }; - }, []); - - return <div ref={artRef} {...rest}></div>; -} diff --git a/lib/anify/getMangaId.js b/lib/anify/getMangaId.js new file mode 100644 index 0000000..e18da65 --- /dev/null +++ b/lib/anify/getMangaId.js @@ -0,0 +1,40 @@ +import axios from "axios"; + +export async function fetchInfo(romaji, english, native) { + try { + const { data: getManga } = await axios.get( + `https://api.anify.tv/search-advanced?query=${ + english || romaji + }&type=manga` + ); + + const findManga = getManga.find( + (manga) => + manga.title.romaji === romaji || + manga.title.english === english || + manga.title.native === native + ); + + if (!findManga) { + return null; + } + + return { id: findManga.id }; + } catch (error) { + console.error("Error fetching data:", error); + return null; + } +} + +export default async function getMangaId(romaji, english, native) { + try { + const data = await fetchInfo(romaji, english, native); + if (data) { + return data; + } else { + return { message: "Schedule not found" }; + } + } catch (error) { + return { error }; + } +} diff --git a/lib/anify/page.js b/lib/anify/page.js index 65ed309..0f0bb93 100644 --- a/lib/anify/page.js +++ b/lib/anify/page.js @@ -1,10 +1,10 @@ import { redis } from "../redis"; // Function to fetch new data -async function fetchData(id, providerId, chapterId, key) { +async function fetchData(id, chapterNumber, providerId, chapterId, key) { try { const res = await fetch( - `https://api.anify.tv/pages?id=${id}&providerId=${providerId}&readId=${chapterId}&apikey=${key}` + `https://api.anify.tv/pages/${id}/${chapterNumber}/${providerId}/${chapterId}&apikey=${key}` ); const data = await res.json(); return data; @@ -16,6 +16,7 @@ async function fetchData(id, providerId, chapterId, key) { export default async function getAnifyPage( mediaId, + chapterNumber, providerId, chapterId, key @@ -28,7 +29,13 @@ export default async function getAnifyPage( if (cached) { return JSON.parse(cached); } else { - const data = await fetchData(mediaId, providerId, chapterId, key); + const data = await fetchData( + mediaId, + chapterNumber, + providerId, + chapterId, + key + ); if (!data.error) { if (redis) { await redis.set(chapterId, JSON.stringify(data), "EX", 60 * 10); diff --git a/lib/anilist/aniAdvanceSearch.js b/lib/anilist/aniAdvanceSearch.js index 02a5c53..cf344b0 100644 --- a/lib/anilist/aniAdvanceSearch.js +++ b/lib/anilist/aniAdvanceSearch.js @@ -23,37 +23,104 @@ export async function aniAdvanceSearch({ return result; }, {}); - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: advanceSearchQuery, - variables: { - ...(search && { - search: search, - ...(!sort && { sort: "SEARCH_MATCH" }), - }), - ...(type && { type: type }), - ...(seasonYear && { seasonYear: seasonYear }), - ...(season && { - season: season, - ...(!seasonYear && { seasonYear: new Date().getFullYear() }), - }), - ...(categorizedGenres && { ...categorizedGenres }), - ...(format && { format: format }), - // ...(genres && { genres: genres }), - // ...(tags && { tags: tags }), - ...(perPage && { perPage: perPage }), - ...(sort && { sort: sort }), + if (type === "MANGA") { + const response = await fetch("https://api.anify.tv/search-advanced", { + method: "POST", + body: JSON.stringify({ + type: "manga", + genres: categorizedGenres, + ...(search && { query: search }), ...(page && { page: page }), + ...(perPage && { perPage: perPage }), + ...(format && { format: format }), + ...(seasonYear && { year: seasonYear }), + ...(type && { type: type }), + }), + }); + + const data = await response.json(); + return { + pageInfo: { + hasNextPage: data.length >= (perPage ?? 20), + currentPage: page, + lastPage: Math.ceil(data.length / (perPage ?? 20)), + perPage: perPage ?? 20, + total: data.length, + }, + media: data.map((item) => ({ + averageScore: item.averageRating, + bannerImage: item.bannerImage, + chapters: item.totalChapters, + coverImage: { + color: item.color, + extraLarge: item.coverImage, + large: item.coverImage, + }, + description: item.description, + duration: item.duration ?? null, + endDate: { + day: null, + month: null, + year: null, + }, + mappings: item.mappings, + format: item.format, + genres: item.genres, + id: item.id, + isAdult: false, + mediaListEntry: null, + nextAiringEpisode: null, + popularity: item.averagePopularity, + season: null, + seasonYear: item.year, + startDate: { + day: null, + month: null, + year: item.year, + }, + status: item.status, + studios: { edges: [] }, + title: { + userPreferred: + item.title.english ?? item.title.romaji ?? item.title.native, + }, + type: item.type, + volumes: item.totalVolumes ?? null, + })), + }; + } else { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", }, - }), - }); + body: JSON.stringify({ + query: advanceSearchQuery, + variables: { + ...(search && { + search: search, + ...(!sort && { sort: "SEARCH_MATCH" }), + }), + ...(type && { type: type }), + ...(seasonYear && { seasonYear: seasonYear }), + ...(season && { + season: season, + ...(!seasonYear && { seasonYear: new Date().getFullYear() }), + }), + ...(categorizedGenres && { ...categorizedGenres }), + ...(format && { format: format }), + // ...(genres && { genres: genres }), + // ...(tags && { tags: tags }), + ...(perPage && { perPage: perPage }), + ...(sort && { sort: sort }), + ...(page && { page: page }), + }, + }), + }); - const datas = await response.json(); - // console.log(datas); - const data = datas.data.Page; - return data; + const datas = await response.json(); + // console.log(datas); + const data = datas.data.Page; + return data; + } } diff --git a/lib/anilist/getMedia.js b/lib/anilist/getMedia.js index 66bb1b0..2e1b0d0 100644 --- a/lib/anilist/getMedia.js +++ b/lib/anilist/getMedia.js @@ -115,6 +115,8 @@ export default function GetMedia(session, stats) { data.data.Page.recommendations.map((i) => i.mediaRecommendation) ); }); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [username, accessToken, status?.stats]); return { anime, manga, recommendations }; diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js index 17ab11b..20c1964 100644 --- a/lib/anilist/useAnilist.js +++ b/lib/anilist/useAnilist.js @@ -1,4 +1,4 @@ -import { toast } from "react-toastify"; +import { toast } from "sonner"; export const useAniList = (session) => { const accessToken = session?.user?.token; @@ -238,11 +238,6 @@ export const useAniList = (session) => { console.log(`Progress Updated: ${progress}`, status); toast.success(`Progress Updated: ${progress}`, { position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", }); } }; diff --git a/lib/consumet/manga/getChapters.js b/lib/consumet/manga/getChapters.js new file mode 100644 index 0000000..7a19bbc --- /dev/null +++ b/lib/consumet/manga/getChapters.js @@ -0,0 +1,80 @@ +let API_URL; +API_URL = process.env.API_URI; +// remove / from the end of the url if it exists +if (API_URL.endsWith("/")) { + API_URL = API_URL.slice(0, -1); +} + +async function fetchInfo(id) { + try { + const providers = [ + "mangadex", + "mangahere", + "mangakakalot", + // "mangapark", + // "mangapill", + "mangasee123", + ]; + let datas = []; + + async function promiseMe(provider) { + try { + const data = await fetch( + `${API_URL}/meta/anilist-manga/info/${id}?provider=${provider}` + ).then((res) => { + if (!res.ok) { + switch (res.status) { + case 404: { + return null; + } + } + } + return res.json(); + }); + if (data.chapters.length > 0) { + datas.push({ + providerId: provider, + chapters: data.chapters, + }); + } + } catch (error) { + console.error(`Error fetching data for provider '${provider}':`, error); + } + } + + await Promise.all(providers.map((provider) => promiseMe(provider))); + + return datas; + } catch (error) { + console.error("Error fetching data:", error); + return null; + } +} + +export default async function getConsumetChapters(id, redis) { + try { + let cached; + let chapters; + + if (redis) { + cached = await redis.get(`chapter:${id}`); + } + + if (cached) { + chapters = JSON.parse(cached); + } else { + chapters = await fetchInfo(id); + } + + if (chapters?.length === 0) { + return null; + } + if (redis) { + await redis.set(`chapter:${id}`, JSON.stringify(chapters), "EX", 60 * 60); // 1 hour + } + + return chapters; + } catch (error) { + return { error }; + } +} diff --git a/lib/consumet/manga/getPage.js b/lib/consumet/manga/getPage.js new file mode 100644 index 0000000..832c1d7 --- /dev/null +++ b/lib/consumet/manga/getPage.js @@ -0,0 +1,49 @@ +let API_URL; +API_URL = process.env.API_URI; +// remove / from the end of the url if it exists +if (API_URL.endsWith("/")) { + API_URL = API_URL.slice(0, -1); +} + +// Function to fetch new data +async function fetchData(id, providerId, chapterId, key) { + try { + const res = await fetch( + `${API_URL}/meta/anilist-manga/read?chapterId=${chapterId}&provider=${providerId}` + ); + const data = await res.json(); + return data; + } catch (error) { + console.error("Error fetching data:", error); + return null; + } +} + +export default async function getConsumetPages( + mediaId, + providerId, + chapterId, + key +) { + try { + // let cached; + // if (redis) { + // cached = await redis.get(chapterId); + // } + // if (cached) { + // return JSON.parse(cached); + // } else { + const data = await fetchData(mediaId, providerId, chapterId, key); + if (!data.error) { + // if (redis) { + // await redis.set(chapterId, JSON.stringify(data), "EX", 60 * 10); + // } + return data; + } else { + return { message: "Manga/Novel not found :(" }; + } + // } + } catch (error) { + return { error }; + } +} diff --git a/lib/graphql/query.js b/lib/graphql/query.js index a09c6ac..45d3d68 100644 --- a/lib/graphql/query.js +++ b/lib/graphql/query.js @@ -176,8 +176,14 @@ query { }`; const mediaInfoQuery = ` - query ($id: Int) { - Media(id: $id) { + query ($id: Int, $type:MediaType) { + Media(id: $id, type:$type) { + mediaListEntry { + status + progress + progressVolumes + status + } id type format @@ -191,6 +197,10 @@ const mediaInfoQuery = ` large color } + startDate { + year + month + } bannerImage description episodes diff --git a/next.config.js b/next.config.js index a0ec43c..d3fd882 100644 --- a/next.config.js +++ b/next.config.js @@ -24,6 +24,10 @@ module.exports = withPWA({ protocol: "https", hostname: "simkl.in", }, + { + protocol: "https", + hostname: "tenor.com", + }, ], }, // distDir: process.env.BUILD_DIR || ".next", diff --git a/package-lock.json b/package-lock.json index 1edee36..d90eeb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "moopa", - "version": "4.1.3", + "version": "4.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "moopa", - "version": "4.1.3", + "version": "4.2.0", "dependencies": { "@apollo/client": "^3.7.3", "@headlessui/react": "^1.7.15", @@ -35,6 +35,8 @@ "react-loading-skeleton": "^3.2.0", "react-toastify": "^9.1.3", "react-use-draggable-scroll": "^0.4.7", + "sharp": "^0.32.6", + "sonner": "^1.0.3", "tailwind-scrollbar-hide": "^1.1.7", "workbox-webpack-plugin": "^7.0.0" }, @@ -3267,6 +3269,11 @@ "dequal": "^2.0.3" } }, + "node_modules/b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" + }, "node_modules/babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -3359,6 +3366,16 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3410,11 +3427,53 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -3558,6 +3617,11 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -3618,6 +3682,18 @@ "node": ">=0.10.0" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3631,6 +3707,31 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3802,6 +3903,28 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3975,6 +4098,14 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4052,6 +4183,14 @@ "node": ">= 4" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -4782,11 +4921,24 @@ "node": ">=0.8.x" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -5008,6 +5160,11 @@ "react-dom": "^18.0.0" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -5133,6 +5290,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -5362,6 +5524,25 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -5415,6 +5596,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -6339,6 +6525,17 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6354,11 +6551,15 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6411,6 +6612,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6761,6 +6967,22 @@ "react": ">= 16.0.0" } }, + "node_modules/node-abi": { + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", + "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", @@ -7402,6 +7624,57 @@ "preact": ">=10" } }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7458,6 +7731,15 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -7495,6 +7777,11 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7508,6 +7795,28 @@ "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-3.0.0.tgz", "integrity": "sha512-janAJkWxWxmLka0hV+XvCTo0M8keeSeOuz8ZL33cTXrkS4ek9mQ2VJm9ri7fm03oTVth19Sfqb1ijCmo7K/vAg==" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -7593,6 +7902,19 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -8037,7 +8359,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -8058,7 +8379,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -8069,8 +8389,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/serialize-javascript": { "version": "6.0.1", @@ -8099,6 +8418,28 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8133,6 +8474,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8141,6 +8538,15 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.0.3.tgz", + "integrity": "sha512-hBoA2zKuYW3lUnpx4K0vAn8j77YuYiwvP9sLQfieNS2pd5FkT20sMyPTDJnl9S+5T27ZJbwQRPiujwvDBwhZQg==", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -8196,6 +8602,23 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", + "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8538,6 +8961,26 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/tar-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", + "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -8761,6 +9204,17 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8986,8 +9440,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { "version": "8.3.2", diff --git a/package.json b/package.json index 91fba68..a13c9fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moopa", - "version": "4.1.3", + "version": "4.2.0", "private": true, "founder": "Factiven", "scripts": { @@ -38,6 +38,8 @@ "react-loading-skeleton": "^3.2.0", "react-toastify": "^9.1.3", "react-use-draggable-scroll": "^0.4.7", + "sharp": "^0.32.6", + "sonner": "^1.0.3", "tailwind-scrollbar-hide": "^1.1.7", "workbox-webpack-plugin": "^7.0.0" }, diff --git a/pages/404.js b/pages/404.js index f6e609f..085d984 100644 --- a/pages/404.js +++ b/pages/404.js @@ -1,27 +1,13 @@ import Head from "next/head"; import Link from "next/link"; -import { useEffect, useState } from "react"; -import { parseCookies } from "nookies"; import Image from "next/image"; import Footer from "@/components/shared/footer"; +import { NewNavbar } from "@/components/shared/NavBar"; +import { useRouter } from "next/router"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; export default function Custom404() { - const [lang, setLang] = useState("en"); - const [cookie, setCookies] = useState(null); - - useEffect(() => { - let lang = null; - if (!cookie) { - const cookie = parseCookies(); - lang = cookie.lang || null; - setCookies(cookie); - } - if (lang === "en" || lang === null) { - setLang("en"); - } else if (lang === "id") { - setLang("id"); - } - }, []); + const router = useRouter(); return ( <> <Head> @@ -30,6 +16,7 @@ export default function Custom404() { <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/svg/c.svg" /> </Head> + <NewNavbar withNav shrink /> <div className="min-h-screen w-screen flex flex-col items-center justify-center "> <Image width={500} @@ -44,11 +31,29 @@ export default function Custom404() { <p className="text-base sm:text-lg xl:text-xl text-gray-300 mb-6 text-center"> The page you're looking for doesn't seem to exist. </p> - <Link href={`/${lang}/`}> - <div className="bg-[#fa7d56] xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-[#fb6f44]"> - Go back home - </div> - </Link> + <div className="flex gap-5 font-karla"> + <button + type="button" + onClick={() => { + router.back(); + }} + className="flex items-center gap-2 py-2 px-4 ring-1 ring-action/70 rounded hover:text-white transition-all duration-200 ease-out" + > + <span> + <ArrowLeftIcon className="w-5 h-5" /> + </span> + Go back + </button> + <button + type="button" + onClick={() => { + router.push("/en"); + }} + className="bg-action xl:text-xl text-white font-bold py-2 px-4 rounded hover:bg-opacity-80 hover:text-white transition-all duration-200 ease-out" + > + Home Page + </button> + </div> </div> <Footer /> </> diff --git a/pages/_app.js b/pages/_app.js index f553a98..e2f780d 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -3,22 +3,23 @@ import { AnimatePresence, motion as m } from "framer-motion"; import NextNProgress from "nextjs-progressbar"; import { SessionProvider } from "next-auth/react"; import "../styles/globals.css"; -import "react-toastify/dist/ReactToastify.css"; import "react-loading-skeleton/dist/skeleton.css"; import { SkeletonTheme } from "react-loading-skeleton"; import SearchPalette from "@/components/searchPalette"; import { SearchProvider } from "@/lib/context/isOpenState"; import Head from "next/head"; import { WatchPageProvider } from "@/lib/context/watchPageProvider"; -import { ToastContainer, toast } from "react-toastify"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { unixTimestampToRelativeTime } from "@/utils/getTimes"; +import SecretPage from "@/components/secret"; +import { Toaster, toast } from "sonner"; export default function App({ Component, pageProps: { session, ...pageProps }, }) { const router = useRouter(); + const [info, setInfo] = useState(null); useEffect(() => { async function getBroadcast() { @@ -31,29 +32,31 @@ export default function App({ }, }); const data = await res.json(); - if ( - data && - data?.message !== "No broadcast" && - data?.message !== "unauthorized" - ) { - toast( - `${data.message} ${ + if (data?.show === true) { + toast.message( + `🚧${data.message} ${ data?.startAt ? unixTimestampToRelativeTime(data.startAt) : "" - }`, + }🚧`, { - position: "top-center", - autoClose: false, - closeOnClick: true, - draggable: true, - theme: "colored", - className: "toaster", - style: { - background: "#232329", - color: "#fff", - }, + position: "bottom-right", + important: true, + duration: 100000, + className: "flex-center font-karla text-white", + // description: `🚧${info}🚧`, } ); + // toast.message(`Announcement`, { + // position: "top-center", + // important: true, + // // duration: 10000, + // description: `🚧${info}🚧`, + // }); } + setInfo( + `${data.message} ${ + data?.startAt ? unixTimestampToRelativeTime(data.startAt) : "" + }` + ); } catch (err) { console.log(err); } @@ -61,12 +64,16 @@ export default function App({ getBroadcast(); }, []); + const handleCheatCodeEntered = () => { + alert("Cheat code entered!"); // You can replace this with your desired action + }; + return ( <> <Head> <meta name="viewport" - content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover" + content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover" /> </Head> <SessionProvider session={session}> @@ -74,7 +81,22 @@ export default function App({ <WatchPageProvider> <AnimatePresence mode="wait"> <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32"> - <ToastContainer pauseOnFocusLoss={false} pauseOnHover={false} /> + <Toaster richColors theme="dark" closeButton /> + <SecretPage + cheatCode={"aofienaef"} + onCheatCodeEntered={handleCheatCodeEntered} + /> + {/* {info && ( + <div className="relative px-3 flex items-center justify-center font-karla w-full py-2 bg-secondary/80 text-white text-center"> + <span className="line-clamp-1 mr-5">🚧{info}🚧</span> + <span + onClick={() => setInfo()} + className="absolute right-3 cursor-pointer" + > + <XMarkIcon className="w-6 h-6" /> + </span> + </div> + )} */} <m.div key={`route-${router.route}`} transition={{ duration: 0.5 }} diff --git a/pages/_error.js b/pages/_error.js new file mode 100644 index 0000000..19dfcff --- /dev/null +++ b/pages/_error.js @@ -0,0 +1,41 @@ +import MobileNav from "@/components/shared/MobileNav"; +import { NewNavbar } from "@/components/shared/NavBar"; +import Footer from "@/components/shared/footer"; +import Head from "next/head"; +import Link from "next/link"; + +function Error({ statusCode }) { + return ( + <> + <Head> + <title>An Error Has Occurred</title> + </Head> + <NewNavbar withNav shrink /> + <MobileNav hideProfile /> + <div className="w-screen h-screen flex-center flex-col gap-5"> + <div className="relative text-3xl">(╯°□°)╯︵ ┻━┻</div> + <div className="flex items-center gap-2 text-xl"> + <span> + {statusCode + ? `An error ${statusCode} occurred on server.` + : "An error occurred on client."} + </span> + </div> + <Link + href="/en" + className="rounded ring-action/50 ring-1 p-2 font-karla bg-action bg-opacity-0 hover:bg-opacity-20 hover:scale-105 text-white transition-all duration-300" + > + Back to home + </Link> + </div> + <Footer /> + </> + ); +} + +Error.getInitialProps = ({ res, err }) => { + const statusCode = res ? res.statusCode : err ? err.statusCode : 404; + return { statusCode }; +}; + +export default Error; diff --git a/pages/_offline.js b/pages/_offline.js new file mode 100644 index 0000000..f440b39 --- /dev/null +++ b/pages/_offline.js @@ -0,0 +1,45 @@ +import Image from "next/image"; +import React from "react"; + +export default function Fallback() { + return ( + <div className="w-screen h-screen flex-center flex-col gap-5"> + <div className="relative"> + <Image + src="/svg/c.svg" + alt="logo" + height={160} + width={160} + quality={100} + className="object-cover" + /> + </div> + <p className="flex items-center gap-2 text-2xl"> + <svg + xmlns="http://www.w3.org/2000/svg" + width="1em" + height="1em" + viewBox="0 0 512 512" + > + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth="32" + d="M93.72 183.25C49.49 198.05 16 233.1 16 288c0 66 54 112 120 112h184.37m147.45-22.26C485.24 363.3 496 341.61 496 312c0-59.82-53-85.76-96-88c-8.89-89.54-71-144-144-144c-26.16 0-48.79 6.93-67.6 18.14" + ></path> + <path + fill="none" + stroke="currentColor" + strokeLinecap="round" + strokeMiterlimit="10" + strokeWidth="32" + d="M448 448L64 64" + ></path> + </svg> + <span>You are Offline :\</span> + </p> + </div> + ); +} diff --git a/pages/admin/index.js b/pages/admin/index.js index cbb5086..2a73fc1 100644 --- a/pages/admin/index.js +++ b/pages/admin/index.js @@ -27,7 +27,12 @@ export async function getServerSideProps(context) { } const admin = sessions?.user?.name === process.env.ADMIN_USERNAME; - const api = process.env.API_URI; + + let api; + api = process.env.API_URI; + if (api.endsWith("/")) { + api = api.slice(0, -1); + } if (!admin) { return { diff --git a/pages/api/v2/admin/broadcast/index.js b/pages/api/v2/admin/broadcast/index.js index d3d3af0..470d61d 100644 --- a/pages/api/v2/admin/broadcast/index.js +++ b/pages/api/v2/admin/broadcast/index.js @@ -1,9 +1,17 @@ import { rateLimitStrict, redis } from "@/lib/redis"; -// import { getServerSession } from "next-auth"; -// import { authOptions } from "pages/api/auth/[...nextauth]"; +import { getServerSession } from "next-auth"; +import { authOptions } from "pages/api/auth/[...nextauth]"; export default async function handler(req, res) { // Check if the custom header "X-Your-Custom-Header" is present and has a specific value + const sessions = await getServerSession(req, res, authOptions); + + const admin = sessions?.user?.name === process.env.ADMIN_USERNAME; + // if req.method === POST and admin === false return 401 + if (!admin && req.method === "DELETE") { + return res.status(401).json({ message: "Unauthorized" }); + } + const customHeaderValue = req.headers["x-broadcast-key"]; if (customHeaderValue !== "get-broadcast") { @@ -21,14 +29,40 @@ export default async function handler(req, res) { }); } - const getId = await redis.get(`broadcast`); - if (getId) { - const broadcast = JSON.parse(getId); - return res - .status(200) - .json({ message: broadcast.message, startAt: broadcast.startAt }); - } else { - return res.status(200).json({ message: "No broadcast" }); + if (req.method === "POST") { + const { message, startAt = undefined, show = false } = req.body; + if (!message) { + return res.status(400).json({ message: "Message is required" }); + } + + const broadcastContent = { + message, + startAt, + show, + }; + await redis.set(`broadcasts`, JSON.stringify(broadcastContent)); + return res.status(200).json({ message: "Broadcast created" }); + } else if (req.method === "DELETE") { + const br = await redis.get(`broadcasts`); + // set broadcast show as false + if (br) { + const broadcast = JSON.parse(br); + broadcast.show = false; + await redis.set(`broadcasts`, JSON.stringify(broadcast)); + } + return res.status(200).json({ message: "Broadcast deleted" }); + } else if (req.method === "GET") { + const getId = await redis.get(`broadcasts`); + if (getId) { + const broadcast = JSON.parse(getId); + return res.status(200).json({ + message: broadcast.message, + startAt: broadcast.startAt, + show: broadcast.show, + }); + } else { + return res.status(200).json({ message: "No broadcast" }); + } } } diff --git a/pages/api/v2/admin/bug-report/index.js b/pages/api/v2/admin/bug-report/index.js index fc5ee77..508e6cd 100644 --- a/pages/api/v2/admin/bug-report/index.js +++ b/pages/api/v2/admin/bug-report/index.js @@ -8,16 +8,6 @@ export default async function handler(req, res) { // create random id each time the endpoint is called const id = Math.random().toString(36).substr(2, 9); - // if (!admin) { - // return res.status(401).json({ message: "Unauthorized" }); - // } - const { data } = req.body; - - // if method is not POST return message "Method not allowed" - if (req.method !== "POST") { - return res.status(405).json({ message: "Method not allowed" }); - } - try { if (redis) { try { @@ -29,16 +19,22 @@ export default async function handler(req, res) { }); } - const getId = await redis.get(`report:${id}`); - if (getId) { + if (req.method === "POST") { + const { data } = req.body; + + data.id = id; + + await redis.set(`report:${id}`, JSON.stringify(data)); return res .status(200) - .json({ message: `Data already exist for id: ${id}` }); + .json({ message: `Report has successfully sent, with Id of ${id}` }); + } else if (req.method === "DELETE") { + const { reportId } = req.body; + await redis.del(`report:${reportId}`); + return res.status(200).json({ message: `Report has been deleted` }); + } else { + return res.status(405).json({ message: "Method not allowed" }); } - await redis.set(`report:${id}`, JSON.stringify(data)); - return res - .status(200) - .json({ message: `Report has successfully sent, with Id of ${id}` }); } return res.status(200).json({ message: "redis is not defined" }); diff --git a/pages/api/v2/episode/[id].js b/pages/api/v2/episode/[id].js index c1fac8b..3f1372b 100644 --- a/pages/api/v2/episode/[id].js +++ b/pages/api/v2/episode/[id].js @@ -3,7 +3,13 @@ import { rateLimitStrict, rateLimiterRedis, redis } from "@/lib/redis"; import appendImagesToEpisodes from "@/utils/combineImages"; import appendMetaToEpisodes from "@/utils/appendMetaToEpisodes"; -const CONSUMET_URI = process.env.API_URI; +let CONSUMET_URI; + +CONSUMET_URI = process.env.API_URI; +if (CONSUMET_URI.endsWith("/")) { + CONSUMET_URI = CONSUMET_URI.slice(0, -1); +} + const API_KEY = process.env.API_KEY; const isAscending = (data) => { @@ -15,37 +21,70 @@ const isAscending = (data) => { return true; }; -async function fetchConsumet(id, dub) { - try { - if (dub) { - return []; +function filterData(data, type) { + // Filter the data based on the type (sub or dub) and providerId + const filteredData = data.map((item) => { + if (item?.map === true) { + if (item.episodes[type].length === 0) { + return null; + } else { + return { + ...item, + episodes: Object?.entries(item.episodes[type]).map( + ([id, episode]) => ({ + ...episode, + }) + ), + }; + } } + return item; + }); - const { data } = await axios.get( - `${CONSUMET_URI}/meta/anilist/episodes/${id}` - ); + const noEmpty = filteredData.filter((i) => i !== null); + return noEmpty; +} - if (data?.message === "Anime not found" && data?.length < 1) { - return []; +async function fetchConsumet(id) { + try { + async function fetchData(dub) { + const { data } = await axios.get( + `${CONSUMET_URI}/meta/anilist/episodes/${id}${dub ? "?dub=true" : ""}` + ); + if (data?.message === "Anime not found" && data?.length < 1) { + return []; + } + + if (dub) { + if (!data?.some((i) => i.id.includes("dub"))) return []; + } + + const reformatted = data.map((item) => ({ + id: item?.id || null, + title: item?.title || null, + img: item?.image || null, + number: item?.number || null, + createdAt: item?.createdAt || null, + description: item?.description || null, + url: item?.url || null, + })); + + return reformatted; } - const reformatted = data.map((item) => ({ - id: item?.id || null, - title: item?.title || null, - img: item?.image || null, - number: item?.number || null, - createdAt: item?.createdAt || null, - description: item?.description || null, - url: item?.url || null, - })); + const [subData, dubData] = await Promise.all([ + fetchData(), + fetchData(true), + ]); const array = [ { map: true, providerId: "gogoanime", - episodes: isAscending(reformatted) - ? reformatted - : reformatted.reverse(), + episodes: { + sub: isAscending(subData) ? subData : subData.reverse(), + dub: isAscending(dubData) ? dubData : dubData.reverse(), + }, }, ]; @@ -73,7 +112,15 @@ async function fetchAnify(id) { 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; return filtered; } catch (error) { console.error("Error fetching and processing data:", error.message); @@ -81,12 +128,16 @@ async function fetchAnify(id) { } } -async function fetchCoverImage(id) { +async function fetchCoverImage(id, available = false) { try { if (!process.env.API_KEY) { return []; } + if (available) { + return null; + } + const { data } = await axios.get( `https://api.anify.tv/content-metadata/${id}?apikey=${API_KEY}` ); @@ -95,7 +146,9 @@ async function fetchCoverImage(id) { return []; } - return data; + const getData = data[0].data; + + return getData; } catch (error) { console.error("Error fetching and processing data:", error.message); return []; @@ -124,10 +177,10 @@ export default async function handler(req, res) { } if (refresh) { - await redis.del(id); + await redis.del(`episode:${id}`); console.log("deleted cache"); } else { - cached = await redis.get(id); + cached = await redis.get(`episode:${id}`); console.log("using redis"); } @@ -136,49 +189,75 @@ export default async function handler(req, res) { if (cached && !refresh) { if (dub) { - const filtered = JSON.parse(cached).filter((item) => - item.episodes.some((epi) => epi.hasDub === true) + const filteredData = filterData(JSON.parse(cached), "dub"); + + let filtered = filteredData.filter((item) => + item?.episodes?.some((epi) => epi.hasDub !== false) ); + + if (meta) { + filtered = await appendMetaToEpisodes(filtered, JSON.parse(meta)); + } + return res.status(200).json(filtered); } else { - return res.status(200).json(JSON.parse(cached)); + const filteredData = filterData(JSON.parse(cached), "sub"); + + let filtered = filteredData; + + if (meta) { + filtered = await appendMetaToEpisodes(filteredData, JSON.parse(meta)); + } + + return res.status(200).json(filtered); } } else { const [consumet, anify, cover] = await Promise.all([ fetchConsumet(id, dub), fetchAnify(id), - fetchCoverImage(id), + fetchCoverImage(id, meta), ]); - const hasImage = consumet.map((i) => - i.episodes.some( - (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/") - ) - ); + // const hasImage = consumet.map((i) => + // i.episodes?.sub?.some( + // (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/") + // ) + // ); + + let subDub = "sub"; + if (dub) { + subDub = "dub"; + } - const rawData = [...consumet, ...(anify[0]?.data ?? [])]; + const rawData = [...consumet, ...anify]; - let data = rawData; + const filteredData = filterData(rawData, subDub); + + let data = filteredData; if (meta) { - data = await appendMetaToEpisodes(rawData, JSON.parse(meta)); - } else if (cover && cover?.length > 0 && !hasImage.includes(true)) - data = await appendImagesToEpisodes(rawData, cover); + data = await appendMetaToEpisodes(filteredData, JSON.parse(meta)); + } else if (cover && !cover.some((e) => e.img === null)) { + await redis.set(`meta:${id}`, JSON.stringify(cover)); + data = await appendMetaToEpisodes(filteredData, cover); + } if (redis && cacheTime !== null) { await redis.set( - id, - JSON.stringify(data.filter((i) => i.episodes.length > 0)), + `episode:${id}`, + JSON.stringify(rawData), "EX", cacheTime ); } if (dub) { - const filtered = data.filter((item) => - item.episodes.some((epi) => epi.hasDub === true) + const filtered = data.filter( + (item) => !item.episodes.some((epi) => epi.hasDub === false) ); - return res.status(200).json(filtered); + return res + .status(200) + .json(filtered.filter((i) => i.episodes.length > 0)); } console.log("fresh data"); diff --git a/pages/api/v2/etc/recent/[page].js b/pages/api/v2/etc/recent/[page].js index 6727787..b1bda0f 100644 --- a/pages/api/v2/etc/recent/[page].js +++ b/pages/api/v2/etc/recent/[page].js @@ -1,6 +1,10 @@ import { rateLimiterRedis, redis } from "@/lib/redis"; -const API_URL = process.env.API_URI; +let API_URL; +API_URL = process.env.API_URI; +if (API_URL.endsWith("/")) { + API_URL = API_URL.slice(0, -1); +} export default async function handler(req, res) { try { diff --git a/pages/api/v2/info/[id].js b/pages/api/v2/info/[id].js deleted file mode 100644 index 243756c..0000000 --- a/pages/api/v2/info/[id].js +++ /dev/null @@ -1,47 +0,0 @@ -import axios from "axios"; -import { rateLimiterRedis, 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) { - try { - const ipAddress = req.socket.remoteAddress; - await rateLimiterRedis.consume(ipAddress); - } catch (error) { - return res.status(429).json({ - error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`, - }); - } - 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/info/index.js b/pages/api/v2/info/index.js new file mode 100644 index 0000000..95770bd --- /dev/null +++ b/pages/api/v2/info/index.js @@ -0,0 +1,60 @@ +import { redis } from "@/lib/redis"; +import axios from "axios"; + +const API_KEY = process.env.API_KEY; + +export async function fetchInfo(id) { + try { + // console.log(id); + const { data } = await axios + .get(`https://api.anify.tv/info/${id}?apikey=${API_KEY}`) + .catch((err) => { + return { + data: null, + }; + }); + + if (!data) { + return null; + } + + const { data: Chapters } = await axios.get( + `https://api.anify.tv/chapters/${data.id}?apikey=${API_KEY}` + ); + + if (!Chapters) { + return null; + } + + return { id: data.id, chapters: Chapters }; + } catch (error) { + console.error("Error fetching data:", error); + return null; + } +} + +export default async function handler(req, res) { + //const [romaji, english, native] = req.query.title; + const { id } = req.query; + try { + let cached; + // const data = await fetchInfo(id); + cached = await redis.get(`manga:${id}`); + + if (cached) { + return res.status(200).json(JSON.parse(cached)); + } + + const manga = await fetchInfo(id); + + if (!manga) { + return res.status(404).json({ error: "Manga not found" }); + } + + await redis.set(`manga:${id}`, JSON.stringify(manga), "ex", 60 * 60 * 24); + + res.status(200).json(manga); + } catch (error) { + res.status(500).json({ error: error.message }); + } +} diff --git a/pages/api/v2/pages/[...id].js b/pages/api/v2/pages/[...id].js new file mode 100644 index 0000000..a9fe0f9 --- /dev/null +++ b/pages/api/v2/pages/[...id].js @@ -0,0 +1,34 @@ +import axios from "axios"; + +async function fetchData(id, number, provider, readId) { + try { + const { data } = await axios.get( + `https://api.anify.tv/pages?id=${id}&chapterNumber=${number}&providerId=${provider}&readId=${encodeURIComponent( + readId + )}` + ); + + if (!data) { + return null; + } + + return data; + } catch (error) { + return null; + } +} + +export default async function handler(req, res) { + const [id, number, provider, readId] = req.query.id; + + try { + const data = await fetchData(id, number, provider, readId); + // if (!data) { + // return res.status(400).json({ error: "Invalid query" }); + // } + + return res.status(200).json(data); + } catch (error) { + return res.status(500).json({ error: error.message }); + } +} diff --git a/pages/api/v2/source/index.js b/pages/api/v2/source/index.js index f15e47d..9ec6082 100644 --- a/pages/api/v2/source/index.js +++ b/pages/api/v2/source/index.js @@ -1,7 +1,11 @@ import { rateLimiterRedis, redis } from "@/lib/redis"; import axios from "axios"; -const CONSUMET_URI = process.env.API_URI; +let CONSUMET_URI; +CONSUMET_URI = process.env.API_URI; +if (CONSUMET_URI.endsWith("/")) { + CONSUMET_URI = CONSUMET_URI.slice(0, -1); +} const API_KEY = process.env.API_KEY; async function consumetSource(id) { @@ -25,7 +29,7 @@ async function anifySource(providerId, watchId, episode, id, sub) { ); return data; } catch (error) { - return null; + return { error: error.message, status: error.response.status }; } } diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js index 910bbc6..e2c0039 100644 --- a/pages/en/anime/[...id].js +++ b/pages/en/anime/[...id].js @@ -72,6 +72,8 @@ export default function Info({ info, color }) { } } fetchData(); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, info, session?.user?.name]); function handleOpen() { @@ -143,7 +145,7 @@ export default function Info({ info, color }) { stats={statuses?.value} prg={progress} max={info?.episodes} - image={info} + info={info} close={handleClose} /> )} @@ -208,7 +210,12 @@ export default function Info({ info, color }) { export async function getServerSideProps(ctx) { const { id } = ctx.query; - const API_URI = process.env.API_URI; + + let API_URI; + API_URI = process.env.API_URI; + if (API_URI.endsWith("/")) { + API_URI = API_URI.slice(0, -1); + } let cache; diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js index c723394..6abf09d 100644 --- a/pages/en/anime/recently-watched.js +++ b/pages/en/anime/recently-watched.js @@ -6,12 +6,12 @@ import Skeleton from "react-loading-skeleton"; import Footer from "@/components/shared/footer"; import { getServerSession } from "next-auth"; import { authOptions } from "../../api/auth/[...nextauth]"; -import { toast } from "react-toastify"; 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"; +import { toast } from "sonner"; export default function PopularAnime({ sessions }) { const [data, setData] = useState(null); @@ -105,11 +105,6 @@ export default function PopularAnime({ sessions }) { if (data?.message === "Episode deleted") { toast.success("Episode removed from history", { position: "bottom-right", - autoClose: 5000, - hideProgressBar: false, - closeOnClick: true, - draggable: true, - theme: "dark", }); } } else { diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js index f918f86..a838b7f 100644 --- a/pages/en/anime/watch/[...info].js +++ b/pages/en/anime/watch/[...info].js @@ -29,8 +29,12 @@ export async function getServerSideProps(context) { }; } - const proxy = process.env.PROXY_URI; - const disqus = process.env.DISQUS_SHORTNAME || null; + let proxy; + proxy = process.env.PROXY_URI; + if (proxy.endsWith("/")) { + proxy = proxy.slice(0, -1); + } + const disqus = process.env.DISQUS_SHORTNAME; const [aniId, provider] = query?.info; const watchId = query?.id; @@ -114,7 +118,7 @@ export async function getServerSideProps(context) { epiNumber: epiNumber || null, dub: dub || null, userData: userData?.[0] || null, - info: data.data.Media || null, + info: data?.data?.Media || null, proxy, disqus, }, @@ -179,9 +183,10 @@ export default function Watch({ if (episodes) { 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 episodeList = getProvider?.episodes.slice( + 0, + getMap?.episodes.length + ); const playingData = getMap?.episodes.find( (i) => i.number === Number(epiNumber) ); @@ -219,6 +224,7 @@ export default function Watch({ return () => { setEpisodeNavigation(null); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessions?.user?.name, epiNumber, dub]); useEffect(() => { @@ -287,6 +293,8 @@ export default function Watch({ }); setMarked(0); }; + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [provider, watchId, info?.id]); useEffect(() => { @@ -524,7 +532,7 @@ export default function Watch({ </div> <div id="secondary" - className={`relative ${theaterMode ? "pt-2" : ""}`} + className={`relative ${theaterMode ? "pt-5" : "pt-4 lg:pt-0"}`} > <EpisodeLists info={info} @@ -534,6 +542,7 @@ export default function Watch({ watchId={watchId} episode={episodesList} artStorage={artStorage} + track={episodeNavigation} dub={dub} /> </div> diff --git a/pages/en/index.js b/pages/en/index.js index 9be3c2c..29b0778 100644 --- a/pages/en/index.js +++ b/pages/en/index.js @@ -118,23 +118,23 @@ 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(); - }, []); + // 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([]); @@ -290,6 +290,8 @@ export default function Home({ detail, populars, upComing }) { } } userData(); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessions?.user?.name, currentAnime, plan]); // console.log({ recentAdded }); @@ -402,7 +404,7 @@ export default function Home({ detail, populars, upComing }) { </div> )} - <div className="lg:mt-16 mt-5 flex flex-col gap-5 items-center"> + <div className="lg:mt-16 mt-5 flex flex-col items-center"> <motion.div className="w-screen flex-none lg:w-[95%] xl:w-[87%]" initial={{ opacity: 0 }} diff --git a/pages/en/manga/[...id].js b/pages/en/manga/[...id].js new file mode 100644 index 0000000..106bce2 --- /dev/null +++ b/pages/en/manga/[...id].js @@ -0,0 +1,425 @@ +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 { + 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, + }; + + 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].js b/pages/en/manga/[id].js deleted file mode 100644 index 6f25532..0000000 --- a/pages/en/manga/[id].js +++ /dev/null @@ -1,146 +0,0 @@ -import ChapterSelector from "@/components/manga/chapters"; -import HamburgerMenu from "@/components/manga/mobile/hamburgerMenu"; -import TopSection from "@/components/manga/info/topSection"; -import Footer from "@/components/shared/footer"; -import Head from "next/head"; -import { useEffect, useState } from "react"; -import { setCookie } from "nookies"; -import { getServerSession } from "next-auth"; -import { authOptions } from "../../api/auth/[...nextauth]"; -import getAnifyInfo from "@/lib/anify/info"; -import { NewNavbar } from "@/components/shared/NavBar"; - -export default function Manga({ info, userManga }) { - const [domainUrl, setDomainUrl] = useState(""); - const [firstEp, setFirstEp] = useState(); - const chaptersData = info.chapters.data; - - useEffect(() => { - setDomainUrl(window.location.origin); - }, []); - - return ( - <> - <Head> - <title> - {info - ? `Manga - ${ - info.title.romaji || info.title.english || info.title.native - }` - : "Getting Info..."} - </title> - <meta name="twitter:card" content="summary_large_image" /> - <meta - name="twitter:title" - content={`Moopa - ${info.title.romaji || info.title.english}`} - /> - <meta - name="twitter:description" - content={`${info.description?.slice(0, 180)}...`} - /> - <meta - name="twitter:image" - content={`${domainUrl}/api/og?title=${ - info.title.romaji || info.title.english - }&image=${info.bannerImage || info.coverImage}`} - /> - <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 /> - <NewNavbar info={info} manga={true} /> - <div className="flex flex-col w-screen items-center gap-5 md:gap-10 py-10 pt-nav"> - <div className="flex-center w-full relative z-30"> - <TopSection info={info} firstEp={firstEp} setCookie={setCookie} /> - <> - <div className="absolute hidden md:block z-20 bottom-0 h-1/2 w-full bg-secondary" /> - <div className="absolute hidden md:block z-20 top-0 h-1/2 w-full bg-transparent" /> - </> - </div> - <div className="w-[90%] xl:w-[70%] min-h-[35vh] z-40"> - {chaptersData.length > 0 ? ( - <ChapterSelector - chaptersData={chaptersData} - data={info} - setFirstEp={setFirstEp} - setCookie={setCookie} - userManga={userManga} - /> - ) : ( - <p>No Chapter Available :(</p> - )} - </div> - </div> - <Footer /> - </div> - </> - ); -} - -export async function getServerSideProps(context) { - 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; - const data = await getAnifyInfo(id, key); - - let userManga = null; - - if (session) { - 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) { - Media (id: $id) { - mediaListEntry { - status - progress - progressVolumes - status - } - id - idMal - title { - romaji - english - native - } - } - } - `, - variables: { - id: parseInt(id), - }, - }), - }); - const data = await response.json(); - const user = data?.data?.Media?.mediaListEntry; - if (user) { - userManga = user; - } - } - - if (!data?.chapters) { - return { - notFound: true, - }; - } - - return { - props: { - info: data, - userManga, - }, - }; -} diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js index a7769e2..1076601 100644 --- a/pages/en/manga/read/[...params].js +++ b/pages/en/manga/read/[...params].js @@ -10,13 +10,27 @@ import { authOptions } from "../../../api/auth/[...nextauth]"; import BottomBar from "@/components/manga/mobile/bottomBar"; import TopBar from "@/components/manga/mobile/topBar"; import Head from "next/head"; -import nookies from "nookies"; import ShortCutModal from "@/components/manga/modals/shortcutModal"; import ChapterModal from "@/components/manga/modals/chapterModal"; -import getAnifyPage from "@/lib/anify/page"; +// import getConsumetPages from "@/lib/consumet/manga/getPage"; +import { mediaInfoQuery } from "@/lib/graphql/query"; +// import { redis } from "@/lib/redis"; +// import getConsumetChapters from "@/lib/consumet/manga/getChapters"; +import { toast } from "sonner"; +import axios from "axios"; +import { redis } from "@/lib/redis"; +import getAnifyInfo from "@/lib/anify/info"; -export default function Read({ data, currentId, sessions }) { - const [info, setInfo] = useState(); +export default function Read({ + data, + info, + chaptersData, + currentId, + sessions, + provider, + mangaDexId, + number, +}) { const [chapter, setChapter] = useState([]); const [layout, setLayout] = useState(1); @@ -30,8 +44,8 @@ export default function Read({ data, currentId, sessions }) { const [paddingX, setPaddingX] = useState(208); const [scaleImg, setScaleImg] = useState(1); - const [nextChapterId, setNextChapterId] = useState(null); - const [prevChapterId, setPrevChapterId] = useState(null); + const [nextChapter, setNextChapter] = useState(null); + const [prevChapter, setPrevChapter] = useState(null); const [currentChapter, setCurrentChapter] = useState(null); const [currentPage, setCurrentPage] = useState(0); @@ -40,17 +54,22 @@ export default function Read({ data, currentId, sessions }) { const router = useRouter(); + // console.log({ info }); + useEffect(() => { - hasRun.current = false; - }, [currentId]); + toast.message("This page is still under development", { + description: "If you found any bugs, please report it to us!", + position: "top-center", + duration: 10000, + }); + }, []); useEffect(() => { - const get = JSON.parse(localStorage.getItem("manga")); - const chapters = get.manga; + hasRun.current = false; + const chapters = chaptersData.find((x) => x.providerId === provider); const currentChapter = chapters.chapters?.find((x) => x.id === currentId); setCurrentChapter(currentChapter); - setInfo(get.data); setChapter(chapters); if (Array.isArray(chapters?.chapters)) { @@ -60,25 +79,36 @@ export default function Read({ data, currentId, sessions }) { if (currentIndex !== -1) { const nextChapter = chapters.chapters[currentIndex - 1]; const prevChapter = chapters.chapters[currentIndex + 1]; - setNextChapterId(nextChapter ? nextChapter.id : null); - setPrevChapterId(prevChapter ? prevChapter.id : null); + setNextChapter(nextChapter ? nextChapter : null); + setPrevChapter(prevChapter ? prevChapter : null); } } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentId]); useEffect(() => { const handleKeyDown = (event) => { - if (event.key === "ArrowRight" && event.ctrlKey && nextChapterId) { + event.preventDefault(); + if (event.key === "ArrowRight" && event.ctrlKey && nextChapter?.id) { router.push( - `/en/manga/read/${chapter.providerId}?id=${ - info.id - }&chapterId=${encodeURIComponent(nextChapterId)}` + `/en/manga/read/${ + chapter.providerId + }?id=${mangaDexId}&chapterId=${encodeURIComponent(nextChapter?.id)}${ + info?.id?.length > 6 ? "" : `&anilist=${info?.id}` + }&num=${nextChapter?.number}` ); - } else if (event.key === "ArrowLeft" && event.ctrlKey && prevChapterId) { + } else if ( + event.key === "ArrowLeft" && + event.ctrlKey && + prevChapter?.id + ) { router.push( - `/en/manga/read/${chapter.providerId}?id=${ - info.id - }&chapterId=${encodeURIComponent(prevChapterId)}` + `/en/manga/read/${ + chapter.providerId + }?id=${mangaDexId}&chapterId=${encodeURIComponent(prevChapter?.id)}${ + info?.id?.length > 6 ? "" : `&anilist=${info?.id}` + }&num=${prevChapter?.number}` ); } if (event.code === "Slash" && event.ctrlKey) { @@ -99,7 +129,9 @@ export default function Read({ data, currentId, sessions }) { return () => { window.removeEventListener("keydown", handleKeyDown); }; - }, [nextChapterId, prevChapterId, visible, isKeyOpen, paddingX]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nextChapter?.id, prevChapter?.id, visible, isKeyOpen, paddingX]); return ( <> @@ -134,13 +166,15 @@ export default function Read({ data, currentId, sessions }) { <TopBar info={info} /> <BottomBar id={info?.id} - prevChapter={prevChapterId} - nextChapter={nextChapterId} + prevChapter={prevChapter} + nextChapter={nextChapter} currentPage={currentPage} chapter={chapter} - page={data} + data={data} setSeekPage={setSeekPage} setIsOpen={setIsChapOpen} + number={number} + mangadexId={mangaDexId} /> </> )} @@ -149,13 +183,17 @@ export default function Read({ data, currentId, sessions }) { data={chapter} page={data} info={info} + number={number} + mediaId={mangaDexId} currentId={currentId} setSeekPage={setSeekPage} + providerId={provider} /> )} {layout === 1 && ( <FirstPanel aniId={info?.id} + providerId={provider} data={data} hasRun={hasRun} currentId={currentId} @@ -164,19 +202,22 @@ export default function Read({ data, currentId, sessions }) { visible={visible} setVisible={setVisible} chapter={chapter} - nextChapter={nextChapterId} - prevChapter={prevChapterId} + nextChapter={nextChapter} + prevChapter={prevChapter} paddingX={paddingX} session={sessions} mobileVisible={mobileVisible} setMobileVisible={setMobileVisible} setCurrentPage={setCurrentPage} + mangadexId={mangaDexId} + number={number} /> )} {layout === 2 && ( <SecondPanel aniId={info?.id} data={data} + chapterData={chapter} hasRun={hasRun} currentChapter={currentChapter} currentId={currentId} @@ -185,12 +226,14 @@ export default function Read({ data, currentId, sessions }) { visible={visible} setVisible={setVisible} session={sessions} + providerId={provider} /> )} {layout === 3 && ( <ThirdPanel aniId={info?.id} data={data} + chapterData={chapter} hasRun={hasRun} currentId={currentId} currentChapter={currentChapter} @@ -202,6 +245,7 @@ export default function Read({ data, currentId, sessions }) { scaleImg={scaleImg} setMobileVisible={setMobileVisible} mobileVisible={mobileVisible} + providerId={provider} /> )} {visible && ( @@ -224,42 +268,130 @@ export default function Read({ data, currentId, sessions }) { )} </div> </> + // <p></p> ); } -export async function getServerSideProps(context) { - const cookies = nookies.get(context); +async function fetchAnifyPages(id, number, provider, readId, key) { + try { + let cached; + cached = await redis.get(`pages:${readId}`); + + if (cached) { + return JSON.parse(cached); + } + + const url = `https://api.anify.tv/pages?id=${id}&chapterNumber=${number}&providerId=${provider}&readId=${encodeURIComponent( + readId + )}`; + + const { data } = await axios.get(url); + + if (!data) { + return null; + } + + await redis.set( + `pages:${readId}`, + JSON.stringify(data), + "EX", + 60 * 60 * 24 * 7 + ); + + return data; + } catch (error) { + return { error: "Error fetching data" }; + } +} + +export async function getServerSideProps(context) { const key = process.env.API_KEY; const query = context.query; const providerId = query.params[0]; const chapterId = query.chapterId; const mediaId = query.id; + const number = query.num; + const anilistId = query.anilist; + + const session = await getServerSession(context.req, context.res, authOptions); + const accessToken = session?.user?.token || null; + + // const data = await getConsumetPages(mediaId, providerId, chapterId, key); + // const chapters = await getConsumetChapters(mediaId, redis); + + const dataManga = await fetchAnifyPages( + mediaId, + number, + providerId, + chapterId, + mediaId, + key + ); + + let info; - if (!cookies.manga || cookies.manga !== mediaId) { + if (anilistId) { + 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 json = await response.json(); + info = json?.data?.Media; + } else { + const datas = await getAnifyInfo(mediaId); + if (datas) { + info = datas; + } + } + + const chapters = await ( + await fetch("https://api.anify.tv/chapters/" + mediaId + "?apikey=" + key) + ).json(); + + if ((dataManga && dataManga?.error) || dataManga?.length === 0) { return { redirect: { - destination: `/en/manga/${mediaId}`, + destination: `/en/manga/${anilistId}?chapter=404`, }, }; } - const session = await getServerSession(context.req, context.res, authOptions); - - const data = await getAnifyPage(mediaId, providerId, chapterId, key); + /* + const { data } = await axios.get( + `https://beta.moopa.live/api/v2/info/${romaji}${ + english ? `/${english}` : "" + }${native ? `/${native}` : ""}?id=${anilistId}` + ); if (data.error) { return { notFound: true, }; } + */ return { props: { - data: data, + data: dataManga, + mangaDexId: mediaId, + info: info, + number: number, + chaptersData: chapters, currentId: chapterId, sessions: session, + provider: providerId, }, }; } diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].js index b931597..7ef5de3 100644 --- a/pages/en/profile/[user].js +++ b/pages/en/profile/[user].js @@ -5,8 +5,8 @@ import Link from "next/link"; import Head from "next/head"; import { useEffect, useState } from "react"; import { getUser } from "@/prisma/user"; -import { toast } from "react-toastify"; import { NewNavbar } from "@/components/shared/NavBar"; +import { toast } from "sonner"; export default function MyList({ media, sessions, user, time, userSettings }) { const [listFilter, setListFilter] = useState("all"); diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].js index 603cd17..2cb609f 100644 --- a/pages/en/search/[...param].js +++ b/pages/en/search/[...param].js @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { AnimatePresence, motion as m } from "framer-motion"; +import { motion as m } from "framer-motion"; import Skeleton from "react-loading-skeleton"; import { useRouter } from "next/router"; import Link from "next/link"; @@ -25,6 +25,8 @@ import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid"; import useDebounce from "@/lib/hooks/useDebounce"; import { NewNavbar } from "@/components/shared/NavBar"; import MobileNav from "@/components/shared/MobileNav"; +import SearchByImage from "@/components/search/searchByImage"; +import { PlayIcon } from "@heroicons/react/24/outline"; export async function getServerSideProps(context) { const { param } = context.query; @@ -91,9 +93,10 @@ export default function Card({ }) { const inputRef = useRef(null); const router = useRouter(); - // const { data: session } = useSession(); const [data, setData] = useState(); + const [imageSearch, setImageSearch] = useState(); + const [loading, setLoading] = useState(true); const [search, setQuery] = useState(query); @@ -125,16 +128,18 @@ export default function Card({ }); if (data?.media?.length === 0) { setNextPage(false); + setLoading(false); } else if (data !== null && page > 1) { setData((prevData) => { return [...(prevData ?? []), ...data?.media]; }); setNextPage(data?.pageInfo.hasNextPage); + setLoading(false); } else { setData(data?.media); + setNextPage(data?.pageInfo.hasNextPage); + setLoading(false); } - setNextPage(data?.pageInfo.hasNextPage); - setLoading(false); } useEffect(() => { @@ -142,6 +147,7 @@ export default function Card({ setPage(1); setNextPage(true); advance(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ debounceSearch, type?.value, @@ -153,11 +159,17 @@ export default function Card({ ]); useEffect(() => { + if (imageSearch) return; advance(); - }, [page]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page, imageSearch]); useEffect(() => { function handleScroll() { + if (imageSearch) { + window.removeEventListener("scroll", handleScroll); + return; + } if (page > 10 || !nextPage) { window.removeEventListener("scroll", handleScroll); return; @@ -174,7 +186,7 @@ export default function Card({ window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); - }, [page, nextPage]); + }, [page, nextPage, imageSearch]); const handleKeyDown = async (event) => { if (event.key === "Enter") { @@ -189,6 +201,7 @@ export default function Card({ }; function trash() { + setImageSearch(); setQuery(); setGenre(); setFormat(); @@ -202,6 +215,18 @@ export default function Card({ setIsVisible(!isVisible); } + const handleVideoHover = (hovered, id) => { + const updatedImageSearch = imageSearch?.map((item) => { + if (item.filename === id) { + return { ...item, hovered }; + } + return item; + }); + setImageSearch(updatedImageSearch); + }; + + // console.log({ loading, data }); + return ( <> <Head> @@ -290,6 +315,7 @@ export default function Card({ > <Cog6ToothIcon className="w-5 h-5" /> </div> + <SearchByImage setMedia={setData} setData={setImageSearch} /> <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} @@ -343,91 +369,200 @@ export default function Card({ )} {/* <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 + key="card-keys" + className={`${ + imageSearch ? "hidden" : "" + } grid pt-3 px-5 xl:px-0 xxs:grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 justify-items-center grid-cols-2 w-screen xl:w-auto xl:gap-7 gap-5 gap-y-10`} + > + {loading + ? "" + : !data && ( + <div className="w-full text-[#ff7f57] col-span-6 items-center flex justify-center text-center font-bold font-karla xl:text-2xl"> + Oops!<br></br> Nothing's Found... + </div> + )} + + {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} + > + <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 className="w-full" key={item}> + <div className="w-full"> + <Skeleton + className="w-full rounded" + style={{ + paddingTop: "140%", // 2:3 aspect ratio (3/2 * 100%) + width: "(min-width: 808px) 50vw, 100vw", + lineHeight: 1, + }} + /> </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} + <div> + <h1 className="font-outfit w-[320px] font-bold xl:text-base text-[15px] pt-4 line-clamp-2"> + <Skeleton width={120} height={26} /> + </h1> + </div> + </div> + ))} + </> + )} + </div> + + {imageSearch && ( + <div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 gap-3 md:gap-7 px-5 lg:px-0"> + {imageSearch.map((a, index) => { + return ( + <m.div + key={index} + initial={{ scale: 0.9 }} + animate={{ scale: 1, transition: { duration: 0.35 } }} + className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" + > + <Link + className="relative aspect-video rounded-md overflow-hidden group" + href={`/en/anime/${a.anilist.id}`} + onMouseEnter={() => { + handleVideoHover(true, a.filename); + }} + onMouseLeave={() => handleVideoHover(false, a.filename)} > - <Link - href={ - anime.format === "MANGA" || anime.format === "NOVEL" - ? `/en/manga/${anime.id}` - : `/en/anime/${anime.id}` - } - title={anime.title.userPreferred} - > + <div className="w-full h-full bg-gradient-to-t from-black/70 from-20% to-transparent group-hover:to-black/40 transition-all duration-300 ease-out absolute z-30" /> + <div className="absolute bottom-3 left-0 mx-2 text-white flex gap-2 items-center w-[80%] z-30"> + <PlayIcon className="w-5 h-5 shrink-0" /> + <h1 + className="font-semibold font-karla line-clamp-1" + title={a?.anilist.title.romaji} + > + {`Episode ${a.episode}`} + </h1> + </div> + + {a?.image && ( <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} + src={a?.image} + width={200} + height={200} + alt="Episode Thumbnail" + className={`w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10 ${ + !a.hovered ? "visible" : "hidden" + }`} /> - </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 }} + )} + {a?.video && ( + <video + src={a.video} + className={`w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10 ${ + a.hovered ? "visible" : "hidden" + }`} + autoPlay + muted + loop + playsInline + /> + )} + </Link> + + <Link + className="flex flex-col font-karla w-full" + href={`/en/anime/${a.anilist.id}`} > - <Skeleton className="h-[192px] w-[135px] xl:h-[265px] xl:w-[185px]" /> - <Skeleton width={110} height={30} /> - </div> - ))} - </> - )} + {/* <h1 className="font-semibold">{a.title}</h1> */} + <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]"> + <span + className="text-white max-w-[120px] md:max-w-[200px] lg:max-w-[220px]" + style={{ + display: "inline-block", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }} + title={a?.anilist.title.romaji} + > + {a?.anilist.title.romaji} + </span>{" "} + | Episode {a.episode} + </p> + </Link> + </m.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> + )} + {!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> + )} </div> {/* </div> */} </div> @@ -2,9 +2,24 @@ This document contains a summary of all significant changes made to this release. -## 🎉 Update v4.1.3 +## 🎉 Update v4.2.0 + +### Added + +- Added scene search for anime +- Added next episode button on watch page +- Added episode selector on watch page +- Added dub gogoanime from consumet ### Fixed -- Resolved issue with seek button not working -- Improved homepage and watchpage responsiveness +- Greatly improved search ui/ux +- Fixed when using search palette it focused on other button instead of search input +- Resolved issue: home button on error page doesn't work +- Resolved issue: website showing error when user pressing `pages` button on reader page + +### Changed + +- Searching manga now using Anify instead of AniList +- Info page for Manga now has a similar UI as Anime making it more consistent +- API Key isn't needed anymore diff --git a/styles/globals.css b/styles/globals.css index 256a4f5..17ca472 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -443,6 +443,13 @@ pre code { } } +/* create media queries for mobile */ +/* @media (max-width: 768px) { + .theater { + display: none; + } +} */ + /* Hide the default checkbox */ .containers input { position: absolute; diff --git a/utils/appendMetaToEpisodes.js b/utils/appendMetaToEpisodes.js index eedcbf5..197788b 100644 --- a/utils/appendMetaToEpisodes.js +++ b/utils/appendMetaToEpisodes.js @@ -2,7 +2,7 @@ async function appendMetaToEpisodes(episodesData, images) { // Create a dictionary for faster lookup of images based on episode number const episodeImages = {}; images.forEach((image) => { - episodeImages[image.episode] = image; + episodeImages[image.number || image.episode] = image; }); // Iterate through each provider's episodes data diff --git a/utils/getRedisWithPrefix.js b/utils/getRedisWithPrefix.js index 31a466d..b85589b 100644 --- a/utils/getRedisWithPrefix.js +++ b/utils/getRedisWithPrefix.js @@ -63,6 +63,19 @@ export async function getValuesWithNumericKeys() { return values; } +export async function getKeysWithNumericKeys() { + const allKeys = await redis.keys("*"); // Fetch all keys in Redis + const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers + + const values = []; + + for (const key of numericKeys) { + const value = await redis.del(key); + } + + return values; +} + export async function countNumericKeys() { const allKeys = await redis.keys("*"); // Fetch all keys in Redis const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers diff --git a/utils/getTimes.js b/utils/getTimes.js index d06f797..491d139 100644 --- a/utils/getTimes.js +++ b/utils/getTimes.js @@ -132,3 +132,10 @@ export function unixTimestampToRelativeTime(unixTimestamp) { return "just now"; } + +export function unixToSeconds(unixTimestamp) { + const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds + const secondsAgo = now - unixTimestamp; + + return secondsAgo; +} diff --git a/utils/imageUtils.js b/utils/imageUtils.js new file mode 100644 index 0000000..e5ac6a9 --- /dev/null +++ b/utils/imageUtils.js @@ -0,0 +1,22 @@ +export function getHeaders(providerId) { + switch (providerId) { + case "mangahere": + return { Referer: "https://mangahere.org" }; + case "mangadex": + return { Referer: "https://mangadex.org" }; + case "mangakakalot": + return { Referer: "https://mangakakalot.com" }; + case "mangapill": + return { Referer: "https://mangapill.com" }; + case "mangasee123": + return { Referer: "https://mangasee123.com" }; + case "comick": + return { Referer: "https://comick.app" }; + default: + return null; + } +} + +export function getRandomId() { + return Math.random().toString(36).substr(2, 9); +} |