From 50a0f0240d7fef133eb5acc1bea2b1168b08e9db Mon Sep 17 00:00:00 2001 From: Factiven Date: Sun, 24 Dec 2023 13:03:54 +0700 Subject: migrate to typescript --- .gitignore | 6 +- README.md | 8 +- components/anime/episode.js | 156 ++--- components/anime/mobile/reused/infoChip.js | 43 -- components/anime/mobile/reused/infoChip.tsx | 58 ++ components/anime/mobile/topSection.js | 323 ---------- components/anime/mobile/topSection.tsx | 378 +++++++++++ components/anime/viewMode/thumbnailDetail.js | 22 +- components/anime/viewMode/thumbnailOnly.js | 16 +- components/anime/viewSelector.js | 20 +- components/disqus.js | 17 - components/disqus.tsx | 26 + components/home/content.js | 533 --------------- components/home/content.tsx | 611 ++++++++++++++++++ components/home/recommendation.js | 125 +++- components/home/schedule.js | 4 +- components/listEditor.js | 160 ----- components/listEditor.tsx | 171 +++++ components/manga/ChaptersComponent.js | 89 +++ components/manga/chapters.js | 2 +- components/manga/leftBar.js | 2 +- components/manga/mobile/bottomBar.js | 2 +- components/manga/panels/firstPanel.js | 2 +- components/manga/panels/secondPanel.js | 4 +- components/manga/panels/thirdPanel.js | 2 +- components/modal.js | 19 - components/modal.tsx | 25 + components/search/searchByImage.js | 119 ---- components/search/searchByImage.tsx | 160 +++++ components/search/selection.js | 415 ------------ components/search/selection.ts | 415 ++++++++++++ components/searchPalette.js | 282 -------- components/searchPalette.tsx | 308 +++++++++ components/shared/MobileNav.js | 170 ----- components/shared/MobileNav.tsx | 174 +++++ components/shared/NavBar.js | 267 -------- components/shared/NavBar.tsx | 289 +++++++++ components/shared/bugReport.js | 194 ------ components/shared/bugReport.tsx | 199 ++++++ components/shared/changelogs.tsx | 265 ++++++++ components/shared/footer.js | 185 ------ components/shared/footer.tsx | 179 ++++++ components/shared/hamburgerMenu.js | 192 ------ components/shared/loading.js | 20 - components/shared/loading.tsx | 16 + .../new-player/components/bufferingIndicator.tsx | 15 + components/watch/new-player/components/buttons.tsx | 277 ++++++++ .../watch/new-player/components/chapter-title.tsx | 11 + .../components/layouts/captions.module.css | 80 +++ .../components/layouts/video-layout.module.css | 13 + .../new-player/components/layouts/video-layout.tsx | 173 +++++ components/watch/new-player/components/menus.tsx | 387 +++++++++++ components/watch/new-player/components/sliders.tsx | 73 +++ .../watch/new-player/components/time-group.tsx | 11 + components/watch/new-player/components/title.tsx | 35 + components/watch/new-player/player.module.css | 50 ++ components/watch/new-player/player.tsx | 471 ++++++++++++++ components/watch/new-player/tracks.tsx | 184 ++++++ components/watch/player/artplayer.js | 387 ----------- .../watch/player/component/controls/quality.js | 15 - components/watch/player/component/overlay.js | 57 -- components/watch/player/playerComponent.js | 527 --------------- components/watch/primary/details.js | 193 ------ components/watch/primary/details.tsx | 234 +++++++ components/watch/secondary/episodeLists.js | 189 ------ components/watch/secondary/episodeLists.tsx | 205 ++++++ jsconfig.json | 11 - lib/anify/getMangaId.js | 40 -- lib/anify/getMangaId.ts | 61 ++ lib/anify/info.js | 24 +- lib/anilist/aniAdvanceSearch.js | 131 ---- lib/anilist/aniAdvanceSearch.ts | 155 +++++ lib/anilist/getUpcomingAnime.js | 2 +- lib/anilist/useAnilist.js | 3 + lib/context/watchPageProvider.js | 12 +- lib/hooks/useCountdownSeconds.ts | 54 ++ lib/hooks/useWatchStorage.tsx | 28 + lib/prisma.js | 5 - lib/prisma.ts | 9 + lib/redis.js | 47 -- lib/redis.ts | 47 ++ next-env.d.ts | 5 + next.config.js | 29 +- package-lock.json | 556 +++++++++------- package.json | 23 +- pages/404.js | 61 -- pages/404.tsx | 60 ++ pages/_app.js | 135 ---- pages/_app.tsx | 105 +++ pages/_document.js | 29 - pages/_document.tsx | 29 + pages/_error.js | 41 -- pages/_error.tsx | 41 ++ pages/_offline.js | 45 -- pages/_offline.tsx | 45 ++ pages/api/auth/[...nextauth].js | 140 ---- pages/api/auth/[...nextauth].ts | 125 ++++ pages/api/og.jsx | 103 --- pages/api/og.tsx | 103 +++ pages/api/v2/episode/[id].js | 297 --------- pages/api/v2/episode/[id].tsx | 333 ++++++++++ pages/api/v2/etc/recent/[page].js | 57 -- pages/api/v2/etc/recent/[page].tsx | 81 +++ pages/api/v2/etc/schedule/index.js | 77 --- pages/api/v2/etc/schedule/index.tsx | 98 +++ pages/en/about.js | 66 -- pages/en/about.tsx | 66 ++ pages/en/anime/[...id].js | 305 --------- pages/en/anime/[...id].tsx | 336 ++++++++++ pages/en/anime/recent.js | 2 +- pages/en/anime/watch/[...info].js | 213 ++++-- pages/en/contact.js | 22 - pages/en/contact.tsx | 22 + pages/en/dmca.js | 112 ---- pages/en/dmca.tsx | 112 ++++ pages/en/index.js | 610 ------------------ pages/en/index.tsx | 712 +++++++++++++++++++++ pages/en/manga/[...id].js | 427 ------------ pages/en/manga/[...id].tsx | 456 +++++++++++++ pages/en/manga/read/[...params].js | 1 + pages/en/profile/[user].js | 496 -------------- pages/en/profile/[user].tsx | 509 +++++++++++++++ pages/en/schedule/index.js | 485 -------------- pages/en/schedule/index.tsx | 484 ++++++++++++++ pages/en/search/[...param].js | 571 ----------------- pages/en/search/[...param].tsx | 598 +++++++++++++++++ pages/id/index.js | 47 -- pages/id/index.tsx | 47 ++ pages/id/manga/[...id].tsx | 159 +++++ pages/id/manga/read/[...id].tsx | 87 +++ pages/id/novel/[...id].tsx | 121 ++++ pages/id/novel/read/index.tsx | 115 ++++ pages/id/search.tsx | 221 +++++++ pages/index.js | 32 - pages/index.tsx | 32 + prisma/user.js | 298 --------- prisma/user.ts | 288 +++++++++ public/icon-144x144.png | Bin 0 -> 41356 bytes public/manifest.json | 6 + public/robots.txt | 5 + styles/globals.css | 34 +- tailwind.config.cjs | 94 +++ tailwind.config.js | 81 --- tsconfig.json | 34 + types/api/AnifyEpisode.ts | 16 + types/api/ConsumetInfo.ts | 154 +++++ types/api/Episode.ts | 19 + types/episodes/AnifyRecentEpisode.ts | 91 +++ types/episodes/ConsumetInfo.ts | 126 ++++ types/episodes/Sessions.ts | 30 + types/episodes/TrackData.ts | 70 ++ types/index.tsx | 17 + types/info/AnifySearchAdvanceTypes.ts | 87 +++ types/info/AnilistInfoTypes.ts | 138 ++++ utils/appendMetaToEpisodes.js | 28 - utils/appendMetaToEpisodes.ts | 51 ++ utils/combineImages.js | 26 - utils/combineImages.ts | 43 ++ utils/getFormat.js | 17 - utils/getFormat.ts | 17 + utils/getGreetings.js | 16 - utils/getGreetings.ts | 16 + utils/getRedisWithPrefix.js | 84 --- utils/getRedisWithPrefix.ts | 86 +++ utils/getTimes.js | 143 ----- utils/getTimes.ts | 159 +++++ utils/imageUtils.js | 38 -- utils/imageUtils.ts | 55 ++ utils/parseMetaData.ts | 36 ++ utils/request/index.ts | 111 ++++ utils/schedulesUtils.js | 83 --- utils/schedulesUtils.ts | 96 +++ utils/useCountdownSeconds.js | 37 -- 173 files changed, 13688 insertions(+), 10015 deletions(-) delete mode 100644 components/anime/mobile/reused/infoChip.js create mode 100644 components/anime/mobile/reused/infoChip.tsx delete mode 100644 components/anime/mobile/topSection.js create mode 100644 components/anime/mobile/topSection.tsx delete mode 100644 components/disqus.js create mode 100644 components/disqus.tsx delete mode 100644 components/home/content.js create mode 100644 components/home/content.tsx delete mode 100644 components/listEditor.js create mode 100644 components/listEditor.tsx create mode 100644 components/manga/ChaptersComponent.js delete mode 100644 components/modal.js create mode 100644 components/modal.tsx delete mode 100644 components/search/searchByImage.js create mode 100644 components/search/searchByImage.tsx delete mode 100644 components/search/selection.js create mode 100644 components/search/selection.ts delete mode 100644 components/searchPalette.js create mode 100644 components/searchPalette.tsx delete mode 100644 components/shared/MobileNav.js create mode 100644 components/shared/MobileNav.tsx delete mode 100644 components/shared/NavBar.js create mode 100644 components/shared/NavBar.tsx delete mode 100644 components/shared/bugReport.js create mode 100644 components/shared/bugReport.tsx create mode 100644 components/shared/changelogs.tsx delete mode 100644 components/shared/footer.js create mode 100644 components/shared/footer.tsx delete mode 100644 components/shared/hamburgerMenu.js delete mode 100644 components/shared/loading.js create mode 100644 components/shared/loading.tsx create mode 100644 components/watch/new-player/components/bufferingIndicator.tsx create mode 100644 components/watch/new-player/components/buttons.tsx create mode 100644 components/watch/new-player/components/chapter-title.tsx create mode 100644 components/watch/new-player/components/layouts/captions.module.css create mode 100644 components/watch/new-player/components/layouts/video-layout.module.css create mode 100644 components/watch/new-player/components/layouts/video-layout.tsx create mode 100644 components/watch/new-player/components/menus.tsx create mode 100644 components/watch/new-player/components/sliders.tsx create mode 100644 components/watch/new-player/components/time-group.tsx create mode 100644 components/watch/new-player/components/title.tsx create mode 100644 components/watch/new-player/player.module.css create mode 100644 components/watch/new-player/player.tsx create mode 100644 components/watch/new-player/tracks.tsx delete mode 100644 components/watch/player/artplayer.js delete mode 100644 components/watch/player/component/controls/quality.js delete mode 100644 components/watch/player/component/overlay.js delete mode 100644 components/watch/player/playerComponent.js delete mode 100644 components/watch/primary/details.js create mode 100644 components/watch/primary/details.tsx delete mode 100644 components/watch/secondary/episodeLists.js create mode 100644 components/watch/secondary/episodeLists.tsx delete mode 100644 jsconfig.json delete mode 100644 lib/anify/getMangaId.js create mode 100644 lib/anify/getMangaId.ts delete mode 100644 lib/anilist/aniAdvanceSearch.js create mode 100644 lib/anilist/aniAdvanceSearch.ts create mode 100644 lib/hooks/useCountdownSeconds.ts create mode 100644 lib/hooks/useWatchStorage.tsx delete mode 100644 lib/prisma.js create mode 100644 lib/prisma.ts delete mode 100644 lib/redis.js create mode 100644 lib/redis.ts create mode 100644 next-env.d.ts delete mode 100644 pages/404.js create mode 100644 pages/404.tsx delete mode 100644 pages/_app.js create mode 100644 pages/_app.tsx delete mode 100644 pages/_document.js create mode 100644 pages/_document.tsx delete mode 100644 pages/_error.js create mode 100644 pages/_error.tsx delete mode 100644 pages/_offline.js create mode 100644 pages/_offline.tsx delete mode 100644 pages/api/auth/[...nextauth].js create mode 100644 pages/api/auth/[...nextauth].ts delete mode 100644 pages/api/og.jsx create mode 100644 pages/api/og.tsx delete mode 100644 pages/api/v2/episode/[id].js create mode 100644 pages/api/v2/episode/[id].tsx delete mode 100644 pages/api/v2/etc/recent/[page].js create mode 100644 pages/api/v2/etc/recent/[page].tsx delete mode 100644 pages/api/v2/etc/schedule/index.js create mode 100644 pages/api/v2/etc/schedule/index.tsx delete mode 100644 pages/en/about.js create mode 100644 pages/en/about.tsx delete mode 100644 pages/en/anime/[...id].js create mode 100644 pages/en/anime/[...id].tsx delete mode 100644 pages/en/contact.js create mode 100644 pages/en/contact.tsx delete mode 100644 pages/en/dmca.js create mode 100644 pages/en/dmca.tsx delete mode 100644 pages/en/index.js create mode 100644 pages/en/index.tsx delete mode 100644 pages/en/manga/[...id].js create mode 100644 pages/en/manga/[...id].tsx delete mode 100644 pages/en/profile/[user].js create mode 100644 pages/en/profile/[user].tsx delete mode 100644 pages/en/schedule/index.js create mode 100644 pages/en/schedule/index.tsx delete mode 100644 pages/en/search/[...param].js create mode 100644 pages/en/search/[...param].tsx delete mode 100644 pages/id/index.js create mode 100644 pages/id/index.tsx create mode 100644 pages/id/manga/[...id].tsx create mode 100644 pages/id/manga/read/[...id].tsx create mode 100644 pages/id/novel/[...id].tsx create mode 100644 pages/id/novel/read/index.tsx create mode 100644 pages/id/search.tsx delete mode 100644 pages/index.js create mode 100644 pages/index.tsx delete mode 100644 prisma/user.js create mode 100644 prisma/user.ts create mode 100644 public/icon-144x144.png create mode 100644 public/robots.txt create mode 100644 tailwind.config.cjs delete mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 types/api/AnifyEpisode.ts create mode 100644 types/api/ConsumetInfo.ts create mode 100644 types/api/Episode.ts create mode 100644 types/episodes/AnifyRecentEpisode.ts create mode 100644 types/episodes/ConsumetInfo.ts create mode 100644 types/episodes/Sessions.ts create mode 100644 types/episodes/TrackData.ts create mode 100644 types/index.tsx create mode 100644 types/info/AnifySearchAdvanceTypes.ts create mode 100644 types/info/AnilistInfoTypes.ts delete mode 100644 utils/appendMetaToEpisodes.js create mode 100644 utils/appendMetaToEpisodes.ts delete mode 100644 utils/combineImages.js create mode 100644 utils/combineImages.ts delete mode 100644 utils/getFormat.js create mode 100644 utils/getFormat.ts delete mode 100644 utils/getGreetings.js create mode 100644 utils/getGreetings.ts delete mode 100644 utils/getRedisWithPrefix.js create mode 100644 utils/getRedisWithPrefix.ts delete mode 100644 utils/getTimes.js create mode 100644 utils/getTimes.ts delete mode 100644 utils/imageUtils.js create mode 100644 utils/imageUtils.ts create mode 100644 utils/parseMetaData.ts create mode 100644 utils/request/index.ts delete mode 100644 utils/schedulesUtils.js create mode 100644 utils/schedulesUtils.ts delete mode 100644 utils/useCountdownSeconds.js 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 @@

+> ⚠️ **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. +

main

@@ -52,7 +54,7 @@ -> **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} /> @@ -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 ( @@ -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 && ( - {info?.episodes && ( -
- {info?.episodes} Episodes -
- )} - {info?.averageScore && ( -
- {info?.averageScore}% -
- )} - {info?.format && ( -
- {getFormat(info?.format)} -
- )} - {info?.status && ( -
- {info?.status} -
- )} - - ); -} diff --git a/components/anime/mobile/reused/infoChip.tsx b/components/anime/mobile/reused/infoChip.tsx new file mode 100644 index 0000000..80ebf83 --- /dev/null +++ b/components/anime/mobile/reused/infoChip.tsx @@ -0,0 +1,58 @@ +import React, { CSSProperties, FC } from "react"; +import { getFormat } from "@/utils/getFormat"; + +interface Info { + episodes?: number; + averageScore?: number; + format?: string; + status?: string; +} + +interface InfoChipProps { + info: Info; + color: any; + className: string; +} + +const InfoChip: FC = ({ info, color, className }) => { + return ( +
+ {info?.episodes && ( +
+ {info?.episodes} Episodes +
+ )} + {info?.averageScore && ( +
+ {info?.averageScore}% +
+ )} + {info?.format && ( +
+ {getFormat(info?.format)} +
+ )} + {info?.status && ( +
+ {info?.status} +
+ )} +
+ ); +}; + +export default InfoChip; diff --git a/components/anime/mobile/topSection.js b/components/anime/mobile/topSection.js deleted file mode 100644 index 6780da5..0000000 --- a/components/anime/mobile/topSection.js +++ /dev/null @@ -1,323 +0,0 @@ -import { - BookOpenIcon, - PlayIcon, - PlusIcon, - ShareIcon, -} from "@heroicons/react/24/solid"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -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"; - -export default function DetailTop({ - info, - statuses, - handleOpen, - watchUrl, - progress, - color, -}) { - const router = useRouter(); - const [readMore, setReadMore] = useState(false); - - const [showAll, setShowAll] = useState(false); - - const isAnime = info.type === "ANIME"; - - useEffect(() => { - setReadMore(false); - }, [info.id]); - - const handleShareClick = async () => { - try { - if (navigator.share) { - await navigator.share({ - title: `${isAnime ? "Watch" : "Read"} Now - ${info?.title?.english}`, - // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, - url: window.location.href, - }); - } else { - // Web Share API is not supported, provide a fallback or show a message - alert("Web Share API is not supported in this browser."); - } - } catch (error) { - console.error("Error sharing:", error); - } - }; - - return ( -
- - - {/* MAIN */} -
-
- poster anime -
-
-
-

