diff options
Diffstat (limited to 'components')
31 files changed, 975 insertions, 815 deletions
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( |