├── .eslintignore ├── src ├── app │ ├── @modal │ │ ├── default.tsx │ │ └── (.)detail │ │ │ └── [animeId] │ │ │ ├── page.tsx │ │ │ ├── anime-detail-not-found.tsx │ │ │ └── anime-detail.tsx │ ├── favicon.ico │ ├── movies │ │ ├── page.tsx │ │ └── movie-list.tsx │ ├── popular │ │ ├── page.tsx │ │ └── popular-anime-list.tsx │ ├── recent-release │ │ ├── page.tsx │ │ └── recent-episode-list.tsx │ ├── popular-carousel.tsx │ ├── genre │ │ ├── [genreId] │ │ │ ├── page.tsx │ │ │ └── anime-genre-list.tsx │ │ └── page.tsx │ ├── search │ │ ├── page.tsx │ │ └── results.tsx │ ├── layout.tsx │ ├── homepage-movie-list.tsx │ ├── homepage-recent-episode-list.tsx │ ├── not-found.tsx │ ├── error.tsx │ ├── watch │ │ └── [episodeId] │ │ │ ├── page.tsx │ │ │ ├── video-player.tsx │ │ │ └── episode-list.tsx │ ├── globals.css │ ├── detail │ │ └── [animeId] │ │ │ └── page.tsx │ └── page.tsx ├── components │ ├── framer │ │ ├── motion-div.tsx │ │ └── motion-layout-group.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── hover-card.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ └── drawer.tsx │ ├── layout │ │ ├── theme-provider.tsx │ │ ├── providers.tsx │ │ ├── theme-toggler.tsx │ │ ├── mobile-search-field.tsx │ │ ├── navbar.tsx │ │ ├── main-drawer.tsx │ │ ├── footer.tsx │ │ └── navbar-search-field.tsx │ ├── update-metadata-component.tsx │ ├── skeleton-loader │ │ └── anime-list.tsx │ ├── hocs │ │ └── with-loader.tsx │ ├── anime │ │ ├── embedded-video-player.tsx │ │ ├── carousel.tsx │ │ ├── carousel-content.tsx │ │ ├── top-airing-section.tsx │ │ ├── default-video-player.tsx │ │ ├── card.tsx │ │ ├── tooltip.tsx │ │ └── video-player.tsx │ ├── custom-tooltip.tsx │ ├── shallow-link.tsx │ ├── swipeable.tsx │ ├── modal-wrapper.tsx │ └── infinite-scroll.tsx └── lib │ ├── utils.ts │ ├── services │ ├── local-storage-service.ts │ └── metadata-service.ts │ ├── helpers │ └── url-helpers.ts │ ├── api │ ├── fetch-api.ts │ └── anime-api.ts │ ├── types │ └── anime.ts │ └── constants.tsx ├── public ├── images │ ├── mobile1.png │ ├── mobile2.png │ ├── anya-500.png │ ├── desktop1.png │ ├── desktop2.png │ ├── konata-not-found.png │ ├── metadata-main-image.png │ └── anime-card-placeholder-blur.png ├── vercel.svg └── next.svg ├── postcss.config.mjs ├── components.json ├── next.config.mjs ├── .gitignore ├── tsconfig.json ├── LICENSE ├── .eslintrc.json ├── package.json ├── tailwind.config.ts └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | src/components/ui/ 2 | 3 | next.config.mjs -------------------------------------------------------------------------------- /src/app/@modal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/images/mobile1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/mobile1.png -------------------------------------------------------------------------------- /public/images/mobile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/mobile2.png -------------------------------------------------------------------------------- /public/images/anya-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/anya-500.png -------------------------------------------------------------------------------- /public/images/desktop1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/desktop1.png -------------------------------------------------------------------------------- /public/images/desktop2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/desktop2.png -------------------------------------------------------------------------------- /public/images/konata-not-found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/konata-not-found.png -------------------------------------------------------------------------------- /public/images/metadata-main-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/metadata-main-image.png -------------------------------------------------------------------------------- /public/images/anime-card-placeholder-blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imamseptian/raznime-next-14/HEAD/public/images/anime-card-placeholder-blur.png -------------------------------------------------------------------------------- /src/components/framer/motion-div.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | const MotionDiv = motion.div; 6 | export default MotionDiv; 7 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/components/framer/motion-layout-group.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LayoutGroup } from 'framer-motion'; 4 | 5 | const MotionLayoutGroup = LayoutGroup; 6 | export default MotionLayoutGroup; 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /src/components/layout/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import { type ThemeProviderProps } from "next-themes/dist/types"; 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return { children }; 8 | } 9 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | // unoptimized: 5 | // process.env.VERCEL_ENV === "production" && 6 | // process.env.VERCEL_PLAN === "free", 7 | unoptimized: true, 8 | remotePatterns: [ 9 | { 10 | hostname: "gogocdn.net", 11 | }, 12 | { 13 | hostname: "placehold.co", 14 | }, 15 | ], 16 | }, 17 | logging: { 18 | fetches: { 19 | fullUrl: true, 20 | }, 21 | }, 22 | }; 23 | 24 | export default nextConfig; 25 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/app/movies/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next'; 2 | import { buildMetadata } from '@/lib/services/metadata-service'; 3 | import AnimeMovieListWithLoader from './movie-list'; 4 | 5 | export const metadata: Metadata = buildMetadata({ 6 | title: 'Discover Anime Movies - Raznime - Stream Anime Online', 7 | }); 8 | 9 | export default async function AnimeMovieListPage() { 10 | return ( 11 |
12 |

