├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── logo-svg.svg ├── logo.png ├── luffy.gif ├── not-found.png └── share-icon │ ├── email.svg │ ├── facebook.svg │ ├── reddit.svg │ └── twitter.svg ├── src ├── components │ ├── Anime │ │ ├── AnimeBannerDetail.tsx │ │ ├── AnimeCard.tsx │ │ ├── AnimeInfoDetail.tsx │ │ ├── Banners.tsx │ │ └── SlideBanner.tsx │ ├── Characters │ │ ├── CharacterCard.tsx │ │ └── CharactersList.tsx │ ├── Comment │ │ ├── CommentItem.tsx │ │ ├── CommentList.tsx │ │ ├── Input.tsx │ │ ├── NewestComment.tsx │ │ └── NewestCommentItem.tsx │ ├── Headers │ │ ├── Logo.tsx │ │ ├── Menu.tsx │ │ └── index.tsx │ ├── Player │ │ └── index.tsx │ ├── Search │ │ ├── Genres.tsx │ │ └── SelectFilter.tsx │ ├── Shared │ │ ├── 404NotFound.tsx │ │ ├── Footer.tsx │ │ ├── GenresItem.tsx │ │ ├── Meta.tsx │ │ ├── ScrollToTop.tsx │ │ ├── ShareSocial.tsx │ │ ├── SwiperContainer.tsx │ │ ├── TitlePrimary.tsx │ │ └── Toast.tsx │ ├── ShowCase │ │ ├── BoxShowCase.tsx │ │ └── ShowCaseItem.tsx │ ├── Skeleton │ │ └── AnimeCardSkeleton.tsx │ └── Watch │ │ ├── AnimeInfo.tsx │ │ ├── Comment.tsx │ │ ├── EpisodeInfo.tsx │ │ ├── EpisodeList.tsx │ │ ├── MoreLikeThis.tsx │ │ ├── Note.tsx │ │ ├── SelectIframe.tsx │ │ └── SelectSource.tsx ├── hooks │ └── useInnerWidth.ts ├── layouts │ ├── AnimeGridLayout.tsx │ └── MainLayout.tsx ├── lib │ └── prisma.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── anime │ │ ├── [id].tsx │ │ ├── genres │ │ │ └── [genres].tsx │ │ └── watch │ │ │ └── [id].tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── comment.ts │ │ ├── like-comment.ts │ │ ├── list.ts │ │ └── subtitles.ts │ ├── characters │ │ └── [id].tsx │ ├── index.tsx │ ├── list.tsx │ ├── search.tsx │ └── sign-in.tsx ├── services │ ├── anime.ts │ ├── characters.ts │ ├── comment.ts │ └── list.ts ├── styles │ └── globals.css ├── types │ ├── amvstr.ts │ ├── anime.ts │ ├── characters.ts │ ├── comment.ts │ ├── global.d.ts │ ├── next-auth.d.ts │ └── utils.ts └── utils │ ├── client.ts │ ├── contants.ts │ ├── filter.ts │ └── path.ts ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_BACKEND_URL # a url backend consumet get here https://github.com/consumet/api.consumet.org 2 | NEXT_PUBLIC_NEXT_ANIME_URL # domain web example: https://next-anime-app.vercel.app/ 3 | NEXT_PUBLIC_GOOGLE_CLIENT_ID # google client id https://console.cloud.google.com/ 4 | NEXT_PUBLIC_GOOGLE_CLIENT_SECRET # google client secret 5 | NEXTAUTH_SECRET # https://next-auth.js.org/configuration/options 6 | DATABASE_URL # mongoDB connect string 7 | NEXTAUTH_URL # domain web example: https://next-anime-app.vercel.app/ 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react-hooks/exhaustive-deps": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

Next Anime is a free anime watch website built using Consumet API