- {info?.season?.toLowerCase() || getMonth(info?.startDate?.month)}{" "} - {info.seasonYear || info?.startDate?.year} -

-

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

-

- {info.title?.english} -

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

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

- )} - - {info?.description && ( - - )} - - - - {info?.relations?.edges?.length > 0 && ( -
-
- {info?.relations?.edges?.length > 0 && ( -
- Relations -
- )} - {info?.relations?.edges?.length > 3 && ( -
setShowAll(!showAll)} - > - {showAll ? "show less" : "show more"} -
- )} -
-
- {info?.relations?.edges - .slice(0, showAll ? info?.relations?.edges.length : 3) - .map((r, index) => { - const rel = r.node; - return ( - -
-
- {rel.id} -
-
-
- {r.relationType.replace(/_/g, " ")} -
-
- {rel.title.userPreferred} -
-
{rel.format}
-
-
- - ); - })} -
-
- )} -
- ); -} - -function getMonth(month) { - if (!month) return ""; - const formattedMonth = new Date(0, month).toLocaleString("default", { - month: "long", - }); - return formattedMonth; -} diff --git a/components/anime/mobile/topSection.tsx b/components/anime/mobile/topSection.tsx new file mode 100644 index 0000000..2d28c66 --- /dev/null +++ b/components/anime/mobile/topSection.tsx @@ -0,0 +1,378 @@ +import { + BookOpenIcon, + PlayIcon, + PlusIcon, + ShareIcon, +} from "@heroicons/react/24/solid"; +import Image from "next/image"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { convertSecondsToTime } from "@/utils/getTimes"; +import Link from "next/link"; +import InfoChip from "./reused/infoChip"; +import Description from "./reused/description"; +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 = undefined, + handleOpen, + watchUrl, + progress, + color, +}: DetailTopProps) { + const router = useRouter(); + const [readMore, setReadMore] = useState(false); + + const [showAll, setShowAll] = useState(false); + + const isAnime = info?.type === "ANIME"; + + useEffect(() => { + setReadMore(false); + }, [info?.id]); + + const handleShareClick = async () => { + try { + if (navigator.share) { + await navigator.share({ + title: `${isAnime ? "Watch" : "Read"} Now - ${info?.title?.english}`, + // text: `Watch [${info?.title?.romaji}] and more on Moopa. Join us for endless anime entertainment"`, + url: window.location.href, + }); + } else { + // Web Share API is not supported, provide a fallback or show a message + alert("Web Share API is not supported in this browser."); + } + } catch (error) { + console.error("Error sharing:", error); + } + }; + + return ( +
+ {/* MAIN */} +
+
+ {info ? ( + poster anime + ) : ( + + )} +
+
+
+

+ {info?.season?.toLowerCase() || getMonth(info?.startDate?.month)}{" "} + {info?.seasonYear || info?.startDate?.year} + {!info && } +

+

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

+

+ {info?.title?.english} +

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

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

+ )} + + {info && info?.description && ( + + )} + + {info && ( + + )} + + {info && info?.relations?.edges?.length > 0 && ( +
+
+ {info?.relations?.edges?.length > 0 && ( +
+ Relations +
+ )} + {info?.relations?.edges?.length > 3 && ( +
setShowAll(!showAll)} + > + {showAll ? "show less" : "show more"} +
+ )} +
+
+ {info?.relations?.edges + .slice(0, showAll ? info?.relations?.edges.length : 3) + .map((r, index) => { + const rel = r.node; + return ( + +
+
+ {rel.id.toString()} +
+
+
+ {r.relationType.replace(/_/g, " ")} +
+
+ {rel.title.userPreferred} +
+
{rel.format}
+
+
+ + ); + })} +
+
+ )} +
+ ); +} + +function getMonth(month: number | undefined) { + if (!month) return ""; + const formattedMonth = new Date(0, month).toLocaleString("default", { + month: "long", + }); + return formattedMonth; +} 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({
{parsedImage && ( {`Episode

- {title || `Episode ${epi?.number || 0}`} + {epi?.title || `Episode ${epi?.number || 0}`}

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

- {description} + {epi?.description}

)}
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 ( */} {parsedImage && ( {`Episode 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 }) {
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.js deleted file mode 100644 index b814851..0000000 --- a/components/disqus.js +++ /dev/null @@ -1,17 +0,0 @@ -import { DiscussionEmbed } from "disqus-react"; - -const DisqusComments = ({ post }) => { - const disqusShortname = post.name || "your_disqus_shortname"; - const disqusConfig = { - url: post.url, - identifier: post.url, // Single post id - title: `${post.title} - Episode ${post.episode}`, // Single post title - }; - - return ( -
- -
- ); -}; -export default DisqusComments; diff --git a/components/disqus.tsx b/components/disqus.tsx new file mode 100644 index 0000000..dca03e2 --- /dev/null +++ b/components/disqus.tsx @@ -0,0 +1,26 @@ +import { DiscussionEmbed } from "disqus-react"; + +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, + identifier: post.url, // Single post id + title: `${post.title} - Episode ${post.episode}`, // Single post title + }; + + return ( +
+ +
+ ); +}; +export default DisqusComments; diff --git a/components/home/content.js b/components/home/content.js deleted file mode 100644 index d2498f6..0000000 --- a/components/home/content.js +++ /dev/null @@ -1,533 +0,0 @@ -import Link from "next/link"; -import React, { useState, useRef, useEffect, Fragment } from "react"; -import { useDraggable } from "react-use-draggable-scroll"; -import Image from "next/image"; -import { MdChevronRight } from "react-icons/md"; -import { - ChevronRightIcon, - ArrowRightCircleIcon, -} from "@heroicons/react/24/outline"; - -import { ChevronLeftIcon } from "@heroicons/react/20/solid"; -import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; -import { useRouter } from "next/router"; -import HistoryOptions from "./content/historyOptions"; -import { toast } from "sonner"; -import { truncateImgUrl } from "@/utils/imageUtils"; - -export default function Content({ - ids, - section, - data, - userData, - og, - userName, - setRemoved, - type = "anime", -}) { - const router = useRouter(); - - const ref = useRef(); - const { events } = useDraggable(ref); - - const [clicked, setClicked] = useState(false); - - useEffect(() => { - const click = localStorage.getItem("clicked"); - - if (click) { - setClicked(JSON.parse(click)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const [scrollLeft, setScrollLeft] = useState(false); - const [scrollRight, setScrollRight] = useState(true); - - const slideLeft = () => { - ref.current.classList.add("scroll-smooth"); - var slider = document.getElementById(ids); - slider.scrollLeft = slider.scrollLeft - 500; - ref.current.classList.remove("scroll-smooth"); - }; - const slideRight = () => { - ref.current.classList.add("scroll-smooth"); - var slider = document.getElementById(ids); - slider.scrollLeft = slider.scrollLeft + 500; - ref.current.classList.remove("scroll-smooth"); - }; - - const handleScroll = (e) => { - const scrollLeft = e.target.scrollLeft > 31; - const scrollRight = - e.target.scrollLeft < e.target.scrollWidth - e.target.clientWidth; - setScrollLeft(scrollLeft); - setScrollRight(scrollRight); - }; - - function handleAlert(e) { - if (localStorage.getItem("clicked")) { - const existingDataString = localStorage.getItem("clicked"); - const existingData = JSON.parse(existingDataString); - - existingData[e] = true; - - const updatedDataString = JSON.stringify(existingData); - - localStorage.setItem("clicked", updatedDataString); - } else { - const newData = { - [e]: true, - }; - - const newDataString = JSON.stringify(newData); - - localStorage.setItem("clicked", newDataString); - } - } - - const array = data; - let filteredData = array?.filter((item) => item !== null); - const slicedData = - filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData; - - const goToPage = () => { - if (section === "Recently Watched") { - router.push(`/en/anime/recently-watched`); - } - if (section === "New Episodes") { - router.push(`/en/anime/recent`); - } - if (section === "Trending Now") { - router.push(`/en/anime/trending`); - } - if (section === "Popular Anime") { - router.push(`/en/anime/popular`); - } - if (section === "Your Plan") { - router.push(`/en/profile/${userName}/#planning`); - } - if (section === "On-Going Anime" || section === "Your Watch List") { - router.push(`/en/profile/${userName}/#current`); - } - }; - - const removeItem = async (id, aniId) => { - if (userName) { - // remove from database - const res = await fetch(`/api/user/update/episode`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: userName, - id, - aniId, - }), - }); - const data = await res.json(); - - if (id) { - // remove from local storage - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); - } - } - if (aniId) { - const currentData = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - - const updatedData = {}; - - for (const key in currentData) { - const item = currentData[key]; - if (item.aniId !== aniId) { - updatedData[key] = item; - } - } - - localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); - } - - // update client - setRemoved(id || aniId); - - if (data?.message === "Episode deleted") { - toast.success("Episode removed from history"); - } - } else { - if (id) { - // remove from local storage - const artplayerSettings = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - if (artplayerSettings[id]) { - delete artplayerSettings[id]; - localStorage.setItem( - "artplayer_settings", - JSON.stringify(artplayerSettings) - ); - } - setRemoved(id); - } - if (aniId) { - const currentData = - JSON.parse(localStorage.getItem("artplayer_settings")) || {}; - - // Create a new object to store the updated data - const updatedData = {}; - - // Iterate through the current data and copy items with different aniId to the updated object - for (const key in currentData) { - const item = currentData[key]; - if (item.aniId !== aniId) { - updatedData[key] = item; - } - } - - // Update localStorage with the filtered data - localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); - setRemoved(aniId); - } - } - }; - - return ( -
-
-

{section}

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

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

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

- {checkProgress(progress)} -

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

- Episode {anime.nextAiringEpisode.episode} in -

-

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

-
- )} -
-
- )} -
- {ids === "recentAdded" && ( -
- )} - { -
- {ids === "recentAdded" && ( - - episode-badge -

- Episode{" "} - - {anime?.currentEpisode || anime?.episodeNumber} - -

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

- {anime.status === "RELEASING" || - ids === "recentAdded" ? ( - - ) : anime.status === "NOT_YET_RELEASED" ? ( - - ) : null} - {anime.title.romaji} -

- - )} -
- ); - }) - : userData - ?.filter((i) => i.title && i.title !== null) - ?.slice(0, 10) - .map((i) => { - const time = i.timeWatched; - const duration = i.duration; - let prog = (time / duration) * 100; - if (prog > 90) prog = 100; - - return ( -
-
- - {i?.nextId && ( - - )} -
- -
-
- -

- {i?.title === i.aniTitle - ? `Episode ${i.episode}` - : i?.title || i.anititle} -

-
- - - {i?.image && ( - Episode Thumbnail - )} - - - - {/*

{i.title}

*/} -

- - {i.aniTitle} - {" "} - | Episode {i.episode} -

- -
- ); - })} - {userData?.filter((i) => i.aniId !== null)?.length >= 10 && - section !== "Recommendations" && ( -
-
-

- More on {section} -

- -
-
- )} - {filteredData?.length >= 10 && section !== "Recommendations" && ( -
-
-

- More on {section} -

- -
-
- )} -
- -
-
- ); -} - -function convertSecondsToTime(sec) { - let days = Math.floor(sec / (3600 * 24)); - let hours = Math.floor((sec % (3600 * 24)) / 3600); - let minutes = Math.floor((sec % 3600) / 60); - - let time = ""; - - if (days > 0) { - time += `${days}d `; - time += `${hours}h`; - } else { - time += `${hours}h `; - time += `${minutes}m`; - } - - return time.trim(); -} - -function checkProgress(entry) { - const { progress, media } = entry; - const { episodes, nextAiringEpisode } = media; - - if (nextAiringEpisode !== null) { - const { episode } = nextAiringEpisode; - - if (episode - progress > 1) { - const missedEpisodes = episode - progress - 1; - return `${missedEpisodes} episode${missedEpisodes > 1 ? "s" : ""} behind`; - } - } - - return; -} diff --git a/components/home/content.tsx b/components/home/content.tsx new file mode 100644 index 0000000..b193381 --- /dev/null +++ b/components/home/content.tsx @@ -0,0 +1,611 @@ +import Link from "next/link"; +import React, { useState, useRef, useEffect, Fragment } from "react"; +import { useDraggable } from "react-use-draggable-scroll"; +import Image from "next/image"; +import { MdChevronRight } from "react-icons/md"; +import { + ChevronRightIcon, + ArrowRightCircleIcon, +} from "@heroicons/react/24/outline"; + +import { ChevronLeftIcon } from "@heroicons/react/20/solid"; +import { ExclamationCircleIcon, PlayIcon } from "@heroicons/react/24/solid"; +import { useRouter } from "next/router"; +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, + data, + userData, + og, + userName, + setRemoved, + type = "anime", +}: ContentProps) { + const ref = useRef(null!); + const { events } = useDraggable(ref); + + const router = useRouter(); + + const [clicked, setClicked] = useState(false); + + useEffect(() => { + const click = localStorage.getItem("clicked"); + + if (click) { + setClicked(JSON.parse(click)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [scrollLeft, setScrollLeft] = useState(false); + const [scrollRight, setScrollRight] = useState(true); + + const slideLeft = () => { + 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 = () => { + 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: any) => { + const scrollLeft = e.target.scrollLeft > 31; + const scrollRight = + e.target.scrollLeft < e.target.scrollWidth - e.target.clientWidth; + setScrollLeft(scrollLeft); + setScrollRight(scrollRight); + }; + + function handleAlert(e: string) { + if (localStorage.getItem("clicked")) { + const existingDataString = localStorage.getItem("clicked"); + const existingData = existingDataString + ? JSON.parse(existingDataString) + : {}; + + existingData[e] = true; + + const updatedDataString = JSON.stringify(existingData); + + localStorage.setItem("clicked", updatedDataString); + } else { + const newData = { + [e]: true, + }; + + const newDataString = JSON.stringify(newData); + + localStorage.setItem("clicked", newDataString); + } + } + + const array = data; + let filteredData = array?.filter((item: any) => item !== null); + const slicedData: SlicedDataTypes[] = + filteredData?.length > 15 ? filteredData?.slice(0, 15) : filteredData; + + const goToPage = () => { + if (section === "Recently Watched") { + router.push(`/en/anime/recently-watched`); + } + if (section === "New Episodes") { + router.push(`/en/anime/recent`); + } + if (section === "Trending Now") { + router.push(`/en/anime/trending`); + } + if (section === "Popular Anime") { + router.push(`/en/anime/popular`); + } + if (section === "Your Plan") { + router.push(`/en/profile/${userName}/#planning`); + } + if (section === "On-Going Anime" || section === "Your Watch List") { + router.push(`/en/profile/${userName}/#current`); + } + }; + + const removeItem = async (id: string, aniId: string) => { + if (userName) { + // remove from database + const res = await fetch(`/api/user/update/episode`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: userName, + id, + aniId, + }), + }); + const data = await res.json(); + + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + + const updatedData: { [key: string]: any } = {}; + + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + } + + // update client + setRemoved(id || aniId); + + if (data?.message === "Episode deleted") { + toast.success("Episode removed from history"); + } + } else { + if (id) { + // remove from local storage + const artplayerSettings = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + if (artplayerSettings[id]) { + delete artplayerSettings[id]; + localStorage.setItem( + "artplayer_settings", + JSON.stringify(artplayerSettings) + ); + } + setRemoved(id); + } + if (aniId) { + const currentData = + JSON.parse(localStorage.getItem("artplayer_settings") || "{}") || {}; + + // Create a new object to store the updated data + const updatedData: { [key: string]: any } = {}; + + // Iterate through the current data and copy items with different aniId to the updated object + for (const key in currentData) { + const item = currentData[key]; + if (item.aniId !== aniId) { + updatedData[key] = item; + } + } + + // Update localStorage with the filtered data + localStorage.setItem("artplayer_settings", JSON.stringify(updatedData)); + setRemoved(aniId); + } + } + }; + + return ( +
+
+

{section}

+ +
+
+
+ +
+
} + > + {ids !== "recentlyWatched" + ? slicedData?.map((anime) => { + 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 ( +
+ + {ids === "onGoing" && ( +
+
+

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

+ {checkProgress(progress) && + !clicked?.hasOwnProperty(anime.id) && ( + + )} + {checkProgress(progress) && ( +
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" + > +

+ {checkProgress(progress)} +

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

+ Episode {anime.nextAiringEpisode.episode} in +

+

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

+
+ )} +
+
+ )} +
+ {ids === "recentAdded" && ( +
+ )} + {image && ( + { + )} +
+ {ids === "recentAdded" && ( + + episode-badge +

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

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

+ {anime.status === "RELEASING" || + ids === "recentAdded" ? ( + + ) : anime.status === "NOT_YET_RELEASED" ? ( + + ) : null} + {anime.title.romaji} +

+ + )} +
+ ); + }) + : userData + ?.filter((i) => i.title && i.title !== null) + ?.slice(0, 10) + .map((i) => { + const time = i.timeWatched; + const duration = i.duration; + let prog = time && duration ? (time / duration) * 100 : 0; + if (prog > 90) prog = 100; + + return ( +
+
+ + {i?.nextId && ( + + )} +
+ +
+
+ +

+ {i?.title === i.aniTitle + ? `Episode ${i.episode}` + : i?.title || i?.aniTitle} +

+
+ + + {i?.image && ( + Episode Thumbnail + )} + + + + {/*

{i.title}

*/} +

+ + {i.aniTitle} + {" "} + | Episode {i.episode} +

+ +
+ ); + })} + {userData && + userData?.filter((i) => i.aniId !== null)?.length >= 10 && + section !== "Recommendations" && ( +
+
+

+ More on {section} +

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

+ More on {section} +

+ +
+
+ )} +
+ +
+
+ ); +} + +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); + + let time = ""; + + if (days > 0) { + time += `${days}d `; + time += `${hours}h`; + } else { + time += `${hours}h `; + time += `${minutes}m`; + } + + return time.trim(); +} + +function checkProgress(entry: { progress: any; media: any }) { + const { progress, media } = entry; + const { episodes, nextAiringEpisode } = media; + + if (nextAiringEpisode !== null) { + const { episode } = nextAiringEpisode; + + if (episode - progress > 1) { + const missedEpisodes = episode - progress - 1; + return `${missedEpisodes} episode${missedEpisodes > 1 ? "s" : ""} behind`; + } + } + + return; +} diff --git a/components/home/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 ( -
-
+
+
-

+

{data[0].title.userPreferred}

- +

{filteredData.slice(0, 9).map((i) => ( + + + + {i.title.userPreferred} + +
+ {i.type === "ANIME" ? ( + + + + ) : ( + + + + )} +
{i.title.userPreferred} - + {/*
{i.title.userPreferred}
a
-
+
*/} ))}
-
+
+
+ {filteredData.slice(0, 9).map((i) => ( +
+ + +
+ {i.type === "ANIME" ? ( + + + + ) : ( + + + + )} +
+ {i.title.userPreferred} + + +

+ {i.status === "RELEASING" ? ( + + ) : i.status === "NOT_YET_RELEASED" ? ( + + ) : null} + {i.title.userPreferred} +

+ +
+ ))} +
+
+
{data[0]?.bannerImage && ( {data[0].title.userPreferred} )}
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.js deleted file mode 100644 index 7d30835..0000000 --- a/components/listEditor.js +++ /dev/null @@ -1,160 +0,0 @@ -import { useState } from "react"; -import Image from "next/image"; -import { useRouter } from "next/router"; -import { toast } from "sonner"; - -const ListEditor = ({ - animeId, - session, - stats, - prg, - max, - info = null, - close, -}) => { - const [status, setStatus] = useState(stats ?? "CURRENT"); - const [progress, setProgress] = useState(prg ?? 0); - const isAnime = info?.type === "ANIME"; - - const router = useRouter(); - - const handleSubmit = async (e) => { - e.preventDefault(); - console.log("Submitting", status?.name, progress); - try { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${session.user.token}`, - }, - body: JSON.stringify({ - query: ` - mutation ($mediaId: Int!, $progress: Int, $status: MediaListStatus) { - SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) { - id - mediaId - progress - status - } - } - `, - variables: { - mediaId: animeId, - progress: progress, - status: status || null, - }, - }), - }); - const { data } = await response.json(); - if (data.SaveMediaListEntry === null) { - toast.error("Something went wrong"); - return; - } - console.log("Saved media list entry", data); - toast.success("Media list entry saved"); - close(); - setTimeout(() => { - // window.location.reload(); - router.reload(); - }, 1000); - // showAlert("Media list entry saved", "success"); - } catch (error) { - toast.error("Something went wrong"); - console.error(error); - } - }; - - return ( -
-
- List Editor -
-
-
- {info?.bannerImage && ( -
- image - image -
- )} -
-
-
- - -
-
- - setProgress(e.target.value)} - className="rounded-sm px-2 py-1 bg-[#363642] w-[50%] sm:w-[150px] text-sm sm:text-base" - min="0" - /> -
-
- -
-
-
-
- ); -}; - -export default ListEditor; diff --git a/components/listEditor.tsx b/components/listEditor.tsx new file mode 100644 index 0000000..2e180a1 --- /dev/null +++ b/components/listEditor.tsx @@ -0,0 +1,171 @@ +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"; + +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 = ({ + animeId, + session, + stats = "CURRENT", + prg = 0, + max, + info = undefined, + close, +}) => { + const [status, setStatus] = useState(stats ?? "CURRENT"); + const [progress, setProgress] = useState(prg ?? 0); + const isAnime: boolean = info?.type === "ANIME"; + + const router = useRouter(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + // console.log("Submitting", status?.name, progress); + try { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session.user.token}`, + }, + body: JSON.stringify({ + query: ` + mutation ($mediaId: Int!, $progress: Int, $status: MediaListStatus) { + SaveMediaListEntry (mediaId: $mediaId, progress: $progress, status: $status) { + id + mediaId + progress + status + } + } + `, + variables: { + mediaId: animeId, + progress: progress, + status: status || null, + }, + }), + }); + const { data } = await response.json(); + if (data.SaveMediaListEntry === null) { + toast.error("Something went wrong"); + return; + } + console.log("Saved media list entry", data); + toast.success("Media list entry saved"); + close(); + setTimeout(() => { + // window.location.reload(); + router.reload(); + }, 1000); + // showAlert("Media list entry saved", "success"); + } catch (error) { + toast.error("Something went wrong"); + console.error(error); + } + }; + + return ( +
+
+ List Editor +
+
+
+ {info?.bannerImage && ( +
+ image + image +
+ )} +
+
+
+ + +
+
+ + 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" + /> +
+
+ +
+
+
+
+ ); +}; + +export default ListEditor; 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 ( +
+ {!loading ? ( + notFound ? ( +
+

+ Oops!

It looks like this manga is not available. +

+
+ ) : info && chapter && chapter.length > 0 ? ( + + ) : ( +
+
+
+
+
+
+
+
+ ) + ) : ( +
+
+
+
+
+
+
+
+ )} +
+ ); +} 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 (
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)} > setSeekPage(x.index)} > (imageRefs.current[index] = el)} > 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.js deleted file mode 100644 index 5d6d0cc..0000000 --- a/components/modal.js +++ /dev/null @@ -1,19 +0,0 @@ -export default function Modal({ open, onClose, children }) { - return ( -
-
e.stopPropagation()} - className={`shadow rounded-xl transition-all ${ - open ? "scale-100 opacity-100" : "scale-75 opacity-0" - }`} - > - {children} -
-
- ); -} diff --git a/components/modal.tsx b/components/modal.tsx new file mode 100644 index 0000000..6865560 --- /dev/null +++ b/components/modal.tsx @@ -0,0 +1,25 @@ +type ModalProps = { + open: boolean; + onClose: () => void; + children: React.ReactNode; +}; + +export default function Modal({ open, onClose, children }: ModalProps) { + return ( +
+
e.stopPropagation()} + className={`shadow rounded-xl transition-all ${ + open ? "scale-100 opacity-100" : "scale-75 opacity-0" + }`} + > + {children} +
+
+ ); +} diff --git a/components/search/searchByImage.js b/components/search/searchByImage.js deleted file mode 100644 index f61418f..0000000 --- a/components/search/searchByImage.js +++ /dev/null @@ -1,119 +0,0 @@ -import { PhotoIcon } from "@heroicons/react/24/outline"; -import { useRouter } from "next/router"; -import React, { useEffect } from "react"; -import { toast } from "sonner"; - -export default function SearchByImage({ - searchPalette = false, - setIsOpen, - setData, - setMedia, -}) { - const router = useRouter(); - - async function findImage(formData) { - const response = new Promise((resolve, reject) => { - fetch("https://api.trace.moe/search?anilistInfo", { - method: "POST", - body: formData, - }) - .then((resp) => { - resolve(resp.json()); - }) - .catch((error) => { - reject(error); - }); - }); - - toast.promise(response, { - loading: "Finding episodes...", - success: `Episodes found!`, - error: "Error", - }); - - response - .then((data) => { - if (data?.result?.length > 0) { - const id = data.result[0].anilist.id; - const datas = data.result.filter((i) => i.anilist.isAdult === false); - if (setData) setData(datas); - if (searchPalette) router.push(`/en/anime/${id}`); - if (setIsOpen) setIsOpen(false); - if (setMedia) setMedia(); - } - }) - .catch((error) => { - console.error("Error:", error); - }); - } - - const handleImageSelect = async (e) => { - const selectedImage = e.target.files[0]; - - if (selectedImage) { - const formData = new FormData(); - formData.append("image", selectedImage); - - try { - await findImage(formData); - } catch (error) { - console.error("An error occurred:", error); - } - } - }; - - useEffect(() => { - // Add a global event listener for the paste event - const handlePaste = async (e) => { - // e.preventDefault(); - - const items = e.clipboardData.items; - - for (let i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") !== -1) { - const blob = items[i].getAsFile(); - - // Create a FormData object and append the pasted image - const formData = new FormData(); - formData.append("image", blob); - - try { - // Send the pasted image to your API for processing - await findImage(formData); - } catch (error) { - console.error("An error occurred:", error); - } - break; // Stop after finding the first image - } - } - }; - - // Add the event listener to the document - document.addEventListener("paste", handlePaste); - - // Clean up the event listener when the component unmounts - return () => { - document.removeEventListener("paste", handlePaste); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
- -
- ); -} diff --git a/components/search/searchByImage.tsx b/components/search/searchByImage.tsx new file mode 100644 index 0000000..2041871 --- /dev/null +++ b/components/search/searchByImage.tsx @@ -0,0 +1,160 @@ +import { PhotoIcon } from "@heroicons/react/24/outline"; +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 = () => {}, +}: SearchByImageProps) { + const router = useRouter(); + + async function findImage(formData: FormData) { + const response = new Promise((resolve, reject) => { + fetch("https://api.trace.moe/search?anilistInfo", { + method: "POST", + body: formData, + }) + .then((resp) => { + resolve(resp.json()); + }) + .catch((error) => { + reject(error); + }); + }); + + toast.promise(response, { + loading: "Finding episodes...", + success: `Episodes found!`, + error: "Error", + }); + + response + .then((data: any) => { + if (data && data?.result?.length > 0) { + const id = data.result[0].anilist.id; + 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({}); + } + }) + .catch((error) => { + console.error("Error:", error); + }); + } + + const handleImageSelect = async (e: any) => { + const selectedImage = e.target.files[0]; + + if (selectedImage) { + const formData = new FormData(); + formData.append("image", selectedImage); + + try { + await findImage(formData); + } catch (error) { + console.error("An error occurred:", error); + } + } + }; + + useEffect(() => { + // Add a global event listener for the paste event + const handlePaste = async (e: any) => { + // e.preventDefault(); + + const items = e.clipboardData.items; + + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") !== -1) { + const blob = items[i].getAsFile(); + + // Create a FormData object and append the pasted image + const formData = new FormData(); + formData.append("image", blob); + + try { + // Send the pasted image to your API for processing + await findImage(formData); + } catch (error) { + console.error("An error occurred:", error); + } + break; // Stop after finding the first image + } + } + }; + + // Add the event listener to the document + document.addEventListener("paste", handlePaste); + + // Clean up the event listener when the component unmounts + return () => { + document.removeEventListener("paste", handlePaste); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ +
+ ); +} + +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.js deleted file mode 100644 index 767361d..0000000 --- a/components/search/selection.js +++ /dev/null @@ -1,415 +0,0 @@ -export const mediaType = [ - { name: "Anime", value: "ANIME" }, - { name: "Manga", value: "MANGA" }, -]; -export const genreOptions = [ - { - name: "Action", - value: "Action", - type: "genres", - }, - { - name: "Adventure", - value: "Adventure", - type: "genres", - }, - { - name: "Comedy", - value: "Comedy", - type: "genres", - }, - { - name: "Drama", - value: "Drama", - type: "genres", - }, - { - name: "Ecchi", - value: "Ecchi", - type: "genres", - }, - { - name: "Fantasy", - value: "Fantasy", - type: "genres", - }, - { - name: "Horror", - value: "Horror", - type: "genres", - }, - { - name: "Mahou Shoujo", - value: "Mahou Shoujo", - type: "genres", - }, - { - name: "Mecha", - value: "Mecha", - type: "genres", - }, - { - name: "Music", - value: "Music", - type: "genres", - }, - { - name: "Mystery", - value: "Mystery", - type: "genres", - }, - { - name: "Psychological", - value: "Psychological", - type: "genres", - }, - { - name: "Romance", - value: "Romance", - type: "genres", - }, - { - name: "Sci-Fi", - value: "Sci-Fi", - type: "genres", - }, - { - name: "Slice of Life", - value: "Slice of Life", - type: "genres", - }, - { - name: "Sports", - value: "Sports", - type: "genres", - }, - { - name: "Supernatural", - value: "Supernatural", - type: "genres", - }, - { - name: "Thriller", - value: "Thriller", - type: "genres", - }, -]; -export const tagsOption = [ - { name: "4-koma", value: "4-koma", type: "tags" }, - { name: "Achronological Order", value: "Achronological Order", type: "tags" }, - { name: "Afterlife", value: "Afterlife", type: "tags" }, - { name: "Age Gap", value: "Age Gap", type: "tags" }, - { name: "Airsoft", value: "Airsoft", type: "tags" }, - { name: "Aliens", value: "Aliens", type: "tags" }, - { name: "Alternate Universe", value: "Alternate Universe", type: "tags" }, - { name: "American Football", value: "American Football", type: "tags" }, - { name: "Amnesia", value: "Amnesia", type: "tags" }, - { name: "Anti-Hero", value: "Anti-Hero", type: "tags" }, - { name: "Archery", value: "Archery", type: "tags" }, - { name: "Assassins", value: "Assassins", type: "tags" }, - { name: "Athletics", value: "Athletics", type: "tags" }, - { name: "Augmented Reality", value: "Augmented Reality", type: "tags" }, - { name: "Aviation", value: "Aviation", type: "tags" }, - { name: "Badminton", value: "Badminton", type: "tags" }, - { name: "Band", value: "Band", type: "tags" }, - { name: "Bar", value: "Bar", type: "tags" }, - { name: "Baseball", value: "Baseball", type: "tags" }, - { name: "Basketball", value: "Basketball", type: "tags" }, - { name: "Battle Royale", value: "Battle Royale", type: "tags" }, - { name: "Biographical", value: "Biographical", type: "tags" }, - { name: "Bisexual", value: "Bisexual", type: "tags" }, - { name: "Body Swapping", value: "Body Swapping", type: "tags" }, - { name: "Boxing", value: "Boxing", type: "tags" }, - { name: "Bullying", value: "Bullying", type: "tags" }, - { name: "Calligraphy", value: "Calligraphy", type: "tags" }, - { name: "Card Battle", value: "Card Battle", type: "tags" }, - { name: "Cars", value: "Cars", type: "tags" }, - { name: "CGI", value: "CGI", type: "tags" }, - { name: "Chibi", value: "Chibi", type: "tags" }, - { name: "Chuunibyou", value: "Chuunibyou", type: "tags" }, - { name: "Classic Literature", value: "Classic Literature", type: "tags" }, - { name: "College", value: "College", type: "tags" }, - { name: "Coming of Age", value: "Coming of Age", type: "tags" }, - { name: "Cosplay", value: "Cosplay", type: "tags" }, - { name: "Crossdressing", value: "Crossdressing", type: "tags" }, - { name: "Crossover", value: "Crossover", type: "tags" }, - { name: "Cultivation", value: "Cultivation", type: "tags" }, - { - name: "Cute Girls Doing Cute Things", - value: "Cute Girls Doing Cute Things", - type: "tags", - }, - { name: "Cyberpunk", value: "Cyberpunk", type: "tags" }, - { name: "Cycling", value: "Cycling", type: "tags" }, - { name: "Dancing", value: "Dancing", type: "tags" }, - { name: "Delinquents", value: "Delinquents", type: "tags" }, - { name: "Demons", value: "Demons", type: "tags" }, - { name: "Development", value: "Development", type: "tags" }, - { name: "Dragons", value: "Dragons", type: "tags" }, - { name: "Drawing", value: "Drawing", type: "tags" }, - { name: "Dystopian", value: "Dystopian", type: "tags" }, - { name: "Economics", value: "Economics", type: "tags" }, - { name: "Educational", value: "Educational", type: "tags" }, - { name: "Ensemble Cast", value: "Ensemble Cast", type: "tags" }, - { name: "Environmental", value: "Environmental", type: "tags" }, - { name: "Episodic", value: "Episodic", type: "tags" }, - { name: "Espionage", value: "Espionage", type: "tags" }, - { name: "Fairy Tale", value: "Fairy Tale", type: "tags" }, - { name: "Family Life", value: "Family Life", type: "tags" }, - { name: "Fashion", value: "Fashion", type: "tags" }, - { name: "Female Protagonist", value: "Female Protagonist", type: "tags" }, - { name: "Fishing", value: "Fishing", type: "tags" }, - { name: "Fitness", value: "Fitness", type: "tags" }, - { name: "Flash", value: "Flash", type: "tags" }, - { name: "Food", value: "Food", type: "tags" }, - { name: "Football", value: "Football", type: "tags" }, - { name: "Foreign", value: "Foreign", type: "tags" }, - { name: "Fugitive", value: "Fugitive", type: "tags" }, - { name: "Full CGI", value: "Full CGI", type: "tags" }, - { name: "Full Colour", value: "Full Colour", type: "tags" }, - { name: "Gambling", value: "Gambling", type: "tags" }, - { name: "Gangs", value: "Gangs", type: "tags" }, - { name: "Gender Bending", value: "Gender Bending", type: "tags" }, - { name: "Gender Neutral", value: "Gender Neutral", type: "tags" }, - { name: "Ghost", value: "Ghost", type: "tags" }, - { name: "Gods", value: "Gods", type: "tags" }, - { name: "Gore", value: "Gore", type: "tags" }, - { name: "Guns", value: "Guns", type: "tags" }, - { name: "Gyaru", value: "Gyaru", type: "tags" }, - { name: "Harem", value: "Harem", type: "tags" }, - { name: "Henshin", value: "Henshin", type: "tags" }, - { name: "Hikikomori", value: "Hikikomori", type: "tags" }, - { name: "Historical", value: "Historical", type: "tags" }, - { name: "Ice Skating", value: "Ice Skating", type: "tags" }, - { name: "Idol", value: "Idol", type: "tags" }, - { name: "Isekai", value: "Isekai", type: "tags" }, - { name: "Iyashikei", value: "Iyashikei", type: "tags" }, - { name: "Josei", value: "Josei", type: "tags" }, - { name: "Kaiju", value: "Kaiju", type: "tags" }, - { name: "Karuta", value: "Karuta", type: "tags" }, - { name: "Kemonomimi", value: "Kemonomimi", type: "tags" }, - { name: "Kids", value: "Kids", type: "tags" }, - { name: "Love Triangle", value: "Love Triangle", type: "tags" }, - { name: "Mafia", value: "Mafia", type: "tags" }, - { name: "Magic", value: "Magic", type: "tags" }, - { name: "Mahjong", value: "Mahjong", type: "tags" }, - { name: "Maids", value: "Maids", type: "tags" }, - { name: "Male Protagonist", value: "Male Protagonist", type: "tags" }, - { name: "Martial Arts", value: "Martial Arts", type: "tags" }, - { name: "Memory Manipulation", value: "Memory Manipulation", type: "tags" }, - { name: "Meta", value: "Meta", type: "tags" }, - { name: "Military", value: "Military", type: "tags" }, - { name: "Monster Girl", value: "Monster Girl", type: "tags" }, - { name: "Mopeds", value: "Mopeds", type: "tags" }, - { name: "Motorcycles", value: "Motorcycles", type: "tags" }, - { name: "Musical", value: "Musical", type: "tags" }, - { name: "Mythology", value: "Mythology", type: "tags" }, - { name: "Nekomimi", value: "Nekomimi", type: "tags" }, - { name: "Ninja", value: "Ninja", type: "tags" }, - { name: "No Dialogue", value: "No Dialogue", type: "tags" }, - { name: "Noir", value: "Noir", type: "tags" }, - { name: "Nudity", value: "Nudity", type: "tags" }, - { name: "Otaku Culture", value: "Otaku Culture", type: "tags" }, - { name: "Outdoor", value: "Outdoor", type: "tags" }, - { name: "Parody", value: "Parody", type: "tags" }, - { name: "Philosophy", value: "Philosophy", type: "tags" }, - { name: "Photography", value: "Photography", type: "tags" }, - { name: "Pirates", value: "Pirates", type: "tags" }, - { name: "Poker", value: "Poker", type: "tags" }, - { name: "Police", value: "Police", type: "tags" }, - { name: "Politics", value: "Politics", type: "tags" }, - { name: "Post-Apocalyptic", value: "Post-Apocalyptic", type: "tags" }, - { name: "Primarily Adult Cast", value: "Primarily Adult Cast", type: "tags" }, - { - name: "Primarily Female Cast", - value: "Primarily Female Cast", - type: "tags", - }, - { name: "Primarily Male Cast", value: "Primarily Male Cast", type: "tags" }, - { name: "Puppetry", value: "Puppetry", type: "tags" }, - { name: "Real Robot", value: "Real Robot", type: "tags" }, - { name: "Rehabilitation", value: "Rehabilitation", type: "tags" }, - { name: "Reincarnation", value: "Reincarnation", type: "tags" }, - { name: "Revenge", value: "Revenge", type: "tags" }, - { name: "Reverse Harem", value: "Reverse Harem", type: "tags" }, - { name: "Robots", value: "Robots", type: "tags" }, - { name: "Rugby", value: "Rugby", type: "tags" }, - { name: "Rural", value: "Rural", type: "tags" }, - { name: "Samurai", value: "Samurai", type: "tags" }, - { name: "Satire", value: "Satire", type: "tags" }, - { name: "School", value: "School", type: "tags" }, - { name: "School Club", value: "School Club", type: "tags" }, - { name: "Seinen", value: "Seinen", type: "tags" }, - { name: "Ships", value: "Ships", type: "tags" }, - { name: "Shogi", value: "Shogi", type: "tags" }, - { name: "Shoujo", value: "Shoujo", type: "tags" }, - { name: "Shoujo Ai", value: "Shoujo Ai", type: "tags" }, - { name: "Shounen", value: "Shounen", type: "tags" }, - { name: "Shounen Ai", value: "Shounen Ai", type: "tags" }, - { name: "Slapstick", value: "Slapstick", type: "tags" }, - { name: "Slavery", value: "Slavery", type: "tags" }, - { name: "Space", value: "Space", type: "tags" }, - { name: "Space Opera", value: "Space Opera", type: "tags" }, - { name: "Steampunk", value: "Steampunk", type: "tags" }, - { name: "Stop Motion", value: "Stop Motion", type: "tags" }, - { name: "Super Power", value: "Super Power", type: "tags" }, - { name: "Super Robot", value: "Super Robot", type: "tags" }, - { name: "Superhero", value: "Superhero", type: "tags" }, - { name: "Surreal Comedy", value: "Surreal Comedy", type: "tags" }, - { name: "Survival", value: "Survival", type: "tags" }, - { name: "Swimming", value: "Swimming", type: "tags" }, - { name: "Swordplay", value: "Swordplay", type: "tags" }, - { name: "Table Tennis", value: "Table Tennis", type: "tags" }, - { name: "Tanks", value: "Tanks", type: "tags" }, - { name: "Teacher", value: "Teacher", type: "tags" }, - { name: "Tennis", value: "Tennis", type: "tags" }, - { name: "Terrorism", value: "Terrorism", type: "tags" }, - { name: "Time Manipulation", value: "Time Manipulation", type: "tags" }, - { name: "Time Skip", value: "Time Skip", type: "tags" }, - { name: "Tragedy", value: "Tragedy", type: "tags" }, - { name: "Trains", value: "Trains", type: "tags" }, - { name: "Triads", value: "Triads", type: "tags" }, - { name: "Tsundere", value: "Tsundere", type: "tags" }, - { name: "Urban Fantasy", value: "Urban Fantasy", type: "tags" }, - { name: "Vampire", value: "Vampire", type: "tags" }, - { name: "Video Games", value: "Video Games", type: "tags" }, - { name: "Virtual World", value: "Virtual World", type: "tags" }, - { name: "Volleyball", value: "Volleyball", type: "tags" }, - { name: "War", value: "War", type: "tags" }, - { name: "Witch", value: "Witch", type: "tags" }, - { name: "Work", value: "Work", type: "tags" }, - { name: "Wrestling", value: "Wrestling", type: "tags" }, - { name: "Writing", value: "Writing", type: "tags" }, - { name: "Wuxia", value: "Wuxia", type: "tags" }, - { name: "Yakuza", value: "Yakuza", type: "tags" }, - { name: "Yandere", value: "Yandere", type: "tags" }, - { name: "Youkai", value: "Youkai", type: "tags" }, - { name: "Zombie", value: "Zombie", type: "tags" }, -]; -export const formatOptions = [ - { name: "TV", value: "TV" }, - { name: "TV Short", value: "TV_SHORT" }, - { name: "Movie", value: "MOVIE" }, - { name: "Special", value: "SPECIAL" }, - { name: "OVA", value: "OVA" }, - { name: "ONA", value: "ONA" }, - { name: "Music", value: "MUSIC" }, - { name: "Manga", value: "MANGA" }, - { name: "Novel", value: "NOVEL" }, - { name: "One Shot", value: "ONE_SHOT" }, -]; -export const animeFormatOptions = [ - { name: "TV", value: "TV" }, - { name: "TV Short", value: "TV_SHORT" }, - { name: "Movie", value: "MOVIE" }, - { name: "Special", value: "SPECIAL" }, - { name: "OVA", value: "OVA" }, - { name: "ONA", value: "ONA" }, -]; -export const mangaFormatOptions = [ - { name: "Manga", value: "MANGA" }, - { name: "Novel", value: "NOVEL" }, - { name: "One Shot", value: "ONE_SHOT" }, -]; -export const sortOptions = [ - { name: "Date Added", value: "ID_DESC" }, - { name: "Title", value: "TITLE_ROMAJI" }, - { name: "Release Date", value: "START_DATE_DESC" }, - { name: "Average Score", value: "SCORE_DESC" }, - { name: "Popularity", value: "POPULARITY_DESC" }, - { name: "Trending", value: ["TRENDING_DESC", "POPULARITY_DESC"] }, - { name: "Favorites", value: "FAVOURITES_DESC" }, -]; -export const yearOptions = [ - { name: "1940", value: "1940" }, - { name: "1941", value: "1941" }, - { name: "1942", value: "1942" }, - { name: "1943", value: "1943" }, - { name: "1944", value: "1944" }, - { name: "1945", value: "1945" }, - { name: "1946", value: "1946" }, - { name: "1947", value: "1947" }, - { name: "1948", value: "1948" }, - { name: "1949", value: "1949" }, - { name: "1950", value: "1950" }, - { name: "1951", value: "1951" }, - { name: "1952", value: "1952" }, - { name: "1953", value: "1953" }, - { name: "1954", value: "1954" }, - { name: "1955", value: "1955" }, - { name: "1956", value: "1956" }, - { name: "1957", value: "1957" }, - { name: "1958", value: "1958" }, - { name: "1959", value: "1959" }, - { name: "1960", value: "1960" }, - { name: "1961", value: "1961" }, - { name: "1962", value: "1962" }, - { name: "1963", value: "1963" }, - { name: "1964", value: "1964" }, - { name: "1965", value: "1965" }, - { name: "1966", value: "1966" }, - { name: "1967", value: "1967" }, - { name: "1968", value: "1968" }, - { name: "1969", value: "1969" }, - { name: "1970", value: "1970" }, - { name: "1971", value: "1971" }, - { name: "1972", value: "1972" }, - { name: "1973", value: "1973" }, - { name: "1974", value: "1974" }, - { name: "1975", value: "1975" }, - { name: "1976", value: "1976" }, - { name: "1977", value: "1977" }, - { name: "1978", value: "1978" }, - { name: "1979", value: "1979" }, - { name: "1980", value: "1980" }, - { name: "1981", value: "1981" }, - { name: "1982", value: "1982" }, - { name: "1983", value: "1983" }, - { name: "1984", value: "1984" }, - { name: "1985", value: "1985" }, - { name: "1986", value: "1986" }, - { name: "1987", value: "1987" }, - { name: "1988", value: "1988" }, - { name: "1989", value: "1989" }, - { name: "1990", value: "1990" }, - { name: "1991", value: "1991" }, - { name: "1992", value: "1992" }, - { name: "1993", value: "1993" }, - { name: "1994", value: "1994" }, - { name: "1995", value: "1995" }, - { name: "1996", value: "1996" }, - { name: "1997", value: "1997" }, - { name: "1998", value: "1998" }, - { name: "1999", value: "1999" }, - { name: "2000", value: "2000" }, - { name: "2001", value: "2001" }, - { name: "2002", value: "2002" }, - { name: "2003", value: "2003" }, - { name: "2004", value: "2004" }, - { name: "2005", value: "2005" }, - { name: "2006", value: "2006" }, - { name: "2007", value: "2007" }, - { name: "2008", value: "2008" }, - { name: "2009", value: "2009" }, - { name: "2010", value: "2010" }, - { name: "2011", value: "2011" }, - { name: "2012", value: "2012" }, - { name: "2013", value: "2013" }, - { name: "2014", value: "2014" }, - { name: "2015", value: "2015" }, - { name: "2016", value: "2016" }, - { name: "2017", value: "2017" }, - { name: "2018", value: "2018" }, - { name: "2019", value: "2019" }, - { name: "2020", value: "2020" }, - { name: "2021", value: "2021" }, - { name: "2022", value: "2022" }, - { name: "2023", value: "2023" }, - { name: "2024", value: "2024" }, -]; -export const seasonOptions = [ - { name: "Winter", value: "WINTER" }, - { name: "Spring", value: "SPRING" }, - { name: "Summer", value: "SUMMER" }, - { name: "Fall", value: "FALL" }, -]; diff --git a/components/search/selection.ts b/components/search/selection.ts new file mode 100644 index 0000000..767361d --- /dev/null +++ b/components/search/selection.ts @@ -0,0 +1,415 @@ +export const mediaType = [ + { name: "Anime", value: "ANIME" }, + { name: "Manga", value: "MANGA" }, +]; +export const genreOptions = [ + { + name: "Action", + value: "Action", + type: "genres", + }, + { + name: "Adventure", + value: "Adventure", + type: "genres", + }, + { + name: "Comedy", + value: "Comedy", + type: "genres", + }, + { + name: "Drama", + value: "Drama", + type: "genres", + }, + { + name: "Ecchi", + value: "Ecchi", + type: "genres", + }, + { + name: "Fantasy", + value: "Fantasy", + type: "genres", + }, + { + name: "Horror", + value: "Horror", + type: "genres", + }, + { + name: "Mahou Shoujo", + value: "Mahou Shoujo", + type: "genres", + }, + { + name: "Mecha", + value: "Mecha", + type: "genres", + }, + { + name: "Music", + value: "Music", + type: "genres", + }, + { + name: "Mystery", + value: "Mystery", + type: "genres", + }, + { + name: "Psychological", + value: "Psychological", + type: "genres", + }, + { + name: "Romance", + value: "Romance", + type: "genres", + }, + { + name: "Sci-Fi", + value: "Sci-Fi", + type: "genres", + }, + { + name: "Slice of Life", + value: "Slice of Life", + type: "genres", + }, + { + name: "Sports", + value: "Sports", + type: "genres", + }, + { + name: "Supernatural", + value: "Supernatural", + type: "genres", + }, + { + name: "Thriller", + value: "Thriller", + type: "genres", + }, +]; +export const tagsOption = [ + { name: "4-koma", value: "4-koma", type: "tags" }, + { name: "Achronological Order", value: "Achronological Order", type: "tags" }, + { name: "Afterlife", value: "Afterlife", type: "tags" }, + { name: "Age Gap", value: "Age Gap", type: "tags" }, + { name: "Airsoft", value: "Airsoft", type: "tags" }, + { name: "Aliens", value: "Aliens", type: "tags" }, + { name: "Alternate Universe", value: "Alternate Universe", type: "tags" }, + { name: "American Football", value: "American Football", type: "tags" }, + { name: "Amnesia", value: "Amnesia", type: "tags" }, + { name: "Anti-Hero", value: "Anti-Hero", type: "tags" }, + { name: "Archery", value: "Archery", type: "tags" }, + { name: "Assassins", value: "Assassins", type: "tags" }, + { name: "Athletics", value: "Athletics", type: "tags" }, + { name: "Augmented Reality", value: "Augmented Reality", type: "tags" }, + { name: "Aviation", value: "Aviation", type: "tags" }, + { name: "Badminton", value: "Badminton", type: "tags" }, + { name: "Band", value: "Band", type: "tags" }, + { name: "Bar", value: "Bar", type: "tags" }, + { name: "Baseball", value: "Baseball", type: "tags" }, + { name: "Basketball", value: "Basketball", type: "tags" }, + { name: "Battle Royale", value: "Battle Royale", type: "tags" }, + { name: "Biographical", value: "Biographical", type: "tags" }, + { name: "Bisexual", value: "Bisexual", type: "tags" }, + { name: "Body Swapping", value: "Body Swapping", type: "tags" }, + { name: "Boxing", value: "Boxing", type: "tags" }, + { name: "Bullying", value: "Bullying", type: "tags" }, + { name: "Calligraphy", value: "Calligraphy", type: "tags" }, + { name: "Card Battle", value: "Card Battle", type: "tags" }, + { name: "Cars", value: "Cars", type: "tags" }, + { name: "CGI", value: "CGI", type: "tags" }, + { name: "Chibi", value: "Chibi", type: "tags" }, + { name: "Chuunibyou", value: "Chuunibyou", type: "tags" }, + { name: "Classic Literature", value: "Classic Literature", type: "tags" }, + { name: "College", value: "College", type: "tags" }, + { name: "Coming of Age", value: "Coming of Age", type: "tags" }, + { name: "Cosplay", value: "Cosplay", type: "tags" }, + { name: "Crossdressing", value: "Crossdressing", type: "tags" }, + { name: "Crossover", value: "Crossover", type: "tags" }, + { name: "Cultivation", value: "Cultivation", type: "tags" }, + { + name: "Cute Girls Doing Cute Things", + value: "Cute Girls Doing Cute Things", + type: "tags", + }, + { name: "Cyberpunk", value: "Cyberpunk", type: "tags" }, + { name: "Cycling", value: "Cycling", type: "tags" }, + { name: "Dancing", value: "Dancing", type: "tags" }, + { name: "Delinquents", value: "Delinquents", type: "tags" }, + { name: "Demons", value: "Demons", type: "tags" }, + { name: "Development", value: "Development", type: "tags" }, + { name: "Dragons", value: "Dragons", type: "tags" }, + { name: "Drawing", value: "Drawing", type: "tags" }, + { name: "Dystopian", value: "Dystopian", type: "tags" }, + { name: "Economics", value: "Economics", type: "tags" }, + { name: "Educational", value: "Educational", type: "tags" }, + { name: "Ensemble Cast", value: "Ensemble Cast", type: "tags" }, + { name: "Environmental", value: "Environmental", type: "tags" }, + { name: "Episodic", value: "Episodic", type: "tags" }, + { name: "Espionage", value: "Espionage", type: "tags" }, + { name: "Fairy Tale", value: "Fairy Tale", type: "tags" }, + { name: "Family Life", value: "Family Life", type: "tags" }, + { name: "Fashion", value: "Fashion", type: "tags" }, + { name: "Female Protagonist", value: "Female Protagonist", type: "tags" }, + { name: "Fishing", value: "Fishing", type: "tags" }, + { name: "Fitness", value: "Fitness", type: "tags" }, + { name: "Flash", value: "Flash", type: "tags" }, + { name: "Food", value: "Food", type: "tags" }, + { name: "Football", value: "Football", type: "tags" }, + { name: "Foreign", value: "Foreign", type: "tags" }, + { name: "Fugitive", value: "Fugitive", type: "tags" }, + { name: "Full CGI", value: "Full CGI", type: "tags" }, + { name: "Full Colour", value: "Full Colour", type: "tags" }, + { name: "Gambling", value: "Gambling", type: "tags" }, + { name: "Gangs", value: "Gangs", type: "tags" }, + { name: "Gender Bending", value: "Gender Bending", type: "tags" }, + { name: "Gender Neutral", value: "Gender Neutral", type: "tags" }, + { name: "Ghost", value: "Ghost", type: "tags" }, + { name: "Gods", value: "Gods", type: "tags" }, + { name: "Gore", value: "Gore", type: "tags" }, + { name: "Guns", value: "Guns", type: "tags" }, + { name: "Gyaru", value: "Gyaru", type: "tags" }, + { name: "Harem", value: "Harem", type: "tags" }, + { name: "Henshin", value: "Henshin", type: "tags" }, + { name: "Hikikomori", value: "Hikikomori", type: "tags" }, + { name: "Historical", value: "Historical", type: "tags" }, + { name: "Ice Skating", value: "Ice Skating", type: "tags" }, + { name: "Idol", value: "Idol", type: "tags" }, + { name: "Isekai", value: "Isekai", type: "tags" }, + { name: "Iyashikei", value: "Iyashikei", type: "tags" }, + { name: "Josei", value: "Josei", type: "tags" }, + { name: "Kaiju", value: "Kaiju", type: "tags" }, + { name: "Karuta", value: "Karuta", type: "tags" }, + { name: "Kemonomimi", value: "Kemonomimi", type: "tags" }, + { name: "Kids", value: "Kids", type: "tags" }, + { name: "Love Triangle", value: "Love Triangle", type: "tags" }, + { name: "Mafia", value: "Mafia", type: "tags" }, + { name: "Magic", value: "Magic", type: "tags" }, + { name: "Mahjong", value: "Mahjong", type: "tags" }, + { name: "Maids", value: "Maids", type: "tags" }, + { name: "Male Protagonist", value: "Male Protagonist", type: "tags" }, + { name: "Martial Arts", value: "Martial Arts", type: "tags" }, + { name: "Memory Manipulation", value: "Memory Manipulation", type: "tags" }, + { name: "Meta", value: "Meta", type: "tags" }, + { name: "Military", value: "Military", type: "tags" }, + { name: "Monster Girl", value: "Monster Girl", type: "tags" }, + { name: "Mopeds", value: "Mopeds", type: "tags" }, + { name: "Motorcycles", value: "Motorcycles", type: "tags" }, + { name: "Musical", value: "Musical", type: "tags" }, + { name: "Mythology", value: "Mythology", type: "tags" }, + { name: "Nekomimi", value: "Nekomimi", type: "tags" }, + { name: "Ninja", value: "Ninja", type: "tags" }, + { name: "No Dialogue", value: "No Dialogue", type: "tags" }, + { name: "Noir", value: "Noir", type: "tags" }, + { name: "Nudity", value: "Nudity", type: "tags" }, + { name: "Otaku Culture", value: "Otaku Culture", type: "tags" }, + { name: "Outdoor", value: "Outdoor", type: "tags" }, + { name: "Parody", value: "Parody", type: "tags" }, + { name: "Philosophy", value: "Philosophy", type: "tags" }, + { name: "Photography", value: "Photography", type: "tags" }, + { name: "Pirates", value: "Pirates", type: "tags" }, + { name: "Poker", value: "Poker", type: "tags" }, + { name: "Police", value: "Police", type: "tags" }, + { name: "Politics", value: "Politics", type: "tags" }, + { name: "Post-Apocalyptic", value: "Post-Apocalyptic", type: "tags" }, + { name: "Primarily Adult Cast", value: "Primarily Adult Cast", type: "tags" }, + { + name: "Primarily Female Cast", + value: "Primarily Female Cast", + type: "tags", + }, + { name: "Primarily Male Cast", value: "Primarily Male Cast", type: "tags" }, + { name: "Puppetry", value: "Puppetry", type: "tags" }, + { name: "Real Robot", value: "Real Robot", type: "tags" }, + { name: "Rehabilitation", value: "Rehabilitation", type: "tags" }, + { name: "Reincarnation", value: "Reincarnation", type: "tags" }, + { name: "Revenge", value: "Revenge", type: "tags" }, + { name: "Reverse Harem", value: "Reverse Harem", type: "tags" }, + { name: "Robots", value: "Robots", type: "tags" }, + { name: "Rugby", value: "Rugby", type: "tags" }, + { name: "Rural", value: "Rural", type: "tags" }, + { name: "Samurai", value: "Samurai", type: "tags" }, + { name: "Satire", value: "Satire", type: "tags" }, + { name: "School", value: "School", type: "tags" }, + { name: "School Club", value: "School Club", type: "tags" }, + { name: "Seinen", value: "Seinen", type: "tags" }, + { name: "Ships", value: "Ships", type: "tags" }, + { name: "Shogi", value: "Shogi", type: "tags" }, + { name: "Shoujo", value: "Shoujo", type: "tags" }, + { name: "Shoujo Ai", value: "Shoujo Ai", type: "tags" }, + { name: "Shounen", value: "Shounen", type: "tags" }, + { name: "Shounen Ai", value: "Shounen Ai", type: "tags" }, + { name: "Slapstick", value: "Slapstick", type: "tags" }, + { name: "Slavery", value: "Slavery", type: "tags" }, + { name: "Space", value: "Space", type: "tags" }, + { name: "Space Opera", value: "Space Opera", type: "tags" }, + { name: "Steampunk", value: "Steampunk", type: "tags" }, + { name: "Stop Motion", value: "Stop Motion", type: "tags" }, + { name: "Super Power", value: "Super Power", type: "tags" }, + { name: "Super Robot", value: "Super Robot", type: "tags" }, + { name: "Superhero", value: "Superhero", type: "tags" }, + { name: "Surreal Comedy", value: "Surreal Comedy", type: "tags" }, + { name: "Survival", value: "Survival", type: "tags" }, + { name: "Swimming", value: "Swimming", type: "tags" }, + { name: "Swordplay", value: "Swordplay", type: "tags" }, + { name: "Table Tennis", value: "Table Tennis", type: "tags" }, + { name: "Tanks", value: "Tanks", type: "tags" }, + { name: "Teacher", value: "Teacher", type: "tags" }, + { name: "Tennis", value: "Tennis", type: "tags" }, + { name: "Terrorism", value: "Terrorism", type: "tags" }, + { name: "Time Manipulation", value: "Time Manipulation", type: "tags" }, + { name: "Time Skip", value: "Time Skip", type: "tags" }, + { name: "Tragedy", value: "Tragedy", type: "tags" }, + { name: "Trains", value: "Trains", type: "tags" }, + { name: "Triads", value: "Triads", type: "tags" }, + { name: "Tsundere", value: "Tsundere", type: "tags" }, + { name: "Urban Fantasy", value: "Urban Fantasy", type: "tags" }, + { name: "Vampire", value: "Vampire", type: "tags" }, + { name: "Video Games", value: "Video Games", type: "tags" }, + { name: "Virtual World", value: "Virtual World", type: "tags" }, + { name: "Volleyball", value: "Volleyball", type: "tags" }, + { name: "War", value: "War", type: "tags" }, + { name: "Witch", value: "Witch", type: "tags" }, + { name: "Work", value: "Work", type: "tags" }, + { name: "Wrestling", value: "Wrestling", type: "tags" }, + { name: "Writing", value: "Writing", type: "tags" }, + { name: "Wuxia", value: "Wuxia", type: "tags" }, + { name: "Yakuza", value: "Yakuza", type: "tags" }, + { name: "Yandere", value: "Yandere", type: "tags" }, + { name: "Youkai", value: "Youkai", type: "tags" }, + { name: "Zombie", value: "Zombie", type: "tags" }, +]; +export const formatOptions = [ + { name: "TV", value: "TV" }, + { name: "TV Short", value: "TV_SHORT" }, + { name: "Movie", value: "MOVIE" }, + { name: "Special", value: "SPECIAL" }, + { name: "OVA", value: "OVA" }, + { name: "ONA", value: "ONA" }, + { name: "Music", value: "MUSIC" }, + { name: "Manga", value: "MANGA" }, + { name: "Novel", value: "NOVEL" }, + { name: "One Shot", value: "ONE_SHOT" }, +]; +export const animeFormatOptions = [ + { name: "TV", value: "TV" }, + { name: "TV Short", value: "TV_SHORT" }, + { name: "Movie", value: "MOVIE" }, + { name: "Special", value: "SPECIAL" }, + { name: "OVA", value: "OVA" }, + { name: "ONA", value: "ONA" }, +]; +export const mangaFormatOptions = [ + { name: "Manga", value: "MANGA" }, + { name: "Novel", value: "NOVEL" }, + { name: "One Shot", value: "ONE_SHOT" }, +]; +export const sortOptions = [ + { name: "Date Added", value: "ID_DESC" }, + { name: "Title", value: "TITLE_ROMAJI" }, + { name: "Release Date", value: "START_DATE_DESC" }, + { name: "Average Score", value: "SCORE_DESC" }, + { name: "Popularity", value: "POPULARITY_DESC" }, + { name: "Trending", value: ["TRENDING_DESC", "POPULARITY_DESC"] }, + { name: "Favorites", value: "FAVOURITES_DESC" }, +]; +export const yearOptions = [ + { name: "1940", value: "1940" }, + { name: "1941", value: "1941" }, + { name: "1942", value: "1942" }, + { name: "1943", value: "1943" }, + { name: "1944", value: "1944" }, + { name: "1945", value: "1945" }, + { name: "1946", value: "1946" }, + { name: "1947", value: "1947" }, + { name: "1948", value: "1948" }, + { name: "1949", value: "1949" }, + { name: "1950", value: "1950" }, + { name: "1951", value: "1951" }, + { name: "1952", value: "1952" }, + { name: "1953", value: "1953" }, + { name: "1954", value: "1954" }, + { name: "1955", value: "1955" }, + { name: "1956", value: "1956" }, + { name: "1957", value: "1957" }, + { name: "1958", value: "1958" }, + { name: "1959", value: "1959" }, + { name: "1960", value: "1960" }, + { name: "1961", value: "1961" }, + { name: "1962", value: "1962" }, + { name: "1963", value: "1963" }, + { name: "1964", value: "1964" }, + { name: "1965", value: "1965" }, + { name: "1966", value: "1966" }, + { name: "1967", value: "1967" }, + { name: "1968", value: "1968" }, + { name: "1969", value: "1969" }, + { name: "1970", value: "1970" }, + { name: "1971", value: "1971" }, + { name: "1972", value: "1972" }, + { name: "1973", value: "1973" }, + { name: "1974", value: "1974" }, + { name: "1975", value: "1975" }, + { name: "1976", value: "1976" }, + { name: "1977", value: "1977" }, + { name: "1978", value: "1978" }, + { name: "1979", value: "1979" }, + { name: "1980", value: "1980" }, + { name: "1981", value: "1981" }, + { name: "1982", value: "1982" }, + { name: "1983", value: "1983" }, + { name: "1984", value: "1984" }, + { name: "1985", value: "1985" }, + { name: "1986", value: "1986" }, + { name: "1987", value: "1987" }, + { name: "1988", value: "1988" }, + { name: "1989", value: "1989" }, + { name: "1990", value: "1990" }, + { name: "1991", value: "1991" }, + { name: "1992", value: "1992" }, + { name: "1993", value: "1993" }, + { name: "1994", value: "1994" }, + { name: "1995", value: "1995" }, + { name: "1996", value: "1996" }, + { name: "1997", value: "1997" }, + { name: "1998", value: "1998" }, + { name: "1999", value: "1999" }, + { name: "2000", value: "2000" }, + { name: "2001", value: "2001" }, + { name: "2002", value: "2002" }, + { name: "2003", value: "2003" }, + { name: "2004", value: "2004" }, + { name: "2005", value: "2005" }, + { name: "2006", value: "2006" }, + { name: "2007", value: "2007" }, + { name: "2008", value: "2008" }, + { name: "2009", value: "2009" }, + { name: "2010", value: "2010" }, + { name: "2011", value: "2011" }, + { name: "2012", value: "2012" }, + { name: "2013", value: "2013" }, + { name: "2014", value: "2014" }, + { name: "2015", value: "2015" }, + { name: "2016", value: "2016" }, + { name: "2017", value: "2017" }, + { name: "2018", value: "2018" }, + { name: "2019", value: "2019" }, + { name: "2020", value: "2020" }, + { name: "2021", value: "2021" }, + { name: "2022", value: "2022" }, + { name: "2023", value: "2023" }, + { name: "2024", value: "2024" }, +]; +export const seasonOptions = [ + { name: "Winter", value: "WINTER" }, + { name: "Spring", value: "SPRING" }, + { name: "Summer", value: "SUMMER" }, + { name: "Fall", value: "FALL" }, +]; diff --git a/components/searchPalette.js b/components/searchPalette.js deleted file mode 100644 index b450423..0000000 --- a/components/searchPalette.js +++ /dev/null @@ -1,282 +0,0 @@ -import { Fragment, useEffect, useRef, useState } from "react"; -import { Combobox, Dialog, Menu, Transition } from "@headlessui/react"; -import useDebounce from "../lib/hooks/useDebounce"; -import Image from "next/image"; -import { useRouter } from "next/router"; -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 SearchByImage from "./search/searchByImage"; - -export default function SearchPalette() { - const { isOpen, setIsOpen } = useSearch(); - const { quickSearch } = useAniList(); - - const [query, setQuery] = useState(""); - const [data, setData] = useState(null); - const debounceSearch = useDebounce(query, 500); - const [loading, setLoading] = useState(false); - const [type, setType] = useState("ANIME"); - - const [nextPage, setNextPage] = useState(false); - - let focusInput = useRef(null); - const router = useRouter(); - - function closeModal() { - setIsOpen(false); - } - - function handleChange(event) { - router.push(`/en/${type.toLowerCase()}/${event}`); - } - - async function advance() { - setLoading(true); - const res = await quickSearch({ - search: debounceSearch, - type, - }); - setData(res?.data?.Page?.results); - setNextPage(res?.data?.Page?.pageInfo?.hasNextPage); - setLoading(false); - } - - useEffect(() => { - advance(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debounceSearch, type]); - - useEffect(() => { - const handleKeyDown = (e) => { - if (e.code === "KeyS" && e.ctrlKey) { - // do your stuff - e.preventDefault(); - setIsOpen((prev) => !prev); - setData(null); - setQuery(""); - } - }; - - window.addEventListener("keydown", handleKeyDown); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - - -
- - -
-
- - - { - handleChange(e); - setData(null); - setIsOpen(false); - setQuery(""); - }} - > -
-
-

For quick access :

-
- CTRL -
- + -
- S -
-
-
- -
- - {type.toLowerCase()} - -
- - -
- - {({ active }) => ( - - )} - - - {({ active }) => ( - - )} - -
-
-
-
- -
-
-
- setQuery(event.target.value)} - /> -
- - {!loading ? ( - - {data?.length > 0 - ? data?.map((i) => ( - - `flex items-center gap-3 p-5 ${ - active ? "bg-primary/40 cursor-pointer" : "" - }` - } - > -
- coverImage -
-
-

- {i.title.userPreferred} -

-

- {i.startDate.year} {getFormat(i.format)} -

-
-
- )) - : !loading && - debounceSearch !== "" && ( -

- No results found. -

- )} - {nextPage && ( - - )} -
- ) : ( -
-
-
- - - - -
-
-
- )} -
-
-
-
-
-
-
-
- ); -} diff --git a/components/searchPalette.tsx b/components/searchPalette.tsx new file mode 100644 index 0000000..b253f59 --- /dev/null +++ b/components/searchPalette.tsx @@ -0,0 +1,308 @@ +import { Fragment, useEffect, useRef, useState } from "react"; +import { Combobox, Dialog, Menu, Transition } from "@headlessui/react"; +import useDebounce from "@/lib/hooks/useDebounce"; +import Image from "next/image"; +import { useRouter } from "next/router"; +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 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 debounceSearch = useDebounce(query, 500); + const [loading, setLoading] = useState(false); + const [type, setType] = useState("ANIME"); + + const [nextPage, setNextPage] = useState(false); + + let focusInput = useRef(null); + const router = useRouter(); + + function closeModal() { + setIsOpen(false); + } + + function handleChange(event: string): void { + router.push(`/en/${type.toLowerCase()}/${event}`); + } + + async function advance(): Promise { + setLoading(true); + const res = await quickSearch({ + search: debounceSearch, + type, + }); + setData(res?.data?.Page?.results); + setNextPage(res?.data?.Page?.pageInfo?.hasNextPage); + setLoading(false); + } + + useEffect(() => { + advance(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debounceSearch, type]); + + useEffect(() => { + const handleKeyDown = (e: any) => { + if (e.code === "KeyS" && e.ctrlKey) { + // do your stuff + e.preventDefault(); + setIsOpen((prev: boolean) => !prev); + setData(null); + setQuery(""); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + +
+ + +
+
+ + + { + handleChange(e); + setData(null); + setIsOpen(false); + setQuery(""); + }} + > +
+
+

For quick access :

+
+ CTRL +
+ + +
+ S +
+
+
+ +
+ + {type.toLowerCase()} + +
+ + +
+ + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + +
+
+
+
+ +
+
+
+ setQuery(event.target.value)} + /> +
+ + {!loading ? ( + + {data && data?.length > 0 + ? data?.map((i) => ( + + `flex items-center gap-3 p-5 ${ + active ? "bg-primary/40 cursor-pointer" : "" + }` + } + > +
+ coverImage +
+
+

+ {i.title.userPreferred} +

+

+ {i.startDate.year} {getFormat(i.format)} +

+
+
+ )) + : !loading && + debounceSearch !== "" && ( +

+ No results found. +

+ )} + {nextPage && ( + + )} +
+ ) : ( +
+
+
+ + + + +
+
+
+ )} +
+
+
+
+
+
+
+
+ ); +} diff --git a/components/shared/MobileNav.js b/components/shared/MobileNav.js deleted file mode 100644 index d0f29c2..0000000 --- a/components/shared/MobileNav.js +++ /dev/null @@ -1,170 +0,0 @@ -import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; -import { CalendarIcon, HomeIcon } from "@heroicons/react/24/outline"; -import { signIn, signOut, useSession } from "next-auth/react"; -import Image from "next/image"; -import Link from "next/link"; -import { useState } from "react"; - -export default function MobileNav({ hideProfile = false }) { - const { data: sessions } = useSession(); - const [isVisible, setIsVisible] = useState(false); - - const handleShowClick = () => { - setIsVisible(true); - }; - - const handleHideClick = () => { - setIsVisible(false); - }; - return ( - <> - {/* NAVBAR */} -
- {!isVisible && ( - - )} -
- - {/* Mobile Menu */} -
- {isVisible && sessions && !hideProfile && ( - - user avatar - - )} - {isVisible && ( -
-
- - - - {sessions ? ( - - ) : ( - - )} -
- -
- )} -
- - ); -} diff --git a/components/shared/MobileNav.tsx b/components/shared/MobileNav.tsx new file mode 100644 index 0000000..7d6dfd6 --- /dev/null +++ b/components/shared/MobileNav.tsx @@ -0,0 +1,174 @@ +import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; +import { CalendarIcon, HomeIcon } from "@heroicons/react/24/outline"; +import { signIn, signOut, useSession } from "next-auth/react"; +import Image from "next/image"; +import Link from "next/link"; +import { useState } from "react"; + +type MobileNavProps = { + hideProfile?: boolean; +}; + +export default function MobileNav({ hideProfile = false }: MobileNavProps) { + const { data: sessions }: { data: any } = useSession(); + const [isVisible, setIsVisible] = useState(false); + + const handleShowClick = () => { + setIsVisible(true); + }; + + const handleHideClick = () => { + setIsVisible(false); + }; + return ( + <> + {/* NAVBAR */} +
+ {!isVisible && ( + + )} +
+ + {/* Mobile Menu */} +
+ {isVisible && sessions && !hideProfile && ( + + user avatar + + )} + {isVisible && ( +
+
+ + + + {sessions ? ( + + ) : ( + + )} +
+ +
+ )} +
+ + ); +} diff --git a/components/shared/NavBar.js b/components/shared/NavBar.js deleted file mode 100644 index 8cfdfc1..0000000 --- a/components/shared/NavBar.js +++ /dev/null @@ -1,267 +0,0 @@ -import { useSearch } from "@/lib/context/isOpenState"; -import { getCurrentSeason } from "@/utils/getTimes"; -import { ArrowLeftIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"; -import { UserIcon } from "@heroicons/react/24/solid"; -import { signIn, signOut, useSession } from "next-auth/react"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; - -const getScrollPosition = (el = window) => ({ - x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft, - y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop, -}); - -export function NewNavbar({ - info, - scrollP = 200, - toTop = false, - withNav = false, - paddingY = "py-3", - home = false, - back = false, - manga = false, - shrink = false, -}) { - const { data: session } = useSession(); - const router = useRouter(); - const [scrollPosition, setScrollPosition] = useState(); - const { setIsOpen } = useSearch(); - - const year = new Date().getFullYear(); - const season = getCurrentSeason(); - - useEffect(() => { - const handleScroll = () => { - setScrollPosition(getScrollPosition()); - }; - - // Add a scroll event listener when the component mounts - window.addEventListener("scroll", handleScroll); - - // Clean up the event listener when the component unmounts - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, []); - return ( - <> - - {toTop && ( - - )} - - ); -} diff --git a/components/shared/NavBar.tsx b/components/shared/NavBar.tsx new file mode 100644 index 0000000..6e8812e --- /dev/null +++ b/components/shared/NavBar.tsx @@ -0,0 +1,289 @@ +import { useSearch } from "@/lib/context/isOpenState"; +import { getCurrentSeason } from "@/utils/getTimes"; +import { ArrowLeftIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"; +import { UserIcon } from "@heroicons/react/24/solid"; +import { signIn, signOut, useSession } from "next-auth/react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { AniListInfoTypes } from "types/info/AnilistInfoTypes"; + +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 }; + } +}; + +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, + paddingY = "py-3", + home = false, + back = false, + manga = false, + shrink = false, + bgHover = false, +}: NavbarProps) { + const { data: session }: { data: any } = useSession(); + const router = useRouter(); + const [scrollPosition, setScrollPosition] = useState< + { x: number; y: number } | undefined + >(); + const { setIsOpen } = useSearch(); + + const year = new Date().getFullYear(); + const season = getCurrentSeason(); + + useEffect(() => { + const handleScroll = () => { + setScrollPosition(getScrollPosition()); + }; + + // Add a scroll event listener when the component mounts + window.addEventListener("scroll", handleScroll); + + // Clean up the event listener when the component unmounts + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + return ( + <> + + {toTop && ( + + )} + + ); +} diff --git a/components/shared/bugReport.js b/components/shared/bugReport.js deleted file mode 100644 index f6bd9f1..0000000 --- a/components/shared/bugReport.js +++ /dev/null @@ -1,194 +0,0 @@ -import { Fragment, useState } from "react"; -import { Dialog, Listbox, Transition } from "@headlessui/react"; -import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; -import { toast } from "sonner"; - -const severityOptions = [ - { id: 1, name: "Low" }, - { id: 2, name: "Medium" }, - { id: 3, name: "High" }, - { id: 4, name: "Critical" }, -]; - -const BugReportForm = ({ isOpen, setIsOpen }) => { - const [bugDescription, setBugDescription] = useState(""); - const [severity, setSeverity] = useState(severityOptions[0]); - - function closeModal() { - setIsOpen(false); - setBugDescription(""); - setSeverity(severityOptions[0]); - } - - const handleSubmit = async (e) => { - e.preventDefault(); - - const bugReport = { - desc: bugDescription, - severity: severity.name, - url: window.location.href, - createdAt: new Date().toISOString(), - }; - - try { - const res = await fetch("/api/v2/admin/bug-report", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - data: bugReport, - }), - }); - - const json = await res.json(); - toast.success(json.message); - closeModal(); - } catch (err) { - console.log(err); - toast.error("Something went wrong: " + err.message); - } - }; - - return ( - <> - - - -
- - -
-
- - -
-

- Report a Bug -

-
-
-
- - -
- -
- - - - {severity.name} - - - - - - - {severityOptions.map((person, personIdx) => ( - - `relative cursor-default select-none py-2 pl-10 pr-4 ${ - active - ? "bg-secondary/50 text-white" - : "text-gray-400" - }` - } - value={person} - > - {({ selected }) => ( - <> - - {person.name} - - {selected ? ( - - - ) : null} - - )} - - ))} - - -
-
-
-
- -
-
-
-
-
-
-
-
-
- - ); -}; - -export default BugReportForm; diff --git a/components/shared/bugReport.tsx b/components/shared/bugReport.tsx new file mode 100644 index 0000000..5c1e3f4 --- /dev/null +++ b/components/shared/bugReport.tsx @@ -0,0 +1,199 @@ +import { Fragment, useState } from "react"; +import { Dialog, Listbox, Transition } from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/20/solid"; +import { toast } from "sonner"; + +const severityOptions = [ + { id: 1, name: "Low" }, + { id: 2, name: "Medium" }, + { id: 3, name: "High" }, + { id: 4, name: "Critical" }, +]; + +interface BugReportFormProps { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +} + +const BugReportForm: React.FC = ({ isOpen, setIsOpen }) => { + const [bugDescription, setBugDescription] = useState(""); + const [severity, setSeverity] = useState(severityOptions[0]); + + function closeModal() { + setIsOpen(false); + setBugDescription(""); + setSeverity(severityOptions[0]); + } + + const handleSubmit = async (e: any) => { + e.preventDefault(); + + const bugReport = { + desc: bugDescription, + severity: severity.name, + url: window.location.href, + createdAt: new Date().toISOString(), + }; + + try { + const res = await fetch("/api/v2/admin/bug-report", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data: bugReport, + }), + }); + + const json = await res.json(); + toast.success(json.message); + closeModal(); + } catch (err: any) { + console.log(err); + toast.error("Something went wrong: " + err.message); + } + }; + + return ( + <> + + + +
+ + +
+
+ + +
+

