From 7327a69b55a20b99b14ee0803d6cf5f8b88c45ef Mon Sep 17 00:00:00 2001 From: Factiven Date: Wed, 13 Sep 2023 00:45:53 +0700 Subject: Update v4 - Merge pre-push to main (#71) * Create build-test.yml * initial v4 commit * update: github workflow * update: push on branch * Update .github/ISSUE_TEMPLATE/bug_report.md * configuring next.config.js file --- components/anime/changeView.js | 30 +- components/anime/episode.js | 185 ++++++--- components/anime/infoDetails.js | 6 +- components/anime/mobile/reused/description.js | 44 ++ components/anime/mobile/reused/infoChip.js | 43 ++ components/anime/mobile/topSection.js | 504 ++++++++++++++++++++--- components/anime/viewMode/listMode.js | 58 ++- components/anime/viewMode/thumbnailDetail.js | 25 +- components/anime/viewMode/thumbnailOnly.js | 30 +- components/anime/watch/primarySide.js | 45 +- components/anime/watch/secondarySide.js | 19 +- components/footer.js | 246 ++++++----- components/home/content.js | 254 ++++++++---- components/home/content/historyOptions.js | 56 +++ components/home/genres.js | 4 +- components/home/mobileNav.js | 202 --------- components/home/recommendation.js | 91 ++++ components/home/schedule.js | 28 +- components/home/staticNav.js | 160 ++++--- components/id-components/player/Artplayer.js | 59 --- components/id-components/player/VideoPlayerId.js | 181 -------- components/id/player/Artplayer.js | 59 +++ components/id/player/VideoPlayerId.js | 181 ++++++++ components/navbar.js | 189 +-------- components/search/dropdown/inputSelect.js | 111 +++++ components/search/dropdown/multiSelector.js | 168 ++++++++ components/search/dropdown/singleSelector.js | 98 +++++ components/search/selection.js | 415 +++++++++++++++++++ components/searchBar.js | 155 ------- components/searchPalette.js | 265 ++++++++++++ components/shared/MobileNav.js | 170 ++++++++ components/shared/hamburgerMenu.js | 192 +++++++++ components/shared/loading.js | 20 + components/videoPlayer.js | 27 +- 34 files changed, 3055 insertions(+), 1265 deletions(-) create mode 100644 components/anime/mobile/reused/description.js create mode 100644 components/anime/mobile/reused/infoChip.js create mode 100644 components/home/content/historyOptions.js delete mode 100644 components/home/mobileNav.js create mode 100644 components/home/recommendation.js delete mode 100644 components/id-components/player/Artplayer.js delete mode 100644 components/id-components/player/VideoPlayerId.js create mode 100644 components/id/player/Artplayer.js create mode 100644 components/id/player/VideoPlayerId.js create mode 100644 components/search/dropdown/inputSelect.js create mode 100644 components/search/dropdown/multiSelector.js create mode 100644 components/search/dropdown/singleSelector.js create mode 100644 components/search/selection.js delete mode 100644 components/searchBar.js create mode 100644 components/searchPalette.js create mode 100644 components/shared/MobileNav.js create mode 100644 components/shared/hamburgerMenu.js create mode 100644 components/shared/loading.js (limited to 'components') diff --git a/components/anime/changeView.js b/components/anime/changeView.js index cab9054..75ebdff 100644 --- a/components/anime/changeView.js +++ b/components/anime/changeView.js @@ -1,14 +1,14 @@ -import { useEffect, useState } from "react"; - -export default function ChangeView({ view, setView, episode }) { - // const [view, setView] = useState(1); - // const episode = null; +export default function ChangeView({ view, setView, episode, map }) { return (
0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -30,7 +30,11 @@ export default function ChangeView({ view, setView, episode }) { height="20" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 1 ? "fill-action" @@ -44,7 +48,11 @@ export default function ChangeView({ view, setView, episode }) {
0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "pointer-events-none" : "cursor-pointer" : "pointer-events-none" @@ -61,7 +69,11 @@ export default function ChangeView({ view, setView, episode }) { fill="none" className={`${ episode?.length > 0 - ? episode?.some((item) => item?.title === null) + ? map?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item.title === null + ) || !map ? "fill-[#1c1c22]" : view === 2 ? "fill-action" diff --git a/components/anime/episode.js b/components/anime/episode.js index 5d3451b..b2f4bd7 100644 --- a/components/anime/episode.js +++ b/components/anime/episode.js @@ -1,13 +1,18 @@ import { useEffect, useState, Fragment } from "react"; -import { ChevronDownIcon, ClockIcon } from "@heroicons/react/20/solid"; -import { convertSecondsToTime } from "../../utils/getTimes"; +import { ChevronDownIcon } from "@heroicons/react/20/solid"; import ChangeView from "./changeView"; import ThumbnailOnly from "./viewMode/thumbnailOnly"; import ThumbnailDetail from "./viewMode/thumbnailDetail"; import ListMode from "./viewMode/listMode"; -import axios from "axios"; +import { convertSecondsToTime } from "../../utils/getTimes"; -export default function AnimeEpisode({ info, progress }) { +export default function AnimeEpisode({ + info, + session, + progress, + setProgress, + setWatch, +}) { const [providerId, setProviderId] = useState(); // default provider const [currentPage, setCurrentPage] = useState(1); // for pagination const [visible, setVisible] = useState(false); // for mobile view @@ -19,42 +24,60 @@ export default function AnimeEpisode({ info, progress }) { const [isDub, setIsDub] = useState(false); const [providers, setProviders] = useState(null); + const [mapProviders, setMapProviders] = useState(null); useEffect(() => { setLoading(true); - setProviders(null); const fetchData = async () => { - try { - const { data: firstResponse } = await axios.get( - `/api/consumet/episode/${info.id}${isDub === true ? "?dub=true" : ""}` - ); - if (firstResponse.data.length > 0) { - const defaultProvider = firstResponse.data?.find( - (x) => x.providerId === "gogoanime" - ); - setProviderId( - defaultProvider?.providerId || firstResponse.data[0].providerId - ); // set to first provider id - } + const response = await fetch( + `/api/v2/episode/${info.id}?releasing=${ + info.status === "RELEASING" ? "true" : "false" + }${isDub ? "&dub=true" : ""}` + ).then((res) => res.json()); + const getMap = response.find((i) => i?.map === true); + let allProvider = response; - setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); - setProviders(firstResponse.data); - setLoading(false); - } catch (error) { - setLoading(false); - setProviders([]); + if (getMap) { + allProvider = response.filter((i) => { + if (i?.providerId === "gogoanime" && i?.map !== true) { + return null; + } + return i; + }); + setMapProviders(getMap?.episodes); } + + if (allProvider.length > 0) { + const defaultProvider = allProvider.find( + (x) => x.providerId === "gogoanime" || x.providerId === "9anime" + ); + setProviderId(defaultProvider?.providerId || allProvider[0].providerId); // set to first provider id + } + + setView(Number(localStorage.getItem("view")) || 3); + setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings"))); + setProviders(allProvider); + setLoading(false); }; fetchData(); + + return () => { + setProviders(null); + setMapProviders(null); + }; }, [info.id, isDub]); const episodes = - providers?.find((provider) => provider.providerId === providerId) - ?.episodes || []; + providers + ?.find((provider) => provider.providerId === providerId) + ?.episodes?.slice(0, mapProviders?.length) || []; const lastEpisodeIndex = currentPage * itemsPerPage; const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage; - const currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + let currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex); + if (isDub) { + currentEpisodes = currentEpisodes.filter((i) => i.hasDub === true); + } const totalPages = Math.ceil(episodes.length / itemsPerPage); const handleChange = (event) => { @@ -66,36 +89,90 @@ export default function AnimeEpisode({ info, progress }) { }; useEffect(() => { - if (episodes?.some((item) => item?.title === null)) { + if ( + !mapProviders || + mapProviders?.every( + (item) => + item?.image?.includes("https://s4.anilist.co/") || + item?.image === null + ) + ) { setView(3); } }, [providerId, episodes]); + useEffect(() => { + if (episodes) { + const getEpi = info?.nextAiringEpisode + ? episodes.find((i) => i.number === progress + 1) + : episodes[0]; + if (getEpi) { + const watchUrl = `/en/anime/watch/${ + info.id + }/${providerId}?id=${encodeURIComponent(getEpi.id)}&num=${ + getEpi.number + }${isDub ? `&dub=${isDub}` : ""}`; + setWatch(watchUrl); + } else { + setWatch(null); + } + } + }, [episodes]); + + useEffect(() => { + if (artStorage) { + // console.log({ artStorage }); + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + // Create a new object to store the updated data + const updatedData = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (Number(item.aniId) === info.id && item.provider === providerId) { + updatedData[key] = item; + } + } + + if (!session?.user?.name) { + setProgress( + Object.keys(updatedData).length > 0 + ? Math.max( + ...Object.keys(updatedData).map( + (key) => updatedData[key].episode + ) + ) + : 0 + ); + } else { + return; + } + } + }, [providerId, artStorage, info.id, session?.user?.name]); + return ( <> -
+
-
+
{info && (

Episodes

)} - {info?.nextAiringEpisode && ( -
-
-

Next :

-
- {convertSecondsToTime( - info.nextAiringEpisode.timeUntilAiring - )} -
-
-
- -
-
+ {info.nextAiringEpisode?.timeUntilAiring && ( +

+ Ep {info.nextAiringEpisode.episode}{" "} + {">>"}{" "} + + {convertSecondsToTime( + info.nextAiringEpisode.timeUntilAiring + )}{" "} + +

)}
@@ -165,9 +242,6 @@ export default function AnimeEpisode({ info, progress }) { ))} - {/* - Select Providers - */}
@@ -197,6 +271,7 @@ export default function AnimeEpisode({ info, progress }) { view={view} setView={setView} episode={currentEpisodes} + map={mapProviders} />
@@ -204,15 +279,21 @@ export default function AnimeEpisode({ info, progress }) { {/* Episodes */} {!loading ? (
{Array.isArray(providers) ? ( providers.length > 0 ? ( currentEpisodes.map((episode, index) => { + const mapData = mapProviders?.find( + (i) => i.number === episode.number + ); + return ( {view === 1 && ( @@ -220,17 +301,20 @@ export default function AnimeEpisode({ info, progress }) { key={index} index={index} info={info} + image={mapData?.image} providerId={providerId} episode={episode} artStorage={artStorage} progress={progress} dub={isDub} - // image={thumbnail} /> )} {view === 2 && (
{rel.id}
- {rel.title.userPreferred || rel.title.romaji} + {rel.title.userPreferred}
{rel.type}
diff --git a/components/anime/mobile/reused/description.js b/components/anime/mobile/reused/description.js new file mode 100644 index 0000000..99973d3 --- /dev/null +++ b/components/anime/mobile/reused/description.js @@ -0,0 +1,44 @@ +export default function Description({ + info, + readMore, + setReadMore, + className, +}) { + return ( +
+
]*>/g, "").length > 240 + ? "" + : "pointer-events-none" + } ${ + readMore ? "hidden" : "" + } absolute z-30 flex items-end justify-center top-0 w-full h-full transition-all duration-200 ease-linear md:opacity-0 md:hover:opacity-100 bg-gradient-to-b from-transparent to-primary to-95%`} + > + +
+

]*>/g, ""), + }} + /> +

