aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFactiven <[email protected]>2023-12-24 13:03:54 +0700
committerFactiven <[email protected]>2023-12-24 13:03:54 +0700
commit50a0f0240d7fef133eb5acc1bea2b1168b08e9db (patch)
tree307e09e505580415a58d64b5fc3580e9235869f1
parentUpdate README.md (#104) (diff)
downloadmoopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.tar.xz
moopa-50a0f0240d7fef133eb5acc1bea2b1168b08e9db.zip
migrate to typescript
-rw-r--r--.gitignore6
-rw-r--r--README.md8
-rw-r--r--components/anime/episode.js156
-rw-r--r--components/anime/mobile/reused/infoChip.tsx (renamed from components/anime/mobile/reused/infoChip.js)23
-rw-r--r--components/anime/mobile/topSection.tsx (renamed from components/anime/mobile/topSection.js)325
-rw-r--r--components/anime/viewMode/thumbnailDetail.js22
-rw-r--r--components/anime/viewMode/thumbnailOnly.js16
-rw-r--r--components/anime/viewSelector.js20
-rw-r--r--components/disqus.tsx (renamed from components/disqus.js)11
-rw-r--r--components/home/content.tsx (renamed from components/home/content.js)200
-rw-r--r--components/home/recommendation.js125
-rw-r--r--components/home/schedule.js4
-rw-r--r--components/listEditor.tsx (renamed from components/listEditor.js)35
-rw-r--r--components/manga/ChaptersComponent.js89
-rw-r--r--components/manga/chapters.js2
-rw-r--r--components/manga/leftBar.js2
-rw-r--r--components/manga/mobile/bottomBar.js2
-rw-r--r--components/manga/panels/firstPanel.js2
-rw-r--r--components/manga/panels/secondPanel.js4
-rw-r--r--components/manga/panels/thirdPanel.js2
-rw-r--r--components/modal.tsx (renamed from components/modal.js)8
-rw-r--r--components/search/searchByImage.tsx (renamed from components/search/searchByImage.js)61
-rw-r--r--components/search/selection.ts (renamed from components/search/selection.js)0
-rw-r--r--components/searchPalette.tsx (renamed from components/searchPalette.js)58
-rw-r--r--components/shared/MobileNav.tsx (renamed from components/shared/MobileNav.js)14
-rw-r--r--components/shared/NavBar.tsx (renamed from components/shared/NavBar.js)62
-rw-r--r--components/shared/bugReport.tsx (renamed from components/shared/bugReport.js)13
-rw-r--r--components/shared/changelogs.tsx265
-rw-r--r--components/shared/footer.tsx (renamed from components/shared/footer.js)10
-rw-r--r--components/shared/hamburgerMenu.js192
-rw-r--r--components/shared/loading.js20
-rw-r--r--components/shared/loading.tsx16
-rw-r--r--components/watch/new-player/components/bufferingIndicator.tsx15
-rw-r--r--components/watch/new-player/components/buttons.tsx277
-rw-r--r--components/watch/new-player/components/chapter-title.tsx11
-rw-r--r--components/watch/new-player/components/layouts/captions.module.css80
-rw-r--r--components/watch/new-player/components/layouts/video-layout.module.css13
-rw-r--r--components/watch/new-player/components/layouts/video-layout.tsx173
-rw-r--r--components/watch/new-player/components/menus.tsx387
-rw-r--r--components/watch/new-player/components/sliders.tsx73
-rw-r--r--components/watch/new-player/components/time-group.tsx11
-rw-r--r--components/watch/new-player/components/title.tsx35
-rw-r--r--components/watch/new-player/player.module.css50
-rw-r--r--components/watch/new-player/player.tsx471
-rw-r--r--components/watch/new-player/tracks.tsx184
-rw-r--r--components/watch/player/artplayer.js387
-rw-r--r--components/watch/player/component/controls/quality.js15
-rw-r--r--components/watch/player/component/overlay.js57
-rw-r--r--components/watch/player/playerComponent.js527
-rw-r--r--components/watch/primary/details.tsx (renamed from components/watch/primary/details.js)57
-rw-r--r--components/watch/secondary/episodeLists.tsx (renamed from components/watch/secondary/episodeLists.js)36
-rw-r--r--jsconfig.json11
-rw-r--r--lib/anify/getMangaId.ts (renamed from lib/anify/getMangaId.js)31
-rw-r--r--lib/anify/info.js24
-rw-r--r--lib/anilist/aniAdvanceSearch.ts (renamed from lib/anilist/aniAdvanceSearch.js)34
-rw-r--r--lib/anilist/getUpcomingAnime.js2
-rw-r--r--lib/anilist/useAnilist.js3
-rw-r--r--lib/context/watchPageProvider.js12
-rw-r--r--lib/hooks/useCountdownSeconds.ts (renamed from utils/useCountdownSeconds.js)25
-rw-r--r--lib/hooks/useWatchStorage.tsx28
-rw-r--r--lib/prisma.ts (renamed from lib/prisma.js)4
-rw-r--r--lib/redis.ts (renamed from lib/redis.js)12
-rw-r--r--next-env.d.ts5
-rw-r--r--next.config.js29
-rw-r--r--package-lock.json556
-rw-r--r--package.json23
-rw-r--r--pages/404.tsx (renamed from pages/404.js)5
-rw-r--r--pages/_app.tsx (renamed from pages/_app.js)66
-rw-r--r--pages/_document.tsx (renamed from pages/_document.js)0
-rw-r--r--pages/_error.tsx (renamed from pages/_error.js)8
-rw-r--r--pages/_offline.tsx (renamed from pages/_offline.js)0
-rw-r--r--pages/api/auth/[...nextauth].ts (renamed from pages/api/auth/[...nextauth].js)65
-rw-r--r--pages/api/og.tsx (renamed from pages/api/og.jsx)2
-rw-r--r--pages/api/v2/episode/[id].tsx (renamed from pages/api/v2/episode/[id].js)128
-rw-r--r--pages/api/v2/etc/recent/[page].js57
-rw-r--r--pages/api/v2/etc/recent/[page].tsx81
-rw-r--r--pages/api/v2/etc/schedule/index.tsx (renamed from pages/api/v2/etc/schedule/index.js)35
-rw-r--r--pages/en/about.tsx (renamed from pages/en/about.js)4
-rw-r--r--pages/en/anime/[...id].tsx (renamed from pages/en/anime/[...id].js)79
-rw-r--r--pages/en/anime/recent.js2
-rw-r--r--pages/en/anime/watch/[...info].js213
-rw-r--r--pages/en/contact.tsx (renamed from pages/en/contact.js)4
-rw-r--r--pages/en/dmca.tsx (renamed from pages/en/dmca.js)4
-rw-r--r--pages/en/index.tsx (renamed from pages/en/index.js)242
-rw-r--r--pages/en/manga/[...id].js427
-rw-r--r--pages/en/manga/[...id].tsx456
-rw-r--r--pages/en/manga/read/[...params].js1
-rw-r--r--pages/en/profile/[user].tsx (renamed from pages/en/profile/[user].js)83
-rw-r--r--pages/en/schedule/index.tsx (renamed from pages/en/schedule/index.js)35
-rw-r--r--pages/en/search/[...param].tsx (renamed from pages/en/search/[...param].js)211
-rw-r--r--pages/id/index.tsx (renamed from pages/id/index.js)4
-rw-r--r--pages/id/manga/[...id].tsx159
-rw-r--r--pages/id/manga/read/[...id].tsx87
-rw-r--r--pages/id/novel/[...id].tsx121
-rw-r--r--pages/id/novel/read/index.tsx115
-rw-r--r--pages/id/search.tsx221
-rw-r--r--pages/index.tsx (renamed from pages/index.js)0
-rw-r--r--prisma/user.ts (renamed from prisma/user.js)112
-rw-r--r--public/icon-144x144.pngbin0 -> 41356 bytes
-rw-r--r--public/manifest.json6
-rw-r--r--public/robots.txt5
-rw-r--r--styles/globals.css34
-rw-r--r--tailwind.config.cjs (renamed from tailwind.config.js)13
-rw-r--r--tsconfig.json34
-rw-r--r--types/api/AnifyEpisode.ts16
-rw-r--r--types/api/ConsumetInfo.ts154
-rw-r--r--types/api/Episode.ts19
-rw-r--r--types/episodes/AnifyRecentEpisode.ts91
-rw-r--r--types/episodes/ConsumetInfo.ts126
-rw-r--r--types/episodes/Sessions.ts30
-rw-r--r--types/episodes/TrackData.ts70
-rw-r--r--types/index.tsx17
-rw-r--r--types/info/AnifySearchAdvanceTypes.ts87
-rw-r--r--types/info/AnilistInfoTypes.ts138
-rw-r--r--utils/appendMetaToEpisodes.ts (renamed from utils/appendMetaToEpisodes.js)29
-rw-r--r--utils/combineImages.ts (renamed from utils/combineImages.js)21
-rw-r--r--utils/getFormat.ts (renamed from utils/getFormat.js)2
-rw-r--r--utils/getGreetings.ts (renamed from utils/getGreetings.js)0
-rw-r--r--utils/getRedisWithPrefix.ts (renamed from utils/getRedisWithPrefix.js)12
-rw-r--r--utils/getTimes.ts (renamed from utils/getTimes.js)36
-rw-r--r--utils/imageUtils.ts (renamed from utils/imageUtils.js)23
-rw-r--r--utils/parseMetaData.ts36
-rw-r--r--utils/request/index.ts111
-rw-r--r--utils/schedulesUtils.ts (renamed from utils/schedulesUtils.js)55
124 files changed, 6648 insertions, 2975 deletions
diff --git a/.gitignore b/.gitignore
index 1e8ff29..74d05ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,8 +8,11 @@
# testing
/coverage
-/pages/en/test.js
+/pages/en/test.tsx
+/pages/en/test-player.tsx
/components/devComp
+/components/test
+/pages/en/w2g.tsx
# next.js
/.next/
@@ -27,6 +30,7 @@ docker-compose.yml
/assets/dummyData.json
/backup
release-template.md
+.vscode
# debug
npm-debug.log*
diff --git a/README.md b/README.md
index 3abbb59..2b4b198 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,8 @@
</p>
+> ⚠️ **DISCLAIMER**: This branch is not stable. Any errors or issues encountered while using this code will not be supported or addressed by me. Use this code at your own risk. I will not provide assistance for any problems that arise from using this code.
+
<p align="center">
<img src="https://github.com/Ani-Moopa/Moopa/assets/97084324/c17d5d6a-36a2-4d08-957d-ad4683dcdf0d" alt="main">
</p>
@@ -52,7 +54,7 @@
</details>
-> **Warning:** If you are not familiar with JavaScript or any other programming language related to this project, please learn it first before attempting to work on this project. **I won't be able to help anyone who doesn't know how to do basic stuff.**
+> **Warning:** If you are not familiar with JavaScript or any other programming language related to this project, please learn it first before attempting to work on this project. **I won't help anyone who doesn't know how to do basic stuff.**
## Introduction
@@ -146,10 +148,6 @@ https://your-website-domain/api/auth/callback/AniListProvider
```bash
npx prisma migrate dev
npx prisma generate
-
-### NOTE
-# If you get a vercel build error related to prisma that says prisma detected but no initialized just change the following line in package.json line number 8
-"build": "next build" to > "build": "npx prisma migrate deploy && npx prisma generate && next build"
```
6. Start local server :
diff --git a/components/anime/episode.js b/components/anime/episode.js
index 3650944..f35df10 100644
--- a/components/anime/episode.js
+++ b/components/anime/episode.js
@@ -6,29 +6,45 @@ import ThumbnailDetail from "./viewMode/thumbnailDetail";
import ListMode from "./viewMode/listMode";
import { toast } from "sonner";
-function allProvider(response, setMapProviders, setProviderId) {
- const getMap = response.find((i) => i?.map === true);
- let allProvider = response;
+const ITEMS_PER_PAGE = 13;
+const DEFAULT_VIEW = 3;
- if (getMap) {
- allProvider = response.filter((i) => {
+const fetchEpisodes = async (info, isDub, refresh = false) => {
+ const response = await fetch(
+ `/api/v2/episode/${info.id}?releasing=${
+ info.status === "RELEASING" ? "true" : "false"
+ }${isDub ? "&dub=true" : ""}${refresh ? "&refresh=true" : ""}`
+ ).then((res) => res.json());
+
+ const providers = filterProviders(response);
+
+ return providers;
+};
+
+const filterProviders = (response) => {
+ const providersWithMap = response.find((i) => i?.map === true);
+ let providers = response;
+
+ if (providersWithMap) {
+ providers = response.filter((i) => {
if (i?.providerId === "gogoanime" && i?.map !== true) {
return null;
}
return i;
});
- setMapProviders(getMap?.episodes);
}
- if (allProvider.length > 0) {
- const defaultProvider = allProvider.find(
+ return providers;
+};
+
+const setDefaultProvider = (providers, setProviderId) => {
+ if (providers.length > 0) {
+ const defaultProvider = providers.find(
(x) => x.providerId === "gogoanime" || x.providerId === "9anime"
);
- setProviderId(defaultProvider?.providerId || allProvider[0].providerId); // set to first provider id
+ setProviderId(defaultProvider?.providerId || providers[0].providerId);
}
-
- return allProvider;
-}
+};
export default function AnimeEpisode({
info,
@@ -48,20 +64,13 @@ export default function AnimeEpisode({
const [isDub, setIsDub] = useState(false);
const [providers, setProviders] = useState(null);
- const [mapProviders, setMapProviders] = useState(null);
useEffect(() => {
setLoading(true);
const fetchData = async () => {
- const response = await fetch(
- `/api/v2/episode/${info.id}?releasing=${
- info.status === "RELEASING" ? "true" : "false"
- }${isDub ? "&dub=true" : ""}`
- ).then((res) => res.json());
-
- const providers = allProvider(response, setMapProviders, setProviderId);
-
- setView(Number(localStorage.getItem("view")) || 3);
+ const providers = await fetchEpisodes(info, isDub);
+ setDefaultProvider(providers, setProviderId);
+ setView(Number(localStorage.getItem("view")) || DEFAULT_VIEW);
setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
setProviders(providers);
setLoading(false);
@@ -71,20 +80,16 @@ export default function AnimeEpisode({
return () => {
setCurrentPage(1);
setProviders(null);
- setMapProviders(null);
};
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [info.id, isDub]);
+ }, [info.id, isDub]); // eslint-disable-next-line react-hooks/exhaustive-deps
const episodes =
- providers
- ?.find((provider) => provider.providerId === providerId)
- ?.episodes?.slice(0, mapProviders?.length) || [];
+ providers?.find((provider) => provider.providerId === providerId)
+ ?.episodes || [];
const lastEpisodeIndex = currentPage * itemsPerPage;
const firstEpisodeIndex = lastEpisodeIndex - itemsPerPage;
- let currentEpisodes = episodes.slice(firstEpisodeIndex, lastEpisodeIndex);
+ let currentEpisodes = episodes?.slice(firstEpisodeIndex, lastEpisodeIndex);
const totalPages = Math.ceil(episodes.length / itemsPerPage);
@@ -98,9 +103,10 @@ export default function AnimeEpisode({
useEffect(() => {
if (
- !mapProviders ||
- mapProviders?.every(
+ !currentEpisodes ||
+ currentEpisodes?.every(
(item) =>
+ // item?.img?.includes("null") ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item?.img === null
@@ -173,67 +179,13 @@ export default function AnimeEpisode({
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) {
- const json = await res.json();
- if (res.status === 429) {
- toast.error(json.error);
- const resp = await fetch(
- `/api/v2/episode/${info.id}?releasing=${
- info.status === "RELEASING" ? "true" : "false"
- }${isDub ? "&dub=true" : ""}`
- ).then((res) => res.json());
-
- if (resp) {
- const providers = allProvider(
- resp,
- setMapProviders,
- setProviderId
- );
- setProviders(providers);
- }
- } else {
- toast.error("Something went wrong");
- setProviders([]);
- }
- setLoading(false);
- } else {
- const remainingRequests = res.headers.get("X-RateLimit-Remaining");
- toast.success("Remaining requests " + remainingRequests);
-
- const data = await res.json();
- const getMap = data.find((i) => i?.map === true) || data[0];
- 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);
+ const providers = await fetchEpisodes(info, isDub, true);
+ setDefaultProvider(providers, setProviderId);
+ setView(Number(localStorage.getItem("view")) || DEFAULT_VIEW);
+ setArtStorage(JSON.parse(localStorage.getItem("artplayer_settings")));
+ setProviders(providers);
+ setLoading(false);
+ }, 5000);
} catch (err) {
console.log(err);
toast.error("Something went wrong");
@@ -257,7 +209,7 @@ export default function AnimeEpisode({
onClick={() => {
handleRefresh();
setProviders(null);
- setMapProviders(null);
+ // setMapProviders(null);
}}
className="relative flex flex-col items-center w-5 h-5 group"
>
@@ -376,7 +328,7 @@ export default function AnimeEpisode({
view={view}
setView={setView}
episode={currentEpisodes}
- map={mapProviders}
+ // map={mapProviders}
/>
</div>
</div>
@@ -395,9 +347,9 @@ export default function AnimeEpisode({
{Array.isArray(providers) ? (
providers.length > 0 ? (
currentEpisodes.map((episode, index) => {
- const mapData = mapProviders?.find(
- (i) => i.number === episode.number
- );
+ // const mapData = mapProviders?.find(
+ // (i) => i.number === episode.number
+ // );
return (
<Fragment key={index}>
@@ -406,7 +358,7 @@ export default function AnimeEpisode({
key={index}
index={index}
info={info}
- image={mapData?.img || mapData?.image}
+ // image={mapData?.img || mapData?.image}
providerId={providerId}
episode={episode}
artStorage={artStorage}
@@ -417,9 +369,9 @@ export default function AnimeEpisode({
{view === 2 && (
<ThumbnailDetail
key={index}
- image={mapData?.img || mapData?.image}
- title={mapData?.title}
- description={mapData?.description}
+ // image={mapData?.img || mapData?.image}
+ // title={mapData?.title}
+ // description={mapData?.description}
index={index}
epi={episode}
provider={providerId}
diff --git a/components/anime/mobile/reused/infoChip.js b/components/anime/mobile/reused/infoChip.tsx
index 41def85..80ebf83 100644
--- a/components/anime/mobile/reused/infoChip.js
+++ b/components/anime/mobile/reused/infoChip.tsx
@@ -1,7 +1,20 @@
-import React from "react";
-import { getFormat } from "../../../../utils/getFormat";
+import React, { CSSProperties, FC } from "react";
+import { getFormat } from "@/utils/getFormat";
-export default function InfoChip({ info, color, className }) {
+interface Info {
+ episodes?: number;
+ averageScore?: number;
+ format?: string;
+ status?: string;
+}
+
+interface InfoChipProps {
+ info: Info;
+ color: any;
+ className: string;
+}
+
+const InfoChip: FC<InfoChipProps> = ({ info, color, className }) => {
return (
<div
className={`flex-wrap w-full justify-start md:pt-1 gap-4 ${className}`}
@@ -40,4 +53,6 @@ export default function InfoChip({ info, color, className }) {
)}
</div>
);
-}
+};
+
+export default InfoChip;
diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.tsx
index 6780da5..2d28c66 100644
--- a/components/anime/mobile/topSection.js
+++ b/components/anime/mobile/topSection.tsx
@@ -11,26 +11,36 @@ import { convertSecondsToTime } from "@/utils/getTimes";
import Link from "next/link";
import InfoChip from "./reused/infoChip";
import Description from "./reused/description";
-import { NewNavbar } from "@/components/shared/NavBar";
+import Skeleton from "react-loading-skeleton";
+import { AniListInfoTypes } from "types/info/AnilistInfoTypes";
+
+type DetailTopProps = {
+ info?: AniListInfoTypes | null;
+ statuses?: any;
+ handleOpen: () => void;
+ watchUrl: string | undefined;
+ progress?: number;
+ color?: string | null;
+};
export default function DetailTop({
info,
- statuses,
+ statuses = undefined,
handleOpen,
watchUrl,
progress,
color,
-}) {
+}: DetailTopProps) {
const router = useRouter();
const [readMore, setReadMore] = useState(false);
const [showAll, setShowAll] = useState(false);
- const isAnime = info.type === "ANIME";
+ const isAnime = info?.type === "ANIME";
useEffect(() => {
setReadMore(false);
- }, [info.id]);
+ }, [info?.id]);
const handleShareClick = async () => {
try {
@@ -51,78 +61,150 @@ 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} />
-
{/* MAIN */}
<div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12">
<div className="shrink-0 w-[180px] h-[250px] rounded overflow-hidden">
- <img
- src={info?.coverImage?.extraLarge || info?.coverImage}
- alt="poster anime"
- width={300}
- height={300}
- className="w-full h-full object-cover"
- />
+ {info ? (
+ <img
+ src={
+ info?.coverImage?.extraLarge?.toString() ??
+ info?.coverImage?.toString()
+ }
+ alt="poster anime"
+ width={300}
+ height={300}
+ className="w-full h-full object-cover"
+ />
+ ) : (
+ <Skeleton className="h-full" />
+ )}
</div>
<div className="flex flex-col gap-4 items-center md:items-start justify-end w-full">
- <div className="flex flex-col gap-1 text-center md:text-start">
+ <div className="flex flex-col gap-1 text-center md:text-start w-full">
<h3 className="font-karla text-lg capitalize leading-none">
{info?.season?.toLowerCase() || getMonth(info?.startDate?.month)}{" "}
- {info.seasonYear || info?.startDate?.year}
+ {info?.seasonYear || info?.startDate?.year}
+ {!info && <Skeleton height={14} width={140} />}
</h3>
<h1 className="font-outfit font-extrabold text-2xl md:text-4xl line-clamp-2 text-white">
{info?.title?.romaji || info?.title?.english}
+ {!info && <Skeleton height={35} width={340} className="" />}
</h1>
<h2 className="font-karla line-clamp-1 text-sm md:text-lg md:pb-2 font-light text-white/70">
- {info.title?.english}
+ {info?.title?.english}
</h2>
- <InfoChip info={info} color={color} className="hidden md:flex" />
- {info?.description && (
- <Description
- info={info}
- readMore={readMore}
- setReadMore={setReadMore}
- className="md:block hidden"
- />
+ {info && (
+ <InfoChip info={info} color={color} className="hidden md:flex" />
+ )}
+ {info ? (
+ info?.description && (
+ <Description
+ info={info}
+ readMore={readMore}
+ setReadMore={setReadMore}
+ className="md:block hidden"
+ />
+ )
+ ) : (
+ <div className="w-full md:px-0 md:block hidden">
+ <Skeleton className="h-[80px] w-[700px]" />
+ </div>
)}
</div>
</div>
</div>
<div className="hidden md:flex gap-5 items-center justify-start w-full">
- <button
- type="button"
- onClick={() => router.push(watchUrl)}
- className={`${
- !watchUrl ? "opacity-30 pointer-events-none" : ""
- } w-[180px] flex-center text-lg font-karla font-semibold gap-2 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`}
- >
- {isAnime ? (
- <PlayIcon className="w-5 h-5" />
- ) : (
- <BookOpenIcon className="w-5 h-5" />
- )}
- {progress > 0 ? (
- statuses?.value === "COMPLETED" ? (
- isAnime ? (
- "Rewatch"
+ {info ? (
+ <button
+ type="button"
+ onClick={() => router.push(watchUrl ?? "#")}
+ className={`${
+ !watchUrl ? "opacity-30 pointer-events-none" : ""
+ } w-[180px] flex-center text-lg font-karla font-semibold gap-2 border-black border-opacity-10 text-black rounded-full py-1 px-4 bg-white hover:opacity-80`}
+ >
+ {isAnime ? (
+ <PlayIcon className="w-5 h-5" />
+ ) : (
+ <BookOpenIcon className="w-5 h-5" />
+ )}
+ {progress && progress > 0 ? (
+ statuses?.value === "COMPLETED" ? (
+ isAnime ? (
+ "Rewatch"
+ ) : (
+ "Reread"
+ )
+ ) : !watchUrl && info?.nextAiringEpisode ? (
+ <span>
+ {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "}
+ </span>
) : (
- "Reread"
+ "Continue"
)
- ) : !watchUrl && info?.nextAiringEpisode ? (
- <span>
- {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "}
- </span>
+ ) : isAnime ? (
+ "Watch Now"
) : (
- "Continue"
- )
- ) : isAnime ? (
- "Watch Now"
+ "Read Now"
+ )}
+ </button>
+ ) : (
+ <div className="h-10 w-[180px] bg-secondary flex-center text-lg font-karla font-semibold gap-2 border-black border-opacity-10 text-black rounded-full" />
+ )}
+ <div className="flex gap-2">
+ {info ? (
+ <button
+ type="button"
+ className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
+ onClick={() => handleOpen()}
+ >
+ <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">
+ Add to List
+ </span>
+ <PlusIcon className="w-5 h-5" />
+ </button>
) : (
- "Read Now"
+ <div className="w-10 h-10 bg-secondary rounded-full" />
)}
- </button>
- <div className="flex gap-2">
+ {info ? (
+ <button
+ type="button"
+ className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
+ onClick={handleShareClick}
+ >
+ <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">
+ Share {isAnime ? "Anime" : "Manga"}
+ </span>
+ <ShareIcon className="w-5 h-5" />
+ </button>
+ ) : (
+ <div className="w-10 h-10 bg-secondary rounded-full" />
+ )}
+ {info ? (
+ <a
+ target="_blank"
+ rel="noopener noreferrer"
+ href={`https://anilist.co/${info.type.toLowerCase()}/${info.id}`}
+ className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
+ >
+ <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">
+ See on AniList
+ </span>
+ <Image
+ src="/svg/anilist-icon.svg"
+ alt="anilist_icon"
+ width={20}
+ height={20}
+ />
+ </a>
+ ) : (
+ <div className="w-10 h-10 bg-secondary rounded-full" />
+ )}
+ </div>
+ </div>
+
+ <div className="md:hidden flex gap-2 items-center justify-center w-[90%]">
+ {info ? (
<button
type="button"
className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
@@ -133,6 +215,46 @@ export default function DetailTop({
</span>
<PlusIcon className="w-5 h-5" />
</button>
+ ) : (
+ <div className="w-10 h-10 bg-secondary rounded-full" />
+ )}
+ {info ? (
+ <button
+ type="button"
+ onClick={() => router.push(watchUrl ?? "#")}
+ className={`${
+ !watchUrl ? "opacity-30 pointer-events-none" : ""
+ } flex items-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-2 px-4 bg-white`}
+ >
+ {isAnime ? (
+ <PlayIcon className="w-5 h-5" />
+ ) : (
+ <BookOpenIcon className="w-5 h-5" />
+ )}
+ {progress && progress > 0 ? (
+ statuses?.value === "COMPLETED" ? (
+ isAnime ? (
+ "Rewatch"
+ ) : (
+ "Reread"
+ )
+ ) : !watchUrl && info?.nextAiringEpisode ? (
+ <span>
+ {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "}
+ </span>
+ ) : (
+ "Continue"
+ )
+ ) : isAnime ? (
+ "Watch Now"
+ ) : (
+ "Read Now"
+ )}
+ </button>
+ ) : (
+ <div className="h-10 w-32 bg-secondary flex-center text-lg font-karla font-semibold gap-2 border-black border-opacity-10 text-black rounded-full" />
+ )}
+ {info ? (
<button
type="button"
className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
@@ -143,81 +265,12 @@ export default function DetailTop({
</span>
<ShareIcon className="w-5 h-5" />
</button>
- <a
- target="_blank"
- rel="noopener noreferrer"
- href={`https://anilist.co/${info.type.toLowerCase()}/${info.id}`}
- className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
- >
- <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">
- See on AniList
- </span>
- <Image
- src="/svg/anilist-icon.svg"
- alt="anilist_icon"
- width={20}
- height={20}
- />
- </a>
- </div>
+ ) : (
+ <div className="w-10 h-10 bg-secondary rounded-full" />
+ )}
</div>
- <div className="md:hidden flex gap-2 items-center justify-center w-[90%]">
- <button
- type="button"
- className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
- onClick={() => handleOpen()}
- >
- <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">
- Add to List
- </span>
- <PlusIcon className="w-5 h-5" />
- </button>
- <button
- type="button"
- onClick={() => router.push(watchUrl)}
- className={`${
- !watchUrl ? "opacity-30 pointer-events-none" : ""
- } flex items-center text-lg font-karla font-semibold gap-1 border-black border-opacity-10 text-black rounded-full py-2 px-4 bg-white`}
- >
- {isAnime ? (
- <PlayIcon className="w-5 h-5" />
- ) : (
- <BookOpenIcon className="w-5 h-5" />
- )}
- {progress > 0 ? (
- statuses?.value === "COMPLETED" ? (
- isAnime ? (
- "Rewatch"
- ) : (
- "Reread"
- )
- ) : !watchUrl && info?.nextAiringEpisode ? (
- <span>
- {convertSecondsToTime(info.nextAiringEpisode.timeUntilAiring)}{" "}
- </span>
- ) : (
- "Continue"
- )
- ) : isAnime ? (
- "Watch Now"
- ) : (
- "Read Now"
- )}
- </button>
- <button
- type="button"
- className="flex-center group relative w-10 h-10 bg-secondary rounded-full"
- onClick={handleShareClick}
- >
- <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">
- Share {isAnime ? "Anime" : "Manga"}
- </span>
- <ShareIcon className="w-5 h-5" />
- </button>
- </div>
-
- {info.nextAiringEpisode?.timeUntilAiring && (
+ {info && info.nextAiringEpisode?.timeUntilAiring && (
<p className="md:hidden">
Episode {info.nextAiringEpisode.episode} in{" "}
<span className="font-bold">
@@ -226,7 +279,7 @@ export default function DetailTop({
</p>
)}
- {info?.description && (
+ {info && info?.description && (
<Description
info={info}
readMore={readMore}
@@ -235,13 +288,15 @@ export default function DetailTop({
/>
)}
- <InfoChip
- info={info}
- color={color}
- className={`${readMore ? "flex" : "hidden"} md:hidden`}
- />
+ {info && (
+ <InfoChip
+ info={info}
+ color={color}
+ className={`${readMore ? "flex" : "hidden"} md:hidden`}
+ />
+ )}
- {info?.relations?.edges?.length > 0 && (
+ {info && info?.relations?.edges?.length > 0 && (
<div className="w-screen md:w-full">
<div className="flex justify-between items-center p-3 md:p-0">
{info?.relations?.edges?.length > 0 && (
@@ -288,7 +343,7 @@ export default function DetailTop({
<div className="w-[90px] bg-image rounded-l-md shrink-0">
<Image
src={rel.coverImage.extraLarge}
- alt={rel.id}
+ alt={rel.id.toString()}
height={500}
width={500}
className="object-cover h-full w-full shrink-0 rounded-l-md"
@@ -314,7 +369,7 @@ export default function DetailTop({
);
}
-function getMonth(month) {
+function getMonth(month: number | undefined) {
if (!month) return "";
const formattedMonth = new Date(0, month).toLocaleString("default", {
month: "long",
diff --git a/components/anime/viewMode/thumbnailDetail.js b/components/anime/viewMode/thumbnailDetail.js
index d8cbfcc..f955fec 100644
--- a/components/anime/viewMode/thumbnailDetail.js
+++ b/components/anime/viewMode/thumbnailDetail.js
@@ -1,3 +1,4 @@
+import { parseImageProxy } from "@/utils/imageUtils";
import Image from "next/image";
import Link from "next/link";
@@ -5,7 +6,7 @@ export default function ThumbnailDetail({
index,
epi,
info,
- image,
+ // image,
title,
description,
provider,
@@ -18,10 +19,10 @@ export default function ThumbnailDetail({
let prog = (time / duration) * 100;
if (prog > 90) prog = 100;
- const parsedImage = image
- ? image?.includes("null")
+ const parsedImage = epi?.img
+ ? epi?.img?.includes("null")
? info.coverImage?.extraLarge
- : image
+ : epi?.img
: info.coverImage?.extraLarge || null;
return (
@@ -36,7 +37,12 @@ export default function ThumbnailDetail({
<div className="relative">
{parsedImage && (
<Image
- src={parsedImage || ""}
+ src={
+ parseImageProxy(
+ parsedImage,
+ provider === "animepahe" ? "https://animepahe.ru" : undefined
+ ) || ""
+ }
alt={`Episode ${epi?.number} Thumbnail`}
width={520}
height={236}
@@ -74,11 +80,11 @@ 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 || 0}`}
+ {epi?.title || `Episode ${epi?.number || 0}`}
</h1>
- {description && (
+ {epi?.description && (
<p className="line-clamp-2 text-xs lg:text-md xl:text-lg italic font-outfit font-extralight">
- {description}
+ {epi?.description}
</p>
)}
</div>
diff --git a/components/anime/viewMode/thumbnailOnly.js b/components/anime/viewMode/thumbnailOnly.js
index c7fe674..06a92f5 100644
--- a/components/anime/viewMode/thumbnailOnly.js
+++ b/components/anime/viewMode/thumbnailOnly.js
@@ -1,9 +1,10 @@
import Image from "next/image";
import Link from "next/link";
+import { parseImageProxy } from "../../../utils/imageUtils";
export default function ThumbnailOnly({
info,
- image,
+ // image,
providerId,
episode,
artStorage,
@@ -15,10 +16,10 @@ export default function ThumbnailOnly({
let prog = (time / duration) * 100;
if (prog > 90) prog = 100;
- const parsedImage = image
- ? image?.includes("null")
+ const parsedImage = episode?.img
+ ? episode?.img?.includes("null")
? info.coverImage?.extraLarge
- : image
+ : episode?.img
: info.coverImage?.extraLarge || null;
return (
<Link
@@ -45,7 +46,12 @@ export default function ThumbnailOnly({
{/* <div className="absolute inset-0 bg-black z-30 opacity-20" /> */}
{parsedImage && (
<Image
- src={parsedImage || ""}
+ src={
+ parseImageProxy(
+ parsedImage,
+ providerId === "animepahe" ? "https://animepahe.ru" : undefined
+ ) || ""
+ }
alt={`Episode ${episode?.number} Thumbnail`}
width={500}
height={500}
diff --git a/components/anime/viewSelector.js b/components/anime/viewSelector.js
index baa13b2..c2ca327 100644
--- a/components/anime/viewSelector.js
+++ b/components/anime/viewSelector.js
@@ -4,13 +4,14 @@ export default function ViewSelector({ view, setView, episode, map }) {
<div
className={
episode?.length > 0
- ? map?.every(
+ ? episode?.every(
(item) =>
item?.img === null ||
+ item?.img?.includes("null") ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
- ) || !map
+ ) || !episode
? "pointer-events-none"
: "cursor-pointer"
: "pointer-events-none"
@@ -32,13 +33,14 @@ export default function ViewSelector({ view, setView, episode, map }) {
height="20"
className={`${
episode?.length > 0
- ? map?.every(
+ ? episode?.every(
(item) =>
item?.img === null ||
+ item?.img?.includes("null") ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
- ) || !map
+ ) || !episode
? "fill-[#1c1c22]"
: view === 1
? "fill-action"
@@ -52,13 +54,14 @@ export default function ViewSelector({ view, setView, episode, map }) {
<div
className={
episode?.length > 0
- ? map?.every(
+ ? episode?.every(
(item) =>
item?.img === null ||
+ item?.img?.includes("null") ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
- ) || !map
+ ) || !episode
? "pointer-events-none"
: "cursor-pointer"
: "pointer-events-none"
@@ -75,13 +78,14 @@ export default function ViewSelector({ view, setView, episode, map }) {
fill="none"
className={`${
episode?.length > 0
- ? map?.every(
+ ? episode?.every(
(item) =>
item?.img === null ||
+ item?.img?.includes("null") ||
item?.img?.includes("https://s4.anilist.co/") ||
item?.image?.includes("https://s4.anilist.co/") ||
item.title === null
- ) || !map
+ ) || !episode
? "fill-[#1c1c22]"
: view === 2
? "fill-action"
diff --git a/components/disqus.js b/components/disqus.tsx
index b814851..dca03e2 100644
--- a/components/disqus.js
+++ b/components/disqus.tsx
@@ -1,6 +1,15 @@
import { DiscussionEmbed } from "disqus-react";
-const DisqusComments = ({ post }) => {
+type DisqusCommentsProps = {
+ post: {
+ name: string;
+ url: string;
+ title: string;
+ episode: number;
+ };
+};
+
+const DisqusComments = ({ post }: DisqusCommentsProps) => {
const disqusShortname = post.name || "your_disqus_shortname";
const disqusConfig = {
url: post.url,
diff --git a/components/home/content.js b/components/home/content.tsx
index d2498f6..b193381 100644
--- a/components/home/content.js
+++ b/components/home/content.tsx
@@ -15,6 +15,63 @@ import HistoryOptions from "./content/historyOptions";
import { toast } from "sonner";
import { truncateImgUrl } from "@/utils/imageUtils";
+type ContentProps = {
+ ids: string;
+ section: string;
+ data?: any;
+ userData?: UserDataTypes[];
+ og?: any;
+ userName?: string;
+ setRemoved?: any;
+ type?: string;
+};
+
+type UserDataTypes = {
+ id: string;
+ aniId?: string;
+ title?: string;
+ aniTitle?: string;
+ image?: string;
+ episode?: number;
+ timeWatched?: number;
+ duration?: number;
+ provider?: string;
+ nextId?: string;
+ nextNumber?: number;
+ dub?: boolean;
+ createdDate: string;
+ userProfileId: string;
+ watchId: string;
+};
+
+interface SlicedDataTypes {
+ id: string | number;
+ slug?: string;
+ nextAiringEpisode?: any;
+ currentEpisode?: number;
+ idMal: number;
+ status: string;
+ title: Title;
+ bannerImage: string;
+ coverImage: CoverImage | string;
+ image?: string;
+ episodeNumber?: number;
+ description: string;
+}
+
+interface Title {
+ romaji: string;
+ english: string;
+ native: string;
+}
+
+interface CoverImage {
+ extraLarge: string;
+ large: string;
+ medium: string;
+ color?: string;
+}
+
export default function Content({
ids,
section,
@@ -24,12 +81,12 @@ export default function Content({
userName,
setRemoved,
type = "anime",
-}) {
- const router = useRouter();
-
- const ref = useRef();
+}: ContentProps) {
+ const ref = useRef<HTMLElement>(null!);
const { events } = useDraggable(ref);
+ const router = useRouter();
+
const [clicked, setClicked] = useState(false);
useEffect(() => {
@@ -45,19 +102,27 @@ export default function Content({
const [scrollRight, setScrollRight] = useState(true);
const slideLeft = () => {
- ref.current.classList.add("scroll-smooth");
- var slider = document.getElementById(ids);
- slider.scrollLeft = slider.scrollLeft - 500;
- ref.current.classList.remove("scroll-smooth");
+ if (ref.current) {
+ ref.current.classList.add("scroll-smooth");
+ var slider = document.getElementById(ids);
+ if (slider?.scrollLeft) {
+ slider.scrollLeft = slider.scrollLeft - 500;
+ }
+ ref.current.classList.remove("scroll-smooth");
+ }
};
const slideRight = () => {
- ref.current.classList.add("scroll-smooth");
- var slider = document.getElementById(ids);
- slider.scrollLeft = slider.scrollLeft + 500;
- ref.current.classList.remove("scroll-smooth");
+ if (ref.current) {
+ ref.current.classList.add("scroll-smooth");
+ var slider = document.getElementById(ids);
+ if (slider?.scrollLeft) {
+ slider.scrollLeft = slider.scrollLeft + 500;
+ }
+ ref.current.classList.remove("scroll-smooth");
+ }
};
- const handleScroll = (e) => {
+ const handleScroll = (e: any) => {
const scrollLeft = e.target.scrollLeft > 31;
const scrollRight =
e.target.scrollLeft < e.target.scrollWidth - e.target.clientWidth;
@@ -65,10 +130,12 @@ export default function Content({
setScrollRight(scrollRight);
};
- function handleAlert(e) {
+ function handleAlert(e: string) {
if (localStorage.getItem("clicked")) {
const existingDataString = localStorage.getItem("clicked");
- const existingData = JSON.parse(existingDataString);
+ const existingData = existingDataString
+ ? JSON.parse(existingDataString)
+ : {};
existingData[e] = true;
@@ -87,8 +154,8 @@ export default function Content({
}
const array = data;
- let filteredData = array?.filter((item) => item !== null);
- const slicedData =
+ let filteredData = array?.filter((item: any) => item !== null);
+ const slicedData: SlicedDataTypes[] =
filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData;
const goToPage = () => {
@@ -112,7 +179,7 @@ export default function Content({
}
};
- const removeItem = async (id, aniId) => {
+ const removeItem = async (id: string, aniId: string) => {
if (userName) {
// remove from database
const res = await fetch(`/api/user/update/episode`, {
@@ -131,7 +198,7 @@ export default function Content({
if (id) {
// remove from local storage
const artplayerSettings =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {};
if (artplayerSettings[id]) {
delete artplayerSettings[id];
localStorage.setItem(
@@ -142,9 +209,9 @@ export default function Content({
}
if (aniId) {
const currentData =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {};
- const updatedData = {};
+ const updatedData: { [key: string]: any } = {};
for (const key in currentData) {
const item = currentData[key];
@@ -166,7 +233,7 @@ export default function Content({
if (id) {
// remove from local storage
const artplayerSettings =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {};
if (artplayerSettings[id]) {
delete artplayerSettings[id];
localStorage.setItem(
@@ -178,10 +245,10 @@ export default function Content({
}
if (aniId) {
const currentData =
- JSON.parse(localStorage.getItem("artplayer_settings")) || {};
+ JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {};
// Create a new object to store the updated data
- const updatedData = {};
+ const updatedData: { [key: string]: any } = {};
// Iterate through the current data and copy items with different aniId to the updated object
for (const key in currentData) {
@@ -223,11 +290,22 @@ export default function Content({
className="flex h-full w-full select-none overflow-x-scroll overflow-y-hidden scrollbar-hide lg:gap-8 gap-4 lg:p-10 py-8 px-5 z-30"
onScroll={handleScroll}
{...events}
- ref={ref}
+ ref={ref as React.RefObject<HTMLDivElement>}
>
{ids !== "recentlyWatched"
? slicedData?.map((anime) => {
- const progress = og?.find((i) => i.mediaId === anime.id);
+ const progress = og?.find((i: any) => i.mediaId === anime.id);
+
+ let image;
+ if (typeof anime.coverImage === "string") {
+ image = truncateImgUrl(anime.coverImage);
+ } else if (anime.coverImage) {
+ image = anime.coverImage.extraLarge || anime.coverImage.large;
+ }
+
+ if (!image && anime.image) {
+ image = anime.image;
+ }
return (
<div
@@ -238,6 +316,14 @@ export default function Content({
href={
ids === "listManga"
? `/en/manga/${anime.id}`
+ : ids === "recentAdded"
+ ? anime?.slug
+ ? `/en/anime/watch/${
+ anime.id
+ }/gogoanime?id=${encodeURIComponent(
+ anime?.slug
+ )}&num=${anime.currentEpisode}`
+ : `/en/${type}/${anime.id}`
: `/en/${type}/${anime.id}`
}
className="hover:scale-105 hover:shadow-lg duration-300 ease-out group relative"
@@ -255,7 +341,7 @@ export default function Content({
)}
{checkProgress(progress) && (
<div
- onClick={() => handleAlert(anime.id)}
+ onClick={() => handleAlert(String(anime.id))}
className="group-hover:visible invisible absolute top-0 bg-black bg-opacity-20 w-full h-full z-20 text-center"
>
<h1 className="text-[12px] lg:text-sm pt-28 lg:pt-44 font-bold opacity-100">
@@ -282,31 +368,20 @@ export default function Content({
{ids === "recentAdded" && (
<div className="absolute bg-gradient-to-b from-black/30 to-transparent from-5% to-30% top-0 z-30 w-full h-full rounded" />
)}
- <Image
- draggable={false}
- src={
- anime.image ||
- anime.coverImage?.extraLarge ||
- anime.coverImage?.large ||
- truncateImgUrl(anime?.coverImage) ||
- "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
- }
- alt={
- anime.title.romaji ||
- anime.title.english ||
- "coverImage"
- }
- width={500}
- height={300}
- placeholder="blur"
- blurDataURL={
- anime.image ||
- anime.coverImage?.extraLarge ||
- anime.coverImage?.large ||
- "https://cdn.discordapp.com/attachments/986579286397964290/1058415946945003611/gray_pfp.png"
- }
- className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90"
- />
+ {image && (
+ <Image
+ draggable={false}
+ src={image}
+ alt={
+ anime.title.romaji ||
+ anime.title.english ||
+ "coverImage"
+ }
+ width={500}
+ height={300}
+ className="z-20 h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] object-cover rounded-md brightness-90"
+ />
+ )}
</div>
{ids === "recentAdded" && (
<Fragment>
@@ -356,7 +431,7 @@ export default function Content({
.map((i) => {
const time = i.timeWatched;
const duration = i.duration;
- let prog = (time / duration) * 100;
+ let prog = time && duration ? (time / duration) * 100 : 0;
if (prog > 90) prog = 100;
return (
@@ -378,9 +453,11 @@ export default function Content({
router.push(
`/en/anime/watch/${i.aniId}/${
i.provider
- }?id=${encodeURIComponent(i?.nextId)}&num=${
- i?.nextNumber
- }${i?.dub ? `&dub=${i?.dub}` : ""}`
+ }?id=${encodeURIComponent(
+ i?.nextId || ""
+ )}&num=${i?.nextNumber}${
+ i?.dub ? `&dub=${i?.dub}` : ""
+ }`
);
}}
>
@@ -404,11 +481,11 @@ export default function Content({
<PlayIcon className="w-5 h-5 shrink-0" />
<h1
className="font-semibold font-karla line-clamp-1"
- title={i?.title || i.anititle}
+ title={i?.title || i?.aniTitle}
>
{i?.title === i.aniTitle
? `Episode ${i.episode}`
- : i?.title || i.anititle}
+ : i?.title || i?.aniTitle}
</h1>
</div>
<span
@@ -456,7 +533,8 @@ export default function Content({
</div>
);
})}
- {userData?.filter((i) => i.aniId !== null)?.length >= 10 &&
+ {userData &&
+ userData?.filter((i) => i.aniId !== null)?.length >= 10 &&
section !== "Recommendations" && (
<div
key={section}
@@ -498,7 +576,7 @@ export default function Content({
);
}
-function convertSecondsToTime(sec) {
+function convertSecondsToTime(sec: number) {
let days = Math.floor(sec / (3600 * 24));
let hours = Math.floor((sec % (3600 * 24)) / 3600);
let minutes = Math.floor((sec % 3600) / 60);
@@ -516,7 +594,7 @@ function convertSecondsToTime(sec) {
return time.trim();
}
-function checkProgress(entry) {
+function checkProgress(entry: { progress: any; media: any }) {
const { progress, media } = entry;
const { episodes, nextAiringEpisode } = media;
diff --git a/components/home/recommendation.js b/components/home/recommendation.js
index 842932c..b643456 100644
--- a/components/home/recommendation.js
+++ b/components/home/recommendation.js
@@ -1,13 +1,22 @@
import Image from "next/image";
// import data from "../../assets/dummyData.json";
-import { BookOpenIcon, PlayIcon } from "@heroicons/react/24/solid";
+import {
+ BookOpenIcon as BookOpenSolid,
+ PlayIcon,
+} from "@heroicons/react/24/solid";
import { useDraggable } from "react-use-draggable-scroll";
import { useRef } from "react";
import Link from "next/link";
+import {
+ BookOpenIcon as BookOpenOutline,
+ PlayCircleIcon,
+} from "@heroicons/react/24/outline";
export default function UserRecommendation({ data }) {
- const ref = useRef(null);
- const { events } = useDraggable(ref);
+ const mobileRef = useRef(null);
+ const desktopRef = useRef(null);
+ const { events: mobileEvent } = useDraggable(mobileRef);
+ const { events: desktopEvent } = useDraggable(desktopRef);
const uniqueRecommendationIds = new Set();
@@ -25,10 +34,13 @@ export default function UserRecommendation({ data }) {
});
return (
- <div className="flex flex-col bg-tersier relative rounded overflow-hidden">
- <div className="flex lg:gap-5 z-50">
+ <div className="flex flex-col lg:bg-tersier relative rounded overflow-hidden">
+ <div className="hidden lg:flex lg:gap-5 z-50">
<div className="flex flex-col items-start justify-center gap-3 lg:gap-7 lg:w-[50%] pl-5 lg:px-10">
- <h2 className="font-bold text-3xl text-white">
+ <h2
+ className="font-inter font-bold text-3xl text-white line-clamp-2"
+ title={data[0].title.userPreferred}
+ >
{data[0].title.userPreferred}
</h2>
<p
@@ -37,53 +49,128 @@ export default function UserRecommendation({ data }) {
}}
className="font-roboto font-light line-clamp-3 lg:line-clamp-3"
/>
- <button
- type="button"
+ <Link
+ href={`/en/${data[0].type.toLowerCase()}/${data[0].id}`}
className="border border-white/70 py-1 px-2 lg:py-2 lg:px-4 rounded-full flex items-center gap-2 text-white font-bold"
>
{data[0].type === "ANIME" ? (
<PlayIcon className="w-5 h-5 text-white" />
) : (
- <BookOpenIcon className="w-5 h-5 text-white" />
+ <BookOpenSolid className="w-5 h-5 text-white" />
)}
{data[0].type === "ANIME" ? "Watch" : "Read"} Now
- </button>
+ </Link>
</div>
<div
id="recommendation-list"
className="flex gap-5 overflow-x-scroll scrollbar-none px-5 py-7 lg:py-10"
- ref={ref}
- {...events}
+ ref={desktopRef}
+ {...desktopEvent}
>
{filteredData.slice(0, 9).map((i) => (
<Link
- key={i.id}
+ key={`desktop-${i.id}`}
href={`/en/${i.type.toLowerCase()}/${i.id}`}
- className="relative snap-start shrink-0 group hover:bg-white/20 p-1 rounded"
+ className="relative flex-center snap-start shrink-0 group rounded"
>
+ <span className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded absolute bg-gradient-to-b from-black/50 from-5% to-30% to-transparent z-40" />
+ <span className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded absolute group-hover:bg-gradient-to-t from-black/90 to-transparent z-40 opacity-0 group-hover:opacity-100 transition-all duration-200 ease" />
+ <span
+ title={i.title.userPreferred}
+ className="absolute bottom-5 text-center line-clamp-2 font-karla font-semibold opacity-0 group-hover:opacity-100 w-[70%] z-50 transition-all duration-200 ease"
+ >
+ {i.title.userPreferred}
+ </span>
+ <div className="absolute top-0 right-0 z-40 font-karla font-bold">
+ {i.type === "ANIME" ? (
+ <span className="flex items-center px-2 py-1 gap-1 text-sm text-white">
+ <PlayCircleIcon className="w-5 h-5" />
+ </span>
+ ) : (
+ <span className="flex items-center px-2 py-1 gap-1 text-sm text-white">
+ <BookOpenOutline className="w-5 h-5" />
+ </span>
+ )}
+ </div>
<Image
src={i.coverImage.extraLarge}
alt={i.title.userPreferred}
width={190}
height={256}
- className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded-md object-cover overflow-hidden transition-all duration-150 ease-in-out"
+ className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] brightness-[90%] rounded-md object-cover overflow-hidden transition-all duration-150 ease-in-out"
/>
- <span className="absolute rounded pointer-events-none w-[240px] h-[50%] transition-all duration-150 ease-in transform translate-x-[75%] group-hover:translate-x-[80%] top-0 left-0 bg-secondary opacity-0 group-hover:opacity-100 flex flex-col z-50">
+ {/* <span className="absolute rounded pointer-events-none w-[240px] h-[50%] transition-all duration-150 ease-in transform group-hover:translate-x-[80%] top-0 left-0 bg-secondary opacity-0 group-hover:opacity-100 flex flex-col z-50">
<div className="">{i.title.userPreferred}</div>
<div>a</div>
- </span>
+ </span> */}
</Link>
))}
</div>
</div>
- <div className="absolute top-0 left-0 z-40 bg-gradient-to-r from-transparent from-30% to-80% to-tersier w-[80%] lg:w-[60%] h-full" />
+ <div className="flex lg:hidden">
+ <div
+ id="recommendation-list"
+ className="flex gap-5 overflow-x-scroll scrollbar-none px-5 py-5 lg:py-10"
+ ref={mobileRef}
+ {...mobileEvent}
+ >
+ {filteredData.slice(0, 9).map((i) => (
+ <div key={`mobile-${i.id}`} className="flex flex-col gap-2">
+ <Link
+ title={i.title.userPreferred}
+ href={`/en/${i.type.toLowerCase()}/${i.id}`}
+ className="relative flex-center snap-start shrink-0 group rounded scale-100 hover:scale-105 duration-300 ease-out"
+ >
+ <span className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] rounded absolute bg-gradient-to-b from-black/50 from-5% to-30% to-transparent z-40" />
+ <div className="absolute top-0 right-0 z-40 font-karla font-bold">
+ {i.type === "ANIME" ? (
+ <span className="flex items-center px-2 py-1 gap-1 text-sm text-white">
+ <PlayCircleIcon className="w-5 h-5" />
+ </span>
+ ) : (
+ <span className="flex items-center px-2 py-1 gap-1 text-sm text-white">
+ <BookOpenOutline className="w-5 h-5" />
+ </span>
+ )}
+ </div>
+ <Image
+ src={i.coverImage.extraLarge}
+ alt={i.title.userPreferred}
+ width={190}
+ height={256}
+ className="h-[190px] w-[135px] lg:h-[265px] lg:w-[185px] shrink-0 brightness-[90%] rounded-md object-cover overflow-hidden transition-all duration-150 ease-in-out"
+ />
+ </Link>
+ <Link
+ href={
+ i.type === "MANGA"
+ ? `/en/manga/${i.id}`
+ : `/en/${i.type.toLowerCase()}/${i.id}`
+ }
+ className="w-[135px] lg:w-[185px] line-clamp-2"
+ title={i.title.romaji}
+ >
+ <h1 className="font-karla font-semibold xl:text-base text-[15px]">
+ {i.status === "RELEASING" ? (
+ <span className="dots bg-green-500" />
+ ) : i.status === "NOT_YET_RELEASED" ? (
+ <span className="dots bg-red-500" />
+ ) : null}
+ {i.title.userPreferred}
+ </h1>
+ </Link>
+ </div>
+ ))}
+ </div>
+ </div>
+ <div className="hidden lg:block absolute top-0 left-0 z-40 bg-gradient-to-r from-transparent from-30% to-80% to-tersier w-[80%] lg:w-[60%] h-full" />
{data[0]?.bannerImage && (
<Image
src={data[0]?.bannerImage}
alt={data[0].title.userPreferred}
width={500}
height={500}
- className="absolute top-0 left-0 z-30 w-[60%] h-full object-cover opacity-30"
+ className="hidden lg:block absolute top-0 left-0 z-30 w-[60%] h-full object-cover opacity-30"
/>
)}
</div>
diff --git a/components/home/schedule.js b/components/home/schedule.js
index bb35d08..19260c2 100644
--- a/components/home/schedule.js
+++ b/components/home/schedule.js
@@ -4,7 +4,7 @@ import { convertUnixToTime } from "../../utils/getTimes";
import { PlayIcon } from "@heroicons/react/20/solid";
import { BackwardIcon, ForwardIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
-import { useCountdown } from "../../utils/useCountdownSeconds";
+import { useCountdown } from "../../lib/hooks/useCountdownSeconds";
export default function Schedule({ data, scheduleData, anime, update }) {
let now = new Date();
@@ -13,7 +13,7 @@ export default function Schedule({ data, scheduleData, anime, update }) {
"Schedule";
currentDay = currentDay.replace("Schedule", "");
- const [day, hours, minutes, seconds] = useCountdown(
+ const { day, hours, minutes, seconds } = useCountdown(
anime[0]?.airingSchedule.nodes[0]?.airingAt * 1000 || Date.now(),
update
);
diff --git a/components/listEditor.js b/components/listEditor.tsx
index 7d30835..2e180a1 100644
--- a/components/listEditor.js
+++ b/components/listEditor.tsx
@@ -1,26 +1,37 @@
-import { useState } from "react";
+import { useState, FormEvent } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { toast } from "sonner";
+import { AniListInfoTypes } from "@/types/info/AnilistInfoTypes";
-const ListEditor = ({
+interface ListEditorProps {
+ animeId: number;
+ session: any; // replace 'any' with the appropriate type
+ stats?: string;
+ prg?: number;
+ max?: number;
+ info?: AniListInfoTypes; // replace 'any' with the appropriate type
+ close: () => void;
+}
+
+const ListEditor: React.FC<ListEditorProps> = ({
animeId,
session,
- stats,
- prg,
+ stats = "CURRENT",
+ prg = 0,
max,
- info = null,
+ info = undefined,
close,
}) => {
- const [status, setStatus] = useState(stats ?? "CURRENT");
- const [progress, setProgress] = useState(prg ?? 0);
- const isAnime = info?.type === "ANIME";
+ const [status, setStatus] = useState<string>(stats ?? "CURRENT");
+ const [progress, setProgress] = useState<number>(prg ?? 0);
+ const isAnime: boolean = info?.type === "ANIME";
const router = useRouter();
- const handleSubmit = async (e) => {
+ const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
- console.log("Submitting", status?.name, progress);
+ // console.log("Submitting", status?.name, progress);
try {
const response = await fetch("https://graphql.anilist.co/", {
method: "POST",
@@ -109,7 +120,7 @@ const ListEditor = ({
<select
name="status"
id="status"
- value={status?.value}
+ value={status || "CURRENT"}
onChange={(e) => setStatus(e.target.value)}
className="rounded-sm px-2 py-1 bg-[#363642] w-[50%] sm:w-[150px] text-sm sm:text-base"
>
@@ -137,7 +148,7 @@ const ListEditor = ({
id="progress"
value={progress}
max={max}
- onChange={(e) => setProgress(e.target.value)}
+ onChange={(e) => setProgress(Number(e.target.value))}
className="rounded-sm px-2 py-1 bg-[#363642] w-[50%] sm:w-[150px] text-sm sm:text-base"
min="0"
/>
diff --git a/components/manga/ChaptersComponent.js b/components/manga/ChaptersComponent.js
new file mode 100644
index 0000000..d031c3b
--- /dev/null
+++ b/components/manga/ChaptersComponent.js
@@ -0,0 +1,89 @@
+import { useEffect } from "react";
+import ChapterSelector from "./chapters";
+import axios from "axios";
+import pls from "@/utils/request";
+
+export default function ChaptersComponent({
+ info,
+ mangaId,
+ aniId,
+ setWatch,
+ chapter,
+ setChapter,
+ loading,
+ setLoading,
+ notFound,
+ setNotFound,
+}) {
+ useEffect(() => {
+ setLoading(true);
+ }, [aniId]);
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ setLoading(true);
+ // console.log(mangaId);
+
+ if (mangaId) {
+ const Chapters = await pls.get(
+ `https://api.anify.tv/chapters/${mangaId}`
+ );
+ // console.log("clean this balls");
+
+ if (!Chapters) {
+ setLoading(false);
+ setNotFound(true);
+ } else {
+ setChapter(Chapters);
+ setLoading(false);
+ }
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false);
+ }
+ }
+ fetchData();
+ }, [mangaId]);
+
+ return (
+ <div>
+ {!loading ? (
+ notFound ? (
+ <div className="h-[20vh] lg:w-full flex-center flex-col gap-5">
+ <p className="text-center font-karla font-bold lg:text-lg">
+ Oops!<br></br> It looks like this manga is not available.
+ </p>
+ </div>
+ ) : info && chapter && chapter.length > 0 ? (
+ <ChapterSelector
+ chaptersData={chapter}
+ mangaId={mangaId}
+ data={info}
+ setWatch={setWatch}
+ />
+ ) : (
+ <div className="flex justify-center">
+ <div className="lds-ellipsis">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+ </div>
+ )
+ ) : (
+ <div className="flex justify-center">
+ <div className="lds-ellipsis">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/components/manga/chapters.js b/components/manga/chapters.js
index 2150686..4e7e42e 100644
--- a/components/manga/chapters.js
+++ b/components/manga/chapters.js
@@ -89,7 +89,7 @@ const ChapterSelector = ({ chaptersData, data, setWatch, mangaId }) => {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [chapters]);
+ }, [chapters, mangaId]);
return (
<div className="flex flex-col gap-2 px-3">
diff --git a/components/manga/leftBar.js b/components/manga/leftBar.js
index 5a98115..5485cd2 100644
--- a/components/manga/leftBar.js
+++ b/components/manga/leftBar.js
@@ -93,7 +93,7 @@ export function LeftBar({
onClick={() => setSeekPage(index)}
>
<Image
- src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
x.url
)}${
x?.headers?.Referer
diff --git a/components/manga/mobile/bottomBar.js b/components/manga/mobile/bottomBar.js
index 5b28de4..1cde8ed 100644
--- a/components/manga/mobile/bottomBar.js
+++ b/components/manga/mobile/bottomBar.js
@@ -108,7 +108,7 @@ export default function BottomBar({
onClick={() => setSeekPage(x.index)}
>
<Image
- src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
x.url
)}${
x?.headers?.Referer
diff --git a/components/manga/panels/firstPanel.js b/components/manga/panels/firstPanel.js
index 596fa58..8470fd0 100644
--- a/components/manga/panels/firstPanel.js
+++ b/components/manga/panels/firstPanel.js
@@ -141,7 +141,7 @@ export default function FirstPanel({
ref={(el) => (imageRefs.current[index] = el)}
>
<Image
- src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
i.url
)}${
i?.headers?.Referer
diff --git a/components/manga/panels/secondPanel.js b/components/manga/panels/secondPanel.js
index fa158b2..23a9da0 100644
--- a/components/manga/panels/secondPanel.js
+++ b/components/manga/panels/secondPanel.js
@@ -136,7 +136,7 @@ export default function SecondPanel({
width={500}
height={500}
className="w-1/2 h-screen object-contain"
- src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
image[image.length - index - 2]?.url
)}${
image[image.length - index - 2]?.headers?.Referer
@@ -157,7 +157,7 @@ export default function SecondPanel({
width={500}
height={500}
className="w-1/2 h-screen object-contain"
- src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
image[image.length - index - 1]?.url
)}${
image[image.length - index - 1]?.headers?.Referer
diff --git a/components/manga/panels/thirdPanel.js b/components/manga/panels/thirdPanel.js
index f13b49d..77bb132 100644
--- a/components/manga/panels/thirdPanel.js
+++ b/components/manga/panels/thirdPanel.js
@@ -127,7 +127,7 @@ export default function ThirdPanel({
height={500}
className="w-full h-screen object-contain"
onClick={() => setMobileVisible(!mobileVisible)}
- src={`https://api.consumet.org/utils/image-proxy?url=${encodeURIComponent(
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
image[image.length - index - 1]?.url
)}${
image[image.length - index - 1]?.headers?.Referer
diff --git a/components/modal.js b/components/modal.tsx
index 5d6d0cc..6865560 100644
--- a/components/modal.js
+++ b/components/modal.tsx
@@ -1,4 +1,10 @@
-export default function Modal({ open, onClose, children }) {
+type ModalProps = {
+ open: boolean;
+ onClose: () => void;
+ children: React.ReactNode;
+};
+
+export default function Modal({ open, onClose, children }: ModalProps) {
return (
<div
onClick={onClose}
diff --git a/components/search/searchByImage.js b/components/search/searchByImage.tsx
index f61418f..2041871 100644
--- a/components/search/searchByImage.js
+++ b/components/search/searchByImage.tsx
@@ -3,15 +3,22 @@ import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { toast } from "sonner";
+type SearchByImageProps = {
+ searchPalette?: boolean;
+ setIsOpen?: (isOpen: boolean) => void;
+ setData?: any; // Replace 'any' with the actual data type
+ setMedia?: (media: any) => void; // Replace 'any' with the actual media type
+};
+
export default function SearchByImage({
searchPalette = false,
setIsOpen,
- setData,
- setMedia,
-}) {
+ setData = () => {},
+ setMedia = () => {},
+}: SearchByImageProps) {
const router = useRouter();
- async function findImage(formData) {
+ async function findImage(formData: FormData) {
const response = new Promise((resolve, reject) => {
fetch("https://api.trace.moe/search?anilistInfo", {
method: "POST",
@@ -32,14 +39,16 @@ export default function SearchByImage({
});
response
- .then((data) => {
- if (data?.result?.length > 0) {
+ .then((data: any) => {
+ if (data && data?.result?.length > 0) {
const id = data.result[0].anilist.id;
- const datas = data.result.filter((i) => i.anilist.isAdult === false);
+ const datas = data.result.filter(
+ (i: any) => i.anilist.isAdult === false
+ );
if (setData) setData(datas);
if (searchPalette) router.push(`/en/anime/${id}`);
if (setIsOpen) setIsOpen(false);
- if (setMedia) setMedia();
+ if (setMedia) setMedia({});
}
})
.catch((error) => {
@@ -47,7 +56,7 @@ export default function SearchByImage({
});
}
- const handleImageSelect = async (e) => {
+ const handleImageSelect = async (e: any) => {
const selectedImage = e.target.files[0];
if (selectedImage) {
@@ -64,7 +73,7 @@ export default function SearchByImage({
useEffect(() => {
// Add a global event listener for the paste event
- const handlePaste = async (e) => {
+ const handlePaste = async (e: any) => {
// e.preventDefault();
const items = e.clipboardData.items;
@@ -117,3 +126,35 @@ export default function SearchByImage({
</div>
);
}
+
+export interface TraceMoeDataTypes {
+ frameCount: number;
+ error: string;
+ result: TraceMoeResultTypes[];
+}
+
+export interface TraceMoeResultTypes {
+ anilist: Anilist;
+ filename: string;
+ episode: any;
+ from: number;
+ to: number;
+ similarity: number;
+ video: string;
+ image: string;
+ hovered?: boolean;
+}
+
+interface Anilist {
+ id: number;
+ idMal: number;
+ title: Title;
+ synonyms: string[];
+ isAdult: boolean;
+}
+
+interface Title {
+ native: string;
+ romaji: string;
+ english: any;
+}
diff --git a/components/search/selection.js b/components/search/selection.ts
index 767361d..767361d 100644
--- a/components/search/selection.js
+++ b/components/search/selection.ts
diff --git a/components/searchPalette.js b/components/searchPalette.tsx
index b450423..b253f59 100644
--- a/components/searchPalette.js
+++ b/components/searchPalette.tsx
@@ -1,39 +1,65 @@
import { Fragment, useEffect, useRef, useState } from "react";
import { Combobox, Dialog, Menu, Transition } from "@headlessui/react";
-import useDebounce from "../lib/hooks/useDebounce";
+import useDebounce from "@/lib/hooks/useDebounce";
import Image from "next/image";
import { useRouter } from "next/router";
-import { useSearch } from "../lib/context/isOpenState";
+import { useSearch } from "@/lib/context/isOpenState";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { BookOpenIcon, PlayIcon } from "@heroicons/react/20/solid";
-import { useAniList } from "../lib/anilist/useAnilist";
-import { getFormat } from "../utils/getFormat";
+import { useAniList } from "@/lib/anilist/useAnilist";
+import { getFormat } from "@/utils/getFormat";
import SearchByImage from "./search/searchByImage";
+type SearchType = "ANIME" | "MANGA";
+
+export interface DataTypes {
+ id: number;
+ title: Title;
+ coverImage: CoverImage;
+ type: string;
+ format: string;
+ bannerImage?: string;
+ isLicensed: boolean;
+ genres: string[];
+ startDate: StartDate;
+}
+
+interface Title {
+ userPreferred: string;
+}
+
+interface CoverImage {
+ medium: string;
+}
+
+interface StartDate {
+ year: number;
+}
+
export default function SearchPalette() {
const { isOpen, setIsOpen } = useSearch();
const { quickSearch } = useAniList();
- const [query, setQuery] = useState("");
- const [data, setData] = useState(null);
+ const [query, setQuery] = useState<string>("");
+ const [data, setData] = useState<DataTypes[] | null>(null);
const debounceSearch = useDebounce(query, 500);
- const [loading, setLoading] = useState(false);
- const [type, setType] = useState("ANIME");
+ const [loading, setLoading] = useState<boolean>(false);
+ const [type, setType] = useState<SearchType>("ANIME");
- const [nextPage, setNextPage] = useState(false);
+ const [nextPage, setNextPage] = useState<boolean>(false);
- let focusInput = useRef(null);
+ let focusInput = useRef<HTMLInputElement>(null);
const router = useRouter();
function closeModal() {
setIsOpen(false);
}
- function handleChange(event) {
+ function handleChange(event: string): void {
router.push(`/en/${type.toLowerCase()}/${event}`);
}
- async function advance() {
+ async function advance(): Promise<void> {
setLoading(true);
const res = await quickSearch({
search: debounceSearch,
@@ -50,11 +76,11 @@ export default function SearchPalette() {
}, [debounceSearch, type]);
useEffect(() => {
- const handleKeyDown = (e) => {
+ const handleKeyDown = (e: any) => {
if (e.code === "KeyS" && e.ctrlKey) {
// do your stuff
e.preventDefault();
- setIsOpen((prev) => !prev);
+ setIsOpen((prev: boolean) => !prev);
setData(null);
setQuery("");
}
@@ -103,7 +129,7 @@ export default function SearchPalette() {
<Combobox
as="div"
className="max-w-2xl mx-auto rounded-lg shadow-2xl relative flex flex-col"
- onChange={(e) => {
+ onChange={(e: any) => {
handleChange(e);
setData(null);
setIsOpen(false);
@@ -202,7 +228,7 @@ export default function SearchPalette() {
>
{!loading ? (
<Fragment>
- {data?.length > 0
+ {data && data?.length > 0
? data?.map((i) => (
<Combobox.Option
key={i.id}
diff --git a/components/shared/MobileNav.js b/components/shared/MobileNav.tsx
index d0f29c2..7d6dfd6 100644
--- a/components/shared/MobileNav.js
+++ b/components/shared/MobileNav.tsx
@@ -5,8 +5,12 @@ import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
-export default function MobileNav({ hideProfile = false }) {
- const { data: sessions } = useSession();
+type MobileNavProps = {
+ hideProfile?: boolean;
+};
+
+export default function MobileNav({ hideProfile = false }: MobileNavProps) {
+ const { data: sessions }: { data: any } = useSession();
const [isVisible, setIsVisible] = useState(false);
const handleShowClick = () => {
@@ -48,11 +52,11 @@ export default function MobileNav({ hideProfile = false }) {
>
{isVisible && sessions && !hideProfile && (
<Link
- href={`/en/profile/${sessions?.user.name}`}
+ href={`/en/profile/${sessions?.user?.name}`}
className="fixed lg:hidden bottom-[100px] w-[60px] h-[60px] flex items-center justify-center right-[20px] rounded-full z-50 bg-[#17171f]"
>
<Image
- src={sessions?.user.image.large}
+ src={sessions?.user?.image?.large}
alt="user avatar"
width={60}
height={60}
@@ -99,7 +103,7 @@ export default function MobileNav({ hideProfile = false }) {
</button>
{sessions ? (
<button
- onClick={() => signOut("AniListProvider")}
+ onClick={() => signOut({ redirect: true })}
className="group flex gap-[1.5px] flex-col items-center "
>
<div>
diff --git a/components/shared/NavBar.js b/components/shared/NavBar.tsx
index 8cfdfc1..6e8812e 100644
--- a/components/shared/NavBar.js
+++ b/components/shared/NavBar.tsx
@@ -7,14 +7,31 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
+import { AniListInfoTypes } from "types/info/AnilistInfoTypes";
-const getScrollPosition = (el = window) => ({
- x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
- y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop,
-});
+const getScrollPosition = (el: Window | Element = window) => {
+ if (el instanceof Window) {
+ return { x: el.pageXOffset, y: el.pageYOffset };
+ } else {
+ return { x: el.scrollLeft, y: el.scrollTop };
+ }
+};
-export function NewNavbar({
- info,
+type NavbarProps = {
+ info?: AniListInfoTypes | null;
+ scrollP?: number;
+ toTop?: boolean;
+ withNav?: boolean;
+ paddingY?: string;
+ home?: boolean;
+ back?: boolean;
+ manga?: boolean;
+ shrink?: boolean;
+ bgHover?: boolean;
+};
+
+export function Navbar({
+ info = null,
scrollP = 200,
toTop = false,
withNav = false,
@@ -23,10 +40,13 @@ export function NewNavbar({
back = false,
manga = false,
shrink = false,
-}) {
- const { data: session } = useSession();
+ bgHover = false,
+}: NavbarProps) {
+ const { data: session }: { data: any } = useSession();
const router = useRouter();
- const [scrollPosition, setScrollPosition] = useState();
+ const [scrollPosition, setScrollPosition] = useState<
+ { x: number; y: number } | undefined
+ >();
const { setIsOpen } = useSearch();
const year = new Date().getFullYear();
@@ -48,8 +68,10 @@ export function NewNavbar({
return (
<>
<nav
- className={`${home ? "" : "fixed"} z-[200] top-0 px-5 w-full ${
- scrollPosition?.y >= scrollP
+ className={`${home ? "" : "fixed"} ${
+ bgHover ? "hover:bg-tersier" : ""
+ } z-[200] top-0 px-5 w-full ${
+ scrollPosition?.y ?? 0 >= scrollP
? home
? ""
: `bg-tersier shadow-tersier shadow-sm ${
@@ -86,7 +108,7 @@ export function NewNavbar({
<span
className={`font-inter font-semibold w-[50%] line-clamp-1 select-none ${
- scrollPosition?.y >= scrollP + 80
+ scrollPosition?.y ?? 0 >= scrollP + 80
? "opacity-100"
: "opacity-0"
} transition-all duration-200 ease-linear`}
@@ -160,7 +182,7 @@ export function NewNavbar({
{session && (
<li className="text-center">
<Link
- href={`/en/profile/${session?.user.name}`}
+ href={`/en/profile/${session?.user?.name}`}
className="hover:text-action/80 transition-all duration-150 ease-linear"
>
My List
@@ -202,28 +224,28 @@ export function NewNavbar({
<button
type="button"
onClick={() =>
- router.push(`/en/profile/${session?.user.name}`)
+ router.push(`/en/profile/${session?.user?.name}`)
}
- className="rounded-full bg-white/30 overflow-hidden"
+ className="rounded-full w-7 h-7 bg-white/30 overflow-hidden"
>
<Image
- src={session?.user.image.large}
+ src={session?.user?.image?.large}
alt="avatar"
width={50}
height={50}
- className="w-full h-full object-cover"
+ className="w-7 h-7 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}`}
+ href={`/en/profile/${session?.user?.name}`}
className="hover:text-action"
>
Profile
</Link>
<button
type="button"
- onClick={() => signOut("AniListProvider")}
+ onClick={() => signOut({ redirect: true })}
className="hover:text-action"
>
Log out
@@ -254,7 +276,7 @@ export function NewNavbar({
});
}}
className={`${
- scrollPosition?.y >= 180
+ scrollPosition?.y ?? 0 >= 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]`}
diff --git a/components/shared/bugReport.js b/components/shared/bugReport.tsx
index f6bd9f1..5c1e3f4 100644
--- a/components/shared/bugReport.js
+++ b/components/shared/bugReport.tsx
@@ -10,7 +10,12 @@ const severityOptions = [
{ id: 4, name: "Critical" },
];
-const BugReportForm = ({ isOpen, setIsOpen }) => {
+interface BugReportFormProps {
+ isOpen: boolean;
+ setIsOpen: (isOpen: boolean) => void;
+}
+
+const BugReportForm: React.FC<BugReportFormProps> = ({ isOpen, setIsOpen }) => {
const [bugDescription, setBugDescription] = useState("");
const [severity, setSeverity] = useState(severityOptions[0]);
@@ -20,7 +25,7 @@ const BugReportForm = ({ isOpen, setIsOpen }) => {
setSeverity(severityOptions[0]);
}
- const handleSubmit = async (e) => {
+ const handleSubmit = async (e: any) => {
e.preventDefault();
const bugReport = {
@@ -44,7 +49,7 @@ const BugReportForm = ({ isOpen, setIsOpen }) => {
const json = await res.json();
toast.success(json.message);
closeModal();
- } catch (err) {
+ } catch (err: any) {
console.log(err);
toast.error("Something went wrong: " + err.message);
}
@@ -94,7 +99,7 @@ const BugReportForm = ({ isOpen, setIsOpen }) => {
<textarea
id="bugDescription"
name="bugDescription"
- rows="4"
+ 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}
diff --git a/components/shared/changelogs.tsx b/components/shared/changelogs.tsx
new file mode 100644
index 0000000..a7b0436
--- /dev/null
+++ b/components/shared/changelogs.tsx
@@ -0,0 +1,265 @@
+import { Dialog, Transition } from "@headlessui/react";
+import Link from "next/link";
+import { Fragment, useEffect, useRef, useState } from "react";
+
+const web = {
+ version: "v4.3.1",
+};
+
+const logs = [
+ {
+ version: "v4.3.1",
+ pre: true,
+ notes: null,
+ highlights: true,
+ changes: [
+ "Fix: Auto Next Episode forcing to play sub even if dub is selected",
+ "Fix: Episode metadata not showing after switching to dub",
+ "Fix: Profile picture weirdly cropped",
+ "Fix: Weird padding on the navbar in profile page",
+ ],
+ },
+ {
+ version: "v4.3.0",
+ pre: true,
+ notes: null,
+ highlights: false,
+ changes: [
+ "Added changelogs section",
+ "Added recommendations based on user lists",
+ "New Player!",
+ "And other minor bug fixes!",
+ ],
+ },
+];
+
+export default function ChangeLogs() {
+ let [isOpen, setIsOpen] = useState(false);
+ let completeButtonRef = useRef(null);
+
+ function closeModal() {
+ localStorage.setItem("version", web.version);
+ setIsOpen(false);
+ }
+
+ function getVersion() {
+ let version = localStorage.getItem("version");
+ if (version !== web.version) {
+ setIsOpen(true);
+ }
+ }
+
+ useEffect(() => {
+ getVersion();
+ }, []);
+
+ return (
+ <>
+ <Transition appear show={isOpen} as={Fragment}>
+ <Dialog
+ as="div"
+ className="relative z-50"
+ onClose={closeModal}
+ initialFocus={completeButtonRef}
+ >
+ <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/25" />
+ </Transition.Child>
+
+ <div className="fixed inset-0 overflow-y-auto">
+ <div className="flex min-h-full items-center justify-center p-4 text-center">
+ <Transition.Child
+ as={Fragment}
+ enter="ease-out duration-300"
+ enterFrom="opacity-0 scale-95"
+ enterTo="opacity-100 scale-100"
+ leave="ease-in duration-200"
+ leaveFrom="opacity-100 scale-100"
+ leaveTo="opacity-0 scale-95"
+ >
+ <Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded bg-secondary p-6 text-left align-middle shadow-xl transition-all">
+ <Dialog.Title
+ as="h3"
+ className="text-lg font-medium leading-6 text-gray-100"
+ >
+ <div className="flex justify-between items-center gap-2">
+ <p className="text-xl">Changelogs</p>
+ <div className="flex gap-2 items-center">
+ {/* Github Icon */}
+ <Link
+ href="/github"
+ className="w-5 h-5 hover:opacity-75"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="#fff"
+ viewBox="0 0 20 20"
+ >
+ <g>
+ <g
+ fill="none"
+ fillRule="evenodd"
+ stroke="none"
+ strokeWidth="1"
+ >
+ <g
+ fill="#fff"
+ transform="translate(-140 -7559)"
+ >
+ <g transform="translate(56 160)">
+ <path d="M94 7399c5.523 0 10 4.59 10 10.253 0 4.529-2.862 8.371-6.833 9.728-.507.101-.687-.219-.687-.492 0-.338.012-1.442.012-2.814 0-.956-.32-1.58-.679-1.898 2.227-.254 4.567-1.121 4.567-5.059 0-1.12-.388-2.034-1.03-2.752.104-.259.447-1.302-.098-2.714 0 0-.838-.275-2.747 1.051a9.396 9.396 0 00-2.505-.345 9.375 9.375 0 00-2.503.345c-1.911-1.326-2.751-1.051-2.751-1.051-.543 1.412-.2 2.455-.097 2.714-.639.718-1.03 1.632-1.03 2.752 0 3.928 2.335 4.808 4.556 5.067-.286.256-.545.708-.635 1.371-.57.262-2.018.715-2.91-.852 0 0-.529-.985-1.533-1.057 0 0-.975-.013-.068.623 0 0 .655.315 1.11 1.5 0 0 .587 1.83 3.369 1.21.005.857.014 1.665.014 1.909 0 .271-.184.588-.683.493-3.974-1.355-6.839-5.199-6.839-9.729 0-5.663 4.478-10.253 10-10.253"></path>
+ </g>
+ </g>
+ </g>
+ </g>
+ </svg>
+ </Link>
+ {/* Discord Icon */}
+ <Link
+ href="/discord"
+ className="w-6 h-6 hover:opacity-75"
+ >
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ preserveAspectRatio="xMidYMid"
+ viewBox="0 -28.5 256 256"
+ >
+ <path
+ fill="#fff"
+ d="M216.856 16.597A208.502 208.502 0 00164.042 0c-2.275 4.113-4.933 9.645-6.766 14.046-19.692-2.961-39.203-2.961-58.533 0-1.832-4.4-4.55-9.933-6.846-14.046a207.809 207.809 0 00-52.855 16.638C5.618 67.147-3.443 116.4 1.087 164.956c22.169 16.555 43.653 26.612 64.775 33.193A161.094 161.094 0 0079.735 175.3a136.413 136.413 0 01-21.846-10.632 108.636 108.636 0 005.356-4.237c42.122 19.702 87.89 19.702 129.51 0a131.66 131.66 0 005.355 4.237 136.07 136.07 0 01-21.886 10.653c4.006 8.02 8.638 15.67 13.873 22.848 21.142-6.58 42.646-16.637 64.815-33.213 5.316-56.288-9.08-105.09-38.056-148.36zM85.474 135.095c-12.645 0-23.015-11.805-23.015-26.18s10.149-26.2 23.015-26.2c12.867 0 23.236 11.804 23.015 26.2.02 14.375-10.148 26.18-23.015 26.18zm85.051 0c-12.645 0-23.014-11.805-23.014-26.18s10.148-26.2 23.014-26.2c12.867 0 23.236 11.804 23.015 26.2 0 14.375-10.148 26.18-23.015 26.18z"
+ ></path>
+ </svg>
+ </Link>
+ </div>
+ </div>
+ </Dialog.Title>
+ <div className="mt-4">
+ <p className="text-sm text-gray-400">
+ Hi! Welcome to the new changelogs section. Here you can
+ see a lists of the latest changes and updates to the site.
+ </p>
+ <p className="inline-block text-sm italic my-2 text-gray-400">
+ *This update is still in it's pre-release state, please
+ expect to see some bugs. If you find any, please report
+ them.
+ </p>
+ </div>
+
+ {logs.map((x) => (
+ <ChangelogsVersions
+ notes={x.notes}
+ version={x.version}
+ pre={x.pre}
+ key={x.version}
+ >
+ {x.changes.map((i, index) => (
+ <p key={index}>- {i}</p>
+ ))}
+ </ChangelogsVersions>
+ ))}
+
+ {/* <div className="my-2 flex items-center justify-evenly">
+ <div className="w-full h-[1px] bg-gradient-to-r from-white/5 to-white/40" />
+ <p className="relative flex flex-1 whitespace-nowrap font-bold mx-2 font-inter">
+ v4.3.0
+ <span className="flex text-xs font-light font-roboto ml-1 italic">
+ pre
+ </span>
+ </p>
+ <div className="w-full h-[1px] bg-gradient-to-l from-white/5 to-white/40" />
+ </div>
+
+ <div className="flex flex-col gap-2 text-sm text-gray-200">
+ <div>
+ <p className="inline-block italic mb-2 text-gray-400">
+ *This update is still in it's pre-release state, please
+ expect to see some bugs. If you find any, please report
+ them.
+ </p>
+
+ <p>- Added changelogs section</p>
+ <p>- Added recommendations based on user lists</p>
+ <p>- New Player!</p>
+ <p>- And other minor bug fixes!</p>
+ </div>
+ </div> */}
+
+ <div className="mt-2 text-gray-400 text-sm">
+ <p>
+ see more changelogs{" "}
+ <Link href="/changelogs" className="text-blue-500">
+ here
+ </Link>
+ </p>
+ </div>
+
+ <div className="flex items-center gap-2 mt-4">
+ <div className="flex-1" />
+ <button
+ type="button"
+ className="inline-flex justify-center rounded-md border border-transparent bg-action/10 px-4 py-2 text-sm font-medium text-action/90 hover:bg-action/20 focus:outline-none"
+ onClick={closeModal}
+ ref={completeButtonRef}
+ >
+ Got it, thanks!
+ </button>
+ </div>
+ </Dialog.Panel>
+ </Transition.Child>
+ </div>
+ </div>
+ </Dialog>
+ </Transition>
+ </>
+ );
+}
+
+type ChangelogsVersionsProps = {
+ version?: string;
+ pre: boolean;
+ notes?: string | null;
+ highlights?: boolean;
+ children: React.ReactNode;
+};
+
+export function ChangelogsVersions({
+ version,
+ pre,
+ notes,
+ highlights,
+ children,
+}: ChangelogsVersionsProps) {
+ return (
+ <>
+ <div className="my-2 flex items-center justify-evenly">
+ <div className="w-full h-[1px] bg-gradient-to-r from-white/5 to-white/40" />
+ <p className="relative flex flex-1 whitespace-nowrap font-bold mx-2 font-inter">
+ {version}
+ {pre && (
+ <span className="flex text-xs font-light font-roboto ml-1 italic">
+ pre
+ </span>
+ )}
+ </p>
+ <div className="w-full h-[1px] bg-gradient-to-l from-white/5 to-white/40" />
+ </div>
+
+ <div className="flex flex-col gap-2 text-sm py-2 text-gray-200">
+ <div>
+ {notes && (
+ <p className="inline-block italic mb-2 text-gray-400">*{notes}</p>
+ )}
+ {children}
+ </div>
+ </div>
+ </>
+ );
+}
diff --git a/components/shared/footer.js b/components/shared/footer.tsx
index a29a3d3..2a513a3 100644
--- a/components/shared/footer.js
+++ b/components/shared/footer.tsx
@@ -76,10 +76,7 @@ function Footer() {
</p>
<div className="flex items-center gap-5">
{/* Github Icon */}
- <Link
- href="https://github.com/Ani-Moopa/Moopa"
- className="w-5 h-5 hover:opacity-75"
- >
+ <Link href="/github" className="w-5 h-5 hover:opacity-75">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="#fff"
@@ -102,10 +99,7 @@ function Footer() {
</svg>
</Link>
{/* Discord Icon */}
- <Link
- href="https://discord.gg/v5fjSdKwr2"
- className="w-6 h-6 hover:opacity-75"
- >
+ <Link href="/discord" className="w-6 h-6 hover:opacity-75">
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
diff --git a/components/shared/hamburgerMenu.js b/components/shared/hamburgerMenu.js
deleted file mode 100644
index 7e4bdf1..0000000
--- a/components/shared/hamburgerMenu.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import { signIn, signOut, useSession } from "next-auth/react";
-import Image from "next/image";
-import Link from "next/link";
-import React, { useState } from "react";
-
-export default function HamburgerMenu() {
- const { data: session } = useSession();
- const [isVisible, setIsVisible] = useState(false);
- const [fade, setFade] = useState(false);
-
- const handleShowClick = () => {
- setIsVisible(true);
- setFade(true);
- };
-
- const handleHideClick = () => {
- setIsVisible(false);
- setFade(false);
- };
-
- return (
- <React.Fragment>
- {/* Mobile Hamburger */}
- {!isVisible && (
- <button
- onClick={handleShowClick}
- className="fixed bottom-[30px] right-[20px] z-[100] flex h-[51px] w-[50px] cursor-pointer items-center justify-center rounded-[8px] bg-[#17171f] shadow-lg lg:hidden"
- id="bars"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- className="h-[42px] w-[61.5px] text-[#8BA0B2] fill-orange-500"
- viewBox="0 0 20 20"
- fill="currentColor"
- >
- <path
- fillRule="evenodd"
- d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
- clipRule="evenodd"
- />
- </svg>
- </button>
- )}
- <div className={`z-50`}>
- {isVisible && (
- <div className="fixed bottom-[30px] right-[20px] z-50 flex h-[51px] w-[300px] items-center justify-center gap-8 rounded-[8px] text-[11px] bg-[#17171f] shadow-lg lg:hidden">
- <div className="grid grid-cols-4 place-items-center gap-6">
- <button className="group flex flex-col items-center">
- <Link href={`/en/`} className="">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
- />
- </svg>
- </Link>
- <Link
- href={`/en/`}
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- home
- </Link>
- </button>
- <button className="group flex flex-col items-center">
- <Link href={`/en/about`}>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
- />
- </svg>
- </Link>
- <Link
- href={`/en/about`}
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- about
- </Link>
- </button>
- <button className="group flex gap-[1.5px] flex-col items-center ">
- <div>
- <Link href={`/en/search/anime`}>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- strokeWidth={1.5}
- stroke="currentColor"
- className="w-6 h-6 group-hover:stroke-action"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
- />
- </svg>
- </Link>
- </div>
- <Link
- href={`/en/search/anime`}
- className="font-karla font-bold text-[#8BA0B2] group-hover:text-action"
- >
- search
- </Link>
- </button>
- {session ? (
- <button
- onClick={() => signOut("AniListProvider")}
- className="group flex gap-[1.5px] flex-col items-center "
- >
- <div>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 96 960 960"
- className="group-hover:fill-action w-6 h-6 fill-txt"
- >
- <path d="M186.666 936q-27 0-46.833-19.833T120 869.334V282.666q0-27 19.833-46.833T186.666 216H474v66.666H186.666v586.668H474V936H186.666zm470.668-176.667l-47-48 102-102H370v-66.666h341.001l-102-102 46.999-48 184 184-182.666 182.666z"></path>
- </svg>
- </div>
- <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
- logout
- </h1>
- </button>
- ) : (
- <button
- onClick={() => signIn("AniListProvider")}
- className="group flex gap-[1.5px] flex-col items-center "
- >
- <div>
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 96 960 960"
- className="group-hover:fill-action w-6 h-6 fill-txt mr-2"
- >
- <path d="M486 936v-66.666h287.334V282.666H486V216h287.334q27 0 46.833 19.833T840 282.666v586.668q0 27-19.833 46.833T773.334 936H486zm-78.666-176.667l-47-48 102-102H120v-66.666h341l-102-102 47-48 184 184-182.666 182.666z"></path>
- </svg>
- </div>
- <h1 className="font-karla font-bold text-[#8BA0B2] group-hover:text-action">
- login
- </h1>
- </button>
- )}
- </div>
- <button onClick={handleHideClick}>
- <svg
- width="20"
- height="21"
- className="fill-orange-500"
- viewBox="0 0 20 21"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <rect
- x="2.44043"
- y="0.941467"
- width="23.5842"
- height="3.45134"
- rx="1.72567"
- transform="rotate(45 2.44043 0.941467)"
- />
- <rect
- x="19.1172"
- y="3.38196"
- width="23.5842"
- height="3.45134"
- rx="1.72567"
- transform="rotate(135 19.1172 3.38196)"
- />
- </svg>
- </button>
- </div>
- )}
- </div>
- </React.Fragment>
- );
-}
diff --git a/components/shared/loading.js b/components/shared/loading.js
deleted file mode 100644
index 4620645..0000000
--- a/components/shared/loading.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Image from "next/image";
-
-export default function Loading() {
- return (
- <>
- <div className="flex flex-col gap-5 items-center justify-center w-full z-[800]">
- {/* <Image
- src="/wait-animation.gif"
- width="0"
- height="0"
- className="w-[30%] h-[30%]"
- /> */}
- <div className="flex flex-col items-center font-karla gap-2">
- <p>Please Wait...</p>
- <div className="loader"></div>
- </div>
- </div>
- </>
- );
-}
diff --git a/components/shared/loading.tsx b/components/shared/loading.tsx
new file mode 100644
index 0000000..902b6f9
--- /dev/null
+++ b/components/shared/loading.tsx
@@ -0,0 +1,16 @@
+export default function Loading() {
+ return (
+ <div className="flex-center flex-col font-karla z-40 gap-2">
+ {/* <div className="flex flex-col gap-5 items-center justify-center w-full z-50"> */}
+ {/* <Image
+ src="/wait-animation.gif"
+ width="0"
+ height="0"
+ className="w-[30%] h-[30%]"
+ /> */}
+ <p>Please Wait...</p>
+ <div className="loader"></div>
+ {/* </div> */}
+ </div>
+ );
+}
diff --git a/components/watch/new-player/components/bufferingIndicator.tsx b/components/watch/new-player/components/bufferingIndicator.tsx
new file mode 100644
index 0000000..4793d55
--- /dev/null
+++ b/components/watch/new-player/components/bufferingIndicator.tsx
@@ -0,0 +1,15 @@
+import { Spinner } from "@vidstack/react";
+
+export default function BufferingIndicator() {
+ return (
+ <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center">
+ <Spinner.Root
+ className="text-white opacity-0 transition-opacity duration-200 ease-linear media-buffering:animate-spin media-buffering:opacity-100"
+ size={84}
+ >
+ <Spinner.Track className="opacity-25" width={8} />
+ <Spinner.TrackFill className="opacity-75" width={8} />
+ </Spinner.Root>
+ </div>
+ );
+}
diff --git a/components/watch/new-player/components/buttons.tsx b/components/watch/new-player/components/buttons.tsx
new file mode 100644
index 0000000..18c2b42
--- /dev/null
+++ b/components/watch/new-player/components/buttons.tsx
@@ -0,0 +1,277 @@
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import {
+ CaptionButton,
+ FullscreenButton,
+ isTrackCaptionKind,
+ MuteButton,
+ PIPButton,
+ PlayButton,
+ Tooltip,
+ useMediaState,
+ type TooltipPlacement,
+ useMediaRemote,
+ useMediaStore,
+} from "@vidstack/react";
+import {
+ ClosedCaptionsIcon,
+ ClosedCaptionsOnIcon,
+ FullscreenExitIcon,
+ FullscreenIcon,
+ MuteIcon,
+ PauseIcon,
+ PictureInPictureExitIcon,
+ PictureInPictureIcon,
+ PlayIcon,
+ ReplayIcon,
+ TheatreModeExitIcon,
+ TheatreModeIcon,
+ VolumeHighIcon,
+ VolumeLowIcon,
+} from "@vidstack/react/icons";
+import { useRouter } from "next/router";
+import { Navigation } from "../player";
+
+export interface MediaButtonProps {
+ tooltipPlacement: TooltipPlacement;
+ navigation?: Navigation;
+ host?: boolean;
+}
+
+export const buttonClass =
+ "group ring-media-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 data-[focus]:ring-4";
+
+export const tooltipClass =
+ "animate-out fade-out slide-out-to-bottom-2 data-[visible]:animate-in data-[visible]:fade-in data-[visible]:slide-in-from-bottom-4 z-10 rounded-sm bg-black/90 px-2 py-0.5 text-sm font-medium text-white parent-data-[open]:hidden";
+
+export function Play({ tooltipPlacement }: MediaButtonProps) {
+ const isPaused = useMediaState("paused"),
+ ended = useMediaState("ended"),
+ tooltipText = isPaused ? "Play" : "Pause",
+ Icon = ended ? ReplayIcon : isPaused ? PlayIcon : PauseIcon;
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <PlayButton className={buttonClass}>
+ <Icon className="w-8 h-8" />
+ </PlayButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {tooltipText}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function MobilePlayButton({ tooltipPlacement, host }: MediaButtonProps) {
+ const isPaused = useMediaState("paused"),
+ ended = useMediaState("ended"),
+ Icon = ended ? ReplayIcon : isPaused ? PlayIcon : PauseIcon;
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <PlayButton
+ className={`${
+ host ? "" : "pointer-events-none"
+ } group ring-media-focus relative inline-flex h-16 w-16 media-paused:cursor-pointer cursor-default items-center justify-center rounded-full outline-none`}
+ >
+ <Icon className="w-10 h-10" />
+ </PlayButton>
+ </Tooltip.Trigger>
+ {/* <Tooltip.Content
+ className="animate-out fade-out slide-out-to-bottom-2 data-[visible]:animate-in data-[visible]:fade-in data-[visible]:slide-in-from-bottom-4 z-10 rounded-sm bg-black/90 px-2 py-0.5 text-sm font-medium text-white parent-data-[open]:hidden"
+ placement={tooltipPlacement}
+ >
+ {tooltipText}
+ </Tooltip.Content> */}
+ </Tooltip.Root>
+ );
+}
+
+export function Mute({ tooltipPlacement }: MediaButtonProps) {
+ const volume = useMediaState("volume"),
+ isMuted = useMediaState("muted");
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <MuteButton className={buttonClass}>
+ {isMuted || volume == 0 ? (
+ <MuteIcon className="w-8 h-8" />
+ ) : volume < 0.5 ? (
+ <VolumeLowIcon className="w-8 h-8" />
+ ) : (
+ <VolumeHighIcon className="w-8 h-8" />
+ )}
+ </MuteButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isMuted ? "Unmute" : "Mute"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function Caption({ tooltipPlacement }: MediaButtonProps) {
+ const track = useMediaState("textTrack"),
+ isOn = track && isTrackCaptionKind(track);
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <CaptionButton className={buttonClass}>
+ {isOn ? (
+ <ClosedCaptionsOnIcon className="w-8 h-8" />
+ ) : (
+ <ClosedCaptionsIcon className="w-8 h-8" />
+ )}
+ </CaptionButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isOn ? "Closed-Captions On" : "Closed-Captions Off"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function TheaterButton({ tooltipPlacement }: MediaButtonProps) {
+ const playerState = useMediaState("currentTime"),
+ isPlaying = useMediaState("playing");
+
+ const { setPlayerState, setTheaterMode, theaterMode } = useWatchProvider();
+
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <button
+ type="button"
+ className={buttonClass}
+ onClick={() => {
+ setPlayerState((prev: any) => ({
+ ...prev,
+ currentTime: playerState,
+ isPlaying: isPlaying,
+ }));
+ setTheaterMode((prev: any) => !prev);
+ }}
+ >
+ {!theaterMode ? (
+ <TheatreModeIcon className="w-8 h-8" />
+ ) : (
+ <TheatreModeExitIcon className="w-8 h-8" />
+ )}
+ </button>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ Theatre Mode
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function PIP({ tooltipPlacement }: MediaButtonProps) {
+ const isActive = useMediaState("pictureInPicture");
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <PIPButton className={buttonClass}>
+ {isActive ? (
+ <PictureInPictureExitIcon className="w-8 h-8" />
+ ) : (
+ <PictureInPictureIcon className="w-8 h-8" />
+ )}
+ </PIPButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isActive ? "Exit PIP" : "Enter PIP"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
+
+export function PlayNextButton({
+ tooltipPlacement,
+ navigation,
+}: MediaButtonProps) {
+ // const remote = useMediaRemote();
+ const router = useRouter();
+ const { dataMedia, track } = useWatchProvider();
+ return (
+ <button
+ title="next-button"
+ type="button"
+ onClick={() => {
+ if (navigation?.next) {
+ router.push(
+ `/en/anime/watch/${dataMedia.id}/${track.provider}?id=${
+ navigation?.next?.id
+ }&num=${navigation?.next?.number}${
+ track?.isDub ? `&dub=${track?.isDub}` : ""
+ }`
+ );
+ }
+ }}
+ className="next-button hidden"
+ >
+ Next Episode
+ </button>
+ );
+}
+
+export function SkipOpButton({ tooltipPlacement }: MediaButtonProps) {
+ const remote = useMediaRemote();
+ const { track } = useWatchProvider();
+ const op = track?.skip?.find((item: any) => item.text === "Opening");
+
+ return (
+ <button
+ type="button"
+ onClick={() => {
+ remote.seek(op?.endTime);
+ }}
+ className="op-button hidden hover:bg-white/80 bg-white px-4 py-2 text-primary font-karla font-semibold rounded-md"
+ >
+ Skip Opening
+ </button>
+ );
+}
+
+export function SkipEdButton({ tooltipPlacement }: MediaButtonProps) {
+ const remote = useMediaRemote();
+ const { duration } = useMediaStore();
+ const { track } = useWatchProvider();
+ const ed = track?.skip?.find((item: any) => item.text === "Ending");
+
+ const endTime =
+ Math.round(duration) === ed?.endTime ? ed?.endTime - 1 : ed?.endTime;
+
+ // console.log(endTime);
+
+ return (
+ <button
+ title="ed-button"
+ type="button"
+ onClick={() => remote.seek(endTime)}
+ className="ed-button hidden cursor-pointer hover:bg-white/80 bg-white px-4 py-2 text-primary font-karla font-semibold rounded-md"
+ >
+ Skip Ending
+ </button>
+ );
+}
+
+export function Fullscreen({ tooltipPlacement }: MediaButtonProps) {
+ const isActive = useMediaState("fullscreen");
+ return (
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <FullscreenButton className={buttonClass}>
+ {isActive ? (
+ <FullscreenExitIcon className="w-8 h-8" />
+ ) : (
+ <FullscreenIcon className="w-8 h-8" />
+ )}
+ </FullscreenButton>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ {isActive ? "Exit Fullscreen" : "Enter Fullscreen"}
+ </Tooltip.Content>
+ </Tooltip.Root>
+ );
+}
diff --git a/components/watch/new-player/components/chapter-title.tsx b/components/watch/new-player/components/chapter-title.tsx
new file mode 100644
index 0000000..779f826
--- /dev/null
+++ b/components/watch/new-player/components/chapter-title.tsx
@@ -0,0 +1,11 @@
+import { ChapterTitle, type ChapterTitleProps } from "@vidstack/react";
+import { ChevronLeftIcon, ChevronRightIcon } from "@vidstack/react/icons";
+
+export function ChapterTitleComponent() {
+ return (
+ <span className="inline-block flex-1 overflow-hidden text-ellipsis whitespace-nowrap px-2 text-sm font-medium text-white">
+ <span className="mr-1 text-txt">&#8226;</span>
+ <ChapterTitle className="ml-1" />
+ </span>
+ );
+}
diff --git a/components/watch/new-player/components/layouts/captions.module.css b/components/watch/new-player/components/layouts/captions.module.css
new file mode 100644
index 0000000..338b96e
--- /dev/null
+++ b/components/watch/new-player/components/layouts/captions.module.css
@@ -0,0 +1,80 @@
+.captions {
+ @apply font-roboto font-medium;
+ /* Recommended settings in the WebVTT spec (https://www.w3.org/TR/webvtt1). */
+ /* --cue-color: var(--media-cue-color, white); */
+ /* --cue-color: white; */
+ /* z-index: 20; */
+ /* --cue-bg-color: var(--media-cue-bg, rgba(0, 0, 0, 0.7)); */
+
+ /* bg color white */
+ --cue-bg-color: rgba(255, 255, 255, 0.9);
+ --cue-font-size: calc(var(--overlay-height) / 100 * 5);
+ --cue-line-height: calc(var(--cue-font-size) * 1.2);
+ --cue-padding-x: 0.5em;
+ --cue-padding-y: 0.1em;
+
+ /* remove background blur */
+
+ /* --cue-text-shadow: 0 0 5px black; */
+
+ font-size: var(--cue-font-size);
+ word-spacing: normal;
+ text-shadow: 0px 2px 8px rgba(0, 0, 0, 1);
+ /* contain: layout style; */
+}
+
+.captions[data-dir="rtl"] :global([data-part="cue-display"]) {
+ direction: rtl;
+}
+
+.captions[aria-hidden="true"] {
+ display: none;
+}
+
+/*************************************************************************************************
+ * Cue Display
+ *************************************************************************************************/
+
+/*
+* Most of the cue styles are set automatically by our [media-captions](https://github.com/vidstack/media-captions)
+* library via CSS variables. They are inferred from the VTT, SRT, or SSA file cue settings. You're
+* free to ignore them and style the captions as desired, but we don't recommend it unless the
+* captions file contains no cue settings. Otherwise, you might be breaking accessibility.
+*/
+.captions :global([data-part="cue-display"]) {
+ position: absolute;
+ direction: ltr;
+ overflow: visible;
+ contain: content;
+ top: var(--cue-top);
+ left: var(--cue-left);
+ right: var(--cue-right);
+ bottom: var(--cue-bottom);
+ width: var(--cue-width, auto);
+ height: var(--cue-height, auto);
+ transform: var(--cue-transform);
+ text-align: var(--cue-text-align);
+ writing-mode: var(--cue-writing-mode, unset);
+ white-space: pre-line;
+ unicode-bidi: plaintext;
+ min-width: min-content;
+ min-height: min-content;
+}
+
+.captions :global([data-part="cue"]) {
+ display: inline-block;
+ contain: content;
+ /* border-radius: 2px; */
+ /* backdrop-filter: unset; */
+ padding: var(--cue-padding-y) var(--cue-padding-x);
+ line-height: var(--cue-line-height);
+ /* background-color: var(--cue-bg-color); */
+ color: var(--cue-color);
+ white-space: pre-wrap;
+ outline: var(--cue-outline);
+ text-shadow: var(--cue-text-shadow);
+}
+
+.captions :global([data-part="cue-display"][data-vertical] [data-part="cue"]) {
+ padding: var(--cue-padding-x) var(--cue-padding-y);
+}
diff --git a/components/watch/new-player/components/layouts/video-layout.module.css b/components/watch/new-player/components/layouts/video-layout.module.css
new file mode 100644
index 0000000..14540f6
--- /dev/null
+++ b/components/watch/new-player/components/layouts/video-layout.module.css
@@ -0,0 +1,13 @@
+.controls {
+ /*
+ * These CSS variables are supported out of the box to easily apply offsets to all popups.
+ * You can also offset via props on `Tooltip.Content`, `Menu.Content`, and slider previews.
+ */
+ --media-tooltip-y-offset: 30px;
+ --media-menu-y-offset: 30px;
+}
+
+.controls :global(.volume-slider) {
+ --media-slider-preview-offset: 30px;
+ margin-left: 1.5px;
+}
diff --git a/components/watch/new-player/components/layouts/video-layout.tsx b/components/watch/new-player/components/layouts/video-layout.tsx
new file mode 100644
index 0000000..fa1f6c3
--- /dev/null
+++ b/components/watch/new-player/components/layouts/video-layout.tsx
@@ -0,0 +1,173 @@
+import captionStyles from "./captions.module.css";
+import styles from "./video-layout.module.css";
+
+import {
+ Captions,
+ Controls,
+ Gesture,
+ Spinner,
+ useMediaState,
+} from "@vidstack/react";
+
+import * as Buttons from "../buttons";
+import * as Menus from "../menus";
+import * as Sliders from "../sliders";
+import { TimeGroup } from "../time-group";
+import { Title } from "../title";
+import { ChapterTitleComponent } from "../chapter-title";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import { Navigation } from "../../player";
+import BufferingIndicator from "../bufferingIndicator";
+import { useEffect, useState } from "react";
+
+export interface VideoLayoutProps {
+ thumbnails?: string;
+ navigation?: Navigation;
+ host?: boolean;
+}
+
+function isMobileDevice() {
+ if (typeof window !== "undefined") {
+ return (
+ typeof window.orientation !== "undefined" ||
+ navigator.userAgent.indexOf("IEMobile") !== -1
+ );
+ }
+ return false;
+}
+
+export function VideoLayout({
+ thumbnails,
+ navigation,
+ host = true,
+}: VideoLayoutProps) {
+ const [isMobile, setIsMobile] = useState(false);
+
+ const { track } = useWatchProvider();
+ const isFullscreen = useMediaState("fullscreen");
+
+ useEffect(() => {
+ setIsMobile(isMobileDevice());
+ }, []);
+
+ return (
+ <>
+ <Gestures host={host} />
+ <Captions
+ className={`${captionStyles.captions} media-preview:opacity-0 media-controls:bottom-[85px] media-captions:opacity-100 absolute inset-0 bottom-2 z-10 select-none break-words opacity-0 transition-[opacity,bottom] duration-300`}
+ />
+ <Controls.Root
+ className={`${styles.controls} media-paused:bg-black/10 duration-200 media-controls:opacity-100 absolute inset-0 z-10 flex h-full w-full flex-col bg-gradient-to-t from-black/30 via-transparent to-black/30 opacity-0 transition-opacity`}
+ >
+ <Controls.Group className="flex justify-between items-center w-full px-2 pt-2">
+ <Title navigation={navigation} />
+ <div className="flex-1" />
+ {/* <Menus.Episodes placement="left start" /> */}
+ </Controls.Group>
+ <div className="flex-1" />
+
+ {/* {isPaused && ( */}
+ <Controls.Group
+ className={`media-paused:opacity-100 media-paused:scale-100 backdrop-blur-sm scale-[160%] opacity-0 duration-200 ease-out flex shadow bg-white/10 rounded-full absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2`}
+ >
+ <Buttons.MobilePlayButton tooltipPlacement="top center" host={host} />
+ </Controls.Group>
+ {/* )} */}
+
+ <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center">
+ <Spinner.Root
+ className="text-white opacity-0 transition-opacity duration-200 ease-linear media-buffering:animate-spin media-buffering:opacity-100"
+ size={84}
+ >
+ <Spinner.Track className="opacity-25" width={8} />
+ <Spinner.TrackFill className="opacity-75" width={8} />
+ </Spinner.Root>
+ </div>
+ {/* </Controls.Group> */}
+
+ <Controls.Group className="flex px-4">
+ <div className="flex-1" />
+ {host && (
+ <>
+ <Buttons.SkipOpButton tooltipPlacement="top end" />
+ <Buttons.SkipEdButton tooltipPlacement="top end" />
+ <Buttons.PlayNextButton
+ navigation={navigation}
+ tooltipPlacement="top end"
+ />
+ </>
+ )}
+ </Controls.Group>
+
+ <Controls.Group className="flex w-full items-center px-2">
+ <Sliders.Time thumbnails={thumbnails} host={host} />
+ </Controls.Group>
+ <Controls.Group className="-mt-0.5 flex w-full items-center px-2 pb-2">
+ <Buttons.Play tooltipPlacement="top start" />
+ <Buttons.Mute tooltipPlacement="top" />
+ <Sliders.Volume />
+ <TimeGroup />
+ <ChapterTitleComponent />
+ <div className="flex-1" />
+ {track?.subtitles && <Buttons.Caption tooltipPlacement="top" />}
+ <Menus.Settings placement="top end" tooltipPlacement="top" />
+ {!isMobile && !isFullscreen && (
+ <Buttons.TheaterButton tooltipPlacement="top" />
+ )}
+ <Buttons.PIP tooltipPlacement="top" />
+ <Buttons.Fullscreen tooltipPlacement="top end" />
+ </Controls.Group>
+ </Controls.Root>
+ </>
+ );
+}
+
+function Gestures({ host }: { host?: boolean }) {
+ const isMobile = isMobileDevice();
+ return (
+ <>
+ {isMobile ? (
+ <>
+ {host && (
+ <Gesture
+ className="absolute inset-0 z-10"
+ event="dblpointerup"
+ action="toggle:paused"
+ />
+ )}
+ <Gesture
+ className="absolute inset-0"
+ event="pointerup"
+ action="toggle:controls"
+ />
+ </>
+ ) : (
+ <>
+ {host && (
+ <Gesture
+ className="absolute inset-0"
+ event="pointerup"
+ action="toggle:paused"
+ />
+ )}
+ <Gesture
+ className="absolute inset-0 z-10"
+ event="dblpointerup"
+ action="toggle:fullscreen"
+ />
+ </>
+ )}
+
+ <Gesture
+ className="absolute top-0 left-0 w-1/5 h-full z-20"
+ event="dblpointerup"
+ action="seek:-10"
+ />
+ <Gesture
+ className="absolute top-0 right-0 w-1/5 h-full z-20"
+ event="dblpointerup"
+ action="seek:10"
+ />
+ </>
+ );
+}
diff --git a/components/watch/new-player/components/menus.tsx b/components/watch/new-player/components/menus.tsx
new file mode 100644
index 0000000..de2b302
--- /dev/null
+++ b/components/watch/new-player/components/menus.tsx
@@ -0,0 +1,387 @@
+// @ts-nocheck
+
+import type { ReactElement } from "react";
+
+// import EpiDataDummy from "@/components/test/episodeDummy.json";
+
+import {
+ Menu,
+ Tooltip,
+ useCaptionOptions,
+ type MenuPlacement,
+ type TooltipPlacement,
+ useVideoQualityOptions,
+ useMediaState,
+ usePlaybackRateOptions,
+} from "@vidstack/react";
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ ClosedCaptionsIcon,
+ SettingsMenuIcon,
+ RadioButtonIcon,
+ RadioButtonSelectedIcon,
+ SettingsIcon,
+ // EpisodesIcon,
+ SettingsSwitchIcon,
+ // PlaybackSpeedCircleIcon,
+ OdometerIcon,
+} from "@vidstack/react/icons";
+
+import { buttonClass, tooltipClass } from "./buttons";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import React from "react";
+
+export interface SettingsProps {
+ placement: MenuPlacement;
+ tooltipPlacement: TooltipPlacement;
+}
+
+export const menuClass =
+ "fixed bottom-0 animate-out fade-out slide-out-to-bottom-2 data-[open]:animate-in data-[open]:fade-in data-[open]:slide-in-from-bottom-4 flex h-[var(--menu-height)] max-h-[200px] lg:max-h-[400px] min-w-[260px] flex-col overflow-y-auto overscroll-y-contain rounded-md border border-white/10 bg-black/95 p-2.5 font-sans text-[15px] font-medium outline-none backdrop-blur-sm transition-[height] duration-300 will-change-[height] data-[resizing]:overflow-hidden";
+
+export const submenuClass =
+ "hidden w-full flex-col items-start justify-center outline-none data-[keyboard]:mt-[3px] data-[open]:inline-block";
+
+export const contentMenuClass =
+ "flex cust-scroll h-[var(--menu-height)] max-h-[180px] lg:max-h-[400px] min-w-[260px] flex-col overflow-y-auto overscroll-y-contain rounded-md border border-white/10 bg-secondary p-2 font-sans text-[15px] font-medium outline-none backdrop-blur-sm transition-[height] duration-300 will-change-[height] data-[resizing]:overflow-hidden";
+
+export function Settings({ placement, tooltipPlacement }: SettingsProps) {
+ const { track } = useWatchProvider();
+ const isSubtitleAvailable = track?.epiData?.subtitles?.length > 0;
+
+ return (
+ <Menu.Root className="parent">
+ <Tooltip.Root>
+ <Tooltip.Trigger asChild>
+ <Menu.Button className={buttonClass}>
+ <SettingsIcon className="h-8 w-8 transform transition-transform duration-200 ease-out group-data-[open]:rotate-90" />
+ </Menu.Button>
+ </Tooltip.Trigger>
+ <Tooltip.Content className={tooltipClass} placement={tooltipPlacement}>
+ Settings
+ </Tooltip.Content>
+ </Tooltip.Root>
+ {/* <Menu.Content className={menuClass} placement={placement}>
+ {isSubtitleAvailable && <CaptionSubmenu />}
+ <QualitySubmenu />
+ </Menu.Content> */}
+ <Menu.Content className={contentMenuClass} placement={placement}>
+ <AutoPlay />
+ <AutoNext />
+ <SpeedSubmenu />
+ {isSubtitleAvailable && <CaptionSubmenu />}
+ <QualitySubmenu />
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+// export function Episodes({ placement }: { placement: MenuPlacement }) {
+// return (
+// <Menu.Root className="parent">
+// <Tooltip.Root>
+// <Tooltip.Trigger asChild>
+// <Menu.Button className={buttonClass}>
+// <EpisodesIcon className="w-10 h-10" />
+// </Menu.Button>
+// </Tooltip.Trigger>
+// </Tooltip.Root>
+// <Menu.Content
+// className={`bg-secondary/95 border border-white/10 max-h-[240px] overflow-y-scroll cust-scroll rounded overflow-hidden z-30 -translate-y-5 -translate-x-2`}
+// placement={placement}
+// >
+// <EpisodeSubmenu />
+// </Menu.Content>
+// </Menu.Root>
+// );
+// }
+
+function SpeedSubmenu() {
+ const options = usePlaybackRateOptions(),
+ hint =
+ options.selectedValue === "1" ? "Normal" : options.selectedValue + "x";
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Playback Rate"
+ hint={hint}
+ icon={OdometerIcon}
+ disabled={options.disabled}
+ >
+ Speed ({hint})
+ </SubmenuButton>
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={options.selectedValue}
+ >
+ {options.map(({ label, value, select }) => (
+ <Radio value={value} onSelect={select} key={value}>
+ {label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+function CaptionSubmenu() {
+ const options = useCaptionOptions(),
+ hint = options.selectedTrack?.label ?? "Off";
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Captions"
+ hint={hint}
+ disabled={options.disabled}
+ icon={ClosedCaptionsIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={options.selectedValue}
+ >
+ {options.map(({ label, value, select }) => (
+ <Radio value={value} onSelect={select} key={value}>
+ {label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+// function EpisodeSubmenu() {
+// return (
+// // <div className="h-full w-[320px]">
+// <div className="flex flex-col h-full w-[360px] font-karla">
+// {/* {EpiDataDummy.map((epi, index) => ( */}
+// <div
+// key={index}
+// className={`flex gap-1 hover:bg-secondary px-3 py-2 ${
+// index === 0
+// ? "pt-4"
+// // : index === EpiDataDummy.length - 1
+// ? "pb-4"
+// : ""
+// }`}
+// >
+// <Image
+// src={epi.img}
+// alt="thumbnail"
+// width={100}
+// height={100}
+// className="object-cover w-[120px] h-[64px] rounded-md"
+// />
+// <div className="flex flex-col pl-2">
+// <h1 className="font-semibold">{epi.title}</h1>
+// <p className="line-clamp-2 text-sm font-light">
+// {epi?.description}
+// </p>
+// </div>
+// </div>
+// ))}
+// </div>
+// // </div>
+// );
+// }
+
+function AutoPlay() {
+ const [options, setOptions] = React.useState([
+ {
+ label: "On",
+ value: "on",
+ selected: false,
+ },
+ {
+ label: "Off",
+ value: "off",
+ selected: true,
+ },
+ ]);
+
+ const { autoplay, setAutoPlay } = useWatchProvider();
+
+ // console.log({ autoplay });
+
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Autoplay Video"
+ hint={
+ autoplay
+ ? options.find((option) => option.value === autoplay)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ icon={SettingsSwitchIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={
+ autoplay
+ ? options.find((option) => option.value === autoplay)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ onChange={(value) => {
+ setOptions((options) =>
+ options.map((option) =>
+ option.value === value
+ ? { ...option, selected: true }
+ : { ...option, selected: false }
+ )
+ );
+ setAutoPlay(value);
+ localStorage.setItem("autoplay", value);
+ }}
+ >
+ {options.map((option) => (
+ <Radio key={option.value} value={option.value}>
+ {option.label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+function AutoNext() {
+ const [options, setOptions] = React.useState([
+ {
+ label: "On",
+ value: "on",
+ selected: false,
+ },
+ {
+ label: "Off",
+ value: "off",
+ selected: true,
+ },
+ ]);
+
+ const { autoNext, setAutoNext } = useWatchProvider();
+
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Autoplay Next"
+ hint={
+ autoNext
+ ? options.find((option) => option.value === autoNext)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ icon={SettingsSwitchIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={
+ autoNext
+ ? options.find((option) => option.value === autoNext)?.value
+ : options.find((option) => option.selected)?.value
+ }
+ onChange={(value) => {
+ setOptions((options) =>
+ options.map((option) =>
+ option.value === value
+ ? { ...option, selected: true }
+ : { ...option, selected: false }
+ )
+ );
+ setAutoNext(value);
+ localStorage.setItem("autoNext", value);
+ }}
+ >
+ {options.map((option) => (
+ <Radio key={option.value} value={option.value}>
+ {option.label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+function QualitySubmenu() {
+ const options = useVideoQualityOptions({ sort: "descending" }),
+ autoQuality = useMediaState("autoQuality"),
+ currentQualityText = options.selectedQuality?.height + "p" ?? "",
+ hint = !autoQuality ? currentQualityText : `Auto (${currentQualityText})`;
+
+ // console.log({ options });
+
+ return (
+ <Menu.Root>
+ <SubmenuButton
+ label="Quality"
+ hint={hint}
+ disabled={options.disabled}
+ icon={SettingsMenuIcon}
+ />
+ <Menu.Content className={submenuClass}>
+ <Menu.RadioGroup
+ className="w-full flex flex-col"
+ value={options.selectedValue}
+ >
+ {options.map(({ label, value, bitrateText, select }) => (
+ <Radio value={value} onSelect={select} key={value}>
+ {label}
+ </Radio>
+ ))}
+ </Menu.RadioGroup>
+ </Menu.Content>
+ </Menu.Root>
+ );
+}
+
+export interface RadioProps extends Menu.RadioProps {}
+
+function Radio({ children, ...props }: RadioProps) {
+ return (
+ <Menu.Radio
+ className="ring-media-focus group relative flex w-full cursor-pointer select-none items-center justify-start rounded-sm p-2.5 outline-none data-[hocus]:bg-white/10 data-[focus]:ring-[3px]"
+ {...props}
+ >
+ <RadioButtonIcon className="h-4 w-4 text-white group-data-[checked]:hidden" />
+ <RadioButtonSelectedIcon
+ className="text-media-brand hidden h-4 w-4 group-data-[checked]:block"
+ type="radio-button-selected"
+ />
+ <span className="ml-2">{children}</span>
+ </Menu.Radio>
+ );
+}
+
+export interface SubmenuButtonProps {
+ label: string;
+ hint: string;
+ disabled?: boolean;
+ icon: ReactElement;
+}
+
+function SubmenuButton({
+ label,
+ hint,
+ icon: Icon,
+ disabled,
+}: SubmenuButtonProps) {
+ return (
+ <Menu.Button
+ className="ring-media-focus data-[open]:bg-secondary parent left-0 z-10 flex w-full cursor-pointer select-none items-center justify-start rounded-sm p-2.5 outline-none ring-inset data-[open]:sticky data-[open]:-top-2.5 data-[hocus]:bg-white/10 data-[focus]:ring-[3px]"
+ disabled={disabled}
+ >
+ <ChevronLeftIcon className="parent-data-[open]:block -ml-0.5 mr-1.5 hidden h-[18px] w-[18px]" />
+ <div className="contents parent-data-[open]:hidden">
+ <Icon className="w-5 h-5" />
+ </div>
+ <span className="ml-1.5 parent-data-[open]:ml-0">{label}</span>
+ <span className="ml-auto text-sm text-white/50">{hint}</span>
+ <ChevronRightIcon className="parent-data-[open]:hidden ml-0.5 h-[18px] w-[18px] text-sm text-white/50" />
+ </Menu.Button>
+ );
+}
diff --git a/components/watch/new-player/components/sliders.tsx b/components/watch/new-player/components/sliders.tsx
new file mode 100644
index 0000000..f31e28a
--- /dev/null
+++ b/components/watch/new-player/components/sliders.tsx
@@ -0,0 +1,73 @@
+import { TimeSlider, VolumeSlider } from "@vidstack/react";
+
+export function Volume() {
+ return (
+ <VolumeSlider.Root className="volume-slider group relative mx-[7.5px] inline-flex h-10 w-full max-w-[80px] cursor-pointer touch-none select-none items-center outline-none aria-hidden:hidden">
+ <VolumeSlider.Track className="relative ring-media-focus z-0 h-[5px] w-full rounded-sm bg-white/30 group-data-[focus]:ring-[3px]">
+ <VolumeSlider.TrackFill className="bg-white absolute h-full w-[var(--slider-fill)] rounded-sm will-change-[width]" />
+ </VolumeSlider.Track>
+
+ <VolumeSlider.Preview
+ className="flex flex-col items-center opacity-0 transition-opacity duration-200 data-[visible]:opacity-100"
+ noClamp
+ >
+ <VolumeSlider.Value className="rounded-sm bg-black px-2 py-px text-[13px] font-medium" />
+ </VolumeSlider.Preview>
+ <VolumeSlider.Thumb className="absolute left-[var(--slider-fill)] top-1/2 z-20 h-[15px] w-[15px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-[#cacaca] bg-white opacity-0 ring-white/40 transition-opacity group-data-[active]:opacity-100 group-data-[dragging]:ring-4 will-change-[left]" />
+ </VolumeSlider.Root>
+ );
+}
+
+export interface TimeSliderProps {
+ thumbnails?: string;
+ host?: boolean;
+}
+
+export function Time({ thumbnails, host }: TimeSliderProps) {
+ return (
+ <TimeSlider.Root
+ className={`${
+ host ? "" : "pointer-events-none"
+ } time-slider group relative mx-[7.5px] inline-flex h-10 w-full cursor-pointer touch-none select-none items-center outline-none`}
+ >
+ <TimeSlider.Chapters className="relative flex h-full w-full items-center rounded-[1px]">
+ {(cues, forwardRef) =>
+ cues.map((cue) => (
+ <div
+ className="last-child:mr-0 group/slider relative mr-0.5 flex h-full w-full items-center rounded-[1px]"
+ style={{ contain: "layout style" }}
+ key={cue.startTime}
+ ref={forwardRef}
+ >
+ <TimeSlider.Track className="relative ring-media-focus z-0 h-[5px] group-hover/slider:h-[10px] transition-all duration-100 w-full rounded-sm bg-white/30 group-data-[focus]:ring-[3px]">
+ <TimeSlider.TrackFill className="bg-white absolute h-full w-[var(--chapter-fill)] rounded-sm will-change-[width]" />
+ <TimeSlider.Progress className="absolute z-10 h-full w-[var(--chapter-progress)] rounded-sm bg-white/50 will-change-[width]" />
+ </TimeSlider.Track>
+ </div>
+ ))
+ }
+ </TimeSlider.Chapters>
+ {/* <TimeSlider.Track className="relative ring-media-focus z-0 h-[5px] w-full rounded-sm bg-white/30 group-data-[focus]:ring-[3px]">
+ <TimeSlider.TrackFill className="bg-white absolute h-full w-[var(--slider-fill)] rounded-sm will-change-[width]" />
+ <TimeSlider.Progress className="absolute z-10 h-full w-[var(--slider-progress)] rounded-sm bg-white/40 will-change-[width]" />
+ </TimeSlider.Track> */}
+
+ <TimeSlider.Thumb className="absolute left-[var(--slider-fill)] top-1/2 z-20 h-[15px] w-[15px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-[#cacaca] bg-white opacity-0 ring-white/40 transition-opacity group-data-[active]:opacity-100 group-data-[dragging]:ring-4 will-change-[left]" />
+
+ <TimeSlider.Preview className="flex flex-col items-center opacity-0 transition-opacity duration-200 data-[visible]:opacity-100 pointer-events-none">
+ {thumbnails ? (
+ <TimeSlider.Thumbnail.Root
+ src={thumbnails}
+ className="block h-[var(--thumbnail-height)] max-h-[160px] min-h-[80px] w-[var(--thumbnail-width)] min-w-[120px] max-w-[180px] overflow-hidden border border-white bg-black"
+ >
+ <TimeSlider.Thumbnail.Img />
+ </TimeSlider.Thumbnail.Root>
+ ) : null}
+
+ <TimeSlider.ChapterTitle className="mt-2 text-sm" />
+
+ <TimeSlider.Value className="text-[13px]" />
+ </TimeSlider.Preview>
+ </TimeSlider.Root>
+ );
+}
diff --git a/components/watch/new-player/components/time-group.tsx b/components/watch/new-player/components/time-group.tsx
new file mode 100644
index 0000000..45fc795
--- /dev/null
+++ b/components/watch/new-player/components/time-group.tsx
@@ -0,0 +1,11 @@
+import { Time } from "@vidstack/react";
+
+export function TimeGroup() {
+ return (
+ <div className="ml-1.5 flex items-center text-sm font-medium">
+ <Time className="time" type="current" />
+ <div className="mx-1 text-white/80">/</div>
+ <Time className="time" type="duration" />
+ </div>
+ );
+}
diff --git a/components/watch/new-player/components/title.tsx b/components/watch/new-player/components/title.tsx
new file mode 100644
index 0000000..6233061
--- /dev/null
+++ b/components/watch/new-player/components/title.tsx
@@ -0,0 +1,35 @@
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import { useMediaRemote } from "@vidstack/react";
+import { ChevronLeftIcon } from "@vidstack/react/icons";
+import { Navigation } from "../player";
+
+type TitleProps = {
+ navigation?: Navigation;
+};
+
+export function Title({ navigation }: TitleProps) {
+ const { dataMedia } = useWatchProvider();
+ const remote = useMediaRemote();
+
+ return (
+ <div className="media-fullscreen:flex hidden text-start flex-1 text-sm font-medium text-white">
+ {/* <p className="pt-4 h-full">
+ </p> */}
+ <button
+ type="button"
+ className="flex items-center gap-2 text-sm font-karla w-full"
+ onClick={() => remote.toggleFullscreen()}
+ >
+ <ChevronLeftIcon className="font-extrabold w-7 h-7" />
+ <span className="max-w-[75%] text-base xl:text-2xl font-semibold whitespace-nowrap overflow-hidden text-ellipsis">
+ {dataMedia?.title?.romaji}
+ </span>
+ <span className="text-base xl:text-2xl font-normal">/</span>
+ <span className="text-base xl:text-2xl font-normal">
+ Episode {navigation?.playing.number}
+ </span>
+ {/* <span className="absolute top-5 left-[1s0%] w-[24%] h-[1px] bg-white" /> */}
+ </button>
+ </div>
+ );
+}
diff --git a/components/watch/new-player/player.module.css b/components/watch/new-player/player.module.css
new file mode 100644
index 0000000..f2f5b39
--- /dev/null
+++ b/components/watch/new-player/player.module.css
@@ -0,0 +1,50 @@
+.player {
+ --media-brand: #f5f5f5;
+ --media-focus-ring-color: #4e9cf6;
+ --media-focus-ring: 0 0 0 3px var(--media-focus-ring-color);
+
+ --media-tooltip-y-offset: 30px;
+ --media-menu-y-offset: 30px;
+
+ background-color: black;
+ border-radius: var(--media-border-radius);
+ color: #f5f5f5;
+ contain: layout;
+ font-family: sans-serif;
+ overflow: hidden;
+}
+
+.player[data-focus]:not([data-playing]) {
+ box-shadow: var(--media-focus-ring);
+}
+
+.player video {
+ height: 100%;
+ object-fit: contain;
+ display: block;
+}
+
+.player video,
+.poster {
+ border-radius: var(--media-border-radius);
+}
+
+.poster {
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.poster[data-visible] {
+ opacity: 1;
+}
+
+.poster img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
diff --git a/components/watch/new-player/player.tsx b/components/watch/new-player/player.tsx
new file mode 100644
index 0000000..b98ff79
--- /dev/null
+++ b/components/watch/new-player/player.tsx
@@ -0,0 +1,471 @@
+import "@vidstack/react/player/styles/base.css";
+
+import { useEffect, useRef, useState } from "react";
+
+import style from "./player.module.css";
+
+import {
+ MediaPlayer,
+ MediaProvider,
+ useMediaStore,
+ useMediaRemote,
+ type MediaPlayerInstance,
+ Track,
+ MediaTimeUpdateEventDetail,
+ MediaTimeUpdateEvent,
+} from "@vidstack/react";
+import { VideoLayout } from "./components/layouts/video-layout";
+import { useWatchProvider } from "@/lib/context/watchPageProvider";
+import { useRouter } from "next/router";
+import { Subtitle } from "types/episodes/TrackData";
+import useWatchStorage from "@/lib/hooks/useWatchStorage";
+import { Sessions } from "types/episodes/Sessions";
+import { useAniList } from "@/lib/anilist/useAnilist";
+
+export interface Navigation {
+ prev: Prev;
+ playing: Playing;
+ next: Next;
+}
+
+export interface Prev {
+ id: string;
+ title: string;
+ img: string;
+ number: number;
+ description: string;
+}
+
+export interface Playing {
+ id: string;
+ title: string;
+ description: string;
+ img: string;
+ number: number;
+}
+
+export interface Next {
+ id: string;
+ title: string;
+ description: string;
+ img: string;
+ number: number;
+}
+
+type VidStackProps = {
+ id: string;
+ navigation: Navigation;
+ userData: UserData;
+ sessions: Sessions;
+};
+
+export type UserData = {
+ id?: string;
+ userProfileId?: string;
+ aniId: string;
+ watchId: string;
+ title: string;
+ aniTitle: string;
+ image: string;
+ episode: number;
+ duration: number;
+ timeWatched: number;
+ provider: string;
+ nextId: string;
+ nextNumber: number;
+ dub: boolean;
+ createdAt: string;
+};
+
+type SkipData = {
+ startTime: number;
+ endTime: number;
+ text: string;
+};
+
+export default function VidStack({
+ id,
+ navigation,
+ userData,
+ sessions,
+}: VidStackProps) {
+ let player = useRef<MediaPlayerInstance>(null);
+
+ const {
+ aspectRatio,
+ setAspectRatio,
+ track,
+ playerState,
+ dataMedia,
+ autoNext,
+ } = useWatchProvider();
+
+ const { qualities, duration } = useMediaStore(player);
+
+ const [getSettings, updateSettings] = useWatchStorage();
+ const { marked, setMarked } = useWatchProvider();
+
+ const { markProgress } = useAniList(sessions);
+
+ const remote = useMediaRemote(player);
+
+ const { defaultQuality = null } = track ?? {};
+
+ const [chapters, setChapters] = useState<string>("");
+
+ const router = useRouter();
+
+ useEffect(() => {
+ if (qualities.length > 0) {
+ const sourceQuality = qualities.reduce(
+ (max, obj) => (obj.height > max.height ? obj : max),
+ qualities[0]
+ );
+ const aspectRatio = calculateAspectRatio(
+ sourceQuality.width,
+ sourceQuality.height
+ );
+
+ setAspectRatio(aspectRatio);
+ }
+ }, [qualities]);
+
+ const [isPlaying, setIsPlaying] = useState(false);
+ let interval: any;
+
+ useEffect(() => {
+ const plyr = player.current;
+
+ function handlePlay() {
+ // console.log("Player is playing");
+ setIsPlaying(true);
+ }
+
+ function handlePause() {
+ // console.log("Player is paused");
+ setIsPlaying(false);
+ }
+
+ function handleEnd() {
+ // console.log("Player ended");
+ setIsPlaying(false);
+ }
+
+ plyr?.addEventListener("play", handlePlay);
+ plyr?.addEventListener("pause", handlePause);
+ plyr?.addEventListener("ended", handleEnd);
+
+ return () => {
+ plyr?.removeEventListener("play", handlePlay);
+ plyr?.removeEventListener("pause", handlePause);
+ plyr?.removeEventListener("ended", handleEnd);
+ };
+ }, [id, duration]);
+
+ useEffect(() => {
+ if (isPlaying) {
+ interval = setInterval(async () => {
+ const currentTime = player.current?.currentTime
+ ? Math.round(player.current?.currentTime)
+ : 0;
+
+ const parsedImage = navigation?.playing?.img?.includes("null")
+ ? dataMedia?.coverImage?.extraLarge
+ : navigation?.playing?.img;
+
+ if (sessions?.user?.name) {
+ // console.log("updating user data");
+ await fetch("/api/user/update/episode", {
+ method: "PUT",
+ body: JSON.stringify({
+ name: sessions?.user?.name,
+ id: String(dataMedia?.id),
+ watchId: navigation?.playing?.id,
+ title:
+ navigation.playing?.title ||
+ dataMedia.title?.romaji ||
+ dataMedia.title?.english,
+ aniTitle: dataMedia.title?.romaji || dataMedia.title?.english,
+ image: parsedImage,
+ number: Number(navigation.playing?.number),
+ duration: duration,
+ timeWatched: currentTime,
+ provider: track?.provider,
+ nextId: navigation?.next?.id,
+ nextNumber: Number(navigation?.next?.number),
+ dub: track?.isDub ? true : false,
+ }),
+ });
+ }
+
+ updateSettings(navigation?.playing?.id, {
+ aniId: String(dataMedia.id),
+ watchId: navigation?.playing?.id,
+ title:
+ navigation.playing?.title ||
+ dataMedia.title?.romaji ||
+ dataMedia.title?.english,
+ aniTitle: dataMedia.title?.romaji || dataMedia.title?.english,
+ image: parsedImage,
+ episode: Number(navigation.playing?.number),
+ duration: duration,
+ timeWatched: currentTime, // update timeWatched with currentTime
+ provider: track?.provider,
+ nextId: navigation?.next?.id,
+ nextNumber: navigation?.next?.number,
+ dub: track?.isDub ? true : false,
+ createdAt: new Date().toISOString(),
+ });
+ // console.log("update");
+ }, 5000);
+ } else {
+ clearInterval(interval);
+ }
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [isPlaying, sessions?.user?.name, track?.isDub, duration]);
+
+ useEffect(() => {
+ const autoplay = localStorage.getItem("autoplay") || "off";
+
+ return player.current!.subscribe(({ canPlay }) => {
+ // console.log("can play?", "->", canPlay);
+ if (canPlay) {
+ if (autoplay === "on") {
+ if (playerState?.currentTime === 0) {
+ remote.play();
+ } else {
+ if (playerState?.isPlaying) {
+ remote.play();
+ } else {
+ remote.pause();
+ }
+ }
+ } else {
+ if (playerState?.isPlaying) {
+ remote.play();
+ } else {
+ remote.pause();
+ }
+ }
+ remote.seek(playerState?.currentTime);
+ }
+ });
+ }, [playerState?.currentTime, playerState?.isPlaying]);
+
+ useEffect(() => {
+ const chapter = track?.skip,
+ videoDuration = Math.round(duration);
+
+ let vtt = "WEBVTT\n\n";
+
+ let lastEndTime = 0;
+
+ if (chapter && chapter?.length > 0) {
+ chapter.forEach((item: SkipData) => {
+ let startMinutes = Math.floor(item.startTime / 60);
+ let startSeconds = item.startTime % 60;
+ let endMinutes = Math.floor(item.endTime / 60);
+ let endSeconds = item.endTime % 60;
+
+ let start = `${startMinutes.toString().padStart(2, "0")}:${startSeconds
+ .toString()
+ .padStart(2, "0")}`;
+ let end = `${endMinutes.toString().padStart(2, "0")}:${endSeconds
+ .toString()
+ .padStart(2, "0")}`;
+
+ vtt += `${start} --> ${end}\n${item.text}\n\n`;
+ if (item.endTime > lastEndTime) {
+ lastEndTime = item.endTime;
+ }
+ });
+
+ if (lastEndTime < videoDuration) {
+ let startMinutes = Math.floor(lastEndTime / 60);
+ let startSeconds = lastEndTime % 60;
+ let endMinutes = Math.floor(videoDuration / 60);
+ let endSeconds = videoDuration % 60;
+
+ let start = `${startMinutes.toString().padStart(2, "0")}:${startSeconds
+ .toString()
+ .padStart(2, "0")}`;
+ let end = `${endMinutes.toString().padStart(2, "0")}:${endSeconds
+ .toString()
+ .padStart(2, "0")}`;
+
+ vtt += `${start} --> ${end}\n\n\n`;
+ }
+
+ const vttBlob = new Blob([vtt], { type: "text/vtt" });
+ const vttUrl = URL.createObjectURL(vttBlob);
+
+ setChapters(vttUrl);
+ }
+ return () => {
+ setChapters("");
+ };
+ }, [track?.skip, duration]);
+
+ useEffect(() => {
+ return () => {
+ if (player.current) {
+ player.current.destroy();
+ }
+ };
+ }, []);
+
+ function onEnded() {
+ if (!navigation?.next?.id) return;
+ if (autoNext === "on") {
+ const nextButton = document.querySelector(".next-button");
+
+ let timeoutId: ReturnType<typeof setTimeout>;
+
+ const stopTimeout = () => {
+ clearTimeout(timeoutId);
+ nextButton?.classList.remove("progress");
+ };
+
+ nextButton?.classList.remove("hidden");
+ nextButton?.classList.add("progress");
+
+ timeoutId = setTimeout(() => {
+ console.log("time is up!");
+ if (navigation?.next) {
+ router.push(
+ `/en/anime/watch/${dataMedia.id}/${track.provider}?id=${
+ navigation?.next?.id
+ }&num=${navigation?.next?.number}${
+ track?.isDub ? `&dub=${track?.isDub}` : ""
+ }`
+ );
+ }
+ }, 7000);
+
+ nextButton?.addEventListener("mouseover", stopTimeout);
+ }
+ }
+
+ function onLoadedMetadata() {
+ const seek: any = getSettings(navigation?.playing?.id);
+ if (playerState?.currentTime !== 0) return;
+ const seekTime = seek?.timeWatched;
+ const percentage = duration !== 0 ? seekTime / Math.round(duration) : 0;
+ const percentagedb =
+ duration !== 0 ? userData?.timeWatched / Math.round(duration) : 0;
+
+ if (percentage >= 0.9 || percentagedb >= 0.9) {
+ remote.seek(0);
+ console.log("Video started from the beginning");
+ } else if (userData?.timeWatched) {
+ remote.seek(userData?.timeWatched);
+ } else {
+ remote.seek(seekTime);
+ }
+ }
+
+ let mark = 0;
+ function onTimeUpdate(detail: MediaTimeUpdateEventDetail) {
+ if (sessions) {
+ let currentTime = detail.currentTime;
+ const percentage = currentTime / duration;
+
+ if (percentage >= 0.9) {
+ // use >= instead of >
+ if (mark < 1 && marked < 1) {
+ mark = 1;
+ setMarked(1);
+ console.log("marking progress");
+ markProgress(dataMedia.id, navigation.playing.number);
+ }
+ }
+ }
+
+ const opButton = document.querySelector(".op-button");
+ const edButton = document.querySelector(".ed-button");
+
+ const op: SkipData = track?.skip.find(
+ (item: SkipData) => item.text === "Opening"
+ ),
+ ed = track?.skip.find((item: SkipData) => item.text === "Ending");
+
+ if (
+ op &&
+ detail.currentTime > op.startTime &&
+ detail.currentTime < op.endTime
+ ) {
+ opButton?.classList.remove("hidden");
+ } else {
+ opButton?.classList.add("hidden");
+ }
+
+ if (
+ ed &&
+ detail.currentTime > ed.startTime &&
+ detail.currentTime < ed.endTime
+ ) {
+ edButton?.classList.remove("hidden");
+ } else {
+ edButton?.classList.add("hidden");
+ }
+ }
+
+ function onSeeked(currentTime: number) {
+ const nextButton = document.querySelector(".next-button");
+ // console.log({ currentTime, duration });
+ if (currentTime !== duration) {
+ nextButton?.classList.add("hidden");
+ }
+ }
+
+ return (
+ <MediaPlayer
+ key={id}
+ className={`${style.player} player`}
+ title={
+ navigation?.playing?.title ||
+ `Episode ${navigation?.playing?.number}` ||
+ "Loading..."
+ }
+ load="idle"
+ crossorigin="anonymous"
+ src={{
+ src: defaultQuality?.url,
+ type: "application/vnd.apple.mpegurl",
+ }}
+ onTimeUpdate={onTimeUpdate}
+ playsinline
+ aspectRatio={aspectRatio}
+ onEnd={onEnded}
+ onSeeked={onSeeked}
+ onLoadedMetadata={onLoadedMetadata}
+ ref={player}
+ >
+ <MediaProvider>
+ {track &&
+ track?.subtitles &&
+ track?.subtitles?.map((track: Subtitle) => (
+ <Track {...track} key={track.src} />
+ ))}
+ {chapters?.length > 0 && (
+ <Track key={chapters} src={chapters} kind="chapters" default={true} />
+ )}
+ </MediaProvider>
+ <VideoLayout thumbnails={track?.thumbnails} navigation={navigation} />
+ </MediaPlayer>
+ );
+}
+
+export function calculateAspectRatio(width: number, height: number) {
+ if (width === 0 && height === 0) {
+ return "16/9";
+ }
+
+ const gcd = (a: number, b: number): any => (b === 0 ? a : gcd(b, a % b));
+ const divisor = gcd(width, height);
+ const aspectRatio = `${width / divisor}/${height / divisor}`;
+ return aspectRatio;
+}
diff --git a/components/watch/new-player/tracks.tsx b/components/watch/new-player/tracks.tsx
new file mode 100644
index 0000000..abc1fb5
--- /dev/null
+++ b/components/watch/new-player/tracks.tsx
@@ -0,0 +1,184 @@
+export const textTracks = [
+ // Subtitles
+ // {
+ // src: "https://media-files.vidstack.io/sprite-fight/subs/english.vtt",
+ // label: "English",
+ // language: "en-US",
+ // kind: "subtitles",
+ // default: true,
+ // },
+ // {
+ // src: "https://media-files.vidstack.io/sprite-fight/subs/spanish.vtt",
+ // label: "Spanish",
+ // language: "es-ES",
+ // kind: "subtitles",
+ // },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ara-3.vtt",
+ label: "Arabic",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/chi-4.vtt",
+ label: "Chinese - Chinese Simplified",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/chi-5.vtt",
+ label: "Chinese - Chinese Traditional",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/hrv-6.vtt",
+ label: "Croatian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/cze-7.vtt",
+ label: "Czech",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/dan-8.vtt",
+ label: "Danish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/dut-9.vtt",
+ label: "Dutch",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/32/6d/326d5416033fe39a1540b11908f191fe/326d5416033fe39a1540b11908f191fe.vtt",
+ label: "English",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/fin-10.vtt",
+ label: "Finnish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/fre-11.vtt",
+ label: "French",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ger-12.vtt",
+ label: "German",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/gre-13.vtt",
+ label: "Greek",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/heb-14.vtt",
+ label: "Hebrew",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/hun-15.vtt",
+ label: "Hungarian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ind-16.vtt",
+ label: "Indonesian",
+ kind: "subtitles",
+ default: true,
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ita-17.vtt",
+ label: "Italian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/jpn-18.vtt",
+ label: "Japanese",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/kor-19.vtt",
+ label: "Korean",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/may-20.vtt",
+ label: "Malay",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/nob-21.vtt",
+ label: "Norwegian Bokmål - Norwegian Bokmal",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/pol-22.vtt",
+ label: "Polish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/por-23.vtt",
+ label: "Portuguese",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/por-24.vtt",
+ label: "Portuguese - Brazilian Portuguese",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/rum-25.vtt",
+ label: "Romanian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/rus-26.vtt",
+ label: "Russian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/spa-27.vtt",
+ label: "Spanish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/spa-28.vtt",
+ label: "Spanish - European Spanish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/swe-29.vtt",
+ label: "Swedish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/tha-30.vtt",
+ label: "Thai",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/tur-31.vtt",
+ label: "Turkish",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/ukr-32.vtt",
+ label: "Ukrainian",
+ kind: "subtitles",
+ },
+ {
+ src: "https://ccb.megaresources.co/3c/0f/3c0fbf6aafc60b02d3be402099638403/vie-33.vtt",
+ label: "Vietnamese",
+ kind: "subtitles",
+ },
+ // // Chapters
+ // {
+ // src: "https://media-files.vidstack.io/sprite-fight/chapters.vtt",
+ // kind: "chapters",
+ // language: "en-US",
+ // default: true,
+ // },
+] as const;
diff --git a/components/watch/player/artplayer.js b/components/watch/player/artplayer.js
deleted file mode 100644
index 666c103..0000000
--- a/components/watch/player/artplayer.js
+++ /dev/null
@@ -1,387 +0,0 @@
-import { useEffect, useRef } from "react";
-import Artplayer from "artplayer";
-import Hls from "hls.js";
-import { useWatchProvider } from "@/lib/context/watchPageProvider";
-import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
-
-export default function NewPlayer({
- playerRef,
- option,
- getInstance,
- provider,
- track,
- 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(() => {
- Artplayer.PLAYBACK_RATE = [0.5, 0.75, 1, 1.15, 1.2, 1.5, 1.7, 2];
-
- const art = new Artplayer({
- ...option,
- container: artRef.current,
- type: "m3u8",
- customType: {
- m3u8: playM3u8,
- },
- ...(subtitles?.length > 0 && {
- 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;
- },
- },
- subtitles?.length > 0 && {
- 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: '<i 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></i>',
- click: function (...args) {
- setPlayerState((prev) => ({
- ...prev,
- currentTime: art.currentTime,
- isPlaying: art.playing,
- }));
- setTheaterMode((prev) => !prev);
- },
- },
- {
- 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;
- },
- },
- {
- 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;
- },
- },
- ],
- });
-
- if ("mediaSession" in navigator) {
- art.on("video:timeupdate", () => {
- const session = navigator.mediaSession;
- if (!session) return;
- session.setPositionState({
- duration: art.duration,
- playbackRate: art.playbackRate,
- position: art.currentTime,
- });
- });
-
- navigator.mediaSession.setActionHandler("play", () => {
- art.play();
- });
-
- navigator.mediaSession.setActionHandler("pause", () => {
- art.pause();
- });
-
- navigator.mediaSession.setActionHandler("previoustrack", () => {
- if (track?.prev) {
- router.push(
- `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent(
- track?.prev?.id
- )}&num=${track?.prev?.number}`
- );
- }
- });
-
- navigator.mediaSession.setActionHandler("nexttrack", () => {
- if (track?.next) {
- router.push(
- `/en/anime/watch/${id}/${provider}?id=${encodeURIComponent(
- track?.next?.id
- )}&num=${track?.next?.number}`
- );
- }
- });
- }
-
- 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);
- }
- };
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- return <div ref={artRef} {...rest}></div>;
-}
diff --git a/components/watch/player/component/controls/quality.js b/components/watch/player/component/controls/quality.js
deleted file mode 100644
index 08dbd0e..0000000
--- a/components/watch/player/component/controls/quality.js
+++ /dev/null
@@ -1,15 +0,0 @@
-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/overlay.js b/components/watch/player/component/overlay.js
deleted file mode 100644
index 1d5ac27..0000000
--- a/components/watch/player/component/overlay.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * @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
deleted file mode 100644
index 665919b..0000000
--- a/components/watch/player/playerComponent.js
+++ /dev/null
@@ -1,527 +0,0 @@
-import React, { useEffect, useState } from "react";
-import NewPlayer from "./artplayer";
-import { icons } from "./component/overlay";
-import { useWatchProvider } from "@/lib/context/watchPageProvider";
-import { useRouter } from "next/router";
-import { useAniList } from "@/lib/anilist/useAnilist";
-import Loading from "@/components/shared/loading";
-
-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);
- const [error, setError] = useState(false);
-
- useEffect(() => {
- setLoading(true);
- const resol = localStorage.getItem("quality");
- const sub = JSON.parse(localStorage.getItem("subSize"));
- if (resol) {
- setResolution(resol);
- }
-
- 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);
- }
-
- const subtitle = data?.subtitles
- ?.filter(
- (subtitle) =>
- subtitle.lang !== "Thumbnails" && subtitle.lang !== "thumbnails"
- )
- ?.map((subtitle) => {
- const isEnglish =
- subtitle.lang === "English" ||
- subtitle.lang === "English / English (US)";
- return {
- ...(isEnglish && { default: true }),
- url: subtitle.url,
- html: `${subtitle.lang}`,
- };
- });
-
- if (subtitle) {
- const defSub = data?.subtitles.find(
- (i) => i.lang === "English" || i.lang === "English / English (US)"
- );
-
- 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);
- };
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [provider, data]);
-
- /**
- * @param {import("artplayer")} art
- */
- function getInstance(art) {
- art.on("ready", () => {
- const autoplay = localStorage.getItem("autoplay_video") || false;
-
- // check media queries for mobile devices
- const isMobile = window.matchMedia("(max-width: 768px)").matches;
-
- // console.log(art.fullscreen);
-
- if (isMobile) {
- art.controls.remove("theater-button");
- // art.controls.remove("fast-rewind");
- // art.controls.remove("fast-forward");
- }
-
- 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("error", (error, reconnectTime) => {
- if (error && reconnectTime >= 5) {
- setError(true);
- console.error("Error while loading video:", error);
- }
- });
-
- 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,
- autoplay: autoplay ? true : false,
- autoSize: false,
- playbackRate: true,
- fullscreen: true,
- autoOrientation: true,
- icons: icons,
- setting: true,
- screenshot: true,
- hotkey: true,
- pip: true,
- airplay: true,
- lock: true,
- };
-
- return (
- <div
- id={id}
- className={`${className} bg-black`}
- style={{ aspectRatio: aspectRatio }}
- >
- <div className="flex-center w-full h-full">
- {!data?.error && !url && (
- <div className="flex-center w-full h-full">
- <Loading />
- </div>
- )}
- {!error ? (
- !loading && track && url && !data?.error ? (
- <NewPlayer
- playerRef={playerRef}
- res={resolution}
- quality={source}
- option={option}
- provider={provider}
- track={track}
- defSize={defSize}
- defSub={defSub}
- subSize={subSize}
- subtitles={subtitle}
- getInstance={getInstance}
- style={{
- width: "100%",
- height: "100%",
- }}
- />
- ) : (
- <p className="text-center">
- {data?.status === 404 && "Not Found"}
- <br />
- {data?.error}
- </p>
- )
- ) : (
- <p className="text-center">
- Something went wrong while loading the video, <br />
- please try from other source
- </p>
- )}
- </div>
- </div>
- );
-}
diff --git a/components/watch/primary/details.js b/components/watch/primary/details.tsx
index 4af12ac..f20f8cf 100644
--- a/components/watch/primary/details.js
+++ b/components/watch/primary/details.tsx
@@ -2,7 +2,20 @@ import { useEffect, useState } from "react";
import { useAniList } from "../../../lib/anilist/useAnilist";
import Skeleton from "react-loading-skeleton";
import DisqusComments from "../../disqus";
-import Image from "next/image";
+import { AniListInfoTypes } from "types/info/AnilistInfoTypes";
+import { SessionTypes } from "pages/en";
+
+type DetailsProps = {
+ info: AniListInfoTypes;
+ session: SessionTypes;
+ epiNumber: number;
+ description: string;
+ id: string;
+ onList: boolean;
+ setOnList: (value: boolean) => void;
+ handleOpen: () => void;
+ disqus: string;
+};
export default function Details({
info,
@@ -14,10 +27,14 @@ export default function Details({
setOnList,
handleOpen,
disqus,
-}) {
+}: DetailsProps) {
const [showComments, setShowComments] = useState(false);
const { markPlanning } = useAniList(session);
+ const [showDesc, setShowDesc] = useState(false);
+
+ const truncatedDesc = truncateText(description, 420);
+
function handlePlan() {
if (onList === false) {
markPlanning(info.id);
@@ -32,6 +49,10 @@ export default function Details({
} else {
setShowComments(true);
}
+ return () => {
+ setShowComments(false);
+ setShowDesc(false);
+ };
}, [id]);
return (
@@ -133,12 +154,28 @@ export default function Details({
))}
</div>
{/* <div className={`bg-secondary rounded-md mt-3 mx-3`}> */}
- <div className={`bg-secondary rounded-md mt-3`}>
+ <div className={`relative bg-secondary rounded-md mt-3`}>
{info && (
- <p
- dangerouslySetInnerHTML={{ __html: description }}
- className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `}
- />
+ <>
+ <p
+ dangerouslySetInnerHTML={{
+ __html: showDesc
+ ? description
+ : description?.length > 420
+ ? truncatedDesc
+ : description,
+ }}
+ className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `}
+ />
+ {!showDesc && description?.length > 120 && (
+ <span
+ onClick={() => setShowDesc((prev) => !prev)}
+ className="flex justify-center items-end rounded-md pb-5 font-semibold font-karla cursor-pointer w-full h-full bg-gradient-to-t from-secondary hover:from-20% to-transparent absolute inset-0"
+ >
+ Read More
+ </span>
+ )}
+ </>
)}
</div>
{/* {<div className="mt-5 px-5"></div>} */}
@@ -177,7 +214,6 @@ export default function Details({
<DisqusComments
key={id}
post={{
- id: id,
title: info.title.romaji,
url: window.location.href,
episode: epiNumber,
@@ -191,3 +227,8 @@ export default function Details({
</div>
);
}
+
+function truncateText(txt: string, length: number) {
+ const text = txt.replace(/(<([^>]+)>)/gi, "");
+ return text.length > length ? text.slice(0, length) + "..." : text;
+}
diff --git a/components/watch/secondary/episodeLists.js b/components/watch/secondary/episodeLists.tsx
index a676be0..2c23f25 100644
--- a/components/watch/secondary/episodeLists.js
+++ b/components/watch/secondary/episodeLists.tsx
@@ -3,6 +3,19 @@ import Image from "next/image";
import Link from "next/link";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
+import { AniListInfoTypes } from "types/info/AnilistInfoTypes";
+import { Episode } from "types/api/Episode";
+
+type EpisodeListsProps = {
+ info: AniListInfoTypes;
+ map: any;
+ providerId: string;
+ watchId: string;
+ episode: Episode[];
+ artStorage: any;
+ track: any;
+ dub: string;
+};
export default function EpisodeLists({
info,
@@ -13,7 +26,7 @@ export default function EpisodeLists({
artStorage,
track,
dub,
-}) {
+}: EpisodeListsProps) {
const progress = info.mediaListEntry?.progress;
const router = useRouter();
@@ -45,8 +58,8 @@ export default function EpisodeLists({
router.push(
`/en/anime/watch/${info.id}/${providerId}?id=${
- selectedEpisode.id
- }&num=${selectedEpisode.number}${dub ? `&dub=${dub}` : ""}`
+ selectedEpisode?.id
+ }&num=${selectedEpisode?.number}${dub ? `&dub=${dub}` : ""}`
);
}}
className="flex items-center text-sm gap-5 rounded-[3px] bg-secondary py-1 px-3 pr-8 font-karla appearance-none cursor-pointer outline-none focus:ring-1 focus:ring-action group-hover:ring-1 group-hover:ring-action"
@@ -64,7 +77,7 @@ export default function EpisodeLists({
<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: any) =>
(item?.img || item?.image) &&
!item?.img?.includes("https://s4.anilist.co/")
) > 0 ? (
@@ -74,7 +87,14 @@ export default function EpisodeLists({
let prog = (time / duration) * 100;
if (prog > 90) prog = 100;
- const mapData = map?.find((i) => i.number === item.number);
+ const mapData = map?.find((i: any) => i.number === item.number);
+
+ const parsedImage = mapData
+ ? mapData?.img?.includes("null") ||
+ mapData?.image?.includes("null")
+ ? info.coverImage?.extraLarge
+ : mapData?.img || mapData?.image
+ : info.coverImage?.extraLarge || null;
return (
<Link
href={`/en/anime/watch/${
@@ -93,11 +113,7 @@ export default function EpisodeLists({
<div className="relative">
{/* <div className="absolute inset-0 w-full h-full z-40" /> */}
<Image
- src={
- mapData?.img ||
- mapData?.image ||
- info?.coverImage?.extraLarge
- }
+ src={parsedImage || info?.coverImage?.extraLarge}
draggable={false}
alt="Anime Cover"
width={1000}
diff --git a/jsconfig.json b/jsconfig.json
deleted file mode 100644
index babd576..0000000
--- a/jsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/components/*": ["components/*"],
- "@/utils/*": ["utils/*"],
- "@/lib/*": ["lib/*"],
- "@/prisma/*": ["prisma/*"]
- }
- }
-}
diff --git a/lib/anify/getMangaId.js b/lib/anify/getMangaId.ts
index 6b1445f..bf7bb71 100644
--- a/lib/anify/getMangaId.js
+++ b/lib/anify/getMangaId.ts
@@ -1,8 +1,25 @@
-import axios from "axios";
+import axios, { AxiosResponse } from "axios";
-export async function fetchInfo(romaji, english, native) {
+interface Manga {
+ id: string;
+ title: {
+ romaji: string;
+ english: string;
+ native: string;
+ };
+}
+
+interface SearchResult {
+ results: Manga[];
+}
+
+export async function fetchInfo(
+ romaji: string,
+ english: string,
+ native: string
+): Promise<{ id: string } | null> {
try {
- const { data: getManga } = await axios.get(
+ const { data: getManga }: AxiosResponse<SearchResult> = await axios.get(
`https://api.anify.tv/search-advanced?query=${
english || romaji
}&type=manga`
@@ -26,10 +43,14 @@ export async function fetchInfo(romaji, english, native) {
}
}
-export default async function getMangaId(romaji, english, native) {
+export default async function getMangaId(
+ romaji: string,
+ english: string,
+ native: string
+): Promise<{ id: string } | { message: string } | { error: any }> {
try {
const data = await fetchInfo(romaji, english, native);
- if (data) {
+ if (data && "id" in data) {
return data;
} else {
return { message: "Schedule not found" };
diff --git a/lib/anify/info.js b/lib/anify/info.js
index 05159ce..8284e94 100644
--- a/lib/anify/info.js
+++ b/lib/anify/info.js
@@ -1,7 +1,6 @@
import axios from "axios";
-import { redis } from "../redis";
-export async function fetchInfo(id, key) {
+export async function fetchInfo(id) {
try {
const { data } = await axios.get(`https://api.anify.tv/info/${id}`);
return data;
@@ -11,24 +10,13 @@ export async function fetchInfo(id, key) {
}
}
-export default async function getAnifyInfo(id, key) {
+export default async function getAnifyInfo(id) {
try {
- let cached;
- if (redis) {
- cached = await redis.get(id);
- }
- if (cached) {
- return JSON.parse(cached);
+ const data = await fetchInfo(id);
+ if (data) {
+ return data;
} else {
- const data = await fetchInfo(id, key);
- if (data) {
- if (redis) {
- await redis.set(id, JSON.stringify(data), "EX", 60 * 10);
- }
- return data;
- } else {
- return { message: "Schedule not found" };
- }
+ return { message: "Anify Info Not Found!" };
}
} catch (error) {
return { error };
diff --git a/lib/anilist/aniAdvanceSearch.js b/lib/anilist/aniAdvanceSearch.ts
index ccfbd27..5251815 100644
--- a/lib/anilist/aniAdvanceSearch.js
+++ b/lib/anilist/aniAdvanceSearch.ts
@@ -1,8 +1,32 @@
+import { AnifySearchAdvanceTypes } from "types/info/AnifySearchAdvanceTypes";
import { advanceSearchQuery } from "../graphql/query";
+export type AniAdvanceSearch = {
+ search?: string;
+ type?: string;
+ genres?: any[];
+ page?: number;
+ sort?: string;
+ format?:
+ | "TV"
+ | "TV_SHORT"
+ | "MOVIE"
+ | "SPECIAL"
+ | "OVA"
+ | "ONA"
+ | "MUSIC"
+ | "MANGA"
+ | "NOVEL"
+ | "ONE_SHOT"
+ | undefined;
+ season?: string;
+ seasonYear?: number;
+ perPage?: number;
+};
+
export async function aniAdvanceSearch({
search,
- type,
+ type = "ANIME",
genres,
page,
sort,
@@ -10,7 +34,7 @@ export async function aniAdvanceSearch({
season,
seasonYear,
perPage,
-}) {
+}: AniAdvanceSearch) {
const categorizedGenres = genres?.reduce((result, item) => {
const existingEntry = result[item.type];
@@ -43,10 +67,10 @@ export async function aniAdvanceSearch({
}),
});
- const data = await response.json();
+ const data: AnifySearchAdvanceTypes = await response.json();
return {
pageInfo: {
- hasNextPage: page < data.total,
+ hasNextPage: page ?? 0 < data.total,
currentPage: page,
lastPage: Math.ceil(data.lastPage),
perPage: perPage ?? 20,
@@ -62,7 +86,7 @@ export async function aniAdvanceSearch({
large: item.coverImage,
},
description: item.description,
- duration: item.duration ?? null,
+ duration: item?.duration ?? null,
endDate: {
day: null,
month: null,
diff --git a/lib/anilist/getUpcomingAnime.js b/lib/anilist/getUpcomingAnime.js
index 2ab9315..d5249f1 100644
--- a/lib/anilist/getUpcomingAnime.js
+++ b/lib/anilist/getUpcomingAnime.js
@@ -59,7 +59,7 @@ const getUpcomingAnime = async () => {
`;
const variables = {
- season: "FALL",
+ season: currentSeason,
year: currentYear,
format: "TV",
};
diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js
index 20c1964..323dd29 100644
--- a/lib/anilist/useAnilist.js
+++ b/lib/anilist/useAnilist.js
@@ -225,6 +225,9 @@ export const useAniList = (session) => {
// if (lists.length > 0) {
await fetchGraphQL(progressWatched, variables);
console.log(`Progress Updated: ${progress}`, status);
+ toast.success(`Progress Updated: ${progress}`, {
+ position: "bottom-right",
+ });
// }
} else if (media && media.type === "MANGA") {
let variables = {
diff --git a/lib/context/watchPageProvider.js b/lib/context/watchPageProvider.js
index a9d707b..c305710 100644
--- a/lib/context/watchPageProvider.js
+++ b/lib/context/watchPageProvider.js
@@ -9,10 +9,14 @@ export const WatchPageProvider = ({ children }) => {
currentTime: 0,
isPlaying: false,
});
- const [autoplay, setAutoPlay] = useState(false);
+ const [autoplay, setAutoPlay] = useState(null);
+ const [autoNext, setAutoNext] = useState(null);
const [marked, setMarked] = useState(0);
const [userData, setUserData] = useState(null);
+ const [dataMedia, setDataMedia] = useState(null);
+
+ const [track, setTrack] = useState(null);
return (
<WatchPageContext.Provider
@@ -29,6 +33,12 @@ export const WatchPageProvider = ({ children }) => {
setAutoPlay,
marked,
setMarked,
+ track,
+ setTrack,
+ dataMedia,
+ setDataMedia,
+ autoNext,
+ setAutoNext,
}}
>
{children}
diff --git a/utils/useCountdownSeconds.js b/lib/hooks/useCountdownSeconds.ts
index df3cb63..3d17ede 100644
--- a/utils/useCountdownSeconds.js
+++ b/lib/hooks/useCountdownSeconds.ts
@@ -1,6 +1,19 @@
import { useEffect, useState } from "react";
-const useCountdown = (targetDate, update) => {
+interface CountdownValues {
+ days: number;
+ hours: number;
+ minutes: number;
+ seconds: number;
+}
+
+interface Props {
+ targetDate: number;
+ update: Function;
+ countdown: CountdownValues;
+}
+
+const useCountdown = (targetDate: number, update: Function): Props => {
const countDownDate = new Date(targetDate).getTime();
const [countDown, setCountDown] = useState(
@@ -19,10 +32,14 @@ const useCountdown = (targetDate, update) => {
return () => clearInterval(interval);
}, [countDownDate, update]);
- return getReturnValues(countDown);
+ return {
+ targetDate,
+ update,
+ countdown: getReturnValues(countDown),
+ };
};
-const getReturnValues = (countDown) => {
+const getReturnValues = (countDown: number): CountdownValues => {
// calculate time left
const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
const hours = Math.floor(
@@ -31,7 +48,7 @@ const getReturnValues = (countDown) => {
const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((countDown % (1000 * 60)) / 1000);
- return [days, hours, minutes, seconds];
+ return { days, hours, minutes, seconds };
};
export { useCountdown };
diff --git a/lib/hooks/useWatchStorage.tsx b/lib/hooks/useWatchStorage.tsx
new file mode 100644
index 0000000..ee24a39
--- /dev/null
+++ b/lib/hooks/useWatchStorage.tsx
@@ -0,0 +1,28 @@
+import { UserData } from "@/components/watch/new-player/player";
+import { useState } from "react";
+
+function useWatchStorage() {
+ // Get initial value from local storage or empty object
+ const [settings, setSettings] = useState(() => {
+ const storedSettings = localStorage?.getItem("artplayer_settings");
+ return storedSettings ? JSON.parse(storedSettings) : {};
+ });
+
+ const getSettings = (id: string): UserData | undefined => {
+ return settings[id];
+ };
+
+ // Function to update settings
+ const updateSettings = (id: string, data?: any) => {
+ // Update state
+ const updatedSettings = { ...settings, [id]: data };
+ setSettings(updatedSettings);
+
+ // Update local storage
+ localStorage.setItem("artplayer_settings", JSON.stringify(updatedSettings));
+ };
+
+ return [getSettings, updateSettings];
+}
+
+export default useWatchStorage;
diff --git a/lib/prisma.js b/lib/prisma.ts
index ed8c421..55acf8d 100644
--- a/lib/prisma.js
+++ b/lib/prisma.ts
@@ -1,5 +1,9 @@
import { PrismaClient } from "@prisma/client";
+declare global {
+ var prisma: PrismaClient | undefined;
+}
+
export const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
diff --git a/lib/redis.js b/lib/redis.ts
index 9522e4c..1778933 100644
--- a/lib/redis.js
+++ b/lib/redis.ts
@@ -1,16 +1,16 @@
import { Redis } from "ioredis";
import { RateLimiterRedis } from "rate-limiter-flexible";
-const REDIS_URL = process.env.REDIS_URL;
+const REDIS_URL: string | undefined = process.env.REDIS_URL;
-let redis;
-let rateLimiterRedis;
-let rateLimitStrict;
-let rateSuperStrict;
+let redis: Redis;
+let rateLimiterRedis: RateLimiterRedis;
+let rateLimitStrict: RateLimiterRedis;
+let rateSuperStrict: RateLimiterRedis;
if (REDIS_URL) {
redis = new Redis(REDIS_URL);
- redis.on("error", (err) => {
+ redis.on("error", (err: Error) => {
console.error("Redis error: ", err);
});
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..4f11a03
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,5 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/next.config.js b/next.config.js
index d3fd882..7920ac0 100644
--- a/next.config.js
+++ b/next.config.js
@@ -9,8 +9,13 @@ const withPWA = require("next-pwa")({
});
module.exports = withPWA({
- reactStrictMode: false,
+ reactStrictMode: true,
+ webpack(config, options) {
+ config.resolve.extensions.push(".ts", ".tsx");
+ return config;
+ },
images: {
+ unoptimized: true,
remotePatterns: [
{
protocol: "https",
@@ -28,6 +33,10 @@ module.exports = withPWA({
protocol: "https",
hostname: "tenor.com",
},
+ {
+ protocol: "https",
+ hostname: "meionovel.id",
+ },
],
},
// distDir: process.env.BUILD_DIR || ".next",
@@ -41,6 +50,24 @@ module.exports = withPWA({
permanent: false,
basePath: false,
},
+ {
+ source: "/changelogs",
+ destination: "https://github.com/Ani-Moopa/Moopa/releases",
+ permanent: false,
+ basePath: false,
+ },
+ {
+ source: "/github",
+ destination: "https://github.com/Ani-Moopa/Moopa",
+ permanent: false,
+ basePath: false,
+ },
+ {
+ source: "/discord",
+ destination: "https://discord.gg/v5fjSdKwr2",
+ permanent: false,
+ basePath: false,
+ },
];
},
// async headers() {
diff --git a/package-lock.json b/package-lock.json
index 5183bfa..613a188 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,28 +1,28 @@
{
"name": "moopa",
- "version": "4.3.0",
+ "version": "4.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "moopa",
- "version": "4.3.0",
+ "version": "4.3.1",
"dependencies": {
- "@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.17",
"@prisma/client": "^5.3.1",
"@vercel/og": "^0.5.4",
- "artplayer": "^5.0.9",
- "artplayer-plugin-hls-quality": "^2.0.0",
- "axios": "^1.6.0",
- "closest-match": "^1.3.3",
+ "@vidstack/react": "^1.8.3",
+ "axios": "^1.4.0",
+ "cookies": "^0.8.0",
"cron": "^2.4.0",
"disqus-react": "^1.1.5",
"framer-motion": "^8.5.0",
"graphql": "^15.8.0",
- "hls.js": "^1.3.2",
+ "hls.js": "^1.4.12",
"ioredis": "^5.3.2",
+ "jsonwebtoken": "^9.0.2",
+ "media-icons": "^1.0.0",
"next": "^13.5.5",
"next-auth": "^4.24.5",
"next-pwa": "^5.6.0",
@@ -39,13 +39,19 @@
"workbox-webpack-plugin": "^7.0.0"
},
"devDependencies": {
+ "@types/cookies": "^0.7.10",
+ "@types/jsonwebtoken": "^9.0.5",
+ "@types/node": "^20.8.10",
+ "@types/react": "^18.2.33",
"autoprefixer": "^10.4.14",
"depcheck": "^1.4.3",
"eslint": "^8.38.0",
"eslint-config-next": "^13.5.2",
"prisma": "^5.3.1",
"tailwind-scrollbar": "^2.1.0",
- "tailwindcss": "^3.3.1"
+ "tailwindcss": "^3.3.1",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "^5.2.2"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -81,47 +87,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/@apollo/client": {
- "version": "3.7.17",
- "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.17.tgz",
- "integrity": "sha512-0EErSHEtKPNl5wgWikHJbKFAzJ/k11O0WO2QyqZSHpdxdAnw7UWHY4YiLbHCFG7lhrD+NTQ3Z/H9Jn4rcikoJA==",
- "dependencies": {
- "@graphql-typed-document-node/core": "^3.1.1",
- "@wry/context": "^0.7.0",
- "@wry/equality": "^0.5.0",
- "@wry/trie": "^0.4.0",
- "graphql-tag": "^2.12.6",
- "hoist-non-react-statics": "^3.3.2",
- "optimism": "^0.16.2",
- "prop-types": "^15.7.2",
- "response-iterator": "^0.2.6",
- "symbol-observable": "^4.0.0",
- "ts-invariant": "^0.10.3",
- "tslib": "^2.3.0",
- "zen-observable-ts": "^1.2.5"
- },
- "peerDependencies": {
- "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
- "graphql-ws": "^5.5.5",
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
- "subscriptions-transport-ws": "^0.9.0 || ^0.11.0"
- },
- "peerDependenciesMeta": {
- "graphql-ws": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "subscriptions-transport-ws": {
- "optional": true
- }
- }
- },
"node_modules/@babel/code-frame": {
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
@@ -1844,14 +1809,6 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
- "node_modules/@graphql-typed-document-node/core": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
- "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
- "peerDependencies": {
- "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
- }
- },
"node_modules/@headlessui/react": {
"version": "1.7.16",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.16.tgz",
@@ -2392,6 +2349,37 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/body-parser": {
+ "version": "1.19.5",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+ "dev": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cookies": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.10.tgz",
+ "integrity": "sha512-hmUCjAk2fwZVPPkkPBcI7jGLIR5mg4OVoNMBwU6aVsMm/iNPY7z9/R+x2fSwLt/ZXoGua6C5Zy2k5xOo9jUyhQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/express": "*",
+ "@types/keygrip": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/eslint": {
"version": "8.44.2",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
@@ -2418,6 +2406,30 @@
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
"peer": true
},
+ "node_modules/@types/express": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.17.41",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz",
+ "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -2427,6 +2439,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/http-errors": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+ "dev": true
+ },
"node_modules/@types/json-schema": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@@ -2438,15 +2456,39 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/jsonwebtoken": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
+ "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/keygrip": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
+ "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==",
+ "dev": true
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true
+ },
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="
},
"node_modules/@types/node": {
- "version": "20.4.7",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.7.tgz",
- "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g=="
+ "version": "20.8.10",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
+ "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/@types/nprogress": {
"version": "0.2.0",
@@ -2459,6 +2501,33 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.9",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz",
+ "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.10",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
+ "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==",
+ "dev": true
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.2.33",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz",
+ "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -2467,6 +2536,32 @@
"@types/node": "*"
}
},
+ "node_modules/@types/scheduler": {
+ "version": "0.16.5",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.5.tgz",
+ "integrity": "sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw=="
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+ "dev": true,
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.5",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
+ "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/trusted-types": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
@@ -2586,6 +2681,21 @@
"node": ">=16"
}
},
+ "node_modules/@vidstack/react": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/@vidstack/react/-/react-1.8.3.tgz",
+ "integrity": "sha512-QCyHy6e3LpzfajtjrhJPXzGYbBrBCUE5qYAatKXX+nxWqRvspa0fJPlnGeWb+tg6DlDsgwDLFjGNWj8qUeUVXQ==",
+ "dependencies": {
+ "media-captions": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.0.0",
+ "react": "^18.0.0"
+ }
+ },
"node_modules/@vue/compiler-core": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
@@ -2837,39 +2947,6 @@
"@xtuc/long": "4.2.2"
}
},
- "node_modules/@wry/context": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz",
- "integrity": "sha512-Nl8WTesHp89RF803Se9X3IiHjdmLBrIvPMaJkl+rKVJAYyPsz1TEUbu89943HpvujtSJgDUx9W4vZw3K1Mr3sA==",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@wry/equality": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.6.tgz",
- "integrity": "sha512-D46sfMTngaYlrH+OspKf8mIJETntFnf6Hsjb0V41jAXJ7Bx2kB8Rv8RCUujuVWYttFtHkUNp7g+FwxNQAr6mXA==",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/@wry/trie": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.4.3.tgz",
- "integrity": "sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -3149,19 +3226,6 @@
"node": ">=8"
}
},
- "node_modules/artplayer": {
- "version": "5.0.9",
- "resolved": "https://registry.npmjs.org/artplayer/-/artplayer-5.0.9.tgz",
- "integrity": "sha512-IM/DShYdmKFEA9jl08LYbTK2Jfz9s7qIjEH0xWjnxvVArUKZZKcoqwr6i54U0c4grtc/Uvb4wtCd78kvtSVlgw==",
- "dependencies": {
- "option-validator": "^2.0.6"
- }
- },
- "node_modules/artplayer-plugin-hls-quality": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/artplayer-plugin-hls-quality/-/artplayer-plugin-hls-quality-2.0.0.tgz",
- "integrity": "sha512-+/tiLXi2BNOuw7z2ayI6cYlZBZEP/ujS01bTtanRi2P0zl8wHafPEk0bAA8VbXxpP9gYT0/DjBIifNR9W0xqhA=="
- },
"node_modules/ast-types-flow": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
@@ -3249,9 +3313,9 @@
}
},
"node_modules/axios": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
- "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz",
+ "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -3448,6 +3512,11 @@
"ieee754": "^1.1.13"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -3659,11 +3728,6 @@
"wrap-ansi": "^7.0.0"
}
},
- "node_modules/closest-match": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/closest-match/-/closest-match-1.3.3.tgz",
- "integrity": "sha512-RSdHrZwNOvt2uMQgqJDJdM/I+5MlJ1tQJEXYrbRjSMXWiCRo06g2hwObJ7+WKt2J9ySK9/pJ0Q2vbL+BPkofDA=="
- },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -3773,6 +3837,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/cookies": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz",
+ "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "keygrip": "~1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/core-js-compat": {
"version": "3.32.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz",
@@ -3871,6 +3947,11 @@
"node": ">=4"
}
},
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4073,6 +4154,14 @@
"node": ">=10"
}
},
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/deps-regex": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.1.4.tgz",
@@ -4140,6 +4229,14 @@
"node": ">=6.0.0"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
@@ -5392,20 +5489,6 @@
"node": ">= 10.x"
}
},
- "node_modules/graphql-tag": {
- "version": "2.12.6",
- "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz",
- "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==",
- "dependencies": {
- "tslib": "^2.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "peerDependencies": {
- "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
- }
- },
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -5497,17 +5580,9 @@
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="
},
"node_modules/hls.js": {
- "version": "1.4.10",
- "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.10.tgz",
- "integrity": "sha512-wAVSj4Fm2MqOHy5+BlYnlKxXvJlv5IuZHjlzHu18QmjRzSDFQiUDWdHs5+NsFMQrgKEBwuWDcyvaMC9dUzJ5Uw=="
- },
- "node_modules/hoist-non-react-statics": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
- "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dependencies": {
- "react-is": "^16.7.0"
- }
+ "version": "1.4.12",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.12.tgz",
+ "integrity": "sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA=="
},
"node_modules/idb": {
"version": "7.1.1",
@@ -6259,6 +6334,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -6274,12 +6370,34 @@
"node": ">=4.0"
}
},
- "node_modules/kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/keygrip": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
+ "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
+ "dependencies": {
+ "tsscmp": "1.0.6"
+ },
"engines": {
- "node": ">=0.10.0"
+ "node": ">= 0.6"
}
},
"node_modules/language-subtag-registry": {
@@ -6394,17 +6512,52 @@
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
"node_modules/lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
},
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+ },
"node_modules/lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -6471,6 +6624,22 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/media-captions": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/media-captions/-/media-captions-1.0.1.tgz",
+ "integrity": "sha512-vicgtBYqNLvZStIPOpxHJxg/T7sVFVyi6A43PQLl5jMjblvRWhZ8V/LVBboeBxddSlPYnLWUQQI41Uv6V0tQRQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/media-icons": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/media-icons/-/media-icons-1.0.0.tgz",
+ "integrity": "sha512-NpGZOUqNLz5BhvGB1CkB/ejinnsiQQCjwNMrz4X6e9PO5E+am44jo75fnxCdXSVEp445B3U3gSbwYyGR9GQV2w==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -7174,34 +7343,6 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
- "node_modules/optimism": {
- "version": "0.16.2",
- "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.2.tgz",
- "integrity": "sha512-zWNbgWj+3vLEjZNIh/okkY2EUfX+vB9TJopzIZwT1xxaMqC5hRLLraePod4c5n4He08xuXNH+zhKFFCu390wiQ==",
- "dependencies": {
- "@wry/context": "^0.7.0",
- "@wry/trie": "^0.3.0"
- }
- },
- "node_modules/optimism/node_modules/@wry/trie": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.2.tgz",
- "integrity": "sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==",
- "dependencies": {
- "tslib": "^2.3.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/option-validator": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/option-validator/-/option-validator-2.0.6.tgz",
- "integrity": "sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==",
- "dependencies": {
- "kind-of": "^6.0.3"
- }
- },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -8064,14 +8205,6 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
- "node_modules/response-iterator": {
- "version": "0.2.6",
- "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz",
- "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==",
- "engines": {
- "node": ">=0.8"
- }
- },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -8800,14 +8933,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/symbol-observable": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
- "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==",
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/tailwind-scrollbar": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.1.0.tgz",
@@ -8862,6 +8987,15 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "dev": true,
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
"node_modules/tailwindcss/node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
@@ -9099,17 +9233,6 @@
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true
},
- "node_modules/ts-invariant": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz",
- "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==",
- "dependencies": {
- "tslib": "^2.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/tsconfig-paths": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
@@ -9139,6 +9262,14 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
"integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig=="
},
+ "node_modules/tsscmp": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+ "engines": {
+ "node": ">=0.6.x"
+ }
+ },
"node_modules/tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
@@ -9256,11 +9387,10 @@
}
},
"node_modules/typescript": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
- "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9283,6 +9413,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+ },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -10062,19 +10197,6 @@
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
"integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="
- },
- "node_modules/zen-observable": {
- "version": "0.8.15",
- "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
- "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
- },
- "node_modules/zen-observable-ts": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz",
- "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==",
- "dependencies": {
- "zen-observable": "0.8.15"
- }
}
}
}
diff --git a/package.json b/package.json
index 5b2c345..3ea7c4c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "moopa",
- "version": "4.3.0",
+ "version": "4.3.1",
"private": true,
"founder": "Factiven",
"scripts": {
@@ -8,24 +8,25 @@
"build": "next build",
"export": "next build && next export",
"start": "next start",
+ "type-check": "tsc",
"lint": "next lint"
},
"dependencies": {
- "@apollo/client": "^3.7.3",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.17",
"@prisma/client": "^5.3.1",
"@vercel/og": "^0.5.4",
- "artplayer": "^5.0.9",
- "artplayer-plugin-hls-quality": "^2.0.0",
- "axios": "^1.6.0",
- "closest-match": "^1.3.3",
+ "@vidstack/react": "^1.8.3",
+ "axios": "^1.4.0",
+ "cookies": "^0.8.0",
"cron": "^2.4.0",
"disqus-react": "^1.1.5",
"framer-motion": "^8.5.0",
"graphql": "^15.8.0",
- "hls.js": "^1.3.2",
+ "hls.js": "^1.4.12",
"ioredis": "^5.3.2",
+ "jsonwebtoken": "^9.0.2",
+ "media-icons": "^1.0.0",
"next": "^13.5.5",
"next-auth": "^4.24.5",
"next-pwa": "^5.6.0",
@@ -42,12 +43,18 @@
"workbox-webpack-plugin": "^7.0.0"
},
"devDependencies": {
+ "@types/cookies": "^0.7.10",
+ "@types/jsonwebtoken": "^9.0.5",
+ "@types/node": "^20.8.10",
+ "@types/react": "^18.2.33",
"autoprefixer": "^10.4.14",
"depcheck": "^1.4.3",
"eslint": "^8.38.0",
"eslint-config-next": "^13.5.2",
"prisma": "^5.3.1",
"tailwind-scrollbar": "^2.1.0",
- "tailwindcss": "^3.3.1"
+ "tailwindcss": "^3.3.1",
+ "tailwindcss-animate": "^1.0.7",
+ "typescript": "^5.2.2"
}
}
diff --git a/pages/404.js b/pages/404.tsx
index 085d984..d8e38da 100644
--- a/pages/404.js
+++ b/pages/404.tsx
@@ -1,8 +1,7 @@
import Head from "next/head";
-import Link from "next/link";
import Image from "next/image";
import Footer from "@/components/shared/footer";
-import { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
import { useRouter } from "next/router";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
@@ -16,7 +15,7 @@ export default function Custom404() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/svg/c.svg" />
</Head>
- <NewNavbar withNav shrink />
+ <Navbar withNav shrink />
<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.tsx
index e2f780d..c6b72ca 100644
--- a/pages/_app.js
+++ b/pages/_app.tsx
@@ -1,25 +1,25 @@
+import "../styles/globals.css";
+import "react-loading-skeleton/dist/skeleton.css";
import { useRouter } from "next/router";
import { AnimatePresence, motion as m } from "framer-motion";
import NextNProgress from "nextjs-progressbar";
import { SessionProvider } from "next-auth/react";
-import "../styles/globals.css";
-import "react-loading-skeleton/dist/skeleton.css";
import { SkeletonTheme } from "react-loading-skeleton";
import SearchPalette from "@/components/searchPalette";
import { SearchProvider } from "@/lib/context/isOpenState";
-import Head from "next/head";
import { WatchPageProvider } from "@/lib/context/watchPageProvider";
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
import { unixTimestampToRelativeTime } from "@/utils/getTimes";
-import SecretPage from "@/components/secret";
+// import SecretPage from "@/components/secret";
import { Toaster, toast } from "sonner";
+import ChangeLogs from "../components/shared/changelogs";
+import type { AppProps } from "next/app";
export default function App({
Component,
pageProps: { session, ...pageProps },
-}) {
+}: AppProps) {
const router = useRouter();
- const [info, setInfo] = useState(null);
useEffect(() => {
async function getBroadcast() {
@@ -33,30 +33,16 @@ export default function App({
});
const data = await res.json();
if (data?.show === true) {
- toast.message(
- `🚧${data.message} ${
+ toast.message(`Update Notice!`, {
+ position: "bottom-right",
+ important: true,
+ duration: 100000,
+ className: "font-karla",
+ description: `${data.message} ${
data?.startAt ? unixTimestampToRelativeTime(data.startAt) : ""
- }🚧`,
- {
- position: "bottom-right",
- important: true,
- duration: 100000,
- className: "flex-center font-karla text-white",
- // description: `🚧${info}🚧`,
- }
- );
- // toast.message(`Announcement`, {
- // position: "top-center",
- // important: true,
- // // duration: 10000,
- // description: `🚧${info}🚧`,
- // });
+ }`,
+ });
}
- setInfo(
- `${data.message} ${
- data?.startAt ? unixTimestampToRelativeTime(data.startAt) : ""
- }`
- );
} catch (err) {
console.log(err);
}
@@ -70,33 +56,17 @@ export default function App({
return (
<>
- <Head>
- <meta
- name="viewport"
- content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover"
- />
- </Head>
<SessionProvider session={session}>
<SearchProvider>
<WatchPageProvider>
<AnimatePresence mode="wait">
<SkeletonTheme baseColor="#232329" highlightColor="#2a2a32">
<Toaster richColors theme="dark" closeButton />
- <SecretPage
+ {/* <SecretPage
cheatCode={"aofienaef"}
onCheatCodeEntered={handleCheatCodeEntered}
- />
- {/* {info && (
- <div className="relative px-3 flex items-center justify-center font-karla w-full py-2 bg-secondary/80 text-white text-center">
- <span className="line-clamp-1 mr-5">🚧{info}🚧</span>
- <span
- onClick={() => setInfo()}
- className="absolute right-3 cursor-pointer"
- >
- <XMarkIcon className="w-6 h-6" />
- </span>
- </div>
- )} */}
+ /> */}
+ <ChangeLogs />
<m.div
key={`route-${router.route}`}
transition={{ duration: 0.5 }}
diff --git a/pages/_document.js b/pages/_document.tsx
index e89e516..e89e516 100644
--- a/pages/_document.js
+++ b/pages/_document.tsx
diff --git a/pages/_error.js b/pages/_error.tsx
index 19dfcff..35a9691 100644
--- a/pages/_error.js
+++ b/pages/_error.tsx
@@ -1,16 +1,16 @@
import MobileNav from "@/components/shared/MobileNav";
-import { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
import Footer from "@/components/shared/footer";
import Head from "next/head";
import Link from "next/link";
-function Error({ statusCode }) {
+function Error({ statusCode }: any) {
return (
<>
<Head>
<title>An Error Has Occurred</title>
</Head>
- <NewNavbar withNav shrink />
+ <Navbar withNav shrink />
<MobileNav hideProfile />
<div className="w-screen h-screen flex-center flex-col gap-5">
<div className="relative text-3xl">(╯°□°)╯︵ ┻━┻</div>
@@ -33,7 +33,7 @@ function Error({ statusCode }) {
);
}
-Error.getInitialProps = ({ res, err }) => {
+Error.getInitialProps = ({ res, err }: any) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode };
};
diff --git a/pages/_offline.js b/pages/_offline.tsx
index f440b39..f440b39 100644
--- a/pages/_offline.js
+++ b/pages/_offline.tsx
diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].ts
index da78d07..70b2e3d 100644
--- a/pages/api/auth/[...nextauth].js
+++ b/pages/api/auth/[...nextauth].ts
@@ -1,29 +1,6 @@
-import NextAuth from "next-auth";
-import { ApolloClient, InMemoryCache, gql } from "@apollo/client";
+import NextAuth, { NextAuthOptions } from "next-auth";
-const defaultOptions = {
- watchQuery: {
- fetchPolicy: "no-cache",
- errorPolicy: "ignore",
- },
- query: {
- fetchPolicy: "no-cache",
- errorPolicy: "all",
- },
-};
-
-const client = new ApolloClient({
- uri: "https://graphql.anilist.co",
- cache: new InMemoryCache(),
- defaultOptions: defaultOptions,
-});
-
-// import clientPromise from "../../../lib/mongodb";
-// import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
-
-export const authOptions = {
- // Configure one or more authentication providers
- // adapter: MongoDBAdapter(clientPromise),
+export const authOptions: NextAuthOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [
{
@@ -38,8 +15,17 @@ export const authOptions = {
userinfo: {
url: process.env.GRAPHQL_ENDPOINT,
async request(context) {
- const { data } = await client.query({
- query: gql`
+ // console.log(context.tokens.access_token);
+ const { data } = await fetch("https://graphql.anilist.co", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ // ...(context.tokens.access_token && {
+ Authorization: `Bearer ${context.tokens.access_token}`,
+ // }),
+ },
+ body: JSON.stringify({
+ query: `
query {
Viewer {
id
@@ -57,34 +43,33 @@ export const authOptions = {
}
}
`,
- context: {
- headers: {
- Authorization: "Bearer " + context.tokens.access_token,
- },
- },
- });
+ }),
+ }).then((res) => res.json());
- const userLists = data.Viewer.mediaListOptions.animeList.customLists;
+ const userLists = data.Viewer?.mediaListOptions.animeList.customLists;
let custLists = userLists || [];
if (!userLists?.includes("Watched using Moopa")) {
custLists.push("Watched using Moopa");
- const fetchGraphQL = async (query, variables) => {
+ const fetchGraphQL = async (
+ query: string,
+ variables: { lists: any }
+ ) => {
const response = await fetch("https://graphql.anilist.co/", {
method: "POST",
headers: {
"Content-Type": "application/json",
- Authorization: context.tokens.access_token
- ? `Bearer ${context.tokens.access_token}`
- : undefined,
+ ...(context.tokens.access_token && {
+ Authorization: `Bearer ${context.tokens.access_token}`,
+ }),
},
body: JSON.stringify({ query, variables }),
});
return response.json();
};
- const customLists = async (lists) => {
+ const customLists = async (lists: any) => {
const setList = `
mutation($lists: [String]){
UpdateUser(animeListOptions: { customLists: $lists }){
@@ -104,7 +89,7 @@ export const authOptions = {
name: data.Viewer.name,
sub: data.Viewer.id,
image: data.Viewer.avatar,
- list: data.Viewer.mediaListOptions.animeList.customLists,
+ list: data.Viewer?.mediaListOptions.animeList.customLists,
};
},
},
diff --git a/pages/api/og.jsx b/pages/api/og.tsx
index d52f90e..47619bc 100644
--- a/pages/api/og.jsx
+++ b/pages/api/og.tsx
@@ -11,7 +11,7 @@ const outfit = fetch(
new URL("../../assets/Outfit-Regular.ttf", import.meta.url)
).then((res) => res.arrayBuffer());
-export default async function handler(request) {
+export default async function handler(request: any) {
const Karla = await karla;
const Outfit = await outfit;
diff --git a/pages/api/v2/episode/[id].js b/pages/api/v2/episode/[id].tsx
index b601f62..b646126 100644
--- a/pages/api/v2/episode/[id].js
+++ b/pages/api/v2/episode/[id].tsx
@@ -1,17 +1,21 @@
+// @ts-nocheck
+
import axios from "axios";
import { rateLimiterRedis, rateSuperStrict, redis } from "@/lib/redis";
import appendMetaToEpisodes from "@/utils/appendMetaToEpisodes";
+import { NextApiRequest, NextApiResponse } from "next";
+import { AnifyEpisode, ConsumetInfo, EpisodeData } from "types";
+import { Episode } from "@/types/api/Episode";
+import { getProviderWithMostEpisodesAndImage } from "@/utils/parseMetaData";
-let CONSUMET_URI;
+let CONSUMET_URI: string | null;
CONSUMET_URI = process.env.API_URI || null;
if (CONSUMET_URI && CONSUMET_URI.endsWith("/")) {
CONSUMET_URI = CONSUMET_URI.slice(0, -1);
}
-const API_KEY = process.env.API_KEY;
-
-const isAscending = (data) => {
+const isAscending = (data: Episode[]) => {
for (let i = 1; i < data.length; i++) {
if (data[i].number < data[i - 1].number) {
return false;
@@ -20,7 +24,16 @@ const isAscending = (data) => {
return true;
};
-function filterData(data, type) {
+export interface RawEpisodeData {
+ map?: boolean;
+ providerId: string;
+ episodes: {
+ sub: Episode[];
+ dub: Episode[];
+ };
+}
+
+function filterData(data: RawEpisodeData[], type: "sub" | "dub") {
// Filter the data based on the type (sub or dub) and providerId
const filteredData = data.map((item) => {
if (item?.map === true) {
@@ -44,10 +57,10 @@ function filterData(data, type) {
return noEmpty;
}
-async function fetchConsumet(id) {
+async function fetchConsumet(id?: string | string[] | undefined) {
try {
- async function fetchData(dub) {
- const { data } = await axios.get(
+ const fetchData = async (dub?: any) => {
+ const { data } = await axios.get<ConsumetInfo>(
`${CONSUMET_URI}/meta/anilist/info/${id}${dub ? "?dub=true" : ""}`
);
if (data?.message === "Anime not found" && data?.length < 1) {
@@ -59,23 +72,32 @@ async function fetchConsumet(id) {
}
const reformatted = data.episodes?.map((item) => ({
- id: item?.id || null,
+ id: item.id,
title: item?.title || null,
img: item?.image || null,
number: item?.number || null,
- createdAt: item?.createdAt || null,
+ createdAt: item?.airDate || null,
description: item?.description || null,
- url: item?.url || null,
}));
return reformatted;
- }
+ };
const [subData, dubData] = await Promise.all([
fetchData(),
fetchData(true),
]);
+ if (subData.every((i) => i.id?.includes("dub"))) {
+ // replace dub in title with sub
+ subData.forEach((item) => {
+ if (item.id?.includes("dub")) {
+ item.id = item.id?.replace("dub", "anime");
+ }
+ });
+ console.log("replaced dub with sub");
+ }
+
const array = [
{
map: true,
@@ -88,38 +110,34 @@ async function fetchConsumet(id) {
];
return array;
- } catch (error) {
+ } catch (error: any) {
console.error("Error fetching and processing data:", error.message);
return [];
}
}
-async function fetchAnify(id) {
+async function fetchAnify(id?: string) {
try {
- const { data } = await axios.get(`https://api.anify.tv/episodes/${id}`);
+ const { data } = await axios.get<AnifyEpisode[]>(
+ `https://api.anify.tv/episodes/${id}`
+ );
if (!data) {
return [];
}
- const filtered = data.filter((item) => item.providerId !== "kass");
- // const modifiedData = filtered.map((provider) => {
- // if (provider.providerId === "gogoanime") {
- // const reversedEpisodes = [...provider.episodes].reverse();
- // return { ...provider, episodes: reversedEpisodes };
- // }
- // return provider;
- // });
+ const filtered = data.filter(
+ (item) => item.providerId !== "9anime" && item.providerId !== "kass"
+ );
- // return modifiedData;
return filtered;
- } catch (error) {
+ } catch (error: any) {
console.error("Error fetching and processing data:", error.message);
return [];
}
}
-async function fetchCoverImage(id, available = false) {
+async function fetchCoverImage(id: string, available = false) {
try {
if (!process.env.API_KEY) {
return [];
@@ -137,16 +155,20 @@ async function fetchCoverImage(id, available = false) {
return [];
}
- const getData = data[0].data;
+ const getData = getProviderWithMostEpisodesAndImage(data);
+ // const getData = data?.[0]?.data;
- return getData;
- } catch (error) {
+ return getData.data;
+ } catch (error: any) {
console.error("Error fetching and processing data:", error.message);
return [];
}
}
-export default async function handler(req, res) {
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
const { id, releasing = "false", dub = false, refresh = null } = req.query;
// if releasing is true then cache for 1 hour, if it false cache for 1 month;
@@ -159,11 +181,11 @@ export default async function handler(req, res) {
let cached;
let meta;
- let headers;
+ let headers: any = {};
if (redis) {
try {
- const ipAddress = req.socket.remoteAddress;
+ const ipAddress: any = req.socket.remoteAddress;
refresh
? await rateSuperStrict.consume(ipAddress)
: await rateLimiterRedis.consume(ipAddress);
@@ -171,9 +193,7 @@ export default async function handler(req, res) {
headers = refresh
? await rateSuperStrict.get(ipAddress)
: await rateLimiterRedis.get(ipAddress);
-
- console.log(headers);
- } catch (error) {
+ } catch (error: any) {
return res.status(429).json({
error: `Too Many Requests, retry after ${getTimeFromMs(
error.msBeforeNext
@@ -182,20 +202,28 @@ export default async function handler(req, res) {
});
}
+ meta = await redis.get(`meta:${id}`);
+ const parsedMeta = JSON.parse(meta);
+ if (parsedMeta?.length === 0) {
+ await redis.del(`meta:${id}`);
+ console.log("deleted meta cache");
+ meta = null;
+ }
+
if (refresh) {
await redis.del(`episode:${id}`);
- console.log("deleted cache");
} else {
cached = await redis.get(`episode:${id}`);
- console.log("using redis");
+ if (cached?.length === 0) {
+ await redis.del(`episode:${id}`);
+ cached = null;
+ }
}
-
- meta = await redis.get(`meta:${id}`);
}
if (cached && !refresh) {
if (dub) {
- const filteredData = filterData(JSON.parse(cached), "dub");
+ const filteredData: EpisodeData[] = filterData(JSON.parse(cached), "dub");
let filtered = filteredData.filter((item) =>
item?.episodes?.some((epi) => epi.hasDub !== false)
@@ -208,7 +236,9 @@ export default async function handler(req, res) {
res.setHeader("X-RateLimit-Remaining", headers.remainingPoints);
res.setHeader("X-RateLimit-BeforeReset", headers.msBeforeNext);
- return res.status(200).json(filtered);
+ return res
+ .status(200)
+ .json(filtered?.filter((i) => i?.providerId !== "9anime"));
} else {
const filteredData = filterData(JSON.parse(cached), "sub");
@@ -221,11 +251,13 @@ export default async function handler(req, res) {
res.setHeader("X-RateLimit-Remaining", headers.remainingPoints);
res.setHeader("X-RateLimit-BeforeReset", headers.msBeforeNext);
- return res.status(200).send(filtered);
+ return res
+ .status(200)
+ .send(filtered?.filter((i) => i?.providerId !== "9anime"));
}
} else {
const [consumet, anify, cover] = await Promise.all([
- fetchConsumet(id, dub),
+ fetchConsumet(id),
fetchAnify(id),
fetchCoverImage(id, meta),
]);
@@ -249,12 +281,16 @@ export default async function handler(req, res) {
if (meta) {
data = await appendMetaToEpisodes(filteredData, JSON.parse(meta));
- } else if (cover && !cover.some((e) => e.img === null)) {
+ } else if (
+ cover &&
+ // !cover?.some((item: { img: null }) => item.img === null) &&
+ cover?.length > 0
+ ) {
if (redis) await redis.set(`meta:${id}`, JSON.stringify(cover));
data = await appendMetaToEpisodes(filteredData, cover);
}
- if (redis && cacheTime !== null) {
+ if (redis && cacheTime !== null && rawData?.length > 0) {
await redis.set(
`episode:${id}`,
JSON.stringify(rawData),
@@ -282,7 +318,7 @@ export default async function handler(req, res) {
}
}
-function getTimeFromMs(time) {
+function getTimeFromMs(time: number) {
const timeInSeconds = time / 1000;
if (timeInSeconds >= 3600) {
diff --git a/pages/api/v2/etc/recent/[page].js b/pages/api/v2/etc/recent/[page].js
deleted file mode 100644
index 2ff22ea..0000000
--- a/pages/api/v2/etc/recent/[page].js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { rateLimitStrict, redis } from "@/lib/redis";
-
-let API_URL;
-API_URL = process.env.API_URI || null;
-if (API_URL && API_URL.endsWith("/")) {
- API_URL = API_URL.slice(0, -1);
-}
-
-export default async function handler(req, res) {
- 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}`,
- });
- }
- }
-
- let cache;
-
- if (redis) {
- cache = await redis.get(`recent-episode`);
- }
-
- if (cache) {
- return res.status(200).json({ results: JSON.parse(cache) });
- } else {
- const page = req.query.page || 1;
-
- var hasNextPage = true;
- var datas = [];
-
- async function fetchData(page) {
- const data = await fetch(
- `https://api.anify.tv/recent?type=anime&page=${page}&perPage=45`
- ).then((res) => res.json());
-
- // const filtered = data?.results?.filter((i) => i.type !== "ONA");
- // hasNextPage = data?.hasNextPage;
- datas = data;
- }
-
- await fetchData(page);
-
- if (redis) {
- await redis.set(`recent-episode`, JSON.stringify(datas), "EX", 60 * 60);
- }
-
- return res.status(200).json({ results: datas });
- }
- } catch (error) {
- res.status(500).json({ error });
- }
-}
diff --git a/pages/api/v2/etc/recent/[page].tsx b/pages/api/v2/etc/recent/[page].tsx
new file mode 100644
index 0000000..e49591c
--- /dev/null
+++ b/pages/api/v2/etc/recent/[page].tsx
@@ -0,0 +1,81 @@
+import { rateLimitStrict, redis } from "@/lib/redis";
+import { AnifyRecentEpisode } from "@/utils/types";
+import axios from "axios";
+import { NextApiRequest, NextApiResponse } from "next";
+
+let API_URL: string | null;
+API_URL = process.env.API_URI || null;
+if (API_URL && API_URL.endsWith("/")) {
+ API_URL = API_URL.slice(0, -1);
+}
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ try {
+ if (redis) {
+ try {
+ const ipAddress: any = req.socket.remoteAddress;
+ await rateLimitStrict?.consume(ipAddress);
+ } catch (error: any) {
+ return res.status(429).json({
+ error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
+ });
+ }
+ }
+
+ let cache;
+
+ if (redis) {
+ cache = await redis.get(`recent-episode`);
+ }
+
+ if (cache) {
+ return res.status(200).json({ results: JSON.parse(cache) });
+ } else {
+ const page = req.query.page || 1;
+
+ var hasNextPage = true;
+ let datas: AnifyRecentEpisode[] = [];
+
+ const fetchData = async (page: any) => {
+ const { data } = await axios.get(
+ `https://api.anify.tv/recent?type=anime&page=${page}&perPage=45&fields=[id,slug,title,currentEpisode,coverImage,episodes]`
+ );
+
+ // const filtered = data?.results?.filter((i) => i.type !== "ONA");
+ // hasNextPage = data?.hasNextPage;
+
+ const newData = data.map((i: AnifyRecentEpisode) => {
+ const getGogo = i.episodes?.data?.find(
+ (x) => x.providerId === "gogoanime"
+ );
+ const getGogoEpisode = getGogo?.episodes?.find(
+ (x) => x.number === i.currentEpisode
+ );
+
+ return {
+ id: i.id,
+ slug: getGogoEpisode?.id,
+ title: i.title,
+ currentEpisode: i.currentEpisode,
+ coverImage: i.coverImage,
+ };
+ });
+
+ datas = newData;
+ };
+
+ await fetchData(page);
+
+ if (redis) {
+ await redis.set(`recent-episode`, JSON.stringify(datas), "EX", 60 * 60);
+ }
+
+ return res.status(200).json({ results: datas });
+ }
+ } catch (error) {
+ res.status(500).json({ error });
+ }
+}
diff --git a/pages/api/v2/etc/schedule/index.js b/pages/api/v2/etc/schedule/index.tsx
index 2ddc82a..e6f0b26 100644
--- a/pages/api/v2/etc/schedule/index.js
+++ b/pages/api/v2/etc/schedule/index.tsx
@@ -1,6 +1,7 @@
import axios from "axios";
import cron from "cron";
import { rateLimiterRedis, redis } from "@/lib/redis";
+import { NextApiRequest, NextApiResponse } from "next";
// Function to fetch new data
async function fetchData() {
@@ -37,22 +38,42 @@ const job = new cron.CronJob("0 0 * * 1", () => {
});
job.start();
-export default async function handler(req, res) {
+interface Title {
+ romaji: string;
+ english: string;
+ native: string;
+}
+
+type CachedData = {
+ id: string;
+ title: Title;
+ coverImage: string;
+ bannerImage: string;
+ airingAt: number;
+ airingEpisode: number;
+};
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
try {
- let cached;
+ let cached: CachedData | null = null;
if (redis) {
try {
- const ipAddress = req.socket.remoteAddress;
- await rateLimiterRedis.consume(ipAddress);
- } catch (error) {
+ const ipAddress: any = req.socket.remoteAddress;
+ await rateLimiterRedis?.consume(ipAddress);
+ } catch (error: any) {
return res.status(429).json({
error: `Too Many Requests, retry after ${error.msBeforeNext / 1000}`,
});
}
- cached = await redis.get("schedule");
+ const cachedData = await redis.get("schedule");
+ cached = cachedData ? JSON.parse(cachedData) : null;
}
+
if (cached) {
- return res.status(200).json(JSON.parse(cached));
+ return res.status(200).json(cached);
} else {
const data = await fetchData();
diff --git a/pages/en/about.js b/pages/en/about.tsx
index aa0ba30..c5e9c51 100644
--- a/pages/en/about.js
+++ b/pages/en/about.tsx
@@ -1,7 +1,7 @@
import Head from "next/head";
import { motion } from "framer-motion";
import Link from "next/link";
-import { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
import Footer from "@/components/shared/footer";
export default function About() {
@@ -21,7 +21,7 @@ export default function About() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/svg/c.svg" />
</Head>
- <NewNavbar withNav={true} scrollP={5} shrink={true} />
+ <Navbar withNav={true} scrollP={5} shrink={true} />
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
diff --git a/pages/en/anime/[...id].js b/pages/en/anime/[...id].tsx
index 25cc4d6..42cae38 100644
--- a/pages/en/anime/[...id].js
+++ b/pages/en/anime/[...id].tsx
@@ -16,18 +16,30 @@ import Footer from "@/components/shared/footer";
import { mediaInfoQuery } from "@/lib/graphql/query";
import MobileNav from "@/components/shared/MobileNav";
+import pls from "@/utils/request/index";
+
import Characters from "@/components/anime/charactersCard";
import { redis } from "@/lib/redis";
-
-export default function Info({ info, color }) {
- const { data: session } = useSession();
+import { toast } from "sonner";
+import { Navbar } from "@/components/shared/NavBar";
+import { AniListInfoTypes } from "types/info/AnilistInfoTypes";
+
+type InfoTypes = {
+ info: AniListInfoTypes;
+ color: string;
+ api: string;
+ chapterNotFound: string;
+};
+
+export default function Info({ info, color, chapterNotFound }: InfoTypes) {
+ const { data: session }: any = useSession();
const { getUserLists } = useAniList(session);
const [loading, setLoading] = useState(false);
- const [progress, setProgress] = useState(0);
- const [statuses, setStatuses] = useState(null);
+ const [progress, setProgress] = useState<number>(0);
+ const [statuses, setStatuses] = useState<any>(null);
const [domainUrl, setDomainUrl] = useState("");
- const [watch, setWatch] = useState();
+ const [watch, setWatch] = useState<string>();
const [open, setOpen] = useState(false);
const { id } = useRouter().query;
@@ -37,6 +49,14 @@ export default function Info({ info, color }) {
);
useEffect(() => {
+ if (chapterNotFound) {
+ toast.error("Source not found");
+ const cleanUrl = window.location.origin + window.location.pathname;
+ window.history.replaceState(null, "", cleanUrl);
+ }
+ }, [chapterNotFound]);
+
+ useEffect(() => {
handleClose();
async function fetchData() {
setLoading(true);
@@ -53,7 +73,9 @@ export default function Info({ info, color }) {
if (user) {
setProgress(user.progress);
- const statusMapping = {
+ const statusMapping: {
+ [key: string]: { name: string; value: string };
+ } = {
CURRENT: { name: "Watching", value: "CURRENT" },
PLANNING: { name: "Plan to watch", value: "PLANNING" },
COMPLETED: { name: "Completed", value: "COMPLETED" },
@@ -118,6 +140,7 @@ export default function Info({ info, color }) {
}&image=${info.bannerImage || info.coverImage.extraLarge}`}
/>
</Head>
+ <Navbar info={info} />
<Modal open={open} onClose={() => handleClose()}>
<div>
{!session && (
@@ -151,7 +174,7 @@ export default function Info({ info, color }) {
)}
</div>
</Modal>
- <MobileNav sessions={session} hideProfile={true} />
+ <MobileNav hideProfile={true} />
<main className="w-screen min-h-screen relative flex flex-col items-center bg-primary gap-5">
<div className="w-screen absolute">
<div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" />
@@ -169,12 +192,10 @@ export default function Info({ info, color }) {
<div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5">
<DetailTop
info={info}
- session={session}
handleOpen={handleOpen}
- loading={loading}
statuses={statuses}
watchUrl={watch}
- progress={progress}
+ progress={progress || 0}
color={color}
/>
@@ -188,6 +209,9 @@ export default function Info({ info, color }) {
{info?.characters?.edges && (
<div className="w-full">
+ {/* <div className="w-full h-[150px] bg-white flex-center text-black">
+ ad banner
+ </div> */}
<Characters info={info?.characters?.edges} />
</div>
)}
@@ -208,8 +232,8 @@ export default function Info({ info, color }) {
);
}
-export async function getServerSideProps(ctx) {
- const { id } = ctx.query;
+export async function getServerSideProps(ctx: any) {
+ const { id, notfound } = ctx.query;
let API_URI;
API_URI = process.env.API_URI || null || null;
@@ -217,7 +241,12 @@ export async function getServerSideProps(ctx) {
API_URI = API_URI.slice(0, -1);
}
- let cache;
+ let cache, chapterNotFound;
+
+ if (notfound) {
+ // create random id string
+ chapterNotFound = Math.random().toString(36).substring(7);
+ }
if (redis) {
cache = await redis.get(`anime:${id}`);
@@ -230,14 +259,15 @@ export async function getServerSideProps(ctx) {
info,
color,
api: API_URI,
+ chapterNotFound: chapterNotFound || null,
},
};
} else {
- const resp = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
+ const [resp] = await pls.post("https://graphql.anilist.co/", {
+ // method: "POST",
+ // headers: {
+ // "Content-Type": "application/json",
+ // },
body: JSON.stringify({
query: mediaInfoQuery,
variables: {
@@ -246,10 +276,10 @@ export async function getServerSideProps(ctx) {
}),
});
- const json = await resp.json();
- const data = json?.data?.Media;
+ // const json = await resp.json();
+ const data = resp?.data?.Media;
- const cacheTime = data.nextAiringEpisode?.episode
+ const cacheTime = data?.nextAiringEpisode?.episode
? 60 * 10
: 60 * 60 * 24 * 30;
@@ -283,12 +313,13 @@ export async function getServerSideProps(ctx) {
info: data,
color: color,
api: API_URI,
+ chapterNotFound: chapterNotFound || null,
},
};
}
}
-function getBrightness(hexColor) {
+function getBrightness(hexColor: { match: (arg0: RegExp) => any[] }) {
if (!hexColor) {
return 200;
}
@@ -299,7 +330,7 @@ function getBrightness(hexColor) {
return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000;
}
-function setTxtColor(hexColor) {
+function setTxtColor(hexColor: { match: (arg0: RegExp) => any[] }) {
const brightness = getBrightness(hexColor);
return brightness < 150 ? "#fff" : "#000";
}
diff --git a/pages/en/anime/recent.js b/pages/en/anime/recent.js
index 4a8111d..240ed1d 100644
--- a/pages/en/anime/recent.js
+++ b/pages/en/anime/recent.js
@@ -83,7 +83,7 @@ export default function Recent({ sessions }) {
<div className="z-50 bg-primary pt-5 pb-3 shadow-md shadow-primary w-full fixed px-3">
<Link href="/en" className="flex gap-2 items-center font-karla">
<ChevronLeftIcon className="w-5 h-5" />
- <h1 className="text-xl">New Episodes</h1>
+ <h1 className="text-xl">Freshly Added</h1>
</Link>
</div>
<div className="grid grid-cols-2 xs:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-6 gap-5 max-w-6xl pt-20">
diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js
index beab366..dc1f412 100644
--- a/pages/en/anime/watch/[...info].js
+++ b/pages/en/anime/watch/[...info].js
@@ -1,5 +1,4 @@
-import React, { useEffect, useRef, useState } from "react";
-import PlayerComponent from "@/components/watch/player/playerComponent";
+import { useEffect, useState } from "react";
import { FlagIcon, ShareIcon } from "@heroicons/react/24/solid";
import Details from "@/components/watch/primary/details";
import EpisodeLists from "@/components/watch/secondary/episodeLists";
@@ -9,13 +8,16 @@ 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 { Navbar } 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 VidStack from "@/components/watch/new-player/player";
+import { useRouter } from "next/router";
+import { Spinner } from "@vidstack/react";
export async function getServerSideProps(context) {
let userData = null;
@@ -81,7 +83,7 @@ export async function getServerSideProps(context) {
color
}
synonyms
-
+
}
}
`,
@@ -91,6 +93,8 @@ export async function getServerSideProps(context) {
}),
});
const data = await ress.json();
+ // const variables = { id: aniId };
+ // const data = await getAnilistMediaInfo(variables, context.req);
try {
if (session) {
@@ -142,17 +146,24 @@ export default function Watch({
const [episodesList, setepisodesList] = useState();
const [mapEpisode, setMapEpisode] = useState(null);
- const [episodeSource, setEpisodeSource] = useState(null);
-
const [open, setOpen] = useState(false);
const [isOpen, setIsOpen] = useState(false);
+ const { setAutoNext } = useWatchProvider();
+
const [onList, setOnList] = useState(false);
- const { theaterMode, setPlayerState, setAutoPlay, setMarked } =
- useWatchProvider();
+ const router = useRouter();
- const playerRef = useRef(null);
+ const {
+ theaterMode,
+ setPlayerState,
+ setAutoPlay,
+ setMarked,
+ setTrack,
+ aspectRatio,
+ setDataMedia,
+ } = useWatchProvider();
useEffect(() => {
async function getInfo() {
@@ -160,6 +171,8 @@ export default function Watch({
setOnList(true);
}
+ setDataMedia(info);
+
const response = await fetch(
`/api/v2/episode/${info.id}?releasing=${
info.status === "RELEASING" ? "true" : "false"
@@ -202,17 +215,18 @@ export default function Watch({
const previousEpisode = episodeList?.find(
(i) => i.number === parseInt(epiNumber) - 1
);
- setEpisodeNavigation({
+ const vidNav = {
prev: previousEpisode,
playing: {
id: currentEpisode.id,
- title: playingData?.title,
+ title: playingData?.title || info?.title?.romaji,
description: playingData?.description,
img: playingData?.img || playingData?.image,
number: currentEpisode.number,
},
next: nextEpisode,
- });
+ };
+ setEpisodeNavigation(vidNav);
}
}
@@ -228,12 +242,17 @@ export default function Watch({
}, [sessions?.user?.name, epiNumber, dub]);
useEffect(() => {
+ const autoNext = localStorage.getItem("autoNext"),
+ autoPlay = localStorage.getItem("autoplay");
+ if (autoNext) {
+ setAutoNext(autoNext);
+ }
+ if (autoPlay) {
+ setAutoPlay(autoPlay);
+ }
+
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: {
@@ -252,6 +271,11 @@ export default function Watch({
}),
}).then((res) => res.json());
+ if (!anify?.sources?.length > 0) {
+ router.push(`/en/anime/${info.id}?notfound=true`);
+ return;
+ }
+
const skip = await fetch(
`https://api.aniskip.com/v2/skip-times/${info.idMal}/${parseInt(
epiNumber
@@ -267,31 +291,77 @@ export default function Watch({
return res.json();
});
- const op =
- skip?.results?.find((item) => item.skipType === "op") || null;
- const ed =
- skip?.results?.find((item) => item.skipType === "ed") || null;
+ let getOp =
+ skip?.results?.find((item) => item.skipType === "op") || null,
+ getEd = skip?.results?.find((item) => item.skipType === "ed") || null;
+
+ const op = getOp
+ ? {
+ startTime:
+ anify?.intro?.start ?? Math.round(getOp?.interval.startTime),
+ endTime:
+ anify?.intro?.end ?? Math.round(getOp?.interval.endTime),
+ text: "Opening",
+ }
+ : null,
+ ed = {
+ startTime:
+ anify?.outro?.start ?? Math.round(getEd?.interval.startTime),
+ endTime: anify?.outro?.end ?? Math.round(getEd?.interval.endTime),
+ text: "Ending",
+ };
+ const skipData = [op, ed].filter((i) => i !== null);
+
+ const quality =
+ anify?.sources?.find(
+ (i) => i.quality === "default" || i.quality === "auto"
+ ) || anify?.sources[0];
+
+ const reFormSubtitles = anify?.subtitles?.map((i) => {
+ return {
+ src: proxy + "/" + i.url,
+ label: i.lang,
+ kind: i.lang === "Thumbnails" ? "thumbnails" : "subtitles",
+ ...(i.lang === "English" && { default: true }),
+ };
+ });
+
+ const thumbnails = reFormSubtitles?.find(
+ (i) => i.kind === "thumbnails"
+ );
+
+ const subtitles = reFormSubtitles?.filter(
+ (i) => i.kind !== "thumbnails"
+ );
const episode = {
- epiData: anify,
- skip: {
- op,
- ed,
+ provider,
+ isDub: dub,
+ defaultQuality: {
+ // url: quality?.url,
+ url: `${proxy}/proxy/m3u8/${encodeURIComponent(
+ String(quality?.url)
+ )}/${encodeURIComponent(JSON.stringify(anify?.headers))}`,
+ headers: anify?.headers,
},
+ subtitles: subtitles,
+ thumbnails: thumbnails?.src,
+ epiData: anify,
+ skip: skipData,
};
- setEpisodeSource(episode);
+ setTrack(episode);
}
}
fetchData();
return () => {
- setEpisodeSource();
setPlayerState({
currentTime: 0,
isPlaying: false,
});
setMarked(0);
+ setTrack(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -424,7 +494,7 @@ export default function Watch({
</Modal>
<BugReportForm isOpen={isOpen} setIsOpen={setIsOpen} />
<main className="w-screen h-full">
- <NewNavbar
+ <Navbar
scrollP={20}
withNav={true}
shrink={true}
@@ -435,21 +505,23 @@ export default function Watch({
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
+ className={`bg-black w-full max-h-[84dvh] h-full flex-center rounded-md`}
+ style={{ aspectRatio: aspectRatio }}
+ >
+ {episodeNavigation ? (
+ <VidStack
+ id={`${watchId}-theater`}
+ navigation={episodeNavigation}
+ sessions={sessions}
+ userData={userData}
+ />
+ ) : (
+ <div className="flex-center aspect-video w-full h-full relative">
+ <SpinLoader />
+ </div>
+ )}
+ </div>
)}
<div
id="default"
@@ -459,20 +531,25 @@ export default function Watch({
>
<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
+ className={`bg-black w-full flex-center rounded-md overflow-hidden ${
+ aspectRatio === "4/3" ? "aspect-video" : ""
+ }`}
+ // style={{ aspectRatio: aspectRatio }}
+ >
+ {episodeNavigation ? (
+ <VidStack
+ id={`${watchId}-default`}
+ navigation={episodeNavigation}
+ sessions={sessions}
+ userData={userData}
+ />
+ ) : (
+ <div className="flex-center aspect-video w-full h-full relative">
+ <SpinLoader />
+ </div>
+ )}
+ </div>
)}
<div
id="details"
@@ -506,7 +583,7 @@ export default function Watch({
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
+ <span className="hidden lg:block">share</span>
</button>
<button
type="button"
@@ -514,11 +591,10 @@ export default function Watch({
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
+ <span className="hidden lg:block">report</span>
</button>
</div>
</div>
- {/* <div>right</div> */}
</div>
<Details
@@ -538,6 +614,11 @@ export default function Watch({
id="secondary"
className={`relative ${theaterMode ? "pt-5" : "pt-4 lg:pt-0"}`}
>
+ {/* <div className="w-full h-[150px] text-black p-3">
+ <span className="bg-white w-full h-full flex-center">
+ ad banner
+ </span>
+ </div> */}
<EpisodeLists
info={info}
session={sessions}
@@ -556,3 +637,17 @@ export default function Watch({
</>
);
}
+
+function SpinLoader() {
+ return (
+ <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center">
+ <Spinner.Root
+ className="text-white animate-spin opacity-100"
+ size={84}
+ >
+ <Spinner.Track className="opacity-25" width={8} />
+ <Spinner.TrackFill className="opacity-75" width={8} />
+ </Spinner.Root>
+ </div>
+ );
+}
diff --git a/pages/en/contact.js b/pages/en/contact.tsx
index 385bdb1..9954f95 100644
--- a/pages/en/contact.js
+++ b/pages/en/contact.tsx
@@ -1,10 +1,10 @@
-import { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
import Footer from "@/components/shared/footer";
const Contact = () => {
return (
<>
- <NewNavbar withNav={true} scrollP={5} shrink={true} />
+ <Navbar 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>
diff --git a/pages/en/dmca.js b/pages/en/dmca.tsx
index e559829..eba28fe 100644
--- a/pages/en/dmca.js
+++ b/pages/en/dmca.tsx
@@ -1,5 +1,5 @@
import MobileNav from "@/components/shared/MobileNav";
-import { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
import Footer from "@/components/shared/footer";
import Head from "next/head";
@@ -21,7 +21,7 @@ export default function DMCA() {
<link rel="icon" href="/svg/c.svg" />
</Head>
<>
- <NewNavbar withNav={true} scrollP={5} shrink={true} />
+ <Navbar withNav={true} scrollP={5} shrink={true} />
<MobileNav hideProfile={true} />
<div className="min-h-screen z-20 flex w-screen justify-center items-center">
diff --git a/pages/en/index.js b/pages/en/index.tsx
index 29b0778..4141015 100644
--- a/pages/en/index.js
+++ b/pages/en/index.tsx
@@ -14,11 +14,11 @@ import Schedule from "@/components/home/schedule";
import getUpcomingAnime from "@/lib/anilist/getUpcomingAnime";
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 { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
+import UserRecommendation from "@/components/home/recommendation";
export async function getServerSideProps() {
let cachedData;
@@ -75,12 +75,55 @@ export async function getServerSideProps() {
}
}
-export default function Home({ detail, populars, upComing }) {
- const { data: sessions } = useSession();
- const { anime: currentAnime, manga: currentManga } = GetMedia(sessions, {
+type HomeProps = {
+ genre: any;
+ detail: any;
+ populars: any;
+ upComing: any;
+};
+
+export interface SessionTypes {
+ name: string;
+ picture: Picture;
+ sub: string;
+ token: string;
+ id: number;
+ image: Image;
+ list: string[];
+ version: string;
+ iat: number;
+ exp: number;
+ jti: string;
+}
+
+interface Picture {
+ large: string;
+ medium: string;
+}
+
+interface Image {
+ large: string;
+ medium: string;
+}
+
+export default function Home({ detail, populars, upComing }: HomeProps) {
+ const { data: sessions }: any = useSession();
+ const userSession: SessionTypes = sessions?.user;
+
+ const {
+ anime: currentAnime,
+ manga: currentManga,
+ recommendations,
+ }: {
+ anime: CurrentMediaTypes[];
+ manga: CurrentMediaTypes[];
+ recommendations: CurrentMediaTypes[];
+ } = GetMedia(sessions, {
stats: "CURRENT",
});
- const { anime: plan } = GetMedia(sessions, { stats: "PLANNING" });
+ const { anime: plan }: { anime: CurrentMediaTypes[] } = GetMedia(sessions, {
+ stats: "PLANNING",
+ });
const { anime: release } = GetMedia(sessions);
const [schedules, setSchedules] = useState(null);
@@ -97,12 +140,12 @@ export default function Home({ detail, populars, upComing }) {
}
useEffect(() => {
- if (sessions?.user?.version) {
- if (sessions.user.version !== "1.0.1") {
- signOut("AniListProvider");
+ if (userSession?.version) {
+ if (userSession?.version !== "1.0.1") {
+ signOut({ redirect: true });
}
}
- }, [sessions?.user?.version]);
+ }, [userSession?.version]);
useEffect(() => {
getRecent();
@@ -118,33 +161,15 @@ export default function Home({ detail, populars, upComing }) {
}
}, [upComing]);
- // useEffect(() => {
- // const getSchedule = async () => {
- // try {
- // const res = await fetch(`/api/v2/etc/schedule`);
- // const data = await res.json();
-
- // if (!res.ok) {
- // setSchedules(null);
- // } else {
- // setSchedules(data);
- // }
- // } catch (err) {
- // console.log(err);
- // }
- // };
- // getSchedule();
- // }, []);
-
- const [releaseData, setReleaseData] = useState([]);
+ const [releaseData, setReleaseData] = useState<any[]>([]);
useEffect(() => {
function getRelease() {
- let releasingAnime = [];
- let progress = [];
- let seenIds = new Set(); // Create a Set to store the IDs of seen anime
- release.map((list) => {
- list.entries.map((entry) => {
+ let releasingAnime: any[] = [];
+ let progress: any[] = [];
+ let seenIds = new Set<number>(); // Create a Set to store the IDs of seen anime
+ (release as any[]).forEach((list: any) => {
+ list.entries.forEach((entry: any) => {
if (
entry.media.status === "RELEASING" &&
!seenIds.has(entry.media.id)
@@ -156,18 +181,18 @@ export default function Home({ detail, populars, upComing }) {
});
});
setReleaseData(releasingAnime);
- setProg(progress);
+ if (progress.length > 0) setProg(progress);
}
getRelease();
}, [release]);
- const [listAnime, setListAnime] = useState(null);
- const [listManga, setListManga] = useState(null);
- const [planned, setPlanned] = useState(null);
- const [user, setUser] = useState(null);
+ const [listAnime, setListAnime] = useState<any[] | null>();
+ const [listManga, setListManga] = useState<any[] | null>(null);
+ const [planned, setPlanned] = useState<any[] | null>(null);
+ const [user, setUser] = useState<any[] | null>(null);
const [removed, setRemoved] = useState();
- const [prog, setProg] = useState(null);
+ const [prog, setProg] = useState<any[] | null>();
const popular = populars?.data;
const data = detail.data[0];
@@ -175,7 +200,7 @@ export default function Home({ detail, populars, upComing }) {
useEffect(() => {
async function userData() {
try {
- if (sessions?.user?.name) {
+ if (userSession?.name) {
await fetch(`/api/user/profile`, {
method: "POST",
headers: {
@@ -189,9 +214,9 @@ export default function Home({ detail, populars, upComing }) {
} catch (error) {
console.log(error);
}
- let data;
+ let data: UserDataType | null = null;
try {
- if (sessions?.user?.name) {
+ if (userSession?.name) {
const res = await fetch(
`/api/user/profile?name=${sessions.user.name}`
);
@@ -220,17 +245,20 @@ export default function Home({ detail, populars, upComing }) {
// Handle the error here
}
if (!data) {
- const dat = JSON.parse(localStorage.getItem("artplayer_settings"));
+ const dat: any = localStorage.getItem("artplayer_settings");
if (dat) {
- const arr = Object.keys(dat).map((key) => dat[key]);
- const newFirst = arr?.sort((a, b) => {
- return new Date(b?.createdAt) - new Date(a?.createdAt);
+ const arr = Object.keys(dat).map((key: string) => dat[key] as any);
+ const newFirst = arr?.sort((a: any, b: any) => {
+ return (
+ new Date(b?.createdAt).getTime() -
+ new Date(a?.createdAt).getTime()
+ );
});
const uniqueTitles = new Set();
// Filter out duplicates and store unique entries
- const filteredData = newFirst.filter((entry) => {
+ const filteredData = newFirst.filter((entry: any) => {
if (uniqueTitles.has(entry.aniTitle)) {
return false;
}
@@ -238,7 +266,9 @@ export default function Home({ detail, populars, upComing }) {
return true;
});
- setUser(filteredData);
+ if (filteredData) {
+ setUser(filteredData);
+ }
}
} else {
// Create a Set to store unique aniTitles
@@ -257,11 +287,11 @@ export default function Home({ detail, populars, upComing }) {
// const data = await res.json();
}
userData();
- }, [sessions?.user?.name, removed]);
+ }, [userSession?.name, removed]);
useEffect(() => {
async function userData() {
- if (!sessions?.user?.name) return;
+ if (!userSession?.name) return;
const getMedia =
currentAnime.find((item) => item.status === "CURRENT") || null;
@@ -292,9 +322,7 @@ export default function Home({ detail, populars, upComing }) {
userData();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [sessions?.user?.name, currentAnime, plan]);
-
- // console.log({ recentAdded });
+ }, [userSession?.name, currentAnime, plan]);
return (
<Fragment>
@@ -304,7 +332,6 @@ export default function Home({ detail, populars, upComing }) {
<link rel="icon" href="/svg/c.svg" />
<link rel="canonical" href="https://moopa.live/en/" />
<meta name="twitter:card" content="summary_large_image" />
- {/* Write the best SEO for this homepage */}
<meta
name="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!"
@@ -339,9 +366,9 @@ export default function Home({ detail, populars, upComing }) {
/>
<meta name="twitter:image" content="/preview.png" />
</Head>
- <MobileNav sessions={sessions} hideProfile={true} />
+ <MobileNav hideProfile={true} />
- <NewNavbar paddingY="pt-2 lg:pt-10" withNav={true} home={true} />
+ <Navbar paddingY="pt-2 lg:pt-10" withNav={true} home={true} />
<div className="h-auto w-screen bg-[#141519] text-[#dbdcdd]">
{/* PC / TABLET */}
<div className=" hidden justify-center lg:flex my-16">
@@ -381,6 +408,16 @@ export default function Home({ detail, populars, upComing }) {
</div>
</div>
</div>
+ {/* <div className="relative w-screen h-screen overflow-hidden">
+ <iframe
+ width="560"
+ height="315"
+ src="https://www.youtube.com/embed/VVfdqw-qvNE?autoplay=1&controls=0&rel=0&mute=1"
+ frameborder="0"
+ allowfullscreen
+ className="absolute w-screen h-screen top-0 scale-[115%] left-0 z-0"
+ />
+ </div> */}
{sessions && (
<div className="flex items-center justify-center lg:bg-none mt-4 lg:mt-0 w-screen">
@@ -411,7 +448,7 @@ export default function Home({ detail, populars, upComing }) {
animate={{ opacity: 1 }}
transition={{ duration: 0.5, staggerChildren: 0.2 }} // Add staggerChildren prop
>
- {user?.length > 0 && user?.some((i) => i?.watchId) && (
+ {user && user?.length > 0 && user?.some((i) => i?.watchId) && (
<motion.section // Add motion.div to each child component
key="recentlyWatched"
initial={{ y: 20, opacity: 0 }}
@@ -423,7 +460,7 @@ export default function Home({ detail, populars, upComing }) {
ids="recentlyWatched"
section="Recently Watched"
userData={user}
- userName={sessions?.user?.name}
+ userName={userSession?.name}
setRemoved={setRemoved}
/>
</motion.section>
@@ -442,12 +479,12 @@ export default function Home({ detail, populars, upComing }) {
section="On-Going Anime"
data={releaseData}
og={prog}
- userName={sessions?.user?.name}
+ userName={userSession?.name}
/>
</motion.section>
)}
- {sessions && listAnime?.length > 0 && (
+ {sessions && listAnime && listAnime?.length > 0 && (
<motion.section // Add motion.div to each child component
key="listAnime"
initial={{ y: 20, opacity: 0 }}
@@ -460,12 +497,12 @@ export default function Home({ detail, populars, upComing }) {
section="Your Watch List"
data={listAnime}
og={prog}
- userName={sessions?.user?.name}
+ userName={userSession?.name}
/>
</motion.section>
)}
- {sessions && listManga?.length > 0 && (
+ {sessions && listManga && listManga?.length > 0 && (
<motion.section // Add motion.div to each child component
key="listManga"
initial={{ y: 20, opacity: 0 }}
@@ -478,13 +515,13 @@ export default function Home({ detail, populars, upComing }) {
section="Your Manga List"
data={listManga}
// og={prog}
- userName={sessions?.user?.name}
+ userName={userSession?.name}
/>
</motion.section>
)}
- {/* {recommendations.length > 0 && (
- <div className="space-y-5 mb-10">
+ {recommendations.length > 0 && (
+ <div className="space-y-4 lg:space-y-5 mb-5 lg:mb-10">
<div className="px-5">
<p className="text-sm lg:text-base">
Based on Your List
@@ -496,10 +533,10 @@ export default function Home({ detail, populars, upComing }) {
</div>
<UserRecommendation data={recommendations} />
</div>
- )} */}
+ )}
{/* SECTION 2 */}
- {sessions && planned?.length > 0 && (
+ {sessions && planned && planned?.length > 0 && (
<motion.section // Add motion.div to each child component
key="plannedAnime"
initial={{ y: 20, opacity: 0 }}
@@ -511,7 +548,7 @@ export default function Home({ detail, populars, upComing }) {
ids="plannedAnime"
section="Your Plan"
data={planned}
- userName={sessions?.user?.name}
+ userName={userSession?.name}
/>
</motion.section>
)}
@@ -534,7 +571,7 @@ export default function Home({ detail, populars, upComing }) {
>
<Content
ids="recentAdded"
- section="New Episodes"
+ section="Freshly Added"
data={recentAdded}
/>
</motion.section>
@@ -556,6 +593,9 @@ export default function Home({ detail, populars, upComing }) {
/>
</motion.section>
)}
+ {/* <div className="w-full h-[150px] bg-white flex-center my-5 text-black">
+ ad banner
+ </div> */}
{/* Schedule */}
{anime.length > 0 && (
@@ -608,3 +648,65 @@ export default function Home({ detail, populars, upComing }) {
</Fragment>
);
}
+
+export interface CurrentMediaTypes {
+ status?: string;
+ name: string;
+ entries: Entry[];
+}
+
+export interface Entry {
+ id: number;
+ mediaId: number;
+ status: string;
+ progress: number;
+ score: number;
+ media: Media;
+}
+
+export interface Media {
+ id: number;
+ status: string;
+ nextAiringEpisode: any;
+ title: Title;
+ episodes: number;
+ coverImage: CoverImage;
+}
+
+export interface Title {
+ english: string;
+ romaji: string;
+}
+
+export interface CoverImage {
+ large: string;
+}
+
+export interface UserDataType {
+ id: string;
+ name: string;
+ setting: Setting;
+ WatchListEpisode: WatchListEpisode[];
+}
+
+export interface Setting {
+ CustomLists: boolean;
+}
+
+export interface WatchListEpisode {
+ id: string;
+ aniId?: string;
+ title?: string;
+ aniTitle?: string;
+ image?: string;
+ episode?: number;
+ timeWatched?: number;
+ duration?: number;
+ provider?: string;
+ nextId?: string;
+ nextNumber?: number;
+ dub?: boolean;
+ createdDate: string;
+ userProfileId: string;
+ watchId: string;
+}
diff --git a/pages/en/manga/[...id].js b/pages/en/manga/[...id].js
deleted file mode 100644
index 5648b2c..0000000
--- a/pages/en/manga/[...id].js
+++ /dev/null
@@ -1,427 +0,0 @@
-import ChapterSelector from "@/components/manga/chapters";
-import Footer from "@/components/shared/footer";
-import Head from "next/head";
-import { useEffect, useState } from "react";
-import { getServerSession } from "next-auth";
-import { authOptions } from "../../api/auth/[...nextauth]";
-import { mediaInfoQuery } from "@/lib/graphql/query";
-import Modal from "@/components/modal";
-import { signIn, useSession } from "next-auth/react";
-import AniList from "@/components/media/aniList";
-import ListEditor from "@/components/listEditor";
-import MobileNav from "@/components/shared/MobileNav";
-import Image from "next/image";
-import DetailTop from "@/components/anime/mobile/topSection";
-import Characters from "@/components/anime/charactersCard";
-import Content from "@/components/home/content";
-import { toast } from "sonner";
-import axios from "axios";
-import getAnifyInfo from "@/lib/anify/info";
-import { redis } from "@/lib/redis";
-import getMangaId from "@/lib/anify/getMangaId";
-
-export default function Manga({ info, anifyData, color, chapterNotFound }) {
- const [domainUrl, setDomainUrl] = useState("");
- const { data: session } = useSession();
-
- const [loading, setLoading] = useState(false);
- const [progress, setProgress] = useState(0);
- const [statuses, setStatuses] = useState(null);
- const [watch, setWatch] = useState();
-
- const [chapter, setChapter] = useState(null);
-
- const [open, setOpen] = useState(false);
-
- const rec = info?.recommendations?.nodes?.map(
- (data) => data.mediaRecommendation
- );
-
- useEffect(() => {
- setDomainUrl(window.location.origin);
- }, []);
-
- useEffect(() => {
- if (chapterNotFound) {
- toast.error("Chapter not found");
- const cleanUrl = window.location.origin + window.location.pathname;
- window.history.replaceState(null, null, cleanUrl);
- }
- }, [chapterNotFound]);
-
- useEffect(() => {
- async function fetchData() {
- try {
- setLoading(true);
-
- const { data } = await axios.get(`/api/v2/info?id=${anifyData.id}`);
-
- if (!data.chapters) {
- setLoading(false);
- return;
- }
-
- setChapter(data);
- setLoading(false);
- } catch (error) {
- console.error(error);
- }
- }
- fetchData();
-
- return () => {
- setChapter(null);
- };
- }, [info?.id]);
-
- function handleOpen() {
- setOpen(true);
- document.body.style.overflow = "hidden";
- }
-
- function handleClose() {
- setOpen(false);
- document.body.style.overflow = "auto";
- }
-
- return (
- <>
- <Head>
- <title>
- {info
- ? `Manga - ${
- info.title.romaji || info.title.english || info.title.native
- }`
- : "Getting Info..."}
- </title>
- <meta name="twitter:card" content="summary_large_image" />
- <meta
- name="twitter:title"
- content={`Moopa - ${info.title.romaji || info.title.english}`}
- />
- <meta
- name="twitter:description"
- content={`${info.description?.slice(0, 180)}...`}
- />
- <meta
- name="twitter:image"
- content={`${domainUrl}/api/og?title=${
- info.title.romaji || info.title.english
- }&image=${info.bannerImage || info.coverImage}`}
- />
- <meta
- name="title"
- data-title-romaji={info?.title?.romaji}
- data-title-english={info?.title?.english}
- data-title-native={info?.title?.native}
- />
- </Head>
- <Modal open={open} onClose={() => handleClose()}>
- <div>
- {!session && (
- <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md">
- <div className="text-md font-extrabold font-karla">
- Edit your list
- </div>
- <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>
- )}
- {session && info && (
- <ListEditor
- animeId={info?.id}
- session={session}
- stats={statuses?.value}
- prg={progress}
- max={info?.episodes}
- info={info}
- close={handleClose}
- />
- )}
- </div>
- </Modal>
- <MobileNav sessions={session} hideProfile={true} />
- <main className="w-screen min-h-screen overflow-hidden relative flex flex-col items-center gap-5">
- {/* <div className="absolute bg-gradient-to-t from-primary from-85% to-100% to-transparent w-screen h-full z-10" /> */}
- <div className="w-screen absolute">
- <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" />
- {info?.bannerImage && (
- <Image
- src={info?.bannerImage}
- alt="banner anime"
- height={1000}
- width={1000}
- blurDataURL={info?.bannerImage}
- className="object-cover bg-image blur-[2px] w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0"
- />
- )}
- </div>
- <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5 pb-10">
- <DetailTop
- info={info}
- session={session}
- handleOpen={handleOpen}
- loading={loading}
- statuses={statuses}
- watchUrl={watch}
- progress={progress}
- color={color}
- />
-
- {!loading ? (
- chapter?.chapters?.length > 0 ? (
- <ChapterSelector
- chaptersData={chapter.chapters}
- mangaId={chapter.id}
- data={info}
- setWatch={setWatch}
- />
- ) : (
- <div className="h-[20vh] lg:w-full flex-center flex-col gap-5">
- <p className="text-center font-karla font-bold lg:text-lg">
- Oops!<br></br> It looks like this manga is not available.
- </p>
- </div>
- )
- ) : (
- <div className="flex justify-center">
- <div className="lds-ellipsis">
- <div></div>
- <div></div>
- <div></div>
- <div></div>
- </div>
- </div>
- )}
-
- {info?.characters?.edges?.length > 0 && (
- <div className="w-full">
- <Characters info={info?.characters?.edges} />
- </div>
- )}
-
- {info && rec && rec?.length !== 0 && (
- <div className="w-full">
- <Content
- ids="recommendAnime"
- section="Recommendations"
- type="manga"
- data={rec}
- />
- </div>
- )}
- </div>
- </main>
- <Footer />
- </>
- );
-}
-
-export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
- const accessToken = session?.user?.token || null;
-
- const { chapter } = context.query;
- const [id1, id2] = context.query.id;
-
- let cached;
- let aniId, mangadexId;
- let info, data, color, chapterNotFound;
-
- if (String(id1).length > 6) {
- aniId = id2;
- mangadexId = id1;
- } else {
- aniId = id1;
- mangadexId = id2;
- }
-
- if (chapter) {
- // create random id string
- chapterNotFound = Math.random().toString(36).substring(7);
- }
-
- if (aniId === "na" && mangadexId) {
- const datas = await getAnifyInfo(mangadexId);
-
- aniId =
- datas.mappings?.filter((i) => i.providerId === "anilist")[0]?.id || null;
-
- if (!aniId) {
- info = datas;
- data = datas;
- color = {
- backgroundColor: `${"#ffff"}`,
- color: "#000",
- };
- // return {
- // redirect: {
- // destination: "/404",
- // permanent: false,
- // },
- // };
- }
- } else if (aniId && !mangadexId) {
- // console.log({ aniId });
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
- },
- body: JSON.stringify({
- query: `query ($id: Int, $type: MediaType) {
- Media (id: $id, type: $type) {
- id
- title {
- romaji
- english
- native
- }
- }
- }`,
- variables: {
- id: parseInt(aniId),
- type: "MANGA",
- },
- }),
- });
- const aniListData = await response.json();
- const info = aniListData?.data?.Media;
-
- const mangaId = await getMangaId(
- info?.title?.romaji,
- info?.title?.english,
- info?.title?.native
- );
- mangadexId = mangaId?.id;
-
- if (!mangadexId) {
- return {
- redirect: {
- destination: "/404",
- permanent: false,
- },
- };
- }
-
- return {
- redirect: {
- destination: `/en/manga/${aniId}/${mangadexId}${
- chapter ? "?chapter=404" : ""
- }`,
- permanent: true,
- },
- };
- } else if (!aniId && mangadexId) {
- const data = await getAnifyInfo(mangadexId);
-
- aniId =
- data.mappings.filter((i) => i.providerId === "anilist")[0]?.id || null;
-
- if (!aniId) {
- info = data;
- // return {
- // redirect: {
- // destination: "/404",
- // permanent: false,
- // },
- // };
- }
-
- return {
- redirect: {
- destination: `/en/manga/${aniId ? aniId : "na"}${`/${mangadexId}`}${
- chapter ? "?chapter=404" : ""
- }`,
- permanent: true,
- },
- };
- } else {
- if (redis) {
- const getCached = await redis.get(`mangaPage:${mangadexId}`);
-
- if (getCached) {
- cached = JSON.parse(getCached);
- }
- }
- // let chapters;
- if (cached) {
- data = cached.data;
- info = cached.info;
- color = cached.color;
- } else {
- data = await getAnifyInfo(mangadexId);
-
- const aniListId =
- data.mappings?.filter((i) => i.providerId === "anilist")[0]?.id || null;
-
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
- },
- body: JSON.stringify({
- query: mediaInfoQuery,
- variables: {
- id: parseInt(aniListId),
- type: "MANGA",
- },
- }),
- });
- const aniListData = await response.json();
- if (aniListData?.data?.Media) info = aniListData?.data?.Media;
-
- const textColor = setTxtColor(info?.color);
-
- color = {
- backgroundColor: `${info?.color || "#ffff"}`,
- color: textColor,
- };
-
- if (redis) {
- await redis.set(
- `mangaPage:${mangadexId}`,
- JSON.stringify({ data, info, color }),
- "ex",
- 60 * 60 * 24
- );
- }
- }
- }
-
- return {
- props: {
- info: info || null,
- anifyData: data || null,
- chapterNotFound: chapterNotFound || null,
- color: color || null,
- },
- };
-}
-
-function getBrightness(hexColor) {
- if (!hexColor) {
- return 200;
- }
- const rgb = hexColor
- .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
- .slice(1)
- .map((x) => parseInt(x, 16));
- return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000;
-}
-
-function setTxtColor(hexColor) {
- const brightness = getBrightness(hexColor);
- return brightness < 150 ? "#fff" : "#000";
-}
diff --git a/pages/en/manga/[...id].tsx b/pages/en/manga/[...id].tsx
new file mode 100644
index 0000000..d1c10a4
--- /dev/null
+++ b/pages/en/manga/[...id].tsx
@@ -0,0 +1,456 @@
+import Footer from "@/components/shared/footer";
+import Head from "next/head";
+import { useEffect, useState } from "react";
+import { getServerSession } from "next-auth";
+import { authOptions } from "../../api/auth/[...nextauth]";
+import { mediaInfoQuery } from "@/lib/graphql/query";
+import Modal from "@/components/modal";
+import { signIn } from "next-auth/react";
+import AniList from "@/components/media/aniList";
+import ListEditor from "@/components/listEditor";
+import MobileNav from "@/components/shared/MobileNav";
+import Image from "next/image";
+import DetailTop from "@/components/anime/mobile/topSection";
+import Characters from "@/components/anime/charactersCard";
+import Content from "@/components/home/content";
+import { toast } from "sonner";
+import getAnifyInfo from "@/lib/anify/info";
+import getMangaId from "@/lib/anify/getMangaId";
+import { useRouter } from "next/router";
+import ChaptersComponent from "@/components/manga/ChaptersComponent";
+import pls from "@/utils/request/index";
+import { AniListInfoTypes } from "types/info/AnilistInfoTypes";
+import { Navbar } from "@/components/shared/NavBar";
+
+type MangaProps = {
+ aniId: string;
+ mangadexId: string;
+ sessions: any;
+ metaData: any;
+ chapterNotFound: string;
+};
+
+export default function Manga({
+ aniId,
+ mangadexId,
+ sessions: session,
+ chapterNotFound,
+ metaData,
+}: MangaProps) {
+ const [domainUrl, setDomainUrl] = useState("");
+
+ const [loading, setLoading] = useState(false);
+ const [watch, setWatch] = useState();
+
+ const [mangaId, setMangaId] = useState<string | null>(mangadexId);
+ const [chapters, setChapters] = useState(null);
+ const [notFound, setNotFound] = useState(false);
+
+ const [info, setInfo] = useState<AniListInfoTypes | null>(null);
+ const [color, setColor] = useState(null);
+
+ const [open, setOpen] = useState(false);
+
+ const router = useRouter();
+
+ const rec = info?.recommendations?.nodes?.map(
+ (data) => data.mediaRecommendation
+ );
+
+ useEffect(() => {
+ setDomainUrl(window.location.origin);
+ }, []);
+
+ useEffect(() => {
+ if (chapterNotFound) {
+ toast.error("Chapter not found");
+ const cleanUrl = window.location.origin + window.location.pathname;
+ window.history.replaceState(null, "", cleanUrl);
+ }
+ }, [chapterNotFound]);
+
+ useEffect(() => {
+ setMangaId(null);
+ }, [aniId]);
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ let info, data, color: any;
+ setChapters(null);
+ setNotFound(false);
+
+ if (aniId && mangadexId) {
+ const [aniListData] = await pls.post("https://graphql.anilist.co/", {
+ body: JSON.stringify({
+ query: mediaInfoQuery,
+ variables: {
+ id: parseInt(aniId),
+ type: "MANGA",
+ },
+ }),
+ });
+ // const aniListData = await response.json();
+ info = aniListData?.data?.Media;
+ const textColor = setTxtColor(info?.color);
+
+ color = {
+ backgroundColor: `${info?.color || "#ffff"}`,
+ color: textColor,
+ };
+
+ setInfo(info);
+ setColor(color);
+ setMangaId(mangadexId);
+ // console.log("wow two of them here");
+ } else if (aniId && !mangadexId) {
+ const [aniListData] = await pls.post("https://graphql.anilist.co/", {
+ body: JSON.stringify({
+ query: mediaInfoQuery,
+ variables: {
+ id: parseInt(aniId),
+ type: "MANGA",
+ },
+ }),
+ });
+ // const aniListData = await response.json();
+ info = aniListData?.data?.Media;
+ const textColor = setTxtColor(info?.color);
+
+ color = {
+ backgroundColor: `${info?.color || "#ffff"}`,
+ color: textColor,
+ };
+
+ setInfo(info);
+ setColor(color);
+
+ const mangaId = await getMangaId(
+ info?.title?.romaji,
+ info?.title?.english,
+ info?.title?.native
+ );
+
+ mangadexId = (mangaId as { id: string }).id;
+
+ if (mangadexId) {
+ setMangaId(mangadexId);
+ // console.log("mangadex is here", mangadexId);
+ router.push("/en/manga/" + aniId + "/" + mangadexId, undefined, {
+ shallow: true,
+ });
+ } else {
+ // console.log("why is this running?");
+ setMangaId(null);
+ setLoading(false);
+ setNotFound(true);
+ // router.push("/en/manga/" + aniId, undefined, { shallow: true });
+ }
+ } else if (!aniId && mangadexId) {
+ data = await getAnifyInfo(mangadexId);
+
+ const aniListId =
+ data.mappings?.filter((i: any) => i.providerId === "anilist")[0]
+ ?.id || null;
+
+ if (aniListId) {
+ const [aniListData] = await pls.post(
+ "https://graphql.anilist.co/",
+ {
+ body: JSON.stringify({
+ query: mediaInfoQuery,
+ variables: {
+ id: parseInt(aniListId),
+ type: "MANGA",
+ },
+ }),
+ }
+ );
+ // const aniListData = await response.json();
+ info = aniListData?.data?.Media;
+
+ router.push(
+ "/en/manga/" + aniListId + "/" + mangadexId,
+ undefined,
+ { shallow: true }
+ );
+ }
+
+ const textColor = setTxtColor(data?.color);
+
+ color = {
+ backgroundColor: `${data?.color || "#ffff"}`,
+ color: textColor,
+ };
+
+ setInfo(aniListId ? info : data);
+ setColor(color);
+ setMangaId(mangadexId);
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ }
+ fetchData();
+
+ return () => {
+ setInfo(null);
+ };
+ }, [session?.user?.token, aniId, mangadexId]);
+
+ function handleOpen() {
+ setOpen(true);
+ document.body.style.overflow = "hidden";
+ }
+
+ function handleClose() {
+ setOpen(false);
+ document.body.style.overflow = "auto";
+ }
+
+ return (
+ <>
+ <Head>
+ <title>
+ {metaData
+ ? `Manga - ${
+ metaData.title.romaji ||
+ metaData.title.english ||
+ metaData.title.native
+ }`
+ : "Getting Info..."}
+ </title>
+ <meta
+ name="description"
+ content={`${metaData?.description?.slice(0, 180)}...`}
+ />
+ <meta
+ name="keywords"
+ content={`${metaData?.genres}, ${metaData?.author} `}
+ />
+ <meta
+ property="og:title"
+ content={`Moopa - ${
+ metaData?.title.romaji || metaData?.title.english
+ }`}
+ />
+ <meta
+ property="og:description"
+ content={`${metaData?.description?.slice(0, 180)}...`}
+ />
+ <meta
+ property="og:image"
+ content={`${domainUrl}/api/og?title=${
+ metaData?.title.romaji || metaData?.title.english
+ }&image=${metaData?.bannerImage || metaData?.coverImage}`}
+ />
+ <meta
+ property="og:url"
+ content={`${domainUrl}/en/manga/${metaData?.id}`}
+ />
+ <meta property="og:type" content="book" />
+ <meta property="og:locale" content="en_US" />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:site" content="@yourTwitterHandle" />
+ <meta
+ name="twitter:title"
+ content={`Moopa - ${
+ metaData?.title.romaji || metaData?.title.english
+ }`}
+ />
+ <meta
+ name="twitter:description"
+ content={`${metaData?.description?.slice(0, 180)}...`}
+ />
+ <meta name="robots" content="noindex" />
+ <meta
+ name="twitter:image"
+ content={`${domainUrl}/api/og?title=${
+ metaData?.title.romaji || metaData?.title.english
+ }&image=${metaData?.bannerImage || metaData?.coverImage}`}
+ />
+ </Head>
+ <Navbar info={info} manga />
+ <Modal open={open} onClose={() => handleClose()}>
+ <div>
+ {!session && (
+ <div className="flex-center flex-col gap-5 px-10 py-5 bg-secondary rounded-md">
+ <div className="text-md font-extrabold font-karla">
+ Edit your list
+ </div>
+ <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>
+ )}
+ {session && info && (
+ <ListEditor
+ animeId={info?.id}
+ session={session}
+ // stats={statuses?.value}
+ // prg={progress}
+ max={info?.episodes}
+ info={info}
+ close={handleClose}
+ />
+ )}
+ </div>
+ </Modal>
+ <MobileNav hideProfile={true} />
+ <main className="w-screen min-h-screen overflow-hidden relative flex flex-col items-center gap-5">
+ <div className="w-screen absolute">
+ <div className="bg-gradient-to-t from-primary from-10% to-transparent absolute h-[280px] w-screen z-10 inset-0" />
+ {info?.bannerImage && (
+ <Image
+ src={info?.bannerImage}
+ alt="banner anime"
+ height={1000}
+ width={1000}
+ blurDataURL={info?.bannerImage}
+ className="object-cover bg-image blur-[2px] w-screen absolute top-0 left-0 h-[250px] brightness-[55%] z-0"
+ />
+ )}
+ </div>
+ <div className="w-full lg:max-w-screen-lg xl:max-w-screen-2xl z-30 flex flex-col gap-5 pb-10">
+ {/* {info && ( */}
+ <DetailTop
+ info={info}
+ handleOpen={handleOpen}
+ // statuses={statuses}
+ watchUrl={watch}
+ // progress={progress}
+ color={color}
+ />
+ {/* )} */}
+
+ <ChaptersComponent
+ info={info}
+ mangaId={mangaId}
+ aniId={aniId}
+ setWatch={setWatch}
+ chapter={chapters}
+ setChapter={setChapters}
+ loading={loading}
+ setLoading={setLoading}
+ notFound={notFound}
+ setNotFound={setNotFound}
+ />
+
+ {info && info.characters.edges.length > 0 && (
+ <div className="w-full">
+ <Characters info={info?.characters?.edges} />
+ </div>
+ )}
+
+ {info && rec && rec?.length !== 0 && (
+ <div className="w-full">
+ <Content
+ ids="recommendAnime"
+ section="Recommendations"
+ type="manga"
+ data={rec}
+ />
+ </div>
+ )}
+ </div>
+ </main>
+ <Footer />
+ </>
+ );
+}
+
+export async function getServerSideProps(context: any) {
+ const session: any = await getServerSession(
+ context.req,
+ context.res,
+ authOptions
+ );
+ const accessToken = session?.user?.token || null;
+
+ const { chapter } = context.query;
+ const [id1, id2] = context.query.id;
+
+ let aniId, mangadexId;
+ let chapterNotFound;
+
+ if (String(id1).length > 6) {
+ aniId = id2;
+ mangadexId = id1;
+ } else {
+ aniId = id1;
+ mangadexId = id2;
+ }
+
+ if (chapter) {
+ // create random id string
+ chapterNotFound = Math.random().toString(36).substring(7);
+ }
+
+ const [aniListData] = await pls.post("https://graphql.anilist.co/", {
+ body: JSON.stringify({
+ query: `query ($id: Int, $type: MediaType) {
+ Media(id: $id, type: $type) {
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ bannerImage
+ genres
+ coverImage {
+ extraLarge
+ large
+ medium
+ color
+ }
+ status
+ description
+ }
+ }`,
+ variables: {
+ id: parseInt(aniId),
+ type: "MANGA",
+ },
+ }),
+ });
+ const info = aniListData?.data?.Media;
+
+ return {
+ props: {
+ aniId: aniId || null,
+ mangadexId: mangadexId || null,
+ accessToken: accessToken || null,
+ sessions: session || null,
+ metaData: info || null,
+ // info: info || null,
+ // anifyData: data || null,
+ chapterNotFound: chapterNotFound || null,
+ // color: color || null,
+ },
+ };
+}
+
+function getBrightness(hexColor: { match: (arg0: RegExp) => any[] }) {
+ if (!hexColor) {
+ return 200;
+ }
+ const rgb = hexColor
+ .match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
+ .slice(1)
+ .map((x) => parseInt(x, 16));
+ return (299 * rgb[0] + 587 * rgb[1] + 114 * rgb[2]) / 1000;
+}
+
+function setTxtColor(hexColor: { match: (arg0: RegExp) => any[] }) {
+ const brightness = getBrightness(hexColor);
+ return brightness < 150 ? "#fff" : "#000";
+}
diff --git a/pages/en/manga/read/[...params].js b/pages/en/manga/read/[...params].js
index a7fa78b..036b999 100644
--- a/pages/en/manga/read/[...params].js
+++ b/pages/en/manga/read/[...params].js
@@ -150,6 +150,7 @@ export default function Read({
data-title-native={info?.title?.native}
/>
<meta id="CoverImage" data-manga-cover={info?.coverImage} />
+ <meta name="robots" content="noindex" />
</Head>
<div className="w-screen flex justify-evenly relative">
<ShortCutModal isOpen={isKeyOpen} setIsOpen={setIsKeyOpen} />
diff --git a/pages/en/profile/[user].js b/pages/en/profile/[user].tsx
index 7ef5de3..82b88af 100644
--- a/pages/en/profile/[user].js
+++ b/pages/en/profile/[user].tsx
@@ -1,14 +1,28 @@
-import { getServerSession } from "next-auth";
-import { authOptions } from "../../api/auth/[...nextauth]";
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 { NewNavbar } from "@/components/shared/NavBar";
import { toast } from "sonner";
+import { Navbar } from "@/components/shared/NavBar";
+import pls from "@/utils/request";
+import { CurrentMediaTypes } from "..";
-export default function MyList({ media, sessions, user, time, userSettings }) {
+type MyListProps = {
+ media: CurrentMediaTypes[];
+ sessions: any;
+ user: any;
+ time: any;
+ userSettings: any;
+};
+
+export default function MyList({
+ media,
+ sessions,
+ user,
+ time,
+ userSettings,
+}: MyListProps) {
const [listFilter, setListFilter] = useState("all");
const [visible, setVisible] = useState(false);
const [useCustomList, setUseCustomList] = useState(true);
@@ -40,26 +54,27 @@ export default function MyList({ media, sessions, user, time, userSettings }) {
if (data) {
toast.success(`Custom List is now ${!useCustomList ? "on" : "off"}`);
}
- localStorage.setItem("customList", !useCustomList);
+ localStorage.setItem("customList", String(!useCustomList));
} catch (error) {
console.error(error);
}
};
- const filterMedia = (status) => {
+ const filterMedia = (status: string) => {
if (status === "all") {
return media;
}
- return media.filter((m) => m.name === status);
+ return media.filter((m: { name: string }) => m.name === status);
};
return (
<>
<Head>
<title>My Lists</title>
</Head>
- <NewNavbar />
- <div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 relative">
+ <Navbar withNav toTop shrink bgHover scrollP={110} paddingY={"py-1"} />
+
+ <div className="w-screen lg:flex justify-between lg:px-10 xl:px-32 py-5 mt-10 xl:mt-16 relative">
<div className="lg:w-[30%] h-full mt-12 lg:mr-10 grid gap-5 mx-3 lg:mx-0 antialiased">
<div className="flex items-center gap-5">
<Image
@@ -289,7 +304,7 @@ export default function MyList({ media, sessions, user, time, userSettings }) {
<div className="absolute -top-10 -left-40 invisible lg:group-hover:visible">
<Image
src={item.media.coverImage.large}
- alt={item.media.id}
+ alt={String(item.media.id)}
width={1000}
height={1000}
className="object-cover h-[186px] w-[140px] shrink-0 rounded-md"
@@ -362,19 +377,14 @@ export default function MyList({ media, sessions, user, time, userSettings }) {
);
}
-export async function getServerSideProps(context) {
- const session = await getServerSession(context.req, context.res, authOptions);
- const accessToken = session?.user?.token || null;
+export async function getServerSideProps(context: any) {
const query = context.query;
- const response = await fetch("https://graphql.anilist.co/", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(accessToken && { Authorization: `Bearer ${accessToken}` }),
- },
- body: JSON.stringify({
- query: `
+ const [data, session] = await pls.post(
+ "https://graphql.anilist.co/",
+ {
+ body: JSON.stringify({
+ query: `
query ($username: String, $status: MediaListStatus) {
MediaListCollection(userName: $username, type: ANIME, status: $status, sort: SCORE_DESC) {
user {
@@ -426,15 +436,15 @@ export async function getServerSideProps(context) {
}
}
`,
- variables: {
- username: query.user,
- },
- }),
- });
-
- const data = await response.json();
+ variables: {
+ username: query.user,
+ },
+ }),
+ },
+ context
+ );
- const get = data.data.MediaListCollection;
+ const get = data?.data?.MediaListCollection;
const sectionOrder = get?.user.mediaListOptions.animeList.sectionOrder;
if (!sectionOrder) {
@@ -451,12 +461,15 @@ export async function getServerSideProps(context) {
const prog = get.lists;
- function getIndex(status) {
+ function getIndex(status: string) {
const index = sectionOrder.indexOf(status);
return index === -1 ? sectionOrder.length : index;
}
- prog.sort((a, b) => getIndex(a.name) - getIndex(b.name));
+ prog.sort(
+ (a: { name: string }, b: { name: string }) =>
+ getIndex(a.name) - getIndex(b.name)
+ );
const user = get.user;
@@ -473,24 +486,24 @@ export async function getServerSideProps(context) {
};
}
-function UnixTimeConverter({ unixTime }) {
+function UnixTimeConverter({ unixTime }: { unixTime: number }) {
const date = new Date(unixTime * 1000); // multiply by 1000 to convert to milliseconds
const formattedDate = date.toISOString().slice(0, 10); // format date to YYYY-MM-DD
return <p>{formattedDate}</p>;
}
-function convertMinutesToDays(minutes) {
+function convertMinutesToDays(minutes: number) {
const hours = minutes / 60;
const days = hours / 24;
if (days >= 1) {
return days % 1 === 0
- ? { days: `${parseInt(days)}` }
+ ? { days: `${days}` }
: { days: `${days.toFixed(1)}` };
} else {
return hours % 1 === 0
- ? { hours: `${parseInt(hours)}` }
+ ? { hours: `${hours}` }
: { hours: `${hours.toFixed(1)}` };
}
}
diff --git a/pages/en/schedule/index.js b/pages/en/schedule/index.tsx
index f1e6730..aa30259 100644
--- a/pages/en/schedule/index.js
+++ b/pages/en/schedule/index.tsx
@@ -18,7 +18,7 @@ import MobileNav from "@/components/shared/MobileNav";
import { useSession } from "next-auth/react";
import { redis } from "@/lib/redis";
import Head from "next/head";
-import { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
const day = [
"Sunday",
@@ -30,7 +30,8 @@ const day = [
"Saturday",
];
-const isAired = (timestamp) => {
+const isAired = (timestamp: number | null) => {
+ if (!timestamp) return false;
const currentTime = new Date().getTime() / 1000;
return timestamp <= currentTime;
};
@@ -51,7 +52,7 @@ export async function getServerSideProps() {
0
);
const timeUntilMidnightJapan = Math.round(
- (midnightTomorrowJapan - nowJapan) / 1000
+ (midnightTomorrowJapan.getTime() - nowJapan.getTime()) / 1000
);
let cachedData;
@@ -109,12 +110,13 @@ export async function getServerSideProps() {
page++;
}
- const timestampToDay = (timestamp) => {
- const options = { weekday: "long" };
- return new Date(timestamp * 1000).toLocaleDateString(undefined, options);
+ const timestampToDay = (timestamp: number) => {
+ return new Date(timestamp * 1000).toLocaleDateString(undefined, {
+ weekday: "long",
+ });
};
- const scheduleByDay = {};
+ const scheduleByDay: { [key: string]: any } = {};
airingSchedules.forEach((schedule) => {
const day = timestampToDay(schedule.airingAt);
if (!scheduleByDay[day]) {
@@ -142,10 +144,7 @@ export async function getServerSideProps() {
// setSchedule(scheduleByDay);
}
-export default function Schedule({ schedule }) {
- const { data: session } = useSession();
-
- // const [schedule, setSchedule] = useState({});
+export default function Schedule({ schedule }: any) {
const [filterDay, setFilterDay] = useState("All");
const [loading, setLoading] = useState(true);
@@ -178,7 +177,7 @@ export default function Schedule({ schedule }) {
let nextAiring = null;
let currentlyAiring = null;
- for (const [, schedules] of Object.entries(sortedSchedule)) {
+ for (const [, schedules] of Object.entries(sortedSchedule as object)) {
for (const s of schedules) {
if (s.airingAt > now) {
if (!nextAiring) {
@@ -196,16 +195,16 @@ export default function Schedule({ schedule }) {
setCurrentlyAiringAnime(currentlyAiring);
}, [sortedSchedule]);
- const scrollContainerRef = useRef(null);
+ const scrollContainerRef = useRef<HTMLUListElement>(null);
useEffect(() => {
// Scroll to center the active button when it changes
if (scrollContainerRef.current) {
const activeButton =
- scrollContainerRef.current.querySelector(".text-action");
+ scrollContainerRef.current?.querySelector(".text-action");
if (activeButton) {
const containerWidth = scrollContainerRef.current.clientWidth;
- const buttonLeft = activeButton.offsetLeft;
+ const buttonLeft = (activeButton as HTMLElement).offsetLeft;
const buttonWidth = activeButton.clientWidth;
const scrollLeft = buttonLeft - containerWidth / 2 + buttonWidth / 2;
scrollContainerRef.current.scrollLeft = scrollLeft;
@@ -264,8 +263,8 @@ export default function Schedule({ schedule }) {
content="Moopa is a website where you can find all the information about your favorite anime and manga."
/>
</Head>
- <MobileNav sessions={session} hideProfile={true} />
- <NewNavbar scrollP={10} toTop={true} />
+ <MobileNav hideProfile={true} />
+ <Navbar scrollP={10} toTop={true} />
<div className="w-screen">
<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]" />
@@ -340,7 +339,7 @@ export default function Schedule({ schedule }) {
>
<div className="ml-4 flex items-center gap-2">
<h3 className="text-lg text-gray-200 font-semibold">
- {timeStamptoAMPM(time)}
+ {time && timeStamptoAMPM(time)}
</h3>
{/* {!isAired(time) && <p>Airing Next</p>} */}
<p
diff --git a/pages/en/search/[...param].js b/pages/en/search/[...param].tsx
index c1fd94c..5a34ff5 100644
--- a/pages/en/search/[...param].js
+++ b/pages/en/search/[...param].tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from "react";
+import { Key, useEffect, useRef, useState } from "react";
import { motion as m } from "framer-motion";
import Skeleton from "react-loading-skeleton";
import { useRouter } from "next/router";
@@ -23,12 +23,15 @@ import {
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/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
import MobileNav from "@/components/shared/MobileNav";
-import SearchByImage from "@/components/search/searchByImage";
+import SearchByImage, {
+ TraceMoeResultTypes,
+} from "@/components/search/searchByImage";
import { PlayIcon } from "@heroicons/react/24/outline";
+import { StaticImport } from "next/dist/shared/lib/get-img-props";
-export async function getServerSideProps(context) {
+export async function getServerSideProps(context: any) {
const { param } = context.query;
const { search, format, genres, season, year } = context.query;
@@ -81,6 +84,15 @@ export async function getServerSideProps(context) {
};
}
+type CardProps = {
+ index: number;
+ query: string;
+ genres: any;
+ formats: any;
+ seasons: any;
+ years: any;
+};
+
export default function Card({
index,
query,
@@ -88,22 +100,25 @@ export default function Card({
formats,
seasons,
years,
-}) {
+}: CardProps) {
const inputRef = useRef(null);
const router = useRouter();
- const [data, setData] = useState();
- const [imageSearch, setImageSearch] = useState();
+ const [data, setData] = useState<any>();
+ const [imageSearch, setImageSearch] = useState<TraceMoeResultTypes[]>();
const [loading, setLoading] = useState(true);
- const [search, setQuery] = useState(query);
+ const [search, setQuery] = useState<string | null | undefined>(query);
const debounceSearch = useDebounce(search, 500);
- const [type, setSelectedType] = useState(mediaType[index]);
+ const [type, setSelectedType] = useState<{
+ name: string;
+ value: string;
+ } | null>(mediaType[index]);
const [year, setYear] = useState(years);
const [season, setSeason] = useState(seasons);
- const [sort, setSelectedSort] = useState();
+ const [sort, setSelectedSort] = useState<{ name: string; value: string }>();
const [genre, setGenre] = useState(genres);
const [format, setFormat] = useState(formats);
@@ -116,7 +131,7 @@ export default function Card({
setLoading(true);
const data = await aniAdvanceSearch({
search: debounceSearch,
- type: type?.value,
+ type: type?.value as "ANIME" | "MANGA" | undefined,
genres: genre,
page: page,
sort: sort?.value,
@@ -128,7 +143,7 @@ export default function Card({
setNextPage(false);
setLoading(false);
} else if (data !== null && page > 1) {
- setData((prevData) => {
+ setData((prevData: any) => {
return [...(prevData ?? []), ...data?.media];
});
setNextPage(data?.pageInfo.hasNextPage);
@@ -144,7 +159,9 @@ export default function Card({
setData(null);
setPage(1);
setNextPage(true);
- advance();
+ if (page === 1) {
+ advance();
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
debounceSearch,
@@ -158,7 +175,9 @@ export default function Card({
useEffect(() => {
if (imageSearch) return;
- advance();
+ if (page > 1) {
+ advance();
+ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, imageSearch]);
@@ -177,21 +196,23 @@ export default function Card({
window.innerHeight + window.pageYOffset >=
document.body.offsetHeight - 3
) {
- setPage((prevPage) => prevPage + 1);
+ if (!loading) {
+ setPage((prevPage) => prevPage + 1);
+ }
}
}
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
- }, [page, nextPage, imageSearch]);
+ }, [page, nextPage, imageSearch, loading]);
- const handleKeyDown = async (event) => {
+ const handleKeyDown = async (event: any) => {
if (event.key === "Enter") {
event.preventDefault();
const inputValue = event.target.value;
if (inputValue === "") {
- setQuery(null);
+ setQuery(undefined);
} else {
setQuery(inputValue);
}
@@ -199,13 +220,13 @@ export default function Card({
};
function trash() {
- setImageSearch();
- setQuery();
- setGenre();
- setFormat();
- setSelectedSort();
- setSeason();
- setYear();
+ setImageSearch(undefined);
+ setQuery(undefined);
+ setGenre(undefined);
+ setFormat(undefined);
+ setSelectedSort(undefined);
+ setSeason(undefined);
+ setYear(undefined);
router.push(`/en/search/${mediaType[index]?.value?.toLowerCase()}`);
}
@@ -213,8 +234,8 @@ export default function Card({
setIsVisible(!isVisible);
}
- const handleVideoHover = (hovered, id) => {
- const updatedImageSearch = imageSearch?.map((item) => {
+ const handleVideoHover = (hovered: boolean, id: any) => {
+ const updatedImageSearch = imageSearch?.map((item: any) => {
if (item.filename === id) {
return { ...item, hovered };
}
@@ -234,7 +255,7 @@ export default function Card({
<link rel="icon" href="/svg/c.svg" />
</Head>
- <NewNavbar
+ <Navbar
scrollP={10}
withNav={true}
shrink={true}
@@ -366,7 +387,7 @@ export default function Card({
</div>
)}
{/* <div> */}
- <div className="flex flex-col gap-14 items-center z-30">
+ <div className="flex flex-col gap-14 items-center z-30 overflow-x-hidden">
<div
key="card-keys"
className={`${
@@ -384,69 +405,75 @@ export default function Card({
{data &&
data?.length > 0 &&
!imageSearch &&
- data?.map((anime, index) => {
- const anilistId = anime?.mappings?.find(
- (x) => x.providerId === "anilist"
- )?.id;
- return (
- <m.div
- initial={{ scale: 0.98 }}
- animate={{ scale: 1, transition: { duration: 0.35 } }}
- className="w-full"
- key={index}
- >
- <Link
- href={
- anime.format === "MANGA" || anime.format === "NOVEL"
- ? `/en/manga/${anilistId ? `${anilistId}/` : ""}${
- anime.id
- }`
- : `/en/anime/${anime.id}`
- }
- title={anime.title.userPreferred}
- className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded"
- style={{
- paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%)
- }}
- >
- <Image
- className="object-cover"
- src={anime.coverImage.extraLarge}
- alt={anime.title.userPreferred}
- sizes="(min-width: 808px) 50vw, 100vw"
- quality={100}
- fill
- />
- </Link>
- <Link
- href={
- anime.format === "MANGA" || anime.format === "NOVEL"
- ? `/en/manga/${anilistId ? `${anilistId}/` : ""}${
- anime.id
- }`
- : `/en/anime/${anime.id}`
- }
- title={anime.title.userPreferred}
+ data?.map(
+ (
+ anime: {
+ format: string;
+ id: any;
+ title: { userPreferred: string };
+ coverImage: { extraLarge: string | StaticImport };
+ status: string;
+ episodes: any;
+ chapters: any;
+ },
+ index: Key | null | undefined
+ ) => {
+ return (
+ <m.div
+ initial={{ scale: 0.98 }}
+ animate={{ scale: 1, transition: { duration: 0.35 } }}
+ className="w-full"
+ key={index}
>
- <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2">
- {anime.status === "RELEASING" ? (
- <span className="dots bg-green-500" />
- ) : anime.status === "NOT_YET_RELEASED" ? (
- <span className="dots bg-red-500" />
- ) : null}
- {anime.title.userPreferred}
- </h1>
- </Link>
- <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]">
- {anime.format || <p>-</p>} &#183;{" "}
- {anime.status || <p>-</p>} &#183;{" "}
- {anime.episodes
- ? `${anime.episodes || "N/A"} Episodes`
- : `${anime.chapters || "N/A"} Chapters`}
- </h2>
- </m.div>
- );
- })}
+ <Link
+ href={
+ anime.format === "MANGA" || anime.format === "NOVEL"
+ ? `/en/manga/${anime.id}`
+ : `/en/anime/${anime.id}`
+ }
+ title={anime.title.userPreferred}
+ className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded"
+ style={{
+ paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%)
+ }}
+ >
+ <Image
+ className="object-cover"
+ src={anime.coverImage.extraLarge}
+ alt={anime.title.userPreferred}
+ sizes="(min-width: 808px) 50vw, 100vw"
+ quality={100}
+ fill
+ />
+ </Link>
+ <Link
+ href={
+ anime.format === "MANGA" || anime.format === "NOVEL"
+ ? `/en/manga/${anime.id}`
+ : `/en/anime/${anime.id}`
+ }
+ title={anime.title.userPreferred}
+ >
+ <h1 className="font-outfit font-bold xl:text-base text-[15px] pt-4 line-clamp-2">
+ {anime.status === "RELEASING" ? (
+ <span className="dots bg-green-500" />
+ ) : anime.status === "NOT_YET_RELEASED" ? (
+ <span className="dots bg-red-500" />
+ ) : null}
+ {anime.title.userPreferred}
+ </h1>
+ </Link>
+ <h2 className="font-outfit xl:text-[15px] text-[11px] font-light pt-2 text-[#8B8B8B]">
+ {anime.format || <p>-</p>} &#183;{" "}
+ {anime.status || <p>-</p>} &#183;{" "}
+ {anime.episodes
+ ? `${anime.episodes || "N/A"} Episodes`
+ : `${anime.chapters || "N/A"} Chapters`}
+ </h2>
+ </m.div>
+ );
+ }
+ )}
{loading && (
<>
@@ -532,7 +559,7 @@ export default function Card({
href={`/en/anime/${a.anilist.id}`}
>
{/* <h1 className="font-semibold">{a.title}</h1> */}
- <p className="flex items-center gap-1 text-sm text-gray-400 w-[320px]">
+ <p className="flex items-center gap-1 text-sm text-gray-400 max-w-[320px]">
<span
className="text-white max-w-[120px] md:max-w-[200px] lg:max-w-[220px]"
style={{
diff --git a/pages/id/index.js b/pages/id/index.tsx
index 5ef870d..9af2d06 100644
--- a/pages/id/index.js
+++ b/pages/id/index.tsx
@@ -3,7 +3,7 @@ import React from "react";
import Image from "next/image";
import Link from "next/link";
import Footer from "@/components/shared/footer";
-import { NewNavbar } from "@/components/shared/NavBar";
+import { Navbar } from "@/components/shared/NavBar";
import MobileNav from "@/components/shared/MobileNav";
export default function Home() {
@@ -16,7 +16,7 @@ export default function Home() {
<link rel="icon" href="/svg/c.svg" />
</Head>
<main className="flex flex-col h-screen">
- <NewNavbar />
+ <Navbar />
<MobileNav hideProfile />
{/* Create an under construction page with tailwind css */}
<div className="h-full w-screen flex-center flex-grow flex-col">
diff --git a/pages/id/manga/[...id].tsx b/pages/id/manga/[...id].tsx
new file mode 100644
index 0000000..513001e
--- /dev/null
+++ b/pages/id/manga/[...id].tsx
@@ -0,0 +1,159 @@
+import axios from "axios";
+import Image from "next/image";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+import { Navbar } from "../../../components/shared/NavBar";
+import MobileNav from "../../../components/shared/MobileNav";
+import pls from "@/utils/request";
+
+export interface DataType {
+ id: string;
+ title: string;
+ description: string;
+ image: string;
+ chapters: ChapterType[];
+}
+
+export interface ChapterType {
+ id: string;
+ title: string;
+ rilis: string;
+}
+
+interface InfoNovelProps {
+ id: string;
+ API: string;
+}
+
+export default function InfoNovel({ id, API }: InfoNovelProps) {
+ const [data, setData] = useState<DataType | null>(null);
+ const [loading, setLoading] = useState<boolean>(true);
+
+ const [filter, setFilter] = useState<string>("");
+
+ useEffect(() => {
+ async function fetchData() {
+ setLoading(true);
+ try {
+ const data = await pls.get(`${API}/api/manga/info/` + id);
+ setData(data);
+ } catch (error) {
+ setData(null);
+ } finally {
+ setLoading(false);
+ }
+ }
+ fetchData();
+
+ return () => {
+ setData(null);
+ };
+ }, [id]);
+
+ const fuzzySearch = (text: string, query: string): boolean => {
+ const textLower = text.toLowerCase().replace(/\.|\s/g, "");
+ const queryLower = query.toLowerCase().replace(/\.|\s/g, "");
+
+ let i = 0;
+ let j = 0;
+
+ while (i < textLower.length && j < queryLower.length) {
+ if (textLower[i] === queryLower[j]) {
+ j++;
+ }
+ i++;
+ }
+
+ return j === queryLower.length;
+ };
+
+ const filteredData = data?.chapters?.filter((chapter: ChapterType) =>
+ fuzzySearch(chapter.title, filter)
+ );
+
+ return (
+ <div className="flex flex-col items-center">
+ <Navbar withNav paddingY="" scrollP={0} />
+ <MobileNav hideProfile />
+ <div className="relative w-full max-w-screen-lg mx-5 mt-5 px-5 lg:px-0 lg:mt-14">
+ {data && (
+ <div className="flex lg:flex-row flex-col z-30 pt-24 lg:px-5">
+ <div className="shrink-0 z-50 w-[170px] h-[240px] rounded overflow-hidden bg-secondary/20">
+ {data?.image && (
+ <Image
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
+ data?.image
+ )}${`&headers=${encodeURIComponent(
+ JSON.stringify({ Referer: "https://komikindo.tv/" })
+ )}`}`}
+ width={200}
+ height={200}
+ alt="coverImage"
+ className="z-50 w-[170px] h-[240px] object-cover"
+ />
+ )}
+ </div>
+ <div className="flex flex-col items-start justify-end gap-2 lg:pl-5 z-30 mt-5 lg:mt-0">
+ <h1 className="font-bold text-2xl lg:text-3xl font-outfit line-clamp-2">
+ {data?.title}
+ </h1>
+ {/* <div className="flex gap-5 w-full">
+ <p className="flex gap-2 font-bold font-karla">
+ Format: <span>{data?.format}</span>
+ </p>
+ <p className="flex gap-2 font-bold font-karla">
+ Release: <span>{data?.year}</span>
+ </p>
+ <p className="flex gap-2 font-bold font-karla">
+ Status: <span>{data?.status}</span>
+ </p>
+ </div> */}
+ <p className="line-clamp-2 font-light font-karla">
+ {data?.description}
+ </p>
+ </div>
+ </div>
+ )}
+
+ <div className="mt-10">
+ <input
+ className="appearance-none rounded bg-secondary px-2 py-1 font-karla outline-none"
+ placeholder="Search..."
+ value={filter}
+ onChange={(e) => setFilter(e.target.value)}
+ />
+ </div>
+
+ <div className="mt-5 flex flex-col gap-3">
+ {filteredData?.map((chapter: ChapterType) => (
+ <Link
+ key={chapter?.id}
+ href={`/id/manga/read/${id}/${chapter?.id}`}
+ className="py-3 bg-secondary w-full px-5 rounded"
+ >
+ <div className="flex justify-between items-center font-karla w-full">
+ <div className="">
+ <p className="font-bold">{chapter?.title}</p>
+ </div>
+ <p className="font-light">{chapter?.rilis}</p>
+ </div>
+ </Link>
+ ))}
+ </div>
+ <div className="w-full bg-secondary rounded-xl h-[200px] absolute inset-0 z-10" />
+ </div>
+ </div>
+ );
+}
+
+export async function getServerSideProps({ params }: any) {
+ const { id } = params;
+ const API = process.env.ID_API;
+ // console.log(id);
+ return {
+ props: {
+ id,
+ API,
+ },
+ };
+}
diff --git a/pages/id/manga/read/[...id].tsx b/pages/id/manga/read/[...id].tsx
new file mode 100644
index 0000000..4978e36
--- /dev/null
+++ b/pages/id/manga/read/[...id].tsx
@@ -0,0 +1,87 @@
+import Image from "next/image";
+import { useEffect, useState } from "react";
+import { Navbar } from "@/components/shared/NavBar";
+import MobileNav from "@/components/shared/MobileNav";
+import pls from "@/utils/request";
+
+type DataType = {
+ id: string;
+ title: string;
+ pages: PageType[];
+};
+
+type PageType = {
+ index: string;
+ src: string;
+};
+
+interface ReadNovelProps {
+ mangaId: string;
+ chapterId: string;
+ API: string;
+}
+
+export default function ReadNovel({ mangaId, chapterId, API }: ReadNovelProps) {
+ const [data, setData] = useState<DataType | null>();
+ const [hideNav, setHideNav] = useState(false);
+
+ useEffect(() => {
+ async function fetchData() {
+ if (chapterId) {
+ const data = await pls.get(`${API}/api/manga/pages/${chapterId}`);
+ setData(data);
+ }
+ }
+ fetchData();
+
+ return () => {
+ setData(null);
+ };
+ }, [chapterId]);
+
+ return (
+ <div className="w-screen flex flex-col items-center">
+ {!hideNav && (
+ <>
+ <Navbar paddingY="2" scrollP={0} />
+ <MobileNav hideProfile />
+ </>
+ )}
+ <div className="block mt-12" onClick={() => setHideNav((prev) => !prev)}>
+ <div className="w-full h-full max-w-screen-lg pointer-events-none select-none">
+ {data?.pages?.map((i) => (
+ <div key={i.index}>
+ <Image
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
+ i.src
+ )}${`&headers=${encodeURIComponent(
+ JSON.stringify({ Referer: "https://komikindo.tv/" })
+ )}`}`}
+ alt="image"
+ width={500}
+ height={500}
+ className="w-full h-full"
+ />
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export async function getServerSideProps({ params }: any) {
+ const { id } = params;
+
+ const [mangaId, chapterId] = id;
+
+ const API = process.env.ID_API;
+
+ return {
+ props: {
+ mangaId,
+ chapterId,
+ API,
+ },
+ };
+}
diff --git a/pages/id/novel/[...id].tsx b/pages/id/novel/[...id].tsx
new file mode 100644
index 0000000..7e9e155
--- /dev/null
+++ b/pages/id/novel/[...id].tsx
@@ -0,0 +1,121 @@
+import axios from "axios";
+import Image from "next/image";
+import Link from "next/link";
+import { useEffect, useState } from "react";
+import { Navbar } from "../../../components/shared/NavBar";
+import MobileNav from "../../../components/shared/MobileNav";
+import { GetServerSideProps } from "next";
+
+type InfoNovelProps = {
+ id: string;
+ API: string;
+};
+
+type NovelData = {
+ image?: string;
+ title?: string;
+ Release?: string;
+ Status?: string;
+ Author?: string;
+ description?: string;
+ chapters?: {
+ chapterId?: string;
+ chapter?: string;
+ release?: string;
+ }[];
+ notFound?: boolean;
+};
+
+export default function InfoNovel({ id, API }: InfoNovelProps) {
+ const [data, setData] = useState<NovelData>();
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ async function fetchData() {
+ setLoading(true);
+ try {
+ const { data } = await axios.get(`${API}/api/novel/info/` + id);
+ setData(data);
+ } catch (error) {
+ setData({
+ notFound: true,
+ });
+ } finally {
+ setLoading(false);
+ }
+ }
+ fetchData();
+
+ return () => {
+ setData(undefined);
+ };
+ }, [id]);
+
+ return (
+ <div className="flex flex-col items-center">
+ <Navbar withNav paddingY="" scrollP={0} />
+ <MobileNav hideProfile />
+ <div className="relative w-full max-w-screen-lg mx-5 mt-5 px-5 lg:px-0 lg:mt-14">
+ {data && (
+ <div className="flex lg:flex-row flex-col z-30 pt-24 lg:px-5">
+ {data?.image && (
+ <Image
+ src={data?.image}
+ width={200}
+ height={200}
+ alt="coverImage"
+ className="z-50 w-[170px] h-[240px] object-cover rounded"
+ />
+ )}
+ <div className="flex flex-col items-start justify-end gap-2 lg:pl-5 z-30 mt-5 lg:mt-0">
+ <h1 className="font-bold text-2xl lg:text-3xl font-outfit line-clamp-2">
+ {data?.title}
+ </h1>
+ <div className="flex gap-5 w-full">
+ <p className="flex gap-2 font-bold font-karla">
+ Release: <span>{data?.Release}</span>
+ </p>
+ <p className="flex gap-2 font-bold font-karla">
+ Status: <span>{data?.Status}</span>
+ </p>
+ <p className="flex-1 gap-2 font-bold font-karla overflow-x-hidden text-ellipsis whitespace-nowrap">
+ Author: <span>{data?.Author}</span>
+ </p>
+ </div>
+ <p className="line-clamp-2 font-light font-karla">
+ {data?.description}
+ </p>
+ </div>
+ </div>
+ )}
+
+ <div className="mt-10 flex flex-col gap-3">
+ {data?.chapters?.map((chapter) => (
+ <Link
+ key={chapter?.chapterId}
+ href={`/id/novel/read/?id=${chapter?.chapterId}`}
+ className="py-3 bg-secondary w-full px-5 rounded"
+ >
+ <div className="flex justify-between w-full">
+ <p className="font-bold font-karla">{chapter?.chapter}</p>
+ <p className="font-light font-karla">{chapter?.release}</p>
+ </div>
+ </Link>
+ ))}
+ </div>
+ <div className="w-full bg-secondary rounded-xl h-[200px] absolute inset-0 z-10" />
+ </div>
+ </div>
+ );
+}
+
+export const getServerSideProps: GetServerSideProps = async ({ params }) => {
+ const { id } = params || {};
+ const API = process.env.ID_API;
+ return {
+ props: {
+ id,
+ API,
+ },
+ };
+};
diff --git a/pages/id/novel/read/index.tsx b/pages/id/novel/read/index.tsx
new file mode 100644
index 0000000..5f36e54
--- /dev/null
+++ b/pages/id/novel/read/index.tsx
@@ -0,0 +1,115 @@
+import Link from "next/link";
+import { useSearchParams } from "next/navigation";
+import { useEffect, useState } from "react";
+import { Navbar } from "@/components/shared/NavBar";
+import MobileNav from "@/components/shared/MobileNav";
+import pls from "@/utils/request/index";
+
+interface IData {
+ novelTitle: string;
+ title: string;
+ navigation: {
+ next: string;
+ prev: string;
+ };
+ content: string;
+}
+
+export async function getServerSideProps() {
+ const API = process.env.ID_API;
+ return {
+ props: {
+ API,
+ },
+ };
+}
+
+export default function ReadNovel({ API }: { API: string }) {
+ const [data, setData] = useState<IData>();
+
+ const searchParams = useSearchParams();
+ const id = searchParams.get("id");
+ const mangaId = id?.split("/")[0];
+
+ useEffect(() => {
+ async function fetchData() {
+ if (id) {
+ const data = await pls.get(`${API}/api/novel/chapter/${id}`);
+ setData(data);
+ }
+ }
+ fetchData();
+
+ return () => {
+ setData(undefined);
+ };
+ }, [id]);
+
+ return (
+ <>
+ <Navbar withNav paddingY="py-2" scrollP={2} />
+ <MobileNav hideProfile />
+ <div className="w-screen flex flex-col items-center">
+ {/* {data && ( */}
+ <div className="flex items-center gap-5 w-full max-w-screen-lg px-5 mt-16 font-karla font-bold">
+ <div className="flex gap-2">
+ <Link
+ href={`/id/novel/read/?id=${data?.navigation?.prev}`}
+ className={`${
+ data?.navigation?.prev ? "" : "pointer-events-none opacity-60"
+ } py-1 px-2 bg-secondary rounded`}
+ >
+ prev
+ </Link>
+ <Link
+ href={`/id/novel/read/?id=${data?.navigation?.next}`}
+ className={`${
+ data?.navigation?.next ? "" : "pointer-events-none opacity-60"
+ } py-1 px-2 bg-secondary rounded`}
+ >
+ next
+ </Link>
+ </div>
+ <span>/</span>
+ <Link href={`/id/novel/${mangaId}`} className="text-lg line-clamp-1">
+ {data?.novelTitle}
+ </Link>
+ </div>
+ {/* )} */}
+ <div className="block mt-5">
+ <div className="px-5 w-full h-full max-w-screen-lg pointer-events-none select-none">
+ <p className="text-xl font-bold my-5">{data?.title}</p>
+ {data?.content && (
+ <p
+ dangerouslySetInnerHTML={{ __html: data?.content }}
+ className="space-y-5"
+ />
+ )}
+ </div>
+ </div>
+ {data?.content && (
+ <div className="px-5 py-10 w-full h-full max-w-screen-lg">
+ <div className="flex w-full gap-2">
+ <Link
+ href={`/id/novel/read/?id=${data?.navigation?.prev}`}
+ className={`${
+ data?.navigation?.prev ? "" : "pointer-events-none opacity-60"
+ } py-1 px-2 bg-secondary rounded`}
+ >
+ prev
+ </Link>
+ <Link
+ href={`/id/novel/read/?id=${data?.navigation?.next}`}
+ className={`${
+ data?.navigation?.next ? "" : "pointer-events-none opacity-60"
+ } py-1 px-2 bg-secondary rounded`}
+ >
+ next
+ </Link>
+ </div>
+ </div>
+ )}
+ </div>
+ </>
+ );
+}
diff --git a/pages/id/search.tsx b/pages/id/search.tsx
new file mode 100644
index 0000000..aa53fcd
--- /dev/null
+++ b/pages/id/search.tsx
@@ -0,0 +1,221 @@
+import Image from "next/image";
+import { Fragment, useEffect, useState } from "react";
+import {
+ CheckIcon,
+ ChevronDownIcon,
+ MagnifyingGlassIcon,
+} from "@heroicons/react/24/outline";
+import Link from "next/link";
+import { Combobox, Transition } from "@headlessui/react";
+import pls from "@/utils/request";
+
+const types = [
+ {
+ name: "Novel",
+ value: "novel",
+ },
+ {
+ name: "Manga",
+ value: "manga",
+ },
+];
+
+type DataType = {
+ id: string;
+ title: string;
+ img: string;
+ synonym?: string;
+ status?: string;
+ genres?: string;
+ release?: string;
+};
+
+export async function getServerSideProps() {
+ const API = process.env.ID_API;
+ return {
+ props: {
+ API,
+ },
+ };
+}
+
+export default function Search({ API }: { API: string }) {
+ const [data, setData] = useState<DataType[] | null>([]);
+ const [query, setQuery] = useState("a");
+
+ const [type, setType] = useState(types[0]);
+
+ const handleQuery = async (e: any) => {
+ e.preventDefault();
+ setData([]);
+
+ try {
+ const data = await pls.get(`${API}/api/${type.value}/search/${query}`);
+ setData(data);
+ } catch (error) {
+ setData(null);
+ }
+ };
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const data = await pls.get(`${API}/api/${type.value}/search/${query}`);
+ setData(data);
+ } catch (error) {
+ setData(null);
+ }
+ }
+ fetchData();
+ return () => {
+ setData(null);
+ };
+ }, [type?.value]);
+
+ useEffect(() => {
+ // run handleQuery when pressing enter
+ const handleEnter = (e: any) => {
+ if (e.key === "Enter") {
+ handleQuery(e);
+ }
+ };
+ window.addEventListener("keydown", handleEnter);
+
+ return () => {
+ window.removeEventListener("keydown", handleEnter);
+ };
+ }, [query, type?.value]);
+
+ const handleChange = (e: any) => {
+ setType(e);
+ setData(null);
+ };
+
+ return (
+ <div className="flex flex-col items-center">
+ <div className="w-full max-w-screen-lg px-5">
+ <div className="flex justify-between mt-16">
+ <div className="flex-1 max-w-[20%] items-center justify-end text-lg relative">
+ <Combobox value={type} onChange={(e) => handleChange(e)}>
+ <Combobox.Button className="h-full w-full gap-5 py-[2px] bg-secondary/70 rounded text-sm font-karla flex items-center justify-between px-2">
+ {type.name}
+ <ChevronDownIcon
+ className="h-5 w-5 text-gray-400"
+ aria-hidden="true"
+ />
+ </Combobox.Button>
+ <Transition
+ as={Fragment}
+ enter="transition ease-out duration-200"
+ enterFrom="transform opacity-0 scale-95 translate-y-5"
+ enterTo="transform opacity-100 scale-100"
+ leave="transition ease-in duration-75"
+ leaveFrom="transform opacity-100 scale-100"
+ leaveTo="transform opacity-0 scale-95 translate-y-5"
+ afterLeave={() => setQuery("")}
+ >
+ <Combobox.Options
+ className="absolute z-[55] mt-1 max-h-60 w-full rounded-md bg-secondary py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
+ style={{ scrollbarGutter: "stable" }}
+ >
+ {types.length === 0 && query !== "" ? (
+ <div className="relative cursor-default select-none py-2 px-4 text-gray-300">
+ Nothing found.
+ </div>
+ ) : (
+ types.map((item) => (
+ <Combobox.Option
+ key={item.value}
+ className={({ active }) =>
+ `relative cursor-pointer select-none py-2 px-2 mx-2 rounded-md ${
+ active ? "bg-white/5 text-white" : "text-gray-300"
+ }`
+ }
+ value={item}
+ >
+ {({ selected, active }) => (
+ <Fragment>
+ <span
+ className={`block truncate ${
+ selected
+ ? "font-medium text-white"
+ : "font-normal"
+ }`}
+ >
+ {item.name}
+ </span>
+ {selected ? (
+ <span
+ className={`absolute inset-y-0 right-0 flex items-center pl-3 pr-1 ${
+ active ? "text-white" : "text-action"
+ }`}
+ >
+ <CheckIcon
+ className="h-5 w-5"
+ aria-hidden="true"
+ />
+ </span>
+ ) : null}
+ </Fragment>
+ )}
+ </Combobox.Option>
+ ))
+ )}
+ </Combobox.Options>
+ </Transition>
+ </Combobox>
+ </div>
+ <form
+ onSubmit={handleQuery}
+ className="flex items-center justify-end relative space-x-2"
+ >
+ <input
+ type="text"
+ value={query}
+ onChange={(e) => setQuery(e.target.value)}
+ className="bg-secondary h-10 px-5 pr-16 rounded-lg text-sm focus:outline-none"
+ />
+ <button type="submit" className="text-white">
+ <MagnifyingGlassIcon className="h-6 w-6 text-white" />
+ </button>
+ </form>
+ </div>
+ <div className="mt-5 grid xxs:grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-6 gap-5 gap-y-5">
+ {data !== null
+ ? data?.map((x, index) => (
+ <div key={x.id + index} className="flex flex-col gap-2">
+ <Link
+ href={`/id/${type.value}/${x.id}`}
+ className="block relative overflow-hidden bg-secondary hover:scale-[1.03] scale-100 transition-all cursor-pointer duration-200 ease-out rounded"
+ style={{
+ paddingTop: "145%", // 2:3 aspect ratio (3/2 * 100%)
+ }}
+ >
+ {x.img && (
+ <Image
+ src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent(
+ x.img
+ )}${`&headers=${encodeURIComponent(
+ JSON.stringify({ Referer: "https://komikindo.tv/" })
+ )}`}`}
+ alt={x.title}
+ sizes="(min-width: 808px) 50vw, 100vw"
+ quality={100}
+ fill
+ className="object-cover"
+ />
+ )}
+ </Link>
+ <div>
+ <h1 className="line-clamp-2 font-karla font-bold">
+ {x.title}
+ </h1>
+ </div>
+ </div>
+ ))
+ : "No results found"}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/pages/index.js b/pages/index.tsx
index 25d5b20..25d5b20 100644
--- a/pages/index.js
+++ b/pages/index.tsx
diff --git a/prisma/user.js b/prisma/user.ts
index c2ba5fd..8a0d856 100644
--- a/prisma/user.js
+++ b/prisma/user.ts
@@ -1,9 +1,24 @@
-import { Prisma } from "@prisma/client";
-// const prisma = new PrismaClient();
+import { Prisma, UserProfile, WatchListEpisode } from "@prisma/client";
import { prisma } from "../lib/prisma";
-export const createUser = async (name) => {
+interface UpdateUserEpisodeParams {
+ name: string;
+ id: string;
+ watchId: string;
+ title: string;
+ image: string;
+ number: number;
+ duration: number;
+ timeWatched: number;
+ aniTitle: string;
+ provider: string;
+ nextId: string;
+ nextNumber: number;
+ dub: boolean;
+}
+
+export const createUser = async (name: string): Promise<UserProfile | null> => {
try {
const checkUser = await prisma.userProfile.findUnique({
where: {
@@ -36,9 +51,12 @@ export const createUser = async (name) => {
}
};
-export const updateUser = async (name, setting) => {
+export const updateUser = async (
+ name: string,
+ setting: any
+): Promise<{ name: string; setting: any } | null> => {
try {
- const user = await prisma.userProfile.updateMany({
+ await prisma.userProfile.updateMany({
where: {
name: name,
},
@@ -46,58 +64,17 @@ export const updateUser = async (name, setting) => {
setting,
},
});
- return user;
- // const checkAnime = await prisma.watchListItem.findUnique({
- // where: {
- // title: anime.title,
- // userProfileId: name,
- // },
- // });
- // if (checkAnime) {
- // const checkEpisode = await prisma.watchListEpisode.findUnique({
- // where: {
- // url: anime.id,
- // },
- // });
- // if (checkEpisode) {
- // return null;
- // } else {
- // const user = await prisma.watchListItem.update({
- // where: {
- // title: anime.title,
- // userProfileId: name,
- // },
- // });
- // }
- // } else {
- // const user = await prisma.userProfile.update({
- // where: { name: name },
- // data: {
- // watchList: {
- // create: {
- // title: anime.title,
- // episodes: {
- // create: {
- // url: anime.id,
- // },
- // },
- // },
- // },
- // },
- // include: {
- // watchList: true,
- // },
- // });
-
- // return user;
- // }
+ return { name: name, setting: setting };
} catch (error) {
console.error(error);
throw new Error("Error updating user");
}
};
-export const getUser = async (name, list = true) => {
+export const getUser = async (
+ name: string,
+ list = true
+): Promise<any | null> => {
try {
if (!name) {
const user = await prisma.userProfile.findMany({
@@ -127,7 +104,7 @@ export const getUser = async (name, list = true) => {
}
};
-export const deleteUser = async (name) => {
+export const deleteUser = async (name: string): Promise<UserProfile | null> => {
try {
const user = await prisma.userProfile.delete({
where: {
@@ -141,7 +118,11 @@ export const deleteUser = async (name) => {
}
};
-export const createList = async (name, id, title) => {
+export const createList = async (
+ name: string,
+ id: string,
+ title: string
+): Promise<UserProfile | null> => {
try {
const checkEpisode = await prisma.watchListEpisode.findFirst({
where: {
@@ -175,7 +156,10 @@ export const createList = async (name, id, title) => {
}
};
-export const getEpisode = async (name, id) => {
+export const getEpisode = async (
+ name: string,
+ id: string
+): Promise<WatchListEpisode[] | null> => {
try {
const episode = await prisma.watchListEpisode.findMany({
where: {
@@ -212,9 +196,9 @@ export const updateUserEpisode = async ({
nextId,
nextNumber,
dub,
-}) => {
+}: UpdateUserEpisodeParams) => {
try {
- const user = await prisma.watchListEpisode.updateMany({
+ await prisma.watchListEpisode.updateMany({
where: {
userProfileId: name,
watchId: watchId,
@@ -235,14 +219,17 @@ export const updateUserEpisode = async ({
},
});
- return user;
+ // return user;
} catch (error) {
console.error(error);
throw new Error("Error updating user episode");
}
};
-export const deleteEpisode = async (name, id) => {
+export const deleteEpisode = async (
+ name: string,
+ id: string
+): Promise<{ success?: boolean; message?: string } | null> => {
try {
const user = await prisma.watchListEpisode.deleteMany({
where: {
@@ -251,7 +238,7 @@ export const deleteEpisode = async (name, id) => {
},
});
if (user) {
- return user;
+ return { success: true };
} else {
return { message: "Episode not found" };
}
@@ -261,7 +248,10 @@ export const deleteEpisode = async (name, id) => {
}
};
-export const deleteList = async (name, id) => {
+export const deleteList = async (
+ name: string,
+ id: string
+): Promise<{ success?: boolean; message?: string } | null> => {
try {
const user = await prisma.watchListEpisode.deleteMany({
where: {
@@ -270,7 +260,7 @@ export const deleteList = async (name, id) => {
},
});
if (user) {
- return user;
+ return { success: true };
} else {
return { message: "Episode not found" };
}
diff --git a/public/icon-144x144.png b/public/icon-144x144.png
new file mode 100644
index 0000000..d2430c6
--- /dev/null
+++ b/public/icon-144x144.png
Binary files differ
diff --git a/public/manifest.json b/public/manifest.json
index cd77de2..5fe3e5d 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -9,6 +9,12 @@
"description": "Watch and Read your favorite Anime/Manga in one single app",
"icons": [
{
+ "src": "/icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..2c3f889
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Disallow: /en/anime/
+Disallow: /en/manga/
+Disallow: /admin/
+Disallow: /api/ \ No newline at end of file
diff --git a/styles/globals.css b/styles/globals.css
index 17ca472..10645f0 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -13,6 +13,11 @@ html {
-webkit-tap-highlight-color: transparent;
}
+:root {
+ --media-brand: 245 245 245;
+ --media-focus: 78 156 246;
+}
+
body {
@apply bg-primary scrollbar-hide text-txt;
}
@@ -413,7 +418,8 @@ pre code {
.next-button {
position: relative;
- @apply xs:w-28 xs:h-9 w-24 h-7 rounded-md font-karla shadow-xl text-black xs:text-[15px] text-xs md:text-sm flex-center hover:bg-[#b9b9b9];
+ @apply px-4 py-2 font-karla text-primary hover:bg-white/80 font-semibold;
+ /* @apply xs:w-28 xs:h-9 w-24 h-7 rounded-md font-karla shadow-xl text-black xs:text-[15px] text-xs md:text-sm flex-center hover:bg-[#b9b9b9]; */
background: #ffffff;
border-radius: 6px;
cursor: pointer;
@@ -432,7 +438,7 @@ pre code {
border-radius: 6px;
}
.next-button.progress::before {
- animation: progress 7s ease forwards;
+ animation: progress 7s linear forwards;
}
@keyframes progress {
0% {
@@ -557,3 +563,27 @@ pre code {
left: unset;
}
}
+
+[data-media-player] {
+ height: 100%;
+ display: block;
+}
+
+[data-media-provider] {
+ height: 100%;
+ border: none;
+}
+
+[data-media-provider] video {
+ height: 100%;
+ object-fit: contain;
+ display: block;
+}
+
+.chat {
+ @apply flex flex-col gap-[10px];
+}
+
+.chat > span {
+ @apply font-karla w-full italic text-white/70;
+}
diff --git a/tailwind.config.js b/tailwind.config.cjs
index 13e9999..e072608 100644
--- a/tailwind.config.js
+++ b/tailwind.config.cjs
@@ -77,5 +77,18 @@ module.exports = {
nocompatible: true,
}),
require("tailwind-scrollbar-hide"),
+ require("@vidstack/react/tailwind.cjs")({
+ // Change the media variants prefix.
+ prefix: "media",
+ }),
+ require("tailwindcss-animate"),
+ customVariants,
],
};
+
+function customVariants({ addVariant, matchVariant }) {
+ // Strict version of `.group` to help with nesting.
+ matchVariant("parent-data", (value) => `.parent[data-${value}] > &`);
+ addVariant("hocus", ["&:hover", "&:focus-visible"]);
+ addVariant("group-hocus", [".group:hover &", ".group:focus-visible &"]);
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..2838c72
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "baseUrl": ".",
+ "paths": {
+ "@/components/*": ["components/*"],
+ "@/utils/*": ["utils/*"],
+ "@/lib/*": ["lib/*"],
+ "@/prisma/*": ["prisma/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ "pages/api/v2/episode/[id].tsx",
+ "utils/schedulesUtils.ts",
+ "pages/middleware.js",
+ "pages/api/auth/[...nextauth].ts",
+ "components/anime/mobile/reused/infoChip.tsx"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/types/api/AnifyEpisode.ts b/types/api/AnifyEpisode.ts
new file mode 100644
index 0000000..93b7e5d
--- /dev/null
+++ b/types/api/AnifyEpisode.ts
@@ -0,0 +1,16 @@
+export interface AnifyEpisode {
+ providerId: string;
+ episodes: Episode[];
+}
+
+export interface Episode {
+ id: string;
+ number: number;
+ title: string;
+ isFiller: boolean;
+ img: null;
+ hasDub: boolean;
+ description: null;
+ rating: null;
+ updatedAt: number;
+}
diff --git a/types/api/ConsumetInfo.ts b/types/api/ConsumetInfo.ts
new file mode 100644
index 0000000..946f64a
--- /dev/null
+++ b/types/api/ConsumetInfo.ts
@@ -0,0 +1,154 @@
+export interface ConsumetInfo {
+ message: string;
+ length: number;
+ id: string;
+ title: Title;
+ malId: number;
+ synonyms: string[];
+ isLicensed: boolean;
+ isAdult: boolean;
+ countryOfOrigin: string;
+ trailer: Trailer;
+ image: string;
+ popularity: number;
+ color: string;
+ cover: string;
+ description: string;
+ status: Status;
+ releaseDate: number;
+ startDate: EndDateClass;
+ endDate: EndDateClass;
+ totalEpisodes: number;
+ currentEpisode: number;
+ rating: number;
+ duration: number;
+ genres: string[];
+ season: string;
+ studios: string[];
+ subOrDub: string;
+ type: RecommendationType;
+ recommendations: Ation[];
+ characters: Character[];
+ relations: Ation[];
+ mappings: Mapping[];
+ artwork: Artwork[];
+ episodes: Episode[];
+}
+
+export interface Artwork {
+ img: string;
+ type: ArtworkType;
+ providerId: ProviderID;
+}
+
+export enum ProviderID {
+ Anilist = "anilist",
+ Mal = "mal",
+ Tvdb = "tvdb",
+}
+
+export enum ArtworkType {
+ Banner = "banner",
+ ClearArt = "clear_art",
+ ClearLogo = "clear_logo",
+ Icon = "icon",
+ Poster = "poster",
+ TopBanner = "top_banner",
+}
+
+export interface Character {
+ id: number;
+ role: Role;
+ name: Name;
+ image: string;
+ voiceActors: VoiceActor[];
+}
+
+export interface Name {
+ first: string;
+ last: null | string;
+ full: string;
+ native: null | string;
+ userPreferred: string;
+}
+
+export enum Role {
+ Main = "MAIN",
+ Supporting = "SUPPORTING",
+}
+
+export interface VoiceActor {
+ id: number;
+ language: Language;
+ name: Name;
+ image: string;
+}
+
+export enum Language {
+ English = "English",
+ French = "French",
+ German = "German",
+ Japanese = "Japanese",
+ Portuguese = "Portuguese",
+ Spanish = "Spanish",
+}
+
+export interface EndDateClass {
+ year: number;
+ month: number;
+ day: number;
+}
+
+export interface Episode {
+ id: string;
+ title: string;
+ description: null;
+ number: number;
+ image: string;
+ airDate: null;
+}
+
+export interface Mapping {
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+}
+
+export interface Ation {
+ id: number;
+ malId: number;
+ title: Title;
+ status: Status;
+ episodes: number | null;
+ image: string;
+ cover: string;
+ rating: number;
+ type: RecommendationType;
+ relationType?: string;
+ color?: string;
+}
+
+export enum Status {
+ Completed = "Completed",
+}
+
+export interface Title {
+ romaji: string;
+ english: string;
+ native: string;
+ userPreferred?: string;
+}
+
+export enum RecommendationType {
+ Manga = "MANGA",
+ Ona = "ONA",
+ Tv = "TV",
+ TvShort = "TV_SHORT",
+}
+
+export interface Trailer {
+ id: string;
+ site: string;
+ thumbnail: string;
+}
diff --git a/types/api/Episode.ts b/types/api/Episode.ts
new file mode 100644
index 0000000..1a7440b
--- /dev/null
+++ b/types/api/Episode.ts
@@ -0,0 +1,19 @@
+export interface EpisodeData {
+ map?: boolean;
+ providerId: string;
+ episodes: Episode[];
+}
+
+export interface Episode {
+ id: string;
+ title: string;
+ img: string;
+ number: number;
+ createdAt?: Date;
+ description: string;
+ url?: string;
+ isFiller?: boolean;
+ hasDub?: boolean;
+ rating?: null;
+ updatedAt?: number;
+}
diff --git a/types/episodes/AnifyRecentEpisode.ts b/types/episodes/AnifyRecentEpisode.ts
new file mode 100644
index 0000000..b4dc466
--- /dev/null
+++ b/types/episodes/AnifyRecentEpisode.ts
@@ -0,0 +1,91 @@
+export interface AnifyRecentEpisode {
+ id: string;
+ slug: string;
+ coverImage: string;
+ bannerImage: string;
+ trailer: string;
+ status: string;
+ season: string;
+ title: Title;
+ currentEpisode: number;
+ mappings?: MappingsEntity[] | null;
+ synonyms?: string[] | null;
+ countryOfOrigin: string;
+ description: string;
+ duration: number;
+ color: string;
+ year: number;
+ rating: RatingOrPopularity;
+ popularity: RatingOrPopularity;
+ type: string;
+ format: string;
+ relations?: RelationsEntity[] | null;
+ totalEpisodes: number;
+ genres?: string[] | null;
+ tags?: string[] | null;
+ episodes: Episodes;
+ averageRating: number;
+ averagePopularity: number;
+ artwork?: ArtworkEntity[] | null;
+ characters?: CharactersEntity[] | null;
+}
+export interface Title {
+ native: string;
+ romaji: string;
+ english: string;
+}
+export interface MappingsEntity {
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+}
+export interface RatingOrPopularity {
+ anidb: number;
+ anilist: number;
+}
+export interface RelationsEntity {
+ id: string;
+ type: string;
+ title: Title;
+ format: string;
+ relationType: string;
+}
+export interface Episodes {
+ data?: DataEntity[] | null;
+ latest: Latest;
+}
+export interface DataEntity {
+ episodes?: EpisodesEntity[] | null;
+ providerId: string;
+}
+export interface EpisodesEntity {
+ id: string;
+ img?: null;
+ title: string;
+ hasDub: boolean;
+ number: number;
+ rating?: null;
+ isFiller: boolean;
+ updatedAt: number;
+ description?: null;
+}
+export interface Latest {
+ updatedAt: number;
+ latestTitle: string;
+ latestEpisode: number;
+}
+export interface ArtworkEntity {
+ img: string;
+ type: string;
+ providerId: string;
+}
+export interface CharactersEntity {
+ name: string;
+ image: string;
+ voiceActor: VoiceActor;
+}
+export interface VoiceActor {
+ name: string;
+ image: string;
+}
diff --git a/types/episodes/ConsumetInfo.ts b/types/episodes/ConsumetInfo.ts
new file mode 100644
index 0000000..811e90b
--- /dev/null
+++ b/types/episodes/ConsumetInfo.ts
@@ -0,0 +1,126 @@
+// Consumets types
+export interface ConsumetInfo {
+ id: string;
+ title: Title;
+ malId: number;
+ synonyms?: null[] | null;
+ isLicensed: boolean;
+ isAdult: boolean;
+ countryOfOrigin: string;
+ trailer: Trailer;
+ image: string;
+ popularity: number;
+ color: string;
+ cover: string;
+ description: string;
+ status: string;
+ releaseDate: number;
+ startDate: StartDateOrEndDate;
+ endDate: StartDateOrEndDate;
+ totalEpisodes: number;
+ currentEpisode: number;
+ rating: number;
+ duration: number;
+ genres?: string[] | null;
+ season: string;
+ studios?: string[] | null;
+ subOrDub: string;
+ type: string;
+ recommendations?: RecommendationsEntity[] | null;
+ characters?: CharactersEntityConsumet[] | null;
+ relations?: RelationsEntityConsumet[] | null;
+ mappings?: MappingsEntity[] | null;
+ artwork?: ArtworkEntity[] | null;
+ episodes?: EpisodesEntity[] | null;
+}
+export interface Trailer {
+ id: string;
+ site: string;
+ thumbnail: string;
+}
+export interface StartDateOrEndDate {
+ year: number;
+ month: number;
+ day: number;
+}
+export interface RecommendationsEntity {
+ id: number;
+ malId: number;
+ title: Title1;
+ status: string;
+ episodes: number;
+ image: string;
+ cover: string;
+ rating: number;
+ type: string;
+}
+export interface Title1 {
+ romaji: string;
+ english?: string | null;
+ native: string;
+ userPreferred: string;
+}
+export interface Title {
+ native: string;
+ romaji: string;
+ english: string;
+}
+export interface CharactersEntityConsumet {
+ id: number;
+ role: string;
+ name: Name;
+ image: string;
+ voiceActors?: VoiceActorsEntity[] | null;
+}
+export interface Name {
+ first: string;
+ last?: string | null;
+ full: string;
+ native: string;
+ userPreferred: string;
+}
+export interface VoiceActorsEntity {
+ id: number;
+ language: string;
+ name: Name1;
+ image: string;
+}
+export interface Name1 {
+ first: string;
+ last: string;
+ full: string;
+ native?: string | null;
+ userPreferred: string;
+}
+export interface RelationsEntityConsumet {
+ id: number;
+ relationType: string;
+ malId: number;
+ title: Title1;
+ status: string;
+ episodes?: number | null;
+ image: string;
+ color: string;
+ type: string;
+ cover: string;
+ rating: number;
+}
+export interface MappingsEntity {
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+}
+export interface ArtworkEntity {
+ img: string;
+ type: string;
+ providerId: string;
+}
+export interface EpisodesEntity {
+ id: string;
+ title: string;
+ description?: null;
+ number: number;
+ image: string;
+ airDate?: null;
+}
diff --git a/types/episodes/Sessions.ts b/types/episodes/Sessions.ts
new file mode 100644
index 0000000..cb92fe7
--- /dev/null
+++ b/types/episodes/Sessions.ts
@@ -0,0 +1,30 @@
+export interface Sessions {
+ user: User;
+ expires: string;
+}
+
+export interface User {
+ name: string;
+ picture: Picture;
+ sub: string;
+ token: string;
+ id: number;
+ image: Image;
+ list: string[];
+ version: string;
+ iat: number;
+ exp: number;
+ jti: string;
+}
+
+export interface Picture {
+ large: string;
+ medium: string;
+ __typename: string;
+}
+
+export interface Image {
+ large: string;
+ medium: string;
+ __typename: string;
+}
diff --git a/types/episodes/TrackData.ts b/types/episodes/TrackData.ts
new file mode 100644
index 0000000..143ea23
--- /dev/null
+++ b/types/episodes/TrackData.ts
@@ -0,0 +1,70 @@
+export type TrackData = {
+ provider: string;
+ defaultQuality: DefaultQuality;
+ subtitles: Subtitle[];
+ thumbnails: string;
+ epiData: EpiData;
+ skip: Skip;
+};
+
+export interface DefaultQuality {
+ url: string;
+ headers: Headers;
+}
+
+export interface Headers {}
+
+export interface Subtitle {
+ src: string;
+ label: string;
+ kind: "subtitles" | "captions" | "descriptions" | "chapters" | "metadata";
+ default?: boolean;
+}
+
+export interface EpiData {
+ sources: Source[];
+ subtitles: Subtitle2[];
+ audio: any[];
+ intro: Intro;
+ outro: Outro;
+ headers: Headers2;
+}
+
+export interface Source {
+ url: string;
+ quality: string;
+}
+
+export interface Subtitle2 {
+ url: string;
+ lang: string;
+}
+
+export interface Intro {
+ start: number;
+ end: number;
+}
+
+export interface Outro {
+ start: number;
+ end: number;
+}
+
+export interface Headers2 {}
+
+export interface Skip {
+ op: Op;
+ ed: any;
+}
+
+export interface Op {
+ interval: Interval;
+ skipType: string;
+ skipId: string;
+ episodeLength: number;
+}
+
+export interface Interval {
+ startTime: number;
+ endTime: number;
+}
diff --git a/types/index.tsx b/types/index.tsx
new file mode 100644
index 0000000..36f6aba
--- /dev/null
+++ b/types/index.tsx
@@ -0,0 +1,17 @@
+import { AnifyEpisode } from "./api/AnifyEpisode";
+import { EpisodeData } from "./api/Episode";
+import { ConsumetInfo as APIConsumetInfo } from "./api/ConsumetInfo";
+import { AnifyRecentEpisode } from "./episodes/AnifyRecentEpisode";
+import { ConsumetInfo } from "./episodes/ConsumetInfo";
+import { Sessions } from "./episodes/Sessions";
+import { TrackData } from "./episodes/TrackData";
+
+export type {
+ AnifyEpisode,
+ EpisodeData,
+ APIConsumetInfo,
+ ConsumetInfo,
+ AnifyRecentEpisode,
+ Sessions,
+ TrackData,
+};
diff --git a/types/info/AnifySearchAdvanceTypes.ts b/types/info/AnifySearchAdvanceTypes.ts
new file mode 100644
index 0000000..18bc108
--- /dev/null
+++ b/types/info/AnifySearchAdvanceTypes.ts
@@ -0,0 +1,87 @@
+export type AnifySearchAdvanceTypes = {
+ total: number;
+ lastPage: number;
+ results: Array<{
+ id: string;
+ slug: string;
+ coverImage: string;
+ bannerImage: string;
+ status: string;
+ title: {
+ native: string;
+ romaji: string;
+ english: string;
+ };
+ duration: number;
+ mappings: Array<{
+ id: string;
+ providerId: string;
+ similarity: number;
+ providerType: string;
+ }>;
+ synonyms: Array<string>;
+ countryOfOrigin: string;
+ description: string;
+ color: string;
+ year: number;
+ rating: {
+ comick: number;
+ anilist: number;
+ };
+ popularity: {
+ comick: number;
+ anilist: number;
+ };
+ type: string;
+ format: string;
+ relations: Array<{
+ id: string;
+ type: string;
+ title: {
+ native: string;
+ romaji: string;
+ english: string;
+ };
+ format: string;
+ relationType: string;
+ }>;
+ currentChapter: any;
+ totalChapters: number;
+ totalVolumes: number;
+ genres: Array<string>;
+ tags: Array<string>;
+ chapters: {
+ data: Array<{
+ chapters: Array<{
+ id: string;
+ title: string;
+ number: number;
+ rating: any;
+ mixdrop?: string;
+ updatedAt: number;
+ }>;
+ providerId: string;
+ }>;
+ latest: {
+ updatedAt: number;
+ latestTitle: string;
+ latestChapter: number;
+ };
+ };
+ averageRating: number;
+ averagePopularity: number;
+ artwork: Array<{
+ img: string;
+ type: string;
+ providerId: string;
+ }>;
+ characters: Array<{
+ name: string;
+ image: string;
+ voiceActor: {
+ name: any;
+ image: any;
+ };
+ }>;
+ }>;
+};
diff --git a/types/info/AnilistInfoTypes.ts b/types/info/AnilistInfoTypes.ts
new file mode 100644
index 0000000..f6cfb6f
--- /dev/null
+++ b/types/info/AnilistInfoTypes.ts
@@ -0,0 +1,138 @@
+export interface AniListInfoTypes {
+ mediaListEntry: MediaListEntry;
+ id: number;
+ type: string;
+ format: string;
+ title: Title;
+ coverImage: CoverImage;
+ startDate: StartDate;
+ bannerImage: string;
+ description: string;
+ episodes: any;
+ nextAiringEpisode: any;
+ averageScore: number;
+ popularity: number;
+ status: string;
+ genres: string[];
+ season: any;
+ studios: Studios;
+ seasonYear: any;
+ duration: any;
+ relations: Relations;
+ recommendations: Recommendations;
+ characters: Characters;
+}
+
+interface Studios {
+ edges: Studio[];
+}
+
+interface Studio {
+ isMain: boolean;
+ node: Node4;
+}
+
+interface Node4 {
+ id: number;
+ name: string;
+}
+
+export interface MediaListEntry {
+ status: string;
+ progress: number;
+ progressVolumes: number;
+}
+
+export interface Title {
+ romaji: string;
+ english: string;
+ native: string;
+}
+
+export interface CoverImage {
+ extraLarge: string;
+ large: string;
+ color: string;
+}
+
+export interface StartDate {
+ year: number;
+ month: number;
+}
+
+export interface Relations {
+ edges: Edge[];
+}
+
+export interface Edge {
+ id: number;
+ relationType: string;
+ node: Node;
+}
+
+export interface Node {
+ id: number;
+ title: Title2;
+ format: string;
+ type: string;
+ status: string;
+ bannerImage?: string;
+ coverImage: CoverImage2;
+}
+
+export interface Title2 {
+ userPreferred: string;
+}
+
+export interface CoverImage2 {
+ extraLarge: string;
+ color: string;
+}
+
+export interface Recommendations {
+ nodes: Node2[];
+}
+
+export interface Node2 {
+ mediaRecommendation: MediaRecommendation;
+}
+
+export interface MediaRecommendation {
+ id: number;
+ title: Title3;
+ coverImage: CoverImage3;
+}
+
+export interface Title3 {
+ romaji: string;
+}
+
+export interface CoverImage3 {
+ extraLarge: string;
+ large: string;
+}
+
+export interface Characters {
+ edges: Edge2[];
+}
+
+export interface Edge2 {
+ role: string;
+ node: Node3;
+}
+
+export interface Node3 {
+ id: number;
+ image: Image;
+ name: Name;
+}
+
+export interface Image {
+ large: string;
+ medium: string;
+}
+
+export interface Name {
+ full: string;
+ userPreferred: string;
+}
diff --git a/utils/appendMetaToEpisodes.js b/utils/appendMetaToEpisodes.ts
index 197788b..5f74df3 100644
--- a/utils/appendMetaToEpisodes.js
+++ b/utils/appendMetaToEpisodes.ts
@@ -1,8 +1,31 @@
-async function appendMetaToEpisodes(episodesData, images) {
+type Image = {
+ number?: number;
+ episode?: number;
+ img: string;
+ title: string;
+ description: string;
+};
+
+type Episode = {
+ number: number;
+ img?: string;
+ title?: string;
+ description?: string;
+};
+
+type ProviderEpisodes = {
+ episodes: Episode[];
+};
+
+async function appendMetaToEpisodes(
+ episodesData: ProviderEpisodes[],
+ images: Image[]
+): Promise<ProviderEpisodes[]> {
// Create a dictionary for faster lookup of images based on episode number
- const episodeImages = {};
+ const episodeImages: { [key: number]: Image } = {};
images.forEach((image) => {
- episodeImages[image.number || image.episode] = image;
+ image.episode && (episodeImages[image.episode] = image);
+ image.number && (episodeImages[image.number] = image);
});
// Iterate through each provider's episodes data
diff --git a/utils/combineImages.js b/utils/combineImages.ts
index abf34ed..01b7ef3 100644
--- a/utils/combineImages.js
+++ b/utils/combineImages.ts
@@ -1,6 +1,23 @@
-async function appendImagesToEpisodes(episodesData, images) {
+interface Image {
+ episode: number;
+ img: string;
+}
+
+interface Episode {
+ number: number;
+ img?: string;
+}
+
+interface ProviderEpisodes {
+ episodes: Episode[];
+}
+
+async function appendImagesToEpisodes(
+ episodesData: ProviderEpisodes[],
+ images: Image[]
+) {
// Create a dictionary for faster lookup of images based on episode number
- const episodeImages = {};
+ const episodeImages: { [key: number]: string } = {};
images.forEach((image) => {
episodeImages[image.episode] = image.img;
});
diff --git a/utils/getFormat.js b/utils/getFormat.ts
index 9a2e3e3..7f3eece 100644
--- a/utils/getFormat.js
+++ b/utils/getFormat.ts
@@ -11,7 +11,7 @@ const data = [
{ name: "One Shot", value: "ONE_SHOT" },
];
-export function getFormat(format) {
+export function getFormat(format: string) {
const results = data.find((item) => item.value === format);
return results?.name;
}
diff --git a/utils/getGreetings.js b/utils/getGreetings.ts
index 1dd2a53..1dd2a53 100644
--- a/utils/getGreetings.js
+++ b/utils/getGreetings.ts
diff --git a/utils/getRedisWithPrefix.js b/utils/getRedisWithPrefix.ts
index b85589b..dacf78e 100644
--- a/utils/getRedisWithPrefix.js
+++ b/utils/getRedisWithPrefix.ts
@@ -1,6 +1,6 @@
import { redis } from "@/lib/redis";
-export async function getValuesWithPrefix(prefix) {
+export async function getValuesWithPrefix(prefix: string) {
let cursor = "0"; // Start at the beginning of the keyspace
let values = [];
@@ -16,7 +16,9 @@ export async function getValuesWithPrefix(prefix) {
// Retrieve values for matching keys and add them to the array
for (const key of matchingKeys) {
const value = await redis.get(key);
- values.push(JSON.parse(value));
+ if (value !== null) {
+ values.push(JSON.parse(value));
+ }
}
// Update the cursor for the next iteration
@@ -26,7 +28,7 @@ export async function getValuesWithPrefix(prefix) {
return values;
}
-export async function countKeysWithPrefix(prefix) {
+export async function countKeysWithPrefix(prefix: string) {
let cursor = "0"; // Start at the beginning of the keyspace
let count = 0;
@@ -67,10 +69,10 @@ export async function getKeysWithNumericKeys() {
const allKeys = await redis.keys("*"); // Fetch all keys in Redis
const numericKeys = allKeys.filter((key) => /^\d+$/.test(key)); // Filter keys that contain only numbers
- const values = [];
+ const values: any[] = [];
for (const key of numericKeys) {
- const value = await redis.del(key);
+ await redis.del(key);
}
return values;
diff --git a/utils/getTimes.js b/utils/getTimes.ts
index 95df803..c3fe0ad 100644
--- a/utils/getTimes.js
+++ b/utils/getTimes.ts
@@ -1,4 +1,4 @@
-export function convertUnixToTime(timestamp) {
+export function convertUnixToTime(timestamp: number) {
const date = new Date(timestamp);
const hours = date.getHours();
const minutes = date.getMinutes();
@@ -34,7 +34,7 @@ export function getCurrentSeason() {
}
}
-export function convertUnixToCountdown(time) {
+export function convertUnixToCountdown(time: number) {
let date = new Date(time * 1000);
let days = date.getDay();
let hours = date.getHours();
@@ -57,7 +57,7 @@ export function convertUnixToCountdown(time) {
return countdown.trim();
}
-export function convertSecondsToTime(sec) {
+export function convertSecondsToTime(sec: number) {
let days = Math.floor(sec / (3600 * 24));
let hours = Math.floor((sec % (3600 * 24)) / 3600);
let minutes = Math.floor((sec % 3600) / 60);
@@ -85,8 +85,8 @@ export function convertSecondsToTime(sec) {
}
// Function to convert timestamp to AM/PM time format
-export const timeStamptoAMPM = (timestamp) => {
- const date = new Date(timestamp * 1000);
+export const timeStamptoAMPM = (timestamp: number | string) => {
+ const date = new Date(Number(timestamp) * 1000);
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? "PM" : "AM";
@@ -95,19 +95,18 @@ export const timeStamptoAMPM = (timestamp) => {
return `${formattedHours}:${minutes.toString().padStart(2, "0")} ${ampm}`;
};
-export const timeStamptoHour = (timestamp) => {
- const options = { hour: "numeric", minute: "numeric", hour12: true };
+export const timeStamptoHour = (timestamp: number) => {
const currentTime = new Date().getTime() / 1000;
const formattedTime = new Date(timestamp * 1000).toLocaleTimeString(
undefined,
- options
+ { hour: "numeric", minute: "numeric", hour12: true }
);
const status = timestamp <= currentTime ? "aired" : "airing";
return `${status} at ${formattedTime}`;
};
-export function unixTimestampToRelativeTime(unixTimestamp) {
+export function unixTimestampToRelativeTime(unixTimestamp: number) {
const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
let secondsDifference = now - unixTimestamp;
@@ -135,9 +134,26 @@ export function unixTimestampToRelativeTime(unixTimestamp) {
return "just now";
}
-export function unixToSeconds(unixTimestamp) {
+export function unixToSeconds(unixTimestamp: number) {
const now = Math.floor(Date.now() / 1000); // Current Unix timestamp in seconds
const secondsAgo = now - unixTimestamp;
return secondsAgo;
}
+
+export function realTimeCountdown(secondsLeft: number): string {
+ let countdown = "";
+ const intervalId = setInterval(() => {
+ secondsLeft--;
+ const hours = Math.floor(secondsLeft / 3600);
+ const minutes = Math.floor((secondsLeft % 3600) / 60);
+ const seconds = secondsLeft % 60;
+ countdown = `${hours.toString().padStart(2, "0")}:${minutes
+ .toString()
+ .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
+ if (secondsLeft <= 0) {
+ clearInterval(intervalId);
+ }
+ }, 1000);
+ return countdown;
+}
diff --git a/utils/imageUtils.js b/utils/imageUtils.ts
index 8025d5b..6220134 100644
--- a/utils/imageUtils.js
+++ b/utils/imageUtils.ts
@@ -1,4 +1,4 @@
-export function getHeaders(providerId) {
+export function getHeaders(providerId: string) {
switch (providerId) {
case "mangahere":
return { Referer: "https://mangahere.org" };
@@ -21,12 +21,14 @@ export function getRandomId() {
return Math.random().toString(36).substr(2, 9);
}
-export function truncateImgUrl(url) {
+export function truncateImgUrl(url: string | undefined) {
+ if (!url) return null;
+
// Find the index of .png if not found find the index of .jpg
let index =
url?.indexOf(".png") !== -1 ? url?.indexOf(".png") : url?.indexOf(".jpg");
- if (index !== -1) {
+ if (index && index !== -1) {
// If .png or .jpg is found
url = url?.slice(0, index + 4); // Slice the string from the start to the index of .png or .jpg plus 4 (the length of .png or .jpg)
} else {
@@ -36,3 +38,18 @@ export function truncateImgUrl(url) {
return url;
}
+
+export function parseImageProxy(
+ url: string | undefined | null,
+ providerId: string | undefined
+) {
+ if (!url) return;
+
+ return providerId
+ ? `https://aoi.moopa.live/utils/image-proxy?url=${truncateImgUrl(
+ url
+ )}${`&headers=${encodeURIComponent(
+ JSON.stringify({ Referer: providerId })
+ )}`}`
+ : url;
+}
diff --git a/utils/parseMetaData.ts b/utils/parseMetaData.ts
new file mode 100644
index 0000000..597c21c
--- /dev/null
+++ b/utils/parseMetaData.ts
@@ -0,0 +1,36 @@
+type Episode = {
+ id: string;
+ description: string | null;
+ hasDub: boolean;
+ img: string | null;
+ isFiller: boolean;
+ number: number;
+ rating: number | null;
+ title: string;
+ updatedAt: number;
+};
+
+type Provider = {
+ providerId: string;
+ data: Episode[];
+};
+
+export function getProviderWithMostEpisodesAndImage(
+ data: Provider[]
+): Provider | null {
+ let maxEpisodesProvider: Provider | null = null;
+
+ for (const provider of data) {
+ if (
+ !maxEpisodesProvider ||
+ provider.data.length > maxEpisodesProvider.data.length
+ ) {
+ const hasImage = provider.data.some((episode) => episode.img !== null);
+ if (hasImage) {
+ maxEpisodesProvider = provider;
+ }
+ }
+ }
+
+ return maxEpisodesProvider;
+}
diff --git a/utils/request/index.ts b/utils/request/index.ts
new file mode 100644
index 0000000..854ef2b
--- /dev/null
+++ b/utils/request/index.ts
@@ -0,0 +1,111 @@
+import axios, { AxiosRequestConfig } from "axios";
+import { getSession } from "next-auth/react";
+import { toast } from "sonner";
+
+function isAnilist(url: string | undefined): boolean {
+ return url?.includes("anilist.co") ?? false;
+}
+
+interface RequestOption extends RequestInit {
+ headers?: {
+ "Content-Type"?: string;
+ Authorization?: string;
+ };
+}
+
+const pls = {
+ // GET request handler
+ async get(
+ url: string,
+ options?: AxiosRequestConfig,
+ ctx?: any
+ ): Promise<any> {
+ try {
+ const session: any | null = isAnilist(url) ? await getSession(ctx) : null;
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await axios.get(url, { ...options, signal });
+ return response.data;
+ } catch (error: any) {
+ handleError(error);
+ // throw error;
+ }
+ },
+
+ // POST request handler
+ async post(url: string, options: RequestOption, ctx?: any): Promise<any> {
+ try {
+ const session: any | null = await getSession(ctx);
+ const accessToken: string | undefined = session?.user?.token;
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(accessToken &&
+ isAnilist(url) && { Authorization: `Bearer ${accessToken}` }),
+ },
+ ...options,
+ signal,
+ });
+
+ const data = await response.json();
+ return [data, session];
+ } catch (error: any) {
+ handleError(error);
+ // throw error;
+ }
+ },
+};
+
+function handleError(error: {
+ response: { status: any; data: any };
+ message: any;
+}) {
+ console.log(error);
+ if (error.response) {
+ const { status, data } = error.response;
+ switch (status) {
+ case 400:
+ toast.error("400 Bad request", {
+ description: data?.message || error.message,
+ });
+ break;
+ case 401:
+ toast.error("401 Unauthorized", {
+ description: data?.message || error.message,
+ });
+ break;
+ case 403:
+ toast.error("403 Forbidden", {
+ description: data?.message || error.message,
+ });
+ break;
+ case 404:
+ toast.error(`Resource not found - 404`, {
+ description: data?.message || error.message,
+ });
+ break;
+ case 500:
+ toast.error("500 Internal server error", {
+ description: data?.message || error.message,
+ });
+ break;
+ default:
+ toast.error("An error occurred", {
+ description: data?.message || error.message,
+ });
+ break;
+ }
+
+ if (data && data.message) {
+ console.error("Error message:", data.message);
+ }
+ }
+}
+
+export default pls;
diff --git a/utils/schedulesUtils.js b/utils/schedulesUtils.ts
index cb8c474..606e3fa 100644
--- a/utils/schedulesUtils.js
+++ b/utils/schedulesUtils.ts
@@ -1,6 +1,20 @@
-// Function to transform the schedule data into the desired format
-export const transformSchedule = (schedule) => {
- const formattedSchedule = {};
+interface ScheduleItem {
+ airingAt: string;
+ // Add other properties of ScheduleItem if available
+}
+
+interface Schedule {
+ [day: string]: ScheduleItem[];
+}
+
+interface FormattedSchedule {
+ [day: string]: {
+ [time: string]: ScheduleItem[];
+ };
+}
+
+export const transformSchedule = (schedule: Schedule): FormattedSchedule => {
+ const formattedSchedule: FormattedSchedule = {};
for (const day of Object.keys(schedule)) {
formattedSchedule[day] = {};
@@ -19,8 +33,10 @@ export const transformSchedule = (schedule) => {
return formattedSchedule;
};
-export const sortScheduleByDay = (schedule) => {
- const daysOfWeek = [
+export const sortScheduleByDay = (
+ schedule: FormattedSchedule
+): FormattedSchedule => {
+ const daysOfWeek: string[] = [
"Saturday",
"Sunday",
"Monday",
@@ -30,17 +46,14 @@ export const sortScheduleByDay = (schedule) => {
"Friday",
];
- // Get the current day of the week (0 = Sunday, 1 = Monday, ...)
- const currentDay = new Date().getDay();
+ const currentDay: number = new Date().getDay();
- // Reorder days of the week to start with today
- const orderedDays = [
+ const orderedDays: string[] = [
...daysOfWeek.slice(currentDay),
...daysOfWeek.slice(0, currentDay),
];
- // Create a new object with sorted days
- const sortedSchedule = {};
+ const sortedSchedule: FormattedSchedule = {};
orderedDays.forEach((day) => {
if (schedule[day]) {
sortedSchedule[day] = schedule[day];
@@ -50,34 +63,34 @@ export const sortScheduleByDay = (schedule) => {
return sortedSchedule;
};
-export const filterScheduleByDay = (sortedSchedule, filterDay) => {
+export const filterScheduleByDay = (
+ sortedSchedule: FormattedSchedule,
+ filterDay: string
+): FormattedSchedule => {
if (filterDay === "All") return sortedSchedule;
- // Create a new object to store the filtered schedules
- const filteredSchedule = {};
- // Iterate through the keys (days) in sortedSchedule
+ const filteredSchedule: FormattedSchedule = {};
+
for (const day in sortedSchedule) {
- // Check if the current day matches the filterDay
if (day === filterDay) {
- // If it matches, add the schedules for that day to the filteredSchedule object
filteredSchedule[day] = sortedSchedule[day];
}
}
- // Return the filtered schedule
return filteredSchedule;
};
-export const filterFormattedSchedule = (formattedSchedule, filterDay) => {
+export const filterFormattedSchedule = (
+ formattedSchedule: FormattedSchedule,
+ filterDay: string
+): FormattedSchedule => {
if (filterDay === "All") return formattedSchedule;
- // Check if the selected day exists in the formattedSchedule
if (formattedSchedule.hasOwnProperty(filterDay)) {
return {
[filterDay]: formattedSchedule[filterDay],
};
}
- // If the selected day does not exist, return an empty object
return {};
};