From 1eee181e219dfd993d396ac3169e7aad3dd285eb Mon Sep 17 00:00:00 2001 From: Factiven Date: Sun, 16 Jul 2023 22:35:39 +0700 Subject: 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) --- components/disqus.js | 17 ++ components/footer.js | 107 +++++++-- components/hero/content.js | 244 --------------------- components/hero/genres.js | 69 ------ components/home/content.js | 267 +++++++++++++++++++++++ components/home/genres.js | 87 ++++++++ components/home/schedule.js | 216 ++++++++++++++++++ components/home/staticNav.js | 112 ++++++++++ components/id-components/player/Artplayer.js | 59 +++++ components/id-components/player/VideoPlayerId.js | 181 +++++++++++++++ components/listEditor.js | 53 +++-- components/manga/chapters.js | 230 ++++++++++++++----- components/manga/info/mobile/mobileButton.js | 39 ++++ components/manga/info/mobile/topMobile.js | 16 ++ components/manga/info/topSection.js | 106 +++++++++ components/manga/leftBar.js | 111 ++++++++++ components/manga/mobile/bottomBar.js | 125 +++++++++++ components/manga/mobile/hamburgerMenu.js | 228 +++++++++++++++++++ components/manga/mobile/topBar.js | 22 ++ components/manga/modals/chapterModal.js | 77 +++++++ components/manga/modals/shortcutModal.js | 197 +++++++++++++++++ components/manga/panels/firstPanel.js | 200 +++++++++++++++++ components/manga/panels/secondPanel.js | 191 ++++++++++++++++ components/manga/panels/thirdPanel.js | 171 +++++++++++++++ components/manga/rightBar.js | 197 +++++++++++++++++ components/media/discord.js | 16 -- components/media/instagram.js | 17 -- components/media/twitter.js | 18 -- components/navbar.js | 42 +++- components/scrollTracker.js | 146 ------------- components/searchBar.js | 19 +- components/useAlert.js | 22 -- components/videoPlayer.js | 2 +- 33 files changed, 2962 insertions(+), 642 deletions(-) create mode 100644 components/disqus.js delete mode 100644 components/hero/content.js delete mode 100644 components/hero/genres.js create mode 100644 components/home/content.js create mode 100644 components/home/genres.js create mode 100644 components/home/schedule.js create mode 100644 components/home/staticNav.js create mode 100644 components/id-components/player/Artplayer.js create mode 100644 components/id-components/player/VideoPlayerId.js create mode 100644 components/manga/info/mobile/mobileButton.js create mode 100644 components/manga/info/mobile/topMobile.js create mode 100644 components/manga/info/topSection.js create mode 100644 components/manga/leftBar.js create mode 100644 components/manga/mobile/bottomBar.js create mode 100644 components/manga/mobile/hamburgerMenu.js create mode 100644 components/manga/mobile/topBar.js create mode 100644 components/manga/modals/chapterModal.js create mode 100644 components/manga/modals/shortcutModal.js create mode 100644 components/manga/panels/firstPanel.js create mode 100644 components/manga/panels/secondPanel.js create mode 100644 components/manga/panels/thirdPanel.js create mode 100644 components/manga/rightBar.js delete mode 100644 components/media/discord.js delete mode 100644 components/media/instagram.js delete mode 100644 components/media/twitter.js delete mode 100644 components/scrollTracker.js delete mode 100644 components/useAlert.js (limited to 'components') 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 ( +
+ +
+ ); +}; +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 ( -
+
{/*

moopa

*/}

moopa

-
-

- © {new Date().getFullYear()} moopa.live | Website Made by - Factiven -

-

- This site does not store any files on our server, we only linked - to the media which is hosted on 3rd party services. -

+
+
+

+ © {new Date().getFullYear()} moopa.live | Website Made by + Factiven +

+

+ This site does not store any files on our server, we only + linked to the media which is hosted on 3rd party services. +

+
+ +
{/*
@@ -43,22 +106,24 @@ function Footer() {
  • This Season
  • - Popular Anime + Popular Anime
  • - Popular Manga + Popular Manga
  • {status === "loading" ? (

    Loading...

    ) : session ? (
  • - My List + + My List +
  • ) : (
  • @@ -70,13 +135,13 @@ function Footer() {
  • - Movies + Movies
  • - TV Shows + TV Shows
  • - DMCA + DMCA
  • diff --git a/components/hero/content.js b/components/hero/content.js deleted file mode 100644 index 24ee942..0000000 --- a/components/hero/content.js +++ /dev/null @@ -1,244 +0,0 @@ -import Link from "next/link"; -import React, { useState, useRef, useEffect } from "react"; -import Image from "next/image"; -import { MdChevronRight } from "react-icons/md"; -import { - ChevronRightIcon, - ArrowRightCircleIcon, -} from "@heroicons/react/24/outline"; - -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 [isDragging, setIsDragging] = useState(false); - const [clicked, setClicked] = useState(false); - - useEffect(() => { - const click = localStorage.getItem("clicked"); - if (click) { - setClicked(JSON.parse(click)); - } - }, []); - - const handleMouseDown = (e) => { - setIsDragging(true); - setStartX(e.pageX - containerRef.current.offsetLeft); - setScrollLefts(containerRef.current.scrollLeft); - }; - - const handleMouseUp = () => { - setIsDragging(false); - }; - - const handleMouseMove = (e) => { - if (!isDragging) return; - e.preventDefault(); - const x = e.pageX - containerRef.current.offsetLeft; - const walk = (x - startX) * 3; - containerRef.current.scrollLeft = scrollLeft - walk; - }; - - const handleClick = (e) => { - if (isDragging) { - e.preventDefault(); - } - }; - - const [scrollLeft, setScrollLeft] = useState(false); - const [scrollRight, setScrollRight] = useState(true); - - const slideLeft = () => { - var slider = document.getElementById(ids); - slider.scrollLeft = slider.scrollLeft - 500; - }; - const slideRight = () => { - var slider = document.getElementById(ids); - slider.scrollLeft = slider.scrollLeft + 500; - }; - - const handleScroll = (e) => { - const scrollLeft = e.target.scrollLeft > 31; - const scrollRight = - e.target.scrollLeft < e.target.scrollWidth - e.target.clientWidth; - setScrollLeft(scrollLeft); - setScrollRight(scrollRight); - }; - - function handleAlert(e) { - if (localStorage.getItem("clicked")) { - const existingDataString = localStorage.getItem("clicked"); - const existingData = JSON.parse(existingDataString); - - existingData[e] = true; - - const updatedDataString = JSON.stringify(existingData); - - localStorage.setItem("clicked", updatedDataString); - } else { - const newData = { - [e]: true, - }; - - const newDataString = JSON.stringify(newData); - - localStorage.setItem("clicked", newDataString); - } - } - - const array = data; - let filteredData = array?.filter((item) => item !== null); - const slicedData = - filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData; - - return ( -
    -
    -

    {section}

    - -
    -
    -
    - -
    -
    - {slicedData?.map((anime) => { - const progress = og?.find((i) => i.mediaId === anime.id); - return ( -
    - - {ids === "onGoing" && ( -
    -
    -

    - {anime.title.romaji || anime.title.english} -

    - {checkProgress(progress) && - !clicked?.hasOwnProperty(anime.id) && ( - - )} - {checkProgress(progress) && ( -
    handleAlert(anime.id)} - className="group-hover:visible invisible absolute top-0 bg-black bg-opacity-20 w-full h-full z-20 text-center" - > -

    - {checkProgress(progress)} -

    -
    - )} -
    -

    Episode {anime.nextAiringEpisode.episode} in

    -

    - {convertSecondsToTime( - anime?.nextAiringEpisode?.timeUntilAiring - )} -

    -
    -
    -
    - )} - {anime.title.romaji - -
    - ); - })} - {filteredData.length >= 10 && section !== "Recommendations" && ( -
    -
    -

    - More on {section} -

    - -
    -
    - )} -
    - -
    -
    - ); -} - -function convertSecondsToTime(sec) { - let days = Math.floor(sec / (3600 * 24)); - let hours = Math.floor((sec % (3600 * 24)) / 3600); - let minutes = Math.floor((sec % 3600) / 60); - - let time = ""; - - if (days > 0) { - time += `${days}d `; - time += `${hours}h`; - } else { - time += `${hours}h `; - time += `${minutes}m`; - } - - return time.trim(); -} - -function checkProgress(entry) { - const { progress, media } = entry; - const { episodes, nextAiringEpisode } = media; - - if (nextAiringEpisode !== null) { - const { episode } = nextAiringEpisode; - - if (episode - progress > 1) { - const missedEpisodes = episode - progress - 1; - return `${missedEpisodes} episode${missedEpisodes > 1 ? "s" : ""} behind`; - } - } - - return; -} diff --git a/components/hero/genres.js b/components/hero/genres.js deleted file mode 100644 index 1c8a475..0000000 --- a/components/hero/genres.js +++ /dev/null @@ -1,69 +0,0 @@ -import Image from "next/image"; -import { ChevronRightIcon } from "@heroicons/react/24/outline"; -import Link from "next/link"; - -const g = [ - { - name: "Action", - img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx20958-HuFJyr54Mmir.jpg", - }, - { - name: "Comedy", - img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx21202-TfzXuWQf2oLQ.png", - }, - { - name: "Horror", - img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx127230-FlochcFsyoF4.png", - }, - { - name: "Romance", - img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx124080-h8EPH92nyRfS.jpg", - }, - { - name: "Music", - img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx130003-5Y8rYzg982sq.png", - }, - { - name: "Sports", - img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx20464-eW7ZDBOcn74a.png", - }, -]; - -export default function Genres() { - return ( -
    -
    -

    Top Genres

    - -
    -
    -
    -
    -
    - {g.map((a, index) => ( - -
    -

    - {a.name} -

    -
    - genres images - - ))} -
    -
    -
    -
    -
    - ); -} diff --git a/components/home/content.js b/components/home/content.js new file mode 100644 index 0000000..d67483d --- /dev/null +++ b/components/home/content.js @@ -0,0 +1,267 @@ +import Link from "next/link"; +import React, { useState, useRef, useEffect } from "react"; +import Image from "next/image"; +import { MdChevronRight } from "react-icons/md"; +import { + ChevronRightIcon, + 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 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); + }; + + const handleMouseUp = () => { + setIsDragging(false); + }; + + const handleMouseMove = (e) => { + if (!isDragging) return; + e.preventDefault(); + const x = e.pageX - containerRef.current.offsetLeft; + const walk = (x - startX) * 3; + containerRef.current.scrollLeft = scrollLeft - walk; + }; + + const handleClick = (e) => { + if (isDragging) { + e.preventDefault(); + } + }; + + const [scrollLeft, setScrollLeft] = useState(false); + const [scrollRight, setScrollRight] = useState(true); + + const slideLeft = () => { + var slider = document.getElementById(ids); + slider.scrollLeft = slider.scrollLeft - 500; + }; + const slideRight = () => { + var slider = document.getElementById(ids); + slider.scrollLeft = slider.scrollLeft + 500; + }; + + const handleScroll = (e) => { + const scrollLeft = e.target.scrollLeft > 31; + const scrollRight = + e.target.scrollLeft < e.target.scrollWidth - e.target.clientWidth; + setScrollLeft(scrollLeft); + setScrollRight(scrollRight); + }; + + function handleAlert(e) { + if (localStorage.getItem("clicked")) { + const existingDataString = localStorage.getItem("clicked"); + const existingData = JSON.parse(existingDataString); + + existingData[e] = true; + + const updatedDataString = JSON.stringify(existingData); + + localStorage.setItem("clicked", updatedDataString); + } else { + const newData = { + [e]: true, + }; + + const newDataString = JSON.stringify(newData); + + localStorage.setItem("clicked", newDataString); + } + } + + const array = data; + let filteredData = array?.filter((item) => item !== null); + const slicedData = + filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData; + + return ( +
    +
    +

    {section}

    + +
    +
    +
    + +
    +
    + {slicedData?.map((anime) => { + const progress = og?.find((i) => i.mediaId === anime.id); + + return ( +
    + + {ids === "onGoing" && ( +
    +
    +

    + {anime.title.romaji || anime.title.english} +

    + {checkProgress(progress) && + !clicked?.hasOwnProperty(anime.id) && ( + + )} + {checkProgress(progress) && ( +
    handleAlert(anime.id)} + className="group-hover:visible invisible absolute top-0 bg-black bg-opacity-20 w-full h-full z-20 text-center" + > +

    + {checkProgress(progress)} +

    +
    + )} + {anime.nextAiringEpisode && ( +
    +

    + Episode {anime.nextAiringEpisode.episode} in +

    +

    + {convertSecondsToTime( + anime?.nextAiringEpisode?.timeUntilAiring + )} +

    +
    + )} +
    +
    + )} + { + +
    + ); + })} + {filteredData.length >= 10 && section !== "Recommendations" && ( +
    +
    +

    + More on {section} +

    + +
    +
    + )} +
    + +
    +
    + ); +} + +function convertSecondsToTime(sec) { + let days = Math.floor(sec / (3600 * 24)); + let hours = Math.floor((sec % (3600 * 24)) / 3600); + let minutes = Math.floor((sec % 3600) / 60); + + let time = ""; + + if (days > 0) { + time += `${days}d `; + time += `${hours}h`; + } else { + time += `${hours}h `; + time += `${minutes}m`; + } + + return time.trim(); +} + +function checkProgress(entry) { + const { progress, media } = entry; + const { episodes, nextAiringEpisode } = media; + + if (nextAiringEpisode !== null) { + const { episode } = nextAiringEpisode; + + if (episode - progress > 1) { + const missedEpisodes = episode - progress - 1; + return `${missedEpisodes} episode${missedEpisodes > 1 ? "s" : ""} behind`; + } + } + + return; +} diff --git a/components/home/genres.js b/components/home/genres.js new file mode 100644 index 0000000..a126c14 --- /dev/null +++ b/components/home/genres.js @@ -0,0 +1,87 @@ +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 = [ + { + name: "Action", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx20958-HuFJyr54Mmir.jpg", + }, + { + name: "Comedy", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx21202-TfzXuWQf2oLQ.png", + }, + { + name: "Horror", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx127230-FlochcFsyoF4.png", + }, + { + name: "Romance", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx124080-h8EPH92nyRfS.jpg", + }, + { + name: "Music", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx130003-5Y8rYzg982sq.png", + }, + { + name: "Sports", + img: "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx20464-eW7ZDBOcn74a.png", + }, +]; + +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 ( +
    +
    +

    Top Genres

    + +
    +
    +
    +
    +
    + {g.map((a, index) => ( + +
    +

    + {a.name} +

    +
    + genres images + + ))} +
    +
    +
    +
    +
    + ); +} 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 ( +
    +

    + Don't miss out! +

    +
    +
    +
    +

    Coming Up Next!

    + + + {data.title.romaji || data.title.english || data.title.native} + + +

    + {data.title.romaji || data.title.english || data.title.native} +

    +
    + {data.bannerImage ? ( + banner next anime + ) : ( + banner next anime + )} +
    + {/* Countdown Timer */} +
    + {/* Countdown Timer */} +
    + {time.days} + Days +
    + +
    + {time.hours} + Hours +
    + +
    + {time.minutes} + Mins +
    + +
    + {time.seconds} + Secs +
    +
    +
    +
    +
    +
    + {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 ( +
    +
    + {uniqueArray.map((i, index) => { + const currentTime = Date.now(); + const hasAired = i.airingAt < currentTime; + + return ( + +
    + coverSchedule +
    +
    +
    +

    + {i.title.romaji} +

    +

    + {convertUnixToTime(i.airingAt)} - Episode{" "} + {i.airingEpisode} +

    +
    +
    + +
    +
    + + ); + })} +
    +
    + ); + })} +
    +
    + +
    {activeSection}
    + +
    +
    +
    +
    + ); +} 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 */} +
    +
    +
    + + moopa + +
      +
    • + + This Season + +
    • +
    • + Manga +
    • +
    • + Anime +
    • + + {status === "loading" ? ( +
    • Loading...
    • + ) : ( + <> + {!sessions && ( +
    • + +
    • + )} + {sessions && ( +
    • + + My List + +
    • + )} + + )} +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + ); +} 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: '', + tooltip: "Backward 5s", + click: function () { + art.backward = 5; + }, + }, + { + name: "fast-forward", + position: "right", + html: '', + 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
    ; +} 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 && ( + { + 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: '', + 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: '', + 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 }) => {
    List Editor
    - - {message && ( - - {message} - - )} -
    {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
    Chapters Not Available
    ; + // 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 ( - <> -
    - {isLoading ? ( -

    Loading...

    - ) : data.chapters?.length > 0 ? ( - data.chapters?.map((chapter, index) => { - return ( -
    - +
    + +
    + + +
    +
    +
    + +
    + {displayedPageNumbers.map((number, index) => + number === "..." ? ( + + ... + + ) : ( + + ) + )} +
    + +
    +
    + {currentChapters.map((chapter, index) => { + const isRead = chapter.number <= userManga?.progress; + return ( +
    + +

    + {chapter.title} +

    +

    - Chapters {index + 1} - -

    - ); - }) - ) : ( -

    No Chapters Available

    - )} + Updated At: {new Date(chapter.updatedAt).toLocaleString()} +

    + +
    + ); + })}
    - +
    ); -} +}; + +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 ( +
    + + +
    + +
    + +
    + ); +} 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 ( +
    + cover image +
    +
    + ); +} 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 ( +
    + +
    + cover image + +
    + + +
    + +
    + +
    +
    +
    +
    +

    + {info.title?.romaji || info.title?.english || info.title?.native} +

    + + {slicedGenre && + slicedGenre.map((genre, index) => { + return ( +
    + {genre} + {index < slicedGenre?.length - 1 && ( + • + )} +
    + ); + })} +
    +
    + + + +
    + {/* Description */} +
    +

    +

    +
    +
    +
    +
    + ); +} 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 ( +
    +
    + + +
    +
    +

    Provider

    +
    +

    + {data.providerId} +

    +
    +
    + {/* Chapters */} +
    +

    Chapters

    +
    +
    + {data?.chapters?.map((x) => { + return ( +
    + +

    + {x.number}.{" "} + {x.title} +

    + +
    + ); + })} +
    +
    +
    + {/* pages */} +
    +

    Pages

    +
    +
    + {Array.isArray(page) ? ( +
    + {page?.map((x) => { + return ( +
    +
    setSeekPage(x.index)} + > + chapter image +

    Page {x.index + 1}

    +
    +
    + ); + })} +
    + ) : ( +
    +

    {page.error || "No Pages."}

    +
    + )} +
    +
    +
    +
    +
    +
    + ); +} 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 ( +
    +
    +
    + + + + +
    + {`${currentPage}/${page.length}`} +
    + {openPage && ( +
    +
    + {Array.isArray(page) ? ( + page.map((x) => { + return ( +
    +
    setSeekPage(x.index)} + > + chapter image +

    Page {x.index + 1}

    +
    +
    + ); + }) + ) : ( +
    not found
    + )} +
    +
    + )} +
    + ); +} 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 && ( + + )} + + {/* Mobile Menu */} +
    + {isVisible && session && ( + + user avatar + + )} + {isVisible && ( +
    +
    + + + + {session ? ( + + ) : ( + + )} +
    + +
    + )} +
    + + ); +} 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 ( +
    + {info && ( + <> + + +

    back

    + + {/*

    moopa

    */} +

    {info.title.romaji}

    + + )} +
    + ); +} 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 ( + <> + + + +
    + + +
    +
    + + + + Select a Chapter + +
    + {data && + data?.chapters?.map((c) => ( + +

    + {c.title} +

    + + ))} +
    +
    +
    +
    +
    +
    +
    + + ); +} 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 ( + <> + + + +
    + + +
    +
    + + + + Keyboard Shortcuts{" "} +
    +
    + CTRL +
    +
    + / +
    +
    +
    +
    +
    +
    + +

    + these shorcuts only work when focused on vertical mode. +

    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + SHIFT +
    +
    + +
    +
    +
    + | +
    +
    +
    + SHIFT +
    +
    + +
    +
    +
    +
    +
    +
    + + {/* Right to Left */} +
    + + {/*

    + these shorcuts only work when focused on Right to Left + mode. +

    */} +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + + {/* works anywhere */} +
    + + +
    +
    + +
    +
    +
    + CTRL +
    +
    + +
    +
    +
    + | +
    +
    +
    + CTRL +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + F +
    +
    +
    +
    +
    +
    + +
    + +
    + + +
    +
    +
    +
    + + ); +} 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 ( +
    +
    + {data && Array.isArray(data) && data?.length > 0 ? ( + data.map((i, index) => ( +
    (imageRefs.current[index] = el)} + > + {i.index} setMobileVisible(!mobileVisible)} + className="w-screen lg:w-full h-auto bg-[#bbb]" + /> +
    + )) + ) : ( +
    + {data.error || "Not found"} :( +
    + )} +
    +
    + + {visible ? ( + + ) : ( + + )} + +
    + + +
    +
    + {`Page ${ + currentImageIndex + 1 + }/${data.length}`} +
    + ); +} 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 ( +
    +
    + {image && Array.isArray(image) && image?.length > 0 ? ( + <> +
    + {image[image.length - index - 2]?.url && ( + Manga Page + )} + Manga Page +
    +
    + + +
    + + ) : ( +
    + {data.error || "Not found"} :( +
    + )} + + {visible ? ( + + ) : ( + + )} + + + Page {index + 1} + {index + 2 > data.length ? "" : `-${index + 2}`}/{data.length} + +
    +
    + ); +} 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 ( +
    +
    + {image && Array.isArray(image) && image?.length > 0 ? ( + <> +
    + 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", + }} + /> +
    +
    + + +
    + + ) : ( +
    + {data.error || "Not found"} :( +
    + )} + + {visible ? ( + + ) : ( + + )} + + + Page {index + 1}/{data.length} + +
    +
    + ); +} 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 ( +
    +
    setIsKeyOpen(true)} + > + +
    +
    +

    Reading mode

    +
    + + +
    +
    + {/* Zoom */} +
    +

    Scale Image

    +
    + + + +
    +
    +
    +

    Tracking

    + {session ? ( +
    +
    + +
    + + +
    +
    +
    + + setProgress(e.target.value)} + className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm" + /> +
    +
    + + setVolumeProgress(e.target.value)} + className="w-full px-2 py-1 rounded-md bg-[#161617] text-sm" + /> +
    + +
    + ) : ( + + )} +
    +
    + ); +} 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 ( - - - - ); -} - -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 ( - - - - - ); -} - -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 ( - - - - ); -} - -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 (
    - moopa + moopa
    {/* Mobile Hamburger */} @@ -57,7 +75,7 @@ function Navbar(props) { > {isVisible && session && (
    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 ( - <> - - {scrolling && ( - - - - - - )} - -
    -
    -
    -
    -
    - - ); -}; - -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) => ( { {query && ( )}
    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 = [ -- cgit v1.2.3