aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-09-25 00:44:40 +0700
committerGitHub <[email protected]>2023-09-25 00:44:40 +0700
commit1a85c2571690ba592ac5183d5eadaf9846fe532b (patch)
tree3f3552c00cd49c0eeab5275275cf5cf5666e5027
parentDelete .github/workflows/deploy.yml (diff)
downloadmoopa-4.1.0.tar.xz
moopa-4.1.0.zip
Update v4.1.0 (#79)v4.1.0
* Update v4.1.0 * Update pages/_app.js
-rw-r--r--.env.example9
-rw-r--r--README.md9
-rw-r--r--components/anime/charactersCard.js160
-rw-r--r--components/anime/episode.js114
-rw-r--r--components/anime/infoDetails.js204
-rw-r--r--components/anime/mobile/topSection.js179
-rw-r--r--components/anime/viewMode/thumbnailDetail.js10
-rw-r--r--components/anime/viewMode/thumbnailOnly.js4
-rw-r--r--components/anime/viewSelector.js (renamed from components/anime/changeView.js)6
-rw-r--r--components/anime/watch/primarySide.js276
-rw-r--r--components/home/content.js19
-rw-r--r--components/home/staticNav.js168
-rw-r--r--components/id/player/Artplayer.js59
-rw-r--r--components/id/player/VideoPlayerId.js181
-rw-r--r--components/layout.js63
-rw-r--r--components/manga/info/topSection.js1
-rw-r--r--components/modal.js2
-rw-r--r--components/navbar.js128
-rw-r--r--components/shared/MobileNav.js8
-rw-r--r--components/shared/NavBar.js265
-rw-r--r--components/shared/bugReport.js200
-rw-r--r--components/shared/footer.js (renamed from components/footer.js)12
-rw-r--r--components/videoPlayer.js412
-rw-r--r--components/watch/player/artplayer.js325
-rw-r--r--components/watch/player/component/controls/quality.js15
-rw-r--r--components/watch/player/component/controls/subtitle.js3
-rw-r--r--components/watch/player/component/overlay.js57
-rw-r--r--components/watch/player/playerComponent.js478
-rw-r--r--components/watch/player/utils/getZoroSource.js0
-rw-r--r--components/watch/primary/details.js (renamed from components/anime/watch/primary/details.js)28
-rw-r--r--components/watch/secondary/episodeLists.js (renamed from components/anime/watch/secondarySide.js)27
-rw-r--r--jsconfig.json11
-rw-r--r--lib/anify/info.js2
-rw-r--r--lib/anify/page.js2
-rw-r--r--lib/anilist/getMedia.js43
-rw-r--r--lib/hooks/watchPageProvider.js41
-rw-r--r--lib/redis.js25
-rw-r--r--next.config.js21
-rw-r--r--package-lock.json10
-rw-r--r--package.json5
-rw-r--r--pages/404.js4
-rw-r--r--pages/_app.js110
-rw-r--r--pages/admin/index.js263
-rw-r--r--pages/api/user/profile.js103
-rw-r--r--pages/api/user/update/episode.js2
-rw-r--r--pages/api/v2/admin/broadcast/index.js40
-rw-r--r--pages/api/v2/admin/bug-report/index.js49
-rw-r--r--pages/api/v2/admin/meta/index.js47
-rw-r--r--pages/api/v2/episode/[id].js121
-rw-r--r--pages/api/v2/etc/recent/[page].js12
-rw-r--r--pages/api/v2/etc/schedule/index.js14
-rw-r--r--pages/api/v2/info/[id].js10
-rw-r--r--pages/api/v2/source/index.js12
-rw-r--r--pages/en/about.js83
-rw-r--r--pages/en/anime/[...id].js29
-rw-r--r--pages/en/anime/popular.js4
-rw-r--r--pages/en/anime/recent.js4
-rw-r--r--pages/en/anime/recently-watched.js13
-rw-r--r--pages/en/anime/trending.js4
-rw-r--r--pages/en/anime/watch/[...info].js598
-rw-r--r--pages/en/contact.js9
-rw-r--r--pages/en/dmca.js16
-rw-r--r--pages/en/index.js111
-rw-r--r--pages/en/manga/[id].js14
-rw-r--r--pages/en/manga/read/[...params].js22
-rw-r--r--pages/en/profile/[user].js9
-rw-r--r--pages/en/schedule/index.js65
-rw-r--r--pages/en/search/[...param].js31
-rw-r--r--pages/id/index.js8
-rw-r--r--release.md22
-rw-r--r--styles/globals.css10
-rw-r--r--utils/appendMetaToEpisodes.js28
-rw-r--r--utils/combineImages.js26
-rw-r--r--utils/getTimes.js26
74 files changed, 3147 insertions, 2354 deletions
diff --git a/.env.example b/.env.example
index d04cf15..1c8825b 100644
--- a/.env.example
+++ b/.env.example
@@ -8,14 +8,15 @@ NEXTAUTH_SECRET='run this cmd in your bash terminal (openssl rand -base64 32) wi
NEXTAUTH_URL="for development use http://localhost:3000/ and for production use your domain url"
## NextJS
-PROXY_URI="I recommend you to use this cors-anywhere as a proxy https://github.com/Rob--W/cors-anywhere follow the instruction on how to use it there. Skip this if you only use gogoanime as a source"
+PROXY_URI="This is what I use for proxying video https://github.com/chaycee/M3U8Proxy. Don't put / at the end of the url."
API_URI="host your own API from this repo https://github.com/consumet/api.consumet.org. Don't put / at the end of the url."
-API_KEY="this API key is used for schedules and manga page. get the key from https://anify.tv/discord"
-DISQUS_SHORTNAME='put your disqus shortname here. (optional)'
+API_KEY="this API key is used for schedules, anime and manga page. get the key from https://anify.tv/discord"
+DISQUS_SHORTNAME='put your disqus shortname here (optional)'
+# ADMIN_USERNAME=""
## Prisma
DATABASE_URL="Your postgresql connection url"
## Redis
-# If you don't want to use redis, just comment the REDIS_URL
+# If you don't want to use redis, just comment the REDIS_URL (press ctrl + / on windows or cmd + / on mac)
REDIS_URL="rediss://username:password@host:port" \ No newline at end of file
diff --git a/README.md b/README.md
index 24c6a6d..17dda87 100644
--- a/README.md
+++ b/README.md
@@ -114,16 +114,17 @@ NEXTAUTH_SECRET='run this cmd in your bash terminal (openssl rand -base64 32) wi
NEXTAUTH_URL="for development use http://localhost:3000/ and for production use your domain url"
## NextJS
-PROXY_URI="use this cors proxy https://github.com/Gratenes/m3u8CloudflareWorkerProxy, follow the instruction there on how to deploy it."
+PROXY_URI="This is what I use for proxying video https://github.com/chaycee/M3U8Proxy. Don't put / at the end of the url."
API_URI="host your own API from this repo https://github.com/consumet/api.consumet.org. Don't put / at the end of the url."
-API_KEY="this API key is used for schedules and manga page. get the key from https://anify.tv/discord"
-DISQUS_SHORTNAME='put your disqus shortname here. (optional)'
+API_KEY="this API key is used for schedules, anime and manga page. get the key from https://anify.tv/discord"
+DISQUS_SHORTNAME='put your disqus shortname here (optional)'
+# ADMIN_USERNAME=""
## Prisma
DATABASE_URL="Your postgresql connection url"
## Redis
-# If you don't want to use redis, just comment the REDIS_URL
+# If you don't want to use redis, just comment the REDIS_URL (press ctrl + / on windows or cmd + / on mac)
REDIS_URL="rediss://username:password@host:port"
```
diff --git a/components/anime/charactersCard.js b/components/anime/charactersCard.js
index abff2ba..6c9197a 100644
--- a/components/anime/charactersCard.js
+++ b/components/anime/charactersCard.js
@@ -3,79 +3,91 @@ import Image from "next/image";
import { useState } from "react";
export default function Characters({ info }) {
+ const [showAll, setShowAll] = useState(false);
- const [showAll, setShowAll] = useState(false);
-
- return (
- <div>
- <div className="flex items-center justify-between lg:gap-3 px-5 z-40 ">
- <h1 className="font-karla text-[20px] font-bold">Characters</h1>
- {info?.length > 6 && (
- <div className="cursor-pointer font-karla" onClick={() => setShowAll(!showAll)}>
- {showAll ? "show less" : "show more"}
- </div>
- )}
- </div>
- {/* for bigger device */}
- <div className="hidden md:grid w-full grid-cols-1 gap-[10px] md:gap-4 md:grid-cols-3 md:pt-7 md:pb-5 px-3 md:px-5 pt-4">
- {info.slice(0, showAll ? info.length : 6).map((item, index) => {
- return <a key={index} className="md:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full cursor-default">
- <div className="text-gray-300 space-x-4 col-span-1 flex w-full h-24 bg-secondary rounded-md overflow-hidden">
- <div className="relative h-full w-20">
- <Image
- draggable={false}
- src={
- item.node.image.large ||
- item.node.image.medium
- }
- width={500}
- height={300}
- alt={
- item.node.name.userPreferred ||
- item.node.name.full ||
- "Character Image"
- }
- className="h-full object-cover"
- />
- </div>
- <div className="py-2 flex flex-col justify-between">
- <p className="font-semibold">{item.node.name.full || item.node.name.userPreferred}</p>
- <p>{item.role}</p>
- </div>
- </div>
- </a>
- })}
- </div>
- {/* for smaller devices */}
- <div className="flex md:hidden h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide gap-4 pt-8 pb-4 px-5 z-30">
- {info.slice(0, showAll ? info.length : 6).map((item, index) => {
- return <div key={index} className="flex flex-col gap-3 shrink-0 cursor-pointer">
- <a className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative">
- <div className="h-[190px] w-[135px] rounded-md z-30">
- <Image
- draggable={false}
- src={
- item.node.image.large ||
- item.node.image.medium
- }
- alt={
- item.node.name.userPreferred ||
- item.node.name.full ||
- "Character Image"
- }
- width={500}
- height={300}
- className="z-20 h-[190px] w-[135px] object-cover rounded-md brightness-90"
- />
- </div>
- </a>
- <a className="w-[135px] lg:w-[185px] line-clamp-2">
- <h1 className="font-karla font-semibold text-[15px]">{item.node.name.full || item.node.name.userPreferred}</h1>
- <h1 className="font-karla float-right italic text-[12px]">~{item.role}</h1>
- </a>
- </div>
- })}
+ return (
+ <div>
+ <div className="flex items-center justify-between lg:gap-3 px-5 z-40 ">
+ <h1 className="font-karla text-[20px] font-bold">Characters</h1>
+ {info?.length > 6 && (
+ <div
+ className="cursor-pointer font-karla"
+ onClick={() => setShowAll(!showAll)}
+ >
+ {showAll ? "show less" : "show more"}
+ </div>
+ )}
+ </div>
+ {/* for bigger device */}
+ <div className="hidden md:grid w-full grid-cols-1 gap-[10px] md:gap-4 md:grid-cols-3 md:pt-7 md:pb-5 px-3 md:px-5 pt-4">
+ {info.slice(0, showAll ? info.length : 6).map((item, index) => {
+ return (
+ <a
+ key={index}
+ className="md:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full cursor-default"
+ >
+ <div className="text-gray-300 space-x-4 col-span-1 flex w-full h-24 bg-secondary rounded-md overflow-hidden">
+ <div className="relative h-full w-20">
+ <Image
+ draggable={false}
+ src={item.node.image.large || item.node.image.medium}
+ width={500}
+ height={300}
+ alt={
+ item.node.name.userPreferred ||
+ item.node.name.full ||
+ "Character Image"
+ }
+ className="h-full object-cover"
+ />
+ </div>
+ <div className="py-2 flex flex-col justify-between">
+ <p className="font-semibold">
+ {item.node.name.full || item.node.name.userPreferred}
+ </p>
+ <p>{item.role}</p>
+ </div>
+ </div>
+ </a>
+ );
+ })}
+ </div>
+ {/* for smaller devices */}
+ <div className="flex md:hidden h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide gap-4 pt-8 pb-4 px-5 z-30">
+ {info.slice(0, showAll ? info.length : 6).map((item, index) => {
+ return (
+ <div
+ key={index}
+ className="flex flex-col gap-3 shrink-0 cursor-pointer"
+ >
+ <a className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative">
+ <div className="h-[190px] w-[135px] rounded-md z-30">
+ <Image
+ draggable={false}
+ src={item.node.image.large || item.node.image.medium}
+ alt={
+ item.node.name.userPreferred ||
+ item.node.name.full ||
+ "Character Image"
+ }
+ width={500}
+ height={300}
+ className="z-20 h-[190px] w-[135px] object-cover rounded-md brightness-90"
+ />
+ </div>
+ </a>
+ <a className="w-[135px] lg:w-[185px] line-clamp-2">
+ <h1 className="font-karla font-semibold text-[15px]">
+ {item.node.name.full || item.node.name.userPreferred}
+ </h1>
+ <h1 className="font-karla float-right italic text-[12px]">
+ ~{item.role}
+ </h1>
+ </a>
</div>
- </div>
- );
-} \ No newline at end of file
+ );
+ })}
+ </div>
+ </div>
+ );
+}
diff --git a/components/anime/episode.js b/components/anime/episode.js
index b2f4bd7..e6420a7 100644
--- a/components/anime/episode.js
+++ b/components/anime/episode.js
@@ -1,10 +1,10 @@
import { useEffect, useState, Fragment } from "react";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
-import ChangeView from "./changeView";
+import ViewSelector from "./viewSelector";
import ThumbnailOnly from "./viewMode/thumbnailOnly";
import ThumbnailDetail from "./viewMode/thumbnailDetail";
import ListMode from "./viewMode/listMode";
-import { convertSecondsToTime } from "../../utils/getTimes";
+import { toast } from "react-toastify";
export default function AnimeEpisode({
info,
@@ -93,8 +93,9 @@ export default function AnimeEpisode({
!mapProviders ||
mapProviders?.every(
(item) =>
+ item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
- item?.image === null
+ item?.img === null
)
) {
setView(3);
@@ -152,27 +153,106 @@ export default function AnimeEpisode({
}
}, [providerId, artStorage, info.id, session?.user?.name]);
+ let debounceTimeout;
+
+ const handleRefresh = async () => {
+ try {
+ setLoading(true);
+ clearTimeout(debounceTimeout);
+ debounceTimeout = setTimeout(async () => {
+ const res = await fetch(
+ `/api/v2/episode/${info.id}?releasing=${
+ info.status === "RELEASING" ? "true" : "false"
+ }${isDub ? "&dub=true" : ""}&refresh=true`
+ );
+ if (!res.ok) {
+ console.log(res);
+ toast.error("Something went wrong", {
+ position: "bottom-left",
+ autoClose: 3000,
+ hideProgressBar: true,
+ theme: "colored",
+ });
+ setProviders([]);
+ setLoading(false);
+ } else {
+ const data = await res.json();
+ const getMap = data.find((i) => i?.map === true);
+ let allProvider = data;
+
+ if (getMap) {
+ allProvider = data.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);
+ }
+ }, 1000);
+ } catch (err) {
+ console.log(err);
+ toast.error("Something went wrong", {
+ position: "bottom-left",
+ autoClose: 3000,
+ hideProgressBar: true,
+ theme: "colored",
+ });
+ }
+ };
+
return (
<>
<div className="flex flex-col gap-5 px-3">
<div className="flex lg:flex-row flex-col gap-5 lg:gap-0 justify-between ">
<div className="flex justify-between">
- <div className="flex items-center md:gap-5">
+ <div className="flex items-center gap-4 md:gap-5">
{info && (
<h1 className="text-[20px] lg:text-2xl font-bold font-karla">
Episodes
</h1>
)}
- {info.nextAiringEpisode?.timeUntilAiring && (
- <p className="hidden md:block bg-gray-100 text-gray-900 rounded-md px-2 font-karla font-medium">
- Ep {info.nextAiringEpisode.episode}{" "}
- <span className="animate-pulse">{">>"}</span>{" "}
- <span className="font-bold">
- {convertSecondsToTime(
- info.nextAiringEpisode.timeUntilAiring
- )}{" "}
+ {info?.status !== "NOT_YET_RELEASED" && (
+ <button
+ type="button"
+ onClick={() => {
+ handleRefresh();
+ setProviders(null);
+ setMapProviders(null);
+ }}
+ className="relative flex flex-col items-center w-5 h-5 group"
+ >
+ <span className="absolute pointer-events-none z-40 opacity-0 -translate-y-8 group-hover:-translate-y-10 group-hover:opacity-100 font-karla shadow-tersier shadow-md whitespace-nowrap bg-secondary px-2 py-1 rounded transition-all duration-200 ease-out">
+ Refresh Episodes
</span>
- </p>
+ <svg
+ fill="currentColor"
+ viewBox="0 0 20 20"
+ xmlns="http://www.w3.org/2000/svg"
+ aria-hidden="true"
+ >
+ <path
+ clipRule="evenodd"
+ fillRule="evenodd"
+ d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
+ />
+ </svg>
+ </button>
)}
</div>
@@ -267,7 +347,7 @@ export default function AnimeEpisode({
</>
)}
- <ChangeView
+ <ViewSelector
view={view}
setView={setView}
episode={currentEpisodes}
@@ -301,7 +381,7 @@ export default function AnimeEpisode({
key={index}
index={index}
info={info}
- image={mapData?.image}
+ image={mapData?.img || mapData?.image}
providerId={providerId}
episode={episode}
artStorage={artStorage}
@@ -312,7 +392,7 @@ export default function AnimeEpisode({
{view === 2 && (
<ThumbnailDetail
key={index}
- image={mapData?.image}
+ image={mapData?.img || mapData?.image}
title={mapData?.title}
description={mapData?.description}
index={index}
@@ -346,7 +426,7 @@ export default function AnimeEpisode({
</div>
)
) : (
- <p>{providers.message}</p>
+ <p>{providers?.message}</p>
)}
</div>
) : (
diff --git a/components/anime/infoDetails.js b/components/anime/infoDetails.js
deleted file mode 100644
index 8200bfa..0000000
--- a/components/anime/infoDetails.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import Image from "next/image";
-import Link from "next/link";
-import Skeleton from "react-loading-skeleton";
-
-export default function DesktopDetails({
- info,
- statuses,
- handleOpen,
- loading,
- color,
- setShowAll,
- showAll,
-}) {
- return (
- <>
- <div className="hidden lg:flex gap-8 w-full flex-nowrap">
- <div className="shrink-0 lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] relative">
- {info ? (
- <>
- <div className="bg-image lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] bg-opacity-30 absolute backdrop-blur-lg z-10 -top-7" />
- <Image
- src={info.coverImage.extraLarge || info.coverImage.large}
- priority={true}
- alt="poster anime"
- height={700}
- width={700}
- className="object-cover lg:h-[250px] lg:w-[180px] w-[115px] h-[164px] z-20 absolute rounded-md -top-7"
- />
- <button
- type="button"
- className="bg-action flex-center z-20 h-[20px] w-[180px] absolute bottom-0 rounded-sm font-karla font-bold"
- onClick={() => handleOpen()}
- >
- {!loading
- ? statuses
- ? statuses.name
- : "Add to List"
- : "Loading..."}
- </button>
- </>
- ) : (
- <Skeleton className="h-[250px] w-[180px]" />
- )}
- </div>
-
- <div className="hidden lg:flex w-full flex-col gap-5 h-[250px]">
- <div className="flex flex-col gap-2">
- <h1
- className="title font-inter font-bold text-[36px] text-white line-clamp-1"
- title={info?.title?.romaji || info?.title?.english}
- >
- {info ? (
- info?.title?.romaji || info?.title?.english
- ) : (
- <Skeleton width={450} />
- )}
- </h1>
- {info ? (
- <div className="flex gap-6">
- {info?.episodes && (
- <div
- className={`dynamic-text rounded-md px-2 font-karla font-bold`}
- style={color}
- >
- {info?.episodes} Episodes
- </div>
- )}
- {info?.startDate?.year && (
- <div
- className={`dynamic-text rounded-md px-2 font-karla font-bold`}
- style={color}
- >
- {info?.startDate?.year}
- </div>
- )}
- {info?.averageScore && (
- <div
- className={`dynamic-text rounded-md px-2 font-karla font-bold`}
- style={color}
- >
- {info?.averageScore}%
- </div>
- )}
- {info?.type && (
- <div
- className={`dynamic-text rounded-md px-2 font-karla font-bold`}
- style={color}
- >
- {info?.type}
- </div>
- )}
- {info?.status && (
- <div
- className={`dynamic-text rounded-md px-2 font-karla font-bold`}
- style={color}
- >
- {info?.status}
- </div>
- )}
- <div
- className={`dynamic-text rounded-md px-2 font-karla font-bold`}
- style={color}
- >
- Sub | EN
- </div>
- </div>
- ) : (
- <Skeleton width={240} height={32} />
- )}
- </div>
- {info ? (
- <p
- dangerouslySetInnerHTML={{ __html: info?.description }}
- className="overflow-y-scroll scrollbar-thin pr-2 scrollbar-thumb-secondary scrollbar-thumb-rounded-lg h-[140px]"
- />
- ) : (
- <Skeleton className="h-[130px]" />
- )}
- </div>
- </div>
-
- <div>
- <div className="flex gap-5 items-center">
- {info?.relations?.edges?.length > 0 && (
- <div className="p-3 lg:p-0 text-[20px] lg:text-2xl font-bold font-karla">
- Relations
- </div>
- )}
- {info?.relations?.edges?.length > 3 && (
- <div
- className="cursor-pointer"
- onClick={() => setShowAll(!showAll)}
- >
- {showAll ? "show less" : "show more"}
- </div>
- )}
- </div>
- <div
- className={`w-screen lg:w-full flex gap-5 overflow-x-scroll snap-x scroll-px-5 scrollbar-none lg:grid lg:grid-cols-3 justify-items-center lg:pt-7 lg:pb-5 px-3 lg:px-4 pt-4 rounded-xl`}
- >
- {info?.relations?.edges ? (
- info?.relations?.edges
- .slice(0, showAll ? info?.relations?.edges.length : 3)
- .map((r, index) => {
- const rel = r.node;
- return (
- <Link
- key={rel.id}
- href={
- rel.type === "ANIME" ||
- rel.type === "OVA" ||
- rel.type === "MOVIE" ||
- rel.type === "SPECIAL" ||
- rel.type === "ONA"
- ? `/en/anime/${rel.id}`
- : `/en/manga/${rel.id}`
- }
- className={`lg:hover:scale-[1.02] snap-start hover:shadow-lg scale-100 transition-transform duration-200 ease-out w-full ${
- rel.type === "MUSIC" ? "pointer-events-none" : ""
- }`}
- >
- <div
- key={rel.id}
- className="w-[400px] lg:w-full h-[126px] bg-secondary flex rounded-md"
- >
- <div className="w-[90px] bg-image rounded-l-md shrink-0">
- <Image
- src={rel.coverImage.extraLarge}
- alt={rel.id}
- height={500}
- width={500}
- className="object-cover h-full w-full shrink-0 rounded-l-md"
- />
- </div>
- <div className="h-full grid px-3 items-center">
- <div className="text-action font-outfit font-bold">
- {r.relationType}
- </div>
- <div className="font-outfit font-thin line-clamp-2">
- {rel.title.userPreferred}
- </div>
- <div className={``}>{rel.type}</div>
- </div>
- </div>
- </Link>
- );
- })
- ) : (
- <>
- {[1, 2, 3].map((item) => (
- <div key={item} className="w-full hidden lg:block">
- <Skeleton className="h-[126px]" />
- </div>
- ))}
- <div className="w-full lg:hidden">
- <Skeleton className="h-[126px]" />
- </div>
- </>
- )}
- </div>
- </div>
- </>
- );
-}
diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js
index 4420d24..8db1465 100644
--- a/components/anime/mobile/topSection.js
+++ b/components/anime/mobile/topSection.js
@@ -1,188 +1,15 @@
-import {
- ArrowUpCircleIcon,
- MagnifyingGlassIcon,
-} from "@heroicons/react/24/solid";
-
-import {
- ArrowLeftIcon,
- PlayIcon,
- PlusIcon,
- ShareIcon,
- UserIcon,
-} from "@heroicons/react/24/solid";
+import { PlayIcon, PlusIcon, ShareIcon } 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());
- };
-
- // 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 (
- <>
- <nav
- className={`fixed z-[200] top-0 py-3 px-5 w-full ${
- scrollPosition?.y >= scrollP
- ? "bg-tersier shadow-tersier shadow-sm"
- : ""
- } transition-all duration-200 ease-linear`}
- >
- <div className="flex items-center justify-between max-w-screen-2xl mx-auto">
- <div className="flex w-full items-center gap-4">
- {info ? (
- <>
- <button
- type="button"
- className="flex-center w-7 h-7 text-white"
- onClick={() => {
- // router.back();
- router.push("/en");
- }}
- >
- <ArrowLeftIcon className="w-full h-full" />
- </button>
- <span
- className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${
- scrollPosition?.y >= scrollP + 80
- ? "opacity-100"
- : "opacity-0"
- } transition-all duration-200 ease-linear`}
- >
- {info.title.romaji}
- </span>
- </>
- ) : (
- // <></>
- <Link
- href={"/en"}
- className="flex-center text-white font-outfit text-2xl font-semibold"
- >
- moopa
- </Link>
- )}
- </div>
- <div className="flex items-center gap-4">
- <button
- type="button"
- onClick={() => setIsOpen(true)}
- className="flex-center w-[26px] h-[26px]"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="32"
- height="32"
- viewBox="0 0 24 24"
- >
- <path
- fill="none"
- stroke="currentColor"
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth="2"
- d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z"
- ></path>
- </svg>
- </button>
- {/* <div
- className="bg-white"
- // title={sessions ? "Go to Profile" : "Login With AniList"}
- > */}
- {session ? (
- <div className="w-7 h-7 relative flex flex-col items-center group">
- <button
- type="button"
- onClick={() =>
- router.push(`/en/profile/${session?.user.name}`)
- }
- className="rounded-full bg-white/30 overflow-hidden"
- >
- <Image
- src={session?.user.image.large}
- alt="avatar"
- width={50}
- height={50}
- className="w-full h-full object-cover"
- />
- </button>
- <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1">
- <Link
- href={`/en/profile/${session?.user.name}`}
- className="hover:text-action"
- >
- Profile
- </Link>
- <div
- onClick={() => signOut("AniListProvider")}
- className="hover:text-action"
- >
- Log out
- </div>
- </div>
- </div>
- ) : (
- <button
- type="button"
- onClick={() => signIn("AniListProvider")}
- title="Login With AniList"
- className="w-7 h-7 bg-white/30 rounded-full overflow-hidden"
- >
- <UserIcon className="w-full h-full translate-y-2" />
- </button>
- )}
- {/* </div> */}
- </div>
- </div>
- </nav>
- {toTop && (
- <button
- type="button"
- onClick={() => {
- window.scrollTo({
- top: 0,
- behavior: "smooth",
- });
- }}
- className={`${
- scrollPosition?.y >= 180
- ? "-translate-x-6 opacity-100"
- : "translate-x-[100%] opacity-0"
- } transform transition-all duration-300 ease-in-out fixed bottom-24 lg:bottom-14 right-0 z-[500]`}
- >
- <ArrowUpCircleIcon className="w-10 h-10 text-white" />
- </button>
- )}
- </>
- );
-}
+import { NewNavbar } from "@/components/shared/NavBar";
export default function DetailTop({
info,
- session,
statuses,
handleOpen,
watchUrl,
@@ -217,7 +44,7 @@ export default function DetailTop({
return (
<div className="gap-6 w-full px-3 pt-4 md:pt-10 flex flex-col items-center justify-center">
- <NewNavbar info={info} session={session} />
+ <NewNavbar info={info} />
{/* MAIN */}
<div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12">
diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js
index db18651..2abfd0b 100644
--- a/components/anime/viewMode/thumbnailDetail.js
+++ b/components/anime/viewMode/thumbnailDetail.js
@@ -32,8 +32,8 @@ export default function ThumbnailDetail({
<Image
src={image || ""}
alt={`Episode ${epi?.number} Thumbnail`}
- width={1000}
- height={1000}
+ width={420}
+ height={236}
className="object-cover z-30 rounded-lg h-[110px] lg:h-[160px] brightness-[65%]"
/>
)}
@@ -41,7 +41,7 @@ export default function ThumbnailDetail({
className={`absolute bottom-0 left-0 h-[2px] bg-red-700`}
style={{
width:
- progress && artStorage && epi?.number <= progress
+ progress || (artStorage && epi?.number <= progress)
? "100%"
: artStorage?.[epi?.id]
? `${prog}%`
@@ -49,7 +49,7 @@ export default function ThumbnailDetail({
}}
/>
<span className="absolute bottom-2 left-2 font-karla font-semibold text-sm lg:text-lg">
- Episode {epi?.number}
+ Episode {epi?.number || 0}
</span>
<div className="z-[9999] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]">
<svg
@@ -68,7 +68,7 @@ export default function ThumbnailDetail({
className={`w-[70%] h-full select-none p-4 flex flex-col justify-center gap-3`}
>
<h1 className="font-karla font-bold text-base lg:text-lg xl:text-xl italic line-clamp-1">
- {title || `Episode ${epi?.number}`}
+ {title || `Episode ${epi?.number || 0}`}
</h1>
{description && (
<p className="line-clamp-2 text-xs lg:text-md xl:text-lg italic font-outfit font-extralight">
diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js
index 69cd8c3..7259beb 100644
--- a/components/anime/viewMode/thumbnailOnly.js
+++ b/components/anime/viewMode/thumbnailOnly.js
@@ -23,7 +23,7 @@ export default function ThumbnailOnly({
className="transition-all duration-200 ease-out lg:hover:scale-105 hover:ring-1 hover:ring-white cursor-pointer bg-secondary shrink-0 relative w-full h-[180px] sm:h-[130px] subpixel-antialiased rounded-md overflow-hidden"
>
<span className="absolute text-sm z-40 bottom-1 left-2 font-karla font-semibold text-white">
- Episode {episode?.number}
+ Episode {episode?.number || 0}
</span>
<span
className={`absolute bottom-7 left-0 h-[2px] bg-red-600`}
@@ -40,7 +40,7 @@ export default function ThumbnailOnly({
{image && (
<Image
src={image || ""}
- alt="epi image"
+ alt={`Episode ${episode?.number} Thumbnail`}
width={500}
height={500}
className="object-cover w-full h-[150px] sm:h-[100px] z-20 brightness-75"
diff --git a/components/anime/changeView.js b/components/anime/viewSelector.js
index 75ebdff..f114a8b 100644
--- a/components/anime/changeView.js
+++ b/components/anime/viewSelector.js
@@ -1,4 +1,4 @@
-export default function ChangeView({ view, setView, episode, map }) {
+export default function ViewSelector({ view, setView, episode, map }) {
return (
<div className="flex gap-3 rounded-sm items-center p-2">
<div
@@ -6,6 +6,7 @@ export default function ChangeView({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
) || !map
@@ -32,6 +33,7 @@ export default function ChangeView({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
) || !map
@@ -50,6 +52,7 @@ export default function ChangeView({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
) || !map
@@ -71,6 +74,7 @@ export default function ChangeView({ view, setView, episode, map }) {
episode?.length > 0
? map?.every(
(item) =>
+ item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
) || !map
diff --git a/components/anime/watch/primarySide.js b/components/anime/watch/primarySide.js
deleted file mode 100644
index a3d9f4f..0000000
--- a/components/anime/watch/primarySide.js
+++ /dev/null
@@ -1,276 +0,0 @@
-import { useEffect, useState } from "react";
-import { ChevronDownIcon } from "@heroicons/react/20/solid";
-import { ForwardIcon } from "@heroicons/react/24/solid";
-import { useRouter } from "next/router";
-import { signIn } from "next-auth/react";
-import Details from "./primary/details";
-import VideoPlayer from "../../videoPlayer";
-import Link from "next/link";
-import Skeleton from "react-loading-skeleton";
-import Modal from "../../modal";
-import AniList from "../../media/aniList";
-
-export default function PrimarySide({
- info,
- session,
- epiNumber,
- navigation,
- providerId,
- watchId,
- onList,
- proxy,
- disqus,
- setOnList,
- episodeList,
- timeWatched,
- dub,
-}) {
- const [episodeData, setEpisodeData] = useState();
- 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 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(
- epiNumber
- )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=`
- ).then((res) => {
- if (!res.ok) {
- switch (res.status) {
- case 404: {
- return null;
- }
- }
- }
- return res.json();
- });
-
- const op =
- skip?.results?.find((item) => item.skipType === "op") || null;
- const ed =
- skip?.results?.find((item) => item.skipType === "ed") || null;
-
- setSkip({ op, ed });
-
- setEpisodeData(anify);
- setLoading(false);
- }
- }
-
- fetchData();
- return () => {
- setEpisodeData();
- setSkip();
- };
- }, [providerId, watchId, info]);
-
- useEffect(() => {
- const mediaSession = navigator.mediaSession;
- if (!mediaSession) return;
-
- const now = navigation?.playing;
- const poster = now?.image || info?.bannerImage;
- const title = now?.title || info?.title?.romaji;
-
- const artwork = poster
- ? [{ src: poster, sizes: "512x512", type: "image/jpeg" }]
- : undefined;
-
- mediaSession.metadata = new MediaMetadata({
- title: title,
- artist: `Moopa ${
- title === info?.title?.romaji
- ? "- Episode " + epiNumber
- : `- ${info?.title?.romaji || info?.title?.english}`
- }`,
- artwork,
- });
- }, [navigation, info, epiNumber]);
-
- function handleOpen() {
- setOpen(true);
- document.body.style.overflow = "hidden";
- }
-
- function handleClose() {
- setOpen(false);
- document.body.style.overflow = "auto";
- }
-
- return (
- <>
- <Modal open={open} onClose={() => handleClose()}>
- {!session && (
- <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md">
- <h1 className="text-md font-extrabold font-karla">
- Edit your list
- </h1>
- <button
- className="flex items-center bg-[#363642] rounded-md text-white p-1"
- onClick={() => signIn("AniListProvider")}
- >
- <h1 className="px-1 font-bold font-karla">Login with AniList</h1>
- <div className="scale-[60%] pb-[1px]">
- <AniList />
- </div>
- </button>
- </div>
- )}
- </Modal>
- <div className="w-full h-full">
- <div key={watchId} className="w-full aspect-video bg-black">
- {!loading ? (
- navigation && episodeData?.sources?.length !== 0 ? (
- <VideoPlayer
- session={session}
- info={info}
- data={episodeData}
- provider={providerId}
- id={watchId}
- progress={epiNumber}
- skip={skip}
- proxy={proxy}
- aniId={info.id}
- aniTitle={info.title?.romaji || info.title?.english}
- track={navigation}
- timeWatched={timeWatched}
- dub={dub}
- />
- ) : (
- <p className="h-full flex-center">
- Video is not available, please try other providers
- </p>
- )
- ) : (
- <div className="flex-center aspect-video bg-black">
- <div className="lds-ellipsis">
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- </div>
- </div>
- )}
- </div>
- <div className="flex flex-col divide-y divide-white/20">
- {info && episodeList ? (
- <div className="flex items-center justify-between py-3 px-3">
- <div className="flex flex-col gap-2 w-[60%]">
- <h1 className="text-xl font-outfit font-semibold line-clamp-1">
- <Link
- href={`/en/anime/${info.id}`}
- className="hover:underline"
- title={navigation?.playing?.title || info.title?.romaji}
- >
- {navigation?.playing?.title || info.title?.romaji}
- </Link>
- </h1>
- <h3 className="text-sm font-karla font-light">
- Episode {epiNumber}
- </h3>
- </div>
- <div className="flex gap-4 items-center justify-end">
- <div className="relative">
- <select
- className="flex items-center gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer"
- value={epiNumber}
- onChange={(e) => {
- const selectedEpisode = episodeList.find(
- (episode) => episode.number === parseInt(e.target.value)
- );
- router.push(
- `/en/anime/watch/${info.id}/${providerId}?id=${
- selectedEpisode.id
- }&num=${selectedEpisode.number}${
- dub ? `&dub=${dub}` : ""
- }`
- );
- }}
- >
- {episodeList.map((episode) => (
- <option key={episode.number} value={episode.number}>
- Episode {episode.number}
- </option>
- ))}
- </select>
- <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" />
- </div>
- <button
- disabled={!navigation?.next}
- className={`${
- !navigation?.next ? "pointer-events-none" : ""
- }relative group`}
- onClick={() => {
- router.push(
- `/en/anime/watch/${info.id}/${providerId}?id=${
- navigation?.next.id
- }&num=${navigation?.next.number}${
- dub ? `&dub=${dub}` : ""
- }`
- );
- }}
- >
- <span className="absolute z-[9999] -left-11 -top-14 p-2 shadow-xl rounded-md transform transition-all whitespace-nowrap bg-secondary lg:group-hover:block group-hover:opacity-1 hidden font-karla font-bold">
- Next Episode
- </span>
- <ForwardIcon
- className={`w-6 h-6 ${
- !navigation?.next ? "text-[#282828]" : ""
- }`}
- />
- </button>
- </div>
- </div>
- ) : (
- <div className="py-3 px-4">
- <div className="text-xl font-outfit font-semibold line-clamp-2">
- <div className="inline hover:underline">
- <Skeleton width={240} />
- </div>
- </div>
- <h4 className="text-sm font-karla font-light">
- <Skeleton width={75} />
- </h4>
- </div>
- )}
- <Details
- info={info}
- session={session}
- description={navigation?.playing?.description || info?.description}
- epiNumber={epiNumber}
- id={watchId}
- onList={onList}
- setOnList={setOnList}
- handleOpen={handleOpen}
- disqus={disqus}
- />
- </div>
- </div>
- </>
- );
-}
diff --git a/components/home/content.js b/components/home/content.js
index c869f6b..9dd4408 100644
--- a/components/home/content.js
+++ b/components/home/content.js
@@ -305,6 +305,7 @@ export default function Content({
anime.image ||
anime.coverImage?.extraLarge ||
anime.coverImage?.large ||
+ anime?.coverImage ||
"https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
}
alt={
@@ -336,7 +337,7 @@ export default function Content({
<p className="absolute z-40 text-center w-[86px] lg:w-[110px] top-1 -right-2 lg:top-[5.5px] lg:-right-2 font-karla text-sm lg:text-base">
Episode{" "}
<span className="text-white">
- {anime?.episodeNumber}
+ {anime?.currentEpisode || anime?.episodeNumber}
</span>
</p>
</Fragment>
@@ -377,16 +378,6 @@ export default function Content({
className="flex flex-col gap-2 shrink-0 cursor-pointer relative group/item"
>
<div className="absolute flex flex-col gap-1 z-40 top-1 right-1 transition-all duration-200 ease-out opacity-0 group-hover/item:opacity-100 scale-90 group-hover/item:scale-100 group-hover/item:visible invisible ">
- {/* <button
- type="button"
- className="flex flex-col items-center group/delete relative"
- onClick={() => removeItem(i.watchId)}
- >
- <XMarkIcon className="w-6 h-6 shrink-0 bg-primary p-1 rounded-full hover:text-action scale-100 hover:scale-105 transition-all duration-200 ease-out" />
- <span className="absolute font-karla bg-secondary shadow-black shadow-2xl py-1 px-2 whitespace-nowrap text-white text-sm rounded-md right-7 -bottom-[2px] z-40 duration-300 transition-all ease-out group-hover/delete:visible group-hover/delete:scale-100 group-hover/delete:translate-x-0 group-hover/delete:opacity-100 opacity-0 translate-x-10 scale-50 invisible">
- Remove from history
- </span>
- </button> */}
<HistoryOptions
remove={removeItem}
watchId={i.watchId}
@@ -443,10 +434,10 @@ export default function Content({
{i?.image && (
<Image
src={i?.image}
- width="0"
- height="0"
+ width={320}
+ height={180}
alt="Episode Thumbnail"
- className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10"
+ className="w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10"
/>
)}
</Link>
diff --git a/components/home/staticNav.js b/components/home/staticNav.js
deleted file mode 100644
index 3f43461..0000000
--- a/components/home/staticNav.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import { signIn, signOut, useSession } from "next-auth/react";
-import { getCurrentSeason } from "../../utils/getTimes";
-import Link from "next/link";
-// 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 = new Date().getFullYear();
- const season = getCurrentSeason();
-
- const router = useRouter();
-
- const { setIsOpen } = useSearch();
-
- return (
- <>
- {/* NAVBAR PC */}
- <div className="flex items-center justify-center w-full">
- <div className="flex w-full items-center justify-between px-4 lg:w-[90%] lg:pt-7">
- <div className="flex items-center lg:gap-16">
- <Link
- href="/en/"
- className=" font-outfit lg:text-[40px] text-[30px] font-bold text-[#FF7F57]"
- >
- moopa
- </Link>
- <ul className="hidden items-center gap-10 pt-2 font-outfit text-[14px] lg:flex">
- <li>
- <Link
- href={`/en/search/anime?season=${season}&year=${year}`}
- className="hover:text-action/80 transition-all duration-150 ease-linear"
- >
- This Season
- </Link>
- </li>
- <li>
- <Link
- href="/en/search/manga"
- className="hover:text-action/80 transition-all duration-150 ease-linear"
- >
- Manga
- </Link>
- </li>
- <li>
- <Link
- href="/en/search/anime"
- className="hover:text-action/80 transition-all duration-150 ease-linear"
- >
- Anime
- </Link>
- </li>
- <li>
- <Link
- href="/en/schedule"
- className="hover:text-action/80 transition-all duration-150 ease-linear"
- >
- Schedule
- </Link>
- </li>
-
- {status === "loading" ? (
- <li>Loading...</li>
- ) : (
- <>
- {!sessions && (
- <li>
- <button
- onClick={() => signIn("AniListProvider")}
- className="hover:text-action/80 transition-all duration-150 ease-linear"
- // className="px-2 py-1 ring-1 ring-action font-bold font-karla rounded-md"
- >
- Sign In
- </button>
- </li>
- )}
- {sessions && (
- <li className="text-center">
- <Link
- href={`/en/profile/${sessions?.user.name}`}
- className="hover:text-action/80 transition-all duration-150 ease-linear"
- >
- My List
- </Link>
- </li>
- )}
- </>
- )}
- </ul>
- </div>
- <div className="flex items-center gap-4">
- <button
- type="button"
- onClick={() => setIsOpen(true)}
- className="flex-center w-[26px] h-[26px]"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="32"
- height="32"
- viewBox="0 0 24 24"
- >
- <path
- fill="none"
- stroke="currentColor"
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth="2"
- d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z"
- ></path>
- </svg>
- </button>
- {/* <div
- className="bg-white"
- // title={sessions ? "Go to Profile" : "Login With AniList"}
- > */}
- {sessions ? (
- <div className="w-8 h-8 relative flex flex-col items-center group">
- <button
- type="button"
- onClick={() =>
- router.push(`/en/profile/${sessions?.user.name}`)
- }
- className="rounded-full bg-white/30 overflow-hidden"
- >
- <Image
- src={sessions?.user.image.large}
- alt="avatar"
- width={50}
- height={50}
- className="w-full h-full object-cover"
- />
- </button>
- <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1">
- <Link
- href={`/en/profile/${sessions?.user.name}`}
- className="hover:text-action"
- >
- Profile
- </Link>
- <div
- onClick={() => signOut("AniListProvider")}
- className="hover:text-action cursor-pointer"
- >
- Log out
- </div>
- </div>
- </div>
- ) : (
- <button
- type="button"
- onClick={() => signIn("AniListProvider")}
- title="Login With AniList"
- className="w-7 h-7 bg-white/30 rounded-full overflow-hidden"
- >
- <UserIcon className="w-full h-full translate-y-2 text-white/50" />
- </button>
- )}
- {/* </div> */}
- </div>
- </div>
- </div>
- </>
- );
-}
diff --git a/components/id/player/Artplayer.js b/components/id/player/Artplayer.js
deleted file mode 100644
index e209433..0000000
--- a/components/id/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: '<svg class="hi-solid hi-rewind inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8.445 14.832A1 1 0 0010 14v-2.798l5.445 3.63A1 1 0 0017 14V6a1 1 0 00-1.555-.832L10 8.798V6a1 1 0 00-1.555-.832l-6 4a1 1 0 000 1.664l6 4z"/></svg>',
- tooltip: "Backward 5s",
- click: function () {
- art.backward = 5;
- },
- },
- {
- name: "fast-forward",
- position: "right",
- html: '<svg class="hi-solid hi-fast-forward inline-block w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4.555 5.168A1 1 0 003 6v8a1 1 0 001.555.832L10 11.202V14a1 1 0 001.555.832l6-4a1 1 0 000-1.664l-6-4A1 1 0 0010 6v2.798l-5.445-3.63z"/></svg>',
- tooltip: "Forward 5s",
- click: function () {
- art.forward = 5;
- },
- },
- ],
- });
-
- art.events.proxy(document, "keydown", (event) => {
- if (event.key === "f" || event.key === "F") {
- art.fullscreen = !art.fullscreen;
- }
- });
-
- if (getInstance && typeof getInstance === "function") {
- getInstance(art);
- }
-
- return () => {
- if (art && art.destroy) {
- art.destroy(false);
- }
- };
- }, []);
-
- return <div ref={artRef} {...rest}></div>;
-}
diff --git a/components/id/player/VideoPlayerId.js b/components/id/player/VideoPlayerId.js
deleted file mode 100644
index 1168313..0000000
--- a/components/id/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 && (
- <Player
- key={`${url}`}
- option={{
- url: `${url}`,
- quality: source,
- title: `${title}`,
- autoplay: true,
- screenshot: true,
- poster: poster ? poster : "",
- }}
- res={resolution}
- quality={source}
- style={{
- width: "100%",
- height: "100%",
- margin: "0 auto 0",
- }}
- getInstance={(art) => {
- art.on("ready", () => {
- const seek = art.storage.get(id);
- const seekTime = seek?.time || 0;
- const duration = art.duration;
- const percentage = seekTime / duration;
-
- if (percentage >= 0.9) {
- art.currentTime = 0;
- console.log("Video started from the beginning");
- } else {
- art.currentTime = seekTime;
- }
- });
-
- art.on("video:timeupdate", () => {
- if (!session) return;
- const mediaSession = navigator.mediaSession;
- const currentTime = art.currentTime;
- const duration = art.duration;
- const percentage = currentTime / duration;
-
- mediaSession.setPositionState({
- duration: art.duration,
- playbackRate: art.playbackRate,
- position: art.currentTime,
- });
-
- if (percentage >= 0.9) {
- // use >= instead of >
- markProgress(aniId, progress, stats);
- art.off("video:timeupdate");
- console.log("Video progress marked");
- }
- });
-
- art.on("video:timeupdate", () => {
- var currentTime = art.currentTime;
- // console.log(art.currentTime);
- art.storage.set(id, {
- time: art.currentTime,
- duration: art.duration,
- });
-
- if (
- op &&
- currentTime >= op.interval.startTime &&
- currentTime <= op.interval.endTime
- ) {
- // Add the layer if it's not already added
- if (!art.controls["op"]) {
- // Remove the other control if it's already added
- if (art.controls["ed"]) {
- art.controls.remove("ed");
- }
-
- // Add the control
- art.controls.add({
- name: "op",
- position: "top",
- html: '<button class="skip-button">Skip Opening</button>',
- click: function (...args) {
- art.seek = op.interval.endTime;
- },
- });
- }
- } else if (
- ed &&
- currentTime >= ed.interval.startTime &&
- currentTime <= ed.interval.endTime
- ) {
- // Add the layer if it's not already added
- if (!art.controls["ed"]) {
- // Remove the other control if it's already added
- if (art.controls["op"]) {
- art.controls.remove("op");
- }
-
- // Add the control
- art.controls.add({
- name: "ed",
- position: "top",
- html: '<button class="skip-button">Skip Ending</button>',
- click: function (...args) {
- art.seek = ed.interval.endTime;
- },
- });
- }
- } else {
- // Remove the controls if they're added
- if (art.controls["op"]) {
- art.controls.remove("op");
- }
- if (art.controls["ed"]) {
- art.controls.remove("ed");
- }
- }
- });
- }}
- />
- )}
- </>
- );
-}
diff --git a/components/layout.js b/components/layout.js
deleted file mode 100644
index 49850c9..0000000
--- a/components/layout.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import Navbar from "./navbar";
-import Footer from "./footer";
-import { useEffect, useState } from "react";
-
-function Layout(props) {
- const [isAtTop, setIsAtTop] = useState(true);
- const [isScrollingDown, setIsScrollingDown] = useState(false);
-
- useEffect(() => {
- const handleScroll = () => {
- const scrollY = window.scrollY;
-
- if (scrollY <= 200) {
- setIsAtTop(true);
- setIsScrollingDown(false);
- } else if (scrollY > lastScrollY) {
- setIsAtTop(false);
- setIsScrollingDown(true);
- } else {
- setIsAtTop(false);
- setIsScrollingDown(false);
- }
-
- lastScrollY = scrollY;
- };
-
- let lastScrollY = window.scrollY;
-
- window.addEventListener("scroll", handleScroll);
-
- return () => {
- window.removeEventListener("scroll", handleScroll);
- };
- }, []);
-
- return (
- <>
- <main
- className={`flex h-auto bg-[#121212] text-white flex-col ${props.className}`}
- >
- {/* PC/Tablet */}
- <Navbar
- className={`absolute z-50 hidden w-full duration-500 lg:fixed lg:top-0 lg:block lg:transition-all ${
- isAtTop
- ? `px-2 pt-2 transition-all duration-1000 ${props.navTop}`
- : isScrollingDown
- ? "lg:h-16 lg:translate-y-[-100%] lg:shadow-sm lg:bg-[#0c0d10] "
- : "lg:h-16 lg:translate-y-0 lg:shadow-sm lg:bg-[#0c0d10]"
- }`}
- />
-
- {/* Mobile */}
- <Navbar
- className={`absolute z-50 w-full duration-300 lg:fixed lg:top-0 lg:hidden lg:transition-all`}
- />
- <div className="grid items-center justify-center">{props.children}</div>
- <Footer />
- </main>
- </>
- );
-}
-
-export default Layout;
diff --git a/components/manga/info/topSection.js b/components/manga/info/topSection.js
index 40b5a37..45d5f11 100644
--- a/components/manga/info/topSection.js
+++ b/components/manga/info/topSection.js
@@ -28,6 +28,7 @@ export default function TopSection({ info, firstEp, setCookie }) {
src={info.coverImage}
width={500}
height={500}
+ priority
alt="cover image"
className="hidden md:block object-cover h-[10rem] xs:h-[14rem] lg:h-[22rem] rounded-sm shadow-lg shadow-[#1b1b1f] bg-[#34343b]/20"
/>
diff --git a/components/modal.js b/components/modal.js
index 78b76d7..5d6d0cc 100644
--- a/components/modal.js
+++ b/components/modal.js
@@ -2,7 +2,7 @@ export default function Modal({ open, onClose, children }) {
return (
<div
onClick={onClose}
- className={`fixed z-50 inset-0 flex justify-center items-center transition-colors ${
+ className={`fixed z-[999] inset-0 flex justify-center items-center transition-colors ${
open ? "visible bg-black bg-opacity-50 backdrop-blur-sm" : "invisible"
}`}
>
diff --git a/components/navbar.js b/components/navbar.js
deleted file mode 100644
index 0bb254f..0000000
--- a/components/navbar.js
+++ /dev/null
@@ -1,128 +0,0 @@
-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";
-import MobileNav from "./shared/MobileNav";
-
-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);
- };
-
- 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");
- }
- }, []);
-
- // console.log(session.user?.image);
-
- return (
- <header className={`${props.className}`}>
- <div className="flex h-16 w-auto items-center justify-between px-5 lg:mx-auto lg:w-[80%] lg:px-0 text-[#dbdcdd]">
- <div className="pb-2 font-outfit text-4xl font-semibold lg:block text-white">
- <Link href={`/${lang}/`}>moopa</Link>
- </div>
-
- <MobileNav sessions={session} />
-
- <nav className="left-0 top-[-100%] hidden w-auto items-center gap-10 px-5 lg:flex">
- <ul className="hidden gap-10 font-roboto text-md lg:flex items-center relative">
- <li>
- <Link
- href={`/${lang}/`}
- className="p-2 transition-all duration-100 hover:text-orange-600"
- >
- home
- </Link>
- </li>
- <li>
- <Link
- href={`/${lang}/about`}
- className="p-2 transition-all duration-100 hover:text-orange-600"
- >
- about
- </Link>
- </li>
- <li>
- <Link
- href={`/${lang}/search/anime`}
- className="p-2 transition-all duration-100 hover:text-orange-600"
- >
- search
- </Link>
- </li>
- {status === "loading" ? (
- <li>Loading...</li>
- ) : (
- <>
- {!session && (
- <li>
- <button
- onClick={() => signIn("AniListProvider")}
- className="ring-1 ring-action font-karla font-bold px-2 py-1 rounded-md"
- >
- Sign in
- </button>
- </li>
- )}
- {session && (
- <li className="flex items-center justify-center group ">
- <button>
- <Image
- src={session?.user.image.large}
- alt="imagine"
- width={500}
- height={500}
- className="object-cover h-10 w-10 rounded-full"
- />
- </button>
- <div className="absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all grid place-items-center gap-1">
- <Link
- href={`/${lang}/profile/${session?.user.name}`}
- className="hover:text-action"
- >
- Profile
- </Link>
- <button
- onClick={() => signOut("AniListProvider")}
- className="hover:text-action"
- >
- Log out
- </button>
- </div>
- {/* My List */}
- </li>
- )}
- </>
- )}
- </ul>
- </nav>
- </div>
- </header>
- );
-}
-
-export default Navbar;
diff --git a/components/shared/MobileNav.js b/components/shared/MobileNav.js
index 6dd1e64..d0f29c2 100644
--- a/components/shared/MobileNav.js
+++ b/components/shared/MobileNav.js
@@ -1,12 +1,12 @@
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
-import { CalendarIcon, ClockIcon, HomeIcon } from "@heroicons/react/24/outline";
-import { signIn, signOut } from "next-auth/react";
+import { CalendarIcon, HomeIcon } from "@heroicons/react/24/outline";
+import { signIn, signOut, useSession } from "next-auth/react";
import Image from "next/image";
import Link from "next/link";
-import { useRouter } from "next/router";
import { useState } from "react";
-export default function MobileNav({ sessions, hideProfile = false }) {
+export default function MobileNav({ hideProfile = false }) {
+ const { data: sessions } = useSession();
const [isVisible, setIsVisible] = useState(false);
const handleShowClick = () => {
diff --git a/components/shared/NavBar.js b/components/shared/NavBar.js
new file mode 100644
index 0000000..42fcff0
--- /dev/null
+++ b/components/shared/NavBar.js
@@ -0,0 +1,265 @@
+import { useSearch } from "@/lib/hooks/isOpenState";
+import { getCurrentSeason } from "@/utils/getTimes";
+import { ArrowLeftIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid";
+import { UserIcon } from "@heroicons/react/24/solid";
+import { signIn, signOut, useSession } from "next-auth/react";
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter } from "next/router";
+import { useEffect, useState } from "react";
+
+const getScrollPosition = (el = window) => ({
+ x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
+ y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop,
+});
+
+export function NewNavbar({
+ info,
+ scrollP = 200,
+ toTop = false,
+ withNav = false,
+ paddingY = "py-3",
+ home = false,
+ back = false,
+ manga = false,
+ shrink = false,
+}) {
+ const { data: session } = useSession();
+ const router = useRouter();
+ const [scrollPosition, setScrollPosition] = useState();
+ const { setIsOpen } = useSearch();
+
+ const year = new Date().getFullYear();
+ const season = getCurrentSeason();
+
+ useEffect(() => {
+ const handleScroll = () => {
+ setScrollPosition(getScrollPosition());
+ };
+
+ // 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 (
+ <>
+ <nav
+ className={`${home ? "" : "fixed"} z-[200] top-0 px-5 w-full ${
+ scrollPosition?.y >= scrollP
+ ? home
+ ? ""
+ : `bg-tersier shadow-tersier shadow-sm ${
+ shrink ? "py-1" : `${paddingY}`
+ }`
+ : `${paddingY}`
+ } transition-all duration-200 ease-linear`}
+ >
+ <div
+ className={`flex items-center justify-between mx-auto ${
+ home ? "lg:max-w-[90%] gap-10" : "max-w-screen-2xl"
+ }`}
+ >
+ <div
+ className={`flex items-center ${
+ withNav ? `${home ? "" : "w-[20%]"} gap-8` : " w-full gap-4"
+ }`}
+ >
+ {info ? (
+ <>
+ <button
+ type="button"
+ className="flex-center w-7 h-7 text-white"
+ onClick={() => {
+ back
+ ? router.back()
+ : manga
+ ? router.push("/en/search/manga")
+ : router.push("/en");
+ }}
+ >
+ <ArrowLeftIcon className="w-full h-full" />
+ </button>
+ <span
+ className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${
+ scrollPosition?.y >= scrollP + 80
+ ? "opacity-100"
+ : "opacity-0"
+ } transition-all duration-200 ease-linear`}
+ >
+ {info.title.romaji}
+ </span>
+ </>
+ ) : (
+ // <></>
+ <Link
+ href={"/en"}
+ className={`flex-center font-outfit font-semibold pb-2 ${
+ home ? "text-4xl text-action" : "text-white text-3xl"
+ }`}
+ >
+ moopa
+ </Link>
+ )}
+ </div>
+
+ {withNav && (
+ <ul
+ className={`hidden w-full items-center gap-10 pt-2 font-outfit text-[14px] lg:pt-0 lg:flex ${
+ home ? "justify-start" : "justify-center"
+ }`}
+ >
+ <li>
+ <Link
+ href={`/en/search/anime?season=${season}&year=${year}`}
+ className="hover:text-action/80 transition-all duration-150 ease-linear"
+ >
+ This Season
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/en/search/manga"
+ className="hover:text-action/80 transition-all duration-150 ease-linear"
+ >
+ Manga
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/en/search/anime"
+ className="hover:text-action/80 transition-all duration-150 ease-linear"
+ >
+ Anime
+ </Link>
+ </li>
+ <li>
+ <Link
+ href="/en/schedule"
+ className="hover:text-action/80 transition-all duration-150 ease-linear"
+ >
+ Schedule
+ </Link>
+ </li>
+
+ {!session && (
+ <li>
+ <button
+ onClick={() => signIn("AniListProvider")}
+ className="hover:text-action/80 transition-all duration-150 ease-linear"
+ // className="px-2 py-1 ring-1 ring-action font-bold font-karla rounded-md"
+ >
+ Sign In
+ </button>
+ </li>
+ )}
+ {session && (
+ <li className="text-center">
+ <Link
+ href={`/en/profile/${session?.user.name}`}
+ className="hover:text-action/80 transition-all duration-150 ease-linear"
+ >
+ My List
+ </Link>
+ </li>
+ )}
+ </ul>
+ )}
+
+ <div className="flex w-[20%] justify-end items-center gap-4">
+ <button
+ type="button"
+ onClick={() => setIsOpen(true)}
+ className="flex-center w-[26px] h-[26px]"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="32"
+ height="32"
+ viewBox="0 0 24 24"
+ >
+ <path
+ fill="none"
+ stroke="currentColor"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ strokeWidth="2"
+ d="M15 15l6 6m-11-4a7 7 0 110-14 7 7 0 010 14z"
+ ></path>
+ </svg>
+ </button>
+ {/* <div
+ className="bg-white"
+ // title={sessions ? "Go to Profile" : "Login With AniList"}
+ > */}
+ {session ? (
+ <div className="w-7 h-7 relative flex flex-col items-center group">
+ <button
+ type="button"
+ onClick={() =>
+ router.push(`/en/profile/${session?.user.name}`)
+ }
+ className="rounded-full bg-white/30 overflow-hidden"
+ >
+ <Image
+ src={session?.user.image.large}
+ alt="avatar"
+ width={50}
+ height={50}
+ className="w-full h-full object-cover"
+ />
+ </button>
+ <div className="hidden absolute z-50 w-28 text-center -bottom-20 text-white shadow-2xl opacity-0 bg-secondary p-1 py-2 rounded-md font-karla font-light invisible group-hover:visible group-hover:opacity-100 duration-300 transition-all md:grid place-items-center gap-1">
+ <Link
+ href={`/en/profile/${session?.user.name}`}
+ className="hover:text-action"
+ >
+ Profile
+ </Link>
+ <button
+ type="button"
+ onClick={() => signOut("AniListProvider")}
+ className="hover:text-action"
+ >
+ Log out
+ </button>
+ </div>
+ </div>
+ ) : (
+ <button
+ type="button"
+ onClick={() => signIn("AniListProvider")}
+ title="Login With AniList"
+ className="w-7 h-7 bg-white/30 rounded-full overflow-hidden"
+ >
+ <UserIcon className="w-full h-full translate-y-1" />
+ </button>
+ )}
+ {/* </div> */}
+ </div>
+ </div>
+ </nav>
+ {toTop && (
+ <button
+ type="button"
+ onClick={() => {
+ window.scrollTo({
+ top: 0,
+ behavior: "smooth",
+ });
+ }}
+ className={`${
+ scrollPosition?.y >= 180
+ ? "-translate-x-6 opacity-100"
+ : "translate-x-[100%] opacity-0"
+ } transform transition-all duration-300 ease-in-out fixed bottom-24 lg:bottom-14 right-0 z-[500]`}
+ >
+ <ArrowUpCircleIcon className="w-10 h-10 text-white" />
+ </button>
+ )}
+ </>
+ );
+}
diff --git a/components/shared/bugReport.js b/components/shared/bugReport.js
new file mode 100644
index 0000000..9b99016
--- /dev/null
+++ b/components/shared/bugReport.js
@@ -0,0 +1,200 @@
+import { Fragment, useState } from "react";
+import { Dialog, Listbox, Transition } from "@headlessui/react";
+import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid";
+import { toast } from "react-toastify";
+
+const severityOptions = [
+ { id: 1, name: "Low" },
+ { id: 2, name: "Medium" },
+ { id: 3, name: "High" },
+ { id: 4, name: "Critical" },
+];
+
+const BugReportForm = ({ isOpen, setIsOpen }) => {
+ const [bugDescription, setBugDescription] = useState("");
+ const [severity, setSeverity] = useState(severityOptions[0]);
+
+ function closeModal() {
+ setIsOpen(false);
+ setBugDescription("");
+ setSeverity(severityOptions[0]);
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ const bugReport = {
+ desc: bugDescription,
+ severity: severity.name,
+ url: window.location.href,
+ createdAt: new Date().toISOString(),
+ };
+
+ try {
+ const res = await fetch("/api/v2/admin/bug-report", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ data: bugReport,
+ }),
+ });
+
+ const json = await res.json();
+ toast.success(json.message, {
+ hideProgressBar: true,
+ theme: "colored",
+ });
+ closeModal();
+ } catch (err) {
+ console.log(err);
+ toast.error("Something went wrong: " + err.message, {
+ hideProgressBar: true,
+ theme: "colored",
+ });
+ }
+ };
+
+ return (
+ <>
+ <Transition appear show={isOpen} as={Fragment}>
+ <Dialog as="div" className="relative z-[200]" onClose={closeModal}>
+ <Transition.Child
+ as={Fragment}
+ enter="ease-out duration-300"
+ enterFrom="opacity-0"
+ enterTo="opacity-100"
+ leave="ease-in duration-200"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+ <div className="fixed inset-0 bg-black bg-opacity-90" />
+ </Transition.Child>
+
+ <div className="fixed inset-0 overflow-y-auto">
+ <div className="flex min-h-full items-center justify-center p-4 ">
+ <Transition.Child
+ as={Fragment}
+ enter="ease-out duration-300"
+ enterFrom="opacity-0 scale-95"
+ enterTo="opacity-100 scale-100"
+ leave="ease-in duration-200"
+ leaveFrom="opacity-100 scale-100"
+ leaveTo="opacity-0 scale-95"
+ >
+ <Dialog.Panel className="w-full max-w-md transition-all">
+ <div className="bg-secondary p-6 rounded-lg shadow-xl">
+ <h2 className={`text-action text-2xl font-semibold mb-4`}>
+ Report a Bug
+ </h2>
+ <form onSubmit={handleSubmit}>
+ <div className="space-y-4">
+ <div>
+ <label
+ htmlFor="bugDescription"
+ className={`block text-txt text-sm font-medium mb-2`}
+ >
+ Bug Description
+ </label>
+ <textarea
+ id="bugDescription"
+ name="bugDescription"
+ rows="4"
+ className={`w-full bg-image text-txt rounded-md border border-txt focus:ring-action focus:border-action transition duration-300 focus:outline-none py-2 px-3`}
+ placeholder="Describe the bug you encountered..."
+ value={bugDescription}
+ onChange={(e) => setBugDescription(e.target.value)}
+ required
+ ></textarea>
+ </div>
+ <Listbox value={severity} onChange={setSeverity}>
+ <div className="relative mt-1">
+ <label
+ htmlFor="severity"
+ className={`block text-txt text-sm font-medium mb-2`}
+ >
+ Severity
+ </label>
+ <Listbox.Button
+ type="button"
+ className="relative w-full cursor-pointer hover:shadow-xl hover:scale-[1.01] transition-all rounded-lg bg-image py-2 pl-3 pr-10 text-left shadow-md sm:text-base duration-300"
+ >
+ <span className="block truncate text-white font-semibold">
+ {severity.name}
+ </span>
+ <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
+ <ChevronDownIcon
+ className="h-5 w-5 text-gray-400"
+ aria-hidden="true"
+ />
+ </span>
+ </Listbox.Button>
+ <Transition
+ as={Fragment}
+ leave="transition ease-in duration-100"
+ leaveFrom="opacity-100"
+ leaveTo="opacity-0"
+ >
+ <Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-image py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
+ {severityOptions.map((person, personIdx) => (
+ <Listbox.Option
+ key={personIdx}
+ className={({ active }) =>
+ `relative cursor-default select-none py-2 pl-10 pr-4 ${
+ active
+ ? "bg-secondary/50 text-white"
+ : "text-gray-400"
+ }`
+ }
+ value={person}
+ >
+ {({ selected }) => (
+ <>
+ <span
+ className={`block truncate ${
+ selected
+ ? "font-medium text-white"
+ : "font-normal"
+ }`}
+ >
+ {person.name}
+ </span>
+ {selected ? (
+ <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-action">
+ <CheckIcon
+ className="h-5 w-5"
+ aria-hidden="true"
+ />
+ </span>
+ ) : null}
+ </>
+ )}
+ </Listbox.Option>
+ ))}
+ </Listbox.Options>
+ </Transition>
+ </div>
+ </Listbox>
+ </div>
+ <div className="mt-4">
+ <button
+ type="submit"
+ className={`w-full bg-action text-white py-2 px-4 rounded-md font-semibold hover:bg-action/80 focus:ring focus:ring-action focus:outline-none transition duration-300`}
+ >
+ Submit Bug Report
+ </button>
+ </div>
+ </form>
+ </div>
+ </Dialog.Panel>
+ </Transition.Child>
+ </div>
+ </div>
+ </Dialog>
+ </Transition>
+ </>
+ );
+};
+
+export default BugReportForm;
diff --git a/components/footer.js b/components/shared/footer.js
index ca5a21f..91af5a8 100644
--- a/components/footer.js
+++ b/components/shared/footer.js
@@ -2,6 +2,7 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { parseCookies, setCookie } from "nookies";
+import Image from "next/image";
function Footer() {
const [year] = useState(new Date().getFullYear());
@@ -46,7 +47,7 @@ function Footer() {
return (
<footer className="flex-col w-full">
<div className="text-[#dbdcdd] z-40 bg-[#0c0d10] lg:flex lg:h-[12rem] w-full lg:items-center lg:justify-between">
- <div className="mx-auto flex w-[85%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 py-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0">
+ <div className="mx-auto flex w-[90%] lg:w-[95%] xl:w-[80%] flex-col space-y-10 py-6 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:py-0">
<div className="flex flex-col gap-2">
{/* <div className="flex items-center gap-2"> */}
{/* <Image
@@ -56,7 +57,7 @@ function Footer() {
height={100}
className="w-10 h-10"
/> */}
- <p className="font-outfit text-4xl">moopa</p>
+ <div className="flex gap-2 font-outfit text-4xl">moopa</div>
<p className="font-karla lg:text-[0.8rem] text-[0.65rem] text-[#9c9c9c] lg:w-[520px] italic">
This site does not store any files on our server, we only linked
to the media which is hosted on 3rd party services.
@@ -80,7 +81,7 @@ function Footer() {
<Link href={`/${lang}/search/manga`}>Popular Manga</Link>
</li>
<li className="cursor-pointer hover:text-action">
- <Link href={`https://ko-fi.com/factiven`}>Donate</Link>
+ <Link href={`/donate`}>Donate</Link>
</li>
</ul>
<ul className="flex flex-col gap-y-[0.7rem]">
@@ -156,10 +157,7 @@ function Footer() {
</Link>
{/* Kofi */}
- <Link
- href="https://ko-fi.com/factiven"
- className="w-6 h-6 hover:opacity-75"
- >
+ <Link href="/donate" className="w-6 h-6 hover:opacity-75">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="#fff"
diff --git a/components/videoPlayer.js b/components/videoPlayer.js
deleted file mode 100644
index f35f4f0..0000000
--- a/components/videoPlayer.js
+++ /dev/null
@@ -1,412 +0,0 @@
-import Player from "../lib/Artplayer";
-import { useEffect, useState } from "react";
-import { useAniList } from "../lib/anilist/useAnilist";
-import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
-import { useRouter } from "next/router";
-
-const fontSize = [
- {
- html: "Small",
- size: "16px",
- },
- {
- html: "Medium",
- size: "36px",
- },
- {
- html: "Large",
- size: "56px",
- },
-];
-
-export default function VideoPlayer({
- info,
- data,
- id,
- progress,
- session,
- aniId,
- skip,
- title,
- poster,
- proxy,
- provider,
- track,
- aniTitle,
- timeWatched,
- dub,
-}) {
- const [url, setUrl] = useState("");
- const [source, setSource] = useState([]);
- const { markProgress } = useAniList(session);
-
- const router = useRouter();
-
- const [resolution, setResolution] = useState("auto");
- const [subSize, setSubSize] = useState({ size: "16px", html: "Small" });
- const [defSize, setDefSize] = useState();
- const [subtitle, setSubtitle] = useState();
- const [defSub, setDefSub] = useState();
-
- const [autoPlay, setAutoPlay] = useState(false);
-
- useEffect(() => {
- const resol = localStorage.getItem("quality");
- const sub = JSON.parse(localStorage.getItem("subSize"));
- if (resol) {
- setResolution(resol);
- }
-
- if (provider === "zoro") {
- const size = fontSize.map((i) => {
- const isDefault = !sub ? i.html === "Small" : i.html === sub?.html;
- return {
- ...(isDefault && { default: true }),
- html: i.html,
- size: i.size,
- };
- });
-
- const defSize = size?.find((i) => i?.default === true);
- setDefSize(defSize);
- setSubSize(size);
- }
-
- async function compiler() {
- try {
- const referer = JSON.stringify(data?.headers);
- const source = data.sources.map((items) => {
- const isDefault =
- provider !== "gogoanime"
- ? items.quality === "default" || items.quality === "auto"
- : resolution === "auto"
- ? items.quality === "default" || items.quality === "auto"
- : items.quality === resolution;
- return {
- ...(isDefault && { default: true }),
- html: items.quality === "default" ? "adaptive" : items.quality,
- url: `${proxy}/proxy/m3u8/${encodeURIComponent(
- String(items.url)
- )}/${encodeURIComponent(String(referer))}`,
- };
- });
-
- const defSource = source?.find((i) => i?.default === true);
-
- if (defSource) {
- setUrl(defSource.url);
- }
-
- if (provider === "zoro") {
- const subtitle = data?.subtitles
- .filter((subtitle) => subtitle.lang !== "Thumbnails")
- .map((subtitle) => {
- const isEnglish = subtitle.lang === "English";
- return {
- ...(isEnglish && { default: true }),
- url: subtitle.url,
- html: `${subtitle.lang}`,
- };
- });
-
- const defSub = data?.subtitles.find((i) => i.lang === "English");
-
- setDefSub(defSub?.url);
-
- setSubtitle(subtitle);
- }
-
- setSource(source);
- } catch (error) {
- console.error(error);
- }
- }
- compiler();
- }, [data, resolution]);
-
- return (
- <>
- {url && (
- <Player
- key={url}
- option={{
- url: `${url}`,
- title: `${title}`,
- autoplay: true,
- screenshot: true,
- moreVideoAttr: {
- crossOrigin: "anonymous",
- },
- poster: poster ? poster : "",
- ...(provider !== "gogoanime" && {
- plugins: [
- artplayerPluginHlsQuality({
- // Show quality in setting
- setting: true,
-
- // Get the resolution text from level
- getResolution: (level) => level.height + "P",
-
- // I18n
- title: "Quality",
- auto: "Auto",
- }),
- ],
- }),
- ...(provider === "zoro" && {
- subtitle: {
- url: `${defSub}`,
- // type: "vtt",
- encoding: "utf-8",
- default: true,
- name: "English",
- escape: false,
- style: {
- color: "#FFFF",
- fontSize: `${defSize?.size}`,
- fontFamily: localStorage.getItem("font")
- ? localStorage.getItem("font")
- : "Arial",
- textShadow: localStorage.getItem("subShadow")
- ? JSON.parse(localStorage.getItem("subShadow")).value
- : "0px 0px 10px #000000",
- },
- },
- }),
- }}
- id={aniId}
- res={resolution}
- quality={source}
- subSize={subSize}
- subtitles={subtitle}
- provider={provider}
- track={track}
- autoplay={autoPlay}
- setautoplay={setAutoPlay}
- style={{
- width: "100%",
- height: "100%",
- margin: "0 auto 0",
- }}
- getInstance={(art) => {
- art.on("ready", () => {
- const seek = art.storage.get(id);
- const seekTime = seek?.timeWatched || 0;
- const duration = art.duration;
- const percentage = seekTime / duration;
- const percentagedb = timeWatched / duration;
-
- if (subSize) {
- art.subtitle.style.fontSize = subSize?.size;
- }
-
- if (percentage >= 0.9 || percentagedb >= 0.9) {
- art.currentTime = 0;
- console.log("Video started from the beginning");
- } else if (timeWatched) {
- art.currentTime = timeWatched;
- } else {
- art.currentTime = seekTime;
- }
- });
-
- let marked = 0;
-
- art.on("video:playing", () => {
- if (!session) return;
- const intervalId = setInterval(async () => {
- const resp = await fetch("/api/user/update/episode", {
- method: "PUT",
- body: JSON.stringify({
- name: session?.user?.name,
- id: String(aniId),
- watchId: id,
- title: track.playing?.title || aniTitle,
- aniTitle: aniTitle,
- image: track.playing?.image || info?.coverImage?.extraLarge,
- number: Number(progress),
- duration: art.duration,
- timeWatched: art.currentTime,
- provider: provider,
- nextId: track.next?.id,
- nextNumber: Number(track.next?.number),
- dub: dub ? true : false,
- }),
- });
- // console.log("updating db", { track });
- }, 5000);
-
- art.on("video:pause", () => {
- clearInterval(intervalId);
- });
-
- art.on("video:ended", () => {
- clearInterval(intervalId);
- });
-
- art.on("destroy", () => {
- clearInterval(intervalId);
- // console.log("clearing interval");
- });
- });
-
- art.on("video:playing", () => {
- const interval = setInterval(async () => {
- art.storage.set(id, {
- aniId: String(aniId),
- watchId: id,
- title: track?.playing?.title || aniTitle,
- aniTitle: aniTitle,
- image: track?.playing?.image || info?.coverImage?.extraLarge,
- episode: Number(progress),
- duration: art.duration,
- timeWatched: art.currentTime,
- provider: provider,
- nextId: track?.next?.id,
- nextNumber: track?.next?.number,
- dub: dub ? true : false,
- createdAt: new Date().toISOString(),
- });
- }, 5000);
-
- art.on("video:pause", () => {
- clearInterval(interval);
- });
-
- art.on("video:ended", () => {
- clearInterval(interval);
- });
-
- art.on("destroy", () => {
- clearInterval(interval);
- });
- });
-
- art.on("resize", () => {
- art.subtitle.style({
- fontSize: art.height * 0.05 + "px",
- });
- });
-
- art.on("video:timeupdate", async () => {
- if (!session) return;
-
- var currentTime = art.currentTime;
- const duration = art.duration;
- const percentage = currentTime / duration;
-
- if (percentage >= 0.9) {
- // use >= instead of >
- if (marked < 1) {
- marked = 1;
- markProgress(aniId, progress);
- }
- }
- });
-
- art.on("video:ended", () => {
- if (!track?.next) return;
- if (localStorage.getItem("autoplay") === "true") {
- art.controls.add({
- name: "next-button",
- position: "top",
- html: '<div class="vid-con"><button class="next-button progress">Play Next</button></div>',
- click: function (...args) {
- if (track?.next) {
- router.push(
- `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent(
- track?.next?.id
- )}&num=${track?.next?.number}${
- dub ? `&dub=${dub}` : ""
- }`
- );
- }
- },
- });
-
- const button = document.querySelector(".next-button");
-
- function stopTimeout() {
- clearTimeout(timeoutId);
- button.classList.remove("progress");
- }
-
- let timeoutId = setTimeout(() => {
- art.controls.remove("next-button");
- if (track?.next) {
- router.push(
- `/en/anime/watch/${aniId}/${provider}?id=${encodeURIComponent(
- track?.next?.id
- )}&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}`
- );
- }
- }, 7000);
-
- button.addEventListener("mouseover", stopTimeout);
- }
- });
-
- art.on("video:timeupdate", () => {
- var currentTime = art.currentTime;
- // console.log(art.currentTime);
-
- if (
- skip?.op &&
- currentTime >= skip.op.interval.startTime &&
- currentTime <= skip.op.interval.endTime
- ) {
- // Add the layer if it's not already added
- if (!art.controls["op"]) {
- // Remove the other control if it's already added
- if (art.controls["ed"]) {
- art.controls.remove("ed");
- }
-
- // Add the control
- art.controls.add({
- name: "op",
- position: "top",
- html: '<button class="skip-button">Skip Opening</button>',
- click: function (...args) {
- art.seek = skip.op.interval.endTime;
- },
- });
- }
- } else if (
- skip?.ed &&
- currentTime >= skip.ed.interval.startTime &&
- currentTime <= skip.ed.interval.endTime
- ) {
- // Add the layer if it's not already added
- if (!art.controls["ed"]) {
- // Remove the other control if it's already added
- if (art.controls["op"]) {
- art.controls.remove("op");
- }
-
- // Add the control
- art.controls.add({
- name: "ed",
- position: "top",
- html: '<button class="skip-button">Skip Ending</button>',
- click: function (...args) {
- art.seek = skip.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/watch/player/artplayer.js b/components/watch/player/artplayer.js
new file mode 100644
index 0000000..4eb766d
--- /dev/null
+++ b/components/watch/player/artplayer.js
@@ -0,0 +1,325 @@
+import { useEffect, useRef } from "react";
+import Artplayer from "artplayer";
+import Hls from "hls.js";
+import { useWatchProvider } from "../../../lib/hooks/watchPageProvider";
+import { seekBackward, seekForward } from "./component/overlay";
+import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
+
+export default function NewPlayer({
+ playerRef,
+ option,
+ getInstance,
+ provider,
+ defSub,
+ defSize,
+ subtitles,
+ subSize,
+ res,
+ quality,
+ ...rest
+}) {
+ const artRef = useRef(null);
+ const { setTheaterMode, setPlayerState, setAutoPlay } = useWatchProvider();
+
+ function playM3u8(video, url, art) {
+ if (Hls.isSupported()) {
+ if (art.hls) art.hls.destroy();
+ const hls = new Hls();
+ hls.loadSource(url);
+ hls.attachMedia(video);
+ art.hls = hls;
+ art.on("destroy", () => hls.destroy());
+ } else if (video.canPlayType("application/vnd.apple.mpegurl")) {
+ video.src = url;
+ } else {
+ art.notice.show = "Unsupported playback format: m3u8";
+ }
+ }
+
+ useEffect(() => {
+ const art = new Artplayer({
+ ...option,
+ container: artRef.current,
+ type: "m3u8",
+ customType: {
+ m3u8: playM3u8,
+ },
+ ...(provider === "zoro" && {
+ subtitle: {
+ url: `${defSub}`,
+ // type: "vtt",
+ encoding: "utf-8",
+ default: true,
+ name: "English",
+ escape: false,
+ style: {
+ color: "#FFFF",
+ fontSize: `${defSize?.size}`,
+ fontFamily: localStorage.getItem("font")
+ ? localStorage.getItem("font")
+ : "Arial",
+ textShadow: localStorage.getItem("subShadow")
+ ? JSON.parse(localStorage.getItem("subShadow")).value
+ : "0px 0px 10px #000000",
+ },
+ },
+ }),
+
+ plugins: [
+ artplayerPluginHlsQuality({
+ // Show quality in setting
+ setting: true,
+
+ // Get the resolution text from level
+ getResolution: (level) => level.height + "P",
+
+ // I18n
+ title: "Quality",
+ auto: "Auto",
+ }),
+ ],
+
+ settings: [
+ // provider === "gogoanime" &&
+ {
+ html: "Autoplay Next",
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M4.05 16.975q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Zm10 0q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Z"></path></svg>',
+ tooltip: "ON/OFF",
+ switch: localStorage.getItem("autoplay") === "true" ? true : false,
+ onSwitch: function (item) {
+ // setPlayNext(!item.switch);
+ localStorage.setItem("autoplay", !item.switch);
+ return !item.switch;
+ },
+ },
+ {
+ html: "Autoplay Video",
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M4.05 16.975q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Zm10 0q-.5.35-1.025.05t-.525-.9v-8.25q0-.6.525-.888t1.025.038l6.2 4.15q.45.3.45.825t-.45.825l-6.2 4.15Z"></path></svg>',
+ // icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M5.59 7.41L7 6l6 6l-6 6l-1.41-1.41L10.17 12L5.59 7.41m6 0L13 6l6 6l-6 6l-1.41-1.41L16.17 12l-4.58-4.59Z"></path></svg>',
+ tooltip: "ON/OFF",
+ switch:
+ localStorage.getItem("autoplay_video") === "true" ? true : false,
+ onSwitch: function (item) {
+ setAutoPlay(!item.switch);
+ localStorage.setItem("autoplay_video", !item.switch);
+ return !item.switch;
+ },
+ },
+ {
+ html: "Alternative Quality",
+ width: 250,
+ tooltip: `${res}`,
+ selector: quality?.alt,
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 512 512"><path fill="currentColor" d="M381.25 112a48 48 0 0 0-90.5 0H48v32h242.75a48 48 0 0 0 90.5 0H464v-32ZM176 208a48.09 48.09 0 0 0-45.25 32H48v32h82.75a48 48 0 0 0 90.5 0H464v-32H221.25A48.09 48.09 0 0 0 176 208Zm160 128a48.09 48.09 0 0 0-45.25 32H48v32h242.75a48 48 0 0 0 90.5 0H464v-32h-82.75A48.09 48.09 0 0 0 336 336Z"></path></svg>',
+ onSelect: function (item) {
+ art.switchQuality(item.url, item.html);
+ localStorage.setItem("quality", item.html);
+ return item.html;
+ },
+ },
+ {
+ html: "Server",
+ width: 250,
+ tooltip: `${quality?.server[0].html}`,
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 32 32"><path fill="currentColor" d="m24.6 24.4l2.6 2.6l-2.6 2.6L26 31l4-4l-4-4zm-2.2 0L19.8 27l2.6 2.6L21 31l-4-4l4-4z"></path><circle cx="11" cy="8" r="1" fill="currentColor"></circle><circle cx="11" cy="16" r="1" fill="currentColor"></circle><circle cx="11" cy="24" r="1" fill="currentColor"></circle><path fill="currentColor" d="M24 3H8c-1.1 0-2 .9-2 2v22c0 1.1.9 2 2 2h7v-2H8v-6h18V5c0-1.1-.9-2-2-2zm0 16H8v-6h16v6zm0-8H8V5h16v6z"></path></svg>',
+ selector: quality?.server,
+ onSelect: function (item) {
+ art.switchQuality(item.url, item.html);
+ localStorage.setItem("quality", item.html);
+ return item.html;
+ },
+ },
+ provider === "zoro" && {
+ html: "Subtitles",
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24"><path fill="currentColor" d="M4 20q-.825 0-1.413-.588T2 18V6q0-.825.588-1.413T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.588 1.413T20 20H4Zm2-4h8v-2H6v2Zm10 0h2v-2h-2v2ZM6 12h2v-2H6v2Zm4 0h8v-2h-8v2Z"></path></svg>',
+ width: 300,
+ tooltip: "Settings",
+ selector: [
+ {
+ html: "Display",
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M480.169-341.796q65.754 0 111.894-46.31 46.141-46.309 46.141-112.063t-46.31-111.894q-46.309-46.141-112.063-46.141t-111.894 46.31q-46.141 46.309-46.141 112.063t46.31 111.894q46.309 46.141 112.063 46.141zm-.371-48.307q-45.875 0-77.785-32.112-31.91-32.112-31.91-77.987 0-45.875 32.112-77.785 32.112-31.91 77.987-31.91 45.875 0 77.785 32.112 31.91 32.112 31.91 77.987 0 45.875-32.112 77.785-32.112 31.91-77.987 31.91zm.226 170.102q-130.921 0-239.6-69.821-108.679-69.82-167.556-186.476-2.687-4.574-3.892-10.811Q67.77-493.347 67.77-500t1.205-12.891q1.205-6.237 3.892-10.811Q131.745-640.358 240.4-710.178q108.655-69.821 239.576-69.821t239.6 69.821q108.679 69.82 167.556 186.476 2.687 4.574 3.892 10.811 1.205 6.238 1.205 12.891t-1.205 12.891q-1.205 6.237-3.892 10.811Q828.255-359.642 719.6-289.822q-108.655 69.821-239.576 69.821zM480-500zm-.112 229.744q117.163 0 215.048-62.347Q792.821-394.949 844.308-500q-51.487-105.051-149.26-167.397-97.772-62.347-214.936-62.347-117.163 0-215.048 62.347Q167.179-605.051 115.282-500q51.897 105.051 149.67 167.397 97.772 62.347 214.936 62.347z"></path></svg>',
+ tooltip: "Show",
+ switch: true,
+ onSwitch: function (item) {
+ item.tooltip = item.switch ? "Hide" : "Show";
+ art.subtitle.show = !item.switch;
+ return !item.switch;
+ },
+ },
+ {
+ html: "Font Size",
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M619.861-177.694q-15.655 0-26.475-10.918-10.821-10.918-10.821-26.516v-492.309H415.128q-15.598 0-26.516-10.959-10.918-10.959-10.918-26.615 0-15.655 10.918-26.475 10.918-10.82 26.516-10.82h409.744q15.598 0 26.516 10.958 10.918 10.959 10.918 26.615 0 15.656-10.918 26.476-10.918 10.82-26.516 10.82H657.435v492.309q0 15.598-10.959 26.516-10.959 10.918-26.615 10.918zm-360 0q-15.655 0-26.475-10.918-10.821-10.918-10.821-26.516v-292.309h-87.437q-15.598 0-26.516-10.959-10.918-10.959-10.918-26.615 0-15.655 10.918-26.475 10.918-10.82 26.516-10.82h249.744q15.598 0 26.516 10.958 10.918 10.959 10.918 26.615 0 15.656-10.918 26.476-10.918 10.82-26.516 10.82h-87.437v292.309q0 15.598-10.959 26.516-10.959 10.918-26.615 10.918z"></path></svg>',
+ selector: subSize,
+ onSelect: function (item) {
+ if (item.html === "Small") {
+ art.subtitle.style({ fontSize: "16px" });
+ localStorage.setItem(
+ "subSize",
+ JSON.stringify({
+ size: "16px",
+ html: "Small",
+ })
+ );
+ } else if (item.html === "Medium") {
+ art.subtitle.style({ fontSize: "36px" });
+ localStorage.setItem(
+ "subSize",
+ JSON.stringify({
+ size: "36px",
+ html: "Medium",
+ })
+ );
+ } else if (item.html === "Large") {
+ art.subtitle.style({ fontSize: "56px" });
+ localStorage.setItem(
+ "subSize",
+ JSON.stringify({
+ size: "56px",
+ html: "Large",
+ })
+ );
+ }
+ },
+ },
+ {
+ html: "Language",
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="35" height="26" viewBox="0 -960 960 960"><path d="M528.282-110.771q-21.744 0-31.308-14.013t-2.205-34.295l135.952-359.307q5.304-14.793 20.292-25.126 14.988-10.334 31.152-10.334 15.398 0 30.85 10.388 15.451 10.387 20.932 25.125l137.128 357.485q8.025 20.949-1.83 35.513-9.855 14.564-33.24 14.564-10.366 0-19.392-6.616-9.025-6.615-12.72-16.242l-30.997-91.808H594.769l-33.381 91.869q-3.645 9.181-13.148 15.989-9.504 6.808-19.958 6.808zm87.871-179.281h131.64l-64.615-180.717h-2.41l-64.615 180.717zM302.104-608.384q14.406 25.624 31.074 48.184 16.669 22.559 37.643 47.021 41.333-44.128 68.628-90.461t46.038-97.897H111.499q-15.674 0-26.278-10.615-10.603-10.616-10.603-26.308t10.615-26.307q10.616-10.616 26.308-10.616h221.537v-36.923q0-15.692 10.615-26.307 10.616-10.616 26.308-10.616t26.307 10.616q10.616 10.615 10.616 26.307v36.923h221.537q15.692 0 26.307 10.616 10.616 10.615 10.616 26.307 0 15.692-10.616 26.308-10.615 10.615-26.307 10.615h-69.088q-19.912 64.153-53.237 125.74-33.325 61.588-82.341 116.412l89.384 90.974-27.692 75.179-115.486-112.922-158.948 158.947q-10.615 10.616-25.667 10.616-15.051 0-25.666-11.026-11.026-10.615-11.026-25.666 0-15.052 11.026-26.077l161.614-161.358q-24.666-28.308-45.551-57.307-20.884-29-37.756-60.103-10.641-19.871-1.346-34.717t33.038-14.846q9.088 0 18.429 5.73 9.34 5.731 13.956 13.577z"></path></svg>',
+ tooltip: "English",
+ selector: [...subtitles],
+ onSelect: function (item) {
+ art.subtitle.switch(item.url, {
+ name: item.html,
+ });
+ return item.html;
+ },
+ },
+ {
+ html: "Font Family",
+ tooltip: localStorage.getItem("font")
+ ? localStorage.getItem("font")
+ : "Arial",
+ selector: [
+ { html: "Arial" },
+ { html: "Comic Sans MS" },
+ { html: "Verdana" },
+ { html: "Tahoma" },
+ { html: "Trebuchet MS" },
+ { html: "Times New Roman" },
+ { html: "Georgia" },
+ { html: "Impact " },
+ { html: "Andalé Mono" },
+ { html: "Palatino" },
+ { html: "Baskerville" },
+ { html: "Garamond" },
+ { html: "Courier New" },
+ { html: "Brush Script MT" },
+ ],
+ onSelect: function (item) {
+ art.subtitle.style({ fontFamily: item.html });
+ localStorage.setItem("font", item.html);
+ return item.html;
+ },
+ },
+ {
+ html: "Font Shadow",
+ tooltip: localStorage.getItem("subShadow")
+ ? JSON.parse(localStorage.getItem("subShadow")).shadow
+ : "Default",
+ selector: [
+ { html: "None", value: "none" },
+ {
+ html: "Uniform",
+ value:
+ "2px 2px 0px #000, -2px -2px 0px #000, 2px -2px 0px #000, -2px 2px 0px #000",
+ },
+ { html: "Raised", value: "-1px 2px 3px rgba(0, 0, 0, 1)" },
+ { html: "Depressed", value: "-2px -3px 3px rgba(0, 0, 0, 1)" },
+ { html: "Glow", value: "0 0 10px rgba(0, 0, 0, 0.8)" },
+ {
+ html: "Block",
+ value:
+ "-3px 3px 4px rgba(0, 0, 0, 1),2px 2px 4px rgba(0, 0, 0, 1),1px -1px 3px rgba(0, 0, 0, 1),-3px -2px 4px rgba(0, 0, 0, 1)",
+ },
+ ],
+ onSelect: function (item) {
+ art.subtitle.style({ textShadow: item.value });
+ localStorage.setItem(
+ "subShadow",
+ JSON.stringify({ shadow: item.html, value: item.value })
+ );
+ return item.html;
+ },
+ },
+ ],
+ },
+ ].filter(Boolean),
+ controls: [
+ {
+ name: "theater-button",
+ index: 11,
+ position: "right",
+ tooltip: "Theater (t)",
+ html: '<p class="theater"><svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M19 3H1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 12H2V5h16v10z"></path></svg></p>',
+ click: function (...args) {
+ setPlayerState((prev) => ({
+ ...prev,
+ currentTime: art.currentTime,
+ isPlaying: art.playing,
+ }));
+ setTheaterMode((prev) => !prev);
+ },
+ },
+ seekBackward,
+ seekForward,
+ ],
+ });
+
+ playerRef.current = art;
+
+ art.events.proxy(document, "keydown", (event) => {
+ // Check if the focus is on an input field or textarea
+ const isInputFocused =
+ document.activeElement.tagName === "INPUT" ||
+ document.activeElement.tagName === "TEXTAREA";
+
+ if (!isInputFocused) {
+ if (event.key === "f" || event.key === "F") {
+ art.fullscreen = !art.fullscreen;
+ }
+
+ if (event.key === "t" || event.key === "T") {
+ setPlayerState((prev) => ({
+ ...prev,
+ currentTime: art.currentTime,
+ isPlaying: art.playing,
+ }));
+ setTheaterMode((prev) => !prev);
+ }
+ }
+ });
+
+ art.events.proxy(document, "keypress", (event) => {
+ // Check if the focus is on an input field or textarea
+ const isInputFocused =
+ document.activeElement.tagName === "INPUT" ||
+ document.activeElement.tagName === "TEXTAREA";
+
+ if (!isInputFocused && event.code === "Space") {
+ event.preventDefault();
+ art.playing ? art.pause() : art.play();
+ }
+ });
+
+ if (getInstance && typeof getInstance === "function") {
+ getInstance(art);
+ }
+
+ return () => {
+ if (art && art.destroy) {
+ art.destroy(false);
+ }
+ };
+ }, []);
+
+ return <div ref={artRef} {...rest}></div>;
+}
diff --git a/components/watch/player/component/controls/quality.js b/components/watch/player/component/controls/quality.js
new file mode 100644
index 0000000..08dbd0e
--- /dev/null
+++ b/components/watch/player/component/controls/quality.js
@@ -0,0 +1,15 @@
+import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
+
+export const QualityPlugins = [
+ artplayerPluginHlsQuality({
+ // Show quality in setting
+ setting: true,
+
+ // Get the resolution text from level
+ getResolution: (level) => level.height + "P",
+
+ // I18n
+ title: "Quality",
+ auto: "Auto",
+ }),
+];
diff --git a/components/watch/player/component/controls/subtitle.js b/components/watch/player/component/controls/subtitle.js
new file mode 100644
index 0000000..02075f7
--- /dev/null
+++ b/components/watch/player/component/controls/subtitle.js
@@ -0,0 +1,3 @@
+import { useState } from "react";
+
+export default function getSubtitles() {}
diff --git a/components/watch/player/component/overlay.js b/components/watch/player/component/overlay.js
new file mode 100644
index 0000000..1d5ac27
--- /dev/null
+++ b/components/watch/player/component/overlay.js
@@ -0,0 +1,57 @@
+/**
+ * @type {import("artplayer/types/icons".Icons)}
+ */
+export const icons = {
+ screenshot:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 20 20"><path fill="currentColor" d="M10 8a3 3 0 1 0 0 6a3 3 0 0 0 0-6zm8-3h-2.4a.888.888 0 0 1-.789-.57l-.621-1.861A.89.89 0 0 0 13.4 2H6.6c-.33 0-.686.256-.789.568L5.189 4.43A.889.889 0 0 1 4.4 5H2C.9 5 0 5.9 0 7v9c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-8 11a5 5 0 0 1-5-5a5 5 0 1 1 10 0a5 5 0 0 1-5 5zm7.5-7.8a.7.7 0 1 1 0-1.4a.7.7 0 0 1 0 1.4z"></path></svg>',
+ play: '<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 20 20"><path fill="currentColor" d="M15 10.001c0 .299-.305.514-.305.514l-8.561 5.303C5.51 16.227 5 15.924 5 15.149V4.852c0-.777.51-1.078 1.135-.67l8.561 5.305c-.001 0 .304.215.304.514z"></path></svg>',
+ pause:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 20 20"><path fill="currentColor" d="M15 3h-2c-.553 0-1 .048-1 .6v12.8c0 .552.447.6 1 .6h2c.553 0 1-.048 1-.6V3.6c0-.552-.447-.6-1-.6zM7 3H5c-.553 0-1 .048-1 .6v12.8c0 .552.447.6 1 .6h2c.553 0 1-.048 1-.6V3.6c0-.552-.447-.6-1-.6z"></path></svg>',
+ volume:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M19 13.805c0 .657-.538 1.195-1.195 1.195H1.533c-.88 0-.982-.371-.229-.822l16.323-9.055C18.382 4.67 19 5.019 19 5.9v7.905z"></path></svg>',
+ fullscreenOff:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M3.28 2.22a.75.75 0 0 0-1.06 1.06L5.44 6.5H2.75a.75.75 0 0 0 0 1.5h4.5A.75.75 0 0 0 8 7.25v-4.5a.75.75 0 0 0-1.5 0v2.69L3.28 2.22Zm10.22.53a.75.75 0 0 0-1.5 0v4.5c0 .414.336.75.75.75h4.5a.75.75 0 0 0 0-1.5h-2.69l3.22-3.22a.75.75 0 0 0-1.06-1.06L13.5 5.44V2.75ZM3.28 17.78l3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 1 0 1.06 1.06Zm10.22-3.22l3.22 3.22a.75.75 0 1 0 1.06-1.06l-3.22-3.22h2.69a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0-.75.75v4.5a.75.75 0 0 0 1.5 0v-2.69Z"></path></svg>',
+ fullscreenOn:
+ '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="m13.28 7.78l3.22-3.22v2.69a.75.75 0 0 0 1.5 0v-4.5a.75.75 0 0 0-.75-.75h-4.5a.75.75 0 0 0 0 1.5h2.69l-3.22 3.22a.75.75 0 0 0 1.06 1.06ZM2 17.25v-4.5a.75.75 0 0 1 1.5 0v2.69l3.22-3.22a.75.75 0 0 1 1.06 1.06L4.56 16.5h2.69a.75.75 0 0 1 0 1.5h-4.5a.747.747 0 0 1-.75-.75Zm10.22-3.97l3.22 3.22h-2.69a.75.75 0 0 0 0 1.5h4.5a.747.747 0 0 0 .75-.75v-4.5a.75.75 0 0 0-1.5 0v2.69l-3.22-3.22a.75.75 0 1 0-1.06 1.06ZM3.5 4.56l3.22 3.22a.75.75 0 0 0 1.06-1.06L4.56 3.5h2.69a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0-.75.75v4.5a.75.75 0 0 0 1.5 0V4.56Z"></path></svg>',
+};
+
+export const backButton = {
+ name: "back-button",
+ index: 10,
+ position: "top",
+ html: "<div class='parent-player-title'><div></div><div className='flex gap-2'><p className='pt-1'><ChevronLeftIcon className='w-7 h-7'/></p><div class='flex flex-col text-white'><p className='font-outfit font-bold text-2xl'>Komi-san wa, Komyushou desu.</p><p className=''>Episode 1</p></div></div></div>",
+ // tooltip: "Your Button",
+ click: function (...args) {
+ console.info("click", args);
+ },
+ mounted: function (...args) {
+ console.info("mounted", args);
+ },
+};
+
+export const seekBackward = {
+ index: 10,
+ name: "fast-rewind",
+ position: "left",
+ html: '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M17.959 4.571L10.756 9.52s-.279.201-.279.481s.279.479.279.479l7.203 4.951c.572.38 1.041.099 1.041-.626V5.196c0-.727-.469-1.008-1.041-.625zm-9.076 0L1.68 9.52s-.279.201-.279.481s.279.479.279.479l7.203 4.951c.572.381 1.041.1 1.041-.625v-9.61c0-.727-.469-1.008-1.041-.625z"></path></svg>',
+ tooltip: "Backward 5s",
+ click: function () {
+ art.backward = 5;
+ },
+};
+
+export const seekForward = {
+ index: 11,
+ name: "fast-forward",
+ position: "left",
+ html: '<svg xmlns="http://www.w3.org/2000/svg" width="20px" height="20px" viewBox="0 0 20 20"><path fill="currentColor" d="M9.244 9.52L2.041 4.571C1.469 4.188 1 4.469 1 5.196v9.609c0 .725.469 1.006 1.041.625l7.203-4.951s.279-.199.279-.478c0-.28-.279-.481-.279-.481zm9.356.481c0 .279-.279.478-.279.478l-7.203 4.951c-.572.381-1.041.1-1.041-.625V5.196c0-.727.469-1.008 1.041-.625L18.32 9.52s.28.201.28.481z"></path></svg>',
+ tooltip: "Forward 5s",
+ click: function () {
+ art.forward = 5;
+ },
+};
+
+// /**
+// * @type {import("artplayer/types/component").ComponentOption}
+// */
+// export const
diff --git a/components/watch/player/playerComponent.js b/components/watch/player/playerComponent.js
new file mode 100644
index 0000000..d498384
--- /dev/null
+++ b/components/watch/player/playerComponent.js
@@ -0,0 +1,478 @@
+import React, { useEffect, useState } from "react";
+import NewPlayer from "./artplayer";
+import { icons } from "./component/overlay";
+import { useWatchProvider } from "../../../lib/hooks/watchPageProvider";
+import { useRouter } from "next/router";
+import { useAniList } from "../../../lib/anilist/useAnilist";
+
+export function calculateAspectRatio(width, height) {
+ const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
+ const divisor = gcd(width, height);
+ const aspectRatio = `${width / divisor}/${height / divisor}`;
+ return aspectRatio;
+}
+
+const fontSize = [
+ {
+ html: "Small",
+ size: "16px",
+ },
+ {
+ html: "Medium",
+ size: "36px",
+ },
+ {
+ html: "Large",
+ size: "56px",
+ },
+];
+
+export default function PlayerComponent({
+ playerRef,
+ session,
+ id,
+ info,
+ watchId,
+ proxy,
+ dub,
+ timeWatched,
+ skip,
+ track,
+ data,
+ provider,
+ className,
+}) {
+ const {
+ aspectRatio,
+ setAspectRatio,
+ playerState,
+ setPlayerState,
+ autoplay,
+ marked,
+ setMarked,
+ } = useWatchProvider();
+
+ const router = useRouter();
+
+ const { markProgress } = useAniList(session);
+
+ const [url, setUrl] = useState("");
+ const [resolution, setResolution] = useState("auto");
+ const [source, setSource] = useState([]);
+ const [subSize, setSubSize] = useState({ size: "16px", html: "Small" });
+ const [defSize, setDefSize] = useState();
+ const [subtitle, setSubtitle] = useState();
+ const [defSub, setDefSub] = useState();
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ setLoading(true);
+ const resol = localStorage.getItem("quality");
+ const sub = JSON.parse(localStorage.getItem("subSize"));
+ if (resol) {
+ setResolution(resol);
+ }
+
+ if (provider === "zoro") {
+ const size = fontSize.map((i) => {
+ const isDefault = !sub ? i.html === "Small" : i.html === sub?.html;
+ return {
+ ...(isDefault && { default: true }),
+ html: i.html,
+ size: i.size,
+ };
+ });
+
+ const defSize = size?.find((i) => i?.default === true);
+ setDefSize(defSize);
+ setSubSize(size);
+ }
+
+ async function compiler() {
+ try {
+ const referer = JSON.stringify(data?.headers);
+ const source = data?.sources?.map((items) => {
+ const isDefault =
+ provider !== "gogoanime"
+ ? items.quality === "default" || items.quality === "auto"
+ : resolution === "auto"
+ ? items.quality === "default" || items.quality === "auto"
+ : items.quality === resolution;
+ return {
+ ...(isDefault && { default: true }),
+ html: items.quality === "default" ? "main" : items.quality,
+ url: `${proxy}/proxy/m3u8/${encodeURIComponent(
+ String(items.url)
+ )}/${encodeURIComponent(String(referer))}`,
+ };
+ });
+
+ const defSource = source?.find((i) => i?.default === true);
+
+ if (defSource) {
+ setUrl(defSource.url);
+ }
+
+ if (provider === "zoro") {
+ const subtitle = data?.subtitles
+ .filter((subtitle) => subtitle.lang !== "Thumbnails")
+ .map((subtitle) => {
+ const isEnglish = subtitle.lang === "English";
+ return {
+ ...(isEnglish && { default: true }),
+ url: subtitle.url,
+ html: `${subtitle.lang}`,
+ };
+ });
+
+ const defSub = data?.subtitles.find((i) => i.lang === "English");
+
+ setDefSub(defSub?.url);
+
+ setSubtitle(subtitle);
+ }
+
+ const alt = source?.filter(
+ (i) =>
+ i?.html !== "main" &&
+ i?.html !== "auto" &&
+ i?.html !== "default" &&
+ i?.html !== "backup"
+ );
+ const server = source?.filter(
+ (i) =>
+ i?.html === "main" ||
+ i?.html === "auto" ||
+ i?.html === "default" ||
+ i?.html === "backup"
+ );
+
+ setSource({ alt, server });
+ setLoading(false);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ compiler();
+
+ return () => {
+ setUrl("");
+ setSource([]);
+ setSubtitle([]);
+ setLoading(true);
+ };
+ }, [provider, data]);
+
+ /**
+ * @param {import("artplayer")} art
+ */
+ function getInstance(art) {
+ art.on("ready", () => {
+ const autoplay = localStorage.getItem("autoplay_video") || false;
+
+ if (autoplay === "true" || autoplay === true) {
+ if (playerState.currentTime === 0) {
+ art.play();
+ } else {
+ if (playerState.isPlaying) {
+ art.play();
+ } else {
+ art.pause();
+ }
+ }
+ } else {
+ if (playerState.isPlaying) {
+ art.play();
+ } else {
+ art.pause();
+ }
+ }
+ art.seek = playerState.currentTime;
+ });
+
+ art.on("ready", () => {
+ if (playerState.currentTime !== 0) return;
+ const seek = art.storage.get(id);
+ const seekTime = seek?.timeWatched || 0;
+ const duration = art.duration;
+ const percentage = seekTime / duration;
+ const percentagedb = timeWatched / duration;
+
+ if (subSize) {
+ art.subtitle.style.fontSize = subSize?.size;
+ }
+
+ if (percentage >= 0.9 || percentagedb >= 0.9) {
+ art.currentTime = 0;
+ console.log("Video started from the beginning");
+ } else if (timeWatched) {
+ art.currentTime = timeWatched;
+ } else {
+ art.currentTime = seekTime;
+ }
+ });
+
+ art.on("play", () => {
+ art.notice.show = "";
+ setPlayerState({ ...playerState, isPlaying: true });
+ });
+ art.on("pause", () => {
+ art.notice.show = "";
+ setPlayerState({ ...playerState, isPlaying: false });
+ });
+
+ art.on("resize", () => {
+ art.subtitle.style({
+ fontSize: art.height * 0.05 + "px",
+ });
+ });
+
+ let mark = 0;
+
+ art.on("video:timeupdate", async () => {
+ if (!session) return;
+
+ var currentTime = art.currentTime;
+ const duration = art.duration;
+ const percentage = currentTime / duration;
+
+ if (percentage >= 0.9) {
+ // use >= instead of >
+ if (mark < 1 && marked < 1) {
+ mark = 1;
+ setMarked(1);
+ markProgress(info.id, track.playing.number);
+ }
+ }
+ });
+
+ art.on("video:playing", () => {
+ if (!session) return;
+ const intervalId = setInterval(async () => {
+ await fetch("/api/user/update/episode", {
+ method: "PUT",
+ body: JSON.stringify({
+ name: session?.user?.name,
+ id: String(info?.id),
+ watchId: watchId,
+ title:
+ track.playing?.title || info.title?.romaji || info.title?.english,
+ aniTitle: info.title?.romaji || info.title?.english,
+ image: track.playing?.img || info?.coverImage?.extraLarge,
+ number: Number(track.playing?.number),
+ duration: art.duration,
+ timeWatched: art.currentTime,
+ provider: provider,
+ nextId: track.next?.id,
+ nextNumber: Number(track.next?.number),
+ dub: dub ? true : false,
+ }),
+ });
+ // console.log("updating db", { track });
+ }, 5000);
+
+ art.on("video:pause", () => {
+ clearInterval(intervalId);
+ });
+
+ art.on("video:ended", () => {
+ clearInterval(intervalId);
+ });
+
+ art.on("destroy", () => {
+ clearInterval(intervalId);
+ // console.log("clearing interval");
+ });
+ });
+
+ art.on("video:playing", () => {
+ const interval = setInterval(async () => {
+ art.storage.set(watchId, {
+ aniId: String(info.id),
+ watchId: watchId,
+ title:
+ track.playing?.title || info.title?.romaji || info.title?.english,
+ aniTitle: info.title?.romaji || info.title?.english,
+ image: track?.playing?.img || info?.coverImage?.extraLarge,
+ episode: Number(track.playing?.number),
+ duration: art.duration,
+ timeWatched: art.currentTime,
+ provider: provider,
+ nextId: track?.next?.id,
+ nextNumber: track?.next?.number,
+ dub: dub ? true : false,
+ createdAt: new Date().toISOString(),
+ });
+ }, 5000);
+
+ art.on("video:pause", () => {
+ clearInterval(interval);
+ });
+
+ art.on("video:ended", () => {
+ clearInterval(interval);
+ });
+
+ art.on("destroy", () => {
+ clearInterval(interval);
+ });
+ });
+
+ art.on("video:loadedmetadata", () => {
+ // get raw video width and height
+ // console.log(art.video.videoWidth, art.video.videoHeight);
+ const aspect = calculateAspectRatio(
+ art.video.videoWidth,
+ art.video.videoHeight
+ );
+
+ setAspectRatio(aspect);
+ });
+
+ art.on("video:timeupdate", () => {
+ var currentTime = art.currentTime;
+ // console.log(art.currentTime);
+
+ if (
+ skip?.op &&
+ currentTime >= skip.op.interval.startTime &&
+ currentTime <= skip.op.interval.endTime
+ ) {
+ // Add the layer if it's not already added
+ if (!art.controls["op"]) {
+ // Remove the other control if it's already added
+ if (art.controls["ed"]) {
+ art.controls.remove("ed");
+ }
+
+ // Add the control
+ art.controls.add({
+ name: "op",
+ position: "top",
+ html: '<button class="skip-button">Skip Opening</button>',
+ click: function (...args) {
+ art.seek = skip.op.interval.endTime;
+ },
+ });
+ }
+ } else if (
+ skip?.ed &&
+ currentTime >= skip.ed.interval.startTime &&
+ currentTime <= skip.ed.interval.endTime
+ ) {
+ // Add the layer if it's not already added
+ if (!art.controls["ed"]) {
+ // Remove the other control if it's already added
+ if (art.controls["op"]) {
+ art.controls.remove("op");
+ }
+
+ // Add the control
+ art.controls.add({
+ name: "ed",
+ position: "top",
+ html: '<button class="skip-button">Skip Ending</button>',
+ click: function (...args) {
+ art.seek = skip.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");
+ }
+ }
+ });
+
+ art.on("video:ended", () => {
+ if (!track?.next) return;
+ if (localStorage.getItem("autoplay") === "true") {
+ art.controls.add({
+ name: "next-button",
+ position: "top",
+ html: '<div class="vid-con"><button class="next-button progress">Play Next</button></div>',
+ click: function (...args) {
+ if (track?.next) {
+ router.push(
+ `/en/anime/watch/${
+ info?.id
+ }/${provider}?id=${encodeURIComponent(track?.next?.id)}&num=${
+ track?.next?.number
+ }${dub ? `&dub=${dub}` : ""}`
+ );
+ }
+ },
+ });
+
+ const button = document.querySelector(".next-button");
+
+ function stopTimeout() {
+ clearTimeout(timeoutId);
+ button.classList.remove("progress");
+ }
+
+ let timeoutId = setTimeout(() => {
+ art.controls.remove("next-button");
+ if (track?.next) {
+ router.push(
+ `/en/anime/watch/${info?.id}/${provider}?id=${encodeURIComponent(
+ track?.next?.id
+ )}&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}`
+ );
+ }
+ }, 7000);
+
+ button.addEventListener("mouseover", stopTimeout);
+ }
+ });
+ }
+
+ /**
+ * @type {import("artplayer/types/option").Option}
+ */
+ const option = {
+ url: url,
+ title: "title",
+ autoplay: autoplay ? true : false,
+ autoSize: false,
+ fullscreen: true,
+ autoOrientation: true,
+ icons: icons,
+ setting: true,
+ screenshot: true,
+ hotkey: true,
+ };
+
+ return (
+ <div
+ id={id}
+ className={`${className} bg-black`}
+ style={{ aspectRatio: aspectRatio }}
+ >
+ <div className="w-full h-full">
+ {!loading && track && url && (
+ <NewPlayer
+ playerRef={playerRef}
+ res={resolution}
+ quality={source}
+ option={option}
+ provider={provider}
+ defSize={defSize}
+ defSub={defSub}
+ subSize={subSize}
+ subtitles={subtitle}
+ getInstance={getInstance}
+ style={{
+ width: "100%",
+ height: "100%",
+ }}
+ />
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/components/watch/player/utils/getZoroSource.js b/components/watch/player/utils/getZoroSource.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/components/watch/player/utils/getZoroSource.js
diff --git a/components/anime/watch/primary/details.js b/components/watch/primary/details.js
index f092879..32e1391 100644
--- a/components/anime/watch/primary/details.js
+++ b/components/watch/primary/details.js
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
-import { useAniList } from "../../../../lib/anilist/useAnilist";
+import { useAniList } from "../../../lib/anilist/useAnilist";
import Skeleton from "react-loading-skeleton";
-import DisqusComments from "../../../disqus";
+import DisqusComments from "../../disqus";
import Image from "next/image";
export default function Details({
@@ -17,7 +17,6 @@ export default function Details({
}) {
const [showComments, setShowComments] = useState(false);
const { markPlanning } = useAniList(session);
- const [url, setUrl] = useState(null);
function handlePlan() {
if (onList === false) {
@@ -27,14 +26,13 @@ export default function Details({
}
useEffect(() => {
- const url = window.location.href;
setShowComments(false);
- setUrl(url);
}, [id]);
return (
<div className="flex flex-col gap-2">
- <div className="px-4 pt-7 pb-4 h-full flex">
+ {/* <div className="px-4 pt-7 pb-4 h-full flex"> */}
+ <div className="pb-4 h-full flex">
<div className="aspect-[9/13] h-[240px]">
{info ? (
<Image
@@ -58,7 +56,11 @@ export default function Details({
Studios
</h2>
<div className="row-start-2">
- {info ? info.studios.edges[0].node.name : <Skeleton width={80} />}
+ {info ? (
+ info.studios?.edges[0].node.name
+ ) : (
+ <Skeleton width={80} />
+ )}
</div>
<div className="hidden xxs:grid col-start-2 place-content-end relative">
<div>
@@ -114,7 +116,8 @@ export default function Details({
</div>
</div>
</div>
- <div className="flex flex-wrap gap-3 px-4 pt-3">
+ {/* <div className="flex flex-wrap gap-3 px-4 pt-3"> */}
+ <div className="flex flex-wrap gap-3 pt-3">
{info &&
info.genres?.map((item, index) => (
<div
@@ -125,7 +128,8 @@ export default function Details({
</div>
))}
</div>
- <div className={`bg-secondary rounded-md mt-3 mx-3`}>
+ {/* <div className={`bg-secondary rounded-md mt-3 mx-3`}> */}
+ <div className={`bg-secondary rounded-md mt-3`}>
{info && (
<p
dangerouslySetInnerHTML={{ __html: description }}
@@ -135,7 +139,7 @@ export default function Details({
</div>
{/* {<div className="mt-5 px-5"></div>} */}
{!showComments && (
- <div className="w-full flex justify-center py-2 font-karla px-3 lg:px-0">
+ <div className="w-full flex justify-center py-2 font-karla lg:px-0">
<button
onClick={() => setShowComments(true)}
className={
@@ -164,14 +168,14 @@ export default function Details({
)}
{showComments && (
<div>
- {info && url && (
+ {info && (
<div className="mt-5 px-5">
<DisqusComments
key={id}
post={{
id: id,
title: info.title.romaji,
- url: url,
+ url: window.location.href,
episode: epiNumber,
name: disqus,
}}
diff --git a/components/anime/watch/secondarySide.js b/components/watch/secondary/episodeLists.js
index c9ef684..8a057ce 100644
--- a/components/anime/watch/secondarySide.js
+++ b/components/watch/secondary/episodeLists.js
@@ -2,7 +2,7 @@ import Skeleton from "react-loading-skeleton";
import Image from "next/image";
import Link from "next/link";
-export default function SecondarySide({
+export default function EpisodeLists({
info,
map,
providerId,
@@ -13,11 +13,15 @@ export default function SecondarySide({
}) {
const progress = info.mediaListEntry?.progress;
return (
- <div className="lg:w-[35%] shrink-0 w-screen">
- <h1 className="text-xl font-karla pl-4 pb-5 font-semibold">Up Next</h1>
+ <div className="w-screen lg:max-w-sm xl:max-w-xl">
+ <h1 className="text-xl font-karla pl-5 pb-5 font-semibold">Up Next</h1>
<div className="flex flex-col gap-5 lg:pl-5 py-2 scrollbar-thin px-2 scrollbar-thumb-[#313131] scrollbar-thumb-rounded-full">
{episode && episode.length > 0 ? (
- map?.some((item) => item.title && item.description) > 0 ? (
+ map?.some(
+ (item) =>
+ (item?.img || item?.image) &&
+ !item?.img?.includes("https://s4.anilist.co/")
+ ) > 0 ? (
episode.map((item) => {
const time = artStorage?.[item.id]?.timeWatched;
const duration = artStorage?.[item.id]?.duration;
@@ -41,9 +45,14 @@ export default function SecondarySide({
>
<div className="w-[43%] lg:w-[40%] h-[110px] relative rounded-lg z-40 shrink-0 overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]">
<div className="relative">
- {/* {mapData?.image && ( */}
+ {/* <div className="absolute inset-0 w-full h-full z-40" /> */}
<Image
- src={mapData?.image || info?.coverImage?.extraLarge}
+ src={
+ mapData?.img ||
+ mapData?.image ||
+ info?.coverImage?.extraLarge
+ }
+ draggable={false}
alt="Anime Cover"
width={1000}
height={1000}
@@ -88,10 +97,10 @@ export default function SecondarySide({
}`}
>
<h1 className="font-karla font-bold italic line-clamp-1">
- {mapData?.title}
+ {mapData?.title || info?.title?.romaji}
</h1>
<p className="line-clamp-2 text-xs italic font-outfit font-extralight">
- {mapData?.description}
+ {mapData?.description || `Episode ${item.number}`}
</p>
</div>
</Link>
@@ -107,7 +116,7 @@ export default function SecondarySide({
item.number
}${dub ? `&dub=${dub}` : ""}`}
key={item.id}
- className={`bg-secondary flex-center w-full h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${
+ className={`bg-secondary flex-center h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${
item.id == watchId
? "pointer-events-none ring-1 ring-action text-[#5d5d5d]"
: "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white"
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..babd576
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/components/*": ["components/*"],
+ "@/utils/*": ["utils/*"],
+ "@/lib/*": ["lib/*"],
+ "@/prisma/*": ["prisma/*"]
+ }
+ }
+}
diff --git a/lib/anify/info.js b/lib/anify/info.js
index e7d4025..08d64aa 100644
--- a/lib/anify/info.js
+++ b/lib/anify/info.js
@@ -1,5 +1,5 @@
import axios from "axios";
-import redis from "../redis";
+import { redis } from "../redis";
export async function fetchInfo(id, key) {
try {
diff --git a/lib/anify/page.js b/lib/anify/page.js
index b2b1207..65ed309 100644
--- a/lib/anify/page.js
+++ b/lib/anify/page.js
@@ -1,4 +1,4 @@
-import redis from "../redis";
+import { redis } from "../redis";
// Function to fetch new data
async function fetchData(id, providerId, chapterId, key) {
diff --git a/lib/anilist/getMedia.js b/lib/anilist/getMedia.js
index c4628ab..66bb1b0 100644
--- a/lib/anilist/getMedia.js
+++ b/lib/anilist/getMedia.js
@@ -1,9 +1,10 @@
import { useEffect, useState } from "react";
export default function GetMedia(session, stats) {
- const [media, setMedia] = useState([]);
+ const [anime, setAnime] = useState([]);
+ const [manga, setManga] = useState([]);
const [recommendations, setRecommendations] = useState([]);
- const accessToken = session?.user?.token;
+ const accessToken = session?.user?.token || null;
const username = session?.user?.name;
const status = stats || null;
@@ -12,7 +13,7 @@ export default function GetMedia(session, stats) {
method: "POST",
headers: {
"Content-Type": "application/json",
- Authorization: accessToken ? `Bearer ${accessToken}` : undefined,
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
body: JSON.stringify({ query, variables }),
});
@@ -42,7 +43,36 @@ export default function GetMedia(session, stats) {
}
}
}
- MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) {
+ anime: MediaListCollection(userName: $username, type: ANIME, status: $status, sort: UPDATED_TIME_DESC) {
+ lists {
+ status
+ name
+ entries {
+ id
+ mediaId
+ status
+ progress
+ score
+ media {
+ id
+ status
+ nextAiringEpisode {
+ timeUntilAiring
+ episode
+ }
+ title {
+ english
+ romaji
+ }
+ episodes
+ coverImage {
+ large
+ }
+ }
+ }
+ }
+ }
+ manga: MediaListCollection(userName: $username, type: MANGA, status: $status, sort: UPDATED_TIME_DESC) {
lists {
status
name
@@ -79,12 +109,13 @@ export default function GetMedia(session, stats) {
status: status?.stats,
sort: "ID_DESC",
}).then((data) => {
- setMedia(data.data.MediaListCollection.lists);
+ setAnime(data.data.anime?.lists);
+ setManga(data.data.manga?.lists);
setRecommendations(
data.data.Page.recommendations.map((i) => i.mediaRecommendation)
);
});
}, [username, accessToken, status?.stats]);
- return { media, recommendations };
+ return { anime, manga, recommendations };
}
diff --git a/lib/hooks/watchPageProvider.js b/lib/hooks/watchPageProvider.js
new file mode 100644
index 0000000..a9d707b
--- /dev/null
+++ b/lib/hooks/watchPageProvider.js
@@ -0,0 +1,41 @@
+import React, { createContext, useContext, useState } from "react";
+
+export const WatchPageContext = createContext();
+
+export const WatchPageProvider = ({ children }) => {
+ const [theaterMode, setTheaterMode] = useState(false);
+ const [aspectRatio, setAspectRatio] = useState("16/9");
+ const [playerState, setPlayerState] = useState({
+ currentTime: 0,
+ isPlaying: false,
+ });
+ const [autoplay, setAutoPlay] = useState(false);
+ const [marked, setMarked] = useState(0);
+
+ const [userData, setUserData] = useState(null);
+
+ return (
+ <WatchPageContext.Provider
+ value={{
+ theaterMode,
+ setTheaterMode,
+ aspectRatio,
+ setAspectRatio,
+ playerState,
+ setPlayerState,
+ userData,
+ setUserData,
+ autoplay,
+ setAutoPlay,
+ marked,
+ setMarked,
+ }}
+ >
+ {children}
+ </WatchPageContext.Provider>
+ );
+};
+
+export function useWatchProvider() {
+ return useContext(WatchPageContext);
+}
diff --git a/lib/redis.js b/lib/redis.js
index ed8b8c5..713b5d9 100644
--- a/lib/redis.js
+++ b/lib/redis.js
@@ -1,13 +1,36 @@
import { Redis } from "ioredis";
+import { RateLimiterRedis } from "rate-limiter-flexible";
const REDIS_URL = process.env.REDIS_URL;
let redis;
+let rateLimiterRedis;
+let rateLimitStrict;
if (REDIS_URL) {
redis = new Redis(REDIS_URL);
+ redis.on("error", (err) => {
+ console.error("Redis error: ", err);
+ });
+
+ const opt = {
+ storeClient: redis,
+ keyPrefix: "rateLimit",
+ points: 50,
+ duration: 1,
+ };
+
+ const optStrict = {
+ storeClient: redis,
+ keyPrefix: "rateLimitStrict",
+ points: 20,
+ duration: 1,
+ };
+
+ rateLimiterRedis = new RateLimiterRedis(opt);
+ rateLimitStrict = new RateLimiterRedis(optStrict);
} else {
console.warn("REDIS_URL is not defined. Redis caching will be disabled.");
}
-export default redis;
+export { redis, rateLimiterRedis, rateLimitStrict };
diff --git a/next.config.js b/next.config.js
index a354717..001a365 100644
--- a/next.config.js
+++ b/next.config.js
@@ -11,17 +11,34 @@ const withPWA = require("next-pwa")({
module.exports = withPWA({
reactStrictMode: false,
images: {
- unoptimized: true,
remotePatterns: [
{
protocol: "https",
- hostname: "s4.anilist.co",
+ hostname: "**.*.*",
+ },
+ {
+ protocol: "https",
+ hostname: "**.**.*.*",
+ },
+ {
+ protocol: "https",
+ hostname: "simkl.in",
},
],
},
distDir: process.env.BUILD_DIR || ".next",
// Uncomment this if you want to use Docker
// output: "standalone",
+ async redirects() {
+ return [
+ {
+ source: "/donate",
+ destination: "https://ko-fi.com/factiven",
+ permanent: false,
+ basePath: false,
+ },
+ ];
+ },
async headers() {
return [
{
diff --git a/package-lock.json b/package-lock.json
index acbbea4..cda86d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "moopa",
- "version": "4.0.6",
+ "version": "4.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "moopa",
- "version": "4.0.6",
+ "version": "4.1.0",
"dependencies": {
"@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
@@ -29,6 +29,7 @@
"next-safe": "^3.4.1",
"nextjs-progressbar": "^0.0.16",
"nookies": "^2.5.2",
+ "rate-limiter-flexible": "^3.0.0",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.7.1",
@@ -7455,6 +7456,11 @@
"safe-buffer": "^5.1.0"
}
},
+ "node_modules/rate-limiter-flexible": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-3.0.0.tgz",
+ "integrity": "sha512-janAJkWxWxmLka0hV+XvCTo0M8keeSeOuz8ZL33cTXrkS4ek9mQ2VJm9ri7fm03oTVth19Sfqb1ijCmo7K/vAg=="
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
diff --git a/package.json b/package.json
index 6bc2b9c..d46fe1a 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
{
"name": "moopa",
- "version": "4.0.7",
+ "version": "4.1.0",
"private": true,
"founder": "Factiven",
"scripts": {
"dev": "next dev",
"build": "next build",
"export": "next build && next export",
- "start": "next start -p $PORT",
+ "start": "next start",
"lint": "next lint"
},
"dependencies": {
@@ -32,6 +32,7 @@
"next-safe": "^3.4.1",
"nextjs-progressbar": "^0.0.16",
"nookies": "^2.5.2",
+ "rate-limiter-flexible": "^3.0.0",
"react": "18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.7.1",
diff --git a/pages/404.js b/pages/404.js
index 5b6162b..f6e609f 100644
--- a/pages/404.js
+++ b/pages/404.js
@@ -1,10 +1,9 @@
import Head from "next/head";
-import Footer from "../components/footer";
-import Navbar from "../components/navbar";
import Link from "next/link";
import { useEffect, useState } from "react";
import { parseCookies } from "nookies";
import Image from "next/image";
+import Footer from "@/components/shared/footer";
export default function Custom404() {
const [lang, setLang] = useState("en");
@@ -31,7 +30,6 @@ export default function Custom404() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/svg/c.svg" />
</Head>
- <Navbar className="bg-[#0c0d10]" />
<div className="min-h-screen w-screen flex flex-col items-center justify-center ">
<Image
width={500}
diff --git a/pages/_app.js b/pages/_app.js
index 5303b71..68b4acd 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -6,9 +6,13 @@ import "../styles/globals.css";
import "react-toastify/dist/ReactToastify.css";
import "react-loading-skeleton/dist/skeleton.css";
import { SkeletonTheme } from "react-loading-skeleton";
-import SearchPalette from "../components/searchPalette";
-import { SearchProvider } from "../lib/hooks/isOpenState";
+import SearchPalette from "@/components/searchPalette";
+import { SearchProvider } from "@/lib/hooks/isOpenState";
import Head from "next/head";
+import { WatchPageProvider } from "@/lib/hooks/watchPageProvider";
+import { ToastContainer, toast } from "react-toastify";
+import { useEffect } from "react";
+import { unixTimestampToRelativeTime } from "@/utils/getTimes";
export default function App({
Component,
@@ -16,6 +20,42 @@ export default function App({
}) {
const router = useRouter();
+ useEffect(() => {
+ async function getBroadcast() {
+ try {
+ const res = await fetch("/api/v2/admin/broadcast", {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ "X-Broadcast-Key": "get-broadcast",
+ },
+ });
+ const data = await res.json();
+ if (
+ data &&
+ data?.message !== "No broadcast" &&
+ data?.message !== "unauthorized"
+ ) {
+ toast(data.message + unixTimestampToRelativeTime(data.startAt), {
+ position: "top-center",
+ autoClose: false,
+ closeOnClick: true,
+ draggable: true,
+ theme: "colored",
+ className: "toaster",
+ style: {
+ background: "#232329",
+ color: "#fff",
+ },
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
+ getBroadcast();
+ }, []);
+
return (
<>
<Head>
@@ -26,37 +66,41 @@ export default function App({
</Head>
<SessionProvider session={session}>
<SearchProvider>
- <AnimatePresence mode="wait">
- <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32">
- <m.div
- key={`route-${router.route}`}
- transition={{ duration: 0.5 }}
- initial="initialState"
- animate="animateState"
- exit="exitState"
- variants={{
- initialState: {
- opacity: 0,
- },
- animateState: {
- opacity: 1,
- },
- exitState: {},
- }}
- className="z-50 w-screen"
- >
- <NextNProgress
- color="#FF7E2C"
- startPosition={0.3}
- stopDelayMs={200}
- height={3}
- showOnShallow={true}
- />
- <SearchPalette />
- <Component {...pageProps} />
- </m.div>
- </SkeletonTheme>
- </AnimatePresence>
+ <WatchPageProvider>
+ <AnimatePresence mode="wait">
+ <SkeletonTheme baseColor="#232329" highlightColor="#2a2a32">
+ <ToastContainer pauseOnFocusLoss={false} pauseOnHover={false} />
+ <m.div
+ key={`route-${router.route}`}
+ transition={{ duration: 0.5 }}
+ initial="initialState"
+ animate="animateState"
+ exit="exitState"
+ variants={{
+ initialState: {
+ opacity: 0,
+ },
+ animateState: {
+ opacity: 1,
+ },
+ exitState: {},
+ }}
+ className="z-50 w-screen"
+ >
+ <NextNProgress
+ color="#FF7E2C"
+ startPosition={0.3}
+ stopDelayMs={200}
+ height={3}
+ showOnShallow={true}
+ />
+
+ <SearchPalette />
+ <Component {...pageProps} />
+ </m.div>
+ </SkeletonTheme>
+ </AnimatePresence>
+ </WatchPageProvider>
</SearchProvider>
</SessionProvider>
</>
diff --git a/pages/admin/index.js b/pages/admin/index.js
new file mode 100644
index 0000000..4fdc8c2
--- /dev/null
+++ b/pages/admin/index.js
@@ -0,0 +1,263 @@
+import { getServerSession } from "next-auth";
+import { authOptions } from "pages/api/auth/[...nextauth]";
+import { useState } from "react";
+import { toast } from "react-toastify";
+
+// Define a function to convert the data
+function convertData(episodes) {
+ const convertedData = episodes.map((episode) => ({
+ episode: episode.episode,
+ title: episode?.title,
+ description: episode?.description || null,
+ img: episode?.img?.hd || episode?.img?.mobile || null, // Use hd if available, otherwise use mobile
+ }));
+
+ return convertedData;
+}
+
+export async function getServerSideProps(context) {
+ const sessions = await getServerSession(
+ context.req,
+ context.res,
+ authOptions
+ );
+
+ if (!sessions) {
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
+
+ const admin = sessions?.user?.name === process.env.ADMIN_USERNAME;
+ const api = process.env.API_URI;
+
+ if (!admin) {
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {
+ session: sessions,
+ api,
+ },
+ };
+}
+
+export default function Admin({ api }) {
+ const [id, setId] = useState();
+ const [resultData, setResultData] = useState(null);
+
+ const [query, setQuery] = useState("");
+ const [tmdbId, setTmdbId] = useState();
+ const [hasilQuery, setHasilQuery] = useState([]);
+ const [season, setSeason] = useState();
+
+ const [override, setOverride] = useState();
+
+ const [loading, setLoading] = useState(false);
+
+ const handleSearch = async () => {
+ try {
+ setLoading(true);
+ setResultData(null);
+ const res = await fetch(`${api}/meta/tmdb/${query}`);
+ const json = await res.json();
+ const data = json.results;
+ setHasilQuery(data);
+ setLoading(false);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleDetail = async () => {
+ try {
+ setLoading(true);
+ const res = await fetch(`${api}/meta/tmdb/info/${tmdbId}?type=TV%20Series
+`);
+ const json = await res.json();
+ const data = json.seasons;
+ setHasilQuery(data);
+ setLoading(false);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleStore = async () => {
+ try {
+ setLoading(true);
+ if (!resultData && !id) {
+ console.log("No data to store");
+ setLoading(false);
+ return;
+ }
+ const data = await fetch("/api/v2/admin/meta", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ id: id,
+ data: resultData,
+ }),
+ });
+ if (data.status === 200) {
+ const json = await data.json();
+ toast.success(json.message);
+ setLoading(false);
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ const handleOverride = async () => {
+ setResultData(JSON.parse(override));
+ };
+
+ return (
+ <>
+ <div className="container mx-auto p-4">
+ <h1 className="text-3xl font-semibold mb-4">Append Data Page</h1>
+ <div>
+ <div className="space-y-3 mb-4">
+ <label>Search Anime:</label>
+ <input
+ type="text"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={query}
+ onChange={(e) => setQuery(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleSearch}
+ >
+ Find Anime{" "}
+ {loading && <span className="animate-spin ml-2">🔄</span>}
+ </button>
+ </div>
+ <div className="space-y-3 mb-4">
+ <label>Get Episodes:</label>
+ <input
+ type="number"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={tmdbId}
+ onChange={(e) => setTmdbId(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleDetail}
+ >
+ Get Details{" "}
+ {loading && <span className="animate-spin ml-2">🔄</span>}
+ </button>
+ </div>
+
+ <div className="space-y-3 mb-4">
+ <label>Override Result:</label>
+ <textarea
+ rows="5"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={override}
+ onChange={(e) => setOverride(e.target.value)}
+ />
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleOverride}
+ >
+ Override{" "}
+ {loading && <span className="animate-spin ml-2">🔄</span>}
+ </button>
+ </div>
+
+ <div className="space-y-3 mb-4">
+ <label className="block text-sm font-medium text-gray-300">
+ Anime ID:
+ </label>
+ <input
+ type="number"
+ className="w-full px-3 py-2 border rounded-md text-black"
+ value={id}
+ onChange={(e) => setId(e.target.value)}
+ />
+ </div>
+ <div className="mb-4">
+ <button
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ onClick={handleStore}
+ >
+ Store Data {season && `Season ${season}`}
+ </button>
+ </div>
+
+ {hasilQuery?.some((i) => i?.season) && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">
+ Which season do you want to format?
+ </h2>
+ <div className="w-full flex gap-2">
+ {hasilQuery?.map((season, index) => (
+ <button
+ type="button"
+ className="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600"
+ key={index}
+ onClick={() => {
+ setLoading(true);
+ const data = hasilQuery[index].episodes;
+ const convertedData = convertData(data);
+ setSeason(index + 1);
+ setResultData(convertedData);
+ console.log(convertedData);
+ setLoading(false);
+ }}
+ >
+ <p>
+ {season.season}{" "}
+ {loading && <span className="animate-spin ml-2">🔄</span>}
+ </p>
+ </button>
+ ))}
+ </div>
+ </div>
+ )}
+
+ {resultData && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">Season {season}</h2>
+ <pre>{JSON.stringify(resultData, null, 2)}</pre>
+ </div>
+ )}
+ {hasilQuery && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">
+ Result Data,{" "}
+ {hasilQuery.length > 0 && `${hasilQuery.length} Seasons`}:
+ </h2>
+ <pre>{JSON.stringify(hasilQuery, null, 2)}</pre>
+ </div>
+ )}
+ </div>
+ <div>
+ {/* {resultData && (
+ <div className="border rounded-md p-4 mt-4">
+ <h2 className="text-lg font-semibold mb-2">Result Data:</h2>
+ <pre>{JSON.stringify(resultData, null, 2)}</pre>
+ </div>
+ )} */}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/pages/api/user/profile.js b/pages/api/user/profile.js
index 89a23d5..5ca6b75 100644
--- a/pages/api/user/profile.js
+++ b/pages/api/user/profile.js
@@ -1,71 +1,66 @@
import { getServerSession } from "next-auth";
import { authOptions } from "../auth/[...nextauth]";
-import {
- createUser,
- deleteUser,
- getUser,
- updateUser,
-} from "../../../prisma/user";
+import { createUser, deleteUser, getUser, updateUser } from "@/prisma/user";
export default async function handler(req, res) {
- // const session = await getServerSession(req, res, authOptions);
- // if (session) {
- // Signed in
- try {
- switch (req.method) {
- case "POST": {
- const { name } = req.body;
- const new_user = await createUser(name);
- if (!new_user) {
- return res.status(200).json({ message: "User is already created" });
- } else {
- return res.status(201).json(new_user);
- }
- }
- case "PUT": {
- const { name, settings } = req.body;
- const user = await updateUser(name, settings);
- if (!user) {
- return res.status(200).json({ message: "Can't update settings" });
- } else {
- return res.status(200).json(user);
+ const session = await getServerSession(req, res, authOptions);
+ if (session) {
+ // Signed in
+ try {
+ switch (req.method) {
+ case "POST": {
+ const { name } = req.body;
+ const new_user = await createUser(name);
+ if (!new_user) {
+ return res.status(200).json({ message: "User is already created" });
+ } else {
+ return res.status(201).json(new_user);
+ }
}
- }
- case "GET": {
- const { name } = req.query;
- const user = await getUser(name);
- if (!user) {
- return res.status(404).json({ message: "User not found" });
- } else {
- return res.status(200).json(user);
+ case "PUT": {
+ const { name, settings } = req.body;
+ const user = await updateUser(name, settings);
+ if (!user) {
+ return res.status(200).json({ message: "Can't update settings" });
+ } else {
+ return res.status(200).json(user);
+ }
}
- }
- case "DELETE": {
- const { name } = req.body;
- // return res.status(200).json({ name });
- if (session.user.name !== name) {
- return res.status(401).json({ message: "Unauthorized" });
- } else {
- const user = await deleteUser(name);
+ case "GET": {
+ const { name } = req.query;
+ const user = await getUser(name);
if (!user) {
return res.status(404).json({ message: "User not found" });
} else {
return res.status(200).json(user);
}
}
+ case "DELETE": {
+ const { name } = req.body;
+ // return res.status(200).json({ name });
+ if (session.user.name !== name) {
+ return res.status(401).json({ message: "Unauthorized" });
+ } else {
+ const user = await deleteUser(name);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ } else {
+ return res.status(200).json(user);
+ }
+ }
+ }
+ default: {
+ return res.status(405).json({ message: "Method not allowed" });
+ }
}
- default: {
- return res.status(405).json({ message: "Method not allowed" });
- }
+ } catch (error) {
+ console.log(error);
+ return res.status(500).json({ message: "Internal server error" });
}
- } catch (error) {
- console.log(error);
- return res.status(500).json({ message: "Internal server error" });
+ } else {
+ // Not Signed in
+ res.status(401);
}
- // } else {
- // // Not Signed in
- // res.status(401);
- // }
- // res.end();
+ res.end();
}
diff --git a/pages/api/user/update/episode.js b/pages/api/user/update/episode.js
index 3ee345d..bee98ab 100644
--- a/pages/api/user/update/episode.js
+++ b/pages/api/user/update/episode.js
@@ -7,7 +7,7 @@ import {
deleteList,
getEpisode,
updateUserEpisode,
-} from "../../../../prisma/user";
+} from "@/prisma/user";
export default async function handler(req, res) {
const session = await getServerSession(req, res, authOptions);
diff --git a/pages/api/v2/admin/broadcast/index.js b/pages/api/v2/admin/broadcast/index.js
new file mode 100644
index 0000000..d3d3af0
--- /dev/null
+++ b/pages/api/v2/admin/broadcast/index.js
@@ -0,0 +1,40 @@
+import { rateLimitStrict, redis } from "@/lib/redis";
+// import { getServerSession } from "next-auth";
+// import { authOptions } from "pages/api/auth/[...nextauth]";
+
+export default async function handler(req, res) {
+ // Check if the custom header "X-Your-Custom-Header" is present and has a specific value
+ const customHeaderValue = req.headers["x-broadcast-key"];
+
+ if (customHeaderValue !== "get-broadcast") {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ try {
+ if (redis) {
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ await rateLimitStrict.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
+
+ const getId = await redis.get(`broadcast`);
+ if (getId) {
+ const broadcast = JSON.parse(getId);
+ return res
+ .status(200)
+ .json({ message: broadcast.message, startAt: broadcast.startAt });
+ } else {
+ return res.status(200).json({ message: "No broadcast" });
+ }
+ }
+
+ return res.status(200).json({ message: "redis is not defined" });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: err.message });
+ }
+}
diff --git a/pages/api/v2/admin/bug-report/index.js b/pages/api/v2/admin/bug-report/index.js
new file mode 100644
index 0000000..fc5ee77
--- /dev/null
+++ b/pages/api/v2/admin/bug-report/index.js
@@ -0,0 +1,49 @@
+import { rateLimitStrict, redis } from "@/lib/redis";
+// import { getServerSession } from "next-auth";
+// import { authOptions } from "pages/api/auth/[...nextauth]";
+
+export default async function handler(req, res) {
+ // const session = await getServerSession(req, res, authOptions);
+ // const admin = session?.user?.name === process.env.ADMIN_USERNAME;
+ // create random id each time the endpoint is called
+ const id = Math.random().toString(36).substr(2, 9);
+
+ // if (!admin) {
+ // return res.status(401).json({ message: "Unauthorized" });
+ // }
+ const { data } = req.body;
+
+ // if method is not POST return message "Method not allowed"
+ if (req.method !== "POST") {
+ return res.status(405).json({ message: "Method not allowed" });
+ }
+
+ try {
+ if (redis) {
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ await rateLimitStrict.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
+
+ const getId = await redis.get(`report:${id}`);
+ if (getId) {
+ return res
+ .status(200)
+ .json({ message: `Data already exist for id: ${id}` });
+ }
+ await redis.set(`report:${id}`, JSON.stringify(data));
+ return res
+ .status(200)
+ .json({ message: `Report has successfully sent, with Id of ${id}` });
+ }
+
+ return res.status(200).json({ message: "redis is not defined" });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: err.message });
+ }
+}
diff --git a/pages/api/v2/admin/meta/index.js b/pages/api/v2/admin/meta/index.js
new file mode 100644
index 0000000..5f51b7f
--- /dev/null
+++ b/pages/api/v2/admin/meta/index.js
@@ -0,0 +1,47 @@
+import { rateLimitStrict, redis } from "@/lib/redis";
+import { getServerSession } from "next-auth";
+import { authOptions } from "pages/api/auth/[...nextauth]";
+
+export default async function handler(req, res) {
+ const session = await getServerSession(req, res, authOptions);
+ const admin = session?.user?.name === process.env.ADMIN_USERNAME;
+
+ if (!admin) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const { id, data } = req.body;
+
+ // if method is not POST return message "Method not allowed"
+ if (req.method !== "POST") {
+ return res.status(405).json({ message: "Method not allowed" });
+ }
+
+ try {
+ if (redis) {
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ await rateLimitStrict.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
+
+ const getId = await redis.get(`meta:${id}`);
+ if (getId) {
+ return res
+ .status(200)
+ .json({ message: `Data already exist for id: ${id}` });
+ }
+ await redis.set(`meta:${id}`, JSON.stringify(data));
+ return res
+ .status(200)
+ .json({ message: `Data stored successfully for id: ${id}` });
+ }
+
+ return res.status(200).json({ message: "redis is not defined" });
+ } catch (err) {
+ console.error(err);
+ res.status(500).json({ error: err.message });
+ }
+}
diff --git a/pages/api/v2/episode/[id].js b/pages/api/v2/episode/[id].js
index c5d98f5..ab2d321 100644
--- a/pages/api/v2/episode/[id].js
+++ b/pages/api/v2/episode/[id].js
@@ -1,5 +1,7 @@
import axios from "axios";
-import redis from "../../../../lib/redis";
+import { rateLimitStrict, rateLimiterRedis, redis } from "@/lib/redis";
+import appendImagesToEpisodes from "@/utils/combineImages";
+import appendMetaToEpisodes from "@/utils/appendMetaToEpisodes";
const CONSUMET_URI = process.env.API_URI;
const API_KEY = process.env.API_KEY;
@@ -23,15 +25,27 @@ async function fetchConsumet(id, dub) {
`${CONSUMET_URI}/meta/anilist/episodes/${id}`
);
- if (data?.message === "Anime not found") {
+ if (data?.message === "Anime not found" || data?.length < 1) {
return [];
}
+ const reformatted = data.map((item) => ({
+ id: item?.id || null,
+ title: item?.title || null,
+ img: item?.image || null,
+ number: item?.number || null,
+ createdAt: item?.createdAt || null,
+ description: item?.description || null,
+ url: item?.url || null,
+ }));
+
const array = [
{
map: true,
providerId: "gogoanime",
- episodes: isAscending(data) ? data : data.reverse(),
+ episodes: isAscending(reformatted)
+ ? reformatted
+ : reformatted.reverse(),
},
];
@@ -74,20 +88,60 @@ async function fetchAnify(id) {
}
}
+async function fetchCoverImage(id) {
+ try {
+ if (!process.env.API_KEY) {
+ return [];
+ }
+
+ const { data } = await axios.get(
+ `https://api.anify.tv/episode-covers/${id}?apikey=${API_KEY}`
+ );
+
+ if (!data) {
+ return [];
+ }
+
+ return data;
+ } catch (error) {
+ console.error("Error fetching and processing data:", error.message);
+ return [];
+ }
+}
+
export default async function handler(req, res) {
- const { id, releasing = "false", dub = false } = req.query;
+ const { id, releasing = "false", dub = false, refresh = null } = req.query;
// if releasing is true then cache for 10 minutes, if it false cache for 1 month;
const cacheTime = releasing === "true" ? 60 * 10 : 60 * 60 * 24 * 30;
let cached;
+ let meta;
if (redis) {
- cached = await redis.get(id);
- console.log("using redis");
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ refresh
+ ? await rateLimitStrict.consume(ipAddress)
+ : await rateLimiterRedis.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
+
+ if (refresh) {
+ await redis.del(id);
+ console.log("deleted cache");
+ } else {
+ cached = await redis.get(id);
+ console.log("using redis");
+ }
+
+ meta = await redis.get(`meta:${id}`);
}
- if (cached) {
+ if (cached && !refresh) {
if (dub) {
const filtered = JSON.parse(cached).filter((item) =>
item.episodes.some((epi) => epi.hasDub === true)
@@ -96,27 +150,46 @@ export default async function handler(req, res) {
} else {
return res.status(200).json(JSON.parse(cached));
}
- }
+ } else {
+ const [consumet, anify, cover] = await Promise.all([
+ fetchConsumet(id, dub),
+ fetchAnify(id),
+ fetchCoverImage(id),
+ ]);
+
+ const hasImage = consumet.map((i) =>
+ i.episodes.some(
+ (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/")
+ )
+ );
- const [consumet, anify] = await Promise.all([
- fetchConsumet(id, dub),
- fetchAnify(id),
- ]);
+ const rawData = [...consumet, ...anify];
- const data = [...consumet, ...anify];
+ let data = rawData;
- if (redis && cacheTime !== null) {
- await redis.set(id, JSON.stringify(data), "EX", cacheTime);
- }
+ if (meta) {
+ data = await appendMetaToEpisodes(rawData, JSON.parse(meta));
+ } else if (cover && cover?.length > 0 && !hasImage.includes(true))
+ data = await appendImagesToEpisodes(rawData, cover);
- if (dub) {
- const filtered = data.filter((item) =>
- item.episodes.some((epi) => epi.hasDub === true)
- );
- return res.status(200).json(filtered);
- }
+ if (redis && cacheTime !== null) {
+ await redis.set(
+ id,
+ JSON.stringify(data.filter((i) => i.episodes.length > 0)),
+ "EX",
+ cacheTime
+ );
+ }
- console.log("fresh data");
+ if (dub) {
+ const filtered = data.filter((item) =>
+ item.episodes.some((epi) => epi.hasDub === true)
+ );
+ return res.status(200).json(filtered);
+ }
- return res.status(200).json(data);
+ console.log("fresh data");
+
+ return res.status(200).json(data.filter((i) => i.episodes.length > 0));
+ }
}
diff --git a/pages/api/v2/etc/recent/[page].js b/pages/api/v2/etc/recent/[page].js
index 19495c1..6727787 100644
--- a/pages/api/v2/etc/recent/[page].js
+++ b/pages/api/v2/etc/recent/[page].js
@@ -1,7 +1,19 @@
+import { rateLimiterRedis, redis } from "@/lib/redis";
+
const API_URL = process.env.API_URI;
export default async function handler(req, res) {
try {
+ if (redis) {
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ await rateLimiterRedis.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
+ }
const page = req.query.page || 1;
var hasNextPage = true;
diff --git a/pages/api/v2/etc/schedule/index.js b/pages/api/v2/etc/schedule/index.js
index 7a13fff..9b8f43d 100644
--- a/pages/api/v2/etc/schedule/index.js
+++ b/pages/api/v2/etc/schedule/index.js
@@ -1,6 +1,6 @@
import axios from "axios";
import cron from "cron";
-import redis from "../../../../../lib/redis";
+import { rateLimiterRedis, redis } from "@/lib/redis";
const API_KEY = process.env.API_KEY;
@@ -43,6 +43,14 @@ export default async function handler(req, res) {
try {
let cached;
if (redis) {
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ await rateLimiterRedis.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
cached = await redis.get("schedule");
}
if (cached) {
@@ -60,9 +68,9 @@ export default async function handler(req, res) {
60 * 60 * 24 * 7
);
}
- res.status(200).json(data);
+ return res.status(200).json(data);
} else {
- res.status(404).json({ message: "Schedule not found" });
+ return res.status(404).json({ message: "Schedule not found" });
}
}
} catch (error) {
diff --git a/pages/api/v2/info/[id].js b/pages/api/v2/info/[id].js
index 41daa6e..243756c 100644
--- a/pages/api/v2/info/[id].js
+++ b/pages/api/v2/info/[id].js
@@ -1,5 +1,5 @@
import axios from "axios";
-import redis from "../../../../lib/redis";
+import { rateLimiterRedis, redis } from "@/lib/redis";
const API_KEY = process.env.API_KEY;
@@ -19,6 +19,14 @@ export default async function handler(req, res) {
const id = req.query.id;
let cached;
if (redis) {
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ await rateLimiterRedis.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
cached = await redis.get(id);
}
if (cached) {
diff --git a/pages/api/v2/source/index.js b/pages/api/v2/source/index.js
index 51ac5ec..74a63cb 100644
--- a/pages/api/v2/source/index.js
+++ b/pages/api/v2/source/index.js
@@ -1,3 +1,4 @@
+import { rateLimiterRedis, redis } from "@/lib/redis";
import axios from "axios";
const CONSUMET_URI = process.env.API_URI;
@@ -33,6 +34,17 @@ export default async function handler(req, res) {
return res.status(405).json({ message: "Method not allowed" });
}
+ if (redis) {
+ try {
+ const ipAddress = req.socket.remoteAddress;
+ await rateLimiterRedis.consume(ipAddress);
+ } catch (error) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
+ }
+
const { source, providerId, watchId, episode, id, sub = "sub" } = req.body;
if (source === "anify") {
diff --git a/pages/en/about.js b/pages/en/about.js
index cfbee6b..aa0ba30 100644
--- a/pages/en/about.js
+++ b/pages/en/about.js
@@ -1,7 +1,8 @@
import Head from "next/head";
-import Layout from "../../components/layout";
import { motion } from "framer-motion";
import Link from "next/link";
+import { NewNavbar } from "@/components/shared/NavBar";
+import Footer from "@/components/shared/footer";
export default function About() {
return (
@@ -20,46 +21,46 @@ export default function About() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/svg/c.svg" />
</Head>
- <Layout>
- <motion.div
- initial={{ opacity: 0 }}
- animate={{ opacity: 1 }}
- exit={{ opacity: 0 }}
- className="flex flex-col justify-center items-center min-h-screen md:py-0 py-16"
- >
- <div className="max-w-screen-lg w-full px-4 py-10">
- <h1 className="text-4xl font-bold mb-6">About Us</h1>
- <p className="text-lg mb-8">
- Moopa is a platform where you can watch and stream anime or read
- manga for free, without any ads or VPNs. Our mission is to provide
- a convenient and enjoyable experience for anime and manga
- enthusiasts all around the world.
- </p>
- <p className="text-lg mb-8">
- At our site, you will find a vast collection of anime and manga
- titles from different genres, including action, adventure, comedy,
- romance, and more. We take pride in our fast and reliable servers,
- which ensure smooth streaming and reading for all our users.
- </p>
- <p className="text-lg mb-8">
- We believe that anime and manga have the power to inspire and
- entertain people of all ages and backgrounds. Our service is
- designed to make it easy for fans to access the content they love,
- whether they are casual viewers or die-hard fans.
- </p>
- <p className="text-lg mb-8">
- Thank you for choosing our website as your go-to platform for
- anime and manga. We hope you enjoy your stay here, and feel free
- to contact us if you have any feedback or suggestions.
- </p>
- <Link href="/en/contact">
- <div className="bg-[#ffffff] text-black font-medium py-3 px-6 rounded-lg hover:bg-action transition duration-300 ease-in-out">
- Contact Us
- </div>
- </Link>
- </div>
- </motion.div>
- </Layout>
+ <NewNavbar withNav={true} scrollP={5} shrink={true} />
+ <motion.div
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ className="flex flex-col justify-center items-center min-h-screen md:py-0 py-16"
+ >
+ <div className="max-w-screen-lg w-full px-4 py-10">
+ <h1 className="text-4xl font-bold mb-6">About Us</h1>
+ <p className="text-lg mb-8">
+ Moopa is a platform where you can watch and stream anime or read
+ manga for free, without any ads or VPNs. Our mission is to provide a
+ convenient and enjoyable experience for anime and manga enthusiasts
+ all around the world.
+ </p>
+ <p className="text-lg mb-8">
+ At our site, you will find a vast collection of anime and manga
+ titles from different genres, including action, adventure, comedy,
+ romance, and more. We take pride in our fast and reliable servers,
+ which ensure smooth streaming and reading for all our users.
+ </p>
+ <p className="text-lg mb-8">
+ We believe that anime and manga have the power to inspire and
+ entertain people of all ages and backgrounds. Our service is
+ designed to make it easy for fans to access the content they love,
+ whether they are casual viewers or die-hard fans.
+ </p>
+ <p className="text-lg mb-8">
+ Thank you for choosing our website as your go-to platform for anime
+ and manga. We hope you enjoy your stay here, and feel free to
+ contact us if you have any feedback or suggestions.
+ </p>
+ <Link href="/en/contact">
+ <div className="bg-[#ffffff] text-black font-medium py-3 px-6 rounded-lg hover:bg-action transition duration-300 ease-in-out">
+ Contact Us
+ </div>
+ </Link>
+ </div>
+ </motion.div>
+ <Footer />
</>
);
}
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].js
index ec19f9f..193f50f 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].js
@@ -2,23 +2,21 @@ import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
-import Content from "../../../components/home/content";
-import Modal from "../../../components/modal";
+import Content from "@/components/home/content";
+import Modal from "@/components/modal";
import { signIn, useSession } from "next-auth/react";
-import AniList from "../../../components/media/aniList";
-import ListEditor from "../../../components/listEditor";
+import AniList from "@/components/media/aniList";
+import ListEditor from "@/components/listEditor";
-import { ToastContainer } from "react-toastify";
-
-import DetailTop from "../../../components/anime/mobile/topSection";
-import AnimeEpisode from "../../../components/anime/episode";
-import { useAniList } from "../../../lib/anilist/useAnilist";
-import Footer from "../../../components/footer";
-import { mediaInfoQuery } from "../../../lib/graphql/query";
-import MobileNav from "../../../components/shared/MobileNav";
-import redis from "../../../lib/redis";
-import Characters from "../../../components/anime/charactersCard";
+import DetailTop from "@/components/anime/mobile/topSection";
+import AnimeEpisode from "@/components/anime/episode";
+import { useAniList } from "@/lib/anilist/useAnilist";
+import Footer from "@/components/shared/footer";
+import { mediaInfoQuery } from "@/lib/graphql/query";
+import MobileNav from "@/components/shared/MobileNav";
+import redis from "@/lib/redis";
+import Characters from "@/components/anime/charactersCard";
export default function Info({ info, color }) {
const { data: session } = useSession();
@@ -117,7 +115,6 @@ export default function Info({ info, color }) {
}&image=${info.bannerImage || info.coverImage.extraLarge}`}
/>
</Head>
- <ToastContainer pauseOnHover={false} />
<Modal open={open} onClose={() => handleClose()}>
<div>
{!session && (
@@ -188,7 +185,7 @@ export default function Info({ info, color }) {
{info?.characters?.edges && (
<div className="w-full">
- <Characters info={info?.characters?.edges}/>
+ <Characters info={info?.characters?.edges} />
</div>
)}
diff --git a/pages/en/anime/popular.js b/pages/en/anime/popular.js
index 7b40a0e..4e6535b 100644
--- a/pages/en/anime/popular.js
+++ b/pages/en/anime/popular.js
@@ -3,11 +3,11 @@ import Image from "next/image";
import Link from "next/link";
import { Fragment, useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
-import Footer from "../../../components/footer";
+import Footer from "@/components/shared/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
import Head from "next/head";
-import MobileNav from "../../../components/shared/MobileNav";
+import MobileNav from "@/components/shared/MobileNav";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
diff --git a/pages/en/anime/recent.js b/pages/en/anime/recent.js
index 89a868a..400e926 100644
--- a/pages/en/anime/recent.js
+++ b/pages/en/anime/recent.js
@@ -3,11 +3,11 @@ import { Fragment, useEffect, useState } from "react";
import Link from "next/link";
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
import Skeleton from "react-loading-skeleton";
-import Footer from "../../../components/footer";
+import Footer from "@/components/shared/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
import Image from "next/image";
-import MobileNav from "../../../components/shared/MobileNav";
+import MobileNav from "@/components/shared/MobileNav";
export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
diff --git a/pages/en/anime/recently-watched.js b/pages/en/anime/recently-watched.js
index 0b7a710..c723394 100644
--- a/pages/en/anime/recently-watched.js
+++ b/pages/en/anime/recently-watched.js
@@ -3,15 +3,15 @@ import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
-import Footer from "../../../components/footer";
+import Footer from "@/components/shared/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import { ToastContainer, toast } from "react-toastify";
+import { toast } from "react-toastify";
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
-import HistoryOptions from "../../../components/home/content/historyOptions";
+import HistoryOptions from "@/components/home/content/historyOptions";
import Head from "next/head";
-import MobileNav from "../../../components/shared/MobileNav";
+import MobileNav from "@/components/shared/MobileNav";
export default function PopularAnime({ sessions }) {
const [data, setData] = useState(null);
@@ -154,7 +154,6 @@ export default function PopularAnime({ sessions }) {
<title>Moopa - Recently Watched Episodes</title>
</Head>
<MobileNav sessions={sessions} />
- <ToastContainer pauseOnHover={false} />
<div className="flex flex-col gap-2 items-center min-h-screen w-screen px-2 relative pb-10">
<div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed left-0 px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
@@ -171,8 +170,6 @@ export default function PopularAnime({ sessions }) {
let prog = (time / duration) * 100;
if (prog > 90) prog = 100;
- console.log({ i });
-
return (
<div
key={i.watchId}
@@ -233,7 +230,7 @@ export default function PopularAnime({ sessions }) {
width={200}
height={200}
alt="Episode Thumbnail"
- className="w-fit group-hover:scale-[1.02] duration-300 ease-out z-10"
+ className="w-full object-cover group-hover:scale-[1.02] duration-300 ease-out z-10"
/>
)}
</Link>
diff --git a/pages/en/anime/trending.js b/pages/en/anime/trending.js
index 18eadf9..aae468a 100644
--- a/pages/en/anime/trending.js
+++ b/pages/en/anime/trending.js
@@ -3,11 +3,11 @@ import Image from "next/image";
import Link from "next/link";
import { Fragment, useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
-import Footer from "../../../components/footer";
+import Footer from "@/components/shared/footer";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
import Head from "next/head";
-import MobileNav from "../../../components/shared/MobileNav";
+import MobileNav from "@/components/shared/MobileNav";
export default function TrendingAnime({ sessions }) {
const [data, setData] = useState(null);
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index aa0b672..f5b4fce 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -1,38 +1,156 @@
+import React, { useEffect, useRef, useState } from "react";
+import PlayerComponent from "@/components/watch/player/playerComponent";
+import { FlagIcon, ShareIcon } from "@heroicons/react/24/solid";
+import Details from "@/components/watch/primary/details";
+import EpisodeLists from "@/components/watch/secondary/episodeLists";
+import { getServerSession } from "next-auth";
+import { useWatchProvider } from "@/lib/hooks/watchPageProvider";
+import { authOptions } from "../../../api/auth/[...nextauth]";
+import { createList, createUser, getEpisode } from "@/prisma/user";
+import Link from "next/link";
+import MobileNav from "@/components/shared/MobileNav";
+import { NewNavbar } from "@/components/shared/NavBar";
+import Modal from "@/components/modal";
+import AniList from "@/components/media/aniList";
+import { signIn } from "next-auth/react";
+import BugReportForm from "@/components/shared/bugReport";
+import Skeleton from "react-loading-skeleton";
import Head from "next/head";
-import { useEffect, useState } from "react";
-import { getServerSession } from "next-auth/next";
-import { authOptions } from "../../../api/auth/[...nextauth]";
+export async function getServerSideProps(context) {
+ let userData = null;
+ const session = await getServerSession(context.req, context.res, authOptions);
+ const accessToken = session?.user?.token || null;
+
+ const query = context?.query;
+ if (!query) {
+ return {
+ notFound: true,
+ };
+ }
-import Navigasi from "../../../../components/home/staticNav";
-import PrimarySide from "../../../../components/anime/watch/primarySide";
-import SecondarySide from "../../../../components/anime/watch/secondarySide";
-import { createList, createUser, getEpisode } from "../../../../prisma/user";
+ const proxy = process.env.PROXY_URI;
+ const disqus = process.env.DISQUS_SHORTNAME;
-export default function Info({
- sessions,
+ const [aniId, provider] = query?.info;
+ const watchId = query?.id;
+ const epiNumber = query?.num;
+ const dub = query?.dub;
+
+ const ress = await fetch(`https://graphql.anilist.co`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
+ },
+ body: JSON.stringify({
+ query: `query ($id: Int) {
+ Media (id: $id) {
+ mediaListEntry {
+ progress
+ status
+ customLists
+ repeat
+ }
+ id
+ idMal
+ title {
+ romaji
+ english
+ native
+ }
+ status
+ genres
+ episodes
+ studios {
+ edges {
+ node {
+ id
+ name
+ }
+ }
+ }
+ bannerImage
+ description
+ coverImage {
+ extraLarge
+ color
+ }
+ synonyms
+
+ }
+ }
+ `,
+ variables: {
+ id: aniId,
+ },
+ }),
+ });
+ const data = await ress.json();
+
+ try {
+ if (session) {
+ await createUser(session.user.name);
+ await createList(session.user.name, watchId);
+ const data = await getEpisode(session.user.name, watchId);
+ userData = JSON.parse(
+ JSON.stringify(data, (key, value) => {
+ if (key === "createdDate") {
+ return String(value);
+ }
+ return value;
+ })
+ );
+ }
+ } catch (error) {
+ console.error(error);
+ // Handle the error here
+ }
+ return {
+ props: {
+ sessions: session,
+ provider: provider || null,
+ watchId: watchId || null,
+ epiNumber: epiNumber || null,
+ dub: dub || null,
+ userData: userData?.[0] || null,
+ info: data.data.Media || null,
+ proxy,
+ disqus,
+ },
+ };
+}
+
+export default function Watch({
+ info,
watchId,
- provider,
- epiNumber,
+ disqus,
+ proxy,
dub,
- info,
userData,
- proxy,
- disqus,
+ sessions,
+ provider,
+ epiNumber,
}) {
- const [currentEpisode, setCurrentEpisode] = useState(null);
- const [loading, setLoading] = useState(false);
-
const [artStorage, setArtStorage] = useState(null);
+
+ const [episodeNavigation, setEpisodeNavigation] = useState(null);
const [episodesList, setepisodesList] = useState();
- const [mapProviders, setMapProviders] = useState(null);
+ const [mapEpisode, setMapEpisode] = useState(null);
+
+ const [episodeSource, setEpisodeSource] = useState(null);
+
+ const [open, setOpen] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
const [onList, setOnList] = useState(false);
- const [origin, setOrigin] = useState(null);
+
+ const { theaterMode, setPlayerState, setAutoPlay, setMarked } =
+ useWatchProvider();
+
+ const playerRef = useRef(null);
useEffect(() => {
- setLoading(true);
- setOrigin(window.location.origin);
async function getInfo() {
if (info.mediaListEntry) {
setOnList(true);
@@ -56,7 +174,7 @@ export default function Info({
});
}
- setMapProviders(getMap?.episodes);
+ setMapEpisode(getMap?.episodes);
}
if (episodes) {
@@ -79,207 +197,319 @@ export default function Info({
const previousEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber) - 1
);
- setCurrentEpisode({
+ setEpisodeNavigation({
prev: previousEpisode,
playing: {
id: currentEpisode.id,
title: playingData?.title,
description: playingData?.description,
- image: playingData?.image,
+ img: playingData?.img || playingData?.image,
number: currentEpisode.number,
},
next: nextEpisode,
});
- } else {
- setLoading(false);
}
}
setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
// setEpiData(episodes);
- setLoading(false);
}
getInfo();
return () => {
- setCurrentEpisode(null);
+ setEpisodeNavigation(null);
};
}, [sessions?.user?.name, epiNumber, dub]);
+ useEffect(() => {
+ async function fetchData() {
+ if (info) {
+ const autoplay =
+ localStorage.getItem("autoplay_video") === "true" ? true : false;
+ setAutoPlay(autoplay);
+
+ const anify = await fetch("/api/v2/source", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ source:
+ provider === "gogoanime" && !watchId.startsWith("/")
+ ? "consumet"
+ : "anify",
+ providerId: provider,
+ 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(
+ epiNumber
+ )}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=`
+ ).then((res) => {
+ if (!res.ok) {
+ switch (res.status) {
+ case 404: {
+ return null;
+ }
+ }
+ }
+ return res.json();
+ });
+
+ const op =
+ skip?.results?.find((item) => item.skipType === "op") || null;
+ const ed =
+ skip?.results?.find((item) => item.skipType === "ed") || null;
+
+ const episode = {
+ epiData: anify,
+ skip: {
+ op,
+ ed,
+ },
+ };
+
+ setEpisodeSource(episode);
+ }
+ }
+
+ fetchData();
+ return () => {
+ setEpisodeSource();
+ setPlayerState({
+ currentTime: 0,
+ isPlaying: false,
+ });
+ setMarked(0);
+ };
+ }, [provider, watchId, info?.id]);
+
+ const handleShareClick = async () => {
+ try {
+ if (navigator.share) {
+ await navigator.share({
+ title: `Watch Now - ${info?.title?.english || info.title.romaji}`,
+ // 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);
+ }
+ };
+
+ function handleOpen() {
+ setOpen(true);
+ document.body.style.overflow = "hidden";
+ }
+
+ function handleClose() {
+ setOpen(false);
+ document.body.style.overflow = "auto";
+ }
+
return (
<>
<Head>
- <title>{info?.title?.romaji || "Retrieving data..."}</title>
+ <title>
+ {episodeNavigation?.playing?.title ||
+ `${info?.title?.romaji} - Episode ${epiNumber}`}
+ </title>
+ {/* Write the best SEO for this watch page with data of anime title from info.title.romaji, episode title from episodeNavigation?.playing?.title, description from episodeNavigation?.playing?.description, episode number from epiNumber */}
+ <meta name="twitter:card" content="summary_large_image" />
+ {/* Write the best SEO for this homepage */}
<meta
- name="title"
- data-title-romaji={info?.title?.romaji}
- data-title-english={info?.title?.english}
- data-title-native={info?.title?.native}
+ name="description"
+ content={episodeNavigation?.playing?.description || info?.description}
/>
<meta
- name="description"
- content={currentEpisode?.playing?.description || info?.description}
+ name="keywords"
+ content="anime, anime streaming, anime streaming website, anime streaming free, anime streaming website free, anime streaming website free english subbed, anime streaming website free english dubbed, anime streaming website free english subbed and dubbed, anime streaming webs
+ ite free english subbed and dubbed download, anime streaming website free english subbed and dubbed"
/>
+ <meta name="robots" content="index, follow" />
+
+ <meta property="og:type" content="website" />
+ <meta property="og:url" content="https://moopa.live/" />
+ <meta
+ property="og:title"
+ content={`Watch - ${
+ episodeNavigation?.playing?.title || info?.title?.english
+ }`}
+ />
+ <meta
+ property="og:description"
+ content="Discover your new favorite anime or manga title! Moopa offers a vast library of high-quality content, accessible on multiple devices and without any interruptions. Start using Moopa today!"
+ />
+ <meta property="og:image" content="/preview.png" />
+ <meta property="og:site_name" content="Moopa" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
- content={`Episode ${epiNumber} - ${
- info.title.romaji || info.title.english
+ content={`Watch - ${
+ episodeNavigation?.playing?.title || info?.title?.english
}`}
/>
<meta
name="twitter:description"
- content={`${
- currentEpisode?.playing?.description?.slice(0, 180) ||
- info?.description?.slice(0, 180)
- }...`}
- />
- <meta
- name="twitter:image"
- content={`${origin}/api/og?title=${
- info.title.romaji || info.title.english
- }&image=${info.bannerImage || info.coverImage.extraLarge}`}
+ content={episodeNavigation?.playing?.description || info?.description}
/>
</Head>
+ <Modal open={open} onClose={() => handleClose()}>
+ {!sessions && (
+ <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md">
+ <h1 className="text-md font-extrabold font-karla">
+ Edit your list
+ </h1>
+ <button
+ className="flex items-center bg-[#363642] rounded-md text-white p-1"
+ onClick={() => signIn("AniListProvider")}
+ >
+ <h1 className="px-1 font-bold font-karla">Login with AniList</h1>
+ <div className="scale-[60%] pb-[1px]">
+ <AniList />
+ </div>
+ </button>
+ </div>
+ )}
+ </Modal>
+ <BugReportForm isOpen={isOpen} setIsOpen={setIsOpen} />
+ <main className="w-screen h-full">
+ <NewNavbar
+ scrollP={20}
+ withNav={true}
+ shrink={true}
+ paddingY={`py-2 ${theaterMode ? "" : "lg:py-4"}`}
+ />
+ <MobileNav hideProfile={true} sessions={sessions} />
+ <div
+ className={`mx-auto pt-16 ${theaterMode ? "lg:pt-16" : "lg:pt-20"}`}
+ >
+ {theaterMode && (
+ <PlayerComponent
+ id={"cinematic"}
+ session={sessions}
+ playerRef={playerRef}
+ dub={dub}
+ info={info}
+ watchId={watchId}
+ proxy={proxy}
+ track={episodeNavigation}
+ data={episodeSource?.epiData}
+ skip={episodeSource?.skip}
+ timeWatched={userData?.timeWatched}
+ provider={provider}
+ className="w-screen max-h-[85dvh]"
+ />
+ )}
+ <div
+ id="default"
+ className={`${
+ theaterMode ? "lg:max-w-[80%]" : "lg:max-w-[95%]"
+ } w-full flex flex-col lg:flex-row mx-auto`}
+ >
+ <div id="primary" className="w-full">
+ {!theaterMode && (
+ <PlayerComponent
+ id={"default"}
+ session={sessions}
+ playerRef={playerRef}
+ dub={dub}
+ info={info}
+ watchId={watchId}
+ proxy={proxy}
+ track={episodeNavigation}
+ data={episodeSource?.epiData}
+ skip={episodeSource?.skip}
+ timeWatched={userData?.timeWatched}
+ provider={provider}
+ />
+ )}
+ <div
+ id="details"
+ className="flex flex-col gap-5 w-full px-3 lg:px-0"
+ >
+ <div className="flex items-end justify-between pt-3 border-b-2 border-secondary pb-2">
+ <div className="w-[55%]">
+ <div className="flex font-outfit font-semibold text-lg lg:text-2xl text-white line-clamp-1">
+ <Link
+ href={`/en/anime/${info?.id}`}
+ className="hover:underline line-clamp-1"
+ >
+ {(episodeNavigation?.playing?.title ||
+ info.title.romaji) ??
+ "Loading..."}
+ </Link>
+ </div>
+ <p className="font-karla">
+ {episodeNavigation?.playing?.number ? (
+ `Episode ${episodeNavigation?.playing?.number}`
+ ) : (
+ <Skeleton width={120} height={16} />
+ )}
+ </p>
+ </div>
+ <div>
+ <div className="flex gap-2 text-sm">
+ <button
+ type="button"
+ onClick={handleShareClick}
+ className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden"
+ >
+ <ShareIcon className="w-5 h-5" />
+ share
+ </button>
+ <button
+ type="button"
+ onClick={() => setIsOpen(true)}
+ className="flex items-center gap-2 px-3 py-1 ring-[1px] ring-white/20 rounded overflow-hidden"
+ >
+ <FlagIcon className="w-5 h-5" />
+ report
+ </button>
+ </div>
+ </div>
+ {/* <div>right</div> */}
+ </div>
- <Navigasi />
- <div className="w-screen flex justify-center my-3 lg:my-10">
- <div className="lg:w-[95%] flex flex-col lg:flex-row gap-5 lg:gap-0 justify-between">
- <PrimarySide
- info={info}
- navigation={currentEpisode}
- episodeList={episodesList}
- session={sessions}
- epiNumber={epiNumber}
- providerId={provider}
- watchId={watchId}
- onList={onList}
- proxy={proxy}
- disqus={disqus}
- setOnList={setOnList}
- setLoading={setLoading}
- loading={loading}
- timeWatched={userData?.timeWatched}
- dub={dub}
- />
- <SecondarySide
- info={info}
- map={mapProviders}
- providerId={provider}
- watchId={watchId}
- episode={episodesList}
- artStorage={artStorage}
- dub={dub}
- />
+ <Details
+ info={info}
+ session={sessions}
+ description={info?.description}
+ epiNumber={epiNumber}
+ id={info}
+ onList={onList}
+ setOnList={setOnList}
+ handleOpen={() => handleOpen()}
+ disqus={disqus}
+ />
+ </div>
+ </div>
+ <div
+ id="secondary"
+ className={`relative ${theaterMode ? "pt-2" : ""}`}
+ >
+ <EpisodeLists
+ info={info}
+ map={mapEpisode}
+ providerId={provider}
+ watchId={watchId}
+ episode={episodesList}
+ artStorage={artStorage}
+ dub={dub}
+ />
+ </div>
+ </div>
</div>
- </div>
+ </main>
</>
);
}
-
-export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
- const accessToken = session?.user?.token || null;
-
- const query = context.query;
- if (!query) {
- return {
- notFound: true,
- };
- }
-
- const proxy = process.env.PROXY_URI;
- const disqus = process.env.DISQUS_SHORTNAME;
-
- const [aniId, provider] = query.info;
- const watchId = query.id;
- const epiNumber = query.num;
- const dub = query.dub;
-
- let userData = null;
-
- const ress = await fetch(`https://graphql.anilist.co`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
- },
- body: JSON.stringify({
- query: `query ($id: Int) {
- Media (id: $id) {
- mediaListEntry {
- progress
- status
- customLists
- repeat
- }
- id
- idMal
- title {
- romaji
- english
- native
- }
- status
- genres
- episodes
- studios {
- edges {
- node {
- id
- name
- }
- }
- }
- bannerImage
- description
- coverImage {
- extraLarge
- color
- }
- synonyms
-
- }
- }
- `,
- variables: {
- id: aniId,
- },
- }),
- });
- const data = await ress.json();
-
- try {
- if (session) {
- await createUser(session.user.name);
- await createList(session.user.name, watchId);
- const data = await getEpisode(session.user.name, watchId);
- userData = JSON.parse(
- JSON.stringify(data, (key, value) => {
- if (key === "createdDate") {
- return String(value);
- }
- return value;
- })
- );
- }
- } catch (error) {
- console.error(error);
- // Handle the error here
- }
-
- return {
- props: {
- sessions: session,
- aniId: aniId || null,
- provider: provider || null,
- watchId: watchId || null,
- epiNumber: epiNumber || null,
- dub: dub || null,
- userData: userData?.[0] || null,
- info: data.data.Media || null,
- proxy,
- disqus,
- },
- };
-}
diff --git a/pages/en/contact.js b/pages/en/contact.js
index 400a9e8..385bdb1 100644
--- a/pages/en/contact.js
+++ b/pages/en/contact.js
@@ -1,8 +1,10 @@
-import Layout from "../../components/layout";
+import { NewNavbar } from "@/components/shared/NavBar";
+import Footer from "@/components/shared/footer";
const Contact = () => {
return (
- <Layout className="">
+ <>
+ <NewNavbar withNav={true} scrollP={5} shrink={true} />
<div className=" flex h-screen w-screen flex-col items-center justify-center font-karla font-bold">
<h1>Contact Us</h1>
<p>If you have any questions or comments, please email us at:</p>
@@ -12,7 +14,8 @@ const Contact = () => {
</a>
</p>
</div>
- </Layout>
+ <Footer />
+ </>
);
};
diff --git a/pages/en/dmca.js b/pages/en/dmca.js
index d6d7ccf..e559829 100644
--- a/pages/en/dmca.js
+++ b/pages/en/dmca.js
@@ -1,5 +1,7 @@
+import MobileNav from "@/components/shared/MobileNav";
+import { NewNavbar } from "@/components/shared/NavBar";
+import Footer from "@/components/shared/footer";
import Head from "next/head";
-import Layout from "../../components/layout";
export default function DMCA() {
return (
@@ -18,11 +20,14 @@ export default function DMCA() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/svg/c.svg" />
</Head>
- <Layout>
+ <>
+ <NewNavbar withNav={true} scrollP={5} shrink={true} />
+
+ <MobileNav hideProfile={true} />
<div className="min-h-screen z-20 flex w-screen justify-center items-center">
- <div className="w-[75%] text-2xl gap-7 flex flex-col my-[10rem]">
+ <div className="px-5 lg:px-0 lg:w-[75%] text-2xl gap-7 flex flex-col my-[10rem]">
<div className="flex">
- <h1 className="text-4xl font-bold font-karla rounded-md bg-[#212121] p-3">
+ <h1 className="text-4xl font-bold font-karla rounded-md bg-secondary p-3">
DMCA - Disclaimer
</h1>
</div>
@@ -100,7 +105,8 @@ export default function DMCA() {
</div>
</div>
</div>
- </Layout>
+ <Footer />
+ </>
</>
);
}
diff --git a/pages/en/index.js b/pages/en/index.js
index 0ef8d27..d4f5584 100644
--- a/pages/en/index.js
+++ b/pages/en/index.js
@@ -1,26 +1,24 @@
-import { aniListData } from "../../lib/anilist/AniList";
+import { aniListData } from "@/lib/anilist/AniList";
import { useState, useEffect, Fragment } from "react";
import Head from "next/head";
import Link from "next/link";
-import Footer from "../../components/footer";
+import Footer from "@/components/shared/footer";
import Image from "next/image";
-import Content from "../../components/home/content";
+import Content from "@/components/home/content";
import { motion } from "framer-motion";
import { signOut, useSession } from "next-auth/react";
-import Genres from "../../components/home/genres";
-import Schedule from "../../components/home/schedule";
-import getUpcomingAnime from "../../lib/anilist/getUpcomingAnime";
+import Genres from "@/components/home/genres";
+import Schedule from "@/components/home/schedule";
+import getUpcomingAnime from "@/lib/anilist/getUpcomingAnime";
-import Navigasi from "../../components/home/staticNav";
-
-import { ToastContainer } from "react-toastify";
-import getMedia from "../../lib/anilist/getMedia";
+import GetMedia from "@/lib/anilist/getMedia";
// import UserRecommendation from "../../components/home/recommendation";
-import MobileNav from "../../components/shared/MobileNav";
-import { getGreetings } from "../../utils/getGreetings";
-import redis from "../../lib/redis";
+import MobileNav from "@/components/shared/MobileNav";
+import { getGreetings } from "@/utils/getGreetings";
+import { redis } from "@/lib/redis";
+import { NewNavbar } from "@/components/shared/NavBar";
export async function getServerSideProps() {
let cachedData;
@@ -79,9 +77,11 @@ export async function getServerSideProps() {
export default function Home({ detail, populars, upComing }) {
const { data: sessions } = useSession();
- const { media: current } = getMedia(sessions, { stats: "CURRENT" });
- const { media: plan } = getMedia(sessions, { stats: "PLANNING" });
- const { media: release, recommendations } = getMedia(sessions);
+ const { anime: currentAnime, manga: currentManga } = GetMedia(sessions, {
+ stats: "CURRENT",
+ });
+ const { anime: plan } = GetMedia(sessions, { stats: "PLANNING" });
+ const { anime: release } = GetMedia(sessions);
const [schedules, setSchedules] = useState(null);
const [anime, setAnime] = useState([]);
@@ -89,7 +89,9 @@ export default function Home({ detail, populars, upComing }) {
const [recentAdded, setRecentAdded] = useState([]);
async function getRecent() {
- const data = await fetch(`/api/v2/etc/recent/1`).then((res) => res.json());
+ const data = await fetch(`/api/v2/etc/recent/1`)
+ .then((res) => res.json())
+ .catch((err) => console.log(err));
setRecentAdded(data?.results);
}
@@ -118,13 +120,17 @@ export default function Home({ detail, populars, upComing }) {
useEffect(() => {
const getSchedule = async () => {
- const res = await fetch(`/api/v2/etc/schedule`);
- const data = await res.json();
+ try {
+ const res = await fetch(`/api/v2/etc/schedule`);
+ const data = await res.json();
- if (!res.ok) {
- setSchedules(null);
- } else {
- setSchedules(data);
+ if (!res.ok) {
+ setSchedules(null);
+ } else {
+ setSchedules(data);
+ }
+ } catch (err) {
+ console.log(err);
}
};
getSchedule();
@@ -155,7 +161,8 @@ export default function Home({ detail, populars, upComing }) {
getRelease();
}, [release]);
- const [list, setList] = useState(null);
+ const [listAnime, setListAnime] = useState(null);
+ const [listManga, setListManga] = useState(null);
const [planned, setPlanned] = useState(null);
const [user, setUser] = useState(null);
const [removed, setRemoved] = useState();
@@ -257,8 +264,14 @@ export default function Home({ detail, populars, upComing }) {
if (!sessions?.user?.name) return;
const getMedia =
- current.filter((item) => item.status === "CURRENT")[0] || null;
- const list = getMedia?.entries
+ currentAnime.find((item) => item.status === "CURRENT") || null;
+ const listAnime = getMedia?.entries
+ .map(({ media }) => media)
+ .filter((media) => media);
+
+ const getManga =
+ currentManga?.find((item) => item.status === "CURRENT") || null;
+ const listManga = getManga?.entries
.map(({ media }) => media)
.filter((media) => media);
@@ -266,15 +279,20 @@ export default function Home({ detail, populars, upComing }) {
.map(({ media }) => media)
.filter((media) => media);
- if (list) {
- setList(list);
+ if (listManga) {
+ setListManga(listManga);
+ }
+ if (listAnime) {
+ setListAnime(listAnime);
}
if (planned) {
setPlanned(planned);
}
}
userData();
- }, [sessions?.user?.name, current, plan]);
+ }, [sessions?.user?.name, currentAnime, plan]);
+
+ // console.log({ recentAdded });
return (
<Fragment>
@@ -321,15 +339,8 @@ export default function Home({ detail, populars, upComing }) {
</Head>
<MobileNav sessions={sessions} hideProfile={true} />
+ <NewNavbar paddingY="pt-2 lg:pt-10" withNav={true} home={true} />
<div className="h-auto w-screen bg-[#141519] text-[#dbdcdd]">
- <Navigasi />
- <ToastContainer
- pauseOnHover={false}
- style={{
- width: "400px",
- }}
- />
-
{/* PC / TABLET */}
<div className=" hidden justify-center lg:flex my-16">
<div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between">
@@ -359,8 +370,8 @@ export default function Home({ detail, populars, upComing }) {
draggable={false}
src={data.coverImage?.extraLarge || data.image}
alt={`cover ${data.title.english || data.title.romaji}`}
- width="0"
- height="0"
+ width={1200}
+ height={1200}
priority
className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]"
/>
@@ -434,7 +445,7 @@ export default function Home({ detail, populars, upComing }) {
</motion.section>
)}
- {sessions && list?.length > 0 && (
+ {sessions && listAnime?.length > 0 && (
<motion.section // Add motion.div to each child component
key="listAnime"
initial={{ y: 20, opacity: 0 }}
@@ -445,13 +456,31 @@ export default function Home({ detail, populars, upComing }) {
<Content
ids="listAnime"
section="Your Watch List"
- data={list}
+ data={listAnime}
og={prog}
userName={sessions?.user?.name}
/>
</motion.section>
)}
+ {sessions && listManga?.length > 0 && (
+ <motion.section // Add motion.div to each child component
+ key="listManga"
+ initial={{ y: 20, opacity: 0 }}
+ whileInView={{ y: 0, opacity: 1 }}
+ transition={{ duration: 0.5 }}
+ viewport={{ once: true }}
+ >
+ <Content
+ ids="listManga"
+ section="Your Manga List"
+ data={listManga}
+ // og={prog}
+ userName={sessions?.user?.name}
+ />
+ </motion.section>
+ )}
+
{/* {recommendations.length > 0 && (
<div className="space-y-5 mb-10">
<div className="px-5">
diff --git a/pages/en/manga/[id].js b/pages/en/manga/[id].js
index e928bd4..6f25532 100644
--- a/pages/en/manga/[id].js
+++ b/pages/en/manga/[id].js
@@ -1,14 +1,14 @@
-import ChapterSelector from "../../../components/manga/chapters";
-import HamburgerMenu from "../../../components/manga/mobile/hamburgerMenu";
-import Navbar from "../../../components/navbar";
-import TopSection from "../../../components/manga/info/topSection";
-import Footer from "../../../components/footer";
+import ChapterSelector from "@/components/manga/chapters";
+import HamburgerMenu from "@/components/manga/mobile/hamburgerMenu";
+import TopSection from "@/components/manga/info/topSection";
+import Footer from "@/components/shared/footer";
import Head from "next/head";
import { useEffect, useState } from "react";
import { setCookie } from "nookies";
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import getAnifyInfo from "../../../lib/anify/info";
+import getAnifyInfo from "@/lib/anify/info";
+import { NewNavbar } from "@/components/shared/NavBar";
export default function Manga({ info, userManga }) {
const [domainUrl, setDomainUrl] = useState("");
@@ -53,7 +53,7 @@ export default function Manga({ info, userManga }) {
</Head>
<div className="min-h-screen w-screen flex flex-col items-center relative">
<HamburgerMenu />
- <Navbar className="absolute top-0 w-full z-40" />
+ <NewNavbar info={info} manga={true} />
<div className="flex flex-col w-screen items-center gap-5 md:gap-10 py-10 pt-nav">
<div className="flex-center w-full relative z-30">
<TopSection info={info} firstEp={firstEp} setCookie={setCookie} />
diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js
index b71f8a7..a7769e2 100644
--- a/pages/en/manga/read/[...params].js
+++ b/pages/en/manga/read/[...params].js
@@ -1,20 +1,19 @@
import { useEffect, useRef, useState } from "react";
-import { LeftBar } from "../../../../components/manga/leftBar";
+import { LeftBar } from "@/components/manga/leftBar";
import { useRouter } from "next/router";
-import RightBar from "../../../../components/manga/rightBar";
-import FirstPanel from "../../../../components/manga/panels/firstPanel";
-import SecondPanel from "../../../../components/manga/panels/secondPanel";
-import ThirdPanel from "../../../../components/manga/panels/thirdPanel";
+import RightBar from "@/components/manga/rightBar";
+import FirstPanel from "@/components/manga/panels/firstPanel";
+import SecondPanel from "@/components/manga/panels/secondPanel";
+import ThirdPanel from "@/components/manga/panels/thirdPanel";
import { getServerSession } from "next-auth";
import { authOptions } from "../../../api/auth/[...nextauth]";
-import BottomBar from "../../../../components/manga/mobile/bottomBar";
-import TopBar from "../../../../components/manga/mobile/topBar";
-import { ToastContainer } from "react-toastify";
+import BottomBar from "@/components/manga/mobile/bottomBar";
+import TopBar from "@/components/manga/mobile/topBar";
import Head from "next/head";
import nookies from "nookies";
-import ShortCutModal from "../../../../components/manga/modals/shortcutModal";
-import ChapterModal from "../../../../components/manga/modals/chapterModal";
-import getAnifyPage from "../../../../lib/anify/page";
+import ShortCutModal from "@/components/manga/modals/shortcutModal";
+import ChapterModal from "@/components/manga/modals/chapterModal";
+import getAnifyPage from "@/lib/anify/page";
export default function Read({ data, currentId, sessions }) {
const [info, setInfo] = useState();
@@ -121,7 +120,6 @@ export default function Read({ data, currentId, sessions }) {
<meta id="CoverImage" data-manga-cover={info?.coverImage} />
</Head>
<div className="w-screen flex justify-evenly relative">
- <ToastContainer pauseOnFocusLoss={false} />
<ShortCutModal isOpen={isKeyOpen} setIsOpen={setIsKeyOpen} />
<ChapterModal
id={info?.id}
diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].js
index 2961328..b931597 100644
--- a/pages/en/profile/[user].js
+++ b/pages/en/profile/[user].js
@@ -1,12 +1,12 @@
import { getServerSession } from "next-auth";
import { authOptions } from "../../api/auth/[...nextauth]";
-import Navbar from "../../../components/navbar";
import Image from "next/image";
import Link from "next/link";
import Head from "next/head";
import { useEffect, useState } from "react";
-import { getUser } from "../../../prisma/user";
-import { ToastContainer, toast } from "react-toastify";
+import { getUser } from "@/prisma/user";
+import { toast } from "react-toastify";
+import { NewNavbar } from "@/components/shared/NavBar";
export default function MyList({ media, sessions, user, time, userSettings }) {
const [listFilter, setListFilter] = useState("all");
@@ -57,8 +57,7 @@ export default function MyList({ media, sessions, user, time, userSettings }) {
<Head>
<title>My Lists</title>
</Head>
- <Navbar />
- <ToastContainer pauseOnHover={false} />
+ <NewNavbar />
<div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 relative">
<div className="lg:w-[30%] h-full mt-12 lg:mr-10 grid gap-5 mx-3 lg:mx-0 antialiased">
diff --git a/pages/en/schedule/index.js b/pages/en/schedule/index.js
index ddb0a49..f1e6730 100644
--- a/pages/en/schedule/index.js
+++ b/pages/en/schedule/index.js
@@ -1,24 +1,24 @@
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
-import { NewNavbar } from "../../../components/anime/mobile/topSection";
import Link from "next/link";
import { CalendarIcon } from "@heroicons/react/24/solid";
import { ClockIcon } from "@heroicons/react/24/outline";
-import Loading from "../../../components/shared/loading";
-import { timeStamptoAMPM, timeStamptoHour } from "../../../utils/getTimes";
+import Loading from "@/components/shared/loading";
+import { timeStamptoAMPM, timeStamptoHour } from "@/utils/getTimes";
import {
filterFormattedSchedule,
filterScheduleByDay,
sortScheduleByDay,
transformSchedule,
-} from "../../../utils/schedulesUtils";
+} from "@/utils/schedulesUtils";
-import { scheduleQuery } from "../../../lib/graphql/query";
-import MobileNav from "../../../components/shared/MobileNav";
+import { scheduleQuery } from "@/lib/graphql/query";
+import MobileNav from "@/components/shared/MobileNav";
import { useSession } from "next-auth/react";
-import redis from "../../../lib/redis";
+import { redis } from "@/lib/redis";
import Head from "next/head";
+import { NewNavbar } from "@/components/shared/NavBar";
const day = [
"Sunday",
@@ -64,9 +64,6 @@ export async function getServerSideProps() {
if (cachedData) {
const scheduleByDay = JSON.parse(cachedData);
- // const today = now.getDay();
- // const todaySchedule = day[today];
-
return {
props: {
schedule: scheduleByDay,
@@ -81,21 +78,6 @@ export async function getServerSideProps() {
const weekStart = yesterdayStart;
const weekEnd = weekStart + 604800;
- // const today = now.getDay();
- // const todaySchedule = day[today];
-
- // const now = new Date();
- // const currentDayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
-
- // // Calculate the number of seconds until the current Saturday at 00:00:00
- // const secondsUntilSaturday = (6 - currentDayOfWeek) * 24 * 60 * 60;
-
- // // Calculate weekStart as the current time minus secondsUntilSaturday
- // const weekStart = Math.floor(now.getTime() / 1000) - secondsUntilSaturday;
-
- // // Calculate weekEnd as one week from weekStart
- // const weekEnd = weekStart + 604800; // One week in seconds
-
let page = 1;
const airingSchedules = [];
@@ -283,8 +265,8 @@ export default function Schedule({ schedule }) {
/>
</Head>
<MobileNav sessions={session} hideProfile={true} />
+ <NewNavbar scrollP={10} toTop={true} />
<div className="w-screen">
- <NewNavbar scrollP={10} session={session} toTop={true} />
<span className="absolute z-20 top-0 left-0 w-screen h-[190px] lg:h-[250px] bg-secondary overflow-hidden">
<div className="absolute top-40 lg:top-36 w-full h-full bg-primary rounded-t-3xl xl:rounded-t-[50px]" />
</span>
@@ -376,13 +358,13 @@ export default function Schedule({ schedule }) {
key={m.id}
// id={`same_${m.id}`}
href={`/en/${m.type.toLowerCase()}/${m.id}`}
- className={`flex bg-secondary rounded group cursor-pointer ml-4 z-50`}
+ className={`flex bg-secondary rounded group cursor-pointer overflow-hidden ml-4 z-50`}
>
<Image
src={m.coverImage.extraLarge}
alt="image"
- width="0"
- height="0"
+ width={300}
+ height={300}
className="w-[50px] h-[65px] object-cover shrink-0"
/>
<div className="flex flex-col justify-center font-karla p-2">
@@ -469,31 +451,12 @@ export default function Schedule({ schedule }) {
<span className="tooltip">Airing Now</span>
</span>
</p>
- {/* <span
- className={`${
- s.id === nextAiringAnime
- ? "bg-orange-700 text-sm px-3 py-1 rounded-full font-bold text-white"
- : ""
- } mx-auto`}
- >
- Airing Next
- </span> */}
- {/* </p> */}
- {/* {s.media?.bannerImage && (
- <Image
- src={s.media?.bannerImage}
- alt="banner"
- width="0"
- height="0"
- className="absolute pointer-events-none top-0 opacity-0 group-hover:opacity-10 transition-all duration-500 ease-linear -z-10 left-0 rounded-l w-full h-[250px] object-cover"
- />
- )} */}
<Image
src={m.coverImage.extraLarge}
alt="image"
- width="0"
- height="0"
- className="w-[50px] h-[65px] object-cover shrink-0"
+ width={200}
+ height={200}
+ className="w-[50px] h-[65px] object-cover shrink-0 rounded-l"
/>
<div className="flex flex-col justify-center font-karla p-2">
<h1 className="font-semibold line-clamp-1 text-sm group-hover:text-action transition-all duration-200 ease-out">
diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].js
index 2ec7681..603cd17 100644
--- a/pages/en/search/[...param].js
+++ b/pages/en/search/[...param].js
@@ -3,14 +3,13 @@ import { AnimatePresence, motion as m } from "framer-motion";
import Skeleton from "react-loading-skeleton";
import { useRouter } from "next/router";
import Link from "next/link";
-import Navbar from "../../../components/navbar";
import Head from "next/head";
-import Footer from "../../../components/footer";
+import Footer from "@/components/shared/footer";
import Image from "next/image";
-import { aniAdvanceSearch } from "../../../lib/anilist/aniAdvanceSearch";
-import MultiSelector from "../../../components/search/dropdown/multiSelector";
-import SingleSelector from "../../../components/search/dropdown/singleSelector";
+import { aniAdvanceSearch } from "@/lib/anilist/aniAdvanceSearch";
+import MultiSelector from "@/components/search/dropdown/multiSelector";
+import SingleSelector from "@/components/search/dropdown/singleSelector";
import {
animeFormatOptions,
formatOptions,
@@ -20,12 +19,12 @@ import {
seasonOptions,
tagsOption,
yearOptions,
-} from "../../../components/search/selection";
-import InputSelect from "../../../components/search/dropdown/inputSelect";
+} from "@/components/search/selection";
+import InputSelect from "@/components/search/dropdown/inputSelect";
import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/20/solid";
-import useDebounce from "../../../lib/hooks/useDebounce";
-// import { NewNavbar } from "../../../components/anime/mobile/topSection";
-// import { useSession } from "next-auth/react";
+import useDebounce from "@/lib/hooks/useDebounce";
+import { NewNavbar } from "@/components/shared/NavBar";
+import MobileNav from "@/components/shared/MobileNav";
export async function getServerSideProps(context) {
const { param } = context.query;
@@ -211,9 +210,15 @@ export default function Card({
<meta name="description" content="Search your favourites Anime/Manga" />
<link rel="icon" href="/svg/c.svg" />
</Head>
- <Navbar />
- {/* <NewNavbar session={session} /> */}
- <main className="w-screen min-h-screen z-40">
+
+ <NewNavbar
+ scrollP={10}
+ withNav={true}
+ shrink={true}
+ paddingY="py-1 lg:py-3"
+ />
+ <MobileNav hideProfile={true} />
+ <main className="w-screen min-h-screen z-40 py-14 lg:py-24">
<div className="max-w-screen-xl flex flex-col gap-3 mx-auto">
<div className="w-full flex justify-between items-end gap-2 my-3 lg:gap-10 px-5 xl:px-0 relative">
<div className="hidden lg:flex items-end w-full gap-5 z-50">
diff --git a/pages/id/index.js b/pages/id/index.js
index 661bc05..b8898e5 100644
--- a/pages/id/index.js
+++ b/pages/id/index.js
@@ -1,9 +1,9 @@
import Head from "next/head";
import React from "react";
-import Navbar from "../../components/navbar";
import Image from "next/image";
import Link from "next/link";
-import Footer from "../../components/footer";
+import Footer from "@/components/shared/footer";
+import { NewNavbar } from "@/components/shared/NavBar";
export default function Home() {
return (
@@ -15,7 +15,7 @@ export default function Home() {
<link rel="icon" href="/svg/c.svg" />
</Head>
<main className="flex flex-col h-screen">
- <Navbar className="bg-[#0c0d10] z-50" />
+ <NewNavbar />
{/* Create an under construction page with tailwind css */}
<div className="h-full w-screen flex-center flex-grow flex-col">
<Image
@@ -26,7 +26,7 @@ export default function Home() {
className="w-[26vw] md:w-[15vw]"
/>
<h1 className="text-2xl sm:text-4xl xl:text-6x font-bold my-4">
- 🚧 We are still working on it 🚧
+ 🚧 Work still on progress 🚧
</h1>
<p className="text-base sm:text-lg xl:text-x text-gray-300 mb-6 text-center">
"Please be patient, as we're still working on this page and it will
diff --git a/release.md b/release.md
index 4529737..79f1090 100644
--- a/release.md
+++ b/release.md
@@ -2,8 +2,24 @@
This document contains a summary of all significant changes made to this release.
-## 🎉 Update v4.0.7
+## 🎉 Update v4.1.0
-### Fixes
+### Added
-- Improved character card UI for smaller devices
+- Added refresh button on episode list, just incase if episodes isn't up to date
+- Added manga list for Anilist user at homepage (will be available to guest user soon)
+- Added ratelimit to API endpoint
+- Watch Page
+ - Added theater mode for more immersive viewing experience
+ - Auto Play and Auto Next buttons are now included inside the player
+ - Added bug report buttons to watch page (this bug report is still experimental and will be change/update in the near future)
+ - Added share button to watch page
+- For Developer (experimental)
+ - Added Admin page for finding metadata for episodes
+ - Added broadcast system (redis is required for this)
+
+### Changed
+
+- The navbar has seen significant enhancements, with the implementation of a single component for all pages.
+- The watch page has been completely rewritten from scratch
+- Implementing import aliases to significantly improve code readability and maintainability
diff --git a/styles/globals.css b/styles/globals.css
index 29816d6..256a4f5 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -258,6 +258,16 @@ input:checked ~ span:last-child {
}
}
+/* Player CSS */
+
+.art-video-player .art-bottom {
+ @apply bg-none;
+}
+
+.parent-player-title {
+ @apply pointer-events-none px-2 py-4 absolute bottom-[90dvh] bg-white w-full h-full bg-gradient-to-b from-black/50 from-5% to-50% to-transparent z-50;
+}
+
/* Add this CSS to your global styles or a component-specific CSS file */
pre {
color: #f8f8f2;
diff --git a/utils/appendMetaToEpisodes.js b/utils/appendMetaToEpisodes.js
new file mode 100644
index 0000000..eedcbf5
--- /dev/null
+++ b/utils/appendMetaToEpisodes.js
@@ -0,0 +1,28 @@
+async function appendMetaToEpisodes(episodesData, images) {
+ // Create a dictionary for faster lookup of images based on episode number
+ const episodeImages = {};
+ images.forEach((image) => {
+ episodeImages[image.episode] = image;
+ });
+
+ // Iterate through each provider's episodes data
+ for (const providerEpisodes of episodesData) {
+ // Iterate through each episode in the provider's episodes data
+ for (const episode of providerEpisodes.episodes) {
+ // Get the episode number
+ const episodeNumber = episode.number;
+
+ // Check if there is an image available for this episode number
+ if (episodeImages[episodeNumber]) {
+ // Append the image URL to the episode data
+ episode.img = episodeImages[episodeNumber].img;
+ episode.title = episodeImages[episodeNumber].title;
+ episode.description = episodeImages[episodeNumber].description;
+ }
+ }
+ }
+
+ return episodesData;
+}
+
+export default appendMetaToEpisodes;
diff --git a/utils/combineImages.js b/utils/combineImages.js
new file mode 100644
index 0000000..abf34ed
--- /dev/null
+++ b/utils/combineImages.js
@@ -0,0 +1,26 @@
+async function appendImagesToEpisodes(episodesData, images) {
+ // Create a dictionary for faster lookup of images based on episode number
+ const episodeImages = {};
+ images.forEach((image) => {
+ episodeImages[image.episode] = image.img;
+ });
+
+ // Iterate through each provider's episodes data
+ for (const providerEpisodes of episodesData) {
+ // Iterate through each episode in the provider's episodes data
+ for (const episode of providerEpisodes.episodes) {
+ // Get the episode number
+ const episodeNumber = episode.number;
+
+ // Check if there is an image available for this episode number
+ if (episodeImages[episodeNumber]) {
+ // Append the image URL to the episode data
+ episode.img = episodeImages[episodeNumber];
+ }
+ }
+ }
+
+ return episodesData;
+}
+
+export default appendImagesToEpisodes;
diff --git a/utils/getTimes.js b/utils/getTimes.js
index 8bbc2ee..d06f797 100644
--- a/utils/getTimes.js
+++ b/utils/getTimes.js
@@ -106,3 +106,29 @@ export const timeStamptoHour = (timestamp) => {
return `${status} at ${formattedTime}`;
};
+
+export function unixTimestampToRelativeTime(unixTimestamp) {
+ const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
+ const secondsAgo = now - unixTimestamp;
+
+ const intervals = [
+ { label: "year", seconds: 31536000 },
+ { label: "month", seconds: 2592000 },
+ { label: "week", seconds: 604800 },
+ { label: "day", seconds: 86400 },
+ { label: "hour", seconds: 3600 },
+ { label: "minute", seconds: 60 },
+ { label: "second", seconds: 1 },
+ ];
+
+ for (const interval of intervals) {
+ const count = Math.floor(secondsAgo / interval.seconds);
+ if (count >= 1) {
+ return count === 1
+ ? ` ${count} ${interval.label} ago`
+ : ` ${count} ${interval.label}s ago`;
+ }
+ }
+
+ return "just now";
+}