+ Report a Bug +

+
+
+
+ + +
+ +
+ + + + {severity.name} + + + + + + + {severityOptions.map((person, personIdx) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active + ? "bg-secondary/50 text-white" + : "text-gray-400" + }` + } + value={person} + > + {({ selected }) => ( + <> + + {person.name} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + ); +}; + +export default BugReportForm; 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 ( + <> + + + +
+ + +
+
+ + + +
+

Changelogs

+
+ {/* Github Icon */} + + + + + + + + + + + + + + {/* Discord Icon */} + + + + + +
+
+
+
+

+ Hi! Welcome to the new changelogs section. Here you can + see a lists of the latest changes and updates to the site. +

+

+ *This update is still in it's pre-release state, please + expect to see some bugs. If you find any, please report + them. +

+
+ + {logs.map((x) => ( + + {x.changes.map((i, index) => ( +

- {i}

+ ))} +
+ ))} + + {/*
+
+

+ v4.3.0 + + pre + +

+
+
+ +
+
+

+ *This update is still in it's pre-release state, please + expect to see some bugs. If you find any, please report + them. +

+ +

- Added changelogs section

+

- Added recommendations based on user lists

+

- New Player!

+

- And other minor bug fixes!

+
+
*/} + +
+

+ see more changelogs{" "} + + here + +

+
+ +
+
+ +
+ + +
+
+
+
+ + ); +} + +type ChangelogsVersionsProps = { + version?: string; + pre: boolean; + notes?: string | null; + highlights?: boolean; + children: React.ReactNode; +}; + +export function ChangelogsVersions({ + version, + pre, + notes, + highlights, + children, +}: ChangelogsVersionsProps) { + return ( + <> +
+
+

+ {version} + {pre && ( + + pre + + )} +

+
+
+ +
+
+ {notes && ( +

*{notes}

+ )} + {children} +
+
+ + ); +} diff --git a/components/shared/footer.js b/components/shared/footer.js deleted file mode 100644 index a29a3d3..0000000 --- a/components/shared/footer.js +++ /dev/null @@ -1,185 +0,0 @@ -import Link from "next/link"; -import { useState } from "react"; -import { useRouter } from "next/router"; - -function Footer() { - const [year] = useState(new Date().getFullYear()); - const [season] = useState(getCurrentSeason()); - - const [checked, setChecked] = useState(false); - - const router = useRouter(); - - function switchLang() { - setChecked(!checked); - if (checked) { - router.push("/en"); - } else { - router.push("/id"); - } - } - - return ( -
-
-
-
-
moopa
-

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

-
-
-
-
    -
  • - - This Season - -
  • -
  • - Popular Anime -
  • -
  • - Popular Manga -
  • -
  • - Donate -
  • -
-
    -
  • - Movies -
  • -
  • - TV Shows -
  • -
  • - DMCA -
  • -
  • - - Github - -
  • -
-
-
-
-
-
-
-

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

-
- {/* Github Icon */} - - - - - - - - - - - - - - {/* Discord Icon */} - - - - - - - {/* Kofi */} - - - - - - - -
-
-
-
- ); -} - -export default Footer; - -function getCurrentSeason() { - const now = new Date(); - const month = now.getMonth() + 1; // getMonth() returns 0-based index - - switch (month) { - case 12: - case 1: - case 2: - return "WINTER"; - case 3: - case 4: - case 5: - return "SPRING"; - case 6: - case 7: - case 8: - return "SUMMER"; - case 9: - case 10: - case 11: - return "FALL"; - default: - return "UNKNOWN SEASON"; - } -} diff --git a/components/shared/footer.tsx b/components/shared/footer.tsx new file mode 100644 index 0000000..2a513a3 --- /dev/null +++ b/components/shared/footer.tsx @@ -0,0 +1,179 @@ +import Link from "next/link"; +import { useState } from "react"; +import { useRouter } from "next/router"; + +function Footer() { + const [year] = useState(new Date().getFullYear()); + const [season] = useState(getCurrentSeason()); + + const [checked, setChecked] = useState(false); + + const router = useRouter(); + + function switchLang() { + setChecked(!checked); + if (checked) { + router.push("/en"); + } else { + router.push("/id"); + } + } + + return ( +
+
+
+
+
moopa
+

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

+
+
+
+
    +
  • + + This Season + +
  • +
  • + Popular Anime +
  • +
  • + Popular Manga +
  • +
  • + Donate +
  • +
+
    +
  • + Movies +
  • +
  • + TV Shows +
  • +
  • + DMCA +
  • +
  • + + Github + +
  • +
+
+
+
+
+
+
+

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

+
+ {/* Github Icon */} + + + + + + + + + + + + + + {/* Discord Icon */} + + + + + + + {/* Kofi */} + + + + + + + +
+
+
+
+ ); +} + +export default Footer; + +function getCurrentSeason() { + const now = new Date(); + const month = now.getMonth() + 1; // getMonth() returns 0-based index + + switch (month) { + case 12: + case 1: + case 2: + return "WINTER"; + case 3: + case 4: + case 5: + return "SPRING"; + case 6: + case 7: + case 8: + return "SUMMER"; + case 9: + case 10: + case 11: + return "FALL"; + default: + return "UNKNOWN SEASON"; + } +} 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 ( - - {/* Mobile Hamburger */} - {!isVisible && ( - - )} -
- {isVisible && ( -
-
- - - - {session ? ( - - ) : ( - - )} -
- -
- )} -
-
- ); -} 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 ( - <> -
- {/* */} -
-

Please Wait...

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

Please Wait...

+
+ {/*
*/} +
+ ); +} 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 ( +
+ + + + +
+ ); +} 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 ( + + + + + + + + {tooltipText} + + + ); +} + +export function MobilePlayButton({ tooltipPlacement, host }: MediaButtonProps) { + const isPaused = useMediaState("paused"), + ended = useMediaState("ended"), + Icon = ended ? ReplayIcon : isPaused ? PlayIcon : PauseIcon; + return ( + + + + + + + {/* + {tooltipText} + */} + + ); +} + +export function Mute({ tooltipPlacement }: MediaButtonProps) { + const volume = useMediaState("volume"), + isMuted = useMediaState("muted"); + return ( + + + + {isMuted || volume == 0 ? ( + + ) : volume < 0.5 ? ( + + ) : ( + + )} + + + + {isMuted ? "Unmute" : "Mute"} + + + ); +} + +export function Caption({ tooltipPlacement }: MediaButtonProps) { + const track = useMediaState("textTrack"), + isOn = track && isTrackCaptionKind(track); + return ( + + + + {isOn ? ( + + ) : ( + + )} + + + + {isOn ? "Closed-Captions On" : "Closed-Captions Off"} + + + ); +} + +export function TheaterButton({ tooltipPlacement }: MediaButtonProps) { + const playerState = useMediaState("currentTime"), + isPlaying = useMediaState("playing"); + + const { setPlayerState, setTheaterMode, theaterMode } = useWatchProvider(); + + return ( + + + + + + Theatre Mode + + + ); +} + +export function PIP({ tooltipPlacement }: MediaButtonProps) { + const isActive = useMediaState("pictureInPicture"); + return ( + + + + {isActive ? ( + + ) : ( + + )} + + + + {isActive ? "Exit PIP" : "Enter PIP"} + + + ); +} + +export function PlayNextButton({ + tooltipPlacement, + navigation, +}: MediaButtonProps) { + // const remote = useMediaRemote(); + const router = useRouter(); + const { dataMedia, track } = useWatchProvider(); + return ( + + ); +} + +export function SkipOpButton({ tooltipPlacement }: MediaButtonProps) { + const remote = useMediaRemote(); + const { track } = useWatchProvider(); + const op = track?.skip?.find((item: any) => item.text === "Opening"); + + return ( + + ); +} + +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 ( + + ); +} + +export function Fullscreen({ tooltipPlacement }: MediaButtonProps) { + const isActive = useMediaState("fullscreen"); + return ( + + + + {isActive ? ( + + ) : ( + + )} + + + + {isActive ? "Exit Fullscreen" : "Enter Fullscreen"} + + + ); +} 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 ( + + + + + ); +} 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 ( + <> + + + + + + <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.js deleted file mode 100644 index 4af12ac..0000000 --- a/components/watch/primary/details.js +++ /dev/null @@ -1,193 +0,0 @@ -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"; - -export default function Details({ - info, - session, - epiNumber, - description, - id, - onList, - setOnList, - handleOpen, - disqus, -}) { - const [showComments, setShowComments] = useState(false); - const { markPlanning } = useAniList(session); - - function handlePlan() { - if (onList === false) { - markPlanning(info.id); - setOnList(true); - } - } - - useEffect(() => { - const isMobile = window.matchMedia("(max-width: 768px)").matches; - if (isMobile) { - setShowComments(false); - } else { - setShowComments(true); - } - }, [id]); - - return ( - <div className="flex flex-col gap-2"> - {/* <div className="px-4 pt-7 pb-4 h-full flex"> */} - <div className="pb-4 h-full flex"> - <div className="aspect-[9/13] h-[240px]"> - {info ? ( - <img - src={info.coverImage.extraLarge} - alt="Anime Cover" - width={1000} - height={1000} - className="object-cover aspect-[9/13] h-[240px] rounded-md" - /> - ) : ( - <Skeleton height={240} /> - )} - </div> - <div - className="grid w-full pl-5 gap-3 h-[240px]" - data-episode={info?.episodes || "0"} - > - <div className="grid grid-cols-2 gap-1 items-center"> - <h2 className="text-sm font-light font-roboto text-[#878787]"> - Studios - </h2> - <div className="row-start-2"> - {info ? ( - info.studios?.edges[0].node.name - ) : ( - <Skeleton width={80} /> - )} - </div> - <div className="hidden xxs:grid col-start-2 place-content-end relative"> - <div> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - onClick={() => { - session ? handlePlan() : handleOpen(); - }} - className={`w-8 h-8 hover:fill-white text-white hover:cursor-pointer ${ - onList ? "fill-white" : "" - }`} - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" - /> - </svg> - </div> - </div> - </div> - <div className="grid gap-1 items-center"> - <h2 className="text-sm font-light font-roboto text-[#878787]"> - Status - </h2> - <div>{info ? info.status : <Skeleton width={75} />}</div> - </div> - <div className="grid gap-1 items-center overflow-y-hidden"> - <h2 className="text-sm font-light font-roboto text-[#878787]"> - Titles - </h2> - <div className="grid grid-flow-dense grid-cols-2 gap-2 h-full w-full"> - {info ? ( - <> - <div className="title-rm line-clamp-3"> - {info.title?.romaji || ""} - </div> - <div className="title-en line-clamp-3"> - {info.title?.english || ""} - </div> - <div className="title-nt line-clamp-3"> - {info.title?.native || ""} - </div> - </> - ) : ( - <Skeleton width={200} height={50} /> - )} - </div> - </div> - </div> - </div> - {/* <div className="flex flex-wrap gap-3 px-4 pt-3"> */} - <div className="flex flex-wrap gap-3 pt-3"> - {info && - info.genres?.map((item, index) => ( - <div - key={index} - className="border border-action text-gray-100 py-1 px-2 rounded-md font-karla text-sm" - > - {item} - </div> - ))} - </div> - {/* <div className={`bg-secondary rounded-md mt-3 mx-3`}> */} - <div className={`bg-secondary rounded-md mt-3`}> - {info && ( - <p - dangerouslySetInnerHTML={{ __html: description }} - className={`p-5 text-sm font-light font-roboto text-[#e4e4e4] `} - /> - )} - </div> - {/* {<div className="mt-5 px-5"></div>} */} - {!showComments && ( - <div className="w-full flex justify-center py-2 font-karla lg:px-0"> - <button - onClick={() => setShowComments(true)} - className={ - showComments - ? "hidden" - : "flex-center gap-2 h-10 bg-secondary rounded w-full lg:w-[50%]" - } - > - Load Disqus{" "} - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth="1.5" - stroke="currentColor" - className="w-5 h-5" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" - /> - </svg> - </button> - </div> - )} - {showComments && ( - <div> - {info && ( - <div className="mt-5"> - <DisqusComments - key={id} - post={{ - id: id, - title: info.title.romaji, - url: window.location.href, - episode: epiNumber, - name: disqus, - }} - /> - </div> - )} - </div> - )} - </div> - ); -} diff --git a/components/watch/primary/details.tsx b/components/watch/primary/details.tsx new file mode 100644 index 0000000..f20f8cf --- /dev/null +++ b/components/watch/primary/details.tsx @@ -0,0 +1,234 @@ +import { useEffect, useState } from "react"; +import { useAniList } from "../../../lib/anilist/useAnilist"; +import Skeleton from "react-loading-skeleton"; +import DisqusComments from "../../disqus"; +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, + session, + epiNumber, + description, + id, + onList, + 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); + setOnList(true); + } + } + + useEffect(() => { + const isMobile = window.matchMedia("(max-width: 768px)").matches; + if (isMobile) { + setShowComments(false); + } else { + setShowComments(true); + } + return () => { + setShowComments(false); + setShowDesc(false); + }; + }, [id]); + + return ( + <div className="flex flex-col gap-2"> + {/* <div className="px-4 pt-7 pb-4 h-full flex"> */} + <div className="pb-4 h-full flex"> + <div className="aspect-[9/13] h-[240px]"> + {info ? ( + <img + src={info.coverImage.extraLarge} + alt="Anime Cover" + width={1000} + height={1000} + className="object-cover aspect-[9/13] h-[240px] rounded-md" + /> + ) : ( + <Skeleton height={240} /> + )} + </div> + <div + className="grid w-full pl-5 gap-3 h-[240px]" + data-episode={info?.episodes || "0"} + > + <div className="grid grid-cols-2 gap-1 items-center"> + <h2 className="text-sm font-light font-roboto text-[#878787]"> + Studios + </h2> + <div className="row-start-2"> + {info ? ( + info.studios?.edges[0].node.name + ) : ( + <Skeleton width={80} /> + )} + </div> + <div className="hidden xxs:grid col-start-2 place-content-end relative"> + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth={1.5} + stroke="currentColor" + onClick={() => { + session ? handlePlan() : handleOpen(); + }} + className={`w-8 h-8 hover:fill-white text-white hover:cursor-pointer ${ + onList ? "fill-white" : "" + }`} + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z" + /> + </svg> + </div> + </div> + </div> + <div className="grid gap-1 items-center"> + <h2 className="text-sm font-light font-roboto text-[#878787]"> + Status + </h2> + <div>{info ? info.status : <Skeleton width={75} />}</div> + </div> + <div className="grid gap-1 items-center overflow-y-hidden"> + <h2 className="text-sm font-light font-roboto text-[#878787]"> + Titles + </h2> + <div className="grid grid-flow-dense grid-cols-2 gap-2 h-full w-full"> + {info ? ( + <> + <div className="title-rm line-clamp-3"> + {info.title?.romaji || ""} + </div> + <div className="title-en line-clamp-3"> + {info.title?.english || ""} + </div> + <div className="title-nt line-clamp-3"> + {info.title?.native || ""} + </div> + </> + ) : ( + <Skeleton width={200} height={50} /> + )} + </div> + </div> + </div> + </div> + {/* <div className="flex flex-wrap gap-3 px-4 pt-3"> */} + <div className="flex flex-wrap gap-3 pt-3"> + {info && + info.genres?.map((item, index) => ( + <div + key={index} + className="border border-action text-gray-100 py-1 px-2 rounded-md font-karla text-sm" + > + {item} + </div> + ))} + </div> + {/* <div className={`bg-secondary rounded-md mt-3 mx-3`}> */} + <div className={`relative bg-secondary rounded-md mt-3`}> + {info && ( + <> + <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>} */} + {!showComments && ( + <div className="w-full flex justify-center py-2 font-karla lg:px-0"> + <button + onClick={() => setShowComments(true)} + className={ + showComments + ? "hidden" + : "flex-center gap-2 h-10 bg-secondary rounded w-full lg:w-[50%]" + } + > + Load Disqus{" "} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + strokeWidth="1.5" + stroke="currentColor" + className="w-5 h-5" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" + /> + </svg> + </button> + </div> + )} + {showComments && ( + <div> + {info && ( + <div className="mt-5"> + <DisqusComments + key={id} + post={{ + title: info.title.romaji, + url: window.location.href, + episode: epiNumber, + name: disqus, + }} + /> + </div> + )} + </div> + )} + </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.js deleted file mode 100644 index a676be0..0000000 --- a/components/watch/secondary/episodeLists.js +++ /dev/null @@ -1,189 +0,0 @@ -import Skeleton from "react-loading-skeleton"; -import Image from "next/image"; -import Link from "next/link"; -import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { useRouter } from "next/router"; - -export default function EpisodeLists({ - info, - map, - providerId, - watchId, - episode, - artStorage, - track, - dub, -}) { - const progress = info.mediaListEntry?.progress; - - const router = useRouter(); - - return ( - <div className="w-screen lg:max-w-sm xl:max-w-lg"> - <div className="flex gap-4 px-3 lg:pl-5 pb-5"> - <button - disabled={!track?.next} - onClick={() => { - router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${ - track?.next?.id - }&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}` - ); - }} - className="text-xl font-karla font-semibold" - > - Next Episode {">"} - </button> - {episode && ( - <div className="relative flex gap-2 items-center group"> - <select - value={track?.playing?.number} - onChange={(e) => { - const selectedEpisode = episode.find( - (episode) => episode.number === parseInt(e.target.value) - ); - - router.push( - `/en/anime/watch/${info.id}/${providerId}?id=${ - selectedEpisode.id - }&num=${selectedEpisode.number}${dub ? `&dub=${dub}` : ""}` - ); - }} - 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" - > - {episode?.map((x) => ( - <option key={x.id} value={x.number}> - Episode {x.number} - </option> - ))} - </select> - <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> - </div> - )} - </div> - <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?.img || item?.image) && - !item?.img?.includes("https://s4.anilist.co/") - ) > 0 ? ( - episode.map((item) => { - const time = artStorage?.[item.id]?.timeWatched; - const duration = artStorage?.[item.id]?.duration; - let prog = (time / duration) * 100; - if (prog > 90) prog = 100; - - const mapData = map?.find((i) => i.number === item.number); - return ( - <Link - href={`/en/anime/watch/${ - info.id - }/${providerId}?id=${encodeURIComponent(item.id)}&num=${ - item.number - }${dub ? `&dub=${dub}` : ""}`} - key={item.id} - className={`bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out ${ - item.id == watchId - ? "pointer-events-none ring-1 ring-action" - : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" - }`} - > - <div className="w-[43%] lg:w-[42%] h-[110px] relative rounded-lg z-40 shrink-0 overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> - <div className="relative"> - {/* <div className="absolute inset-0 w-full h-full z-40" /> */} - <Image - src={ - mapData?.img || - mapData?.image || - info?.coverImage?.extraLarge - } - draggable={false} - alt="Anime Cover" - width={1000} - height={1000} - className={`object-cover z-30 rounded-lg h-[110px] ${ - item.id == watchId - ? "brightness-[30%]" - : "brightness-75" - }`} - /> - {/* )} */} - <span - className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} - style={{ - width: - progress !== undefined && progress >= item?.number - ? "100%" - : artStorage?.[item?.id] !== undefined - ? `${prog}%` - : "0%", - }} - /> - <span className="absolute bottom-2 left-2 font-karla font-bold text-sm text-white"> - Episode {item?.number} - </span> - {item.id == watchId && ( - <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]"> - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - className="w-5 h-5" - > - <path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" /> - </svg> - </div> - )} - </div> - </div> - <div - className={`w-full h-full overflow-x-hidden select-none p-4 flex flex-col gap-2 ${ - item.id == watchId ? "text-[#7a7a7a]" : "" - }`} - > - <h1 className="font-karla font-bold italic line-clamp-1"> - {mapData?.title || info?.title?.romaji} - </h1> - <p className="line-clamp-2 text-xs italic font-outfit font-extralight"> - {mapData?.description || `Episode ${item.number}`} - </p> - </div> - </Link> - ); - }) - ) : ( - episode.map((item) => { - return ( - <Link - href={`/en/anime/watch/${ - info.id - }/${providerId}?id=${encodeURIComponent(item.id)}&num=${ - item.number - }${dub ? `&dub=${dub}` : ""}`} - key={item.id} - className={`bg-secondary flex-center h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${ - item.id == watchId - ? "pointer-events-none ring-1 ring-action text-[#5d5d5d]" - : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" - }`} - > - Episode {item.number} - </Link> - ); - }) - ) - ) : ( - <> - {[1].map((item) => ( - <Skeleton - key={item} - className="bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out" - /> - ))} - </> - )} - </div> - </div> - ); -} diff --git a/components/watch/secondary/episodeLists.tsx b/components/watch/secondary/episodeLists.tsx new file mode 100644 index 0000000..2c23f25 --- /dev/null +++ b/components/watch/secondary/episodeLists.tsx @@ -0,0 +1,205 @@ +import Skeleton from "react-loading-skeleton"; +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, + map, + providerId, + watchId, + episode, + artStorage, + track, + dub, +}: EpisodeListsProps) { + const progress = info.mediaListEntry?.progress; + + const router = useRouter(); + + return ( + <div className="w-screen lg:max-w-sm xl:max-w-lg"> + <div className="flex gap-4 px-3 lg:pl-5 pb-5"> + <button + disabled={!track?.next} + onClick={() => { + router.push( + `/en/anime/watch/${info.id}/${providerId}?id=${ + track?.next?.id + }&num=${track?.next?.number}${dub ? `&dub=${dub}` : ""}` + ); + }} + className="text-xl font-karla font-semibold" + > + Next Episode {">"} + </button> + {episode && ( + <div className="relative flex gap-2 items-center group"> + <select + value={track?.playing?.number} + onChange={(e) => { + const selectedEpisode = episode.find( + (episode) => episode.number === parseInt(e.target.value) + ); + + router.push( + `/en/anime/watch/${info.id}/${providerId}?id=${ + selectedEpisode?.id + }&num=${selectedEpisode?.number}${dub ? `&dub=${dub}` : ""}` + ); + }} + 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" + > + {episode?.map((x) => ( + <option key={x.id} value={x.number}> + Episode {x.number} + </option> + ))} + </select> + <ChevronDownIcon className="absolute right-2 top-1/2 transform -translate-y-1/2 w-5 h-5 pointer-events-none" /> + </div> + )} + </div> + <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: any) => + (item?.img || item?.image) && + !item?.img?.includes("https://s4.anilist.co/") + ) > 0 ? ( + episode.map((item) => { + const time = artStorage?.[item.id]?.timeWatched; + const duration = artStorage?.[item.id]?.duration; + let prog = (time / duration) * 100; + if (prog > 90) prog = 100; + + 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/${ + info.id + }/${providerId}?id=${encodeURIComponent(item.id)}&num=${ + item.number + }${dub ? `&dub=${dub}` : ""}`} + key={item.id} + className={`bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out ${ + item.id == watchId + ? "pointer-events-none ring-1 ring-action" + : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" + }`} + > + <div className="w-[43%] lg:w-[42%] h-[110px] relative rounded-lg z-40 shrink-0 overflow-hidden shadow-[4px_0px_5px_0px_rgba(0,0,0,0.3)]"> + <div className="relative"> + {/* <div className="absolute inset-0 w-full h-full z-40" /> */} + <Image + src={parsedImage || info?.coverImage?.extraLarge} + draggable={false} + alt="Anime Cover" + width={1000} + height={1000} + className={`object-cover z-30 rounded-lg h-[110px] ${ + item.id == watchId + ? "brightness-[30%]" + : "brightness-75" + }`} + /> + {/* )} */} + <span + className={`absolute bottom-0 left-0 h-[2px] bg-red-700`} + style={{ + width: + progress !== undefined && progress >= item?.number + ? "100%" + : artStorage?.[item?.id] !== undefined + ? `${prog}%` + : "0%", + }} + /> + <span className="absolute bottom-2 left-2 font-karla font-bold text-sm text-white"> + Episode {item?.number} + </span> + {item.id == watchId && ( + <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 scale-[1.5]"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + className="w-5 h-5" + > + <path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" /> + </svg> + </div> + )} + </div> + </div> + <div + className={`w-full h-full overflow-x-hidden select-none p-4 flex flex-col gap-2 ${ + item.id == watchId ? "text-[#7a7a7a]" : "" + }`} + > + <h1 className="font-karla font-bold italic line-clamp-1"> + {mapData?.title || info?.title?.romaji} + </h1> + <p className="line-clamp-2 text-xs italic font-outfit font-extralight"> + {mapData?.description || `Episode ${item.number}`} + </p> + </div> + </Link> + ); + }) + ) : ( + episode.map((item) => { + return ( + <Link + href={`/en/anime/watch/${ + info.id + }/${providerId}?id=${encodeURIComponent(item.id)}&num=${ + item.number + }${dub ? `&dub=${dub}` : ""}`} + key={item.id} + className={`bg-secondary flex-center h-[50px] rounded-lg scale-100 transition-all duration-300 ease-out ${ + item.id == watchId + ? "pointer-events-none ring-1 ring-action text-[#5d5d5d]" + : "cursor-pointer hover:scale-[1.02] ring-0 hover:ring-1 hover:shadow-lg ring-white" + }`} + > + Episode {item.number} + </Link> + ); + }) + ) + ) : ( + <> + {[1].map((item) => ( + <Skeleton + key={item} + className="bg-secondary flex w-full h-[110px] rounded-lg scale-100 transition-all duration-300 ease-out" + /> + ))} + </> + )} + </div> + </div> + ); +} 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.js deleted file mode 100644 index 6b1445f..0000000 --- a/lib/anify/getMangaId.js +++ /dev/null @@ -1,40 +0,0 @@ -import axios from "axios"; - -export async function fetchInfo(romaji, english, native) { - try { - const { data: getManga } = await axios.get( - `https://api.anify.tv/search-advanced?query=${ - english || romaji - }&type=manga` - ); - - const findManga = getManga?.results?.find( - (manga) => - manga.title.romaji === romaji || - manga.title.english === english || - manga.title.native === native - ); - - if (!findManga) { - return null; - } - - return { id: findManga.id }; - } catch (error) { - console.error("Error fetching data:", error); - return null; - } -} - -export default async function getMangaId(romaji, english, native) { - try { - const data = await fetchInfo(romaji, english, native); - if (data) { - return data; - } else { - return { message: "Schedule not found" }; - } - } catch (error) { - return { error }; - } -} diff --git a/lib/anify/getMangaId.ts b/lib/anify/getMangaId.ts new file mode 100644 index 0000000..bf7bb71 --- /dev/null +++ b/lib/anify/getMangaId.ts @@ -0,0 +1,61 @@ +import axios, { AxiosResponse } from "axios"; + +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 }: AxiosResponse<SearchResult> = await axios.get( + `https://api.anify.tv/search-advanced?query=${ + english || romaji + }&type=manga` + ); + + const findManga = getManga?.results?.find( + (manga) => + manga.title.romaji === romaji || + manga.title.english === english || + manga.title.native === native + ); + + if (!findManga) { + return null; + } + + return { id: findManga.id }; + } catch (error) { + console.error("Error fetching data:", error); + return null; + } +} + +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 && "id" in data) { + return data; + } else { + return { message: "Schedule not found" }; + } + } catch (error) { + return { error }; + } +} 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.js deleted file mode 100644 index ccfbd27..0000000 --- a/lib/anilist/aniAdvanceSearch.js +++ /dev/null @@ -1,131 +0,0 @@ -import { advanceSearchQuery } from "../graphql/query"; - -export async function aniAdvanceSearch({ - search, - type, - genres, - page, - sort, - format, - season, - seasonYear, - perPage, -}) { - const categorizedGenres = genres?.reduce((result, item) => { - const existingEntry = result[item.type]; - - if (existingEntry) { - existingEntry.push(item.value); - } else { - result[item.type] = [item.value]; - } - - return result; - }, {}); - - if (type === "MANGA") { - const controller = new AbortController(); - const signal = controller.signal; - - const response = await fetch("https://api.anify.tv/search-advanced", { - method: "POST", - signal: signal, - body: JSON.stringify({ - sort: "averageRating", - sortDirection: "DESC", - ...(categorizedGenres && { ...categorizedGenres }), - ...(search && { query: search }), - ...(page && { page: page }), - ...(perPage && { perPage: perPage }), - ...(format && { format: [format] }), - ...(seasonYear && { year: Number(seasonYear) }), - ...(type && { type: format === "NOVEL" ? "novel" : type }), - }), - }); - - const data = await response.json(); - return { - pageInfo: { - hasNextPage: page < data.total, - currentPage: page, - lastPage: Math.ceil(data.lastPage), - perPage: perPage ?? 20, - total: data.total, - }, - media: data.results?.map((item) => ({ - averageScore: item.averageRating, - bannerImage: item.bannerImage, - chapters: item.totalChapters, - coverImage: { - color: item.color, - extraLarge: item.coverImage, - large: item.coverImage, - }, - description: item.description, - duration: item.duration ?? null, - endDate: { - day: null, - month: null, - year: null, - }, - mappings: item.mappings, - format: item.format, - genres: item.genres, - id: item.id, - isAdult: false, - mediaListEntry: null, - nextAiringEpisode: null, - popularity: item.averagePopularity, - season: null, - seasonYear: item.year, - startDate: { - day: null, - month: null, - year: item.year, - }, - status: item.status, - studios: { edges: [] }, - title: { - userPreferred: - item.title.english ?? item.title.romaji ?? item.title.native, - }, - type: item.type, - volumes: item.totalVolumes ?? null, - })), - }; - } else { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: advanceSearchQuery, - variables: { - ...(search && { - search: search, - ...(!sort && { sort: "SEARCH_MATCH" }), - }), - ...(type && { type: type }), - ...(seasonYear && { seasonYear: seasonYear }), - ...(season && { - season: season, - ...(!seasonYear && { seasonYear: new Date().getFullYear() }), - }), - ...(categorizedGenres && { ...categorizedGenres }), - ...(format && { format: format }), - // ...(genres && { genres: genres }), - // ...(tags && { tags: tags }), - ...(perPage && { perPage: perPage }), - ...(sort && { sort: sort }), - ...(page && { page: page }), - }, - }), - }); - - const datas = await response.json(); - // console.log(datas); - const data = datas.data.Page; - return data; - } -} diff --git a/lib/anilist/aniAdvanceSearch.ts b/lib/anilist/aniAdvanceSearch.ts new file mode 100644 index 0000000..5251815 --- /dev/null +++ b/lib/anilist/aniAdvanceSearch.ts @@ -0,0 +1,155 @@ +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 = "ANIME", + genres, + page, + sort, + format, + season, + seasonYear, + perPage, +}: AniAdvanceSearch) { + const categorizedGenres = genres?.reduce((result, item) => { + const existingEntry = result[item.type]; + + if (existingEntry) { + existingEntry.push(item.value); + } else { + result[item.type] = [item.value]; + } + + return result; + }, {}); + + if (type === "MANGA") { + const controller = new AbortController(); + const signal = controller.signal; + + const response = await fetch("https://api.anify.tv/search-advanced", { + method: "POST", + signal: signal, + body: JSON.stringify({ + sort: "averageRating", + sortDirection: "DESC", + ...(categorizedGenres && { ...categorizedGenres }), + ...(search && { query: search }), + ...(page && { page: page }), + ...(perPage && { perPage: perPage }), + ...(format && { format: [format] }), + ...(seasonYear && { year: Number(seasonYear) }), + ...(type && { type: format === "NOVEL" ? "novel" : type }), + }), + }); + + const data: AnifySearchAdvanceTypes = await response.json(); + return { + pageInfo: { + hasNextPage: page ?? 0 < data.total, + currentPage: page, + lastPage: Math.ceil(data.lastPage), + perPage: perPage ?? 20, + total: data.total, + }, + media: data.results?.map((item) => ({ + averageScore: item.averageRating, + bannerImage: item.bannerImage, + chapters: item.totalChapters, + coverImage: { + color: item.color, + extraLarge: item.coverImage, + large: item.coverImage, + }, + description: item.description, + duration: item?.duration ?? null, + endDate: { + day: null, + month: null, + year: null, + }, + mappings: item.mappings, + format: item.format, + genres: item.genres, + id: item.id, + isAdult: false, + mediaListEntry: null, + nextAiringEpisode: null, + popularity: item.averagePopularity, + season: null, + seasonYear: item.year, + startDate: { + day: null, + month: null, + year: item.year, + }, + status: item.status, + studios: { edges: [] }, + title: { + userPreferred: + item.title.english ?? item.title.romaji ?? item.title.native, + }, + type: item.type, + volumes: item.totalVolumes ?? null, + })), + }; + } else { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query: advanceSearchQuery, + variables: { + ...(search && { + search: search, + ...(!sort && { sort: "SEARCH_MATCH" }), + }), + ...(type && { type: type }), + ...(seasonYear && { seasonYear: seasonYear }), + ...(season && { + season: season, + ...(!seasonYear && { seasonYear: new Date().getFullYear() }), + }), + ...(categorizedGenres && { ...categorizedGenres }), + ...(format && { format: format }), + // ...(genres && { genres: genres }), + // ...(tags && { tags: tags }), + ...(perPage && { perPage: perPage }), + ...(sort && { sort: sort }), + ...(page && { page: page }), + }, + }), + }); + + const datas = await response.json(); + // console.log(datas); + const data = datas.data.Page; + return data; + } +} 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/lib/hooks/useCountdownSeconds.ts b/lib/hooks/useCountdownSeconds.ts new file mode 100644 index 0000000..3d17ede --- /dev/null +++ b/lib/hooks/useCountdownSeconds.ts @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; + +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( + countDownDate - new Date().getTime() + ); + + useEffect(() => { + const interval = setInterval(() => { + const newCountDown = countDownDate - new Date().getTime(); + setCountDown(newCountDown); + if (newCountDown <= 0 && newCountDown > -1000) { + update(); + } + }, 1000); + + return () => clearInterval(interval); + }, [countDownDate, update]); + + return { + targetDate, + update, + countdown: getReturnValues(countDown), + }; +}; + +const getReturnValues = (countDown: number): CountdownValues => { + // calculate time left + const days = Math.floor(countDown / (1000 * 60 * 60 * 24)); + const hours = Math.floor( + (countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) + ); + const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((countDown % (1000 * 60)) / 1000); + + 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.js deleted file mode 100644 index ed8c421..0000000 --- a/lib/prisma.js +++ /dev/null @@ -1,5 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -export const prisma = global.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== "production") global.prisma = prisma; diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 0000000..55acf8d --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +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.js deleted file mode 100644 index 9522e4c..0000000 --- a/lib/redis.js +++ /dev/null @@ -1,47 +0,0 @@ -import { Redis } from "ioredis"; -import { RateLimiterRedis } from "rate-limiter-flexible"; - -const REDIS_URL = process.env.REDIS_URL; - -let redis; -let rateLimiterRedis; -let rateLimitStrict; -let rateSuperStrict; - -if (REDIS_URL) { - redis = new Redis(REDIS_URL); - redis.on("error", (err) => { - console.error("Redis error: ", err); - }); - - const opt = { - storeClient: redis, - keyPrefix: "rateLimit", - points: 50, - duration: 1, - }; - - const optStrict = { - storeClient: redis, - keyPrefix: "rateLimitStrict", - points: 20, - duration: 1, - }; - - const optSuperStrict = { - storeClient: redis, - keyPrefix: "rateLimitSuperStrict", - points: 3, - // duration 10 minutes - duration: 10 * 60, - blockDuration: 10 * 60, - }; - - rateLimiterRedis = new RateLimiterRedis(opt); - rateLimitStrict = new RateLimiterRedis(optStrict); - rateSuperStrict = new RateLimiterRedis(optSuperStrict); -} else { - console.warn("REDIS_URL is not defined. Redis caching will be disabled."); -} - -export { redis, rateLimiterRedis, rateLimitStrict, rateSuperStrict }; diff --git a/lib/redis.ts b/lib/redis.ts new file mode 100644 index 0000000..1778933 --- /dev/null +++ b/lib/redis.ts @@ -0,0 +1,47 @@ +import { Redis } from "ioredis"; +import { RateLimiterRedis } from "rate-limiter-flexible"; + +const REDIS_URL: string | undefined = process.env.REDIS_URL; + +let redis: Redis; +let rateLimiterRedis: RateLimiterRedis; +let rateLimitStrict: RateLimiterRedis; +let rateSuperStrict: RateLimiterRedis; + +if (REDIS_URL) { + redis = new Redis(REDIS_URL); + redis.on("error", (err: Error) => { + console.error("Redis error: ", err); + }); + + const opt = { + storeClient: redis, + keyPrefix: "rateLimit", + points: 50, + duration: 1, + }; + + const optStrict = { + storeClient: redis, + keyPrefix: "rateLimitStrict", + points: 20, + duration: 1, + }; + + const optSuperStrict = { + storeClient: redis, + keyPrefix: "rateLimitSuperStrict", + points: 3, + // duration 10 minutes + duration: 10 * 60, + blockDuration: 10 * 60, + }; + + rateLimiterRedis = new RateLimiterRedis(opt); + rateLimitStrict = new RateLimiterRedis(optStrict); + rateSuperStrict = new RateLimiterRedis(optSuperStrict); +} else { + console.warn("REDIS_URL is not defined. Redis caching will be disabled."); +} + +export { redis, rateLimiterRedis, rateLimitStrict, rateSuperStrict }; 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.js deleted file mode 100644 index 085d984..0000000 --- a/pages/404.js +++ /dev/null @@ -1,61 +0,0 @@ -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 { useRouter } from "next/router"; -import { ArrowLeftIcon } from "@heroicons/react/24/outline"; - -export default function Custom404() { - const router = useRouter(); - return ( - <> - <Head> - <title>Not Found - - - - - -
- 404 -

- Oops! Page not found -

-

- The page you're looking for doesn't seem to exist. -

-
- - -
-
-