aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-07-16 22:35:39 +0700
committerFactiven <[email protected]>2023-07-16 22:35:39 +0700
commit1eee181e219dfd993d396ac3169e7aad3dd285eb (patch)
tree23fe54e9c3f8810f3ac9ab6b29070b4f0d4b9d20 /components
parentremoved console.log (diff)
downloadmoopa-1eee181e219dfd993d396ac3169e7aad3dd285eb.tar.xz
moopa-1eee181e219dfd993d396ac3169e7aad3dd285eb.zip
Update v3.6.4
- Added Manga page with a working tracker for AniList user - Added schedule component to home page - Added disqus comment section so you can fight on each other (not recommended) - Added /id and /en route for english and indonesian subs (id route still work in progress)
Diffstat (limited to 'components')
-rw-r--r--components/disqus.js17
-rw-r--r--components/footer.js107
-rw-r--r--components/home/content.js (renamed from components/hero/content.js)47
-rw-r--r--components/home/genres.js (renamed from components/hero/genres.js)24
-rw-r--r--components/home/schedule.js216
-rw-r--r--components/home/staticNav.js112
-rw-r--r--components/id-components/player/Artplayer.js59
-rw-r--r--components/id-components/player/VideoPlayerId.js181
-rw-r--r--components/listEditor.js53
-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
-rw-r--r--components/media/discord.js16
-rw-r--r--components/media/instagram.js17
-rw-r--r--components/media/twitter.js18
-rw-r--r--components/navbar.js42
-rw-r--r--components/scrollTracker.js146
-rw-r--r--components/searchBar.js19
-rw-r--r--components/useAlert.js22
-rw-r--r--components/videoPlayer.js2
31 files changed, 2664 insertions, 344 deletions
diff --git a/components/disqus.js b/components/disqus.js
new file mode 100644
index 0000000..b276995
--- /dev/null
+++ b/components/disqus.js
@@ -0,0 +1,17 @@
+import { DiscussionEmbed } from "disqus-react";
+
+const DisqusComments = ({ post }) => {
+ const disqusShortname = "your_disqus_shortname";
+ const disqusConfig = {
+ url: post.url,
+ identifier: post.id, // Single post id
+ title: `${post.title} - Episode ${post.episode}`, // Single post title
+ };
+
+ return (
+ <div>
+ <DiscussionEmbed shortname={disqusShortname} config={disqusConfig} />
+ </div>
+ );
+};
+export default DisqusComments;
diff --git a/components/footer.js b/components/footer.js
index 22c6868..10aa76f 100644
--- a/components/footer.js
+++ b/components/footer.js
@@ -1,31 +1,94 @@
-import Twitter from "./media/twitter";
-import Instagram from "./media/instagram";
import Link from "next/link";
-import Image from "next/image";
import { signIn, useSession } from "next-auth/react";
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import { useRouter } from "next/router";
+import { parseCookies, setCookie } from "nookies";
function Footer() {
const { data: session, status } = useSession();
const [year, setYear] = useState(new Date().getFullYear());
const [season, setSeason] = useState(getCurrentSeason());
+ const [lang, setLang] = useState("en");
+ const [checked, setChecked] = useState(false);
+ const [cookie, setCookies] = useState(null);
+
+ const router = useRouter();
+
+ useEffect(() => {
+ let lang = null;
+ if (!cookie) {
+ const cookie = parseCookies();
+ lang = cookie.lang || null;
+ setCookies(cookie);
+ }
+ if (lang === "en" || lang === null) {
+ setLang("en");
+ setChecked(false);
+ } else if (lang === "id") {
+ setLang("id");
+ setChecked(true);
+ }
+ }, []);
+
+ function switchLang() {
+ setChecked(!checked);
+ if (checked) {
+ console.log("switching to en");
+ setCookie(null, "lang", "en", {
+ maxAge: 365 * 24 * 60 * 60,
+ path: "/",
+ });
+ router.push("/en");
+ } else {
+ console.log("switching to id");
+ setCookie(null, "lang", "id", {
+ maxAge: 365 * 24 * 60 * 60,
+ path: "/",
+ });
+ router.push("/id");
+ }
+ }
+
return (
- <section className="text-[#dbdcdd] z-40 bg-[#0c0d10] lg:flex lg:h-[12rem] lg:items-center lg:justify-between">
+ <section className="text-[#dbdcdd] z-50 bg-[#0c0d10] lg:flex lg:h-[12rem] w-full lg:items-center lg:justify-between">
<div className="mx-auto flex w-[80%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 pb-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0">
<div className="flex items-center gap-24">
<div className="lg:flex grid items-center lg:gap-10 gap-3">
{/* <h1 className="font-outfit text-[2.56rem]">moopa</h1> */}
<h1 className="font-outfit text-[40px]">moopa</h1>
- <div>
- <p className="flex items-center gap-1 font-karla lg:text-[0.81rem] text-[0.7rem] text-[#CCCCCC]">
- &copy; {new Date().getFullYear()} moopa.live | Website Made by
- Factiven
- </p>
- <p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic">
- This site does not store any files on our server, we only linked
- to the media which is hosted on 3rd party services.
- </p>
+ <div className="flex flex-col gap-5">
+ <div className="flex flex-col gap-1">
+ <p className="flex items-center gap-1 font-karla lg:text-[0.81rem] text-[0.7rem] text-[#CCCCCC]">
+ &copy; {new Date().getFullYear()} moopa.live | Website Made by
+ Factiven
+ </p>
+ <p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic">
+ This site does not store any files on our server, we only
+ linked to the media which is hosted on 3rd party services.
+ </p>
+ </div>
+
+ <label className="flex items-center relative w-max cursor-pointer select-none text-txt">
+ <span className="text-base text-[#cccccc] font-inter font-semibold mr-3">
+ Lang
+ </span>
+ <input
+ type="checkbox"
+ checked={checked}
+ onChange={() => switchLang()}
+ className="appearance-none transition-colors cursor-pointer w-14 h-5 rounded-full focus:outline-none focus:ring-offset-2 focus:ring-offset-black focus:ring-action bg-secondary"
+ />
+ <span className="absolute font-medium text-xs uppercase right-2 text-action">
+ {" "}
+ EN{" "}
+ </span>
+ <span className="absolute font-medium text-xs uppercase right-[2.1rem] text-action">
+ {" "}
+ ID{" "}
+ </span>
+ <span className="w-6 h-6 right-[2.1rem] absolute rounded-full transform transition-transform bg-gray-200" />
+ </label>
</div>
</div>
{/* <div className="lg:hidden lg:block">
@@ -43,22 +106,24 @@ function Footer() {
<ul className="flex flex-col gap-y-[0.7rem] ">
<li className="cursor-pointer hover:text-action">
<Link
- href={`/search/anime?season=${season}&seasonYear=${year}`}
+ href={`/${lang}/search/anime?season=${season}&seasonYear=${year}`}
>
This Season
</Link>
</li>
<li className="cursor-pointer hover:text-action">
- <Link href="/search/anime">Popular Anime</Link>
+ <Link href={`/${lang}/search/anime`}>Popular Anime</Link>
</li>
<li className="cursor-pointer hover:text-action">
- <Link href="/search/manga">Popular Manga</Link>
+ <Link href={`/${lang}/search/manga`}>Popular Manga</Link>
</li>
{status === "loading" ? (
<p>Loading...</p>
) : session ? (
<li className="cursor-pointer hover:text-action">
- <Link href={`/profile/${session?.user?.name}`}>My List</Link>
+ <Link href={`/${lang}/profile/${session?.user?.name}`}>
+ My List
+ </Link>
</li>
) : (
<li className="hover:text-action">
@@ -70,13 +135,13 @@ function Footer() {
</ul>
<ul className="flex flex-col gap-y-[0.7rem]">
<li className="cursor-pointer hover:text-action">
- <Link href="/search/anime">Movies</Link>
+ <Link href={`/${lang}/search/anime`}>Movies</Link>
</li>
<li className="cursor-pointer hover:text-action">
- <Link href="/search/anime">TV Shows</Link>
+ <Link href={`/${lang}/search/anime`}>TV Shows</Link>
</li>
<li className="cursor-pointer hover:text-action">
- <Link href="/dmca">DMCA</Link>
+ <Link href={`/${lang}/dmca`}>DMCA</Link>
</li>
<li className="cursor-pointer hover:text-action">
<Link href="https://github.com/DevanAbinaya/Ani-Moopa">
diff --git a/components/hero/content.js b/components/home/content.js
index 24ee942..d67483d 100644
--- a/components/hero/content.js
+++ b/components/home/content.js
@@ -7,28 +7,44 @@ import {
ArrowRightCircleIcon,
} from "@heroicons/react/24/outline";
+import { parseCookies } from "nookies";
+
import { ChevronLeftIcon } from "@heroicons/react/20/solid";
import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
export default function Content({ ids, section, data, og }) {
const [startX, setStartX] = useState(null);
- const [scrollLefts, setScrollLefts] = useState(null);
const containerRef = useRef(null);
+ const [cookie, setCookie] = useState(null);
const [isDragging, setIsDragging] = useState(false);
const [clicked, setClicked] = useState(false);
+ const [lang, setLang] = useState("en");
+
useEffect(() => {
const click = localStorage.getItem("clicked");
+
if (click) {
setClicked(JSON.parse(click));
}
+
+ let lang = null;
+ if (!cookie) {
+ const cookie = parseCookies();
+ lang = cookie.lang || null;
+ setCookie(cookie);
+ }
+ if (lang === "en" || lang === null) {
+ setLang("en");
+ } else if (lang === "id") {
+ setLang("id");
+ }
}, []);
const handleMouseDown = (e) => {
setIsDragging(true);
setStartX(e.pageX - containerRef.current.offsetLeft);
- setScrollLefts(containerRef.current.scrollLeft);
};
const handleMouseUp = () => {
@@ -122,13 +138,14 @@ export default function Content({ ids, section, data, og }) {
>
{slicedData?.map((anime) => {
const progress = og?.find((i) => i.mediaId === anime.id);
+
return (
<div
key={anime.id}
className="flex shrink-0 cursor-pointer items-center"
>
<Link
- href={`/anime/${anime.id}`}
+ href={`/${lang}/anime/${anime.id}`}
className="hover:scale-105 hover:shadow-lg group relative duration-300 ease-out"
>
{ids === "onGoing" && (
@@ -151,14 +168,18 @@ export default function Content({ ids, section, data, og }) {
</h1>
</div>
)}
- <div className="flex gap-1 text-[13px] lg:text-base">
- <h1>Episode {anime.nextAiringEpisode.episode} in</h1>
- <h1 className="font-bold">
- {convertSecondsToTime(
- anime?.nextAiringEpisode?.timeUntilAiring
- )}
- </h1>
- </div>
+ {anime.nextAiringEpisode && (
+ <div className="flex gap-1 text-[13px] lg:text-base">
+ <h1>
+ Episode {anime.nextAiringEpisode.episode} in
+ </h1>
+ <h1 className="font-bold">
+ {convertSecondsToTime(
+ anime?.nextAiringEpisode?.timeUntilAiring
+ )}
+ </h1>
+ </div>
+ )}
</div>
</div>
)}
@@ -170,7 +191,9 @@ export default function Content({ ids, section, data, og }) {
anime.coverImage?.large ||
"https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
}
- alt={anime.title.romaji || anime.title.english}
+ alt={
+ anime.title.romaji || anime.title.english || "coverImage"
+ }
width={209}
height={300}
placeholder="blur"
diff --git a/components/hero/genres.js b/components/home/genres.js
index 1c8a475..a126c14 100644
--- a/components/hero/genres.js
+++ b/components/home/genres.js
@@ -1,6 +1,8 @@
import Image from "next/image";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
+import { useEffect, useState } from "react";
+import { parseCookies } from "nookies";
const g = [
{
@@ -30,6 +32,22 @@ const g = [
];
export default function Genres() {
+ const [lang, setLang] = useState("en");
+ const [cookie, setCookie] = useState(null);
+
+ useEffect(() => {
+ let lang = null;
+ if (!cookie) {
+ const cookie = parseCookies();
+ lang = cookie.lang || null;
+ setCookie(cookie);
+ }
+ if (lang === "en" || lang === null) {
+ setLang("en");
+ } else if (lang === "id") {
+ setLang("id");
+ }
+ }, []);
return (
<div className="antialiased">
<div className="flex items-center justify-between lg:justify-normal lg:gap-3 px-5">
@@ -37,12 +55,12 @@ export default function Genres() {
<ChevronRightIcon className="w-5 h-5" />
</div>
<div className="flex xl:justify-center items-center relative">
- <div className="bg-gradient-to-r from-primary to-transparent z-40 absolute w-7 h-[300px] left-0" />
+ <div className="bg-gradient-to-r from-primary to-transparent z-40 absolute w-7 h-[200px] left-0" />
<div className="flex lg:gap-8 gap-3 lg:p-10 py-8 px-5 z-30 overflow-y-hidden overflow-x-scroll snap-x snap-proximity scrollbar-none relative">
<div className="flex lg:gap-10 gap-3">
{g.map((a, index) => (
<Link
- href={`/search/anime/?genres=${a.name}`}
+ href={`${lang}/search/anime/?genres=${a.name}`}
key={index}
className="relative hover:shadow-lg hover:scale-105 duration-200 cursor-pointer ease-out h-[190px] w-[135px] lg:h-[265px] lg:w-[230px] rounded-md shrink-0"
>
@@ -62,7 +80,7 @@ export default function Genres() {
))}
</div>
</div>
- <div className="bg-gradient-to-l from-primary to-transparent z-40 absolute w-7 h-[300px] right-0" />
+ <div className="bg-gradient-to-l from-primary to-transparent z-40 absolute w-7 h-[200px] right-0" />
</div>
</div>
);
diff --git a/components/home/schedule.js b/components/home/schedule.js
new file mode 100644
index 0000000..35414d2
--- /dev/null
+++ b/components/home/schedule.js
@@ -0,0 +1,216 @@
+import { Textfit } from "react-textfit";
+import Image from "next/image";
+import { useEffect, useRef, useState } from "react";
+import { convertUnixToTime } from "../../utils/getTimes";
+import { PlayIcon } from "@heroicons/react/20/solid";
+import { BackwardIcon, ForwardIcon } from "@heroicons/react/24/solid";
+import Link from "next/link";
+
+export default function Schedule({ data, scheduleData, time }) {
+ let now = new Date();
+ let currentDay =
+ now.toLocaleString("default", { weekday: "long" }).toLowerCase() +
+ "Schedule";
+ currentDay = currentDay.replace("Schedule", "");
+
+ const [activeSection, setActiveSection] = useState(currentDay);
+
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ const index = Object.keys(scheduleData).indexOf(activeSection + "Schedule");
+ if (scrollRef.current) {
+ scrollRef.current.scrollLeft = scrollRef.current.clientWidth * index;
+ }
+ }, [activeSection, scheduleData]);
+
+ const handleScroll = (e) => {
+ const { scrollLeft, clientWidth } = e.target;
+ const index = Math.floor(scrollLeft / clientWidth);
+ let day = Object.keys(scheduleData)[index];
+ day = day.replace("Schedule", "");
+ setActiveSection(day);
+ };
+
+ // buttons to scroll horizontally
+ const scrollLeft = () => {
+ if (scrollRef.current.scrollLeft === 0) {
+ scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
+ } else {
+ scrollRef.current.scrollLeft -= scrollRef.current.offsetWidth;
+ }
+ };
+
+ const scrollRight = () => {
+ const difference =
+ scrollRef.current.scrollWidth -
+ scrollRef.current.offsetWidth -
+ scrollRef.current.scrollLeft;
+ if (difference < 5) {
+ // adjust the threshold as needed
+ scrollRef.current.scrollLeft = 0;
+ } else {
+ scrollRef.current.scrollLeft += scrollRef.current.offsetWidth;
+ }
+ };
+
+ return (
+ <div className="flex flex-col gap-5 px-4 lg:px-0">
+ <h1 className="font-bold font-karla text-[20px] lg:px-5">
+ Don't miss out!
+ </h1>
+ <div className="rounded mb-5 shadow-md shadow-black">
+ <div className="overflow-hidden w-full h-[96px] lg:h-[10rem] rounded relative">
+ <div className="absolute flex flex-col justify-center pl-5 lg:pl-16 rounded z-20 bg-gradient-to-r from-30% from-[#0c0c0c] to-transparent w-full h-full">
+ <h1 className="text-xs lg:text-lg">Coming Up Next!</h1>
+ <Textfit
+ mode="single"
+ min={16}
+ max={40}
+ className="w-1/2 lg:w-2/5 hidden lg:block font-medium font-karla leading-[2.9rem] text-white line-clamp-1"
+ >
+ <Link
+ href={`/en/anime/${data.id}`}
+ className="hover:underline underline-offset-4 decoration-2"
+ >
+ {data.title.romaji || data.title.english || data.title.native}
+ </Link>
+ </Textfit>
+ <h1 className="w-1/2 lg:hidden font-medium font-karla leading-9 text-white line-clamp-1">
+ {data.title.romaji || data.title.english || data.title.native}
+ </h1>
+ </div>
+ {data.bannerImage ? (
+ <Image
+ src={data.bannerImage || data.coverImage.large}
+ width={500}
+ height={500}
+ alt="banner next anime"
+ className="absolute z-10 top-0 right-0 w-3/4 h-full object-cover brightness-[30%]"
+ />
+ ) : (
+ <Image
+ src={data.coverImage.large}
+ width={500}
+ height={500}
+ sizes="100vw"
+ alt="banner next anime"
+ className="absolute z-10 top-0 right-0 h-full object-contain object-right brightness-[90%]"
+ />
+ )}
+ <div
+ className={`absolute flex justify-end items-center pr-5 gap-5 md:gap-10 z-20 w-1/2 h-full right-0 ${
+ data.bannerImage ? "md:pr-16" : "md:pr-48"
+ }`}
+ >
+ {/* Countdown Timer */}
+ <div className="flex items-center gap-2 md:gap-5 font-bold font-karla text-sm md:text-xl">
+ {/* Countdown Timer */}
+ <div className="flex flex-col items-center">
+ <span className="text-action/80">{time.days}</span>
+ <span className="text-sm lg:text-base font-medium">Days</span>
+ </div>
+ <span></span>
+ <div className="flex flex-col items-center">
+ <span className="text-action/80">{time.hours}</span>
+ <span className="text-sm lg:text-base font-medium">Hours</span>
+ </div>
+ <span></span>
+ <div className="flex flex-col items-center">
+ <span className="text-action/80">{time.minutes}</span>
+ <span className="text-sm lg:text-base font-medium">Mins</span>
+ </div>
+ <span></span>
+ <div className="flex flex-col items-center">
+ <span className="text-action/80">{time.seconds}</span>
+ <span className="text-sm lg:text-base font-medium">Secs</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="w-full bg-tersier rounded-b overflow-hidden">
+ <div
+ ref={scrollRef}
+ className="flex overflow-x-scroll snap snap-x snap-proximity scrollbar-hide"
+ onScroll={handleScroll}
+ >
+ {Object.entries(scheduleData).map(([section, data], index) => {
+ const uniqueArray = data.reduce((accumulator, current) => {
+ if (!accumulator.find((item) => item.id === current.id)) {
+ accumulator.push(current);
+ }
+ return accumulator;
+ }, []);
+
+ return (
+ <div
+ key={index}
+ className="snap-start flex-shrink-0 h-[240px] overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-thumb-rounded w-full"
+ style={{ scrollbarGutter: "stable" }}
+ >
+ <div className="flex flex-col gap-2 px-2 pt-2">
+ {uniqueArray.map((i, index) => {
+ const currentTime = Date.now();
+ const hasAired = i.airingAt < currentTime;
+
+ return (
+ <Link
+ key={`${i.id}-${index}`}
+ href={`/en/anime/${i.id}`}
+ className={`${
+ hasAired ? "opacity-40" : ""
+ } h-full w-full flex items-center p-2 flex-shrink-0 hover:bg-secondary cursor-pointer`}
+ >
+ <div className="shrink-0">
+ <Image
+ src={i.coverImage}
+ alt="coverSchedule"
+ width={300}
+ height={300}
+ className="w-10 h-10 object-cover rounded"
+ />
+ </div>
+ <div className="flex items-center justify-between w-full">
+ <div className="font-karla px-2">
+ <h1 className="font-semibold text-sm line-clamp-1">
+ {i.title.romaji}
+ </h1>
+ <p className="font-semibold text-xs text-gray-400">
+ {convertUnixToTime(i.airingAt)} - Episode{" "}
+ {i.airingEpisode}
+ </p>
+ </div>
+ <div>
+ <PlayIcon className="w-6 h-6 text-gray-300" />
+ </div>
+ </div>
+ </Link>
+ );
+ })}
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ <div className="flex items-center bg-tersier justify-between font-karla p-2 border-t border-secondary/40">
+ <button
+ type="button"
+ className="bg-secondary px-2 py-1 rounded"
+ onClick={scrollLeft}
+ >
+ <BackwardIcon className="w-5 h-5" />
+ </button>
+ <div className="font-bold uppercase">{activeSection}</div>
+ <button
+ type="button"
+ className="bg-secondary px-2 py-1 rounded"
+ onClick={scrollRight}
+ >
+ <ForwardIcon className="w-5 h-5" />
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/components/home/staticNav.js b/components/home/staticNav.js
new file mode 100644
index 0000000..93f7b26
--- /dev/null
+++ b/components/home/staticNav.js
@@ -0,0 +1,112 @@
+import { signIn, useSession } from "next-auth/react";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+import { getCurrentSeason } from "../../utils/getTimes";
+import Link from "next/link";
+import { parseCookies } from "nookies";
+
+export default function Navigasi() {
+ const { data: sessions, status } = useSession();
+ const [year, setYear] = useState(new Date().getFullYear());
+ const [season, setSeason] = useState(getCurrentSeason());
+
+ const [lang, setLang] = useState("en");
+ const [cookie, setCookies] = useState(null);
+
+ const router = useRouter();
+
+ useEffect(() => {
+ let lang = null;
+ if (!cookie) {
+ const cookie = parseCookies();
+ lang = cookie.lang || null;
+ setCookies(cookie);
+ }
+ if (lang === "en" || lang === null) {
+ setLang("en");
+ } else if (lang === "id") {
+ setLang("id");
+ }
+ }, []);
+
+ const handleFormSubmission = (inputValue) => {
+ router.push(`/${lang}/search/${encodeURIComponent(inputValue)}`);
+ };
+
+ const handleKeyDown = async (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ const inputValue = event.target.value;
+ handleFormSubmission(inputValue);
+ }
+ };
+ return (
+ <>
+ {/* NAVBAR PC */}
+ <div className="flex items-center justify-center">
+ <div className="flex w-full items-center justify-between px-5 lg:mx-[94px]">
+ <div className="flex items-center lg:gap-16 lg:pt-7">
+ <Link
+ href="/en/"
+ className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]"
+ >
+ moopa
+ </Link>
+ <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex">
+ <li>
+ <Link
+ href={`/en/search/anime?season=${season}&seasonYear=${year}`}
+ >
+ This Season
+ </Link>
+ </li>
+ <li>
+ <Link href="/en/search/manga">Manga</Link>
+ </li>
+ <li>
+ <Link href="/en/search/anime">Anime</Link>
+ </li>
+
+ {status === "loading" ? (
+ <li>Loading...</li>
+ ) : (
+ <>
+ {!sessions && (
+ <li>
+ <button
+ onClick={() => signIn("AniListProvider")}
+ className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md"
+ >
+ Sign in
+ </button>
+ </li>
+ )}
+ {sessions && (
+ <li className="text-center">
+ <Link href={`/en/profile/${sessions?.user.name}`}>
+ My List
+ </Link>
+ </li>
+ )}
+ </>
+ )}
+ </ul>
+ </div>
+ <div className="relative flex lg:scale-75 scale-[65%] items-center mb-7 lg:mb-1">
+ <div className="search-box ">
+ <input
+ className="search-text"
+ type="text"
+ placeholder="Search Anime"
+ onKeyDown={handleKeyDown}
+ />
+ <div className="search-btn">
+ <i className="fas fa-search"></i>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/components/id-components/player/Artplayer.js b/components/id-components/player/Artplayer.js
new file mode 100644
index 0000000..e209433
--- /dev/null
+++ b/components/id-components/player/Artplayer.js
@@ -0,0 +1,59 @@
+import { useEffect, useRef } from "react";
+import Artplayer from "artplayer";
+
+export default function Player({ option, res, getInstance, ...rest }) {
+ const artRef = useRef();
+
+ useEffect(() => {
+ const art = new Artplayer({
+ ...option,
+ container: artRef.current,
+ fullscreen: true,
+ hotkey: true,
+ lock: true,
+ setting: true,
+ playbackRate: true,
+ autoOrientation: true,
+ pip: true,
+ theme: "#f97316",
+ controls: [
+ {
+ name: "fast-rewind",
+ position: "right",
+ html: '<svg class="hi-solid hi-rewind inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>',
+ tooltip: "Backward 5s",
+ click: function () {
+ art.backward = 5;
+ },
+ },
+ {
+ name: "fast-forward",
+ position: "right",
+ html: '<svg class="hi-solid hi-fast-forward inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>',
+ tooltip: "Forward 5s",
+ click: function () {
+ art.forward = 5;
+ },
+ },
+ ],
+ });
+
+ art.events.proxy(document, "keydown", (event) => {
+ if (event.key === "f" || event.key === "F") {
+ art.fullscreen = !art.fullscreen;
+ }
+ });
+
+ if (getInstance && typeof getInstance === "function") {
+ getInstance(art);
+ }
+
+ return () => {
+ if (art && art.destroy) {
+ art.destroy(false);
+ }
+ };
+ }, []);
+
+ return <div ref={artRef} {...rest}></div>;
+}
diff --git a/components/id-components/player/VideoPlayerId.js b/components/id-components/player/VideoPlayerId.js
new file mode 100644
index 0000000..1168313
--- /dev/null
+++ b/components/id-components/player/VideoPlayerId.js
@@ -0,0 +1,181 @@
+import Player from "./Artplayer";
+import { useEffect, useState } from "react";
+import { useAniList } from "../../../lib/anilist/useAnilist";
+
+export default function VideoPlayerId({
+ data,
+ id,
+ progress,
+ session,
+ aniId,
+ stats,
+ op,
+ ed,
+ title,
+ poster,
+}) {
+ const [url, setUrl] = useState("");
+ const [source, setSource] = useState([]);
+ const { markProgress } = useAniList(session);
+
+ const [resolution, setResolution] = useState("auto");
+
+ useEffect(() => {
+ const resol = localStorage.getItem("quality");
+ if (resol) {
+ setResolution(resol);
+ }
+
+ async function compiler() {
+ try {
+ const source = data.map((i) => {
+ return {
+ url: `${i.episode}`,
+ html: `${i.size}p`,
+ };
+ });
+
+ const defSource = source.find(
+ (i) =>
+ i?.html === "1080p" ||
+ i?.html === "720p" ||
+ i?.html === "480p" ||
+ i?.html === "360p"
+ );
+
+ if (defSource) {
+ setUrl(defSource.url);
+ }
+
+ setSource(source);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ compiler();
+ }, [data, resolution]);
+
+ return (
+ <>
+ {url && (
+ <Player
+ key={`${url}`}
+ option={{
+ url: `${url}`,
+ quality: source,
+ title: `${title}`,
+ autoplay: true,
+ screenshot: true,
+ poster: poster ? poster : "",
+ }}
+ res={resolution}
+ quality={source}
+ style={{
+ width: "100%",
+ height: "100%",
+ margin: "0 auto 0",
+ }}
+ getInstance={(art) => {
+ art.on("ready", () => {
+ const seek = art.storage.get(id);
+ const seekTime = seek?.time || 0;
+ const duration = art.duration;
+ const percentage = seekTime / duration;
+
+ if (percentage >= 0.9) {
+ art.currentTime = 0;
+ console.log("Video started from the beginning");
+ } else {
+ art.currentTime = seekTime;
+ }
+ });
+
+ art.on("video:timeupdate", () => {
+ if (!session) return;
+ const mediaSession = navigator.mediaSession;
+ const currentTime = art.currentTime;
+ const duration = art.duration;
+ const percentage = currentTime / duration;
+
+ mediaSession.setPositionState({
+ duration: art.duration,
+ playbackRate: art.playbackRate,
+ position: art.currentTime,
+ });
+
+ if (percentage >= 0.9) {
+ // use >= instead of >
+ markProgress(aniId, progress, stats);
+ art.off("video:timeupdate");
+ console.log("Video progress marked");
+ }
+ });
+
+ art.on("video:timeupdate", () => {
+ var currentTime = art.currentTime;
+ // console.log(art.currentTime);
+ art.storage.set(id, {
+ time: art.currentTime,
+ duration: art.duration,
+ });
+
+ if (
+ op &&
+ currentTime >= op.interval.startTime &&
+ currentTime <= op.interval.endTime
+ ) {
+ // Add the layer if it's not already added
+ if (!art.controls["op"]) {
+ // Remove the other control if it's already added
+ if (art.controls["ed"]) {
+ art.controls.remove("ed");
+ }
+
+ // Add the control
+ art.controls.add({
+ name: "op",
+ position: "top",
+ html: '<button class="skip-button">Skip Opening</button>',
+ click: function (...args) {
+ art.seek = op.interval.endTime;
+ },
+ });
+ }
+ } else if (
+ ed &&
+ currentTime >= ed.interval.startTime &&
+ currentTime <= ed.interval.endTime
+ ) {
+ // Add the layer if it's not already added
+ if (!art.controls["ed"]) {
+ // Remove the other control if it's already added
+ if (art.controls["op"]) {
+ art.controls.remove("op");
+ }
+
+ // Add the control
+ art.controls.add({
+ name: "ed",
+ position: "top",
+ html: '<button class="skip-button">Skip Ending</button>',
+ click: function (...args) {
+ art.seek = ed.interval.endTime;
+ },
+ });
+ }
+ } else {
+ // Remove the controls if they're added
+ if (art.controls["op"]) {
+ art.controls.remove("op");
+ }
+ if (art.controls["ed"]) {
+ art.controls.remove("ed");
+ }
+ }
+ });
+ }}
+ />
+ )}
+ </>
+ );
+}
diff --git a/components/listEditor.js b/components/listEditor.js
index 58177d3..d88f2af 100644
--- a/components/listEditor.js
+++ b/components/listEditor.js
@@ -1,10 +1,8 @@
import { useState } from "react";
-import useAlert from "./useAlert";
-import { AnimatePresence, motion as m } from "framer-motion";
import Image from "next/image";
+import { toast } from "react-toastify";
const ListEditor = ({ animeId, session, stats, prg, max, image = null }) => {
- const { message, type, showAlert } = useAlert();
const [status, setStatus] = useState(stats ?? "");
const [progress, setProgress] = useState(prg ?? 0);
@@ -38,14 +36,41 @@ const ListEditor = ({ animeId, session, stats, prg, max, image = null }) => {
});
const { data } = await response.json();
if (data.SaveMediaListEntry === null) {
- showAlert("Something went wrong", "error");
+ toast.error("Something went wrong", {
+ position: "bottom-right",
+ autoClose: 5000,
+ hideProgressBar: true,
+ closeOnClick: false,
+ pauseOnHover: true,
+ draggable: true,
+ theme: "colored",
+ });
return;
}
console.log("Saved media list entry", data);
- // success();
- showAlert("Media list entry saved", "success");
+ toast.success("Media list entry saved", {
+ position: "bottom-right",
+ autoClose: 5000,
+ hideProgressBar: true,
+ closeOnClick: false,
+ pauseOnHover: true,
+ draggable: true,
+ theme: "dark",
+ });
+ setTimeout(() => {
+ window.location.reload();
+ }, 3000);
+ // showAlert("Media list entry saved", "success");
} catch (error) {
- showAlert("Something went wrong", "error");
+ toast.error("Something went wrong", {
+ position: "bottom-right",
+ autoClose: 5000,
+ hideProgressBar: true,
+ closeOnClick: false,
+ pauseOnHover: true,
+ draggable: true,
+ theme: "colored",
+ });
console.error(error);
}
};
@@ -55,20 +80,6 @@ const ListEditor = ({ animeId, session, stats, prg, max, image = null }) => {
<div className="absolute font-karla font-bold -top-8 rounded-sm px-2 py-1 text-sm">
List Editor
</div>
- <AnimatePresence>
- {message && (
- <m.div
- initial={{ opacity: 0, y: 10 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0, y: 10, transition: { duration: 0.2 } }}
- className={`${
- type === "success" ? "bg-green-500" : "bg-red-500"
- } text-white px-4 py-1 mb-2 rounded-md text-sm sm:text-base`}
- >
- {message}
- </m.div>
- )}
- </AnimatePresence>
<div className="relative bg-secondary rounded-sm w-screen md:w-auto">
<div className="md:flex">
{image && (
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>
+ );
+}
diff --git a/components/media/discord.js b/components/media/discord.js
deleted file mode 100644
index dd8781d..0000000
--- a/components/media/discord.js
+++ /dev/null
@@ -1,16 +0,0 @@
-function Discord(props) {
- return (
- <svg
- className={props.className}
- xmlns="http://www.w3.org/2000/svg"
- width="45"
- height="37"
- fill="none"
- viewBox="0 0 45 37"
- >
- <path d="M36.881 5.047a.107.107 0 00-.054-.05 33.437 33.437 0 00-8.415-2.682.125.125 0 00-.078.01.13.13 0 00-.057.055 24.674 24.674 0 00-1.048 2.212 30.649 30.649 0 00-9.452 0 22.619 22.619 0 00-1.064-2.212.135.135 0 00-.058-.054.13.13 0 00-.077-.011 33.342 33.342 0 00-8.416 2.681.121.121 0 00-.055.05c-5.36 8.226-6.828 16.25-6.108 24.175a.148.148 0 00.054.1 33.947 33.947 0 0010.323 5.36.13.13 0 00.146-.048 25.3 25.3 0 002.111-3.53.136.136 0 00.006-.11.134.134 0 00-.077-.077 22.316 22.316 0 01-3.225-1.58.139.139 0 01-.013-.226c.216-.166.433-.34.64-.515a.125.125 0 01.134-.018c6.766 3.173 14.091 3.173 20.777 0a.124.124 0 01.135.017c.207.175.424.35.643.517a.137.137 0 01.052.116.14.14 0 01-.064.11 20.941 20.941 0 01-3.227 1.578.131.131 0 00-.076.078.14.14 0 00.006.11 28.405 28.405 0 002.11 3.528.128.128 0 00.145.05 33.831 33.831 0 0010.34-5.36.134.134 0 00.055-.098c.862-9.162-1.444-17.121-6.113-24.176zM15.644 24.396c-2.037 0-3.716-1.922-3.716-4.282 0-2.36 1.646-4.28 3.716-4.28 2.086 0 3.748 1.938 3.715 4.28 0 2.36-1.646 4.281-3.715 4.281zm13.737 0c-2.037 0-3.715-1.922-3.715-4.282 0-2.36 1.646-4.28 3.715-4.28 2.087 0 3.749 1.938 3.716 4.28 0 2.36-1.63 4.281-3.716 4.281z"></path>
- </svg>
- );
-}
-
-export default Discord;
diff --git a/components/media/instagram.js b/components/media/instagram.js
deleted file mode 100644
index 909b8c2..0000000
--- a/components/media/instagram.js
+++ /dev/null
@@ -1,17 +0,0 @@
-function Instagram(props) {
- return (
- <svg
- className={props.className}
- xmlns="http://www.w3.org/2000/svg"
- width="37"
- height="37"
- fill="none"
- viewBox="0 0 37 37"
- >
- <path d="M18.402 10.898c-4.568 0-8.353 3.719-8.353 8.352a8.327 8.327 0 008.353 8.353c4.633 0 8.353-3.785 8.353-8.353s-3.785-8.352-8.353-8.352zm0 13.703c-2.937 0-5.35-2.414-5.35-5.35 0-2.937 2.414-5.352 5.35-5.352 2.937 0 5.351 2.415 5.351 5.351 0 2.937-2.414 5.351-5.35 5.351zM27.081 12.594a1.892 1.892 0 100-3.785 1.892 1.892 0 000 3.785z"></path>
- <path d="M31.975 5.807c-1.696-1.762-4.11-2.675-6.852-2.675H11.681c-5.677 0-9.462 3.785-9.462 9.462V25.97c0 2.806.913 5.22 2.74 6.983 1.762 1.696 4.112 2.545 6.787 2.545h13.312c2.806 0 5.155-.914 6.852-2.545 1.762-1.697 2.676-4.111 2.676-6.917V12.594c0-2.74-.914-5.09-2.61-6.787zm-.26 20.23c0 2.023-.718 3.654-1.893 4.763-1.175 1.11-2.806 1.697-4.764 1.697H11.746c-1.958 0-3.589-.587-4.764-1.697-1.174-1.174-1.761-2.806-1.761-4.829V12.594c0-1.958.587-3.59 1.761-4.764 1.11-1.11 2.806-1.696 4.764-1.696H25.19c1.957 0 3.589.587 4.763 1.761 1.11 1.175 1.762 2.806 1.762 4.699v13.443z"></path>
- </svg>
- );
-}
-
-export default Instagram;
diff --git a/components/media/twitter.js b/components/media/twitter.js
deleted file mode 100644
index b62f0d1..0000000
--- a/components/media/twitter.js
+++ /dev/null
@@ -1,18 +0,0 @@
-function Twitter(props) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="37"
- height="37"
- fill="none"
- viewBox="0 0 37 37"
- >
- <path
- className={props.className}
- d="M.289 31.833c4.018.361 7.638-.65 10.934-3.122-3.447-.304-5.753-2.023-7.039-5.247 1.127.138 2.182.174 3.303-.13-3.715-1.156-5.695-3.584-5.962-7.5 1.113.52 2.16.895 3.396.91-2.204-1.655-3.331-3.801-3.288-6.504a7.007 7.007 0 011.012-3.52c4.083 4.777 9.206 7.422 15.515 7.863-.036-.275-.058-.491-.087-.715-.52-4.004 2.082-7.646 6.056-8.405 2.493-.477 4.698.18 6.562 1.9.29.26.535.319.896.225a18.022 18.022 0 004.17-1.626c-.513 1.69-1.568 2.963-3 4.032.326-.058.658-.101.984-.166.34-.072.679-.152 1.019-.246.325-.087.643-.188.96-.296.326-.108.651-.231 1.034-.296-.188.26-.368.534-.57.787a16.848 16.848 0 01-2.87 2.826.57.57 0 00-.195.405c.058 2.565-.303 5.065-1.134 7.494-1.634 4.784-4.48 8.657-8.752 11.425-2.11 1.366-4.415 2.262-6.88 2.782a22.72 22.72 0 01-7.363.318 20.397 20.397 0 01-7.032-2.16c-.585-.297-1.149-.63-1.72-.947.022-.03.036-.058.05-.087z"
- ></path>
- </svg>
- );
-}
-
-export default Twitter;
diff --git a/components/navbar.js b/components/navbar.js
index 2bb2f92..e148b09 100644
--- a/components/navbar.js
+++ b/components/navbar.js
@@ -2,12 +2,16 @@ 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";
function Navbar(props) {
const { data: session, status } = 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);
@@ -18,13 +22,27 @@ function Navbar(props) {
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");
+ }
+ }, []);
+
// console.log(session.user?.image);
return (
<header className={`${props.className}`}>
<div className="flex h-16 w-auto items-center justify-between px-5 lg:mx-auto lg:w-[80%] lg:px-0 text-[#dbdcdd]">
<div className="pb-2 font-outfit text-4xl font-semibold lg:block text-white">
- <Link href="/">moopa</Link>
+ <Link href={`/${lang}/`}>moopa</Link>
</div>
{/* Mobile Hamburger */}
@@ -57,7 +75,7 @@ function Navbar(props) {
>
{isVisible && session && (
<Link
- href={`/profile/${session?.user?.name}`}
+ 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
@@ -73,7 +91,7 @@ function Navbar(props) {
<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="/" className="">
+ <Link href={`/${lang}/`} className="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -90,14 +108,14 @@ function Navbar(props) {
</svg>
</Link>
<Link
- href="/"
+ 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="/about">
+ <Link href={`/${lang}/about`}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -114,7 +132,7 @@ function Navbar(props) {
</svg>
</Link>
<Link
- href="/about"
+ href={`/${lang}/about`}
className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
>
about
@@ -122,7 +140,7 @@ function Navbar(props) {
</button>
<button className="group flex gap-[1.5px] flex-col items-center ">
<div>
- <Link href="/search/anime">
+ <Link href={`/${lang}/search/anime`}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -140,7 +158,7 @@ function Navbar(props) {
</Link>
</div>
<Link
- href="/search/anime"
+ href={`/${lang}/search/anime`}
className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
>
search
@@ -219,7 +237,7 @@ function Navbar(props) {
<ul className="hidden gap-10 font-roboto text-md lg:flex items-center relative">
<li>
<Link
- href="/"
+ href={`/${lang}/`}
className="p-2 transition-all duration-100 hover:text-orange-600"
>
home
@@ -227,7 +245,7 @@ function Navbar(props) {
</li>
<li>
<Link
- href="/about"
+ href={`/${lang}/about`}
className="p-2 transition-all duration-100 hover:text-orange-600"
>
about
@@ -235,7 +253,7 @@ function Navbar(props) {
</li>
<li>
<Link
- href="/search/anime"
+ href={`/${lang}/search/anime`}
className="p-2 transition-all duration-100 hover:text-orange-600"
>
search
@@ -268,7 +286,7 @@ function Navbar(props) {
</button>
<div className="absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all grid place-items-center gap-1">
<Link
- href={`/profile/${session?.user.name}`}
+ href={`/${lang}/profile/${session?.user.name}`}
className="hover:text-action"
>
Profile
diff --git a/components/scrollTracker.js b/components/scrollTracker.js
deleted file mode 100644
index 66419bb..0000000
--- a/components/scrollTracker.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import { useState, useEffect } from "react";
-import { motion as m, AnimatePresence } from "framer-motion";
-
-const ScrollTracker = () => {
- const [scrollPercentage, setScrollPercentage] = useState(0);
- const [scrolling, setScrolling] = useState(false);
- // console.log(id);
-
- function handleUnload() {
- const currentChapter = localStorage.getItem("currentChapterId");
- const scrollData = JSON.parse(localStorage.getItem("watchedManga")) || [];
- const scroll = localStorage.getItem("scrollPercentage");
- if (scroll < 5) {
- return;
- }
-
- const existingDataIndex = scrollData.findIndex(
- (data) => data.id === currentChapter
- );
- if (existingDataIndex !== -1) {
- // Update existing data
- scrollData[existingDataIndex].timestamp = Date.now();
- scrollData[existingDataIndex].percentage = parseFloat(
- localStorage.getItem("scrollPercentage")
- );
- } else {
- // Add new data
- scrollData.push({
- timestamp: Date.now(),
- percentage: parseFloat(localStorage.getItem("scrollPercentage")),
- id: currentChapter,
- });
- }
-
- localStorage.setItem("watchedManga", JSON.stringify(scrollData));
- }
-
- function handlePageHide() {
- localStorage.setItem("scrollPercentage", scrollPercentage);
- handleUnload;
- }
-
- // console.log(data?.id);
-
- useEffect(() => {
- function handleScroll() {
- const scrollTop = document.documentElement.scrollTop;
- const scrollHeight =
- document.documentElement.scrollHeight -
- document.documentElement.clientHeight;
- const percentage = (scrollTop / scrollHeight) * 100;
- setScrollPercentage(percentage);
- localStorage.setItem("scrollPercentage", percentage);
- }
-
- function handlePageshow() {
- const currentChapter = localStorage.getItem("currentChapterId");
- const lastScrollPercentage =
- JSON.parse(localStorage.getItem("watchedManga"))
- ?.filter((data) => data.id === currentChapter)
- .map((data) => data.percentage) || 0;
-
- if (lastScrollPercentage >= 95) {
- return;
- }
-
- const scrollTop =
- (lastScrollPercentage / 100) *
- (document.documentElement.scrollHeight -
- document.documentElement.clientHeight);
- document.documentElement.scrollTop = scrollTop;
- }
-
- window.addEventListener("scroll", handleScroll);
- window.addEventListener("pageshow", handlePageshow);
- window.addEventListener("beforeunload", handleUnload);
- window.addEventListener("pagehide", handlePageHide);
-
- return () => {
- window.removeEventListener("scroll", handleScroll);
- window.removeEventListener("pageshow", handlePageshow);
- window.removeEventListener("beforeunload", handleUnload);
- window.removeEventListener("pagehide", handlePageHide);
- };
- }, []);
-
- useEffect(() => {
- if (scrollPercentage > 5) {
- setScrolling(true);
- } else {
- setScrolling(false);
- }
- }, [scrollPercentage]);
-
- function handleScrollTop(e) {
- e.preventDefault();
- window.scrollTo({
- top: 0,
- behavior: "smooth",
- });
- }
-
- // console.log(scrollPercentage);
-
- return (
- <>
- <AnimatePresence>
- {scrolling && (
- <m.div
- key="back-to-top-button"
- initial={{ opacity: 0, x: 100 }}
- animate={{ opacity: 1, x: 0 }}
- exit={{ opacity: 0 }}
- className="fixed lg:right-10 lg:bottom-10 cursor-pointer text-white bottom-9 right-20 rounded-md z-40 bg-[#121212] hover:bg-[#2d303a] p-2 lg:p-3"
- onClick={handleScrollTop}
- >
- <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"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M4.5 15.75l7.5-7.5 7.5 7.5"
- />
- </svg>
- </m.div>
- )}
- </AnimatePresence>
- <div className="fixed bottom-0 w-screen z-40">
- <div className="h-1 w-full relative">
- <div
- className="absolute top-0 left-0 bg-[#ff8a57] h-1"
- style={{ width: `${scrollPercentage}%` }}
- ></div>
- </div>
- </div>
- </>
- );
-};
-
-export default ScrollTracker;
diff --git a/components/searchBar.js b/components/searchBar.js
index 35e9b45..20d2d7c 100644
--- a/components/searchBar.js
+++ b/components/searchBar.js
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from "react";
import { motion as m, AnimatePresence } from "framer-motion";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
-import { useAniList } from "../lib/useAnilist";
+import { useAniList } from "../lib/anilist/useAnilist";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -16,6 +16,8 @@ const SearchBar = () => {
const [data, setData] = useState(null);
const [query, setQuery] = useState("");
+ const [lang, setLang] = useState("en");
+
useEffect(() => {
if (isOpen) {
searchBoxRef.current.querySelector("input").focus();
@@ -58,10 +60,19 @@ const SearchBar = () => {
}
}, [query]);
+ useEffect(() => {
+ const lang = localStorage.getItem("lang") || "id";
+ if (lang === "en" || lang === null) {
+ setLang("en");
+ } else if (lang === "id") {
+ setLang("id");
+ }
+ }, []);
+
function handleSubmit(e) {
e.preventDefault();
if (data?.media.length) {
- router.push(`/anime/${data?.media[0].id}`);
+ router.push(`${lang}/anime/${data?.media[0].id}`);
}
}
@@ -92,7 +103,7 @@ const SearchBar = () => {
{data?.media.map((i) => (
<Link
key={i.id}
- href={i.type === "ANIME" ? `/anime/${i.id}` : `/`}
+ href={i.type === "ANIME" ? `${lang}/anime/${i.id}` : `/`}
className="flex hover:bg-[#3e3e3e] rounded-md"
>
<Image
@@ -131,7 +142,7 @@ const SearchBar = () => {
{query && (
<button className="flex items-center gap-2 justify-center">
<MagnifyingGlassIcon className="h-5 w-5" />
- <Link href={`/search/${query}`}>More Results...</Link>
+ <Link href={`${lang}/search/${query}`}>More Results...</Link>
</button>
)}
</div>
diff --git a/components/useAlert.js b/components/useAlert.js
deleted file mode 100644
index fa82c42..0000000
--- a/components/useAlert.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useState } from "react";
-
-const useAlert = () => {
- const [message, setMessage] = useState("");
- const [type, setType] = useState("");
-
- const showAlert = (message, type = "success") => {
- setMessage(message);
- setType(type);
- setTimeout(() => {
- setMessage("");
- setType("");
- if (type === "success") {
- window.location.reload();
- }
- }, 3000);
- };
-
- return { message, type, showAlert };
-};
-
-export default useAlert;
diff --git a/components/videoPlayer.js b/components/videoPlayer.js
index 6d98af0..22e6916 100644
--- a/components/videoPlayer.js
+++ b/components/videoPlayer.js
@@ -1,6 +1,6 @@
import Player from "../lib/Artplayer";
import { useEffect, useState } from "react";
-import { useAniList } from "../lib/useAnilist";
+import { useAniList } from "../lib/anilist/useAnilist";
import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
const fontSize = [