diff options
| author | Artrix <[email protected]> | 2024-01-05 05:12:52 -0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-01-05 20:12:52 +0700 |
| commit | 553fe1c71082b040e9f9667ad3e99acdb33990b2 (patch) | |
| tree | 0c770c406c8ff934ce34d8b10dbae948a554a619 | |
| parent | migrate to typescript (diff) | |
| download | moopa-553fe1c71082b040e9f9667ad3e99acdb33990b2.tar.xz moopa-553fe1c71082b040e9f9667ad3e99acdb33990b2.zip | |
feat: Implement a way to review/rate anime (#108)
* Make details cover lead back to anime page
* Make 'markProgress' use object instead of param list
* Import Link
* Implement Rate modal
* Pass session into useAniList
Co-authored-by: Factiven <[email protected]>
* Reimplement using markComplete & add toast for failure
* redefined ratemodal
* fix: home page client error
* update version
---------
Co-authored-by: Factiven <[email protected]>
24 files changed, 395 insertions, 187 deletions
diff --git a/components/anime/mobile/topSection.tsx b/components/anime/mobile/topSection.tsx index 2d28c66..b5d4f62 100644 --- a/components/anime/mobile/topSection.tsx +++ b/components/anime/mobile/topSection.tsx @@ -65,7 +65,7 @@ export default function DetailTop({ <div className="flex flex-col md:flex-row w-full items-center md:items-end gap-5 pt-12"> <div className="shrink-0 w-[180px] h-[250px] rounded overflow-hidden"> {info ? ( - <img + <Image src={ info?.coverImage?.extraLarge?.toString() ?? info?.coverImage?.toString() diff --git a/components/home/schedule.js b/components/home/schedule.js index 19260c2..df61eba 100644 --- a/components/home/schedule.js +++ b/components/home/schedule.js @@ -13,11 +13,13 @@ export default function Schedule({ data, scheduleData, anime, update }) { "Schedule"; currentDay = currentDay.replace("Schedule", ""); - const { day, hours, minutes, seconds } = useCountdown( + const { countdown } = useCountdown( anime[0]?.airingSchedule.nodes[0]?.airingAt * 1000 || Date.now(), update ); + const {days: day, hours, minutes, seconds} = countdown; + const [currentPage, setCurrentPage] = useState(0); const [days, setDay] = useState(); diff --git a/components/listEditor.tsx b/components/listEditor.tsx index 2e180a1..045e254 100644 --- a/components/listEditor.tsx +++ b/components/listEditor.tsx @@ -2,7 +2,7 @@ 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"; +import { AniListInfoTypes } from "types/info/AnilistInfoTypes"; interface ListEditorProps { animeId: number; diff --git a/components/manga/panels/firstPanel.js b/components/manga/panels/firstPanel.js index 8470fd0..0ceb2fb 100644 --- a/components/manga/panels/firstPanel.js +++ b/components/manga/panels/firstPanel.js @@ -66,13 +66,13 @@ export default function FirstPanel({ if (session) { if (aniId?.length > 6) return; const currentChapter = chapter.chapters?.find( - (x) => x.id === currentId + (x) => x.id === currentId, ); if (currentChapter) { const chapterNumber = currentChapter.number ?? chapter.chapters.indexOf(currentChapter) + 1; - markProgress(aniId, chapterNumber); + markProgress({ mediaId: aniId, progress: chapterNumber }); console.log("marking progress"); } } @@ -142,14 +142,14 @@ export default function FirstPanel({ > <Image src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent( - i.url + i.url, )}${ i?.headers?.Referer ? `&headers=${encodeURIComponent( - JSON.stringify(i?.headers) + JSON.stringify(i?.headers), )}` : `&headers=${encodeURIComponent( - JSON.stringify(getHeaders(chapter.providerId)) + JSON.stringify(getHeaders(chapter.providerId)), )}` }`} alt={index} @@ -213,10 +213,10 @@ export default function FirstPanel({ `/en/manga/read/${ chapter.providerId }?id=${mangadexId}&chapterId=${encodeURIComponent( - prevChapter?.id + prevChapter?.id, )}${aniId?.length > 6 ? "" : `&anilist=${aniId}`}&num=${ prevChapter?.number - }` + }`, ) } > @@ -234,10 +234,10 @@ export default function FirstPanel({ `/en/manga/read/${ chapter.providerId }?id=${mangadexId}&chapterId=${encodeURIComponent( - nextChapter?.id + nextChapter?.id, )}${aniId?.length > 6 ? "" : `&anilist=${aniId}`}&num=${ nextChapter?.number - }` + }`, ) } > diff --git a/components/manga/panels/secondPanel.js b/components/manga/panels/secondPanel.js index 23a9da0..6ebc292 100644 --- a/components/manga/panels/secondPanel.js +++ b/components/manga/panels/secondPanel.js @@ -69,12 +69,12 @@ export default function SecondPanel({ if (index + 1 >= image.length - 4 && !hasRun.current) { const current = chapterData.chapters?.find( - (x) => x.id === currentChapter.id + (x) => x.id === currentChapter.id, ); const chapterNumber = chapterData.chapters.indexOf(current) + 1; if (chapterNumber) { - markProgress(aniId, chapterNumber); + markProgress({ mediaId: aniId, progress: chapterNumber }); } hasRun.current = true; } @@ -98,15 +98,15 @@ export default function SecondPanel({ if (index + 1 >= image.length - 4 && !hasRun.current) { console.log("marking progress"); const current = chapterData.chapters?.find( - (x) => x.id === currentChapter.id + (x) => x.id === currentChapter.id, ); const chapterNumber = chapterData.chapters.indexOf(current) + 1; if (chapterNumber) { - markProgress(aniId, chapterNumber); + markProgress({ mediaId: aniId, progress: chapterNumber }); } - markProgress(aniId, chapterNumber); + markProgress({ mediaId: aniId, progress: chapterNumber }); hasRun.current = true; } }; @@ -137,16 +137,16 @@ export default function SecondPanel({ height={500} className="w-1/2 h-screen object-contain" src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent( - image[image.length - index - 2]?.url + image[image.length - index - 2]?.url, )}${ image[image.length - index - 2]?.headers?.Referer ? `&headers=${encodeURIComponent( JSON.stringify( - image[image.length - index - 2]?.headers - ) + image[image.length - index - 2]?.headers, + ), )}` : `&headers=${encodeURIComponent( - JSON.stringify(getHeaders(providerId)) + JSON.stringify(getHeaders(providerId)), )}` }`} alt="Manga Page" @@ -158,14 +158,16 @@ export default function SecondPanel({ height={500} className="w-1/2 h-screen object-contain" src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent( - image[image.length - index - 1]?.url + image[image.length - index - 1]?.url, )}${ image[image.length - index - 1]?.headers?.Referer ? `&headers=${encodeURIComponent( - JSON.stringify(image[image.length - index - 1]?.headers) + JSON.stringify( + image[image.length - index - 1]?.headers, + ), )}` : `&headers=${encodeURIComponent( - JSON.stringify(getHeaders(providerId)) + JSON.stringify(getHeaders(providerId)), )}` }`} alt="Manga Page" diff --git a/components/manga/panels/thirdPanel.js b/components/manga/panels/thirdPanel.js index 77bb132..7c43f6e 100644 --- a/components/manga/panels/thirdPanel.js +++ b/components/manga/panels/thirdPanel.js @@ -66,12 +66,12 @@ export default function ThirdPanel({ } if (index + 1 >= image.length - 2 && !hasRun.current) { const current = chapterData.chapters?.find( - (x) => x.id === currentChapter.id + (x) => x.id === currentChapter.id, ); const chapterNumber = chapterData.chapters.indexOf(current) + 1; if (chapterNumber) { - markProgress(aniId, chapterNumber); + markProgress({ mediaId: aniId, progress: chapterNumber }); } hasRun.current = true; } @@ -94,12 +94,12 @@ export default function ThirdPanel({ } if (index + 1 >= image.length - 2 && !hasRun.current) { const current = chapterData.chapters?.find( - (x) => x.id === currentChapter.id + (x) => x.id === currentChapter.id, ); const chapterNumber = chapterData.chapters.indexOf(current) + 1; if (chapterNumber) { - markProgress(aniId, chapterNumber); + markProgress({ mediaId: aniId, progress: chapterNumber }); } hasRun.current = true; @@ -128,14 +128,16 @@ export default function ThirdPanel({ className="w-full h-screen object-contain" onClick={() => setMobileVisible(!mobileVisible)} src={`https://aoi.moopa.live/utils/image-proxy?url=${encodeURIComponent( - image[image.length - index - 1]?.url + image[image.length - index - 1]?.url, )}${ image[image.length - index - 1]?.headers?.Referer ? `&headers=${encodeURIComponent( - JSON.stringify(image[image.length - index - 1]?.headers) + JSON.stringify( + image[image.length - index - 1]?.headers, + ), )}` : `&headers=${encodeURIComponent( - JSON.stringify(getHeaders(providerId)) + JSON.stringify(getHeaders(providerId)), )}` }`} alt="Manga Page" diff --git a/components/manga/rightBar.js b/components/manga/rightBar.js index 9672fc4..3da04d9 100644 --- a/components/manga/rightBar.js +++ b/components/manga/rightBar.js @@ -43,7 +43,7 @@ export default function RightBar({ parsedProgress === parseInt(parsedProgress) && parsedVolumeProgress === parseInt(parsedVolumeProgress) ) { - markProgress(id, progress, status, volumeProgress); + markProgress({ mediaId: id, progress, stats: status, volumeProgress }); hasRun.current = true; } else { toast.error("Progress must be a whole number!"); diff --git a/components/shared/RateModal.tsx b/components/shared/RateModal.tsx new file mode 100644 index 0000000..6231eaf --- /dev/null +++ b/components/shared/RateModal.tsx @@ -0,0 +1,135 @@ +import { useAniList } from "@/lib/anilist/useAnilist"; +import { useWatchProvider } from "@/lib/context/watchPageProvider"; +import { useState } from "react"; +import { toast } from "sonner"; + +type Props = { + toggle: boolean; + position: "top" | "bottom"; + setToggle: (prev: any) => void; + session: any; +}; + +export default function RateModal({ + toggle, + position, + setToggle, + session, +}: Props) { + const [startRate, setStartRate] = useState(false); + const { markComplete } = useAniList(session); + + const { dataMedia } = useWatchProvider(); + + async function handleSubmit(event: any) { + event.preventDefault(); + const data = new FormData(event.target); + const rating = data.get("rating"); + const notes = data.get("notes"); + try { + await markComplete(dataMedia?.id, { notes, scoreRaw: rating }); + toast.success("Successfully rated!"); + setToggle((prev: any) => { + return { + ...prev, + isOpen: false, + }; + }); + } catch (error) { + toast.error("Failed to rate!"); + } + } + + function handleClose() { + setToggle((prev: any) => { + return { + ...prev, + isOpen: false, + }; + }); + } + return ( + <> + <div + className={`w-full h-[20dvh] fixed bg-gradient-to-${ + position === "top" + ? `b top-0 from-black/20` + : "t -bottom-5 from-black/40" + } to-transparent z-10 transition-all duration-200 ease-in-out ${ + toggle ? "" : "opacity-0 pointer-events-none" + }`} + /> + <div + style={{ width: startRate ? "300px" : "240px" }} + className={`${ + position === "top" + ? toggle + ? `top-5` + : `-top-48` + : toggle + ? `bottom-10` + : `-bottom-48` + } fixed text-white font-semibold z-50 font-karla transition-all duration-300 ease-in-out left-1/2 transform -translate-x-1/2 bg-secondary p-3 rounded flex flex-col justify-center items-center gap-4`} + > + <p className="text-lg">What do you think?</p> + <div + className={`flex gap-2 font-medium text-center transition-all duration-200 ${ + startRate + ? "scale-50 hidden pointer-events-none" + : "scale-100 opacity-100" + }`} + > + <button + onClick={() => setStartRate(true)} + className="w-[100px] py-1 bg-action/10 rounded text-action" + > + Rate Now + </button> + <button + onClick={handleClose} + className="w-[100px] py-1 border border-opacity-0 hover:border-opacity-10 rounded border-white" + > + Close + </button> + </div> + {startRate && ( + <form + onSubmit={handleSubmit} + className="flex flex-col items-center gap-3 w-full" + > + <input + type="number" + min={1} + max={100} + required + name="rating" + placeholder="rate from 1-100" + className="appearance-none w-full text-white placeholder-zinc-400 bg-white/10 py-1 px-2 rounded text-sm" + /> + <input + type="text" + name="notes" + placeholder="notes" + className="appearance-none w-full text-white placeholder-zinc-400 bg-white/10 py-1 px-2 rounded text-sm" + /> + <div className="flex gap-2 w-full"> + <button + type="submit" + className="w-full py-1 bg-action/10 hover:bg-action/20 rounded text-action" + > + Submit + </button> + <button + type="button" + onClick={handleClose} + className="w-full py-1 rounded hover:bg-white/20" + > + Cancel + </button> + </div> + </form> + )} + </div> + </> + ); +} diff --git a/components/shared/changelogs.tsx b/components/shared/changelogs.tsx index a7b0436..208b1ff 100644 --- a/components/shared/changelogs.tsx +++ b/components/shared/changelogs.tsx @@ -3,34 +3,46 @@ import Link from "next/link"; import { Fragment, useEffect, useRef, useState } from "react"; const web = { - version: "v4.3.1", + version: "v4.4.0", }; const logs = [ { version: "v4.3.1", - pre: true, + pre: false, 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!", + "Added rate modal when user finished watching the whole series", + "Fix: only half of the episodes has episodes thumbnail", + "Fix: pressing back button in anime info page redirects user to the wrong page", + "Progressively migrate codebase to typescript", ], }, + // { + // version: "v4.3.1", + // pre: true, + // notes: null, + // highlights: false, + // 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() { @@ -146,11 +158,11 @@ export default function ChangeLogs() { Hi! Welcome to the new changelogs section. Here you can see a lists of the latest changes and updates to the site. </p> - <p className="inline-block text-sm italic my-2 text-gray-400"> + {/* <p className="inline-block text-sm italic my-2 text-gray-400"> *This update is still in it's pre-release state, please expect to see some bugs. If you find any, please report them. - </p> + </p> */} </div> {logs.map((x) => ( @@ -166,32 +178,6 @@ export default function ChangeLogs() { </ChangelogsVersions> ))} - {/* <div className="my-2 flex items-center justify-evenly"> - <div className="w-full h-[1px] bg-gradient-to-r from-white/5 to-white/40" /> - <p className="relative flex flex-1 whitespace-nowrap font-bold mx-2 font-inter"> - v4.3.0 - <span className="flex text-xs font-light font-roboto ml-1 italic"> - pre - </span> - </p> - <div className="w-full h-[1px] bg-gradient-to-l from-white/5 to-white/40" /> - </div> - - <div className="flex flex-col gap-2 text-sm text-gray-200"> - <div> - <p className="inline-block italic mb-2 text-gray-400"> - *This update is still in it's pre-release state, please - expect to see some bugs. If you find any, please report - them. - </p> - - <p>- Added changelogs section</p> - <p>- Added recommendations based on user lists</p> - <p>- New Player!</p> - <p>- And other minor bug fixes!</p> - </div> - </div> */} - <div className="mt-2 text-gray-400 text-sm"> <p> see more changelogs{" "} diff --git a/components/watch/new-player/components/layouts/video-layout.tsx b/components/watch/new-player/components/layouts/video-layout.tsx index fa1f6c3..93e4629 100644 --- a/components/watch/new-player/components/layouts/video-layout.tsx +++ b/components/watch/new-player/components/layouts/video-layout.tsx @@ -17,13 +17,14 @@ 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"; +import RateModal from "@/components/shared/RateModal"; export interface VideoLayoutProps { thumbnails?: string; navigation?: Navigation; host?: boolean; + session?: any; } function isMobileDevice() { @@ -40,22 +41,40 @@ export function VideoLayout({ thumbnails, navigation, host = true, + session, }: VideoLayoutProps) { const [isMobile, setIsMobile] = useState(false); - const { track } = useWatchProvider(); + const { track, setRatingModalState, ratingModalState } = useWatchProvider(); const isFullscreen = useMediaState("fullscreen"); useEffect(() => { setIsMobile(isMobileDevice()); }, []); + useEffect(() => { + setRatingModalState((prev: any) => { + return { + ...prev, + isFullscreen: isFullscreen, + }; + }); + }, [isFullscreen]); + return ( <> <Gestures host={host} /> <Captions className={`${captionStyles.captions} media-preview:opacity-0 media-controls:bottom-[85px] media-captions:opacity-100 absolute inset-0 bottom-2 z-10 select-none break-words opacity-0 transition-[opacity,bottom] duration-300`} /> + {ratingModalState.isFullscreen && ( + <RateModal + toggle={ratingModalState.isOpen} + setToggle={setRatingModalState} + position="top" + session={session} + /> + )} <Controls.Root className={`${styles.controls} media-paused:bg-black/10 duration-200 media-controls:opacity-100 absolute inset-0 z-10 flex h-full w-full flex-col bg-gradient-to-t from-black/30 via-transparent to-black/30 opacity-0 transition-opacity`} > diff --git a/components/watch/new-player/player.tsx b/components/watch/new-player/player.tsx index b98ff79..f2d11f7 100644 --- a/components/watch/new-player/player.tsx +++ b/components/watch/new-player/player.tsx @@ -12,7 +12,6 @@ import { type MediaPlayerInstance, Track, MediaTimeUpdateEventDetail, - MediaTimeUpdateEvent, } from "@vidstack/react"; import { VideoLayout } from "./components/layouts/video-layout"; import { useWatchProvider } from "@/lib/context/watchPageProvider"; @@ -98,6 +97,7 @@ export default function VidStack({ playerState, dataMedia, autoNext, + setRatingModalState, } = useWatchProvider(); const { qualities, duration } = useMediaStore(player); @@ -379,7 +379,20 @@ export default function VidStack({ mark = 1; setMarked(1); console.log("marking progress"); - markProgress(dataMedia.id, navigation.playing.number); + // @ts-ignore Fix when convert useAnilist to typescript + markProgress({ + mediaId: dataMedia.id, + progress: navigation.playing.number, + }); + + if (dataMedia.episodes === +navigation.playing?.number) { + setRatingModalState((prev: any) => { + return { + ...prev, + isOpen: true, + }; + }); + } } } } @@ -424,7 +437,7 @@ export default function VidStack({ return ( <MediaPlayer key={id} - className={`${style.player} player`} + className={`${style.player} player relative`} title={ navigation?.playing?.title || `Episode ${navigation?.playing?.number}` || @@ -454,7 +467,11 @@ export default function VidStack({ <Track key={chapters} src={chapters} kind="chapters" default={true} /> )} </MediaProvider> - <VideoLayout thumbnails={track?.thumbnails} navigation={navigation} /> + <VideoLayout + thumbnails={track?.thumbnails} + navigation={navigation} + session={sessions} + /> </MediaPlayer> ); } diff --git a/components/watch/primary/details.tsx b/components/watch/primary/details.tsx index f20f8cf..dd739f2 100644 --- a/components/watch/primary/details.tsx +++ b/components/watch/primary/details.tsx @@ -4,6 +4,8 @@ import Skeleton from "react-loading-skeleton"; import DisqusComments from "../../disqus"; import { AniListInfoTypes } from "types/info/AnilistInfoTypes"; import { SessionTypes } from "pages/en"; +import Link from "next/link"; +import Image from "next/image"; type DetailsProps = { info: AniListInfoTypes; @@ -61,13 +63,18 @@ export default function Details({ <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" - /> + <Link + className="hover:scale-105 hover:shadow-lg duration-300 ease-out" + href={`/en/anime/${id}`} + > + <Image + src={info.coverImage.extraLarge} + alt="Anime Cover" + width={1000} + height={1000} + className="object-cover aspect-[9/13] h-[240px] rounded-md" + /> + </Link> ) : ( <Skeleton height={240} /> )} diff --git a/lib/anilist/useAnilist.js b/lib/anilist/useAnilist.js index 323dd29..36c1496 100644 --- a/lib/anilist/useAnilist.js +++ b/lib/anilist/useAnilist.js @@ -4,15 +4,21 @@ export const useAniList = (session) => { const accessToken = session?.user?.token; const fetchGraphQL = async (query, variables) => { - const response = await fetch("https://graphql.anilist.co/", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(accessToken && { Authorization: `Bearer ${accessToken}` }), - }, - body: JSON.stringify({ query, variables }), - }); - return response.json(); + try { + const response = await fetch("https://graphql.anilist.co/", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(accessToken && { Authorization: `Bearer ${accessToken}` }), + }, + body: JSON.stringify({ query, variables }), + }); + return response.json(); + } catch (error) { + toast.error("An error occurred, please try again later", { + position: "bottom-right", + }); + } }; const quickSearch = async ({ search, type, isAdult = false }) => { @@ -103,18 +109,22 @@ export const useAniList = (session) => { return data; }; - const markComplete = async (mediaId) => { + const markComplete = async (mediaId, { notes, scoreRaw }) => { if (!accessToken) return; const completeQuery = ` - mutation($mediaId: Int) { - SaveMediaListEntry(mediaId: $mediaId, status: COMPLETED) { + mutation($mediaId: Int, $notes: String, $scoreRaw: Int) { + SaveMediaListEntry(mediaId: $mediaId, status: COMPLETED, scoreRaw: $scoreRaw, notes: $notes) { id mediaId status } } `; - const data = await fetchGraphQL(completeQuery, { mediaId }); + const data = await fetchGraphQL(completeQuery, { + mediaId, + scoreRaw, + notes, + }); console.log({ Complete: data }); }; @@ -162,11 +172,18 @@ export const useAniList = (session) => { return data; }; - const markProgress = async (mediaId, progress, stats, volumeProgress) => { + const markProgress = async ({ + mediaId, + progress, + stats, + volumeProgress, + scoreRaw = 0, + notes, + }) => { if (!accessToken) return; const progressWatched = ` - mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String], $repeat: Int) { - SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists, repeat: $repeat) { + mutation($mediaId: Int, $progress: Int, $status: MediaListStatus, $progressVolumes: Int, $lists: [String], $repeat: Int, $scoreRaw: Int, $notes: String) { + SaveMediaListEntry(mediaId: $mediaId, progress: $progress, status: $status, progressVolumes: $progressVolumes, customLists: $lists, repeat: $repeat, scoreRaw: $scoreRaw, notes: $notes) { id mediaId progress @@ -214,6 +231,8 @@ export const useAniList = (session) => { status, progressVolumes: volumeProgress, lists, + scoreRaw, + notes, }; if (videoEpisode === mediaEpisode) { @@ -235,6 +254,8 @@ export const useAniList = (session) => { progress, status: stats, progressVolumes: volumeProgress, + scoreRaw, + notes, }; await fetchGraphQL(progressWatched, variables); diff --git a/lib/context/watchPageProvider.js b/lib/context/watchPageProvider.js index c305710..b7d78b3 100644 --- a/lib/context/watchPageProvider.js +++ b/lib/context/watchPageProvider.js @@ -16,6 +16,11 @@ export const WatchPageProvider = ({ children }) => { const [userData, setUserData] = useState(null); const [dataMedia, setDataMedia] = useState(null); + const [ratingModalState, setRatingModalState] = useState({ + isOpen: false, + isFullscreen: false, + }); + const [track, setTrack] = useState(null); return ( @@ -39,6 +44,8 @@ export const WatchPageProvider = ({ children }) => { setDataMedia, autoNext, setAutoNext, + ratingModalState, + setRatingModalState, }} > {children} diff --git a/package.json b/package.json index 3ea7c4c..6f77d24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moopa", - "version": "4.3.1", + "version": "4.4.0", "private": true, "founder": "Factiven", "scripts": { diff --git a/pages/api/v2/episode/[id].tsx b/pages/api/v2/episode/[id].tsx index b646126..ddf5635 100644 --- a/pages/api/v2/episode/[id].tsx +++ b/pages/api/v2/episode/[id].tsx @@ -139,10 +139,6 @@ async function fetchAnify(id?: string) { async function fetchCoverImage(id: string, available = false) { try { - if (!process.env.API_KEY) { - return []; - } - if (available) { return null; } @@ -171,10 +167,10 @@ export default async function handler( ) { const { id, releasing = "false", dub = false, refresh = null } = req.query; - // if releasing is true then cache for 1 hour, if it false cache for 1 month; + // if releasing is true then cache for 3 hour, if it false cache for 1 month; let cacheTime = null; if (releasing === "true") { - cacheTime = 60 * 60; // 1 hour + cacheTime = 60 * 60 * 3; // 3 hours } else if (releasing === "false") { cacheTime = 60 * 60 * 24 * 30; // 1 month } @@ -210,7 +206,7 @@ export default async function handler( meta = null; } - if (refresh) { + if (refresh !== null) { await redis.del(`episode:${id}`); } else { cached = await redis.get(`episode:${id}`); @@ -262,12 +258,6 @@ export default async function handler( fetchCoverImage(id, meta), ]); - // const hasImage = consumet.map((i) => - // i.episodes?.sub?.some( - // (e) => e.img !== null || !e.img.includes("https://s4.anilist.co/") - // ) - // ); - let subDub = "sub"; if (dub) { subDub = "dub"; diff --git a/pages/api/v2/etc/recent/[page].tsx b/pages/api/v2/etc/recent/[page].tsx index e49591c..4e3bc98 100644 --- a/pages/api/v2/etc/recent/[page].tsx +++ b/pages/api/v2/etc/recent/[page].tsx @@ -1,5 +1,5 @@ import { rateLimitStrict, redis } from "@/lib/redis"; -import { AnifyRecentEpisode } from "@/utils/types"; +import { AnifyRecentEpisode } from "types"; import axios from "axios"; import { NextApiRequest, NextApiResponse } from "next"; diff --git a/pages/en/anime/watch/[...info].js b/pages/en/anime/watch/[...info].js index dc1f412..259ebee 100644 --- a/pages/en/anime/watch/[...info].js +++ b/pages/en/anime/watch/[...info].js @@ -5,6 +5,7 @@ import EpisodeLists from "@/components/watch/secondary/episodeLists"; import { getServerSession } from "next-auth"; import { useWatchProvider } from "@/lib/context/watchPageProvider"; import { authOptions } from "../../../api/auth/[...nextauth]"; +import { useAniList } from "@/lib/anilist/useAnilist"; import { createList, createUser, getEpisode } from "@/prisma/user"; import Link from "next/link"; import MobileNav from "@/components/shared/MobileNav"; @@ -18,6 +19,7 @@ import Head from "next/head"; import VidStack from "@/components/watch/new-player/player"; import { useRouter } from "next/router"; import { Spinner } from "@vidstack/react"; +import RateModal from "@/components/shared/RateModal"; export async function getServerSideProps(context) { let userData = null; @@ -32,11 +34,11 @@ export async function getServerSideProps(context) { } let proxy; - proxy = process.env.PROXY_URI; + proxy = process.env.PROXY_URI || null; if (proxy && proxy.endsWith("/")) { proxy = proxy.slice(0, -1); } - const disqus = process.env.DISQUS_SHORTNAME; + const disqus = process.env.DISQUS_SHORTNAME || null; const [aniId, provider] = query?.info; const watchId = query?.id; @@ -149,7 +151,8 @@ export default function Watch({ const [open, setOpen] = useState(false); const [isOpen, setIsOpen] = useState(false); - const { setAutoNext } = useWatchProvider(); + const { setAutoNext, ratingModalState, setRatingModalState } = + useWatchProvider(); const [onList, setOnList] = useState(false); @@ -494,6 +497,14 @@ export default function Watch({ </Modal> <BugReportForm isOpen={isOpen} setIsOpen={setIsOpen} /> <main className="w-screen h-full"> + {!ratingModalState.isFullscreen && ( + <RateModal + toggle={ratingModalState.isOpen} + setToggle={setRatingModalState} + position="bottom" + session={sessions} + /> + )} <Navbar scrollP={20} withNav={true} @@ -614,11 +625,6 @@ export default function Watch({ id="secondary" className={`relative ${theaterMode ? "pt-5" : "pt-4 lg:pt-0"}`} > - {/* <div className="w-full h-[150px] text-black p-3"> - <span className="bg-white w-full h-full flex-center"> - ad banner - </span> - </div> */} <EpisodeLists info={info} session={sessions} @@ -641,10 +647,7 @@ export default function Watch({ function SpinLoader() { return ( <div className="pointer-events-none absolute inset-0 z-50 flex h-full w-full items-center justify-center"> - <Spinner.Root - className="text-white animate-spin opacity-100" - size={84} - > + <Spinner.Root className="text-white animate-spin opacity-100" size={84}> <Spinner.Track className="opacity-25" width={8} /> <Spinner.TrackFill className="opacity-75" width={8} /> </Spinner.Root> diff --git a/pages/en/index.tsx b/pages/en/index.tsx index 4141015..faead42 100644 --- a/pages/en/index.tsx +++ b/pages/en/index.tsx @@ -19,6 +19,7 @@ import { getGreetings } from "@/utils/getGreetings"; import { redis } from "@/lib/redis"; import { Navbar } from "@/components/shared/NavBar"; import UserRecommendation from "@/components/home/recommendation"; +import { useRouter } from "next/router"; export async function getServerSideProps() { let cachedData; @@ -28,7 +29,7 @@ export async function getServerSideProps() { } if (cachedData) { - const { genre, detail, populars } = JSON.parse(cachedData); + const { genre, detail, populars, firstTrend } = JSON.parse(cachedData); const upComing = await getUpcomingAnime(); return { props: { @@ -36,6 +37,7 @@ export async function getServerSideProps() { detail, populars, upComing, + firstTrend, }, }; } else { @@ -56,6 +58,7 @@ export async function getServerSideProps() { genre: genreDetail.props, detail: trendingDetail.props, populars: popularDetail.props, + firstTrend: trendingDetail.props.data[0], }), // set cache for 2 hours "EX", 60 * 60 * 2 @@ -70,6 +73,7 @@ export async function getServerSideProps() { detail: trendingDetail.props, populars: popularDetail.props, upComing, + firstTrend: trendingDetail.props.data[0], }, }; } @@ -80,6 +84,7 @@ type HomeProps = { detail: any; populars: any; upComing: any; + firstTrend: any; }; export interface SessionTypes { @@ -106,7 +111,12 @@ interface Image { medium: string; } -export default function Home({ detail, populars, upComing }: HomeProps) { +export default function Home({ + detail, + populars, + upComing, + firstTrend, +}: HomeProps) { const { data: sessions }: any = useSession(); const userSession: SessionTypes = sessions?.user; @@ -126,6 +136,8 @@ export default function Home({ detail, populars, upComing }: HomeProps) { }); const { anime: release } = GetMedia(sessions); + const router = useRouter(); + const [schedules, setSchedules] = useState(null); const [anime, setAnime] = useState([]); @@ -195,7 +207,6 @@ export default function Home({ detail, populars, upComing }: HomeProps) { const [prog, setProg] = useState<any[] | null>(); const popular = populars?.data; - const data = detail.data[0]; useEffect(() => { async function userData() { @@ -324,6 +335,10 @@ export default function Home({ detail, populars, upComing }: HomeProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [userSession?.name, currentAnime, plan]); + function removeHtmlTags(text: string): string { + return text?.replace(/<[^>]+>/g, ""); + } + return ( <Fragment> <Head> @@ -370,54 +385,41 @@ export default function Home({ detail, populars, upComing }: HomeProps) { <Navbar paddingY="pt-2 lg:pt-10" withNav={true} home={true} /> <div className="h-auto w-screen bg-[#141519] text-[#dbdcdd]"> - {/* PC / TABLET */} - <div className=" hidden justify-center lg:flex my-16"> - <div className="relative grid grid-rows-2 items-center lg:flex lg:h-[467px] lg:w-[80%] lg:justify-between"> - <div className="row-start-2 flex h-full flex-col gap-7 lg:w-[55%] lg:justify-center"> - <h1 className="w-[85%] font-outfit font-extrabold lg:text-[34px] line-clamp-2"> - {data.title.english || data.title.romaji || data.title.native} - </h1> - <p - className="font-roboto font-light lg:text-[18px] line-clamp-5" - dangerouslySetInnerHTML={{ __html: data?.description }} - /> - - <div className="lg:pt-5 flex"> - <Link - href={`/en/anime/${data.id}`} - className="rounded-sm p-3 text-md font-karla font-light ring-1 ring-[#FF7F57]" + <div className="hidden lg:flex w-full justify-center my-16"> + <div className="flex justify-between w-[80%] h-[470px]"> + <div className="flex flex-col items-start justify-center w-[55%] gap-5"> + <p className="font-outfit font-extrabold text-[34px] line-clamp-2 leading-10"> + {firstTrend?.title?.english || firstTrend?.title?.romaji} + </p> + <p className="font-roboto font-light lg:text-[18px] line-clamp-3 tracking-wide"> + {removeHtmlTags(firstTrend?.description)} + </p> + {firstTrend && ( + <button + onClick={() => { + router.push(`/en/anime/${firstTrend?.id}`); + }} + className="p-3 text-md font-karla font-light ring-1 ring-action/50 rounded" > START WATCHING - </Link> - </div> + </button> + )} </div> - <div className="z-10 row-start-1 flex justify-center "> - <div className="relative lg:h-[467px] lg:w-[322px] lg:scale-100"> - <div className="absolute bg-gradient-to-t from-[#141519] to-transparent lg:h-[467px] lg:w-[322px]" /> - - <Image - draggable={false} - src={data.coverImage?.extraLarge || data.image} - alt={`cover ${data.title.english || data.title.romaji}`} - width={1200} - height={1200} - priority - className="rounded-tl-xl rounded-tr-xl object-cover bg-blend-overlay lg:h-[467px] lg:w-[322px]" - /> - </div> + <div className="relative block h-[467px] w-[322px]"> + <div className="absolute bg-gradient-to-t from-primary to-transparent w-full h-full inset-0 z-20" /> + <Image + src={firstTrend?.coverImage?.extraLarge || firstTrend?.image} + alt={`cover ${ + firstTrend?.title?.english || firstTrend?.title?.romaji + }`} + fill + sizes="100%" + quality={100} + className="object-cover rounded z-10" + /> </div> </div> </div> - {/* <div className="relative w-screen h-screen overflow-hidden"> - <iframe - width="560" - height="315" - src="https://www.youtube.com/embed/VVfdqw-qvNE?autoplay=1&controls=0&rel=0&mute=1" - frameborder="0" - allowfullscreen - className="absolute w-screen h-screen top-0 scale-[115%] left-0 z-0" - /> - </div> */} {sessions && ( <div className="flex items-center justify-center lg:bg-none mt-4 lg:mt-0 w-screen"> diff --git a/pages/en/schedule/index.tsx b/pages/en/schedule/index.tsx index aa30259..42a28c6 100644 --- a/pages/en/schedule/index.tsx +++ b/pages/en/schedule/index.tsx @@ -1,3 +1,5 @@ +// @ts-nocheck + import Image from "next/image"; import { useEffect, useRef, useState } from "react"; import Link from "next/link"; @@ -15,7 +17,6 @@ import { import { scheduleQuery } from "@/lib/graphql/query"; import MobileNav from "@/components/shared/MobileNav"; -import { useSession } from "next-auth/react"; import { redis } from "@/lib/redis"; import Head from "next/head"; import { Navbar } from "@/components/shared/NavBar"; @@ -344,7 +345,7 @@ export default function Schedule({ schedule }: any) { {/* {!isAired(time) && <p>Airing Next</p>} */} <p className={`absolute left-0 h-1.5 w-1.5 rounded-full ${ - isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime + isAired(+time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime }`} ></p> </div> @@ -371,14 +372,15 @@ export default function Schedule({ schedule }: any) { {m.title.romaji} </h1> <p className="text-gray-400 group-hover:text-action/80 transition-all duration-200 ease-out"> - Ep {s.episode} {timeStamptoHour(s.airingAt)} + Ep {s?.episode}{" "} + {timeStamptoHour(s.airingAt)} </p> </div> </Link> <p key={`p_${s.id}_${index}`} className={`absolute translate-x-full top-1/2 -translate-y-1/2 h-full w-0.5 ${ - isAired(time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime + isAired(+time) ? "bg-action" : "bg-gray-600" // Add a class for currently airing anime }`} ></p> </> @@ -2,8 +2,11 @@ This document contains a summary of all significant changes made to this release. -## 🎉 Update v4.2.5 +## 🎉 Update v4.4.0 ### What's Changed -- fix: Gogoanime episode id is null +- Added rate modal when user finished watching the whole series +- Fix: only half of the episodes has episodes thumbnail +- Fix: pressing back button in anime info page redirects user to the wrong page +- Progressively migrate codebase to typescript diff --git a/styles/globals.css b/styles/globals.css index 10645f0..79d6080 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -587,3 +587,14 @@ pre code { .chat > span { @apply font-karla w-full italic text-white/70; } +/* Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type="number"] { + -moz-appearance: textfield; +} diff --git a/tailwind.config.cjs b/tailwind.config.js index e072608..e072608 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.js diff --git a/tsconfig.json b/tsconfig.json index 2838c72..c1fa3ea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,8 +27,7 @@ "pages/api/v2/episode/[id].tsx", "utils/schedulesUtils.ts", "pages/middleware.js", - "pages/api/auth/[...nextauth].ts", - "components/anime/mobile/reused/infoChip.tsx" + "pages/api/auth/[...nextauth].ts" ], "exclude": ["node_modules"] } |