6 | 7 | ## Video Demo 8 | 9 | Video link: [Click Here](http://www.youtube.com/watch?v=mJNpHoj0dkg) 10 | 11 | ## Live demo 12 | 13 | Official website: [https://nqafe.vercel.app/](https://nqafe.vercel.app/) 14 | 15 | ## Anime sources 16 | 17 | From Consumet API 18 | You can refer to [https://github.com/consumet/api.consumet.org](https://github.com/consumet/api.consumet.org) 19 | 20 | ## Main technology used 21 | 22 | - Nextjs, React, Typescript, Tailwind 23 | - Prisma 24 | - MongoDB 25 | - Swiper (slider) 26 | - Next-Auth 27 | - React Query 28 | - [Player](https://www.npmjs.com/package/vnetwork-player) 29 | 30 | ## Features 31 | 32 | - Watch anime by episode support iframe and custom player 33 | - Advanced anime search by keyword, season, format, status, genre,... 34 | - Sign in via google or github using next auth 35 | - Customize video player adjust playback speed, video quality, subtitles 36 | - Save anime to your lists 37 | - Infinity page scrolling using react-intersection-observer 38 | - Support ssr, seo friendly by nextjs 39 | - Comment on anime while watching 40 | 41 | ## Screenshots, Preview 42 | 43 | ![Screenshot 1](https://res.cloudinary.com/annnn/image/upload/v1683903029/localhost_3000__2_sxk4pr.png) 44 | ![Screenshot 2](https://res.cloudinary.com/annnn/image/upload/v1683903024/localhost_3000__4_uea2iw.png) 45 | 46 | ## Star History 47 | 48 | 49 | 50 | 51 | 52 | Star History Chart 53 | 54 | 55 | 56 | ## Summary 57 | 58 | ### 👉 If you like this project, give it a star ✨ and share 👨🏻‍💻 it to your friends 👈 59 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-anime", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 4099", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate" 11 | }, 12 | "dependencies": { 13 | "@next-auth/prisma-adapter": "^1.0.6", 14 | "@prisma/client": "^4.14.0", 15 | "@types/node": "18.16.3", 16 | "@types/react": "18.2.5", 17 | "@types/react-dom": "18.2.3", 18 | "autoprefixer": "10.4.14", 19 | "axios": "^1.4.0", 20 | "dayjs": "^1.11.7", 21 | "eslint": "8.39.0", 22 | "eslint-config-next": "13.4.0", 23 | "hls.js": "^1.5.1", 24 | "next": "13.4.0", 25 | "nextjs-progressbar": "^0.0.16", 26 | "postcss": "8.4.23", 27 | "react": "18.2.0", 28 | "react-cssfx-loading": "^2.1.0", 29 | "react-dom": "18.2.0", 30 | "react-hot-toast": "^2.4.1", 31 | "react-icons": "^4.8.0", 32 | "react-intersection-observer": "^9.4.3", 33 | "react-lazy-load-image-component": "^1.5.6", 34 | "react-query": "^3.39.3", 35 | "subtitle-converter": "^3.0.12", 36 | "swiper": "^9.2.4", 37 | "tailwindcss": "3.3.2", 38 | "typescript": "5.0.4", 39 | "vnetwork-player": "^1.1.33" 40 | }, 41 | "devDependencies": { 42 | "@types/qs": "^6.9.7", 43 | "@types/react-lazy-load-image-component": "^1.5.3", 44 | "prisma": "^4.14.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mongodb" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | // Necessary for Next auth 14 | model Account { 15 | id String @id @default(cuid()) @map("_id") 16 | userId String 17 | type String 18 | provider String 19 | providerAccountId String 20 | refresh_token String? // @db.Text 21 | access_token String? // @db.Text 22 | expires_at Int? 23 | token_type String? 24 | scope String? 25 | id_token String? // @db.Text 26 | session_state String? 27 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 28 | 29 | @@unique([provider, providerAccountId]) 30 | } 31 | 32 | model Session { 33 | id String @id @default(cuid()) @map("_id") 34 | sessionToken String @unique 35 | userId String 36 | expires DateTime 37 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 | } 39 | 40 | model User { 41 | id String @id @default(cuid()) @map("_id") 42 | name String? 43 | email String? @unique 44 | emailVerified DateTime? 45 | image String? 46 | accounts Account[] 47 | sessions Session[] 48 | list List[] 49 | comment Comment[] 50 | like Like[] 51 | } 52 | 53 | model VerificationToken { 54 | id String @id @default(cuid()) @map("_id") 55 | identifier String 56 | token String @unique 57 | expires DateTime 58 | 59 | @@unique([identifier, token]) 60 | } 61 | 62 | model List { 63 | id String @id @default(cuid()) @map("_id") 64 | animeId String 65 | user User @relation(fields: [userId], references: [id]) 66 | userId String 67 | animeImage String 68 | animeType String 69 | animeColor String 70 | animeTitle String 71 | nextEpisodeTime BigInt? 72 | } 73 | 74 | model Comment { 75 | id String @id @default(cuid()) @map("_id") 76 | animeId String 77 | user User @relation(fields: [userId], references: [id]) 78 | userId String 79 | text String 80 | createdAt DateTime @default(now()) 81 | like Like[] 82 | animeName String 83 | } 84 | 85 | model Like { 86 | id String @id @default(cuid()) @map("_id") 87 | comment Comment @relation(fields: [commentId], references: [id]) 88 | user User @relation(fields: [userId], references: [id]) 89 | commentId String 90 | userId String 91 | } 92 | -------------------------------------------------------------------------------- /public/logo-svg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/an678-mhg/next-anime/4275aa4907cce111fbd9fe7b73c2d4aa126ce88d/public/logo.png -------------------------------------------------------------------------------- /public/luffy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/an678-mhg/next-anime/4275aa4907cce111fbd9fe7b73c2d4aa126ce88d/public/luffy.gif -------------------------------------------------------------------------------- /public/not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/an678-mhg/next-anime/4275aa4907cce111fbd9fe7b73c2d4aa126ce88d/public/not-found.png -------------------------------------------------------------------------------- /public/share-icon/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 11 | 12 | 13 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/share-icon/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/share-icon/reddit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/share-icon/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Anime/AnimeBannerDetail.tsx: -------------------------------------------------------------------------------- 1 | import { getAnimeTitle } from "@/src/utils/contants"; 2 | import path from "@/src/utils/path"; 3 | import Link from "next/link"; 4 | import React, { useEffect, useState } from "react"; 5 | import { AiFillClockCircle, AiOutlinePlus } from "react-icons/ai"; 6 | import { 7 | BsCheck2, 8 | BsDot, 9 | BsFillCalendarDateFill, 10 | BsFillPlayCircleFill, 11 | } from "react-icons/bs"; 12 | import { LazyLoadImage } from "react-lazy-load-image-component"; 13 | import ShareSocial from "../Shared/ShareSocial"; 14 | import { AnimeInfo } from "@/src/types/anime"; 15 | import AnimeInfoDetail from "./AnimeInfoDetail"; 16 | import { checkAnimeInList, createList } from "@/src/services/list"; 17 | import { useMutation } from "react-query"; 18 | import { toast } from "react-hot-toast"; 19 | import { CircularProgress } from "react-cssfx-loading"; 20 | import { useSession } from "next-auth/react"; 21 | import { useRouter } from "next/router"; 22 | 23 | interface AnimeBannerDetailProps { 24 | info: AnimeInfo; 25 | } 26 | 27 | const AnimeBannerDetail: React.FC = ({ info }) => { 28 | const [isInTheList, setIsInTheList] = useState(false); 29 | const [isError, setIsError] = useState(false); 30 | const [checkAnimeLoading, setCheckAnimeLoading] = useState(false); 31 | 32 | const { data: session } = useSession(); 33 | const router = useRouter(); 34 | 35 | const { mutateAsync, isLoading } = useMutation(createList, { 36 | onSuccess: (response) => { 37 | toast.success(`${response.status} anime in list success`); 38 | setIsInTheList(() => (response.status === "Add" ? true : false)); 39 | }, 40 | onError: () => { 41 | toast.error("Add anime in list failed"); 42 | setIsInTheList((prev) => !prev); 43 | }, 44 | }); 45 | 46 | useEffect(() => { 47 | if (!session?.user) { 48 | return; 49 | } 50 | 51 | setCheckAnimeLoading(true); 52 | 53 | checkAnimeInList(info?.id) 54 | .then((data) => { 55 | data ? setIsInTheList(true) : setIsInTheList(false); 56 | }) 57 | .catch(() => setIsError(true)) 58 | .finally(() => setCheckAnimeLoading(false)); 59 | }, [session?.user, info?.id]); 60 | 61 | const handleClick = () => { 62 | if (!session?.user) { 63 | return router?.push( 64 | `${path.signIn}?redirect=${encodeURIComponent(router?.asPath)}` 65 | ); 66 | } 67 | 68 | if (isError) { 69 | return toast.error("Something went wrong f5 and try again"); 70 | } 71 | 72 | if (isInTheList) { 73 | if (!window.confirm("You want to remove this anime from the list")) { 74 | return; 75 | } 76 | } 77 | 78 | mutateAsync({ 79 | animeColor: info?.color || "#fff", 80 | animeId: info?.id, 81 | animeImage: info?.image, 82 | animeTitle: getAnimeTitle(info?.title), 83 | animeType: info?.type, 84 | nextEpisodeTime: info?.nextAiringEpisode?.airingTime, 85 | }); 86 | }; 87 | 88 | return ( 89 |
90 |
91 |
92 | 97 |
98 |
99 |

100 | Home 101 | 102 |

103 | {getAnimeTitle(info?.title)} 104 |

105 |

106 |
110 | {getAnimeTitle(info?.title)} 111 |
112 |
113 | {info?.type && ( 114 |

115 | 116 | {info?.type} 117 |

118 | )} 119 | {info?.duration && ( 120 |

121 | 122 | {info?.duration}m 123 |

124 | )} 125 | {info?.releaseDate && ( 126 |

127 | 128 | {info?.releaseDate} 129 |

130 | )} 131 |
132 |
133 | 137 | 138 | Watch now 139 | 140 | 162 |
163 |
167 |
168 |
169 |

Share

170 |

to your friends