+ ); +} diff --git a/components/anime/mobile/reused/infoChip.js b/components/anime/mobile/reused/infoChip.js new file mode 100644 index 0000000..41def85 --- /dev/null +++ b/components/anime/mobile/reused/infoChip.js @@ -0,0 +1,43 @@ +import React from "react"; +import { getFormat } from "../../../../utils/getFormat"; + +export default function InfoChip({ info, color, className }) { + return ( +
+ {info?.episodes && ( +
+ {info?.episodes} Episodes +
+ )} + {info?.averageScore && ( +
+ {info?.averageScore}% +
+ )} + {info?.format && ( +
+ {getFormat(info?.format)} +
+ )} + {info?.status && ( +
+ {info?.status} +
+ )} +
+ ); +} diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js index e9c9c7d..25d387f 100644 --- a/components/anime/mobile/topSection.js +++ b/components/anime/mobile/topSection.js @@ -1,81 +1,459 @@ -import { HeartIcon } from "@heroicons/react/20/solid"; +import { + ArrowUpCircleIcon, + MagnifyingGlassIcon, +} from "@heroicons/react/24/solid"; import { - TvIcon, - ArrowTrendingUpIcon, - RectangleStackIcon, -} from "@heroicons/react/24/outline"; + ArrowLeftIcon, + PlayIcon, + PlusIcon, + ShareIcon, + UserIcon, +} from "@heroicons/react/24/solid"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useSearch } from "../../../lib/hooks/isOpenState"; +import { useEffect, useState } from "react"; +import { convertSecondsToTime } from "../../../utils/getTimes"; +import Link from "next/link"; +import { signIn } from "next-auth/react"; +import InfoChip from "./reused/infoChip"; +import Description from "./reused/description"; + +const getScrollPosition = (el = window) => ({ + x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, + y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop, +}); + +export function NewNavbar({ info, session, scrollP = 200, toTop = false }) { + const router = useRouter(); + const [scrollPosition, setScrollPosition] = useState(); + const { isOpen, setIsOpen } = useSearch(); + + useEffect(() => { + const handleScroll = () => { + setScrollPosition(getScrollPosition()); + }; -export default function DetailTop({ info, statuses, handleOpen, loading }) { + // Add a scroll event listener when the component mounts + window.addEventListener("scroll", handleScroll); + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); return ( -
-
-

- {info?.title?.romaji || info?.title?.english} -

-

-

- {info?.genres - ?.slice(0, info?.genres?.length > 3 ? info?.genres?.length : 3) - .map((item, index) => ( - + + {toTop && ( + + )} + + ); +} + +export default function DetailTop({ + info, + session, + statuses, + handleOpen, + watchUrl, + progress, + color, +}) { + const router = useRouter(); + const [readMore, setReadMore] = useState(false); + + const [showAll, setShowAll] = useState(false); + + useEffect(() => { + setReadMore(false); + }, [info.id]); + + const handleShareClick = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: `Watch Now - ${info?.title?.english}`, + // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, + url: window.location.href, + }); + } else { + // Web Share API is not supported, provide a fallback or show a message + alert("Web Share API is not supported in this browser."); + } + } catch (error) { + console.error("Error sharing:", error); + } + }; + + return ( +
+ + + {/* MAIN */} +
+
+ coverImage +
+
+
+

+ {info?.season?.toLowerCase()} {info.seasonYear} +

+

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

+

+ {info.title?.english} +

+ + {info?.description && ( + + )} +
+
-
-
- {info && info.status !== "NOT_YET_RELEASED" ? ( - <> -
- -

{info?.type}

-
-
- -

{info?.averageScore ? `${info?.averageScore}%` : "N/A"}

-
-
- - {info?.episodes ? ( -

{info?.episodes} Episodes

- ) : ( -

N/A

- )} -
- + +
+ +
+ + + + + See on AniList + + anilist_icon +
+ +
+ + + +
+ + {info.nextAiringEpisode?.timeUntilAiring && ( +

+ Episode {info.nextAiringEpisode.episode} in{" "} + + {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "} + +

+ )} + + {info?.description && ( + + )} + + + + {info?.relations?.edges?.length > 0 && ( +
+
+ {info?.relations?.edges?.length > 0 && ( +
+ Relations +
+ )} + {info?.relations?.edges?.length > 3 && ( +
setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} +
+ )} +
+
+ {info?.relations?.edges + .slice(0, showAll ? info?.relations?.edges.length : 3) + .map((r, index) => { + const rel = r.node; + return ( + +
+
+ {rel.id} +
+
+
+ {r.relationType.replace(/_/g, " ")} +
+
+ {rel.title.userPreferred} +
+
{rel.format}
+
+
+ + ); + })} +
+
+ )}
); } diff --git a/components/anime/viewMode/listMode.js b/components/anime/viewMode/listMode.js index f3bcf05..5beded1 100644 --- a/components/anime/viewMode/listMode.js +++ b/components/anime/viewMode/listMode.js @@ -3,7 +3,6 @@ import Link from "next/link"; export default function ListMode({ info, episode, - index, artStorage, providerId, progress, @@ -15,39 +14,32 @@ export default function ListMode({ if (prog > 90) prog = 100; return ( -
- +
+ + {episode.number} + +

-

Episode {episode.number}

- {episode.title && ( -

- "{episode.title}" -

- )} - - {index !== episode?.length - 1 && } -
+ }`} + > + {episode?.title || `Episode ${episode.number}`} +

+

{providerId}

+
+ ); } diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js index 6efeb77..296e0d2 100644 --- a/components/anime/viewMode/thumbnailDetail.js +++ b/components/anime/viewMode/thumbnailDetail.js @@ -5,6 +5,9 @@ export default function ThumbnailDetail({ index, epi, info, + image, + title, + description, provider, artStorage, progress, @@ -25,13 +28,15 @@ export default function ThumbnailDetail({ >
- Anime Cover + {image && ( + Anime Cover + )}

- {epi?.title} + {title}

- {epi?.description && ( + {description && (

- {epi?.description} + {description}

)}
diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js index 99f02bd..69cd8c3 100644 --- a/components/anime/viewMode/thumbnailOnly.js +++ b/components/anime/viewMode/thumbnailOnly.js @@ -3,6 +3,7 @@ import Link from "next/link"; export default function ThumbnailOnly({ info, + image, providerId, episode, artStorage, @@ -35,25 +36,16 @@ export default function ThumbnailOnly({ : "0%", }} /> -
- epi image + {/*
*/} + {image && ( + epi image + )} ); } diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js index b032fd6..a3d9f4f 100644 --- a/components/anime/watch/primarySide.js +++ b/components/anime/watch/primarySide.js @@ -9,18 +9,14 @@ import Link from "next/link"; import Skeleton from "react-loading-skeleton"; import Modal from "../../modal"; import AniList from "../../media/aniList"; -import axios from "axios"; export default function PrimarySide({ info, session, epiNumber, - setLoading, navigation, - loading, providerId, watchId, - status, onList, proxy, disqus, @@ -33,15 +29,31 @@ export default function PrimarySide({ const [open, setOpen] = useState(false); const [skip, setSkip] = useState(); + const [loading, setLoading] = useState(true); + const router = useRouter(); useEffect(() => { setLoading(true); async function fetchData() { if (info) { - const { data } = await axios.get( - `/api/consumet/source/${providerId}/${watchId}` - ); + const anify = await fetch("/api/v2/source", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + source: + providerId === "gogoanime" && !watchId.startsWith("/") + ? "consumet" + : "anify", + providerId: providerId, + watchId: watchId, + episode: epiNumber, + id: info.id, + sub: dub ? "dub" : "sub", + }), + }).then((res) => res.json()); const skip = await fetch( `https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt( @@ -65,10 +77,9 @@ export default function PrimarySide({ setSkip({ op, ed }); - setEpisodeData(data); + setEpisodeData(anify); setLoading(false); } - // setMal(malId); } fetchData(); @@ -134,7 +145,7 @@ export default function PrimarySide({
{!loading ? ( - episodeData && ( + navigation && episodeData?.sources?.length !== 0 ? ( + ) : ( +

+ Video is not available, please try other providers +

) ) : ( -
+
+
+
+
+
+
+
+
)}
diff --git a/components/anime/watch/secondarySide.js b/components/anime/watch/secondarySide.js index 5d9b8f9..c9ef684 100644 --- a/components/anime/watch/secondarySide.js +++ b/components/anime/watch/secondarySide.js @@ -4,24 +4,27 @@ import Link from "next/link"; export default function SecondarySide({ info, + map, providerId, watchId, episode, - progress, artStorage, dub, }) { + const progress = info.mediaListEntry?.progress; return (

Up Next

{episode && episode.length > 0 ? ( - episode.some((item) => item.title && item.description) > 0 ? ( + map?.some((item) => item.title && item.description) > 0 ? ( episode.map((item) => { const time = artStorage?.[item.id]?.timeWatched; const duration = artStorage?.[item.id]?.duration; let prog = (time / duration) * 100; if (prog > 90) prog = 100; + + const mapData = map?.find((i) => i.number === item.number); return (
+ {/* {mapData?.image && ( */} Anime Cover + {/* )} */} - Episode {item.number} + Episode {item?.number} {item.id == watchId && (
@@ -78,15 +83,15 @@ export default function SecondarySide({

- {item.title} + {mapData?.title}

- {item?.description} + {mapData?.description}

diff --git a/components/footer.js b/components/footer.js index d658172..ca5a21f 100644 --- a/components/footer.js +++ b/components/footer.js @@ -1,13 +1,11 @@ import Link from "next/link"; -import { signIn, useSession } from "next-auth/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 [year] = useState(new Date().getFullYear()); + const [season] = useState(getCurrentSeason()); const [lang, setLang] = useState("en"); const [checked, setChecked] = useState(false); @@ -41,118 +39,160 @@ function Footer() { }); 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. -

-
- - -
+
+
+
+
+ {/*
*/} + {/* Website Logo */} +

moopa

+

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

+ {/*
*/}
- {/*
- gambar -
*/} -
-
-
-
    -
  • - - This Season - -
  • -
  • - Popular Anime -
  • -
  • - Popular Manga -
  • - {status === "loading" ? ( -

    Loading...

    - ) : session ? ( +
    +
    +
    • - - My List + + This Season
    • - ) : ( -
    • - +
    • + Popular Anime +
    • +
    • + Popular Manga +
    • +
    • + Donate +
    • +
    +
      +
    • + + Movies +
    • - )} -
    -
      -
    • - Movies -
    • -
    • - TV Shows -
    • -
    • - DMCA -
    • -
    • - - Github - -
    • -
    +
  • + TV Shows +
  • +
  • + DMCA +
  • +
  • + + Github + +
  • +
+
+
+
+
+
+
+

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

+
+ {/* Github Icon */} + + + + + + + + + + + + + + {/* Discord Icon */} + + + + + + + {/* Kofi */} + + + + + + +
-
+ ); } diff --git a/components/home/content.js b/components/home/content.js index 70f0e3f..e18e5d8 100644 --- a/components/home/content.js +++ b/components/home/content.js @@ -1,5 +1,6 @@ import Link from "next/link"; -import React, { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect, Fragment } from "react"; +import { useDraggable } from "react-use-draggable-scroll"; import Image from "next/image"; import { MdChevronRight } from "react-icons/md"; import { @@ -14,6 +15,7 @@ import { ChevronLeftIcon } from "@heroicons/react/20/solid"; import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; import { useRouter } from "next/router"; import { toast } from "react-toastify"; +import HistoryOptions from "./content/historyOptions"; export default function Content({ ids, @@ -26,11 +28,10 @@ export default function Content({ }) { const router = useRouter(); - const [startX, setStartX] = useState(null); - const containerRef = useRef(null); + const ref = useRef(); + const { events } = useDraggable(ref); const [cookie, setCookie] = useState(null); - const [isDragging, setIsDragging] = useState(false); const [clicked, setClicked] = useState(false); const [lang, setLang] = useState("en"); @@ -55,39 +56,20 @@ export default function Content({ } }, []); - 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 = () => { + ref.current.classList.add("scroll-smooth"); var slider = document.getElementById(ids); slider.scrollLeft = slider.scrollLeft - 500; + ref.current.classList.remove("scroll-smooth"); }; const slideRight = () => { + ref.current.classList.add("scroll-smooth"); var slider = document.getElementById(ids); slider.scrollLeft = slider.scrollLeft + 500; + ref.current.classList.remove("scroll-smooth"); }; const handleScroll = (e) => { @@ -128,6 +110,9 @@ export default function Content({ if (section === "Recently Watched") { router.push(`/${lang}/anime/recently-watched`); } + if (section === "New Episodes") { + router.push(`/${lang}/anime/recent`); + } if (section === "Trending Now") { router.push(`/${lang}/anime/trending`); } @@ -142,7 +127,7 @@ export default function Content({ } }; - const removeItem = async (id) => { + const removeItem = async (id, aniId) => { if (userName) { // remove from database const res = await fetch(`/api/user/update/episode`, { @@ -152,24 +137,42 @@ export default function Content({ }, body: JSON.stringify({ name: userName, - id: id, + id, + aniId, }), }); const data = await res.json(); - // remove from local storage - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + const updatedData = {}; + + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); } // update client - setRemoved(id); + setRemoved(id || aniId); if (data?.message === "Episode deleted") { toast.success("Episode removed from history", { @@ -182,17 +185,38 @@ export default function Content({ }); } } else { - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + setRemoved(id); + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings")) || {}; + + // Create a new object to store the updated data + const updatedData = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + // Update localStorage with the filtered data + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + setRemoved(aniId); } - - setRemoved(id); } }; @@ -218,13 +242,10 @@ export default function Content({
{ids !== "recentlyWatched" ? slicedData?.map((anime) => { @@ -241,14 +262,14 @@ export default function Content({ title={anime.title.romaji} > {ids === "onGoing" && ( -
+

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

{checkProgress(progress) && !clicked?.hasOwnProperty(anime.id) && ( - + )} {checkProgress(progress) && (
)} - { +
+ {ids === "recentAdded" && ( +
+ )} + { +
+ {ids === "recentAdded" && ( + + episode-bade +

+ Episode{" "} + + {anime?.episodeNumber} + +

+
+ )} {ids !== "onGoing" && (

- {anime.status === "RELEASING" ? ( + {anime.status === "RELEASING" || + ids === "recentAdded" ? ( ) : anime.status === "NOT_YET_RELEASED" ? ( @@ -333,22 +377,50 @@ export default function Content({ key={i.watchId} className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item" > -
-
+ {/*
+ */} + + {i?.nextId && ( + + )}
@@ -372,8 +444,8 @@ export default function Content({ {i?.image && ( Episode Thumbnail @@ -411,7 +483,7 @@ export default function Content({ section !== "Recommendations" && (
diff --git a/components/home/content/historyOptions.js b/components/home/content/historyOptions.js new file mode 100644 index 0000000..1b9c5ed --- /dev/null +++ b/components/home/content/historyOptions.js @@ -0,0 +1,56 @@ +import { Menu, Transition } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import React, { Fragment } from "react"; + +export default function HistoryOptions({ remove, watchId, aniId }) { + return ( + +
+ + + + Remove from history + + +
+ + +
+ + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + +
+
+
+
+ ); +} diff --git a/components/home/genres.js b/components/home/genres.js index 3eefecd..f054fc9 100644 --- a/components/home/genres.js +++ b/components/home/genres.js @@ -55,7 +55,7 @@ export default function Genres() {
-
+
{g.map((a, index) => ( @@ -80,7 +80,7 @@ export default function Genres() { ))}
-
+
); diff --git a/components/home/mobileNav.js b/components/home/mobileNav.js deleted file mode 100644 index 52c9d52..0000000 --- a/components/home/mobileNav.js +++ /dev/null @@ -1,202 +0,0 @@ -import { signIn, signOut } from "next-auth/react"; -import Link from "next/link"; -import { useState } from "react"; - -export default function MobileNav({ sessions }) { - const [isVisible, setIsVisible] = useState(false); - - const handleShowClick = () => { - setIsVisible(true); - }; - - const handleHideClick = () => { - setIsVisible(false); - }; - return ( - <> - {/* NAVBAR */} -
- {!isVisible && ( - - )} -
- - {/* Mobile Menu */} -
- {isVisible && sessions && ( - - user avatar - - )} - {isVisible && ( -
-
- - - - {sessions ? ( - - ) : ( - - )} -
- -
- )} -
- - ); -} diff --git a/components/home/recommendation.js b/components/home/recommendation.js new file mode 100644 index 0000000..842932c --- /dev/null +++ b/components/home/recommendation.js @@ -0,0 +1,91 @@ +import Image from "next/image"; +// import data from "../../assets/dummyData.json"; +import { BookOpenIcon, PlayIcon } from "@heroicons/react/24/solid"; +import { useDraggable } from "react-use-draggable-scroll"; +import { useRef } from "react"; +import Link from "next/link"; + +export default function UserRecommendation({ data }) { + const ref = useRef(null); + const { events } = useDraggable(ref); + + const uniqueRecommendationIds = new Set(); + + // Filter out duplicates from the recommendations array + const filteredData = data.filter((recommendation) => { + // Check if the ID is already in the set + if (uniqueRecommendationIds.has(recommendation.id)) { + // If it's a duplicate, return false to exclude it from the filtered array + return false; + } + + // If it's not a duplicate, add the ID to the set and return true + uniqueRecommendationIds.add(recommendation.id); + return true; + }); + + return ( +
+
+
+

+ {data[0].title.userPreferred} +

+

]*>/g, ""), + }} + className="font-roboto font-light line-clamp-3 lg:line-clamp-3" + /> + +

+
+ {filteredData.slice(0, 9).map((i) => ( + + {i.title.userPreferred} + +
{i.title.userPreferred}
+
a
+
+ + ))} +
+
+
+ {data[0]?.bannerImage && ( + {data[0].title.userPreferred} + )} +
+ ); +} diff --git a/components/home/schedule.js b/components/home/schedule.js index 4043c5e..a9846a7 100644 --- a/components/home/schedule.js +++ b/components/home/schedule.js @@ -1,17 +1,23 @@ import Image from "next/image"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, 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"; +import { useCountdown } from "../../utils/useCountdownSeconds"; -export default function Schedule({ data, scheduleData, time }) { +export default function Schedule({ data, scheduleData, anime, update }) { let now = new Date(); let currentDay = now.toLocaleString("default", { weekday: "long" }).toLowerCase() + "Schedule"; currentDay = currentDay.replace("Schedule", ""); + const [day, hours, minutes, seconds] = useCountdown( + anime[0]?.airingSchedule.nodes[0]?.airingAt * 1000 || Date.now(), + update + ); + const [currentPage, setCurrentPage] = useState(0); const [days, setDay] = useState(); @@ -37,8 +43,6 @@ export default function Schedule({ data, scheduleData, time }) { setCurrentPage(todayIndex >= 0 ? todayIndex : 0); }, [currentDay, days]); - // console.log({ scheduleData }); - return (

@@ -46,7 +50,7 @@ export default function Schedule({ data, scheduleData, time }) {

-
+

Coming Up Next!

{data.bannerImage ? ( banner next anime ) : ( {/* Countdown Timer */}
- {time.days} + {day} Days
- {time.hours} + {hours} Hours
- {time.minutes} + {minutes} Mins
- {time.seconds} + {seconds} Secs
diff --git a/components/home/staticNav.js b/components/home/staticNav.js index 93f7b26..b22a9e3 100644 --- a/components/home/staticNav.js +++ b/components/home/staticNav.js @@ -1,51 +1,27 @@ -import { signIn, useSession } from "next-auth/react"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { signIn, signOut, useSession } from "next-auth/react"; import { getCurrentSeason } from "../../utils/getTimes"; import Link from "next/link"; -import { parseCookies } from "nookies"; +// import { } from "@heroicons/react/24/solid"; +import { useSearch } from "../../lib/hooks/isOpenState"; +import Image from "next/image"; +import { UserIcon } from "@heroicons/react/20/solid"; +import { useRouter } from "next/router"; 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 year = new Date().getFullYear(); + const season = getCurrentSeason(); 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 { setIsOpen } = useSearch(); - 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 */}
-
-
+
+
  • This Season
  • - Manga + + Manga +
  • - Anime + + Anime + +
  • +
  • + + Schedule +
  • {status === "loading" ? ( @@ -75,15 +70,19 @@ export default function Navigasi() {
  • )} {sessions && (
  • - + My List
  • @@ -92,18 +91,73 @@ export default function Navigasi() { )}
    -
    -
    - -
    - -
    -
    +
    + + {/*
    */} + {sessions ? ( + + ) : ( + + )} + {/*
    */}
    diff --git a/components/id-components/player/Artplayer.js b/components/id-components/player/Artplayer.js deleted file mode 100644 index e209433..0000000 --- a/components/id-components/player/Artplayer.js +++ /dev/null @@ -1,59 +0,0 @@ -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 deleted file mode 100644 index 1168313..0000000 --- a/components/id-components/player/VideoPlayerId.js +++ /dev/null @@ -1,181 +0,0 @@ -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/id/player/Artplayer.js b/components/id/player/Artplayer.js new file mode 100644 index 0000000..e209433 --- /dev/null +++ b/components/id/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/player/VideoPlayerId.js b/components/id/player/VideoPlayerId.js new file mode 100644 index 0000000..1168313 --- /dev/null +++ b/components/id/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/navbar.js b/components/navbar.js index e148b09..7edd6c1 100644 --- a/components/navbar.js +++ b/components/navbar.js @@ -3,6 +3,7 @@ import Link from "next/link"; import { useSession, signIn, signOut } from "next-auth/react"; import Image from "next/image"; import { parseCookies } from "nookies"; +import MobileNav from "./shared/MobileNav"; function Navbar(props) { const { data: session, status } = useSession(); @@ -45,193 +46,7 @@ function Navbar(props) { moopa
    - {/* Mobile Hamburger */} - {!isVisible && ( - - )} - - {/* Mobile Menu */} -
    - {isVisible && session && ( - - user avatar - - )} - {isVisible && ( -
    -
    - - - - {session ? ( - - ) : ( - - )} -
    - -
    - )} -
    +