diff options
| author | Factiven <[email protected]> | 2023-07-16 22:35:39 +0700 |
|---|---|---|
| committer | Factiven <[email protected]> | 2023-07-16 22:35:39 +0700 |
| commit | 1eee181e219dfd993d396ac3169e7aad3dd285eb (patch) | |
| tree | 23fe54e9c3f8810f3ac9ab6b29070b4f0d4b9d20 /components | |
| parent | removed console.log (diff) | |
| download | moopa-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')
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]"> - © {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]"> + © {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 = [ |