Discover Anime Movies

13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/popular/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next'; 2 | import { buildMetadata } from '@/lib/services/metadata-service'; 3 | import PopularAnimeListWithLoader from './popular-anime-list'; 4 | 5 | export const metadata: Metadata = buildMetadata({ 6 | title: 'Discover Popular Anime - Raznime - Stream Anime Online', 7 | }); 8 | 9 | export default async function PopularAnimeListPage() { 10 | return ( 11 |
12 |

Popular Anime

13 | 14 | 15 |
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/recent-release/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next'; 2 | import { buildMetadata } from '@/lib/services/metadata-service'; 3 | import RecentEpisodeListWithLoader from './recent-episode-list'; 4 | 5 | export const metadata: Metadata = buildMetadata({ 6 | title: 'Recent Released Episodes - Raznime - Stream Anime Online', 7 | }); 8 | 9 | export default async function RecentReleasePage() { 10 | return ( 11 |
12 |

Recent Released Episode

13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/components/layout/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 7 | 8 | export function Providers({ children, ...props }: ThemeProviderProps) { 9 | const [queryClient] = useState( 10 | () => new QueryClient({ 11 | defaultOptions: { 12 | queries: { 13 | staleTime: 60 * 1000, 14 | }, 15 | }, 16 | }), 17 | ); 18 | 19 | return ( 20 | 21 | 22 | { children } 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/components/update-metadata-component.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | import type { Metadata } from 'next'; 5 | import { updateMetadataFromClient } from '@/lib/services/metadata-service'; 6 | 7 | /** 8 | * Updates the metadata title and description after calling this component 9 | * This component is an alternative way to run the client update metadata function after server component rendering 10 | * 11 | * @param {Metadata} props - The props object containing the metadata title and description 12 | * @param {string} props.title - The new title for the metadata 13 | * @param {string} [props.description=''] - The new description for the metadata (default: '') 14 | * @return {null} This function does not return anything 15 | */ 16 | export default function UpdateMetadataComponent({ title, description = '' }: Metadata) { 17 | useEffect(() => { 18 | updateMetadataFromClient({ 19 | title: title as string, 20 | description, 21 | }); 22 | }, [title, description]); 23 | 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/layout/theme-toggler.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Moon, Sun } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | 8 | /** 9 | * Renders a theme toggler component that allows the user to switch between light and dark themes. 10 | * 11 | * @return {JSX.Element} The rendered theme toggler component. 12 | */ 13 | export default function ThemeToggler() { 14 | const { theme, setTheme } = useTheme(); 15 | 16 | const toggleTheme = () => { 17 | if (theme === "dark") { 18 | setTheme("light"); 19 | } else { 20 | setTheme("dark"); 21 | } 22 | }; 23 | 24 | return ( 25 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Razhael 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/@modal/(.)detail/[animeId]/page.tsx: -------------------------------------------------------------------------------- 1 | import ModalWrapper from '@/components/modal-wrapper'; 2 | import React from "react"; 3 | import AnimeDetailWithLoader from './anime-detail'; 4 | 5 | interface AnimeDetailPageInterceptProps { 6 | params: { animeId: string } 7 | } 8 | 9 | /** 10 | * Render modal that contain anime detail information 11 | * 12 | * @param {AnimeDetailPageInterceptProps} props - The component props 13 | * @param {string} props.params.animeId - The ID of the anime 14 | * @return {JSX.Element} The rendered modal component 15 | */ 16 | export default async function AnimeDetailPageIntercept({ 17 | params: { animeId }, 18 | }: AnimeDetailPageInterceptProps) { 19 | return ( 20 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/skeleton-loader/anime-list.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@/components/ui/skeleton'; 2 | import MotionDiv from '@/components/framer/motion-div'; 3 | 4 | /** 5 | * Renders a skeleton loader for a list of anime cards. 6 | * 7 | * @return {JSX.Element} The skeleton loader component. 8 | */ 9 | export default function AnimeListSkeletonLoader() { 10 | return ( 11 | 12 |
13 | { 14 | Array(20).fill(0).map((_, index) => ( 15 | 29 | 30 | )) 31 | } 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/popular-carousel.tsx: -------------------------------------------------------------------------------- 1 | import { getPopularAnimeCarouselData } from '@/lib/api/anime-api'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | import { withLoader } from '@/components/hocs/with-loader'; 4 | import AnimeCarousel from '@/components/anime/carousel'; 5 | 6 | /** 7 | * Asynchronously fetches the popular anime list from the server and renders an AnimeCarousel component with the data. 8 | * 9 | * @return {JSX.Element | null} The AnimeCarousel component with the popular anime list data, or null if there was an error fetching the data. 10 | */ 11 | async function PopularCarouselSection() { 12 | const popularAnimeListResponse = await getPopularAnimeCarouselData(); 13 | 14 | const { 15 | data: popularAnimeList, 16 | isError, 17 | } = popularAnimeListResponse ?? {}; 18 | 19 | if (isError) { 20 | return null; 21 | } 22 | 23 | return ( 24 | 25 | ); 26 | } 27 | 28 | function SkeletonLoader() { 29 | return ( 30 | 31 | ); 32 | } 33 | 34 | const PopularCarouselWithLoader = withLoader(PopularCarouselSection, SkeletonLoader); 35 | 36 | export default PopularCarouselWithLoader; 37 | -------------------------------------------------------------------------------- /src/components/hocs/with-loader.tsx: -------------------------------------------------------------------------------- 1 | import { RefreshCcw } from 'lucide-react'; 2 | import { Suspense } from 'react'; 3 | 4 | /** 5 | * Higher-order component that adds a loader to a given component. 6 | * 7 | * @param {React.ComponentType

} WrappedComponent - The component to be wrapped with a loader. 8 | * @param {React.ComponentType

} [Loader] - Optional custom loader component. Defaults to a spinning refresh icon. 9 | * @returns {React.FC

} The wrapped component with a loader. 10 | */ 11 | export const withLoader =

( 12 | WrappedComponent: React.ComponentType

, 13 | Loader?: React.ComponentType

, 14 | ) => { 15 | function DefaultLoader() { 16 | return ( 17 |

18 | 19 |
20 | ); 21 | } 22 | 23 | const MyLoader = Loader || DefaultLoader; 24 | 25 | const WithLoaderComponent: React.FC

= function WithLoaderComponent(props) { 26 | return ( 27 | }> 28 | 29 | 30 | ); 31 | }; 32 | 33 | return WithLoaderComponent; 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/genre/[genreId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ANIME_GENRES } from '@/lib/constants'; 2 | import { notFound } from 'next/navigation'; 3 | import React from 'react'; 4 | import { Metadata } from 'next'; 5 | import { buildMetadata } from '@/lib/services/metadata-service'; 6 | import AnimeGenreListWithloader from './anime-genre-list'; 7 | 8 | export const generateMetadata = ({ params: { genreId } }: GenreAnimeListPageProps): Metadata => { 9 | const genreLabel = ANIME_GENRES.find((genre) => genre.id === genreId)?.title || ''; 10 | 11 | return buildMetadata({ 12 | title: `${genreLabel} Anime - Raznime - Stream Anime Online`, 13 | }); 14 | }; 15 | 16 | interface GenreAnimeListPageProps { 17 | params: { 18 | genreId: string 19 | } 20 | } 21 | 22 | export default function GenreAnimeListPage({ params: { genreId } }: GenreAnimeListPageProps) { 23 | const currentGenre = ANIME_GENRES.find((genre) => genre.id === genreId); 24 | 25 | if (!currentGenre) { 26 | notFound(); 27 | } 28 | 29 | return ( 30 |

31 |

{ `${currentGenre?.title} Anime` }

32 | 33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "airbnb", "airbnb-typescript"], 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["align-assignments"], 7 | "rules": { 8 | // to align json keys 9 | "key-spacing": [ 10 | "warn", 11 | { 12 | "align": { 13 | "beforeColon": true, 14 | "afterColon": true, 15 | "on": "colon" 16 | } 17 | } 18 | ], 19 | "@typescript-eslint/quotes": "off", 20 | "react/react-in-jsx-scope": "off", 21 | "react/jsx-props-no-spreading": "off", 22 | "import/prefer-default-export": "off", 23 | "no-multi-spaces": "off", 24 | // for variable alignment 25 | "align-assignments/align-assignments": "warn", 26 | "react/require-default-props": "off", 27 | "max-len": "off", 28 | "@typescript-eslint/no-use-before-define": "off", 29 | "indent": [ 30 | "error", 31 | 2, 32 | { 33 | "SwitchCase": 1 34 | } 35 | ], 36 | "react/jsx-curly-spacing": [ 37 | "warn", 38 | { 39 | "when": "always", 40 | "children": true, 41 | "spacing": { 42 | "objectLiterals": "never" 43 | } 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const HoverCard = HoverCardPrimitive.Root 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger 11 | 12 | const HoverCardContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 26 | )) 27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 28 | 29 | export { HoverCard, HoverCardTrigger, HoverCardContent } 30 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/layout/mobile-search-field.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { Search } from 'lucide-react'; 5 | import { Button } from '@/components/ui/button'; 6 | import NavbarSearchField from '@/components/layout/navbar-search-field'; 7 | 8 | /** 9 | * Renders a mobile search field component. 10 | * The field can be toggled to show/hide using the button with search icon 11 | * 12 | * @return {JSX.Element} The mobile search field component. 13 | */ 14 | export default function MobileSearchField() { 15 | const [showSearchField, setShowSearchField] = useState(false); 16 | 17 | return ( 18 | <> 19 | 29 | 30 |
31 |
32 | setShowSearchField(false) } 34 | onSelect={ () => setShowSearchField(false) } 35 | /> 36 |
37 | 38 |
39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | card: "border-transparent bg-card text-card-foreground hover:bg-card/80", 18 | outline: "text-foreground" 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: "default", 23 | }, 24 | } 25 | ) 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
34 | ) 35 | } 36 | 37 | export { Badge, badgeVariants } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raznime-next-14", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3005", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-hover-card": "^1.0.7", 13 | "@radix-ui/react-slot": "^1.0.2", 14 | "@tanstack/react-query": "^5.37.1", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "framer-motion": "^11.2.5", 18 | "hls.js": "^1.5.8", 19 | "lucide-react": "^0.379.0", 20 | "next": "14.2.3", 21 | "next-themes": "^0.3.0", 22 | "plyr": "^3.7.8", 23 | "react": "^18", 24 | "react-dom": "^18", 25 | "sonner": "^1.4.41", 26 | "tailwind-merge": "^2.3.0", 27 | "tailwindcss-animate": "^1.0.7", 28 | "vaul": "^0.9.1" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "@typescript-eslint/eslint-plugin": "^7.10.0", 35 | "@typescript-eslint/parser": "^7.10.0", 36 | "eslint": "^8", 37 | "eslint-config-airbnb": "^19.0.4", 38 | "eslint-config-airbnb-typescript": "^18.0.0", 39 | "eslint-config-next": "14.2.3", 40 | "eslint-plugin-align-assignments": "^1.1.2", 41 | "postcss": "^8", 42 | "tailwindcss": "^3.4.1", 43 | "typescript": "^5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/anime/embedded-video-player.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { type AnimeEpisodeOtherStreamingServer } from '@/lib/types/anime'; 4 | import React, { useEffect, useState } from 'react'; 5 | import 'plyr/dist/plyr.css'; 6 | import { Skeleton } from '@/components/ui/skeleton'; 7 | 8 | /** 9 | * A React component that renders an embedded video player from another website for anime episodes. 10 | * 11 | * @component 12 | * @param {AnimeEmbeddedVideoPlayerProps} props - The props for the AnimeEmbeddedVideoPlayer component. 13 | * @returns {React.ReactElement} The rendered AnimeEmbeddedVideoPlayer component. 14 | */ 15 | export default function AnimeEmbeddedVideoPlayer({ server }: { server: AnimeEpisodeOtherStreamingServer }) { 16 | const [isLoading, setIsLoading] = useState(false); 17 | 18 | useEffect(() => { 19 | setIsLoading(true); 20 | }, [server]); 21 | 22 | return ( 23 |
24 | { 25 | isLoading && ( 26 | 27 | ) 28 | } 29 | 30 | { 31 | server && ( 32 |