diff options
Diffstat (limited to 'components/manga')
| -rw-r--r-- | components/manga/chapters.js | 230 | ||||
| -rw-r--r-- | components/manga/info/mobile/mobileButton.js | 39 | ||||
| -rw-r--r-- | components/manga/info/mobile/topMobile.js | 16 | ||||
| -rw-r--r-- | components/manga/info/topSection.js | 106 | ||||
| -rw-r--r-- | components/manga/leftBar.js | 111 | ||||
| -rw-r--r-- | components/manga/mobile/bottomBar.js | 125 | ||||
| -rw-r--r-- | components/manga/mobile/hamburgerMenu.js | 228 | ||||
| -rw-r--r-- | components/manga/mobile/topBar.js | 22 | ||||
| -rw-r--r-- | components/manga/modals/chapterModal.js | 77 | ||||
| -rw-r--r-- | components/manga/modals/shortcutModal.js | 197 | ||||
| -rw-r--r-- | components/manga/panels/firstPanel.js | 200 | ||||
| -rw-r--r-- | components/manga/panels/secondPanel.js | 191 | ||||
| -rw-r--r-- | components/manga/panels/thirdPanel.js | 171 | ||||
| -rw-r--r-- | components/manga/rightBar.js | 197 |
14 files changed, 1859 insertions, 51 deletions
diff --git a/components/manga/chapters.js b/components/manga/chapters.js index 56e07ae..fd7beea 100644 --- a/components/manga/chapters.js +++ b/components/manga/chapters.js @@ -1,61 +1,189 @@ -import axios from "axios"; import Link from "next/link"; -import React, { useEffect, useState } from "react"; - -export default function Content({ ids, providers }) { - const [data, setData] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - async function fetchData() { - setIsLoading(true); - try { - const res = await axios.get( - `https://api.eucrypt.my.id/meta/anilist-manga/info/${ids}?provider=${providers}` - ); - const data = res.data; - setData(data); - setError(null); // Reset error state if data is successfully fetched - } catch (error) { - setError(error); +import { useState, useEffect } from "react"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { setCookie } from "nookies"; + +const ChapterSelector = ({ chaptersData, data, setFirstEp, userManga }) => { + const [selectedProvider, setSelectedProvider] = useState( + chaptersData[0]?.providerId || "" + ); + const [selectedChapter, setSelectedChapter] = useState(""); + const [chapters, setChapters] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [chaptersPerPage] = useState(10); + + useEffect(() => { + const selectedChapters = chaptersData.find( + (c) => c.providerId === selectedProvider + ); + if (selectedChapters) { + setSelectedChapter(selectedChapters); + setFirstEp(selectedChapters); } + setChapters(selectedChapters?.chapters || []); + }, [selectedProvider, chaptersData]); + + // Get current posts + const indexOfLastChapter = currentPage * chaptersPerPage; + const indexOfFirstChapter = indexOfLastChapter - chaptersPerPage; + const currentChapters = chapters.slice( + indexOfFirstChapter, + indexOfLastChapter + ); + + // Change page + const paginate = (pageNumber) => setCurrentPage(pageNumber); + const nextPage = () => setCurrentPage((prev) => prev + 1); + const prevPage = () => setCurrentPage((prev) => prev - 1); - setIsLoading(false); + function saveManga() { + localStorage.setItem( + "manga", + JSON.stringify({ manga: selectedChapter, data: data }) + ); + setCookie(null, "manga", data.id, { + maxAge: 24 * 60 * 60, + path: "/", + }); } - useEffect(() => { - fetchData(); - }, [providers, fetchData]); - useEffect(() => { - // console.log("Data changed:", data); - }, [data]); - if (error) { - // Handle 404 Not Found error - return <div>Chapters Not Available</div>; + // console.log(selectedChapter); + + // Create page numbers + const pageNumbers = []; + for (let i = 1; i <= Math.ceil(chapters.length / chaptersPerPage); i++) { + pageNumbers.push(i); } - // console.log(isLoading); + + // Custom function to handle pagination display + const getDisplayedPageNumbers = (currentPage, totalPages, margin) => { + const pageRange = [...Array(totalPages).keys()].map((i) => i + 1); + + if (totalPages <= 10) { + return pageRange; + } + + if (currentPage <= margin) { + return [...pageRange.slice(0, margin), "...", totalPages]; + } + + if (currentPage > totalPages - margin) { + return [1, "...", ...pageRange.slice(-margin)]; + } + + return [ + 1, + "...", + ...pageRange.slice(currentPage - 2, currentPage + 1), + "...", + totalPages, + ]; + }; + + const displayedPageNumbers = getDisplayedPageNumbers( + currentPage, + pageNumbers.length, + 9 + ); + + // console.log(currentChapters); + return ( - <> - <div className="flex h-[540px] flex-col gap-5 overflow-y-scroll"> - {isLoading ? ( - <p>Loading...</p> - ) : data.chapters?.length > 0 ? ( - data.chapters?.map((chapter, index) => { - return ( - <div key={index}> - <Link - href={`/manga/chapter/[chapter]`} - as={`/manga/chapter/read?id=${chapter.id}&provider=${providers}`} + <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"> + <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" + value={selectedProvider} + onChange={(e) => setSelectedProvider(e.target.value)} + > + {/* <option value="">--Select a provider--</option> */} + {chaptersData.map((provider, index) => ( + <option key={index} 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" /> + </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> + ) : ( + <button + key={number} + onClick={() => paginate(number)} + className={`w-10 shrink-0 py-1 rounded-md hover:bg-[#363639] ${ + number === currentPage ? "bg-[#363639]" : "bg-secondary" + }`} + > + {number} + </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} + > + <h2 + className={`text-lg font-medium ${ + isRead ? "text-[#424245]" : "" + }`} + > + {chapter.title} + </h2> + <p + className={`text-[#59595d] ${isRead ? "text-[#313133]" : ""}`} > - Chapters {index + 1} - </Link> - </div> - ); - }) - ) : ( - <p>No Chapters Available</p> - )} + Updated At: {new Date(chapter.updatedAt).toLocaleString()} + </p> + </Link> + </div> + ); + })} </div> - </> + </div> ); -} +}; + +export default ChapterSelector; diff --git a/components/manga/info/mobile/mobileButton.js b/components/manga/info/mobile/mobileButton.js new file mode 100644 index 0000000..0016b59 --- /dev/null +++ b/components/manga/info/mobile/mobileButton.js @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000..2e6b23a --- /dev/null +++ b/components/manga/info/mobile/topMobile.js @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..14dc5e5 --- /dev/null +++ b/components/manga/info/topSection.js @@ -0,0 +1,106 @@ +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} + 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="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 new file mode 100644 index 0000000..272b07a --- /dev/null +++ b/components/manga/leftBar.js @@ -0,0 +1,111 @@ +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 }) { + 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"> + <button + type="button" + onClick={goBack} + className="flex items-center p-2 gap-2 line-clamp-1 cursor-pointer" + > + <ArrowLeftIcon className="w-5 h-5 shrink-0" /> + <h1 className="line-clamp-1 font-semibold text-start text-sm xl:text-base"> + {info?.title?.romaji} + </h1> + </button> + + <div className="flex flex-col p-2 gap-2"> + <div className="flex font-karla flex-col gap-2"> + <h1 className="font-bold xl:text-lg">Provider</h1> + <div className="w-full px-2"> + <p className="bg-[#161617] text-sm xl:text-base capitalize rounded-md py-1 px-2"> + {data.providerId} + </p> + </div> + </div> + {/* Chapters */} + <div className="flex font-karla flex-col gap-2"> + <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) => { + return ( + <div + key={x.id} + 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)}`} + className="" + > + <h1 className="line-clamp-1"> + <span className="font-bold">{x.number}.</span>{" "} + {x.title} + </h1> + </Link> + </div> + ); + })} + </div> + </div> + </div> + {/* pages */} + <div className="flex font-karla flex-col gap-2"> + <h1 className="font-bold xl:text-lg">Pages</h1> + <div className="px-2"> + <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) => { + return ( + <div + key={x.url} + className="hover:bg-[#424245] cursor-pointer rounded-sm w-full" + > + <div + className="flex flex-col items-center cursor-pointer" + onClick={() => setSeekPage(x.index)} + > + <Image + src={`https://img.moopa.live/image-proxy?url=${encodeURIComponent( + x.url + )}&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> + </div> + </div> + ); + })} + </div> + ) : ( + <div className="py-4"> + <p>{page.error || "No Pages."}</p> + </div> + )} + </div> + </div> + </div> + </div> + </div> + </div> + ); +} diff --git a/components/manga/mobile/bottomBar.js b/components/manga/mobile/bottomBar.js new file mode 100644 index 0000000..a388f17 --- /dev/null +++ b/components/manga/mobile/bottomBar.js @@ -0,0 +1,125 @@ +import { + ChevronLeftIcon, + ChevronRightIcon, + ChevronUpIcon, + RectangleStackIcon, +} from "@heroicons/react/24/outline"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useState } from "react"; + +export default function BottomBar({ + id, + prevChapter, + nextChapter, + currentPage, + chapter, + page, + setSeekPage, + setIsOpen, +}) { + 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 ${ + openPage ? "bottom-0" : "bottom-5" + }`} + > + <div className="flex justify-between px-2"> + <div className="flex gap-2"> + <button + type="button" + className={`flex-center shadow-lg ring-1 ring-black ring-opacity-5 rounded-md p-2 ${ + prevChapter + ? "bg-secondary" + : "pointer-events-none bg-[#18181A] text-[#424245]" + }`} + onClick={() => + router.push( + `/en/manga/read/${ + chapter.providerId + }?id=${id}&chapterId=${encodeURIComponent(prevChapter)}` + ) + } + > + <ChevronLeftIcon className="w-5 h-5" /> + </button> + <button + type="button" + className={`flex-center shadow-lg ring-1 ring-black ring-opacity-5 rounded-md p-2 ${ + nextChapter + ? "bg-secondary" + : "pointer-events-none bg-[#18181A] text-[#424245]" + }`} + onClick={() => + router.push( + `/en/manga/read/${ + chapter.providerId + }?id=${id}&chapterId=${encodeURIComponent(nextChapter)}` + ) + } + > + <ChevronRightIcon className="w-5 h-5" /> + </button> + <button + type="button" + className={`flex-center gap-2 shadow-lg ring-1 ring-black ring-opacity-5 rounded-md p-2 bg-secondary`} + onClick={() => setOpenPage(!openPage)} + > + <ChevronUpIcon + className={`w-5 h-5 transition-transform ${ + openPage ? "rotate-180 transform" : "" + }`} + /> + <h1>Pages</h1> + </button> + <button + type="button" + className={`flex-center gap-2 shadow-lg ring-1 ring-black ring-opacity-5 rounded-md p-2 bg-secondary`} + onClick={() => setIsOpen(true)} + > + <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> + </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) => { + return ( + <div + key={x.url} + className="hover:bg-[#424245] shrink-0 cursor-pointer rounded-sm" + > + <div + className="flex flex-col shrink-0 items-center cursor-pointer" + onClick={() => setSeekPage(x.index)} + > + <Image + src={`https://img.moopa.live/image-proxy?url=${encodeURIComponent( + x.url + )}&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> + </div> + </div> + ); + }) + ) : ( + <div>not found</div> + )} + </div> + </div> + )} + </div> + ); +} diff --git a/components/manga/mobile/hamburgerMenu.js b/components/manga/mobile/hamburgerMenu.js new file mode 100644 index 0000000..fcdbcce --- /dev/null +++ b/components/manga/mobile/hamburgerMenu.js @@ -0,0 +1,228 @@ +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/mobile/topBar.js b/components/manga/mobile/topBar.js new file mode 100644 index 0000000..7290e05 --- /dev/null +++ b/components/manga/mobile/topBar.js @@ -0,0 +1,22 @@ +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; + +export default function TopBar({ info }) { + return ( + <div className="fixed lg:hidden flex items-center justify-between px-3 z-50 top-0 h-[5vh] w-screen p-2 bg-secondary"> + {info && ( + <> + <Link + href={`/en/manga/${info.id}`} + className="flex gap-2 items-center" + > + <ArrowLeftIcon className="w-6 h-6" /> + <h1>back</h1> + </Link> + {/* <h1 className="font-outfit text-action font-bold text-lg">moopa</h1> */} + <h1 className="w-[50%] line-clamp-1 text-end">{info.title.romaji}</h1> + </> + )} + </div> + ); +} diff --git a/components/manga/modals/chapterModal.js b/components/manga/modals/chapterModal.js new file mode 100644 index 0000000..ddec0e8 --- /dev/null +++ b/components/manga/modals/chapterModal.js @@ -0,0 +1,77 @@ +import { Dialog, Transition } from "@headlessui/react"; +import Link from "next/link"; +import { Fragment } from "react"; + +export default function ChapterModal({ + id, + currentId, + data, + isOpen, + setIsOpen, +}) { + function closeModal() { + setIsOpen(false); + } + + return ( + <> + <Transition appear show={isOpen} as={Fragment}> + <Dialog as="div" className="relative z-10" onClose={closeModal}> + <Transition.Child + as={Fragment} + enter="ease-out duration-100" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-100" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black bg-opacity-25" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-2 text-center"> + <Transition.Child + as={Fragment} + enter="ease-out duration-100" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-100" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-md max-h-[25rem] transform rounded-2xl bg-secondary px-3 py-4 text-left align-middle shadow-xl transition-all"> + <Dialog.Title + as="h3" + className="font-medium leading-6 text-gray-200" + > + Select a Chapter + </Dialog.Title> + <div className="bg-[#161617] rounded-lg mt-3 flex flex-col overflow-y-scroll scrollbar-thin max-h-[15rem] text-sm"> + {data && + data?.chapters?.map((c) => ( + <Link + key={c.id} + href={`/en/manga/read/${ + data.providerId + }?id=${id}&chapterId=${encodeURIComponent(c.id)}`} + className="p-2 hover:bg-[#424245] rounded-sm" + onClick={closeModal} + > + <h1 + className={`${c.id === currentId && "text-action"}`} + > + {c.title} + </h1> + </Link> + ))} + </div> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + </> + ); +} diff --git a/components/manga/modals/shortcutModal.js b/components/manga/modals/shortcutModal.js new file mode 100644 index 0000000..28790a1 --- /dev/null +++ b/components/manga/modals/shortcutModal.js @@ -0,0 +1,197 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { + ArrowSmallDownIcon, + ArrowSmallLeftIcon, + ArrowSmallRightIcon, + ArrowSmallUpIcon, +} from "@heroicons/react/24/solid"; +import { Fragment } from "react"; + +export default function ShortCutModal({ isOpen, setIsOpen }) { + function closeModal() { + setIsOpen(false); + } + + return ( + <> + <Transition appear show={isOpen} as={Fragment}> + <Dialog as="div" className="relative z-10" onClose={closeModal}> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <div className="fixed inset-0 bg-black bg-opacity-50" /> + </Transition.Child> + + <div className="fixed inset-0 overflow-y-auto"> + <div className="flex min-h-full items-center justify-center p-4 text-center"> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0 scale-95" + enterTo="opacity-100 scale-100" + leave="ease-in duration-200" + leaveFrom="opacity-100 scale-100" + leaveTo="opacity-0 scale-95" + > + <Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-2xl bg-secondary p-6 text-left align-middle shadow-xl transition-all"> + <Dialog.Title + as="h3" + className="flex gap-2 items-center text-xl font-semibold leading-6 text-gray-100" + > + Keyboard Shortcuts{" "} + <div className="flex gap-2 text-white text-xs"> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + CTRL + </div> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + / + </div> + </div> + </Dialog.Title> + <div className="mt-3 w-full bg-gray-500 h-[1px]" /> + <div className="mt-2 flex flex-col flex-wrap gap-10"> + <div className="space-y-1"> + <label className="text-gray-100 font-bold"> + VERTICAL + </label> + <p className="text-sm text-gray-400"> + these shorcuts only work when focused on vertical mode. + </p> + <div className="space-y-2"> + <div className="space-y-2"> + <label className="text-gray-400 text-sm font-karla font-extrabold"> + SCROLL + </label> + <div className="flex gap-2"> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallUpIcon className="w-5 h-5" /> + </div> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallDownIcon className="w-5 h-5" /> + </div> + </div> + </div> + <div className="space-y-2"> + <label className="text-gray-400 text-sm font-karla font-extrabold"> + SCALE IMAGE + </label> + <div className="flex items-center gap-2"> + <div className="flex items-center gap-2"> + <div className="bg-[#424245] text-white text-sm font-bold px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <span>SHIFT</span> + </div> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallUpIcon className="w-5 h-5" /> + </div> + </div> + <div className="font-bold text-gray-400 text-sm"> + | + </div> + <div className="flex items-center gap-2"> + <div className="bg-[#424245] text-white text-sm font-bold px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <span>SHIFT</span> + </div> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallDownIcon className="w-5 h-5" /> + </div> + </div> + </div> + </div> + </div> + </div> + + {/* Right to Left */} + <div className="space-y-1"> + <label className="text-gray-100 font-bold"> + RIGHT TO LEFT + </label> + {/* <p className="text-sm text-gray-400 w-[18rem]"> + these shorcuts only work when focused on Right to Left + mode. + </p> */} + <div className="space-y-2"> + <label className="text-gray-400 text-sm font-karla font-extrabold uppercase"> + Navigate Through Panels + </label> + <div className="flex gap-2"> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallLeftIcon className="w-5 h-5" /> + </div> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallRightIcon className="w-5 h-5" /> + </div> + </div> + </div> + </div> + + {/* works anywhere */} + <div className="space-y-3"> + <label className="text-gray-100 font-bold"> + WORKS ANYWHERE + </label> + + <div className="space-y-4"> + <div className="space-y-2"> + <label className="text-gray-400 text-sm font-karla font-extrabold uppercase"> + Navigate Through Chapters + </label> + <div className="flex items-center gap-2"> + <div className="flex items-center gap-2"> + <div className="bg-[#424245] text-white text-sm font-bold px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <span>CTRL</span> + </div> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallLeftIcon className="w-5 h-5" /> + </div> + </div> + <div className="font-bold text-gray-400 text-sm"> + | + </div> + <div className="flex items-center gap-2"> + <div className="bg-[#424245] text-white text-sm font-bold px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <span>CTRL</span> + </div> + <div className="bg-[#424245] text-white px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + <ArrowSmallRightIcon className="w-5 h-5" /> + </div> + </div> + </div> + </div> + <div className="space-y-2"> + <label className="text-gray-400 text-sm font-karla font-extrabold uppercase"> + Show/Hide SideBar + </label> + <div className="flex"> + <div className="bg-[#424245] text-white text-sm font-bold px-2 py-1 shadow-md shadow-[#141415] rounded-md"> + F + </div> + </div> + </div> + </div> + </div> + </div> + + <div className="mt-4 text-right"> + <button + type="button" + className="inline-flex justify-center rounded-md border border-transparent bg-orange-100 px-4 py-2 text-sm font-medium text-orange-900 hover:bg-orange-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-500 focus-visible:ring-offset-2" + onClick={closeModal} + > + Got it, thanks! + </button> + </div> + </Dialog.Panel> + </Transition.Child> + </div> + </div> + </Dialog> + </Transition> + </> + ); +} diff --git a/components/manga/panels/firstPanel.js b/components/manga/panels/firstPanel.js new file mode 100644 index 0000000..29484be --- /dev/null +++ b/components/manga/panels/firstPanel.js @@ -0,0 +1,200 @@ +import { useEffect, useRef, useState } from "react"; +import { + ArrowsPointingOutIcon, + ArrowsPointingInIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "@heroicons/react/24/outline"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useAniList } from "../../../lib/anilist/useAnilist"; + +export default function FirstPanel({ + aniId, + data, + hasRun, + currentId, + seekPage, + setSeekPage, + visible, + setVisible, + chapter, + nextChapter, + prevChapter, + paddingX, + session, + mobileVisible, + setMobileVisible, + setCurrentPage, +}) { + const { markProgress } = useAniList(session); + const [currentImageIndex, setCurrentImageIndex] = useState(0); + const imageRefs = useRef([]); + const scrollContainerRef = useRef(); + + const router = useRouter(); + + useEffect(() => { + const handleScroll = () => { + const scrollTop = scrollContainerRef.current.scrollTop; + let index = 0; + + for (let i = 0; i < imageRefs.current.length; i++) { + const img = imageRefs.current[i]; + if ( + scrollTop >= img?.offsetTop - scrollContainerRef.current.offsetTop && + scrollTop < + img.offsetTop - + scrollContainerRef.current.offsetTop + + img.offsetHeight + ) { + index = i; + break; + } + } + + if (index === data.length - 3 && !hasRun.current) { + if (session) { + const currentChapter = chapter.chapters?.find( + (x) => x.id === currentId + ); + if (currentChapter) { + markProgress(aniId, currentChapter.number); + console.log("marking progress"); + } + } + hasRun.current = true; + } + + setCurrentPage(index + 1); + setCurrentImageIndex(index); + setSeekPage(index); + }; + + scrollContainerRef?.current?.addEventListener("scroll", handleScroll, { + passive: true, + }); + + return () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.removeEventListener("scroll", handleScroll, { + passive: true, + }); + } + }; + }, [data, session, chapter]); + + useEffect(() => { + if (scrollContainerRef.current && seekPage !== currentImageIndex) { + const targetImageRef = imageRefs.current[seekPage]; + if (targetImageRef) { + scrollContainerRef.current.scrollTo({ + top: targetImageRef.offsetTop - scrollContainerRef.current.offsetTop, + behavior: "smooth", + }); + } + } + }, [seekPage, currentImageIndex]); + + useEffect(() => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo(0, 0); + } + }, [currentId]); + + useEffect(() => { + if (typeof window !== "undefined") { + const root = window.document.documentElement; + root.style.setProperty("--dynamic-padding", `${paddingX}px`); + } + }, [paddingX]); + + return ( + <section className="flex-grow flex flex-col items-center relative"> + <div + // style={{ paddingLeft: paddingX, paddingRight: paddingX }} + className="longPanel h-screen w-full overflow-y-scroll lg:scrollbar-thin scrollbar-thumb-txt scrollbar-thumb-rounded-sm" + ref={scrollContainerRef} + > + {data && Array.isArray(data) && data?.length > 0 ? ( + data.map((i, index) => ( + <div + key={i.url} + className="w-screen lg:h-auto lg:w-full" + ref={(el) => (imageRefs.current[index] = el)} + > + <Image + src={`https://img.moopa.live/image-proxy?url=${encodeURIComponent( + i.url + )}&headers=${encodeURIComponent( + JSON.stringify({ Referer: i.headers.Referer }) + )}`} + alt={i.index} + width={500} + height={500} + onClick={() => setMobileVisible(!mobileVisible)} + className="w-screen lg:w-full h-auto bg-[#bbb]" + /> + </div> + )) + ) : ( + <div className="w-full flex-center h-full"> + {data.error || "Not found"} :( + </div> + )} + </div> + <div className="absolute hidden lg:flex bottom-5 left-5 gap-5"> + <span className="flex bg-secondary p-2 rounded-sm"> + {visible ? ( + <button type="button" onClick={() => setVisible(!visible)}> + <ArrowsPointingOutIcon className="w-5 h-5" /> + </button> + ) : ( + <button type="button" onClick={() => setVisible(!visible)}> + <ArrowsPointingInIcon className="w-5 h-5" /> + </button> + )} + </span> + <div className="flex gap-2"> + <button + type="button" + className={`flex-center rounded-sm p-2 ${ + prevChapter + ? "bg-secondary" + : "pointer-events-none bg-[#18181A] text-[#424245]" + }`} + onClick={() => + router.push( + `/en/manga/read/${ + chapter.providerId + }?id=${aniId}&chapterId=${encodeURIComponent(prevChapter)}` + ) + } + > + <ChevronLeftIcon className="w-5 h-5" /> + </button> + <button + type="button" + className={`flex-center rounded-sm p-2 ${ + nextChapter + ? "bg-secondary" + : "pointer-events-none bg-[#18181A] text-[#424245]" + }`} + onClick={() => + router.push( + `/en/manga/read/${ + chapter.providerId + }?id=${aniId}&chapterId=${encodeURIComponent(nextChapter)}` + ) + } + > + <ChevronRightIcon className="w-5 h-5" /> + </button> + </div> + </div> + <span className="hidden lg:flex bg-secondary p-2 rounded-sm absolute bottom-5 right-5">{`Page ${ + currentImageIndex + 1 + }/${data.length}`}</span> + </section> + ); +} diff --git a/components/manga/panels/secondPanel.js b/components/manga/panels/secondPanel.js new file mode 100644 index 0000000..6048fb4 --- /dev/null +++ b/components/manga/panels/secondPanel.js @@ -0,0 +1,191 @@ +import { useEffect, useRef, useState } from "react"; +import Image from "next/image"; +import { + ArrowsPointingOutIcon, + ArrowsPointingInIcon, +} from "@heroicons/react/24/outline"; +import { useAniList } from "../../../lib/anilist/useAnilist"; + +export default function SecondPanel({ + aniId, + data, + hasRun, + currentChapter, + currentId, + seekPage, + setSeekPage, + visible, + setVisible, + session, +}) { + const [index, setIndex] = useState(0); + const [image, setImage] = useState(null); + + const { markProgress } = useAniList(session); + + useEffect(() => { + setIndex(0); + setSeekPage(0); + }, [data, currentId]); + + const seekToIndex = (newIndex) => { + if (newIndex >= 0 && newIndex < data.length) { + // if newIndex is odd, decrease it by 1 to show the previous page + if (newIndex % 2 !== 0) { + newIndex = newIndex - 1; + } + setIndex(newIndex); + setSeekPage(newIndex); + } + }; + + useEffect(() => { + seekToIndex(seekPage); + }, [seekPage]); + + useEffect(() => { + if (data && Array.isArray(data) && data?.length > 0) { + setImage([...data].reverse()); // Create a copy of data before reversing + } + }, [data]); + + useEffect(() => { + const handleKeyDown = (event) => { + if (event.key === "ArrowRight") { + if (index > 0) { + setIndex(index - 2); + setSeekPage(index - 2); + } + } else if (event.key === "ArrowLeft") { + if (index < image.length - 2) { + setIndex(index + 2); + setSeekPage(index + 2); + } + + 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); + } + + markProgress(aniId, chapterNumber); + hasRun.current = true; + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [index, image]); + + const handleNext = () => { + if (index < image.length - 2) { + setIndex(index + 2); + setSeekPage(index + 2); + } + + 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); + } + + markProgress(aniId, chapterNumber); + hasRun.current = true; + } + }; + + const handlePrev = () => { + if (index > 0) { + setIndex(index - 2); + setSeekPage(index - 2); + } + }; + return ( + <div className="flex-grow h-screen"> + <div className="flex items-center w-full relative group"> + {image && Array.isArray(image) && image?.length > 0 ? ( + <> + <div + className={`flex w-full ${ + image[image.length - index - 2]?.url + ? "justify-between" + : "justify-center" + }`} + > + {image[image.length - index - 2]?.url && ( + <Image + key={image[image.length - index - 2]?.url} + width={500} + height={500} + className="w-1/2 h-screen object-contain" + src={`https://img.moopa.live/image-proxy?url=${encodeURIComponent( + image[image.length - index - 2]?.url + )}&headers=${encodeURIComponent( + JSON.stringify({ + Referer: image[image.length - index - 2]?.headers.Referer, + }) + )}`} + alt="Manga Page" + /> + )} + <Image + key={image[image.length - index - 1]?.url} + width={500} + height={500} + className="w-1/2 h-screen object-contain" + src={`https://img.moopa.live/image-proxy?url=${encodeURIComponent( + image[image.length - index - 1]?.url + )}&headers=${encodeURIComponent( + JSON.stringify({ + Referer: image[image.length - index - 1]?.headers.Referer, + }) + )}`} + alt="Manga Page" + /> + </div> + <div className="absolute w-full hidden group-hover:flex justify-between mt-4"> + <button + className="px-4 py-2 bg-secondary text-white rounded-r" + onClick={handleNext} + > + Next + </button> + <button + className="px-4 py-2 bg-secondary text-white rounded-l" + onClick={handlePrev} + > + Previous + </button> + </div> + </> + ) : ( + <div className="w-full flex-center h-full"> + {data.error || "Not found"} :( + </div> + )} + <span className="absolute hidden group-hover:flex bottom-5 left-5 bg-secondary p-2"> + {visible ? ( + <button type="button" onClick={() => setVisible(!visible)}> + <ArrowsPointingOutIcon className="w-5 h-5" /> + </button> + ) : ( + <button type="button" onClick={() => setVisible(!visible)}> + <ArrowsPointingInIcon className="w-5 h-5" /> + </button> + )} + </span> + <span className="absolute hidden group-hover:flex bottom-5 right-5 bg-secondary p-2"> + Page {index + 1} + {index + 2 > data.length ? "" : `-${index + 2}`}/{data.length} + </span> + </div> + </div> + ); +} diff --git a/components/manga/panels/thirdPanel.js b/components/manga/panels/thirdPanel.js new file mode 100644 index 0000000..7dff76b --- /dev/null +++ b/components/manga/panels/thirdPanel.js @@ -0,0 +1,171 @@ +import { useEffect, useRef, useState } from "react"; +import Image from "next/image"; +import { + ArrowsPointingOutIcon, + ArrowsPointingInIcon, +} from "@heroicons/react/24/outline"; +import { useAniList } from "../../../lib/anilist/useAnilist"; + +export default function ThirdPanel({ + aniId, + data, + hasRun, + currentId, + currentChapter, + seekPage, + setSeekPage, + visible, + setVisible, + session, + scaleImg, + setMobileVisible, + mobileVisible, +}) { + const [index, setIndex] = useState(0); + const [image, setImage] = useState(null); + const { markProgress } = useAniList(session); + + useEffect(() => { + setIndex(0); + setSeekPage(0); + }, [data, currentId]); + + const seekToIndex = (newIndex) => { + if (newIndex >= 0 && newIndex < data.length) { + setIndex(newIndex); + setSeekPage(newIndex); + } + }; + + useEffect(() => { + seekToIndex(seekPage); + }, [seekPage]); + + useEffect(() => { + if (data && Array.isArray(data) && data?.length > 0) { + setImage([...data].reverse()); // Create a copy of data before reversing + } + }, [data]); + + useEffect(() => { + const handleKeyDown = (event) => { + if (event.key === "ArrowRight") { + if (index > 0) { + setIndex(index - 1); + setSeekPage(index - 1); + } + } else if (event.key === "ArrowLeft") { + if (index < image.length - 1) { + setIndex(index + 1); + 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); + } + + markProgress(aniId, chapterNumber); + hasRun.current = true; + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [index, image]); + + const handleNext = () => { + if (index < image.length - 1) { + setIndex(index + 1); + 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); + } + + markProgress(aniId, chapterNumber); + hasRun.current = true; + } + }; + + const handlePrev = () => { + if (index > 0) { + setIndex(index - 1); + setSeekPage(index - 1); + } + }; + + return ( + <div className="flex-grow h-screen"> + <div className="flex items-center w-full relative group"> + {image && Array.isArray(image) && image?.length > 0 ? ( + <> + <div + className={`flex w-full justify-center items-center lg:scrollbar-thin scrollbar-thumb-txt scrollbar-thumb-rounded-sm overflow-x-hidden`} + > + <Image + key={image[image.length - index - 1]?.url} + width={500} + height={500} + className="w-full h-screen object-contain" + onClick={() => setMobileVisible(!mobileVisible)} + src={`https://img.moopa.live/image-proxy?url=${encodeURIComponent( + image[image.length - index - 1]?.url + )}&headers=${encodeURIComponent( + JSON.stringify({ + Referer: image[image.length - index - 1]?.headers.Referer, + }) + )}`} + alt="Manga Page" + style={{ + transform: `scale(${scaleImg})`, + transformOrigin: "top", + }} + /> + </div> + <div className="absolute w-full hidden group-hover:flex justify-between mt-4"> + <button + className="px-4 py-2 bg-secondary text-white rounded-r" + onClick={handleNext} + > + Next + </button> + <button + className="px-4 py-2 bg-secondary text-white rounded-l" + onClick={handlePrev} + > + Previous + </button> + </div> + </> + ) : ( + <div className="w-full flex-center h-full"> + {data.error || "Not found"} :( + </div> + )} + <span className="absolute hidden group-hover:flex bottom-5 left-5 bg-secondary p-2"> + {visible ? ( + <button type="button" onClick={() => setVisible(!visible)}> + <ArrowsPointingOutIcon className="w-5 h-5" /> + </button> + ) : ( + <button type="button" onClick={() => setVisible(!visible)}> + <ArrowsPointingInIcon className="w-5 h-5" /> + </button> + )} + </span> + <span className="absolute hidden group-hover:flex bottom-5 right-5 bg-secondary p-2"> + Page {index + 1}/{data.length} + </span> + </div> + </div> + ); +} diff --git a/components/manga/rightBar.js b/components/manga/rightBar.js new file mode 100644 index 0000000..6d37e4a --- /dev/null +++ b/components/manga/rightBar.js @@ -0,0 +1,197 @@ +import { + ChevronDownIcon, + ExclamationCircleIcon, +} 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"; + +export default function RightBar({ + id, + hasRun, + session, + currentChapter, + paddingX, + setPaddingX, + layout, + setLayout, + setIsKeyOpen, + scaleImg, + setScaleImg, +}) { + const { markProgress } = useAniList(session); + + const [status, setStatus] = useState("CURRENT"); + const [progress, setProgress] = useState(0); + const [volumeProgress, setVolumeProgress] = useState(0); + + useEffect(() => { + if (currentChapter?.number) { + setProgress(currentChapter.number); + } + }, [currentChapter]); + + const saveProgress = async () => { + if (session) { + const parsedProgress = parseFloat(progress); + const parsedVolumeProgress = parseFloat(volumeProgress); + + if ( + parsedProgress === parseInt(parsedProgress) && + parsedVolumeProgress === parseInt(parsedVolumeProgress) + ) { + 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", + }); + } + } + }; + + const changeMode = (e) => { + setLayout(Number(e.target.value)); + // console.log(e.target.value); + }; + + return ( + <div className="hidden lg:flex flex-col gap-5 shrink-0 w-[16rem] bg-secondary py-5 px-3 relative"> + <div + className="fixed right-5 bottom-5 group cursor-pointer" + title="Keyboard Shortcuts" + onClick={() => setIsKeyOpen(true)} + > + <ExclamationCircleIcon className="w-6 h-6" /> + </div> + <div className="flex flex-col gap-3 w-full"> + <h1 className="font-karla font-bold xl:text-lg">Reading mode</h1> + <div className="flex relative"> + <select + className="bg-[#161617] text-sm xl:text-base cursor-pointer w-full p-1 px-3 font-karla rounded-md appearance-none" + defaultValue={layout} + onChange={changeMode} + > + <option value={1}>Vertical</option> + <option value={2}>Right to Left</option> + <option value={3}>Right to Left {"(1 Page)"}</option> + </select> + <ChevronDownIcon className="w-5 h-5 text-white absolute inset-0 my-auto mx-52" /> + </div> + </div> + {/* Zoom */} + <div className="flex flex-col gap-3 w-full"> + <h1 className="font-karla font-bold xl:text-lg">Scale Image</h1> + <div className="grid grid-cols-3 text-sm xl:text-base gap-5 place-content-evenly justify-items-center"> + <button + type="button" + onClick={() => { + setPaddingX(paddingX - 50); + setScaleImg(scaleImg + 0.1); + }} + className="bg-[#161617] w-full flex-center p-1 rounded-md" + > + + + </button> + <button + type="button" + onClick={() => { + setPaddingX(paddingX + 50); + setScaleImg(scaleImg - 0.1); + }} + className="bg-[#161617] w-full flex-center p-1 rounded-md" + > + - + </button> + <button + type="button" + onClick={() => { + setPaddingX(208); + setScaleImg(1); + }} + className="bg-[#161617] w-full flex-center p-1 rounded-md" + > + reset + </button> + </div> + </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" /> + </div> + </div> + <div className="space-y-1"> + <label className="font-karla font-semibold text-gray-500 text-xs"> + Chapter Progress + </label> + <input + 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" + onClick={() => signIn("AniListProvider")} + className="flex-center gap-2 bg-[#363639] hover:bg-[#363639]/50 text-white hover:text-txt p-2 rounded-md cursor-pointer shadow-md" + > + <span className="font-karla">Login to AniList</span> + <div className="flex-center w-5 h-5"> + <AniList /> + </div> + </button> + )} + </div> + </div> + ); +} |