aboutsummaryrefslogtreecommitdiff
path: root/components/manga
diff options
context:
space:
mode:
Diffstat (limited to 'components/manga')
-rw-r--r--components/manga/chapters.js230
-rw-r--r--components/manga/info/mobile/mobileButton.js39
-rw-r--r--components/manga/info/mobile/topMobile.js16
-rw-r--r--components/manga/info/topSection.js106
-rw-r--r--components/manga/leftBar.js111
-rw-r--r--components/manga/mobile/bottomBar.js125
-rw-r--r--components/manga/mobile/hamburgerMenu.js228
-rw-r--r--components/manga/mobile/topBar.js22
-rw-r--r--components/manga/modals/chapterModal.js77
-rw-r--r--components/manga/modals/shortcutModal.js197
-rw-r--r--components/manga/panels/firstPanel.js200
-rw-r--r--components/manga/panels/secondPanel.js191
-rw-r--r--components/manga/panels/thirdPanel.js171
-rw-r--r--components/manga/rightBar.js197
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>
+ );
+}