171 |
172 | 176 |
177 |
178 |
179 |
180 | 181 |
182 |
183 | ); 184 | }; 185 | 186 | export default AnimeBannerDetail; 187 | -------------------------------------------------------------------------------- /src/components/Anime/AnimeCard.tsx: -------------------------------------------------------------------------------- 1 | import path from "@/src/utils/path"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import { LazyLoadImage } from "react-lazy-load-image-component"; 5 | 6 | interface AnimeCardProps { 7 | title: string; 8 | type: string; 9 | image: string; 10 | id: string; 11 | color: string; 12 | } 13 | 14 | const AnimeCard: React.FC = ({ image, title, id, color }) => { 15 | return ( 16 | 17 |
18 | 24 |
25 | 26 | ); 27 | }; 28 | 29 | export default AnimeCard; 30 | -------------------------------------------------------------------------------- /src/components/Anime/AnimeInfoDetail.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import GenresItem from "../Shared/GenresItem"; 3 | import dayjs from "dayjs"; 4 | import { AnimeInfo } from "@/src/types/anime"; 5 | 6 | interface AnimeInfoDetailProps { 7 | info: AnimeInfo; 8 | } 9 | 10 | const AnimeInfoDetail: React.FC = ({ info }) => { 11 | return ( 12 |
13 |
14 |
15 |
16 |

Japanese:

17 |

{info?.title?.native}

18 |
19 |
20 |

Synonyms:

21 |

{info?.synonyms[0]}

22 |
23 |
24 |

Aired:

25 |

26 | {dayjs(info?.nextAiringEpisode?.airingTime * 1000).format( 27 | "DD/MM/YYYY" 28 | )} 29 |

30 |
31 |
32 |

Country:

33 |

{info?.countryOfOrigin}

34 |
35 |
36 |

Daration:

37 |

{info?.duration}

38 |
39 |
40 |

Status:

41 |

{info?.status}

42 |
43 |
44 |

Rating:

45 |

{info?.rating}

46 |
47 |
48 |
49 | {info?.genres?.map((item) => ( 50 | 51 | ))} 52 |
53 |
54 |
55 |

Studios:

56 |

{info?.studios}

57 |
58 |
59 |

Current episode:

60 |

{info?.currentEpisode}

61 |
62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default AnimeInfoDetail; 69 | -------------------------------------------------------------------------------- /src/components/Anime/Banners.tsx: -------------------------------------------------------------------------------- 1 | import { Anime } from "@/src/types/anime"; 2 | import React from "react"; 3 | import { BsFillPlayCircleFill, BsFillCalendarDateFill } from "react-icons/bs"; 4 | import { AiFillClockCircle, AiOutlineRight } from "react-icons/ai"; 5 | import { GrStatusDisabledSmall } from "react-icons/gr"; 6 | import Link from "next/link"; 7 | import GenresItem from "../Shared/GenresItem"; 8 | import { LazyLoadImage } from "react-lazy-load-image-component"; 9 | import path from "@/src/utils/path"; 10 | import { getAnimeTitle, setBackgroundImage } from "@/src/utils/contants"; 11 | 12 | interface BannersProps { 13 | anime: Anime; 14 | } 15 | 16 | const Banners: React.FC = ({ anime }) => { 17 | return ( 18 |
22 |
23 |
24 |

28 | {getAnimeTitle(anime?.title)} 29 |

30 |
31 | {anime?.type && ( 32 |

33 | 34 | {anime?.type} 35 |

36 | )} 37 | {anime?.duration && ( 38 |

39 | 40 | {anime?.duration}m 41 |

42 | )} 43 | {anime?.releaseDate && ( 44 |

45 | 46 | {anime?.releaseDate} 47 |

48 | )} 49 | {anime?.status && ( 50 |

51 | 52 | {anime?.status} 53 |

54 | )} 55 |
56 |
57 | {anime?.genres?.slice(0, 3)?.map((item) => ( 58 | 59 | ))} 60 |
61 |
65 |
66 | 70 | 71 | 72 | Watch now 73 | 74 | 75 | 79 | Detail 80 | 81 | 82 |
83 |
84 |
85 | 90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export default Banners; 97 | -------------------------------------------------------------------------------- /src/components/Anime/SlideBanner.tsx: -------------------------------------------------------------------------------- 1 | import "swiper/css"; 2 | import "swiper/css/navigation"; 3 | import "swiper/css/pagination"; 4 | import React from "react"; 5 | import { Swiper, SwiperSlide } from "swiper/react"; 6 | import { Autoplay, Pagination } from "swiper"; 7 | import { Anime } from "@/src/types/anime"; 8 | import Banners from "./Banners"; 9 | 10 | interface SlideProps { 11 | tredingAnime: Anime[]; 12 | } 13 | 14 | const SlideBanner: React.FC = ({ tredingAnime }) => { 15 | return ( 16 | 21 | {tredingAnime?.map((anime) => ( 22 | 23 | 24 | 25 | ))} 26 | 27 | ); 28 | }; 29 | 30 | export default SlideBanner; 31 | -------------------------------------------------------------------------------- /src/components/Characters/CharacterCard.tsx: -------------------------------------------------------------------------------- 1 | import { Character } from "@/src/types/utils"; 2 | import path from "@/src/utils/path"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | import { LazyLoadImage } from "react-lazy-load-image-component"; 6 | 7 | interface CharacterCardProps { 8 | character: Character; 9 | } 10 | 11 | const CharacterCard: React.FC = ({ character }) => { 12 | return ( 13 | 17 | 22 |
23 |

{character?.name?.full}

24 |

{character?.role}

25 |
26 | 27 | ); 28 | }; 29 | 30 | export default CharacterCard; 31 | -------------------------------------------------------------------------------- /src/components/Characters/CharactersList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TitlePrimary from "../Shared/TitlePrimary"; 3 | import { Character } from "../../types/utils"; 4 | import CharacterCard from "./CharacterCard"; 5 | 6 | interface CharactersListProps { 7 | characters: Character[]; 8 | } 9 | 10 | const CharactersList: React.FC = ({ characters }) => { 11 | return ( 12 |
13 | 14 |
15 | {characters?.map((item) => ( 16 | 17 | ))} 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default CharactersList; 24 | -------------------------------------------------------------------------------- /src/components/Comment/CommentItem.tsx: -------------------------------------------------------------------------------- 1 | import { deleteComment, likeComment } from "@/src/services/comment"; 2 | import { Comment } from "@/src/types/comment"; 3 | import { calculateCreatedTime } from "@/src/utils/contants"; 4 | import React from "react"; 5 | import { CircularProgress } from "react-cssfx-loading"; 6 | import { BiTrashAlt } from "react-icons/bi"; 7 | import { BsDot, BsReplyFill } from "react-icons/bs"; 8 | import { LazyLoadImage } from "react-lazy-load-image-component"; 9 | import { useMutation, useQueryClient } from "react-query"; 10 | import { useSession } from "next-auth/react"; 11 | import { AiFillLike, AiOutlineLike } from "react-icons/ai"; 12 | import { toast } from "react-hot-toast"; 13 | 14 | interface CommentItemProps { 15 | comment: Comment; 16 | } 17 | 18 | const CommentItem: React.FC = ({ comment }) => { 19 | const queryClient = useQueryClient(); 20 | const { data } = useSession(); 21 | 22 | const key = `comment-${comment?.animeId}`; 23 | 24 | const { mutateAsync, isLoading } = useMutation(deleteComment, { 25 | onSuccess: () => { 26 | queryClient?.setQueryData( 27 | key, 28 | (queryClient?.getQueryData(key) as Comment[]).filter( 29 | (item) => item.id !== comment.id 30 | ) 31 | ); 32 | }, 33 | }); 34 | 35 | const { mutateAsync: likeCommentMutate } = useMutation(likeComment); 36 | 37 | const handleDelete = () => { 38 | if (!window.confirm("Are you sure delete comment!")) { 39 | return; 40 | } 41 | 42 | mutateAsync(comment?.id); 43 | }; 44 | 45 | const handleLikeComment = () => { 46 | if (!data?.user?.id) { 47 | return toast.error("Login first!"); 48 | } 49 | 50 | queryClient.setQueryData( 51 | key, 52 | (queryClient.getQueryData(key) as Comment[]).map((item) => { 53 | if (item.id === comment.id) { 54 | return { 55 | ...item, 56 | isLiked: !item.isLiked, 57 | _count: { 58 | ...item._count, 59 | like: item.isLiked ? item._count.like - 1 : item._count.like + 1, 60 | }, 61 | }; 62 | } 63 | 64 | return item; 65 | }) 66 | ); 67 | 68 | likeCommentMutate({ commentId: comment.id, userId: data?.user?.id! }); 69 | }; 70 | 71 | return ( 72 |
73 |
74 | 79 |
80 |
81 |

{comment?.user?.name}

82 | 83 | 84 |

85 | {calculateCreatedTime(comment.createdAt)} 86 |

87 |
88 |

{comment.text}

89 |
90 |
91 |
92 |
96 | {!comment?.isLiked ? : } 97 | {comment?._count?.like} 98 |
99 |
100 | 101 | 0 102 |
103 | {data?.user?.id === comment.userId && ( 104 | 116 | )} 117 |
118 |
119 | ); 120 | }; 121 | 122 | export default CommentItem; 123 | -------------------------------------------------------------------------------- /src/components/Comment/CommentList.tsx: -------------------------------------------------------------------------------- 1 | import { Comment } from "@/src/types/comment"; 2 | import React from "react"; 3 | import CommentItem from "./CommentItem"; 4 | 5 | interface CommentListProps { 6 | comments: Comment[]; 7 | } 8 | 9 | const CommentList: React.FC = ({ comments }) => { 10 | return ( 11 |
12 | {comments?.length === 0 && ( 13 |
14 | No comment yet 15 |
16 | )} 17 | {comments?.map((comment) => ( 18 | 19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default CommentList; 25 | -------------------------------------------------------------------------------- /src/components/Comment/Input.tsx: -------------------------------------------------------------------------------- 1 | import path from "@/src/utils/path"; 2 | import Link from "next/link"; 3 | import React, { FormEvent, useState } from "react"; 4 | import { useSession } from "next-auth/react"; 5 | import { AiOutlineSend } from "react-icons/ai"; 6 | import { useMutation, useQueryClient } from "react-query"; 7 | import { createComment } from "@/src/services/comment"; 8 | import { toast } from "react-hot-toast"; 9 | import { CircularProgress } from "react-cssfx-loading"; 10 | import { CreateCommentBody } from "@/src/types/comment"; 11 | import { Comment } from "@prisma/client"; 12 | import { Comment as CommentType } from "@/src/types/comment"; 13 | import { MdMessage } from "react-icons/md"; 14 | 15 | interface InputProps { 16 | animeId: string; 17 | animeName: string; 18 | } 19 | 20 | const Input: React.FC = ({ animeId, animeName }) => { 21 | const { data: session } = useSession(); 22 | const [text, setText] = useState(""); 23 | const queryClient = useQueryClient(); 24 | 25 | const { mutateAsync, isLoading } = useMutation(createComment, { 26 | onError: () => { 27 | toast.error("Failed to create new comment"); 28 | }, 29 | onSuccess: (response: Comment) => { 30 | const comment: CommentType = { 31 | animeId: response.animeId, 32 | createdAt: response.createdAt, 33 | id: response.id, 34 | text: response.text, 35 | user: { 36 | name: session?.user?.name!, 37 | email: session?.user?.email!, 38 | emailVerified: null, 39 | id: session?.user?.id!, 40 | image: session?.user?.image!, 41 | }, 42 | userId: response.userId, 43 | animeName, 44 | _count: { 45 | like: 0, 46 | }, 47 | isLiked: false, 48 | }; 49 | 50 | setText(""); 51 | 52 | const key = `comment-${animeId}`; 53 | 54 | queryClient.setQueriesData( 55 | [key], 56 | [...(queryClient.getQueryData(key) as Comment[]), comment] 57 | ); 58 | }, 59 | }); 60 | 61 | const handleCreateComment = async (e: FormEvent) => { 62 | e.preventDefault(); 63 | 64 | if (!session?.user) return; 65 | 66 | if (!text.trim()) return; 67 | 68 | const newComment: CreateCommentBody = { 69 | animeId, 70 | text, 71 | userId: session?.user?.id, 72 | animeName, 73 | }; 74 | 75 | mutateAsync(newComment); 76 | }; 77 | 78 | return ( 79 |
83 | {!session?.user ? ( 84 |
85 |

86 | You must{" "} 87 | 88 | login 89 | {" "} 90 | to comment 91 |

92 |
93 | ) : ( 94 |
95 | 96 | setText(e.target.value)} 99 | placeholder="Leave a comment...." 100 | className="font-semibold text-sm bg-transparent flex-1 outline-none py-2" 101 | /> 102 | 109 |
110 | )} 111 |
112 | ); 113 | }; 114 | 115 | export default Input; 116 | -------------------------------------------------------------------------------- /src/components/Comment/NewestComment.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NewestCommentItem from "./NewestCommentItem"; 3 | import { SwiperSlide } from "swiper/react"; 4 | import TitlePrimary from "../Shared/TitlePrimary"; 5 | import { Comment } from "@/src/types/comment"; 6 | import SwiperContainer from "../Shared/SwiperContainer"; 7 | 8 | interface NewestCommentProps { 9 | comments: Comment[]; 10 | } 11 | 12 | const NewestComment: React.FC = ({ comments }) => { 13 | return ( 14 |
15 |
16 |
17 | 18 |
19 | 20 |
21 | 28 | {comments.map((item) => ( 29 | 30 | 31 | 32 | ))} 33 | 34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default NewestComment; 41 | -------------------------------------------------------------------------------- /src/components/Comment/NewestCommentItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LazyLoadImage } from "react-lazy-load-image-component"; 3 | import { Comment } from "@/src/types/comment"; 4 | import { calculateCreatedTime } from "@/src/utils/contants"; 5 | import Link from "next/link"; 6 | import path from "@/src/utils/path"; 7 | import { HiOutlineDocumentText } from "react-icons/hi"; 8 | 9 | interface NewestCommentItemProps { 10 | comment: Comment; 11 | } 12 | 13 | const NewestCommentItem: React.FC = ({ comment }) => { 14 | return ( 15 |
16 |
17 |
18 | 23 |
24 |

{comment.user?.name}

25 |

26 | {calculateCreatedTime(comment.createdAt)} 27 |

28 |
29 |
30 |

{comment.text}

31 |
32 | 36 | 37 | {comment.animeName} 38 | 39 |
40 | ); 41 | }; 42 | 43 | export default NewestCommentItem; 44 | -------------------------------------------------------------------------------- /src/components/Headers/Logo.tsx: -------------------------------------------------------------------------------- 1 | import path from "@/src/utils/path"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import { TbBrandNextjs } from "react-icons/tb"; 5 | 6 | const Logo = () => { 7 | return ( 8 | 9 |

10 | Anime 11 |

12 | 13 | ); 14 | }; 15 | 16 | export default Logo; 17 | -------------------------------------------------------------------------------- /src/components/Headers/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | AiOutlineLogout, 4 | AiOutlineMail, 5 | AiOutlineOrderedList, 6 | } from "react-icons/ai"; 7 | import { signOut } from "next-auth/react"; 8 | import Link from "next/link"; 9 | import path from "@/src/utils/path"; 10 | import { LazyLoadImage } from "react-lazy-load-image-component"; 11 | 12 | interface MenuProps { 13 | email: string; 14 | name: string; 15 | avatar: string; 16 | } 17 | 18 | const Menu: React.FC = ({ email, name, avatar }) => { 19 | return ( 20 |
e.stopPropagation()} 22 | className="opacity-animation absolute w-[200px] right-0 bg-[#222] top-[30px] rounded-md overflow-hidden" 23 | > 24 |
    25 |
  • 26 | 31 | {name} 32 |
  • 33 |
  • 34 | 35 | {email} 36 |
  • 37 |
    38 |
  • 39 | 43 | 44 | List 45 | 46 |
  • 47 |
    48 |
  • signOut()} 50 | className="hover:bg-gray-500 transition-colors rounded-md p-2 flex items-center space-x-3 cursor-pointer" 51 | > 52 | 53 | Log out 54 |
  • 55 |
56 |
57 | ); 58 | }; 59 | 60 | export default Menu; 61 | -------------------------------------------------------------------------------- /src/components/Headers/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import Logo from "./Logo"; 3 | import { CiSearch } from "react-icons/ci"; 4 | import Link from "next/link"; 5 | import path from "@/src/utils/path"; 6 | import { useSession } from "next-auth/react"; 7 | import { LazyLoadImage } from "react-lazy-load-image-component"; 8 | import Menu from "./Menu"; 9 | 10 | const Headers = () => { 11 | const headerRef = useRef(null); 12 | const { data } = useSession(); 13 | const [showMenu, setShowMenu] = useState(false); 14 | 15 | useEffect(() => { 16 | const handleFixedHeader = () => { 17 | const header = headerRef?.current as HTMLDivElement; 18 | const sticky = header && header?.offsetTop; 19 | 20 | if (header) { 21 | if (window.pageYOffset > sticky) { 22 | header.classList.add("is-sticky"); 23 | } else { 24 | header.classList.remove("is-sticky"); 25 | } 26 | } 27 | }; 28 | 29 | window.addEventListener("scroll", handleFixedHeader); 30 | 31 | return () => window.removeEventListener("scroll", handleFixedHeader); 32 | }, []); 33 | 34 | useEffect(() => { 35 | document.addEventListener("click", () => setShowMenu(false)); 36 | 37 | return () => 38 | document.removeEventListener("click", () => setShowMenu(false)); 39 | }, []); 40 | 41 | return ( 42 |
43 |
44 | 45 |
46 | 47 | 48 | 49 | {data?.user ? ( 50 |
{ 52 | e.stopPropagation(); 53 | setShowMenu((prev) => !prev); 54 | }} 55 | className="flex items-center justify-center relative" 56 | > 57 | 62 | {showMenu && ( 63 | 68 | )} 69 |
70 | ) : ( 71 | 75 | Sign In 76 | 77 | )} 78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default Headers; 85 | -------------------------------------------------------------------------------- /src/components/Player/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import VPlayer, {PlayerProps} from "vnetwork-player"; 3 | 4 | import 'vnetwork-player/dist/vnetwork-player.min.css' 5 | 6 | const Player:React.FC = (props) => { 7 | return 8 | } 9 | 10 | export default Player -------------------------------------------------------------------------------- /src/components/Search/Genres.tsx: -------------------------------------------------------------------------------- 1 | import { genresFilter } from "@/src/utils/filter"; 2 | import React from "react"; 3 | import { BsCheck2 } from "react-icons/bs"; 4 | 5 | interface GenresProps { 6 | handleChangeGenres: (item: string) => void; 7 | genresIsCheck: (item: string) => boolean; 8 | } 9 | 10 | const Genres: React.FC = ({ 11 | handleChangeGenres, 12 | genresIsCheck, 13 | }) => { 14 | return ( 15 |
16 | {genresFilter?.map((item) => ( 17 |

handleChangeGenres(item.value)} 19 | className="border cursor-pointer border-white flex items-center space-x-3 p-2 text-sm flex-2 rounded-md" 20 | key={item?.value} 21 | > 22 | {item?.label} {genresIsCheck(item.value) && } 23 |

24 | ))} 25 |
26 | ); 27 | }; 28 | 29 | export default Genres; 30 | -------------------------------------------------------------------------------- /src/components/Search/SelectFilter.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | formatFilter, 3 | seasonFilter, 4 | sortFilter, 5 | statusFilter, 6 | } from "@/src/utils/filter"; 7 | import React, { ChangeEvent, useRef, useState } from "react"; 8 | import { CiSearch } from "react-icons/ci"; 9 | import Genres from "./Genres"; 10 | import { Queries } from "@/src/pages/search"; 11 | 12 | interface SelectFilterProps { 13 | queries: Queries; 14 | setQueries: React.Dispatch>; 15 | } 16 | 17 | const SelectFilter: React.FC = ({ queries, setQueries }) => { 18 | const timeout = useRef(); 19 | const [queryTmp, setQueryTmp] = useState(""); 20 | 21 | const handleOnChange = (e: ChangeEvent) => { 22 | const name = e.target.name; 23 | const value = e.target.value; 24 | setQueries({ ...queries, [name]: value }); 25 | }; 26 | 27 | const handleOnChangeQuery = (e: ChangeEvent) => { 28 | const value = e.target.value; 29 | setQueryTmp(value); 30 | 31 | if (timeout.current) { 32 | clearTimeout(timeout.current); 33 | } 34 | 35 | timeout.current = setTimeout(() => { 36 | setQueries({ ...queries, query: value }); 37 | }, 300); 38 | }; 39 | 40 | const handleChangeGenres = (item: string) => { 41 | if (queries?.genres?.some((value) => value === item)) { 42 | return setQueries({ 43 | ...queries, 44 | genres: queries?.genres?.filter((value) => value !== item), 45 | }); 46 | } 47 | 48 | setQueries({ ...queries, genres: [...queries?.genres, item] }); 49 | }; 50 | 51 | const genresIsCheck = (item: string) => { 52 | return queries?.genres?.some((value) => value === item); 53 | }; 54 | 55 | return ( 56 |
57 |
58 |
59 | 60 |
61 | 62 | 68 |
69 |
70 |
71 | 72 |
73 | 89 |
90 |
91 |
92 | 93 |
94 | 110 |
111 |
112 |
113 | 114 |
115 | 131 |
132 |
133 |
134 | 135 |
136 | 152 |
153 |
154 |
155 | 159 |
160 | ); 161 | }; 162 | 163 | export default SelectFilter; 164 | -------------------------------------------------------------------------------- /src/components/Shared/404NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LazyLoadImage } from "react-lazy-load-image-component"; 3 | import MainLayout from "../../layouts/MainLayout"; 4 | 5 | const NotFound = () => { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /src/components/Shared/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Logo from "../Headers/Logo"; 3 | import ShareSocial from "./ShareSocial"; 4 | 5 | const Footer = () => { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 | 16 |
17 |
18 |

A-Z

19 |
20 |

21 | Searching anime order by alphabet name A to Z. 22 |

23 |
24 |
25 |

26 | Terms of service 27 |

28 |

29 | DMCA 30 |

31 |

32 | Contact 33 |

34 |

35 | Proxy Sites 36 |

37 |
38 |
39 | Next Anime does not store any files on our server, we only linked to 40 | the media which is hosted on 3rd party services.4 41 |
42 | ©next-anime.vercel.app 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Footer; 50 | -------------------------------------------------------------------------------- /src/components/Shared/GenresItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import path from "../../utils/path"; 4 | 5 | interface GenresItemProps { 6 | genres: string; 7 | } 8 | 9 | const GenresItem: React.FC = ({ genres }) => { 10 | return ( 11 | 15 | {genres} 16 | 17 | ); 18 | }; 19 | 20 | export default GenresItem; 21 | -------------------------------------------------------------------------------- /src/components/Shared/Meta.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Head from "next/head"; 3 | import { useRouter } from "next/router"; 4 | import { BASE_URL } from "../../utils/contants"; 5 | 6 | interface MetaProps { 7 | title: string; 8 | description: string; 9 | image: string; 10 | } 11 | 12 | const Meta: FC = ({ title, image, description }) => { 13 | const router = useRouter(); 14 | 15 | return ( 16 | 17 | {title} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Meta; 37 | -------------------------------------------------------------------------------- /src/components/Shared/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { HiArrowUp } from "react-icons/hi"; 3 | 4 | const ScrollToTop = () => { 5 | const [show, setShow] = useState(false); 6 | 7 | useEffect(() => { 8 | const handleScroll = () => { 9 | window.scrollY >= 150 ? setShow(true) : setShow(false); 10 | }; 11 | 12 | window.addEventListener("scroll", handleScroll); 13 | 14 | return () => { 15 | window.removeEventListener("scroll", handleScroll); 16 | }; 17 | }, []); 18 | 19 | const handleScrollTop = () => 20 | window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); 21 | 22 | return ( 23 |
30 | 31 |
32 | ); 33 | }; 34 | 35 | export default ScrollToTop; 36 | -------------------------------------------------------------------------------- /src/components/Shared/ShareSocial.tsx: -------------------------------------------------------------------------------- 1 | import { providers } from "@/src/utils/contants"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import { LazyLoadImage } from "react-lazy-load-image-component"; 5 | 6 | interface ShareSocialProps { 7 | link: string; 8 | title: string; 9 | } 10 | 11 | const ShareSocial: React.FC = ({ link, title }) => { 12 | return ( 13 |
14 | {providers?.map((provider) => ( 15 | 21 | 28 | 29 | ))} 30 |
31 | ); 32 | }; 33 | 34 | export default ShareSocial; 35 | -------------------------------------------------------------------------------- /src/components/Shared/SwiperContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { Swiper } from "swiper/react"; 3 | import { Layout } from "../../types/utils"; 4 | import useInnerWidth from "../../hooks/useInnerWidth"; 5 | 6 | interface SwiperContainerProps extends Layout { 7 | xl: number; 8 | lg: number; 9 | md: number; 10 | sm: number; 11 | spaceBetween: number; 12 | className?: string; 13 | } 14 | 15 | const SwiperContainer: React.FC = ({ 16 | children, 17 | lg, 18 | md, 19 | sm, 20 | spaceBetween, 21 | xl, 22 | className = "", 23 | }) => { 24 | const width = useInnerWidth(); 25 | 26 | const slidesPerView = useMemo(() => { 27 | return width >= 1200 ? xl : width >= 1024 ? lg : width >= 768 ? md : sm; 28 | }, [width]); 29 | 30 | return ( 31 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | export default SwiperContainer; 42 | -------------------------------------------------------------------------------- /src/components/Shared/TitlePrimary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface TitlePrimaryProps { 4 | title: string; 5 | } 6 | 7 | const TitlePrimary: React.FC = ({ title }) => { 8 | return

{title}

; 9 | }; 10 | 11 | export default TitlePrimary; 12 | -------------------------------------------------------------------------------- /src/components/Shared/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "react-hot-toast"; 2 | 3 | const Toast = () => { 4 | return ; 5 | }; 6 | 7 | export default Toast; 8 | -------------------------------------------------------------------------------- /src/components/ShowCase/BoxShowCase.tsx: -------------------------------------------------------------------------------- 1 | import { Anime } from "@/src/types/anime"; 2 | import React from "react"; 3 | import ShowCaseItem from "./ShowCaseItem"; 4 | 5 | interface BoxShowCaseProps { 6 | title: string; 7 | anime: Anime[]; 8 | } 9 | 10 | const BoxShowCase: React.FC = ({ title, anime }) => { 11 | return ( 12 |
13 |

14 | {title} 15 |

16 |
17 | {anime?.map((item) => ( 18 | 28 | ))} 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default BoxShowCase; 35 | -------------------------------------------------------------------------------- /src/components/ShowCase/ShowCaseItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getAnimeTitle } from "../../utils/contants"; 3 | import { BsFillCalendarDateFill, BsFillPlayCircleFill } from "react-icons/bs"; 4 | import path from "../../utils/path"; 5 | import { Title } from "../../types/utils"; 6 | import { AiFillClockCircle } from "react-icons/ai"; 7 | import Link from "next/link"; 8 | import { LazyLoadImage } from "react-lazy-load-image-component"; 9 | 10 | interface ShowCaseItemProps { 11 | id: string; 12 | image: string; 13 | title: Title; 14 | type?: string; 15 | duration?: number; 16 | releaseDate?: number; 17 | border?: boolean; 18 | color: string; 19 | } 20 | 21 | const ShowCaseItem: React.FC = ({ 22 | duration, 23 | id, 24 | image, 25 | releaseDate, 26 | title, 27 | type, 28 | border = true, 29 | color, 30 | }) => { 31 | return ( 32 | 37 | 42 |
43 |
44 | {getAnimeTitle(title)} 45 |
46 |
47 | {type && ( 48 |

49 | 50 | {type} 51 |

52 | )} 53 | {duration && ( 54 |

55 | 56 | {duration}m 57 |

58 | )} 59 |
60 |
61 | 62 | ); 63 | }; 64 | 65 | export default ShowCaseItem; 66 | -------------------------------------------------------------------------------- /src/components/Skeleton/AnimeCardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AnimeCardSkeleton = () => { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | }; 10 | 11 | export default AnimeCardSkeleton; 12 | -------------------------------------------------------------------------------- /src/components/Watch/AnimeInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TitlePrimary from "../Shared/TitlePrimary"; 3 | import { LazyLoadImage } from "react-lazy-load-image-component"; 4 | import { getAnimeTitle } from "@/src/utils/contants"; 5 | import { NextAiringEpisode, Title } from "@/src/types/utils"; 6 | import { BsFillCalendarDateFill, BsFillPlayCircleFill } from "react-icons/bs"; 7 | import { AiFillClockCircle } from "react-icons/ai"; 8 | import dayjs from "dayjs"; 9 | 10 | interface AnimeInfoProps { 11 | image: string; 12 | title: Title; 13 | type: string; 14 | duration: number; 15 | releaseDate: number; 16 | description: string; 17 | nextAiringEpisode: NextAiringEpisode; 18 | } 19 | 20 | const AnimeInfo: React.FC = ({ 21 | duration, 22 | image, 23 | releaseDate, 24 | title, 25 | type, 26 | description, 27 | nextAiringEpisode, 28 | }) => { 29 | return ( 30 |
31 | 32 |
33 | 38 |
39 |
{getAnimeTitle(title)}
40 |
41 | {type && ( 42 |

43 | 44 | {type} 45 |

46 | )} 47 | {duration && ( 48 |

49 | 50 | {duration}m 51 |

52 | )} 53 | {releaseDate && ( 54 |

55 | 56 | {releaseDate} 57 |

58 | )} 59 |
60 |
64 | {nextAiringEpisode?.airingTime && nextAiringEpisode?.episode && ( 65 |

66 | Watch episode {nextAiringEpisode?.episode} on the day{" "} 67 | {dayjs(nextAiringEpisode?.airingTime * 1000).format("DD/MM/YYYY")} 68 |

69 | )} 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default AnimeInfo; 77 | -------------------------------------------------------------------------------- /src/components/Watch/Comment.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TitlePrimary from "../Shared/TitlePrimary"; 3 | import Input from "../Comment/Input"; 4 | import CommentList from "../Comment/CommentList"; 5 | import { useQuery } from "react-query"; 6 | import { getCommentByEpisodeId } from "@/src/services/comment"; 7 | import { CircularProgress } from "react-cssfx-loading"; 8 | 9 | interface CommentProps { 10 | animeName: string; 11 | animeId: string; 12 | } 13 | 14 | const Comment: React.FC = ({ animeName, animeId }) => { 15 | const { data: comments, isLoading } = useQuery([`comment-${animeId}`], () => 16 | getCommentByEpisodeId(animeId) 17 | ); 18 | 19 | return ( 20 |
21 | 22 | {comments && !isLoading ? ( 23 | <> 24 | 25 | 26 | 27 | ) : ( 28 |
29 | 30 |
31 | )} 32 |
33 | ); 34 | }; 35 | 36 | export default Comment; 37 | -------------------------------------------------------------------------------- /src/components/Watch/EpisodeInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Episode } from "@/src/types/utils"; 2 | import React from "react"; 3 | import TitlePrimary from "../Shared/TitlePrimary"; 4 | import { LazyLoadImage } from "react-lazy-load-image-component"; 5 | 6 | interface EpisodeInfoProps { 7 | episode: Episode; 8 | } 9 | 10 | const EpisodeInfo: React.FC = ({ episode }) => { 11 | return ( 12 |
13 | 14 |
15 | 16 |
17 |

{episode?.title}

18 |

{episode?.description}

19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default EpisodeInfo; 26 | -------------------------------------------------------------------------------- /src/components/Watch/EpisodeList.tsx: -------------------------------------------------------------------------------- 1 | import { Episode } from "@/src/types/utils"; 2 | import React from "react"; 3 | 4 | interface EpisodeListProps { 5 | episodes: Episode[]; 6 | episodeId: string; 7 | animeId: string; 8 | handleSelectEpisode: (episode: Episode) => void; 9 | } 10 | 11 | const EpisodeList: React.FC = ({ 12 | episodeId, 13 | episodes, 14 | handleSelectEpisode, 15 | }) => { 16 | return ( 17 |
18 | {episodes?.length === 0 && ( 19 |
No episodes found
20 | )} 21 | {episodes?.map((item) => ( 22 | 31 | ))} 32 |
33 | ); 34 | }; 35 | 36 | export default EpisodeList; 37 | -------------------------------------------------------------------------------- /src/components/Watch/MoreLikeThis.tsx: -------------------------------------------------------------------------------- 1 | import { Recommendation, Relation } from "@/src/types/anime"; 2 | import React, { useState } from "react"; 3 | import ShowCaseItem from "../ShowCase/ShowCaseItem"; 4 | 5 | interface MoreLikeThisProps { 6 | relations: Relation[]; 7 | recommendations: Recommendation[]; 8 | } 9 | 10 | const MoreLikeThis: React.FC = ({ 11 | recommendations, 12 | relations, 13 | }) => { 14 | const [selectType, setSelectType] = useState("recommendations"); 15 | const moreLikeThis = selectType === "related" ? relations : recommendations; 16 | 17 | return ( 18 |
19 |
20 | {["recommendations", "related"].map((item) => ( 21 | 30 | ))} 31 |
32 |
33 | {moreLikeThis.length === 0 && ( 34 |
35 | No {selectType} found! 36 |
37 | )} 38 | {moreLikeThis?.map((item) => ( 39 | 49 | ))} 50 |
51 |
52 | ); 53 | }; 54 | 55 | export default MoreLikeThis; 56 | -------------------------------------------------------------------------------- /src/components/Watch/Note.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TitlePrimary from "../Shared/TitlePrimary"; 3 | 4 | const Note = () => { 5 | return ( 6 |
7 | 8 |

9 | { 10 | "If you can't see the movie, try f5 again, change the source, or use an iframe" 11 | } 12 |

13 |
14 | ); 15 | }; 16 | 17 | export default Note; 18 | -------------------------------------------------------------------------------- /src/components/Watch/SelectIframe.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | interface SelectIframeProps { 4 | listIframe: string[]; 5 | setIframe: React.Dispatch>; 6 | iframe: string | null | undefined; 7 | } 8 | 9 | const SelectIframe: React.FC = ({ 10 | iframe, 11 | listIframe, 12 | setIframe, 13 | }) => { 14 | useEffect(() => { 15 | if (listIframe?.[0]) { 16 | setIframe(listIframe?.[0]); 17 | } 18 | }, []); 19 | 20 | return ( 21 |
22 |

Iframe:

23 | 38 |
39 | ); 40 | }; 41 | 42 | export default SelectIframe; 43 | -------------------------------------------------------------------------------- /src/components/Watch/SelectSource.tsx: -------------------------------------------------------------------------------- 1 | import path from "@/src/utils/path"; 2 | import { useRouter } from "next/router"; 3 | import React from "react"; 4 | 5 | const provider = [ 6 | { value: "gogoanime", label: "Gogo" }, 7 | { value: "zoro", label: "Zoro" }, 8 | ]; 9 | 10 | interface SelectSourceProps { 11 | idAnime: string; 12 | } 13 | 14 | const SelectSource: React.FC = ({ idAnime }) => { 15 | const router = useRouter(); 16 | 17 | return ( 18 |
19 |

Source:

20 | 37 |
38 | ); 39 | }; 40 | 41 | export default SelectSource; 42 | -------------------------------------------------------------------------------- /src/hooks/useInnerWidth.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const useInnerWidth = () => { 4 | const [width, setWidth] = useState(0); 5 | 6 | useEffect(() => { 7 | const handleResize = () => { 8 | setWidth(window.innerWidth); 9 | }; 10 | 11 | window?.addEventListener("resize", handleResize); 12 | 13 | return () => { 14 | window.removeEventListener("resize", handleResize); 15 | }; 16 | }, []); 17 | 18 | useEffect(() => { 19 | setWidth(window.innerWidth); 20 | }, []); 21 | 22 | return width; 23 | }; 24 | 25 | export default useInnerWidth; 26 | -------------------------------------------------------------------------------- /src/layouts/AnimeGridLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Layout } from "../types/utils"; 3 | import TitlePrimary from "../components/Shared/TitlePrimary"; 4 | 5 | interface AnimeGridLayoutProps extends Layout { 6 | title?: string; 7 | className: string; 8 | } 9 | 10 | const AnimeGridLayout: React.FC = ({ 11 | children, 12 | className, 13 | title, 14 | }) => { 15 | return ( 16 |
17 | {title && } 18 |
19 | {children} 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default AnimeGridLayout; 26 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/src/components/Shared/Footer"; 2 | import Headers from "@/src/components/Headers"; 3 | import { Layout } from "@/src/types/utils"; 4 | import React from "react"; 5 | 6 | const MainLayout: React.FC = ({ children }) => { 7 | return ( 8 |
9 | 10 |
{children}
11 |
12 |
13 | ); 14 | }; 15 | 16 | export default MainLayout; 17 | -------------------------------------------------------------------------------- /src/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let prisma: PrismaClient | null = null; 4 | 5 | if (typeof window === "undefined") { 6 | if (process.env.NODE_ENV === "production") { 7 | prisma = new PrismaClient(); 8 | } else { 9 | if (!global.prisma) { 10 | global.prisma = new PrismaClient(); 11 | } 12 | 13 | prisma = global.prisma; 14 | } 15 | } 16 | 17 | // @ts-ignore 18 | export default prisma; 19 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import NotFound from "../components/Shared/404NotFound"; 2 | 3 | const Error = () => { 4 | return ; 5 | }; 6 | 7 | export default Error; 8 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "react-lazy-load-image-component/src/effects/blur.css"; 2 | import "@/src/styles/globals.css"; 3 | import { type AppType } from "next/app"; 4 | import { type Session } from "next-auth"; 5 | import { SessionProvider } from "next-auth/react"; 6 | import NextNProgress from "nextjs-progressbar"; 7 | import { QueryClient, QueryClientProvider } from "react-query"; 8 | import dynamic from "next/dynamic"; 9 | import ScrollToTop from "../components/Shared/ScrollToTop"; 10 | import { toast } from "react-hot-toast"; 11 | 12 | const Toast = dynamic(() => import("../components/Shared/Toast"), { 13 | ssr: false, 14 | }); 15 | 16 | const queryClient = new QueryClient({ 17 | defaultOptions: { 18 | queries: { refetchOnWindowFocus: false }, 19 | mutations: { 20 | onError: () => { 21 | toast.error("Something went wrong!"); 22 | }, 23 | }, 24 | }, 25 | }); 26 | 27 | const MyApp: AppType<{ session: Session | null }> = ({ 28 | Component, 29 | pageProps: { session, ...pageProps }, 30 | }) => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default MyApp; 44 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/anime/[id].tsx: -------------------------------------------------------------------------------- 1 | import AnimeBannerDetail from "@/src/components/Anime/AnimeBannerDetail"; 2 | import AnimeInfoDetail from "@/src/components/Anime/AnimeInfoDetail"; 3 | import AnimeCard from "@/src/components/Anime/AnimeCard"; 4 | import AnimeGridLayout from "@/src/layouts/AnimeGridLayout"; 5 | import MainLayout from "@/src/layouts/MainLayout"; 6 | import { getAnimeInfo } from "@/src/services/anime"; 7 | import { AnimeInfo } from "@/src/types/anime"; 8 | import { getAnimeTitle, setBackgroundImage } from "@/src/utils/contants"; 9 | import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from "next"; 10 | import React from "react"; 11 | import Meta from "@/src/components/Shared/Meta"; 12 | import CharactersList from "@/src/components/Characters/CharactersList"; 13 | 14 | interface AnimeProps { 15 | info: AnimeInfo; 16 | } 17 | 18 | const Anime: React.FC = ({ info }) => { 19 | return ( 20 | 21 | 26 |
27 |
31 | 32 |
33 |
34 | 35 |
36 | {info?.characters?.length > 0 && ( 37 | 38 | )} 39 | {info?.relations?.length > 0 && ( 40 | 41 | {info?.relations?.map((item) => ( 42 | 50 | ))} 51 | 52 | )} 53 | {info?.recommendations?.length > 0 && ( 54 | 55 | {info?.recommendations?.map((item) => ( 56 | 64 | ))} 65 | 66 | )} 67 |
68 |
69 | ); 70 | }; 71 | 72 | export const getStaticPaths: GetStaticPaths = async () => { 73 | return { 74 | paths: [], 75 | fallback: "blocking", 76 | }; 77 | }; 78 | 79 | export const getStaticProps: GetStaticProps = async ( 80 | context: GetStaticPropsContext 81 | ) => { 82 | try { 83 | const id = context.params?.id as string; 84 | 85 | if (!id) { 86 | return { 87 | notFound: true, 88 | }; 89 | } 90 | 91 | const info = await getAnimeInfo(id); 92 | 93 | return { 94 | props: { 95 | info, 96 | }, 97 | revalidate: 60, 98 | }; 99 | } catch (error) { 100 | return { 101 | notFound: true, 102 | revalidate: 10, 103 | }; 104 | } 105 | }; 106 | 107 | export default Anime; 108 | -------------------------------------------------------------------------------- /src/pages/anime/genres/[genres].tsx: -------------------------------------------------------------------------------- 1 | import AnimeCard from "@/src/components/Anime/AnimeCard"; 2 | import Meta from "@/src/components/Shared/Meta"; 3 | import AnimeCardSkeleton from "@/src/components/Skeleton/AnimeCardSkeleton"; 4 | import AnimeGridLayout from "@/src/layouts/AnimeGridLayout"; 5 | import MainLayout from "@/src/layouts/MainLayout"; 6 | import { searchAdvanced } from "@/src/services/anime"; 7 | import { Anime } from "@/src/types/anime"; 8 | import { convertQueryArrayParams, getAnimeTitle } from "@/src/utils/contants"; 9 | import { GetServerSideProps, GetServerSidePropsContext } from "next"; 10 | import React from "react"; 11 | import { CircularProgress } from "react-cssfx-loading"; 12 | import { InView } from "react-intersection-observer"; 13 | import { useInfiniteQuery } from "react-query"; 14 | 15 | interface GenresProps { 16 | genres: string; 17 | } 18 | 19 | const Genres: React.FC = ({ genres }) => { 20 | const { 21 | data, 22 | isLoading, 23 | fetchNextPage, 24 | hasNextPage, 25 | isFetchingNextPage, 26 | isError, 27 | } = useInfiniteQuery( 28 | [`genres-${genres}`], 29 | (pageParam) => 30 | searchAdvanced({ 31 | genres: convertQueryArrayParams([genres]), 32 | page: pageParam?.pageParam || 1, 33 | }), 34 | { 35 | getNextPageParam: (lastPage) => 36 | lastPage.hasNextPage ? (lastPage.currentPage as number) + 1 : null, 37 | } 38 | ); 39 | 40 | return ( 41 | 42 | 47 |
48 |

49 | {genres} 50 |

51 | 52 | {isError && ( 53 |
54 | Something went wrong 55 |
56 | )} 57 | {data?.pages?.length === 0 || 58 | (data?.pages[0]?.results?.length === 0 && ( 59 |
No results
60 | ))} 61 | {isLoading && ( 62 | 63 | {Array.from(Array(20).keys()).map((item) => ( 64 | 65 | ))} 66 | 67 | )} 68 | 69 | 70 | {data && 71 | data?.pages 72 | ?.reduce((final, item) => { 73 | // @ts-ignore 74 | final.push(...item.results); 75 | return final; 76 | }, [] as Anime[]) 77 | .map((item) => ( 78 | 86 | ))} 87 | 88 | 89 | { 92 | if (InVidew && hasNextPage && !isFetchingNextPage) { 93 | fetchNextPage(); 94 | } 95 | }} 96 | > 97 | {({ ref }) => ( 98 |
102 | {isFetchingNextPage && } 103 |
104 | )} 105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | export const getServerSideProps: GetServerSideProps = async ( 112 | context: GetServerSidePropsContext 113 | ) => { 114 | return { 115 | props: { 116 | genres: context?.params?.genres, 117 | }, 118 | }; 119 | }; 120 | 121 | export default Genres; 122 | -------------------------------------------------------------------------------- /src/pages/anime/watch/[id].tsx: -------------------------------------------------------------------------------- 1 | import Meta from "@/src/components/Shared/Meta"; 2 | import AnimeInfoComp from "@/src/components/Watch/AnimeInfo"; 3 | import EpisodeInfo from "@/src/components/Watch/EpisodeInfo"; 4 | import EpisodeList from "@/src/components/Watch/EpisodeList"; 5 | import MoreLikeThis from "@/src/components/Watch/MoreLikeThis"; 6 | import { 7 | default_provider, 8 | getAnimeEpisodeStreaming, 9 | getAnimeInfo, 10 | } from "@/src/services/anime"; 11 | import { AnimeInfo } from "@/src/types/anime"; 12 | import { Episode } from "@/src/types/utils"; 13 | import { getAnimeTitle, getStreamAnimeWithProxy } from "@/src/utils/contants"; 14 | import { GetServerSideProps, GetServerSidePropsContext } from "next"; 15 | import dynamic from "next/dynamic"; 16 | import { useRouter } from "next/router"; 17 | import React, { useEffect, useRef, useState } from "react"; 18 | import { useQuery } from "react-query"; 19 | import Note from "@/src/components/Watch/Note"; 20 | import Comment from "@/src/components/Watch/Comment"; 21 | import MainLayout from "@/src/layouts/MainLayout"; 22 | import Hls from "hls.js"; 23 | 24 | interface WatchProps { 25 | info: AnimeInfo; 26 | } 27 | 28 | const Player = dynamic(() => import('@/src/components/Player'), {ssr: false}) 29 | 30 | const Watch: React.FC = ({ info }) => { 31 | const [episode, setEpisode] = useState(info?.episodes?.[0]); 32 | const [isWatchIframe, setIsWatchIframe] = useState(false); 33 | 34 | const playerRef = useRef(null); 35 | 36 | const router = useRouter(); 37 | 38 | const { data, isError, isFetching } = useQuery( 39 | [`watch-${JSON.stringify(episode)}`], 40 | () => { 41 | if (!episode) return null; 42 | return getAnimeEpisodeStreaming( 43 | episode?.id 44 | ); 45 | } 46 | ); 47 | 48 | const handleSelectEpisode = (episode: Episode) => { 49 | setEpisode(episode); 50 | } 51 | 52 | useEffect(() => { 53 | setEpisode(info?.episodes?.[0]); 54 | setIsWatchIframe(false); 55 | }, [router?.query?.provider]); 56 | 57 | useEffect(() => { 58 | return () => { 59 | if (document.pictureInPictureElement) { 60 | document.exitPictureInPicture(); 61 | } 62 | }; 63 | }, [episode]); 64 | 65 | return ( 66 | 67 | 72 |
73 |
74 |
75 | {!episode && ( 76 |
77 | Please select the episode 78 |
79 | )} 80 | {isError && ( 81 |
82 | Failed to data source episode 83 |
84 | )} 85 | {isFetching && ( 86 |
Loading episode data
87 | )} 88 | {!isError && !isFetching && episode && data && ( 89 |
90 | {isWatchIframe ? ( 91 |