This commit is contained in:
Tejas Panchal
2025-07-24 21:31:38 +05:30
parent d502d2dbc5
commit 9f256d4254
116 changed files with 0 additions and 17816 deletions

View File

@@ -1,15 +0,0 @@
#Refer https://github.com/itzzzme/anime-api to host your backend API
VITE_API_URL=<your_hosted_api>/api
#Refer this gist to setup proxy server https://gist.github.com/itzzzme/180813be2c7b45eedc8ce8344c8dea3b
VITE_PROXY_URL=<proxy_server_name>/?url=
#Refer https://github.com/itzzzme/m3u8proxy to host you m3u8 proxy server though it's optional but if you don't set it up you may get CORS error for some servers if you set up from the given repo then only the url structure will look like this
VITE_M3U8_PROXY_URL=<m3u8_proxy_server_name>/m3u8-proxy?url=
#totaly optional / if you don't want to setup worker just change the code of getQtip.utils.js following the pattern of any other utils file
VITE_WORKER_URL=https://worker1.workers.dev,https://worker2.workers.dev,https://worker3.workers.dev,...
VITE_BASE_IFRAME_URL=https://megaplay.buzz/stream/s-2
VITE_BASE_IFRAME_URL_2=https://vidwish.live/stream/s-2

133
.gitignore vendored
View File

@@ -1,133 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# lock json files
package-lock.json
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Sayan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

119
README.md
View File

@@ -1,119 +0,0 @@
<p align="center">
<div align="center">
<a href="https://zenime.site/">
<img alt="AnimeHi" src="https://raw.githubusercontent.com/itzzzme/zenime/refs/heads/main/public/logo.png" width="220"/>
</a>
</div>
<h3 align="center">Zenime - Ad free anime streaming platform</h3>
<p align="center">
<a href="https://github.com/itzzzme/zenime">
<img src="https://img.shields.io/github/stars/itzzzme/zenime" alt="Github Stars">
</a>
<img src="https://img.shields.io/github/issues/itzzzme/zenime" alt="Github Issues">
<a href="https://github.com/itzzzme/zenime">
<img src="https://img.shields.io/github/forks/itzzzme/zenime" alt="Github Forks" />
</a>
</p>
</p>
<p align="center">
<a href="https://zenime.site">Zenime</a> is an open-source anime streaming service that uses <a href="https://github.com/itzzzme/anime-api">custom</a> API, built using ReactJS with javascript and Tailwind CSS. It lets you easily find any anime with intuitive search & suggestion feature and stream without any ads.
</p>
<details>
<summary>View more Features</summary>
### General
- Sub Anime support
- Dub Anime support
- User-friendly interface
- Mobile responsive
- Fast page load
- Character & Voice Actors
### Watch Page
- Related Animes
- Recommended Animes
- Available seasons
- Estimated schedule of upcoming episodes
- **Player**
- Autoplay
- Autoskip intro/outro
- Autonext
</details>
## Previews
<div style="text-align: left;">
<img src="https://raw.githubusercontent.com/itzzzme/zenime/refs/heads/main/public/homepage.webp" alt="Home Page" style="max-width: 80%;" >
<details>
<summary style="margin-top:10px">View more screenshots</summary>
<br/>
AnimeInfo Page
<img style="margin-top:10px" src="https://raw.githubusercontent.com/itzzzme/zenime/refs/heads/main/public/animeinfo.webp" alt="AnimeInfo Page" style="max-width: 80%;">
<br/>
Searchbar
<img style="margin-top:10px" src="https://raw.githubusercontent.com/itzzzme/zenime/refs/heads/main/public/searchbar.webp" alt="Searchbar" style="max-width: 50%;">
<br/>
Character & Voice Actors
<img style="margin-top:10px" src="https://raw.githubusercontent.com/itzzzme/zenime/refs/heads/main/public/voiceactors.webp" alt="Character & Voice Actors" style="max-width: 80%;">
<br/>
Watch Page
<img style="margin-top:10px" src="https://raw.githubusercontent.com/itzzzme/zenime/refs/heads/main/public/watchpage.webp" alt="Watch Page" style="max-width: 80%;">
<br/>
</details>
</div>
## Installation and Local Development
### 1. Make sure you have node installed on your device
### 2. Run the following code to clone the repository and install all required dependencies
```bash
git clone https://github.com/itzzzme/zenime.git
cd zenime
npm install # or yarn
```
### 3. Refer the <a href="https://github.com/itzzzme/zenime/blob/main/.env.example">.env.example</a> to set your .env file up
## Start the server
```bash
npm start # or npm run dev (to run develepment server)
```
## Live Deployment
### Vercel
Host your own instance of <a href="https://zenime.site">Zenime</a> on vercel
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/itzzzme/zenime)
### Render
Host your own instance of <a href="https://zenime.site">Zenime</a> on Render.
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/itzzzme/zenime)
### Pull Requests
- Pull requests are welcomed that address bug fixes, improvements, or new features.
- Fork the repository and create a new branch for your changes.
- Ensure your code follows our coding standards.
- Include tests if applicable.
- Describe your changes clearly in the pull request, explaining the problem and solution.
### Reporting Issues
If you discover any issues or have suggestions for improvement, please open an issue. Provide a clear and concise description of the problem, steps to reproduce it, and any relevant information about your environment.
### Support
If you like the project feel free to drop a star ✨. Your appreciation means a lot.
<p align="center" style="text-decoration: none;">Made by <a href="https://github.com/itzzzme" tarGET="_blank">itzzzme
</a>🫰</p>

View File

@@ -1,20 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

View File

@@ -1,38 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

View File

@@ -1,93 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="shortcut icon" href="favicon.png" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="JustAnime is a Free anime streaming website which you can watch English Subbed and Dubbed Anime online. WATCH NOW!. Hianime, 9animetv, aniwatchtv"
/>
<meta
name="keywords"
content="justanime, just anime, zenime, hianime to, aniwatch, zorox, zoro anime, zoro to, zoroxtv, watch anime online free, free watch anime, anime online to watch"
/>
<meta name="author" content="Zenime Team" />
<meta name="robots" content="index, follow" />
<meta name="rating" content="General" />
<meta name="language" content="English" />
<meta
property="og:title"
content="JustAnime | Watch Free Anime, Online Anime Streaming - JustAnime"
/>
<meta
property="og:description"
content="JustAnime to is a free no ads anime site to watch free anime. Online anime streaming at justanime with DUB, SUB in HD. Hianime, 9animetv, Zoro, s3taku,justanime."
/>
<meta property="og:image" content="https://i.postimg.cc/pVqqMKkR/2IAVHlI.webp" />
<meta property="og:url" content="https://zenime.site" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="JustAnime" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="JustAnime | Free Anime Streaming Platform"
/>
<meta
name="twitter:description"
content="Stream free English-subbed and dubbed anime online with no ads. Enjoy hassle-free viewing and daily updates on JustAnime!"
/>
<meta name="twitter:image" content="https://i.postimg.cc/pVqqMKkR/2IAVHlI.webp" />
<meta name="twitter:site" content="@ZenimeOfficial" />
<title>JustAnime | Free Anime Streaming Platform</title>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "Zenime - Watch Anime Free",
"description": "Zenime offers free streaming of English-subbed and dubbed anime series and movies. No account needed and no ads!",
"thumbnailUrl": "https://i.postimg.cc/pVqqMKkR/2IAVHlI.webp",
"uploadDate": "2024-11-08",
"contentUrl": "https://zenime.site",
"duration": "PT30M",
"interactionStatistic": {
"@type": "InteractionCounter",
"interactionType": { "@type": "WatchAction" },
"userInteractionCount": 50000
},
"author": {
"@type": "Individual",
"name": "itzzzme",
"url": "https://zenime.site"
},
"publisher": {
"@type": "Individual",
"name": "itzzzme",
"logo": {
"@type": "ImageObject",
"url": "https://i.postimg.cc/SsKY6Y9f/2H76i57.png"
}
},
"potentialAction": {
"@type": "WatchAction",
"target": "https://zenime.site"
}
}
</script>
<link rel="canonical" href="https://zenime.site" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}

View File

@@ -1,6 +0,0 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

6506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +0,0 @@
{
"name": "justanime",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"host": "vite --host"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@radix-ui/react-icons": "^1.3.0",
"artplayer": "^5.2.3",
"artplayer-plugin-chapter": "^1.0.0",
"artplayer-plugin-hls-control": "^1.0.1",
"axios": "^1.7.7",
"cheerio": "^1.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"hls.js": "^1.5.17",
"lucide-react": "^0.447.0",
"react": "^18.3.1",
"react-content-loader": "^7.0.2",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-lazy-load": "^4.0.1",
"react-router-dom": "^6.26.2",
"styled-components": "^6.1.13",
"swiper": "^11.2.5",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"vite": "^5.4.1"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

View File

@@ -1,4 +0,0 @@
User-Agent: *
Allow: /
Sitemap: https://zenime.site/sitemap.xml

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -1,27 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#root {
margin: 0;
padding: 0;
}
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
max-width: 2048px;
margin-inline: auto;
}
.content {
width: 100%;
flex-grow: 1;
}
footer {
margin-top: auto;
}

View File

@@ -1,73 +0,0 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import { HomeInfoProvider } from "./context/HomeInfoContext";
import Home from "./pages/Home/Home";
import AnimeInfo from "./pages/animeInfo/AnimeInfo";
import Navbar from "./components/navbar/Navbar";
import Footer from "./components/footer/Footer";
import Error from "./components/error/Error";
import Category from "./pages/category/Category";
import AtoZ from "./pages/a2z/AtoZ";
import { azRoute, categoryRoutes } from "./utils/category.utils";
import "./App.css";
import Search from "./pages/search/Search";
import Watch from "./pages/watch/Watch";
import Producer from "./components/producer/Producer";
import SplashScreen from "./components/splashscreen/SplashScreen";
function App() {
const location = useLocation();
// Scroll to top on location change
useEffect(() => {
window.scrollTo(0, 0);
}, [location]);
// Check if the current route is for the splash screen
const isSplashScreen = location.pathname === "/";
return (
<HomeInfoProvider>
<div className="app-container">
<main className="content">
{!isSplashScreen && <Navbar />}
<Routes>
<Route path="/" element={<SplashScreen />} />
<Route path="/home" element={<Home />} />
<Route path="/:id" element={<AnimeInfo />} />
<Route path="/watch/:id" element={<Watch />} />
<Route path="/random" element={<AnimeInfo random={true} />} />
<Route path="/404-not-found-page" element={<Error error="404" />} />
<Route path="/error-page" element={<Error />} />
{/* Render category routes */}
{categoryRoutes.map((path) => (
<Route
key={path}
path={`/${path}`}
element={
<Category path={path} label={path.split("-").join(" ")} />
}
/>
))}
{/* Render A to Z routes */}
{azRoute.map((path) => (
<Route
key={path}
path={`/${path}`}
element={<AtoZ path={path} />}
/>
))}
<Route path="/producer/:id" element={<Producer />} />
<Route path="/search" element={<Search />} />
{/* Catch-all route for 404 */}
<Route path="*" element={<Error error="404" />} />
</Routes>
{!isSplashScreen && <Footer />}
</main>
</div>
</HomeInfoProvider>
);
}
export default App;

View File

@@ -1,58 +0,0 @@
import { Skeleton } from "@/src/components/ui/Skeleton/Skeleton";
import CategoryCardLoader from "./CategoryCard.loader";
import SidecardLoader from "./Sidecard.loader";
const SkeletonItems = ({ count, className }) => (
[...Array(count)].map((_, index) => <Skeleton key={index} className={className} />)
);
function AnimeInfoLoader() {
return (
<>
<div className="relative grid grid-cols-[minmax(0,75%),minmax(0,25%)] h-fit w-full overflow-hidden mt-[64px] max-[1200px]:flex max-[1200px]:flex-col max-md:mt-[50px]">
<Skeleton className="absolute inset-0 w-full h-full blur-lg z-[-900] bg-gray-500" animation={false} />
<div className="flex items-start z-10 px-14 py-[70px] bg-[#252434] bg-opacity-70 gap-x-8 max-[1024px]:px-6 max-[1024px]:py-10 max-[1024px]:gap-x-4 max-[575px]:flex-col max-[575px]:items-center max-[575px]:justify-center">
<Skeleton className="w-[200px] h-[270px] rounded-none" />
<div className="flex flex-col ml-4 gap-y-5 w-full max-[575px]:items-center max-[575px]:justify-center max-[575px]:mt-0">
<ul className="flex gap-x-2 items-center w-fit max-[1200px]:hidden">
<SkeletonItems count={3} className="w-[40px] h-[15px] rounded-xl " />
</ul>
<div className="flex flex-col gap-y-2">
<SkeletonItems count={2} className="w-[50%] h-[20px] rounded-xl " />
</div>
<div className="flex gap-x-[3px]">
<SkeletonItems count={6} className="w-[30px] h-[20px] rounded-sm" />
</div>
<Skeleton className="w-[150px] h-[40px] rounded-3xl mt-4" />
<div className="flex flex-col gap-y-2 mt-5 max-[575px]:hidden">
<SkeletonItems count={3} className="w-[80%] h-[15px] rounded-3xl " />
</div>
<div className="flex gap-x-4 items-center mt-4">
<Skeleton className="w-[60px] h-[60px] rounded-full bg-gray-500 max-[575px]:hidden" />
<div className="flex flex-col w-fit gap-y-2">
<SkeletonItems count={2} className="w-[150px] h-[10px] rounded-xl " />
</div>
</div>
</div>
</div>
<div className="bg-[#4c4b57c3] flex items-center px-8 max-[1200px]:py-10 max-[1200px]:bg-[#363544e0] max-[575px]:p-4">
<div className="w-full flex flex-col h-fit gap-y-4">
<SkeletonItems count={6} className="w-full h-[15px] rounded-xl" />
<div className="flex gap-x-4 py-2 mt-4">
<Skeleton className="w-[50px] h-[20px] " />
<div className="flex flex-wrap gap-1">
<SkeletonItems count={4} className="w-[30px] h-[20px] rounded-sm bg-gray-500" />
</div>
</div>
<SkeletonItems count={2} className="w-[90%] h-[15px] rounded-xl " />
</div>
</div>
</div>
<div className="w-full grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex flex-col px-4">
<CategoryCardLoader className="mt-[60px]"/>
<SidecardLoader className="mt-[60px]" />
</div>
</>
);
}
export default AnimeInfoLoader;

View File

@@ -1,26 +0,0 @@
import { Skeleton } from "../ui/Skeleton/Skeleton";
import CategoryCardLoader from "./CategoryCard.loader";
const SkeletonItems = ({ count, className }) => (
[...Array(count)].map((_, index) => <Skeleton key={index} className={className} />)
);
function AtoZLoader() {
return (
<div className="max-w-[1260px] mx-auto px-[15px] flex flex-col mt-[64px] max-md:mt-[50px]">
<ul className="flex gap-x-4 mt-[50px] items-center w-fit max-[1200px]:hidden">
<Skeleton className="w-[50px] h-[15px]" />
<Skeleton className="w-[70px] h-[15px]" />
</ul>
<div className="flex flex-col gap-y-5 mt-6">
<Skeleton className="w-[200px] h-[15px]" />
<div className='flex gap-x-[7px] flex-wrap justify-start gap-y-2 max-md:justify-start'>
<SkeletonItems count={20} className="w-[40px] h-[20px] rounded-sm"/>
</div>
</div>
<CategoryCardLoader showLabelSkeleton={false}/>
</div >
);
}
export default AtoZLoader;

View File

@@ -1,27 +0,0 @@
import { Skeleton } from "../ui/Skeleton/Skeleton"
const SkeletonItems = ({ count, className }) => (
[...Array(count)].map((_, index) => <Skeleton key={index} className={className} />)
);
function CartLoader() {
return (
<div className="flex flex-col w-1/4 space-y-7 max-[1200px]:w-full">
<Skeleton className="w-[200px] h-[20px]" />
<div className='w-full space-y-4 flex flex-col '>
{[...Array(5)].map((item, index) => (
<div key={index} style={{ borderBottom: "1px solid rgba(255, 255, 255, .075)" }} className="flex pb-4 items-center">
<Skeleton className="w-[60px] h-[75px] rounded-none" />
<div className='flex flex-col ml-4 space-y-2 w-full'>
<Skeleton className='w-[90%] h-[15px]' />
<div className='flex items-center w-fit space-x-1'>
<SkeletonItems count={3} className="w-[30px] h-[15px]" />
</div>
</div>
</div>
))}
</div>
<Skeleton className='w-[100px] h-[30px]' />
</div>
)
}
export default CartLoader

View File

@@ -1,23 +0,0 @@
import { Skeleton } from "../ui/Skeleton/Skeleton"
import CategoryCardLoader from "./CategoryCard.loader"
import SidecardLoader from "./Sidecard.loader"
function CategoryLoader() {
return (
<div className='w-full flex flex-col gap-y-4 mt-[64px] max-md:mt-[50px]'>
<div className="flex gap-x-4 items-center p-5 mt-4">
<Skeleton className="w-[60px] h-[60px] rounded-full bg-gray-500 max-[575px]:hidden" />
<div className="flex flex-col w-fit gap-y-2">
<Skeleton className="w-[150px] h-[10px] rounded-xl " />
<Skeleton className="w-[150px] h-[10px] rounded-xl " />
</div>
</div>
<div className="w-full px-4 grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex max-[1200px]:flex-col max-[1200px]:gap-y-10">
<CategoryCardLoader className={"mt-[0px]"} />
<SidecardLoader />
</div>
</div>
)
}
export default CategoryLoader

View File

@@ -1,35 +0,0 @@
import { Skeleton } from "../ui/Skeleton/Skeleton";
function CategoryCardLoader({ className, showLabelSkeleton = true }) {
return (
<div className={`w-full ${className}`}>
{showLabelSkeleton && (
<Skeleton className="w-[200px] h-[20px] max-[320px]:w-[70px]" />
)}
<div className="grid grid-cols-6 gap-x-3 gap-y-8 mt-6 max-[1400px]:grid-cols-4 max-[758px]:grid-cols-3 max-[478px]:grid-cols-2">
{[...Array(12)].map((_, index) => (
<div
key={index}
className="flex flex-col"
style={{ height: "fit-content" }}
>
<div className="w-full relative">
<Skeleton className="w-full h-[250px] object-cover max-[1200px]:h-[35vw] max-[758px]:h-[45vw] max-[478px]:h-[60vw] rounded-none" />
<div className="absolute left-2 bottom-4 flex items-center justify-center w-fit space-x-1 z-20 max-[320px]:w-[80%] max-[320px]:left-0">
<Skeleton className="w-[50px] h-[15px] bg-gray-600 max-[320px]:w-[40%]" />
<Skeleton className="w-[50px] h-[15px] bg-gray-600 max-[320px]:w-[40%]" />
</div>
</div>
<Skeleton className="mt-1 w-[90%] h-[15px]" />
<div className="flex items-center gap-x-2 w-full mt-2">
<Skeleton className="w-[40%] h-[12px]" />
<Skeleton className="w-[40%] h-[12px]" />
</div>
</div>
))}
</div>
</div>
);
}
export default CategoryCardLoader;

View File

@@ -1,32 +0,0 @@
import CartLoader from "./Cart.loader";
import CategoryCardLoader from "./CategoryCard.loader";
import SidecardLoader from "./Sidecard.loader";
import SpotlightLoader from "./Spotlight.loader";
import Trendingloader from "./Trending.loader";
function HomeLoader() {
return (
<div className="px-4 w-full h-full max-[1200px]:px-0 bg-[#3a395100]">
<SpotlightLoader />
<Trendingloader />
<div className="mt-16 flex gap-6 max-[1200px]:px-4 max-[1200px]:grid max-[1200px]:grid-cols-2 max-[1200px]:mt-12 max-[1200px]:gap-y-10 max-[680px]:grid-cols-1">
<CartLoader />
<CartLoader />
<CartLoader />
<CartLoader />
</div>
<div className="w-full grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex flex-col max-[1200px]:px-4">
<div>
<CategoryCardLoader className="mt-[60px]" />
<CategoryCardLoader className="mt-[60px]" />
<CategoryCardLoader className="mt-[60px]" />
</div>
<div className="w-full mt-[60px]">
<SidecardLoader />
<SidecardLoader />
</div>
</div>
</div>
);
}
export default HomeLoader;

View File

@@ -1,24 +0,0 @@
import AnimeInfoLoader from "./AnimeInfo.loader";
import HomeLoader from "./Home.loader";
import CategoryLoader from "./Category.loader";
import AtoZLoader from "./AtoZ.loader";
import ProducerLoader from "./Producer.loader";
const Loader = ({ type }) => {
switch (type) {
case "home":
return <HomeLoader />;
case "animeInfo":
return <AnimeInfoLoader />;
case "category":
return <CategoryLoader />;
case "producer":
return <ProducerLoader />;
case "AtoZ":
return <AtoZLoader />;
default:
return <div className="loading-skeleton default-skeleton"></div>;
}
};
export default Loader;

View File

@@ -1,15 +0,0 @@
import CategoryCardLoader from "./CategoryCard.loader";
import SidecardLoader from "./Sidecard.loader";
function ProducerLoader() {
return (
<div className="w-full mt-[100px] flex flex-col gap-y-4 max-md:mt-[50px]">
<div className="w-full px-4 grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex max-[1200px]:flex-col max-[1200px]:gap-y-10">
<CategoryCardLoader className={"mt-[0px]"} />
<SidecardLoader />
</div>
</div>
);
}
export default ProducerLoader;

View File

@@ -1,26 +0,0 @@
import { Skeleton } from "../ui/Skeleton/Skeleton";
function SidecardLoader({ className }) {
return (
<div className={`flex flex-col space-y-6 ${className}`}>
<Skeleton className='w-[200px] h-[15px]' />
<div className='flex flex-col space-y-4 bg-[#2B2A3C] p-4 pt-8 w-full'>
{[...Array(10)].map((_, index) => (
<div key={index} className='flex items-center gap-x-4'>
<div className="flex pb-4 relative container items-center">
<Skeleton className="w-[60px] h-[75px] rounded-md" />
<div className='flex flex-col ml-4 space-y-2 w-[60%]'>
<Skeleton className='w-[90%] h-[15px]' />
<div className='flex flex-wrap items-center space-x-1'>
<Skeleton className="w-[30%] h-[15px]" />
<Skeleton className="w-[30%] h-[15px]" />
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
export default SidecardLoader;

View File

@@ -1,34 +0,0 @@
import { Skeleton } from "../ui/Skeleton/Skeleton"
const SkeletonItems = ({ count, className }) => (
[...Array(count)].map((_, index) => <Skeleton key={index} className={className} />)
);
function SpotlightLoader() {
return (
<section className="w-full h-[600px] max-[1390px]:h-[530px] max-[1300px]:h-[500px] max-md:h-[420px] relative">
<div className="absolute flex flex-col left-0 bottom-[100px] w-[55%] p-4 z-10 max-[1390px]:w-[45%] max-[1390px]:bottom-[10px] max-[1300px]:w-[600px] max-[1120px]:w-[60%] max-md:w-[90%] max-[300px]:w-full">
<Skeleton className="w-[400px] h-[20px] max-md:w-[180px]" />
<Skeleton className="w-[70%] h-[20px] mt-6 text-left max-[1300px]:mt-4 max-sm:w-[80%] max-[320px]:w-full " />
<div className="flex h-fit justify-center items-center w-fit space-x-5 mt-8 max-[1300px]:mt-6 max-md:hidden">
<SkeletonItems count={2} className="w-[30px] h-[15px]" />
<div className="flex space-x-3 w-fit">
<Skeleton className="w-[80px] h-[15px]" />
<div className='flex space-x-[1px] rounded-r-[5px] rounded-l-[5px] w-fit py-[1px] overflow-hidden'>
<SkeletonItems count={2} className="w-[30px] h-[15px]" />
</div>
</div>
</div>
<div className="mt-6 max-[1300px]:w-[500px] flex flex-col gap-y-2 max-[1120px]:w-[90%] max-md:hidden">
<Skeleton className="w-full h-[13px]" />
<Skeleton className="w-[85%] h-[13px]" />
<Skeleton className="w-[70%] h-[13px]" />
</div>
<div className='flex gap-x-5 mt-10 max-md:mt-6 max-sm:w-full max-[320px]:flex-col max-[320px]:space-y-3'>
<Skeleton className="w-[170px] h-[40px] max-[575px]:w-[120px] max-[575px]:h-[30px]" />
<Skeleton className="w-[150px] h-[40px] max-[575px]:w-[120px] max-[575px]:h-[30px]" />
</div>
</div>
</section >
)
}
export default SpotlightLoader

View File

@@ -1,34 +0,0 @@
import { useState, useEffect } from "react";
import { Skeleton } from "../ui/Skeleton/Skeleton";
function TrendingLoader() {
const [count, setCount] = useState(() => window.innerWidth < 720 ? 3 : window.innerWidth < 1300 ? 4 : 6);
useEffect(() => {
const updateCount = () => {
if (window.innerWidth < 720) {
setCount(3);
} else if (window.innerWidth < 1300) {
setCount(4);
} else {
setCount(6);
}
};
updateCount();
window.addEventListener("resize", updateCount);
return () => window.removeEventListener("resize", updateCount);
}, []);
return (
<div className="flex flex-col w-full mt-10 max-[1200px]:px-4">
<Skeleton className="w-[150px] h-[20px] max-[400px]:w-[100px]" />
<div className="w-full h-[250px] overflow-hidden flex mt-6 justify-around max-[1300px]:h-fit">
{[...Array(count)].map((_, index) => (
<div key={index}>
<Skeleton className="w-[200px] h-full rounded-none max-[1300px]:w-[22vw] max-[1300px]:h-[30vw] max-[720px]:w-[25vw] max-[720px]:h-[35vw]" />
</div>
))}
</div>
</div>
);
}
export default TrendingLoader;

View File

@@ -1,21 +0,0 @@
import { Skeleton } from "../ui/Skeleton/Skeleton"
function VoiceActorlistLoader() {
return (
<div className="w-full h-fit grid grid-cols-2 gap-4 overflow-y-hidden max-sm:gap-2 max-md:h-[400px] max-md:flex max-md:flex-col">
{[...Array(10)].map((_, index) => (
<div key={index} className="h-[80px] p-4 rounded-md bg-[#444445]">
<div className="flex h-full items-center gap-x-2">
<Skeleton className="w-[45px] h-[45px] rounded-full max-sm:w-[30px] max-sm:h-[30px]" />
<div className="flex flex-col gap-y-1">
<Skeleton className="h-[10px] w-[100px] rounded-md max-[300px]:w-[50px] max-[300px]:h-[8px]" />
<Skeleton className="h-[10px] w-[70px] rounded-md max-[300px]:w-[20px] max-[300px]:h-[8px]" />
</div>
</div>
</div>
))}
</div>
)
}
export default VoiceActorlistLoader

View File

@@ -1,133 +0,0 @@
.spotlight {
overflow: hidden;
}
.spotlight-overlay {
width: 100.1%;
height: 100.1%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom:0;
background: radial-gradient(
circle at 130% center,
rgba(32, 31, 49, 0) 50%,
rgba(32, 31, 49, 0.5) 60%,
rgba(32, 31, 49, 1) 80%,
rgba(32, 31, 49, 1) 100%
),
linear-gradient(
to top,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to left,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to bottom,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
);
z-index: 1;
}
@media only screen and (max-width: 1300px) {
.spotlight-overlay {
background: radial-gradient(
circle at 130% center,
rgba(32, 31, 49, 0) 50%,
rgba(32, 31, 49, 0.5) 60%,
rgba(32, 31, 49, 1) 80%,
rgba(32, 31, 49, 1) 100%
),
linear-gradient(
to top,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to left,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to bottom,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 50%,
rgba(32, 31, 49, 0) 100%
);
}
}
@media only screen and (max-width: 1200px) {
.spotlight-overlay {
background: radial-gradient(
circle at 100% center,
rgba(32, 31, 49, 0) 50%,
rgba(32, 31, 49, 0.5) 60%,
rgba(32, 31, 49, 1) 95%,
rgba(32, 31, 49, 1) 100%
),
linear-gradient(
to top,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to left,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to bottom,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 70%,
rgba(32, 31, 49, 0) 100%
);
}
}
@media only screen and (max-width: 900px) {
.spotlight-overlay {
background: radial-gradient(
circle at 60% center,
rgba(32, 31, 49, 0) 50%,
rgba(32, 31, 49, 0.5) 85%,
rgba(32, 31, 49, 1) 95%,
rgba(32, 31, 49, 1) 100%
),
linear-gradient(
to top,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 70%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to left,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to bottom,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 70%,
rgba(32, 31, 49, 0) 100%
),
linear-gradient(
to right,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 15%,
rgba(32, 31, 49, 0) 100%
);
}
}

View File

@@ -1,136 +0,0 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPlay,
faClosedCaptioning,
faMicrophone,
faCalendar,
faClock,
} from "@fortawesome/free-solid-svg-icons";
import { FaChevronRight } from "react-icons/fa";
import { Link } from "react-router-dom";
import { useLanguage } from "@/src/context/LanguageContext";
import "./Banner.css";
function Banner({ item, index }) {
const { language } = useLanguage();
return (
<section className="spotlight w-full h-full">
<img
src={`https://wsrv.nl/?url=${item.poster}`}
alt={item.title}
className="absolute right-0 object-cover h-full w-[80%] bg-auto max-[1200px]:w-full max-[1200px]:bottom-0"
/>
<div className="spotlight-overlay"></div>
<div className="absolute flex flex-col left-0 bottom-[50px] w-[55%] p-4 z-10 max-[1390px]:w-[45%] max-[1390px]:bottom-[10px] max-[1300px]:w-[600px] max-[1120px]:w-[60%] max-md:w-[90%] max-[300px]:w-full">
<p className="text-[#ffbade] font-semibold text-[20px] w-fit max-[1300px]:text-[15px]">
#{index + 1} Spotlight
</p>
<h3 className="text-white line-clamp-2 text-5xl font-bold mt-6 text-left max-[1390px]:text-[45px] max-[1300px]:text-3xl max-[1300px]:mt-4 max-md:text-2xl max-md:mt-1 max-[575px]:text-[22px] max-sm:leading-6 max-sm:w-[80%] max-[320px]:w-full ">
{language === "EN" ? item.title : item.japanese_title}
</h3>
<div className="flex h-fit justify-center items-center w-fit space-x-5 mt-8 max-[1300px]:mt-6 max-md:hidden">
{item.tvInfo && (
<>
{item.tvInfo.showType && (
<div className="flex space-x-1 justify-center items-center">
<FontAwesomeIcon
icon={faPlay}
className="text-[8px] bg-white px-[4px] py-[3px] rounded-full"
/>
<p className="text-white text-[16px]">
{item.tvInfo.showType}
</p>
</div>
)}
{item.tvInfo.duration && (
<div className="flex space-x-1 justify-center items-center">
<FontAwesomeIcon
icon={faClock}
className="text-white text-[14px]"
/>
<p className="text-white text-[17px]">
{item.tvInfo.duration}
</p>
</div>
)}
{item.tvInfo.releaseDate && (
<div className="flex space-x-1 justify-center items-center">
<FontAwesomeIcon
icon={faCalendar}
className="text-white text-[14px]"
/>
<p className="text-white text-[16px]">
{item.tvInfo.releaseDate}
</p>
</div>
)}
<div className="flex space-x-3 w-fit">
{item.tvInfo.quality && (
<div className="bg-[#ffbade] py-[1px] px-[6px] rounded-md w-fit text-[11px] font-bold h-fit">
{item.tvInfo.quality}
</div>
)}
<div className="flex space-x-[1px] rounded-r-[5px] rounded-l-[5px] w-fit py-[1px] overflow-hidden">
{item.tvInfo.episodeInfo?.sub && (
<div className="flex space-x-1 justify-center items-center bg-[#B0E3AF] px-[4px]">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.episodeInfo.sub}
</p>
</div>
)}
{item.tvInfo.episodeInfo?.dub && (
<div className="flex space-x-1 justify-center items-center bg-[#B9E7FF] px-[4px]">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[12px]"
/>
<p className="text-[12px] font-semibold">
{item.tvInfo.episodeInfo.dub}
</p>
</div>
)}
</div>
</div>
</>
)}
</div>
<p className="text-white text-[17px] font-sm mt-6 text-left line-clamp-3 max-[1200px]:line-clamp-2 max-[1300px]:w-[500px] max-[1120px]:w-[90%] max-md:hidden">
{item.description}
</p>
<div className="flex gap-x-5 mt-10 max-md:mt-6 max-sm:w-full max-[320px]:flex-col max-[320px]:space-y-3">
<button className="flex justify-center items-center bg-[#ffbade] px-4 py-2 rounded-3xl gap-x-2 max-[320px]:w-fit ">
<FontAwesomeIcon
icon={faPlay}
className="text-[8px] bg-[#000000] px-[6px] py-[6px] rounded-full text-[#ffbade] max-[320px]:text-[6px]"
/>
<Link
to={`/watch/${item.id}`}
className="max-[1000px]:text-[15px] font-semibold max-[320px]:text-[12px]"
>
Watch Now
</Link>
</button>
<Link
to={`/${item.id}`}
className="flex bg-[#3B3A52] justify-center items-center px-4 py-2 rounded-3xl gap-x-2 max-[320px]:w-fit max-[320px]:px-3"
>
<p className="text-white max-[1000px]:text-[15px] font-semibold max-[320px]:text-[12px]">
Detail
</p>
<FaChevronRight className="text-white max-[320px]:text-[10px]" />
</Link>
</div>
</div>
</section>
);
}
export default Banner;

View File

@@ -1,10 +0,0 @@
.dot {
width: 4px;
height: 4px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background: rgba(255, 255, 255, .3);
display: inline-block;
}

View File

@@ -1,132 +0,0 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faClosedCaptioning,
faMicrophone,
} from "@fortawesome/free-solid-svg-icons";
import { FaChevronRight } from "react-icons/fa";
import { useLanguage } from "@/src/context/LanguageContext";
import "./Cart.css";
import { Link, useNavigate } from "react-router-dom";
import { useState } from "react";
import useToolTipPosition from "@/src/hooks/useToolTipPosition";
import Qtip from "../qtip/Qtip";
function Cart({ label, data, path }) {
const { language } = useLanguage();
const navigate = useNavigate();
const [hoveredItem, setHoveredItem] = useState(null);
const [hoverTimeout, setHoverTimeout] = useState(null);
const { tooltipPosition, tooltipHorizontalPosition, cardRefs } =
useToolTipPosition(hoveredItem, data);
const handleImageEnter = (item, index) => {
if (hoverTimeout) clearTimeout(hoverTimeout);
setHoveredItem(item.id + index);
};
const handleImageLeave = () => {
setHoverTimeout(
setTimeout(() => {
setHoveredItem(null);
}, 300)
);
};
return (
<div className="flex flex-col w-1/4 space-y-7 max-[1200px]:w-full">
<h1 className="font-bold text-2xl text-[#ffbade] max-md:text-xl">
{label}
</h1>
<div className="w-full space-y-4 flex flex-col">
{data &&
data.slice(0, 5).map((item, index) => (
<div
key={index}
style={{ borderBottom: "1px solid rgba(255, 255, 255, .075)" }}
className="flex pb-4 items-center relative"
ref={(el) => (cardRefs.current[index] = el)}
>
<img
src={`https://wsrv.nl/?url=${item.poster}`}
alt={item.title}
className="flex-shrink-0 w-[60px] h-[75px] rounded-md object-cover cursor-pointer"
onClick={() => navigate(`/watch/${item.id}`)}
onMouseEnter={() => handleImageEnter(item, index)}
onMouseLeave={handleImageLeave}
/>
{hoveredItem === item.id + index && window.innerWidth > 1024 && (
<div
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition}
${
tooltipHorizontalPosition === "left-1/2"
? "translate-x-[-100px]"
: "translate-x-[-200px]"
}
z-[100000] transform transition-all duration-300 ease-in-out
${
hoveredItem === item.id + index
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-2"
}`}
onMouseEnter={() => {
if (hoverTimeout) clearTimeout(hoverTimeout);
}}
onMouseLeave={handleImageLeave}
>
<Qtip id={item.id} />
</div>
)}
<div className="flex flex-col ml-4 space-y-2 w-full">
<Link
to={`/${item.id}`}
className="w-full line-clamp-2 text-[1em] font-[500] hover:cursor-pointer hover:text-[#ffbade] transform transition-all ease-out max-[1200px]:text-[14px]"
>
{language === "EN" ? item.title : item.japanese_title}
</Link>
<div className="flex items-center flex-wrap w-fit space-x-1">
{item.tvInfo?.sub && (
<div className="flex space-x-1 justify-center items-center bg-[#B0E3AF] rounded-[4px] px-[4px] text-black py-[2px]">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">{item.tvInfo.sub}</p>
</div>
)}
{item.tvInfo?.dub && (
<div className="flex space-x-1 justify-center items-center bg-[#B9E7FF] rounded-[4px] px-[8px] text-black py-[2px]">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">{item.tvInfo.dub}</p>
</div>
)}
<div className="flex items-center w-fit pl-1 gap-x-1">
<div className="dot"></div>
<p className="text-[14px] text-[#D2D2D3]">
{item.tvInfo.showType}
</p>
</div>
</div>
</div>
</div>
))}
<Link
to={`/${path}`}
className="flex w-fit items-baseline rounded-3xl gap-x-2 group"
>
<p className="text-white text-[17px] h-fit leading-4 group-hover:text-[#ffbade] transform transition-all ease-out">
View more
</p>
<FaChevronRight className="text-white text-[10px] group-hover:text-[#ffbade] transform transition-all ease-out" />
</Link>
</div>
</div>
);
}
export default Cart;

View File

@@ -1,27 +0,0 @@
.overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to top,
rgba(32, 31, 49, 1) 0%,
rgba(32, 31, 49, 0) 20%,
rgba(32, 31, 49, 0) 100%
);
z-index: 50;
}
.dot {
width: 5px;
height: 5px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
display: inline-block;
}

View File

@@ -1,340 +0,0 @@
import React, { useCallback, useEffect, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faClosedCaptioning,
faMicrophone,
faPlay,
} from "@fortawesome/free-solid-svg-icons";
import { FaChevronRight } from "react-icons/fa";
import "./CategoryCard.css";
import { useLanguage } from "@/src/context/LanguageContext";
import { Link, useNavigate } from "react-router-dom";
import Qtip from "../qtip/Qtip";
import useToolTipPosition from "@/src/hooks/useToolTipPosition";
const CategoryCard = React.memo(
({
label,
data,
showViewMore = true,
className,
categoryPage = false,
cardStyle,
path,
limit,
}) => {
const { language } = useLanguage();
const navigate = useNavigate();
const [showPlay, setShowPlay] = useState(false);
if (limit) {
data = data.slice(0, limit);
}
const [itemsToRender, setItemsToRender] = useState({
firstRow: [],
remainingItems: [],
});
const getItemsToRender = useCallback(() => {
if (categoryPage) {
const firstRow =
window.innerWidth > 758 && data.length > 4 ? data.slice(0, 4) : [];
const remainingItems =
window.innerWidth > 758 && data.length > 4
? data.slice(4)
: data.slice(0);
return { firstRow, remainingItems };
}
return { firstRow: [], remainingItems: data.slice(0) };
}, [categoryPage, data]);
useEffect(() => {
const handleResize = () => {
setItemsToRender(getItemsToRender());
};
const newItems = getItemsToRender();
setItemsToRender((prev) => {
if (
JSON.stringify(prev.firstRow) !== JSON.stringify(newItems.firstRow) ||
JSON.stringify(prev.remainingItems) !==
JSON.stringify(newItems.remainingItems)
) {
return newItems;
}
return prev;
});
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [getItemsToRender]);
const [hoveredItem, setHoveredItem] = useState(null);
const [hoverTimeout, setHoverTimeout] = useState(null);
const { tooltipPosition, tooltipHorizontalPosition, cardRefs } =
useToolTipPosition(hoveredItem, data);
const handleMouseEnter = (item, index) => {
const timeout = setTimeout(() => {
setHoveredItem(item.id + index);
setShowPlay(true);
}, 400);
setHoverTimeout(timeout);
};
const handleMouseLeave = () => {
clearTimeout(hoverTimeout);
setHoveredItem(null);
setShowPlay(false);
};
return (
<div className={`w-full ${className}`}>
<div className="flex items-center justify-between">
<h1 className="font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px] capitalize">
{label}
</h1>
{showViewMore && (
<Link
to={`/${path}`}
className="flex w-fit items-baseline h-fit rounded-3xl gap-x-1 group"
>
<p className="text-white text-[12px] font-semibold h-fit leading-0 group-hover:text-[#ffbade] transition-all ease-out">
View more
</p>
<FaChevronRight className="text-white text-[10px] group-hover:text-[#ffbade] transition-all ease-out" />
</Link>
)}
</div>
<>
{categoryPage && (
<div
className={`grid grid-cols-4 gap-x-3 gap-y-8 transition-all duration-300 ease-in-out ${
categoryPage && itemsToRender.firstRow.length > 0
? "mt-8 max-[758px]:hidden"
: ""
}`}
>
{itemsToRender.firstRow.map((item, index) => (
<div
key={index}
className="flex flex-col transition-transform duration-300 ease-in-out"
style={{ height: "fit-content" }}
ref={(el) => (cardRefs.current[index] = el)}
>
<div
className="w-full relative group hover:cursor-pointer"
onClick={() =>
navigate(
`${
path === "top-upcoming"
? `/${item.id}`
: `/watch/${item.id}`
}`
)
}
onMouseEnter={() => handleMouseEnter(item, index)}
onMouseLeave={handleMouseLeave}
>
{hoveredItem === item.id + index && showPlay && (
<FontAwesomeIcon
icon={faPlay}
className="text-[40px] text-white absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[10000]"
/>
)}
<div className="overlay"></div>
<div className="overflow-hidden">
<img
src={`https://wsrv.nl/?url=${item.poster}`}
alt={item.title}
className={`w-full h-[320px] object-cover max-[1200px]:h-[35vw] max-[758px]:h-[45vw] max-[478px]:h-[60vw] group-hover:blur-[7px] transform transition-all duration-300 ease-in-out ultra-wide:h-[400px] ${cardStyle}`}
/>
</div>
{(item.tvInfo?.rating === "18+" ||
item?.adultContent === true) && (
<div className="text-white px-2 rounded-md bg-[#FF5700] absolute top-2 left-2 flex items-center justify-center text-[14px] font-bold">
18+
</div>
)}
<div className="absolute left-2 bottom-3 flex items-center justify-center w-fit space-x-1 z-[100] max-[270px]:flex-col max-[270px]:gap-y-[3px]">
{item.tvInfo?.sub && (
<div className="flex space-x-1 justify-center items-center bg-[#B0E3AF] rounded-[2px] px-[4px] text-black py-[2px]">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.sub}
</p>
</div>
)}
{item.tvInfo?.dub && (
<div className="flex space-x-1 justify-center items-center bg-[#B9E7FF] rounded-[2px] px-[8px] text-black py-[2px]">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.dub}
</p>
</div>
)}
{item.tvInfo?.eps && (
<div className="flex space-x-1 justify-center items-center bg-[#a9a6b16f] rounded-[2px] px-[8px] text-white py-[2px]">
<p className="text-[12px] font-extrabold">
{item.tvInfo.eps}
</p>
</div>
)}
</div>
{hoveredItem === item.id + index &&
window.innerWidth > 1024 && (
<div
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition} z-[100000] transform transition-all duration-300 ease-in-out ${
hoveredItem === item.id + index
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-2"
}`}
>
<Qtip id={item.id} />
</div>
)}
</div>
<Link
to={`/${item.id}`}
className="text-white font-semibold mt-1 item-title hover:text-[#FFBADE] hover:cursor-pointer line-clamp-1"
>
{language === "EN" ? item.title : item.japanese_title}
</Link>
{item.description && (
<div className="line-clamp-3 text-[13px] font-extralight text-[#b1b0b0] max-[1200px]:hidden">
{item.description}
</div>
)}
<div className="flex items-center gap-x-2 w-full mt-2 overflow-hidden">
<div className="text-gray-400 text-[14px] text-nowrap overflow-hidden text-ellipsis">
{item.tvInfo.showType.split(" ").shift()}
</div>
<div className="dot"></div>
<div className="text-gray-400 text-[14px] text-nowrap overflow-hidden text-ellipsis">
{item.tvInfo?.duration === "m" ||
item.tvInfo?.duration === "?" ||
item.duration === "m" ||
item.duration === "?"
? "N/A"
: item.tvInfo?.duration || item.duration || "N/A"}
</div>
</div>
</div>
))}
</div>
)}
<div className="grid grid-cols-6 gap-x-3 gap-y-8 mt-6 transition-all duration-300 ease-in-out max-[1400px]:grid-cols-4 max-[758px]:grid-cols-3 max-[478px]:grid-cols-2">
{itemsToRender.remainingItems.map((item, index) => (
<div
key={index}
className="flex flex-col transition-transform duration-300 ease-in-out"
style={{ height: "fit-content" }}
ref={(el) => (cardRefs.current[index] = el)}
>
<div
className="w-full relative group hover:cursor-pointer"
onClick={() =>
navigate(
`${
path === "top-upcoming"
? `/${item.id}`
: `/watch/${item.id}`
}`
)
}
onMouseEnter={() => handleMouseEnter(item, index)}
onMouseLeave={handleMouseLeave}
>
{hoveredItem === item.id + index && showPlay && (
<FontAwesomeIcon
icon={faPlay}
className="text-[40px] text-white absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[10000]"
/>
)}
<div className="overlay"></div>
<div className="overflow-hidden">
<img
src={`https://wsrv.nl/?url=${item.poster}`}
alt={item.title}
className={`w-full h-[250px] object-cover max-[1200px]:h-[35vw] max-[758px]:h-[45vw] max-[478px]:h-[60vw] ${cardStyle} group-hover:blur-[7px] transform transition-all duration-300 ease-in-out `}
/>
</div>
{(item.tvInfo?.rating === "18+" ||
item?.adultContent === true) && (
<div className="text-white px-2 rounded-md bg-[#FF5700] absolute top-2 left-2 flex items-center justify-center text-[14px] font-bold">
18+
</div>
)}
<div className="absolute left-2 bottom-4 flex items-center justify-center w-fit space-x-1 z-[100] max-[270px]:flex-col max-[270px]:gap-y-[3px]">
{item.tvInfo?.sub && (
<div className="flex space-x-1 justify-center items-center bg-[#B0E3AF] rounded-[2px] px-[4px] text-black py-[2px]">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.sub}
</p>
</div>
)}
{item.tvInfo?.dub && (
<div className="flex space-x-1 justify-center items-center bg-[#B9E7FF] rounded-[2px] px-[8px] text-black py-[2px]">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.dub}
</p>
</div>
)}
</div>
{hoveredItem === item.id + index &&
window.innerWidth > 1024 && (
<div
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition} z-[100000] transform transition-all duration-300 ease-in-out ${
hoveredItem === item.id + index
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-2"
}`}
>
<Qtip id={item.id} />
</div>
)}
</div>
<Link
to={`/${item.id}`}
className="text-white font-semibold mt-1 item-title hover:text-[#FFBADE] hover:cursor-pointer line-clamp-1"
>
{language === "EN" ? item.title : item.japanese_title}
</Link>
<div className="flex items-center gap-x-2 w-full mt-2 overflow-hidden">
<div className="text-gray-400 text-[14px] text-nowrap overflow-hidden text-ellipsis">
{item.tvInfo.showType.split(" ").shift()}
</div>
<div className="dot"></div>
<div className="text-gray-400 text-[14px] text-nowrap overflow-hidden text-ellipsis">
{item.tvInfo?.duration === "m" ||
item.tvInfo?.duration === "?" ||
item.duration === "m" ||
item.duration === "?"
? "N/A"
: item.tvInfo?.duration || item.duration || "N/A"}
</div>
</div>
</div>
))}
</div>
</>
</div>
);
}
);
CategoryCard.displayName = "CategoryCard";
export default CategoryCard;

View File

@@ -1,132 +0,0 @@
import { Navigation } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react";
import { Link } from "react-router-dom";
import { useEffect, useState, useRef, useMemo } from "react";
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/navigation";
import { FaHistory, FaChevronLeft, FaChevronRight } from "react-icons/fa";
import { useLanguage } from "@/src/context/LanguageContext";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlay } from "@fortawesome/free-solid-svg-icons";
const ContinueWatching = () => {
const [watchList, setWatchList] = useState([]);
const { language } = useLanguage();
const swiperRef = useRef(null);
useEffect(() => {
const data = JSON.parse(localStorage.getItem("continueWatching") || "[]");
setWatchList(data);
}, []);
// Memoize watchList to avoid unnecessary re-renders
const memoizedWatchList = useMemo(() => watchList, [watchList]);
const removeFromWatchList = (episodeId) => {
setWatchList((prevList) => {
const updatedList = prevList.filter(
(item) => item.episodeId !== episodeId
);
localStorage.setItem("continueWatching", JSON.stringify(updatedList));
return updatedList;
});
};
if (memoizedWatchList.length === 0) return null;
return (
<div className="mt-6 max-[1200px]:px-6 max-md:px-0">
<div className="flex items-center justify-between max-md:pl-4">
<div className="flex items-center gap-x-2 justify-center">
<FaHistory className="text-[#ffbade]" />
<h1 className="text-[#ffbade] text-2xl font-bold max-[450px]:text-xl max-[450px]:mb-1 max-[350px]:text-lg">
Continue Watching
</h1>
</div>
<div className="flex gap-x-2 pr-2 max-[350px]:hidden">
<button className="btn-prev bg-gray-700 text-white p-3 rounded-full hover:bg-gray-500 transition max-[768px]:p-2">
<FaChevronLeft className="text-xs" />
</button>
<button className="btn-next bg-gray-700 text-white p-3 rounded-full hover:bg-gray-500 transition max-[768px]:p-2">
<FaChevronRight className="text-xs" />
</button>
</div>
</div>
<div className="relative mx-auto overflow-hidden z-[1] mt-6 max-[450px]:mt-3">
<Swiper
ref={swiperRef}
className="w-full h-full"
slidesPerView={3}
spaceBetween={15}
breakpoints={{
640: { slidesPerView: 4, spaceBetween: 15 },
768: { slidesPerView: 4, spaceBetween: 15 },
1024: { slidesPerView: 5, spaceBetween: 15 },
1300: { slidesPerView: 6, spaceBetween: 15 },
1600: { slidesPerView: 7, spaceBetween: 20 },
}}
modules={[Navigation]}
navigation={{
nextEl: ".btn-next",
prevEl: ".btn-prev",
}}
>
{memoizedWatchList.map((item, index) => (
<SwiperSlide
key={index}
className="text-center flex justify-center items-center"
>
<div className="w-full h-auto pb-[140%] relative inline-block overflow-hidden">
<button
className="absolute top-2 right-2 bg-black text-white px-3 py-2 bg-opacity-60 rounded-full text-sm z-10 font-extrabold hover:bg-white hover:text-black transition-all"
onClick={() => removeFromWatchList(item.episodeId)}
>
</button>
<Link
to={`/watch/${item?.id}?ep=${item.episodeId}`}
className="inline-block bg-[#2a2c31] absolute left-0 top-0 w-full h-full group"
>
<img
src={`https://wsrv.nl/?url=${item?.poster}`}
alt={item?.title}
className="block w-full h-full object-cover transition-all duration-300 ease-in-out group-hover:blur-[4px]"
title={item?.title}
loading="lazy"
/>
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<FontAwesomeIcon
icon={faPlay}
className="text-[50px] text-white absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[10000] max-[450px]:text-[36px]"
/>
</div>
</Link>
{item?.adultContent === true && (
<div className="text-white px-2 rounded-md bg-[#FF5700] absolute top-2 left-2 flex items-center justify-center text-[14px] font-bold">
18+
</div>
)}
<div className="absolute bottom-0 left-0 flex flex-col gap-y-2 right-0 p-2 bg-gradient-to-t from-black via-black/80 to-transparent max-[450px]:gap-y-1">
<p className="text-white text-md font-bold text-left truncate max-[450px]:text-sm">
{language === "EN"
? item?.title
: item?.japanese_title}
</p>
<p className="text-gray-300 text-sm font-semibold text-left max-[450px]:text-[12px]">
Episode {item.episodeNum}
</p>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
);
};
export default ContinueWatching;

View File

@@ -1,15 +0,0 @@
@keyframes glow {
0% {
box-shadow: 0 0 7px #ffbade;
}
50% {
box-shadow: 0 0 20px #ffbade;
}
100% {
box-shadow: 0 0 7px #ffbade;
}
}
.glow-animation {
animation: glow 1.5s infinite;
}

View File

@@ -1,303 +0,0 @@
import { useLanguage } from "@/src/context/LanguageContext";
import {
faAngleDown,
faCirclePlay,
faList,
faCheck,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { useState, useEffect, useRef } from "react";
import "./Episodelist.css";
function Episodelist({
episodes,
onEpisodeClick,
currentEpisode,
totalEpisodes,
}) {
const [activeEpisodeId, setActiveEpisodeId] = useState(currentEpisode);
const { language } = useLanguage();
const listContainerRef = useRef(null);
const activeEpisodeRef = useRef(null);
const [showDropDown, setShowDropDown] = useState(false);
const [selectedRange, setSelectedRange] = useState([1, 100]);
const [activeRange, setActiveRange] = useState("1-100");
const [episodeNum, setEpisodeNum] = useState(currentEpisode);
const dropDownRef = useRef(null);
const [searchedEpisode, setSearchedEpisode] = useState(null);
const scrollToActiveEpisode = () => {
if (activeEpisodeRef.current && listContainerRef.current) {
const container = listContainerRef.current;
const activeEpisode = activeEpisodeRef.current;
const containerTop = container.getBoundingClientRect().top;
const containerHeight = container.clientHeight;
const activeEpisodeTop = activeEpisode.getBoundingClientRect().top;
const activeEpisodeHeight = activeEpisode.clientHeight;
const offset = activeEpisodeTop - containerTop;
container.scrollTop =
container.scrollTop +
offset -
containerHeight / 2 +
activeEpisodeHeight / 2;
}
};
useEffect(() => {
setActiveEpisodeId(episodeNum);
}, [episodeNum]);
useEffect(() => {
scrollToActiveEpisode();
}, [activeEpisodeId]);
useEffect(() => {
const handleClickOutside = (event) => {
if (dropDownRef.current && !dropDownRef.current.contains(event.target)) {
setShowDropDown(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
function handleChange(e) {
const value = e.target.value;
if (value.trim() === "") {
const newRange = findRangeForEpisode(1);
setSelectedRange(newRange);
setActiveRange(`${newRange[0]}-${newRange[1]}`);
setSearchedEpisode(null);
} else if (!value || isNaN(value)) {
setSearchedEpisode(null);
} else if (
!isNaN(value) &&
parseInt(value, 10) > totalEpisodes &&
episodeNum !== null
) {
const newRange = findRangeForEpisode(episodeNum);
setSelectedRange(newRange);
setActiveRange(`${newRange[0]}-${newRange[1]}`);
setSearchedEpisode(null);
} else if (!isNaN(value) && value.trim() !== "") {
const num = parseInt(value, 10);
const foundEpisode = episodes.find((item) => item?.episode_no === num);
if (foundEpisode) {
const newRange = findRangeForEpisode(num);
setSelectedRange(newRange);
setActiveRange(`${newRange[0]}-${newRange[1]}`);
setSearchedEpisode(foundEpisode?.id);
}
} else {
setSearchedEpisode(null);
}
}
function findRangeForEpisode(episodeNumber) {
const step = 100;
const start = Math.floor((episodeNumber - 1) / step) * step + 1;
const end = Math.min(start + step - 1, totalEpisodes);
return [start, end];
}
function generateRangeOptions(totalEpisodes) {
const ranges = [];
const step = 100;
for (let i = 0; i < totalEpisodes; i += step) {
const start = i + 1;
const end = Math.min(i + step, totalEpisodes);
ranges.push(`${start}-${end}`);
}
return ranges;
}
useEffect(() => {
if (currentEpisode && episodeNum) {
if (episodeNum < selectedRange[0] || episodeNum > selectedRange[1]) {
const newRange = findRangeForEpisode(episodeNum);
setSelectedRange(newRange);
setActiveRange(`${newRange[0]}-${newRange[1]}`);
}
}
}, [currentEpisode, totalEpisodes, episodeNum]);
const handleRangeSelect = (range) => {
const [start, end] = range.split("-").map(Number);
setSelectedRange([start, end]);
};
useEffect(() => {
const activeEpisode = episodes.find(
(item) => item?.id.match(/ep=(\d+)/)?.[1] === activeEpisodeId
);
if (activeEpisode) {
setEpisodeNum(activeEpisode?.episode_no);
}
}, [activeEpisodeId, episodes]);
return (
<div className="relative flex flex-col w-full h-full max-[1200px]:max-h-[500px]">
<div className="sticky top-0 z-10 flex flex-col gap-y-[5px] justify-start px-3 py-4 bg-[#0D0D15]">
<h1 className="text-[13px] font-bold">List of episodes:</h1>
{totalEpisodes > 100 && (
<div className="w-full flex gap-x-4 items-center max-[1200px]:justify-between">
<div className="min-w-fit flex text-[13px]">
<div
onClick={() => setShowDropDown((prev) => !prev)}
className="text-white w-fit mt-1 text-[13px] relative cursor-pointer bg-[#0D0D15] flex justify-center items-center"
ref={dropDownRef}
>
<FontAwesomeIcon icon={faList} />
<div className="w-fit flex justify-center items-center gap-x-2 ml-4">
<p className="text-white text-[12px]">
EPS:&nbsp;{selectedRange[0]}-{selectedRange[1]}
</p>
<FontAwesomeIcon
icon={faAngleDown}
className="mt-[2px] text-[10px]"
/>
</div>
{showDropDown && (
<div className="absolute flex flex-col top-full mt-[10px] left-0 z-30 bg-white w-[150px] max-h-[200px] overflow-y-auto rounded-l-[8px]">
{generateRangeOptions(totalEpisodes).map((item, index) => (
<div
key={index}
onClick={() => {
handleRangeSelect(item);
setActiveRange(item);
}}
className={`hover:bg-gray-200 cursor-pointer text-black ${
item === activeRange ? "bg-[#EFF0F4]" : ""
}`}
>
<p className="font-semibold text-[12px] p-3 flex justify-between items-center">
EPS:&nbsp;{item}
{item === activeRange ? (
<FontAwesomeIcon icon={faCheck} />
) : null}
</p>
</div>
))}
</div>
)}
</div>
</div>
<div className="border-[1px] border-[#ffffff34] rounded-sm py-[4px] px-[8px] flex items-center gap-x-[10px]">
<FontAwesomeIcon
icon={faMagnifyingGlass}
className="text-[11px]"
/>
<input
type="text"
className="w-full bg-transparent focus:outline-none rounded-sm text-[13px] font-bold placeholder:text-[12px] placeholder:font-medium"
placeholder="Number of Ep"
onChange={handleChange}
/>
</div>
</div>
)}
</div>
<div ref={listContainerRef} className="w-full h-full overflow-y-auto">
<div
className={`${
totalEpisodes > 30
? "p-3 grid grid-cols-5 gap-1 max-[1200px]:grid-cols-12 max-[860px]:grid-cols-10 max-[575px]:grid-cols-8 max-[478px]:grid-cols-6 max-[350px]:grid-cols-5"
: ""
}`}
>
{totalEpisodes > 30
? episodes
.slice(selectedRange[0] - 1, selectedRange[1])
.map((item, index) => {
const episodeNumber = item?.id.match(/ep=(\d+)/)?.[1];
const isActive =
activeEpisodeId === episodeNumber ||
currentEpisode === episodeNumber;
const isSearched = searchedEpisode === item?.id;
return (
<div
key={item?.id}
ref={isActive ? activeEpisodeRef : null}
className={`flex items-center justify-center rounded-[3px] h-[30px] text-[13.5px] font-medium cursor-pointer group ${
item?.filler
? isActive
? "bg-[#ffbade]"
: "bg-gradient-to-r from-[#5a4944] to-[#645a4b]"
: ""
} md:hover:bg-[#67686F]
md:hover:text-white
${
isActive
? "bg-[#ffbade] text-black"
: "bg-[#35373D] text-gray-400"
} ${isSearched ? "glow-animation" : ""} `}
onClick={() => {
if (episodeNumber) {
onEpisodeClick(episodeNumber);
setActiveEpisodeId(episodeNumber);
setSearchedEpisode(null);
}
}}
>
<span
className={`${
item?.filler
? "text-white md:group-hover:text-[#ffbade]"
: ""
}`}
>
{index + selectedRange[0]}
</span>
</div>
);
})
: episodes?.map((item, index) => {
const episodeNumber = item?.id.match(/ep=(\d+)/)?.[1];
const isActive =
activeEpisodeId === episodeNumber ||
currentEpisode === episodeNumber;
const isSearched = searchedEpisode === item?.id;
return (
<div
key={item?.id}
ref={isActive ? activeEpisodeRef : null}
className={`w-full pl-5 pr-2 py-3 flex items-center justify-start gap-x-8 cursor-pointer ${
(index + 1) % 2 && !isActive
? "bg-[#201F2D] text-gray-400"
: "bg-none"
} group md:hover:bg-[#2B2A42] ${
isActive ? "text-[#ffbade] bg-[#2B2A42]" : ""
} ${isSearched ? "glow-animation" : ""}`}
onClick={() => {
if (episodeNumber) {
onEpisodeClick(episodeNumber);
setActiveEpisodeId(episodeNumber);
setSearchedEpisode(null);
}
}}
>
<p className="text-[14px] font-medium">{index + 1}</p>
<div className="w-full flex items-center justify-between gap-x-[5px]">
<h1 className="line-clamp-1 text-[15px] font-light group-hover:text-[#ffbade]">
{language === "EN" ? item?.title : item?.japanese_title}
</h1>
{isActive && (
<FontAwesomeIcon
icon={faCirclePlay}
className="w-[20px] h-[20px] text-[#ffbade]"
/>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
);
}
export default Episodelist;

View File

@@ -1,21 +0,0 @@
import { FaChevronLeft } from "react-icons/fa"
import { useNavigate } from "react-router-dom"
function Error({ error }) {
const navigate = useNavigate();
return (
<div className="bg-[#201F31] w-full h-screen flex justify-center items-center">
<div className="flex flex-col w-fit h-fit items-center justify-center">
<img src="https://s1.gifyu.com/images/SBlOe.png" alt="" className="w-[300px] h-[300px] max-[500px]:w-[200px] max-[500px]:h-[200px]" />
<h1 className="text-white text-[35px] leading-5 mt-7">{error === "404" ? "404 Error" : "Error"}</h1>
<p className="mt-5">Oops! We couldn&apos;t find this page.</p>
<button className="bg-[#ffbade] py-2 px-4 w-fit rounded-3xl text-black text-light flex items-center gap-x-2 mt-7">
<FaChevronLeft className="text-[#ffbade] w-[20px] h-[20px] rounded-full p-1 bg-black" />
<p onClick={() => navigate('/home')} className="text-[18px]">Back to homepage</p>
</button>
</div>
</div>
)
}
export default Error

View File

@@ -1,62 +0,0 @@
import logoTitle from "@/src/config/logoTitle.js";
import website_name from "@/src/config/website.js";
import { Link } from "react-router-dom";
function Footer() {
return (
<footer className="flex flex-col w-full mt-[100px] px-4 max-[500px]:px-0">
<div
style={{ borderBottom: "1px solid rgba(255, 255, 255, .075)" }}
className="w-full text-left max-[500px]:hidden"
>
<img
src="https://i.postimg.cc/SsKY6Y9f/2H76i57.png"
alt={logoTitle}
className="w-[200px] h-[100px]"
/>
</div>
<div className="flex py-5 flex-col w-full space-y-4 max-md:items-center max-[500px]:bg-[#373646]">
<div className="flex w-fit items-center space-x-6 max-[500px]:hidden">
<p className="text-2xl font-bold max-md:text-lg">A-Z LIST</p>
<p
style={{ borderLeft: "1px solid rgba(255, 255, 255, 0.6)" }}
className="text-md font-semibold pl-6"
>
Searching anime order by alphabet name A to Z
</p>
</div>
<div className="flex gap-x-[7px] flex-wrap justify-start gap-y-2 max-md:justify-start max-[500px]:hidden">
{[
"All",
"#",
"0-9",
...Array.from({ length: 26 }, (_, i) =>
String.fromCharCode(65 + i)
),
].map((item, index) => (
<Link
to={`az-list/${item === "All" ? "" : item}`}
key={index}
className="text-lg bg-[#373646] px-2 rounded-md font-bold hover:text-black hover:bg-[#FFBADE] hover:cursor-pointer transition-all ease-out"
>
{item}
</Link>
))}
</div>
<div className="flex flex-col w-full text-left space-y-2 pt-4 max-md:items-center max-[470px]:px-[5px]">
<p className="text-[#9B9BA3] text-[16px] max-md:text-center max-md:text-[12px]">
{website_name} does not host any files, it merely pulls streams from
3rd party services. Legal issues should be taken up with the file
hosts and providers. {website_name} is not responsible for any media
files shown by the video providers.
</p>
<p className="text-[#9B9BA3] max-md:text-[14px]">
© {website_name}. All rights reserved.
</p>
</div>
</div>
</footer>
);
}
export default Footer;

View File

@@ -1,54 +0,0 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
function Genre({ data }) {
const colors = [
"#A4B389",
"#FFBADE",
"#935C5F",
"#AD92BC",
"#ABCCD8",
"#D8B2AB",
"#85E1CD",
"#B7C996",
];
const [showAll, setShowAll] = useState(false);
const toggleGenres = () => {
setShowAll((prev) => !prev);
};
return (
<div className="flex flex-col w-full">
<h1 className="font-bold text-2xl text-[#ffbade]">Genres</h1>
<div className="bg-[#373646] py-6 px-4 mt-6 max-[478px]:bg-transparent max-[478px]:px-0">
<div className="grid grid-cols-3 grid-rows-2 gap-x-4 gap-y-3 w-full max-[478px]:flex max-[478px]:flex-wrap max-[478px]:gap-2">
{data &&
(showAll ? data : data.slice(0, 24)).map((item, index) => {
const textColor = colors[index % colors.length];
return (
<Link
to={`/genre/${item}`}
key={index}
className="rounded-[4px] py-2 px-3 hover:bg-[#555462] hover:cursor-pointer max-[478px]:bg-[#373646] max-[478px]:py-[6px]"
style={{ color: textColor }}
>
<div className="overflow-hidden text-left text-ellipsis text-nowrap font-bold">
{item.charAt(0).toUpperCase() + item.slice(1)}
</div>
</Link>
);
})}
</div>
<button
className="w-full bg-[#555462d3] py-3 mt-4 hover:bg-[#555462] rounded-md font-bold transform transition-all ease-out"
onClick={toggleGenres}
>
{showAll ? "Show less" : "Show more"}
</button>
</div>
</div>
);
}
export default React.memo(Genre);

View File

@@ -1,147 +0,0 @@
import { useState, useEffect } from "react";
import logoTitle from "@/src/config/logoTitle";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBars,
faFilm,
faRandom,
faStar,
} from "@fortawesome/free-solid-svg-icons";
import { useLanguage } from "@/src/context/LanguageContext";
import { Link, useLocation } from "react-router-dom";
import Sidebar from "../sidebar/Sidebar";
import { SearchProvider } from "@/src/context/SearchContext";
import WebSearch from "../searchbar/WebSearch";
import MobileSearch from "../searchbar/MobileSearch";
import { FaTelegramPlane } from "react-icons/fa";
function Navbar() {
const location = useLocation();
const { language, toggleLanguage } = useLanguage();
const [isNotHomePage, setIsNotHomePage] = useState(
location.pathname !== "/" && location.pathname !== "/home"
);
const [isScrolled, setIsScrolled] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 0);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
const handleHamburgerClick = () => {
setIsSidebarOpen(true);
};
const handleCloseSidebar = () => {
setIsSidebarOpen(false);
};
const handleRandomClick = () => {
if (location.pathname === "/random") {
window.location.reload();
}
};
useEffect(() => {
setIsNotHomePage(
location.pathname !== "/" && location.pathname !== "/home"
);
}, [location.pathname]);
return (
<SearchProvider>
<nav
className={`fixed top-0 left-0 w-full h-16 z-[1000000] flex p-4 py-8 items-center justify-between transition-all duration-300 ease-in-out ${
isNotHomePage ? "bg-[#201F31]" : "bg-opacity-0"
} ${
isScrolled ? "bg-[#2D2B44] bg-opacity-90 backdrop-blur-md" : ""
} max-[600px]:h-fit max-[600px]:flex-col max-[1200px]:bg-opacity-100 max-[600px]:py-2`}
>
<div className="flex gap-x-6 items-center w-fit max-lg:w-full max-lg:justify-between">
<div className="flex gap-x-6 items-center w-fit">
<FontAwesomeIcon
icon={faBars}
className="text-2xl text-white mt-1 cursor-pointer"
onClick={handleHamburgerClick}
/>
<Link
to="/"
className="text-4xl font-bold max-[575px]:text-3xl cursor-pointer"
>
{logoTitle.slice(0, 3)}
<span className="text-[#FFBADE]">{logoTitle.slice(3, 4)}</span>
{logoTitle.slice(4)}
</Link>
</div>
<WebSearch />
</div>
<div className="flex gap-x-7 items-center max-lg:hidden">
{[
{ icon: faRandom, label: "Random", path: "/random" },
{ icon: faFilm, label: "Movie", path: "/movie" },
{ icon: faStar, label: "Popular", path: "/most-popular" },
].map((item) => (
<Link
key={item.path}
to={
item.path === "/random"
? location.pathname === "/random"
? "#"
: "/random"
: item.path
}
onClick={item.path === "/random" ? handleRandomClick : undefined}
className="flex flex-col gap-y-1 items-center cursor-pointer"
>
<FontAwesomeIcon
icon={item.icon}
className="text-[#ffbade] text-xl font-bold"
/>
<p className="text-[15px]">{item.label}</p>
</Link>
))}
<div className="flex flex-col gap-y-1 items-center w-auto">
<div className="flex">
{["EN", "JP"].map((lang, index) => (
<button
key={lang}
onClick={() => toggleLanguage(lang)}
className={`px-1 py-[1px] text-xs font-bold ${
index === 0 ? "rounded-l-[3px]" : "rounded-r-[3px]"
} ${
language === lang
? "bg-[#ffbade] text-black"
: "bg-gray-600 text-white"
}`}
>
{lang}
</button>
))}
</div>
<div className="w-full">
<p className="whitespace-nowrap text-[15px]">Anime name</p>
</div>
</div>
<Link
to="https://t.me/zenime_discussion"
className="flex flex-col gap-y-1 items-center cursor-pointer"
>
<FaTelegramPlane
// icon={faTelegram}
className="text-xl font-bold text-[#ffbade]"
/>
<p className="text-[15px] mb-[1px] text-white">Join Telegram</p>
</Link>
</div>
<MobileSearch />
</nav>
<Sidebar isOpen={isSidebarOpen} onClose={handleCloseSidebar} />
</SearchProvider>
);
}
export default Navbar;

View File

@@ -1,76 +0,0 @@
import { faAngleDoubleLeft, faAngleDoubleRight, faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
function PageSlider({ page, totalPages, handlePageChange, start = false, style }) {
const renderPageNumbers = () => {
const pages = [];
if (totalPages === 1) return null;
if (totalPages <= 3) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (page === 1) {
pages.push(1, 2, 3);
} else if (page === 2) {
pages.push(1, 2, 3, 4);
} else if (page === totalPages) {
pages.push(totalPages - 2, totalPages - 1, totalPages);
} else if (page === totalPages - 1) {
pages.push(totalPages - 3, totalPages - 2, totalPages - 1, totalPages);
} else {
pages.push(page - 2, page - 1, page, page + 1, page + 2);
}
}
return pages.map((p) => (
<button
key={p}
onClick={() => handlePageChange(p)}
className={`w-[40px] text-[15px] mx-1 flex justify-center items-center p-2 rounded-full font-bold ${page === p ? 'bg-[#ffbade] text-[#2B2A3C] cursor-default' : 'bg-[#2B2A3C] text-[#999] hover:text-[#ffbade]'} ${start ? "bg-[#353537]" : "bg-[#2B2A3C]"} `}
>
{p}
</button>
));
};
return (
<div className={`w-full flex ${start ? "justify-start" : "justify-center"} items-center mt-12 overflow-hidden`} style={style}>
<div className="flex justify-center mt-4 w-fit">
{page > 1 && totalPages > 2 && (
<button
onClick={() => handlePageChange(1)}
className={`w-[40px] mx-1 p-2 ${start ? "bg-[#353537]" : "bg-[#2B2A3C]"} rounded-full text-[#999] text-[8px] hover:text-[#ffbade]`}
>
<FontAwesomeIcon icon={faAngleDoubleLeft} />
</button>
)}
{page > 1 && (
<button
onClick={() => { if (page > 0) handlePageChange(page - 1) }}
className={`w-[40px] mx-1 p-2 ${start ? "bg-[#353537]" : "bg-[#2B2A3C]"} rounded-full text-[#999] text-[8px] hover:text-[#ffbade]`}
>
<FontAwesomeIcon icon={faChevronLeft} />
</button>
)}
{renderPageNumbers()}
{page < totalPages && (
<button
onClick={() => { if (page < totalPages) handlePageChange(page + 1) }}
className={`w-[40px] mx-1 p-2 ${start ? "bg-[#353537]" : "bg-[#2B2A3C]"} rounded-full text-[#999] text-[8px] hover:text-[#ffbade]`}
>
<FontAwesomeIcon icon={faChevronRight} />
</button>
)}
{page < totalPages && totalPages > 2 && (
<button
onClick={() => handlePageChange(totalPages)}
className={`w-[40px] mx-1 p-2 ${start ? "bg-[#353537]" : "bg-[#2B2A3C]"} rounded-full text-[#999] text-[8px] hover:text-[#ffbade]`}
>
<FontAwesomeIcon icon={faAngleDoubleRight} />
</button>
)}
</div>
</div>
)
}
export default PageSlider

View File

@@ -1,148 +0,0 @@
/* eslint-disable react/prop-types */
import { useEffect, useState } from "react";
import BouncingLoader from "../ui/bouncingloader/Bouncingloader";
import axios from "axios";
export default function IframePlayer({
animeId,
episodeId,
serverName,
servertype,
animeInfo,
episodeNum,
episodes,
playNext,
autoNext,
}) {
const api_url=import.meta.env.VITE_API_URL;
const baseURL =
serverName.toLowerCase() === "hd-1"
? import.meta.env.VITE_BASE_IFRAME_URL
: serverName.toLowerCase() === "hd-4"
? import.meta.env.VITE_BASE_IFRAME_URL_2
: undefined;
const [loading, setLoading] = useState(true);
const [iframeLoaded, setIframeLoaded] = useState(false);
const [iframeSrc, setIframeSrc] = useState("");
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(
episodes?.findIndex(
(episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId
)
);
useEffect(() => {
const loadIframeUrl = async () => {
setLoading(true);
setIframeLoaded(false);
setIframeSrc("");
const lowerName = serverName.toLowerCase();
if (lowerName === "hd-1" || lowerName === "hd-4") {
setIframeSrc(`${baseURL}/${episodeId}/${servertype}`);
} else if (lowerName === "hd-2" || lowerName === "hd-3") {
try {
const res = await axios.get(
`${api_url}/stream?id=${animeId}?ep=${episodeId}&server=${serverName}&type=${servertype}`
);
const link = res?.data?.results?.streamingLink?.link;
if (link) {
setIframeSrc(`${link}&_debug=true`);
} else {
console.error("Streaming link not found in response");
}
} catch (err) {
console.error("Failed to fetch streaming link:", err);
}
}
};
loadIframeUrl();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [episodeId, servertype, serverName, animeInfo]);
useEffect(() => {
if (episodes?.length > 0) {
const newIndex = episodes.findIndex(
(episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId
);
setCurrentEpisodeIndex(newIndex);
}
}, [episodeId, episodes]);
useEffect(() => {
const handleMessage = (event) => {
const { currentTime, duration } = event.data;
if (typeof currentTime === "number" && typeof duration === "number") {
if (
currentTime >= duration &&
currentEpisodeIndex < episodes?.length - 1 &&
autoNext
) {
playNext(episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]);
}
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [autoNext, currentEpisodeIndex, episodes, playNext]);
useEffect(() => {
setLoading(true);
setIframeLoaded(false);
return () => {
const continueWatching = JSON.parse(localStorage.getItem("continueWatching")) || [];
const newEntry = {
id: animeInfo?.id,
data_id: animeInfo?.data_id,
episodeId,
episodeNum,
adultContent: animeInfo?.adultContent,
poster: animeInfo?.poster,
title: animeInfo?.title,
japanese_title: animeInfo?.japanese_title,
};
if (!newEntry.data_id) return;
const existingIndex = continueWatching.findIndex(
(item) => item.data_id === newEntry.data_id
);
if (existingIndex !== -1) {
continueWatching[existingIndex] = newEntry;
} else {
continueWatching.push(newEntry);
}
localStorage.setItem("continueWatching", JSON.stringify(continueWatching));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [episodeId, servertype]);
return (
<div className="relative w-full h-full overflow-hidden">
{/* Loader Overlay */}
<div
className={`absolute inset-0 flex justify-center items-center bg-black bg-opacity-50 z-10 transition-opacity duration-500 ${
loading ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
}`}
>
<BouncingLoader />
</div>
<iframe
key={`${episodeId}-${servertype}-${serverName}-${iframeSrc}`}
src={iframeSrc}
allowFullScreen
className={`w-full h-full transition-opacity duration-500 ${
iframeLoaded ? "opacity-100" : "opacity-0"
}`}
onLoad={() => {
setIframeLoaded(true);
setTimeout(() => setLoading(false), 1000);
}}
></iframe>
</div>
);
}

View File

@@ -1,59 +0,0 @@
.art-subtitle {
padding-inline: 0px !important;
gap: 2px !important;
}
.art-volume-panel {
padding-bottom: 20px !important;
}
.art-settings {
margin-bottom: 20px !important;
}
.art-subtitle {
margin-bottom: 1rem !important;
}
.art-subtitle-line {
min-width: fit-content;
background-color: rgba(0, 0, 0, 0.479) !important;
padding-inline: 3px !important;
}
.art-subtitle-line,
.art-subtitle-line * {
font-size: inherit !important;
color: inherit !important;
line-height: inherit !important;
font-weight: inherit !important;
white-space: inherit !important;
}
@media screen and (max-width: 370px) {
.art-progress {
padding-bottom: 5px !important;
}
.art-controls-left .art-control {
justify-content: flex-start !important;
}
.art-controls-right .art-control {
justify-content: flex-end !important;
}
.art-controls-right .art-control svg {
width: 22px;
height: 22px;
}
.art-controls-left .art-control svg {
width: 22px;
height: 22px;
}
.art-state .art-icon svg {
width: 50px;
height: 50px;
}
}
@media screen and (max-width: 350px) {
.art-controls-right .art-control svg {
width: 20px;
height: 20px;
}
.art-controls-left .art-control svg {
width: 20px;
height: 20px;
}
}

View File

@@ -1,494 +0,0 @@
/* eslint-disable react/prop-types */
import Hls from "hls.js";
import { useEffect, useRef, useState } from "react";
import Artplayer from "artplayer";
import artplayerPluginChapter from "./artPlayerPluinChaper";
import autoSkip from "./autoSkip";
import artplayerPluginVttThumbnail from "./artPlayerPluginVttThumbnail";
import {
backward10Icon,
backwardIcon,
captionIcon,
forward10Icon,
forwardIcon,
fullScreenOffIcon,
fullScreenOnIcon,
loadingIcon,
logo,
muteIcon,
pauseIcon,
pipIcon,
playIcon,
playIconLg,
settingsIcon,
volumeIcon,
} from "./PlayerIcons";
import "./Player.css";
import website_name from "@/src/config/website";
import getChapterStyles from "./getChapterStyle";
import artplayerPluginHlsControl from "artplayer-plugin-hls-control";
import artplayerPluginUploadSubtitle from "./artplayerPluginUploadSubtitle";
Artplayer.LOG_VERSION = false;
Artplayer.CONTEXTMENU = false;
const KEY_CODES = {
M: "m",
I: "i",
F: "f",
V: "v",
SPACE: " ",
ARROW_UP: "arrowup",
ARROW_DOWN: "arrowdown",
ARROW_RIGHT: "arrowright",
ARROW_LEFT: "arrowleft",
};
export default function Player({
streamUrl,
subtitles,
thumbnail,
intro,
outro,
serverName,
autoSkipIntro,
autoPlay,
autoNext,
episodeId,
episodes,
playNext,
animeInfo,
episodeNum,
streamInfo,
}) {
const artRef = useRef(null);
const leftAtRef = useRef(0);
const proxy = import.meta.env.VITE_PROXY_URL;
const m3u8proxy = import.meta.env.VITE_M3U8_PROXY_URL?.split(",") || [];
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(
episodes?.findIndex(
(episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId
)
);
useEffect(() => {
if (episodes?.length > 0) {
const newIndex = episodes.findIndex(
(episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId
);
setCurrentEpisodeIndex(newIndex);
}
}, [episodeId, episodes]);
useEffect(() => {
const applyChapterStyles = () => {
const existingStyles = document.querySelectorAll(
"style[data-chapter-styles]"
);
existingStyles.forEach((style) => style.remove());
const styleElement = document.createElement("style");
styleElement.setAttribute("data-chapter-styles", "true");
const styles = getChapterStyles(intro, outro);
styleElement.textContent = styles;
document.head.appendChild(styleElement);
return () => {
styleElement.remove();
};
};
if (streamUrl || intro || outro) {
const cleanup = applyChapterStyles();
return cleanup;
}
}, [streamUrl, intro, outro]);
const playM3u8 = (video, url, art) => {
if (Hls.isSupported()) {
if (art.hls) art.hls.destroy();
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
art.hls = hls;
art.on("destroy", () => hls.destroy());
// hls.on(Hls.Events.ERROR, (event, data) => {
// console.error("HLS.js error:", data);
// });
video.addEventListener("timeupdate", () => {
const currentTime = Math.round(video.currentTime);
const duration = Math.round(video.duration);
if (duration > 0 && currentTime >= duration) {
art.pause();
if (currentEpisodeIndex < episodes?.length - 1 && autoNext) {
playNext(
episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]
);
}
}
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
video.addEventListener("timeupdate", () => {
const currentTime = Math.round(video.currentTime);
const duration = Math.round(video.duration);
if (duration > 0 && currentTime >= duration) {
art.pause();
if (currentEpisodeIndex < episodes?.length - 1 && autoNext) {
playNext(
episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]
);
}
}
});
} else {
console.log("Unsupported playback format: m3u8");
}
};
const createChapters = () => {
const chapters = [];
if (intro?.start !== 0 || intro?.end !== 0) {
chapters.push({ start: intro.start, end: intro.end, title: "intro" });
}
if (outro?.start !== 0 || outro?.end !== 0) {
chapters.push({ start: outro.start, end: outro.end, title: "outro" });
}
return chapters;
};
const handleKeydown = (event, art) => {
const tagName = event.target.tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") return;
switch (event.key.toLowerCase()) {
case KEY_CODES.M:
art.muted = !art.muted;
break;
case KEY_CODES.I:
art.pip = !art.pip;
break;
case KEY_CODES.F:
event.preventDefault();
event.stopPropagation();
art.fullscreen = !art.fullscreen;
break;
case KEY_CODES.V:
event.preventDefault();
event.stopPropagation();
art.subtitle.show = !art.subtitle.show;
break;
case KEY_CODES.SPACE:
event.preventDefault();
event.stopPropagation();
art.playing ? art.pause() : art.play();
break;
case KEY_CODES.ARROW_UP:
event.preventDefault();
event.stopPropagation();
art.volume = Math.min(art.volume + 0.1, 1);
break;
case KEY_CODES.ARROW_DOWN:
event.preventDefault();
event.stopPropagation();
art.volume = Math.max(art.volume - 0.1, 0);
break;
case KEY_CODES.ARROW_RIGHT:
event.preventDefault();
event.stopPropagation();
art.currentTime = Math.min(art.currentTime + 10, art.duration);
break;
case KEY_CODES.ARROW_LEFT:
event.preventDefault();
event.stopPropagation();
art.currentTime = Math.max(art.currentTime - 10, 0);
break;
default:
break;
}
};
useEffect(() => {
if (!streamUrl || !artRef.current) return;
const iframeUrl = streamInfo?.streamingLink?.iframe;
const headers = {};
headers.referer=new URL(iframeUrl).origin+"/";
const art = new Artplayer({
url:
m3u8proxy[Math.floor(Math.random() * m3u8proxy?.length)] +
encodeURIComponent(streamUrl) +
"&headers=" +
encodeURIComponent(JSON.stringify(headers)),
container: artRef.current,
type: "m3u8",
autoplay: autoPlay,
volume: 1,
setting: true,
playbackRate: true,
pip: true,
hotkey: false,
fullscreen: true,
mutex: true,
playsInline: true,
lock: true,
airplay: true,
autoOrientation: true,
fastForward: true,
aspectRatio: true,
plugins: [
artplayerPluginHlsControl({
quality: {
setting: true,
getName: (level) => level.height + "P",
title: "Quality",
auto: "Auto",
},
}),
artplayerPluginUploadSubtitle(),
artplayerPluginChapter({ chapters: createChapters() }),
],
subtitle: {
style: {
color: "#fff",
"font-weight": "400",
left: "50%",
transform: "translateX(-50%)",
"margin-bottom": "2rem",
},
escape: false,
},
layers: [
{
name: website_name,
html: logo,
tooltip: website_name,
style: {
opacity: 1,
position: "absolute",
top: "5px",
right: "5px",
transition: "opacity 0.5s ease-out",
},
},
{
html: "",
style: {
position: "absolute",
left: "50%",
top: 0,
width: "20%",
height: "100%",
transform: "translateX(-50%)",
},
disable: !Artplayer.utils.isMobile,
click: () => art.toggle(),
},
{
name: "rewind",
html: "",
style: { position: "absolute", left: 0, top: 0, width: "40%", height: "100%" },
disable: !Artplayer.utils.isMobile,
click: () => {
art.controls.show = !art.controls.show;
},
},
{
name: "forward",
html: "",
style: { position: "absolute", right: 0, top: 0, width: "40%", height: "100%" },
disable: !Artplayer.utils.isMobile,
click: () => {
art.controls.show = !art.controls.show;
},
},
{
name: "backwardIcon",
html: backwardIcon,
style: {
position: "absolute",
left: "25%",
top: "50%",
transform: "translate(50%,-50%)",
opacity: 0,
transition: "opacity 0.5s ease-in-out",
},
disable: !Artplayer.utils.isMobile,
},
{
name: "forwardIcon",
html: forwardIcon,
style: {
position: "absolute",
right: "25%",
top: "50%",
transform: "translate(50%, -50%)",
opacity: 0,
transition: "opacity 0.5s ease-in-out",
},
disable: !Artplayer.utils.isMobile,
},
],
controls: [
{
html: backward10Icon,
position: "right",
tooltip: "Backward 10s",
click: () => {
art.currentTime = Math.max(art.currentTime - 10, 0);
},
},
{
html: forward10Icon,
position: "right",
tooltip: "Forward 10s",
click: () => {
art.currentTime = Math.min(art.currentTime + 10, art.duration);
},
},
],
icons: {
play: playIcon,
pause: pauseIcon,
setting: settingsIcon,
volume: volumeIcon,
pip: pipIcon,
volumeClose: muteIcon,
state: playIconLg,
loading: loadingIcon,
fullscreenOn: fullScreenOnIcon,
fullscreenOff: fullScreenOffIcon,
},
customType: { m3u8: playM3u8 },
});
art.on("resize", () => {
art.subtitle.style({
fontSize:
(art.width > 500 ? art.width * 0.02 : art.width * 0.03) + "px",
});
});
art.on("ready", () => {
const continueWatchingList = JSON.parse(localStorage.getItem("continueWatching")) || [];
const currentEntry = continueWatchingList.find((item) => item.episodeId === episodeId);
if (currentEntry?.leftAt) art.currentTime = currentEntry.leftAt;
art.on("video:timeupdate", () => {
leftAtRef.current = Math.floor(art.currentTime);
});
setTimeout(() => {
art.layers[website_name].style.opacity = 0;
}, 2000);
const defaultSubtitle = subtitles?.find((sub) => sub.label.toLowerCase() === "english");
if (defaultSubtitle) {
art.subtitle.switch(defaultSubtitle.file, {
name: defaultSubtitle.label,
default: true,
});
}
const skipRanges = [
...(intro.start != null && intro.end != null ? [[intro.start + 1, intro.end - 1]] : []),
...(outro.start != null && outro.end != null ? [[outro.start + 1, outro.end]] : []),
];
autoSkipIntro && art.plugins.add(autoSkip(skipRanges));
document.addEventListener("keydown", (event) => handleKeydown(event, art));
art.subtitle.style({
fontSize: (art.width > 500 ? art.width * 0.02 : art.width * 0.03) + "px",
});
if (thumbnail) {
art.plugins.add(
artplayerPluginVttThumbnail({
vtt: `${proxy}${thumbnail}`,
})
);
}
const $rewind = art.layers["rewind"];
const $forward = art.layers["forward"];
Artplayer.utils.isMobile &&
art.proxy($rewind, "dblclick", () => {
art.currentTime = Math.max(0, art.currentTime - 10);
art.layers["backwardIcon"].style.opacity = 1;
setTimeout(() => {
art.layers["backwardIcon"].style.opacity = 0;
}, 300);
});
Artplayer.utils.isMobile &&
art.proxy($forward, "dblclick", () => {
art.currentTime = Math.max(0, art.currentTime + 10);
art.layers["forwardIcon"].style.opacity = 1;
setTimeout(() => {
art.layers["forwardIcon"].style.opacity = 0;
}, 300);
});
if (subtitles?.length > 0) {
const defaultEnglishSub =
subtitles.find((sub) => sub.label.toLowerCase() === "english" && sub.default) ||
subtitles.find((sub) => sub.label.toLowerCase() === "english");
art.setting.add({
name: "captions",
icon: captionIcon,
html: "Subtitle",
tooltip: defaultEnglishSub?.label || "default",
position: "right",
selector: [
{
html: "Display",
switch: true,
onSwitch: (item) => {
item.tooltip = item.switch ? "Hide" : "Show";
art.subtitle.show = !item.switch;
return !item.switch;
},
},
...subtitles.map((sub) => ({
default: sub.label.toLowerCase() === "english" && sub === defaultEnglishSub,
html: sub.label,
url: sub.file,
})),
],
onSelect: (item) => {
art.subtitle.switch(item.url, { name: item.html });
return item.html;
},
});
}
});
return () => {
if (art && art.destroy) {
art.destroy(false);
}
document.removeEventListener("keydown", handleKeydown);
const continueWatching = JSON.parse(localStorage.getItem("continueWatching")) || [];
const newEntry = {
id: animeInfo?.id,
data_id: animeInfo?.data_id,
episodeId,
episodeNum,
adultContent: animeInfo?.adultContent,
poster: animeInfo?.poster,
title: animeInfo?.title,
japanese_title: animeInfo?.japanese_title,
leftAt: leftAtRef.current,
};
if (!newEntry.data_id) return;
const existingIndex = continueWatching.findIndex((item) => item.data_id === newEntry.data_id);
if (existingIndex !== -1) {
continueWatching[existingIndex] = newEntry;
} else {
continueWatching.push(newEntry);
}
localStorage.setItem("continueWatching", JSON.stringify(continueWatching));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [streamUrl, subtitles, intro, outro]);
return <div ref={artRef} className="w-full h-full"></div>;
}

View File

@@ -1,103 +0,0 @@
const backward10Icon = `<svg viewBox="-5 -10 75 75" xmlns="http://www.w3.org/2000/svg" width="35" height="35">
<path d="M11.9199 45H7.20508V26.5391L2.60645 28.3154V24.3975L11.4219 20.7949H11.9199V45ZM30.1013 35.0059C30.1013 38.3483 29.4926 40.9049 28.2751 42.6758C27.0687 44.4466 25.3422 45.332 23.0954 45.332C20.8708 45.332 19.1498 44.4743 17.9323 42.7588C16.726 41.0322 16.1006 38.5641 16.0564 35.3545V30.7891C16.0564 27.4577 16.6596 24.9121 17.8659 23.1523C19.0723 21.3815 20.8044 20.4961 23.0622 20.4961C25.32 20.4961 27.0521 21.3704 28.2585 23.1191C29.4649 24.8678 30.0792 27.3636 30.1013 30.6064V35.0059ZM25.3864 30.1084C25.3864 28.2048 25.1983 26.777 24.822 25.8252C24.4457 24.8734 23.8591 24.3975 23.0622 24.3975C21.5681 24.3975 20.7933 26.1406 20.738 29.627V35.6533C20.738 37.6012 20.9262 39.0511 21.3025 40.0029C21.6898 40.9548 22.2875 41.4307 23.0954 41.4307C23.8591 41.4307 24.4236 40.988 24.7888 40.1025C25.1651 39.2061 25.3643 37.8392 25.3864 36.002V30.1084Z" fill="white"/>
<path d="M11.9894 5.45398V0L2 7.79529L11.9894 15.5914V10.3033H47.0886V40.1506H33.2442V45H52V5.45398H11.9894Z" fill="white"/>
</svg>`;
const forward10Icon = `
<svg viewBox="-5 -10 75 75" xmlns="http://www.w3.org/2000/svg" width="35" height="35">
<path d="M29.9199 45H25.2051V26.5391L20.6064 28.3154V24.3975L29.4219 20.7949H29.9199V45ZM48.1013 35.0059C48.1013 38.3483 47.4926 40.9049 46.2751 42.6758C45.0687 44.4466 43.3422 45.332 41.0954 45.332C38.8708 45.332 37.1498 44.4743 35.9323 42.7588C34.726 41.0322 34.1006 38.5641 34.0564 35.3545V30.7891C34.0564 27.4577 34.6596 24.9121 35.8659 23.1523C37.0723 21.3815 38.8044 20.4961 41.0622 20.4961C43.32 20.4961 45.0521 21.3704 46.2585 23.1191C47.4649 24.8678 48.0792 27.3636 48.1013 30.6064V35.0059ZM43.3864 30.1084C43.3864 28.2048 43.1983 26.777 42.822 25.8252C42.4457 24.8734 41.8591 24.3975 41.0622 24.3975C39.5681 24.3975 38.7933 26.1406 38.738 29.627V35.6533C38.738 37.6012 38.9262 39.0511 39.3025 40.0029C39.6898 40.9548 40.2875 41.4307 41.0954 41.4307C41.8591 41.4307 42.4236 40.988 42.7888 40.1025C43.1651 39.2061 43.3643 37.8392 43.3864 36.002V30.1084Z" fill="white"/>
<path d="M40.0106 5.45398V0L50 7.79529L40.0106 15.5914V10.3033H4.9114V40.1506H18.7558V45H2.01875e-06V5.45398H40.0106Z" fill="white"/>
</svg>`;
const forwardIcon = `<svg viewBox="0 0 512 512" width="30" height="30">
<path d="M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"/>
</svg>`;
const backwardIcon = `<svg viewBox="0 0 512 512" width="30" height="30" transform="scale(-1, 1)">
<path d="M500.5 231.4l-192-160C287.9 54.3 256 68.6 256 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2zm-256 0l-192-160C31.9 54.3 0 68.6 0 96v320c0 27.4 31.9 41.8 52.5 24.6l192-160c15.3-12.8 15.3-36.4 0-49.2z"/>
</svg>`;
const volumeIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="24" height="24"><path d="M116.5,42.8v154.4c0,2.8-1.7,3.6-3.8,1.7l-54.1-48H29c-2.8,0-5.2-2.3-5.2-5.2V94.3c0-2.8,2.3-5.2,5.2-5.2h29.6l54.1-48C114.8,39.2,116.5,39.9,116.5,42.8z"/><path d="M136.2,160v-20c11.1,0,20-8.9,20-20s-8.9-20-20-20V80c22.1,0,40,17.9,40,40S158.3,160,136.2,160z"/><path d="M216.2,120c0-44.2-35.8-80-80-80v20c33.1,0,60,26.9,60,60s-26.9,60-60,60v20C180.4,199.9,216.1,164.1,216.2,120z" fill="#fff"/></svg>`;
const muteIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="24" height="24">
<path d="M116.4,42.8v154.5c0,2.8-1.7,3.6-3.8,1.7l-54.1-48.1H28.9c-2.8,0-5.2-2.3-5.2-5.2V94.2c0-2.8,2.3-5.2,5.2-5.2h29.6l54.1-48.1C114.6,39.1,116.4,39.9,116.4,42.8z M212.3,96.4l-14.6-14.6l-23.6,23.6l-23.6-23.6l-14.6,14.6l23.6,23.6l-23.6,23.6l14.6,14.6l23.6-23.6l23.6,23.6l14.6-14.6L188.7,120L212.3,96.4z"
fill="#fff"/>
</svg>
`;
const captionIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 16 240 240" width="28" height="28">
<path d="M215,40H25c-2.7,0-5,2.2-5,5v150c0,2.7,2.2,5,5,5h190c2.7,0,5-2.2,5-5V45C220,42.2,217.8,40,215,40z M108.1,137.7c0.7-0.7,1.5-1.5,2.4-2.3l6.6,7.8c-2.2,2.4-5,4.4-8,5.8c-8,3.5-17.3,2.4-24.3-2.9c-3.9-3.6-5.9-8.7-5.5-14v-25.6c0-2.7,0.5-5.3,1.5-7.8c0.9-2.2,2.4-4.3,4.2-5.9c5.7-4.5,13.2-6.2,20.3-4.6c3.3,0.5,6.3,2,8.7,4.3c1.3,1.3,2.5,2.6,3.5,4.2l-7.1,6.9c-2.4-3.7-6.5-5.9-10.9-5.9c-2.4-0.2-4.8,0.7-6.6,2.3c-1.7,1.7-2.5,4.1-2.4,6.5v25.6C90.4,141.7,102,143.5,108.1,137.7z M152.9,137.7c0.7-0.7,1.5-1.5,2.4-2.3l6.6,7.8c-2.2,2.4-5,4.4-8,5.8c-8,3.5-17.3,2.4-24.3-2.9c-3.9-3.6-5.9-8.7-5.5-14v-25.6c0-2.7,0.5-5.3,1.5-7.8c0.9-2.2,2.4-4.3,4.2-5.9c5.7-4.5,13.2-6.2,20.3-4.6c3.3,0.5,6.3,2,8.7,4.3c1.3,1.3,2.5,2.6,3.5,4.2l-7.1,6.9c-2.4-3.7-6.5-5.9-10.9-5.9c-2.4-0.2-4.8,0.7-6.6,2.3c-1.7,1.7-2.5,4.1-2.4,6.5v25.6C135.2,141.7,146.8,143.5,152.9,137.7z"
fill="#fff"/>
</svg>
`;
const captionOffIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 16 240 240" width="28" height="28"><path d="M99.4,97.8c-2.4-0.2-4.8,0.7-6.6,2.3c-1.7,1.7-2.5,4.1-2.4,6.5v25.6c0,9.6,11.6,11.4,17.7,5.5c0.7-0.7,1.5-1.5,2.4-2.3l6.6,7.8c-2.2,2.4-5,4.4-8,5.8c-8,3.5-17.3,2.4-24.3-2.9c-3.9-3.6-5.9-8.7-5.5-14v-25.6c0-2.7,0.5-5.3,1.5-7.8c0.9-2.2,2.4-4.3,4.2-5.9c5.7-4.5,13.2-6.2,20.3-4.6c3.3,0.5,6.3,2,8.7,4.3c1.3,1.3,2.5,2.6,3.5,4.2l-7.1,6.9C107.9,100,103.8,97.8,99.4,97.8z M144.1,97.8c-2.4-0.2-4.8,0.7-6.6,2.3c-1.7,1.7-2.5,4.1-2.4,6.5v25.6c0,9.6,11.6,11.4,17.7,5.5c0.7-0.7,1.5-1.5,2.4-2.3l6.6,7.8c-2.2,2.4-5,4.4-8,5.8c-8,3.5-17.3,2.4-24.3-2.9c-3.9-3.6-5.9-8.7-5.5-14v-25.6c0-2.7,0.5-5.3,1.5-7.8c0.9-2.2,2.4-4.3,4.2-5.9c5.7-4.5,13.2-6.2,20.3-4.6c3.3,0.5,6.3,2,8.7,4.3c1.3,1.3,2.5,2.6,3.5,4.2l-7.1,6.9C152.6,100,148.5,97.8,144.1,97.8L144.1,97.8z M200,60v120H40V60H200 M215,40H25c-2.7,0-5,2.2-5,5v150c0,2.7,2.2,5,5,5h190c2.7,0,5-2.2,5-5V45C220,42.2,217.8,40,215,40z" fill="#fff"/></svg>`;
const pipOffIcon = `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20 5.75V9.75H22V4.78C22 4.21116 21.5389 3.75 20.97 3.75H2.03C1.46116 3.75 1 4.21113 1 4.78V17.72C1 18.2889 1.46119 18.75 2.03 18.75H12V16.75H3V5.75H20ZM14 13.25C14 12.6977 14.4477 12.25 15 12.25H22C22.5523 12.25 23 12.6977 23 13.25V19.25C23 19.8023 22.5523 20.25 22 20.25H15C14.4477 20.25 14 19.8023 14 19.25V13.25ZM10 9.25L8.20711 11.0429L10.7071 13.5429L9.29289 14.9571L6.79289 12.4571L5 14.25V9.25H10Z" fill="#fff"/>
</svg>`;
const loadingIcon = `<svg width="80" height="80" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_l9ve{animation:spinner_rcyq 1.2s cubic-bezier(0.52,.6,.25,.99) infinite}.spinner_cMYp{animation-delay:.4s}.spinner_gHR3{animation-delay:.8s}@keyframes spinner_rcyq{0%{transform:translate(12px,12px) scale(0);opacity:1}100%{transform:translate(0,0) scale(1);opacity:0}}</style><path class="spinner_l9ve" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/><path class="spinner_l9ve spinner_cMYp" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/><path class="spinner_l9ve spinner_gHR3" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z" transform="translate(12, 12) scale(0)"/></svg>`;
const pipIcon = `<svg width="24" height="24" viewBox="0 0 24 24" style="margin-bottom: 3px; vertical-align: middle;" xmlns="http://www.w3.org/2000/svg">
<path d="M20 5.125V9.125H22V4.155C22 3.58616 21.5389 3.125 20.97 3.125H2.03C1.46116 3.125 1 3.58613 1 4.155V17.095C1 17.6639 1.46119 18.125 2.03 18.125H12V16.125H3V5.125H20ZM14 11.875C14 11.3227 14.4477 10.875 15 10.875H22C22.5523 10.875 23 11.3227 23 11.875V17.875C23 18.4273 22.5523 18.875 22 18.875H15C14.4477 18.875 14 18.4273 14 17.875V11.875ZM6 12.375L7.79289 10.5821L5.29288 8.0821L6.7071 6.66788L9.20711 9.16789L11 7.375V12.375H6Z" fill="white"/>
</svg>`;
const playIconLg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="80" height="80"><path d="M62.8,199.5c-1,0.8-2.4,0.6-3.3-0.4c-0.4-0.5-0.6-1.1-0.5-1.8V42.6c-0.2-1.3,0.7-2.4,1.9-2.6c0.7-0.1,1.3,0.1,1.9,0.4l154.7,77.7c2.1,1.1,2.1,2.8,0,3.8L62.8,199.5z" fill="white"/></svg>`;
const playIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="24" height="24"><path d="M62.8,199.5c-1,0.8-2.4,0.6-3.3-0.4c-0.4-0.5-0.6-1.1-0.5-1.8V42.6c-0.2-1.3,0.7-2.4,1.9-2.6c0.7-0.1,1.3,0.1,1.9,0.4l154.7,77.7c2.1,1.1,2.1,2.8,0,3.8L62.8,199.5z"/></svg>`;
const pauseIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="24" height="24"><path d="M100,194.9c0.2,2.6-1.8,4.8-4.4,5c-0.2,0-0.4,0-0.6,0H65c-2.6,0.2-4.8-1.8-5-4.4c0-0.2,0-0.4,0-0.6V45c-0.2-2.6,1.8-4.8,4.4-5c0.2,0,0.4,0,0.6,0h30c2.6-0.2,4.8,1.8,5,4.4c0,0.2,0,0.4,0,0.6V194.9z M180,45.1c0.2-2.6-1.8-4.8-4.4-5c-0.2,0-0.4,0-0.6,0h-30c-2.6-0.2-4.8,1.8-5,4.4c0,0.2,0,0.4,0,0.6V195c-0.2,2.6,1.8,4.8,4.4,5c0.2,0,0.4,0,0.6,0h30c2.6,0.2,4.8-1.8,5-4.4c0-0.2,0-0.4,0-0.6V45.1z"/></svg>`;
const uploadIcon = `
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
width="24"
height="24"
style="vertical-align: middle;">
<path fill-rule="evenodd" d="M8 0a5.53 5.53 0 0 0-3.594 1.342c-.766.66-1.321 1.52-1.464 2.383C1.266 4.095 0 5.555 0 7.318 0 9.366 1.708 11 3.781 11H7.5V5.707L5.354 7.854a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V11h4.188C14.502 11 16 9.57 16 7.773c0-1.636-1.242-2.969-2.834-3.194C12.923 1.999 10.69 0 8 0m-.5 14.5V11h1v3.5a.5.5 0 0 1-1 0"/>
</svg>`;
const settingsIcon = `
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 240 240"
width="24"
height="24"
style="margin-bottom: 5px; vertical-align: middle;">
<path d="M204,145l-25-14c0.8-3.6,1.2-7.3,1-11c0.2-3.7-0.2-7.4-1-11l25-14c2.2-1.6,3.1-4.5,2-7l-16-26c-1.2-2.1-3.8-2.9-6-2l-25,14c-6-4.2-12.3-7.9-19-11V35c0.2-2.6-1.8-4.8-4.4-5c-0.2,0-0.4,0-0.6,0h-30c-2.6-0.2-4.8,1.8-5,4.4c0,0.2,0,0.4,0,0.6v28c-6.7,3.1-13,6.7-19,11L56,60c-2.2-0.9-4.8-0.1-6,2L35,88c-1.6,2.2-1.3,5.3,0.9,6.9c0,0,0.1,0,0.1,0.1l25,14c-0.8,3.6-1.2,7.3-1,11c-0.2,3.7,0.2,7.4,1,11l-25,14c-2.2,1.6-3.1,4.5-2,7l16,26c1.2,2.1,3.8,2.9,6,2l25-14c5.7,4.6,12.2,8.3,19,11v28c-0.2,2.6,1.8,4.8,4.4,5c0.2,0,0.4,0,0.6,0h30c2.6,0.2,4.8-1.8,5-4.4c0-0.2,0-0.4,0-0.6v-28c7-2.3,13.5-6,19-11l25,14c2.5,1.3,5.6,0.4,7-2l15-26C206.7,149.4,206,146.7,204,145z M120,149.9c-16.5,0-30-13.4-30-30s13.4-30,30-30s30,13.4,30,30c0.3,16.3-12.6,29.7-28.9,30C120.7,149.9,120.4,149.9,120,149.9z"/>
</svg>`;
const fullScreenOnIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="24" height="24" style="margin-bottom: 5px; vertical-align: middle;"><path d="M96.3,186.1c1.9,1.9,1.3,4-1.4,4.4l-50.6,8.4c-1.8,0.5-3.7-0.6-4.2-2.4c-0.2-0.6-0.2-1.2,0-1.7l8.4-50.6c0.4-2.7,2.4-3.4,4.4-1.4l14.5,14.5l28.2-28.2l14.3,14.3l-28.2,28.2L96.3,186.1z M195.8,39.1l-50.6,8.4c-2.7,0.4-3.4,2.4-1.4,4.4l14.5,14.5l-28.2,28.2l14.3,14.3l28.2-28.2l14.5,14.5c1.9,1.9,4,1.3,4.4-1.4l8.4-50.6c0.5-1.8-0.6-3.6-2.4-4.2C197,39,196.4,39,195.8,39.1L195.8,39.1z" fill="#fff"/></svg>`;
const fullScreenOffIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240"width="24" height="24" style="margin-bottom: 5px; vertical-align: middle;"><path d="M109.2,134.9l-8.4,50.1c-0.4,2.7-2.4,3.3-4.4,1.4L82,172l-27.9,27.9l-14.2-14.2l27.9-27.9l-14.4-14.4c-1.9-1.9-1.3-3.9,1.4-4.4l50.1-8.4c1.8-0.5,3.6,0.6,4.1,2.4C109.4,133.7,109.4,134.3,109.2,134.9L109.2,134.9z M172.1,82.1L200,54.2L185.8,40l-27.9,27.9l-14.4-14.4c-1.9-1.9-3.9-1.3-4.4,1.4l-8.4,50.1c-0.5,1.8,0.6,3.6,2.4,4.1c0.5,0.2,1.2,0.2,1.7,0l50.1-8.4c2.7-0.4,3.3-2.4,1.4-4.4L172.1,82.1z"/></svg>`;
const logo = `<p style="display: flex; gap: 7px; align-items: center; background-color:#1F2020; padding:5px;padding-inline:7px; border-radius:5px">
<b style="color: #ffbade;">Powered by</b>
<span style="font-size: 14px;">
Zen<span style="color: #ffbade;">!</span>me
</span>
</p>
`;
export {
backward10Icon,
forward10Icon,
backwardIcon,
forwardIcon,
playIcon,
playIconLg,
pauseIcon,
loadingIcon,
uploadIcon,
settingsIcon,
pipIcon,
pipOffIcon,
volumeIcon,
muteIcon,
captionIcon,
captionOffIcon,
fullScreenOnIcon,
fullScreenOffIcon,
logo,
};

View File

@@ -1,72 +0,0 @@
import getVttArray from "./getVttArray";
export default function artplayerPluginVttThumbnail(option) {
return async (art) => {
const {
constructor: {
utils: { setStyle, isMobile, addClass },
},
template: { $progress },
} = art;
let timer = null;
const thumbnails = await getVttArray(option.vtt);
function showThumbnails($control, find, width) {
setStyle($control, "backgroundImage", `url(${find.url})`);
setStyle($control, "height", `${find.h}px`);
setStyle($control, "width", `${find.w}px`);
setStyle($control, "backgroundPosition", `-${find.x}px -${find.y}px`);
if (width <= find.w / 2) {
setStyle($control, "left", 0);
} else if (width > $progress.clientWidth - find.w / 2) {
setStyle($control, "left", `${$progress.clientWidth - find.w}px`);
} else {
setStyle($control, "left", `${width - find.w / 2}px`);
}
}
art.controls.add({
name: "vtt-thumbnail",
position: "top",
index: 20,
style: option.style || {},
mounted($control) {
addClass($control, "art-control-thumbnails");
art.on("setBar", async (type, percentage, event) => {
const isMobileDroging = type === "played" && event && isMobile;
if (type === "hover" || isMobileDroging) {
const width = $progress.clientWidth * percentage;
const second = percentage * art.duration;
setStyle($control, "display", "flex");
const find = thumbnails.find(
(item) => second >= item.start && second <= item.end
);
if (!find) return setStyle($control, "display", "none");
if (width > 0 && width < $progress.clientWidth) {
showThumbnails($control, find, width);
} else {
if (!isMobile) {
setStyle($control, "display", "none");
}
}
if (isMobileDroging) {
clearTimeout(timer);
timer = setTimeout(() => {
setStyle($control, "display", "none");
}, 500);
}
}
});
},
});
return {
name: "artplayerPluginVttThumbnail",
};
};
}

View File

@@ -1,211 +0,0 @@
import style from "./pluginChapterStyle.js";
export default function artplayerPluginChapter(option = {}) {
return (art) => {
const { $player } = art.template;
const { setStyle, append, clamp, query, isMobile, addClass, removeClass } =
art.constructor.utils;
const html = `
<div class="art-chapter">
<div class="art-chapter-inner">
<div class="art-progress-hover"></div>
<div class="art-progress-loaded"></div>
<div class="art-progress-played"></div>
</div>
</div>
`;
let titleTimer = null;
let $chapters = [];
const $progress = art.query(".art-control-progress");
const $inner = art.query(".art-control-progress-inner");
const $control = append($inner, '<div class="art-chapters"></div>');
const $title = append($inner, '<div class="art-chapter-title"></div>');
function showTitle({ $chapter, width }) {
const title = $chapter.dataset.title.trim();
if (title) {
setStyle($title, "display", "flex");
$title.innerText = title;
const titleWidth = $title.clientWidth;
if (width <= titleWidth / 2) {
setStyle($title, "left", 0);
} else if (width > $inner.clientWidth - titleWidth / 2) {
setStyle($title, "left", `${$inner.clientWidth - titleWidth}px`);
} else {
setStyle($title, "left", `${width - titleWidth / 2}px`);
}
} else {
setStyle($title, "display", "none");
}
}
function update(chapters = []) {
$chapters = [];
$control.innerText = "";
removeClass($player, "artplayer-plugin-chapter");
if (!Array.isArray(chapters)) return;
if (!chapters.length) return;
if (!art.duration) return;
chapters = chapters.sort((a, b) => a.start - b.start);
for (let i = 0; i < chapters.length; i++) {
const chapter = chapters[i];
const nextChapter = chapters[i + 1];
if (chapter.end === Infinity) {
chapter.end = art.duration;
}
if (
typeof chapter.start !== "number" ||
typeof chapter.end !== "number" ||
typeof chapter.title !== "string"
) {
throw new Error("Illegal chapter data type");
}
if (
chapter.start < 0 ||
chapter.end > Math.ceil(art.duration) ||
chapter.start >= chapter.end
) {
throw new Error("Illegal chapter time point");
}
if (nextChapter && chapter.end > nextChapter.start) {
throw new Error("Illegal chapter time point");
}
}
if (chapters[0].start > 0) {
chapters.unshift({ start: 0, end: chapters[0].start, title: "" });
}
if (chapters[chapters.length - 1].end < art.duration) {
chapters.push({
start: chapters[chapters.length - 1].end,
end: art.duration,
title: "",
});
}
for (let i = 0; i < chapters.length - 1; i++) {
if (chapters[i].end !== chapters[i + 1].start) {
chapters.splice(i + 1, 0, {
start: chapters[i].end,
end: chapters[i + 1].start,
title: "",
});
}
}
$chapters = chapters.map((chapter) => {
const $chapter = append($control, html);
const start = clamp(chapter.start, 0, art.duration);
const end = clamp(chapter.end, 0, art.duration);
const duration = end - start;
const percentage = duration / art.duration;
$chapter.dataset.start = start;
$chapter.dataset.end = end;
$chapter.dataset.duration = duration;
$chapter.dataset.title = chapter.title.trim();
$chapter.style.width = `${percentage * 100}%`;
return {
$chapter,
$hover: query(".art-progress-hover", $chapter),
$loaded: query(".art-progress-loaded", $chapter),
$played: query(".art-progress-played", $chapter),
};
});
addClass($player, "artplayer-plugin-chapter");
art.emit("setBar", "loaded", art.loaded || 0);
}
art.on("setBar", (type, percentage, event) => {
if (!$chapters.length) return;
for (let i = 0; i < $chapters.length; i++) {
const { $chapter, $loaded, $played, $hover } = $chapters[i];
const $target = {
hover: $hover,
loaded: $loaded,
played: $played,
}[type];
if (!$target) return;
const width = $control.clientWidth * percentage;
const currentTime = art.duration * percentage;
const duration = parseFloat($chapter.dataset.duration);
const start = parseFloat($chapter.dataset.start);
const end = parseFloat($chapter.dataset.end);
if (currentTime < start) {
setStyle($target, "width", 0);
}
if (currentTime > end) {
setStyle($target, "width", "100%");
}
if (currentTime >= start && currentTime <= end) {
const percentage = (currentTime - start) / duration;
setStyle($target, "width", `${percentage * 100}%`);
if (isMobile) {
if (type === "played" && event) {
showTitle({ $chapter, width });
clearTimeout(titleTimer);
titleTimer = setTimeout(() => {
setStyle($title, "display", "none");
}, 500);
}
} else {
if (type === "hover") {
showTitle({ $chapter, width });
}
}
}
}
});
if (!isMobile) {
art.proxy($progress, "mouseleave", () => {
if (!$chapters.length) return;
setStyle($title, "display", "none");
});
}
art.once("video:loadedmetadata", () => update(option.chapters));
return {
name: "artplayerPluginChapter",
update: ({ chapters }) => update(chapters),
};
};
}
if (typeof document !== "undefined") {
const id = "artplayer-plugin-chapter";
const $style = document.getElementById(id);
if ($style) {
$style.textContent = style;
} else {
const $style = document.createElement("style");
$style.id = id;
$style.textContent = style;
document.head.appendChild($style);
}
}
if (typeof window !== "undefined") {
window["artplayerPluginChapter"] = artplayerPluginChapter;
}

View File

@@ -1,49 +0,0 @@
import { uploadIcon } from "./PlayerIcons";
export default function artplayerPluginUploadSubtitle() {
return (art) => {
const { getExt } = art.constructor.utils;
art.setting.add({
html: `
<div class="subtitle-upload-wrapper" style="position: relative;">
<input
type="file"
name="subtitle-upload"
id="subtitle-upload"
style="display: none;"
/>
<label
for="subtitle-upload"
class="subtitle-upload-label"
style="cursor: pointer; user-select: none;"
>
Upload Subtitle
</label>
</div>
`,
icon: uploadIcon,
onClick(setting, $setting) {
const $input = $setting.querySelector("input[name='subtitle-upload']");
const $label = $setting.querySelector(".subtitle-upload-label");
art.proxy($input, "change", (event) => {
const file = event.target?.files?.[0];
if (!file) return;
const url = URL.createObjectURL(file);
art.subtitle.switch(url, {
type: getExt(file.name),
});
event.target.value = null;
// Update UI
$label.textContent = file.name;
art.notice.show = `Upload Subtitle ${file.name}`;
setting.tooltip = file.name;
});
},
});
};
}

View File

@@ -1,74 +0,0 @@
export default function autoSkip(option) {
function validateRanges(ranges) {
if (!Array.isArray(ranges)) {
throw new TypeError("Option must be an array of time ranges");
}
ranges.forEach((range, index) => {
if (!Array.isArray(range) || range.length !== 2) {
throw new TypeError(
`Range at index ${index} must be an array of two numbers`
);
}
const [start, end] = range;
if (
typeof start !== "number" ||
(typeof end !== "number" && end !== Infinity)
) {
throw new TypeError(
`Range at index ${index} must contain valid numbers or Infinity`
);
}
if (start > end && end !== Infinity) {
throw new RangeError(
`In range at index ${index}, start time must be less than end time`
);
}
if (index > 0) {
const prevEnd = ranges[index - 1][1];
if (prevEnd !== Infinity && start <= prevEnd) {
throw new RangeError(
`Range at index ${index} overlaps with the previous range`
);
}
}
});
}
validateRanges(option);
return (art) => {
let skipRanges = option;
function updateRanges() {
const duration = art.duration;
skipRanges = skipRanges.map(([start, end]) => [
start,
end === Infinity ? duration : end,
]);
}
function checkAndSkip() {
const currentTime = art.currentTime;
for (const [start, end] of skipRanges) {
if (currentTime >= start && currentTime < end) {
art.seek = end;
break;
}
}
}
art.on("video:timeupdate", checkAndSkip);
art.on("video:loadedmetadata", updateRanges);
return {
name: "autoSkip",
update(newOption = []) {
validateRanges(newOption);
skipRanges = newOption;
updateRanges();
},
};
};
}

View File

@@ -1,82 +0,0 @@
export default function getChapterStyles(intro, outro) {
let styles = `
.art-chapters {
gap: 0px !important;
}
`;
if (intro && outro) {
if (
intro.start === 0 &&
intro.end === 0 &&
outro.start === 0 &&
outro.end === 0
) {
styles += ``;
} else if (
intro.start === 0 &&
intro.end === 0 &&
outro.start !== 0 &&
outro.end !== 0
) {
styles += `
.art-chapter:nth-child(2) {
background-color: #fdd253;
transform: scaleY(0.6);
}
`;
} else if (
intro.start === 0 &&
intro.end !== 0 &&
outro.start === 0 &&
outro.end === 0
) {
styles += `
.art-chapter:nth-child(1){
background-color: #fdd253;
transform: scaleY(0.6);
}
`;
} else if (
intro.start === 0 &&
intro.end !== 0 &&
outro.start !== 0 &&
outro.end !== 0
) {
styles += `
.art-chapter:nth-child(1),
.art-chapter:nth-child(3) {
background-color: #fdd253;
transform: scaleY(0.6);
}
`;
} else if (
intro.start !== 0 &&
intro.end !== 0 &&
outro.start === 0 &&
outro.end === 0
) {
styles += `
.art-chapter:nth-child(2) {
background-color: #fdd253;
transform: scaleY(0.6);
}
`;
} else if (
intro.start !== 0 &&
intro.end !== 0 &&
outro.start !== 0 &&
outro.end !== 0
) {
styles += `
.art-chapter:nth-child(2),
.art-chapter:nth-child(4) {
background-color: #fdd253;
transform: scaleY(0.6);
}
`;
}
}
return styles;
}

View File

@@ -1,101 +0,0 @@
function padEnd(str, targetLength, padString) {
if (str.length > targetLength) {
return String(str);
} else {
targetLength = targetLength - str.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
return String(str) + padString.slice(0, targetLength);
}
}
function t2d(time) {
var arr = time.split(".");
var left = arr[0].split(":") || [];
var right = padEnd(arr[1] || "0", 3, "0");
var ms = Number(right) / 1000;
var h = Number(left[left.length - 3] || 0) * 3600;
var m = Number(left[left.length - 2] || 0) * 60;
var s = Number(left[left.length - 1] || 0);
return h + m + s + ms;
}
export default async function getVttArray(vttUrl = "") {
const vttString = await (await fetch(vttUrl)).text();
let lines = vttString.split(/\r?\n/).filter((item) => item.trim());
const vttArray = [];
//checking if the WEBVTT header is present
const isWebVTTHeader = lines[0].trim().toUpperCase() === "WEBVTT";
let startIndex = 0;
let increment = 2;
// Check if the first line is an index line
const indexLineReg = /^\d+$/; // Regex to match lines containing only digits
if (!isWebVTTHeader && indexLineReg.test(lines[0].trim())) {
// console.log("WEBVTT not present but index line is present");
increment = 3; // Set increment to 3 if an index line is present
startIndex = 1; // Start from the second line
} else if (isWebVTTHeader) {
// If WEBVTT is present, check the next line
// console.log("WEBVTT lines is present checking if index line is present...");
const indexLine = lines[1];
if (indexLine && indexLineReg.test(indexLine.trim())) {
// console.log("Index line is present");
increment = 3; // Set increment to 3 if an index line is present
startIndex = 2; // Start from the line after the index
} else {
// console.log("Index line is not present");
startIndex = 1; // If no index line, start from the line after WEBVTT
increment = 2; // Set increment to 2
}
}
for (let i = startIndex; i < lines.length; i += increment) {
const time = lines[i];
const text = lines[i + 1];
if (!text.trim()) continue;
// console.log(`Processing time line: ${time}`); // Logging processing timestamps
const timeReg =
/((?:[0-9]{2}:)?(?:[0-9]{2}:)?[0-9]{2}(?:.[0-9]{3})?)(?: ?--> ?)((?:[0-9]{2}:)?(?:[0-9]{2}:)?[0-9]{2}(?:.[0-9]{3})?)/;
const timeMatch = time.match(timeReg);
if (!timeMatch) {
// console.warn(`Failed to match time: ${time}`); // Log failed matches
continue; // Skip to the next loop iteration if match fails
}
const textReg = /(.*)#(\w{4})=(.*)/i;
const textMatch = text.match(textReg);
const start = Math.floor(t2d(timeMatch[1]));
const end = Math.floor(t2d(timeMatch[2]));
let url = textMatch[1];
const isAbsoluteUrl = /^\/|((https?|ftp|file):\/\/)/i.test(url);
if (!isAbsoluteUrl) {
const urlArr = vttUrl.split("/");
urlArr.pop();
urlArr.push(url);
url = urlArr.join("/");
}
const result = { start, end, url };
const keys = textMatch[2].split("");
const values = textMatch[3].split(",");
for (let j = 0; j < keys.length; j++) {
result[keys[j]] = values[j];
}
vttArray.push(result);
}
return vttArray;
}

View File

@@ -1,55 +0,0 @@
export default `
.artplayer-plugin-chapter .art-control-progress-inner {
height: 100% !important;
background-color: transparent !important;
}
.artplayer-plugin-chapter .art-control-progress-inner > .art-progress-hover,
.artplayer-plugin-chapter .art-control-progress-inner > .art-progress-loaded,
.artplayer-plugin-chapter .art-control-progress-inner > .art-progress-played {
display: none !important;
}
.artplayer-plugin-chapter .art-control-thumbnails {
bottom: calc(var(--art-bottom-gap) + 64px) !important;
}
.artplayer-plugin-chapter .art-chapters {
position: absolute;
z-index: 0;
inset: 0;
display: flex;
align-items: center;
gap: 4px;
height: 100%;
transform: scaleY(1.25);
}
.artplayer-plugin-chapter .art-chapters .art-chapter {
display: flex;
align-items: center;
height: 100%;
}
.artplayer-plugin-chapter .art-chapters .art-chapter .art-chapter-inner {
position: relative;
cursor: pointer;
width: 100%;
height: 50%;
border-radius: 10px;
overflow: hidden;
transition: height var(--art-transition-duration) ease;
background-color: var(--art-progress-color);
}
.artplayer-plugin-chapter .art-chapters .art-chapter:hover .art-chapter-inner {
height: 100%;
}
.artplayer-plugin-chapter .art-chapter-title {
display: none;
position: absolute;
z-index: 70;
top: -50px;
left: 0;
padding: 3px 5px;
line-height: 1;
font-size: 14px;
border-radius: var(--art-border-radius);
white-space: nowrap;
background-color: var(--art-tip-background);
}
`;

View File

@@ -1,102 +0,0 @@
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import Error from "../error/Error";
import Topten from "../topten/Topten";
import Genre from "../genres/Genre";
import SidecardLoader from "../Loader/Sidecard.loader";
import PageSlider from "../pageslider/PageSlider";
import CategoryCard from "../categorycard/CategoryCard";
import { useEffect, useState } from "react";
import { useHomeInfo } from "@/src/context/HomeInfoContext";
import getProducer from "@/src/utils/getProducer.utils";
import Loader from "../Loader/Loader";
function Producer() {
const { id } = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const [producerInfo, setProducerInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [totalPages, setTotalPages] = useState(0);
const page = parseInt(searchParams.get("page")) || 1;
const { homeInfo, homeInfoLoading } = useHomeInfo();
const navigate = useNavigate();
useEffect(() => {
const fetchProducerInfo = async () => {
setLoading(true);
try {
const data = await getProducer(id, page);
setProducerInfo(data.data);
setTotalPages(data.totalPages);
setLoading(false);
} catch (err) {
setError(err);
console.error("Error fetching category info:", err);
}
};
fetchProducerInfo();
window.scrollTo(0, 0);
}, [id, page]);
if (loading) return <Loader type="producer" />;
if (error) {
navigate("/error-page");
return <Error />;
}
if (!producerInfo) {
navigate("/404-not-found-page");
return null;
}
const handlePageChange = (newPage) => {
setSearchParams({ page: newPage });
};
return (
<div className="w-full flex flex-col gap-y-4 mt-[100px] max-md:mt-[50px]">
{producerInfo ? (
<div className="w-full px-4 grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex max-[1200px]:flex-col max-[1200px]:gap-y-10">
{page > totalPages ? (
<p className="font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px] max-[300px]:leading-6">
You came a long way, go back <br className="max-[300px]:hidden" />
nothing is here
</p>
) : (
<div>
{producerInfo && (
<CategoryCard
label={
(id.charAt(0).toUpperCase() + id.slice(1))
.split("-")
.join(" ") + " Anime"
}
data={producerInfo}
showViewMore={false}
className={"mt-0"}
categoryPage={true}
/>
)}
<PageSlider
page={page}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
</div>
)}
<div className="w-full flex flex-col gap-y-10">
{homeInfoLoading ? (
<SidecardLoader />
) : (
<>
{homeInfo && homeInfo.topten && (
<Topten data={homeInfo.topten} className="mt-0" />
)}
{homeInfo?.genres && <Genre data={homeInfo.genres} />}
</>
)}
</div>
</div>
) : (
<Error />
)}
</div>
);
}
export default Producer;

View File

@@ -1,159 +0,0 @@
import BouncingLoader from "../ui/bouncingloader/Bouncingloader";
import getQtip from "@/src/utils/getQtip.utils";
import { useState, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPlay,
faStar,
faClosedCaptioning,
faMicrophone,
} from "@fortawesome/free-solid-svg-icons";
import { Link } from "react-router-dom";
function Qtip({ id }) {
const [qtip, setQtip] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchQtipInfo = async () => {
setLoading(true);
try {
const data = await getQtip(id);
setQtip(data);
} catch (err) {
console.error("Error fetching anime info:", err);
setError(err);
} finally {
setLoading(false);
}
};
fetchQtipInfo();
}, [id]);
return (
<div className="w-[320px] h-fit rounded-xl p-4 flex justify-center items-center bg-[#3e3c50] bg-opacity-70 backdrop-blur-[10px] z-50">
{loading || error || !qtip ? (
<BouncingLoader />
) : (
<div className="w-full flex flex-col justify-start gap-y-2">
<h1 className="text-xl font-semibold text-white text-[13px] leading-6">
{qtip.title}
</h1>
<div className="w-full flex items-center relative mt-2">
{qtip?.rating && (
<div className="flex gap-x-2 items-center">
<FontAwesomeIcon icon={faStar} className="text-[#ffc107]" />
<p className="text-[#b7b7b8]">{qtip.rating}</p>
</div>
)}
<div className="flex ml-4 gap-x-[1px] overflow-hidden rounded-md items-center h-fit">
{qtip?.quality && (
<div className="bg-[#ffbade] px-[7px] w-fit flex justify-center items-center py-[1px] text-black">
<p className="text-[12px] font-semibold">{qtip.quality}</p>
</div>
)}
<div className="flex gap-x-[1px] w-fit items-center py-[1px]">
{qtip?.subCount && (
<div className="flex gap-x-1 justify-center items-center bg-[#B0E3AF] px-[7px] text-black">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[13px]"
/>
<p className="text-[13px] font-semibold">{qtip.subCount}</p>
</div>
)}
{qtip?.dubCount && (
<div className="flex gap-x-1 justify-center items-center bg-[#B9E7FF] px-[7px] text-black">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[13px]"
/>
<p className="text-[13px] font-semibold">{qtip.dubCount}</p>
</div>
)}
{qtip?.episodeCount && (
<div className="flex gap-x-1 justify-center items-center bg-[#a199a3] px-[7px] text-black">
<p className="text-[13px] font-semibold">
{qtip.episodeCount}
</p>
</div>
)}
</div>
{qtip?.type && (
<div className="absolute right-0 top-0 justify-center items-center rounded-sm bg-[#ffbade] px-[6px] text-black">
<p className="font-semibold text-[13px]">{qtip.type}</p>
</div>
)}
</div>
</div>
{qtip?.description && (
<p className="text-[#d7d7d8] text-[13px] leading-4 font-light line-clamp-3 mt-1">
{qtip.description}
</p>
)}
<div className="flex flex-col mt-1">
{qtip?.japaneseTitle && (
<div className="leading-4">
<span className="text-[#b7b7b8] text-[13px]">
Japanese:&nbsp;
</span>
<span className="text-[13px]">{qtip.japaneseTitle}</span>
</div>
)}
{qtip?.Synonyms && (
<div className="leading-4">
<span className="text-[#b7b7b8] text-[13px]">
Synonyms:&nbsp;
</span>
<span className="text-[13px]">{qtip.Synonyms}</span>
</div>
)}
{qtip?.airedDate && (
<div className="leading-4">
<span className="text-[#b7b7b8] text-[13px]">Aired:&nbsp;</span>
<span className="text-[13px]">{qtip.airedDate}</span>
</div>
)}
{qtip?.status && (
<div className="leading-4">
<span className="text-[#b7b7b8] text-[13px]">
Status:&nbsp;
</span>
<span className="text-[13px]">{qtip.status}</span>
</div>
)}
{qtip?.genres && (
<div className="leading-4 flex flex-wrap text-wrap">
<span className="text-[#b7b7b8] text-[13px]">
Genres:&nbsp;
</span>
{qtip.genres.map((genre, index) => (
<Link
to={`/genre/${genre}`}
key={index}
className="text-[13px] hover:text-[#ffbade]"
>
<span>
{genre}
{index === qtip.genres.length - 1 ? "" : ","}&nbsp;
</span>
</Link>
))}
</div>
)}
</div>
<Link
to={qtip.watchLink}
className="w-[80%] flex mt-4 justify-center items-center gap-x-2 bg-[#ffbade] py-[9px] rounded-3xl"
>
<FontAwesomeIcon icon={faPlay} className="text-[14px] text-black" />
<p className="text-[14px] font-semibold text-black">Watch Now</p>
</Link>
</div>
)}
</div>
);
}
export default Qtip;

View File

@@ -1,241 +0,0 @@
import { useState, useEffect, useRef } from "react";
import getSchedInfo from "../../utils/getScheduleInfo.utils";
import { Pagination, Navigation } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
import BouncingLoader from "../ui/bouncingloader/Bouncingloader";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlay } from "@fortawesome/free-solid-svg-icons";
import "./schedule.css";
import { Link } from "react-router-dom";
const Schedule = () => {
const [dates, setDates] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [showAll, setShowAll] = useState(false);
const [currentActiveIndex, setCurrentActiveIndex] = useState(null);
const [scheduleData, setscheduleData] = useState([]);
const [currentTime, setCurrentTime] = useState(new Date());
const cardRefs = useRef([]);
const swiperRef = useRef(null);
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const monthName = currentDate.toLocaleString("default", { month: "short" });
const daysInMonth = new Date(year, month + 1, 0).getDate();
const GMTOffset = `GMT ${
new Date().getTimezoneOffset() > 0 ? "-" : "+"
}${String(Math.floor(Math.abs(new Date().getTimezoneOffset()) / 60)).padStart(
2,
"0"
)}:${String(Math.abs(new Date().getTimezoneOffset()) % 60).padStart(2, "0")}`;
const months = [];
useEffect(() => {
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const dayname = date.toLocaleString("default", { weekday: "short" });
const yearr = date.getFullYear();
const monthh = String(date.getMonth() + 1).padStart(2, "0");
const dayy = String(date.getDate()).padStart(2, "0");
const fulldate = `${yearr}-${monthh}-${dayy}`;
months.push({ day, monthName, dayname, fulldate });
}
setDates(months);
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const todayIndex = dates.findIndex(
(date) =>
date.fulldate ===
`${currentDate.getFullYear()}-${String(
currentDate.getMonth() + 1
).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}`
);
if (todayIndex !== -1) {
setCurrentActiveIndex(todayIndex);
toggleActive(todayIndex);
}
}, [dates]);
const fetchSched = async (date) => {
try {
setLoading(true);
// Check if cached data exists
const cachedData = localStorage.getItem(`schedule-${date}`);
if (cachedData) {
const parsedData = JSON.parse(cachedData);
setscheduleData(Array.isArray(parsedData) ? parsedData : []);
} else {
const data = await getSchedInfo(date);
setscheduleData(Array.isArray(data) ? data : []);
localStorage.setItem(`schedule-${date}`, JSON.stringify(data || []));
}
} catch (err) {
console.error("Error fetching schedule info:", err);
setError(err);
} finally {
setLoading(false);
}
};
const toggleActive = (index) => {
cardRefs.current.forEach((card) => {
if (card) {
card.classList.remove("active");
}
});
if (cardRefs.current[index]) {
cardRefs.current[index].classList.add("active");
if (dates[index] && dates[index].fulldate) {
fetchSched(dates[index].fulldate);
}
setCurrentActiveIndex(index);
}
};
const toggleShowAll = () => {
setShowAll(!showAll);
};
useEffect(() => {
setShowAll(false);
if (currentActiveIndex !== null && swiperRef.current) {
swiperRef.current.slideTo(currentActiveIndex);
}
}, [currentActiveIndex]);
return (
<>
<div className="w-full mt-[60px] max-[480px]:mt-[40px]">
<div className="flex items-center justify-between max-[570px]:flex-col max-[570px]:items-start max-[570px]:gap-y-2">
<div className="font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px]">
Estimated Schedule
</div>
<p className="leading-[28px] px-[10px] bg-white text-black rounded-full my-[6px] text-[16px] font-bold max-[478px]:text-[12px] max-[275px]:text-[10px]">
({GMTOffset}) {currentTime.toLocaleDateString()}{" "}
{currentTime.toLocaleTimeString()}
</p>
</div>
</div>
<div className="w-full overflow-x-scroll space-x-4 scrollbar-hide pt-10 px-6 max-[480px]:px-4 max-[478px]:pt-4">
<div className="relative w-full">
<Swiper
slidesPerView={3}
spaceBetween={2}
breakpoints={{
250: { slidesPerView: 3, spaceBetween: 10 },
640: { slidesPerView: 4, spaceBetween: 10 },
768: { slidesPerView: 5, spaceBetween: 10 },
1024: { slidesPerView: 7, spaceBetween: 10 },
1300: { slidesPerView: 7, spaceBetween: 15 },
}}
modules={[Pagination, Navigation]}
navigation={{
nextEl: ".next",
prevEl: ".prev",
}}
onSwiper={(swiper) => (swiperRef.current = swiper)}
>
{dates &&
dates.map((date, index) => (
<SwiperSlide key={index}>
<div
ref={(el) => (cardRefs.current[index] = el)}
onClick={() => toggleActive(index)}
className={`h-[70px] flex flex-col justify-center items-center w-full text-center rounded-xl shadow-lg cursor-pointer ${
currentActiveIndex === index
? "bg-[#ffbade] text-black"
: "bg-white bg-opacity-5 text-[#ffffff] hover:bg-[#373646] transition-all duration-300 ease-in-out"
}`}
>
<div className="text-[18px] font-bold max-[400px]:text-[14px] max-[350px]:text-[12px]">
{date.dayname}
</div>
<div
className={`text-[14px] max-[400px]:text-[12px] ${
currentActiveIndex === index
? "text-black"
: "text-gray-400"
} max-[350px]:text-[10px]`}
>
{date.monthName} {date.day}
</div>
</div>
</SwiperSlide>
))}
</Swiper>
<button className="next absolute top-1/2 right-[-15px] transform -translate-y-1/2 flex justify-center items-center cursor-pointer">
<FaChevronRight className="text-[12px]" />
</button>
<button className="prev absolute top-1/2 left-[-15px] transform -translate-y-1/2 flex justify-center items-center cursor-pointer">
<FaChevronLeft className="text-[12px]" />
</button>
</div>
</div>
{loading ? (
<div className="w-full h-[70px] flex justify-center items-center">
<BouncingLoader />
</div>
) : !scheduleData || scheduleData.length === 0 ? (
<div className="w-full h-[70px] flex justify-center items-center mt-5 text-xl">
No data to display
</div>
) : error ? (
<div className="w-full h-[70px] flex justify-center items-center mt-5 text-xl">
Something went wrong
</div>
) : (
<div className="flex flex-col mt-5 items-start">
{(showAll
? scheduleData
: Array.isArray(scheduleData)
? scheduleData.slice(0, 7)
: []
).map((item, idx) => (
<Link
to={`/${item.id}`}
key={idx}
className="w-full flex justify-between py-4 border-[#FFFFFF0D] border-b-[1px] group cursor-pointer max-[325px]:py-2"
>
<div className="flex items-center max-w-[500px] gap-x-7 max-[400px]:gap-x-2">
<div className="text-lg font-semibold text-[#ffffff59] group-hover:text-[#ffbade] transition-all duration-300 ease-in-out max-[600px]:text-[14px] max-[275px]:text-[12px]">
{item.time || "N/A"}
</div>
<h3 className="text-[17px] font-semibold line-clamp-1 group-hover:text-[#ffbade] transition-all duration-300 ease-in-out max-[600px]:text-[14px] max-[275px]:text-[12px]">
{item.title || "N/A"}
</h3>
</div>
<button className="max-w-[150px] flex items-center py-1 px-4 rounded-lg gap-x-2 group-hover:bg-[#ffbade] transition-all duration-300 ease-in-out">
<FontAwesomeIcon
icon={faPlay}
className="mt-[1px] text-[10px] max-[320px]:text-[8px] group-hover:text-black transition-all duration-300 ease-in-out"
/>
<p className="text-[14px] text-white group-hover:text-black transition-all duration-300 ease-in-out max-[275px]:text-[12px]">
Episode {item.episode_no || "N/A"}
</p>
</button>
</Link>
))}
{scheduleData.length > 7 && (
<button
onClick={toggleShowAll}
className="text-white py-4 hover:text-[#ffbade] font-semibold transition-all duration-300 ease-in-out max-sm:text-[13px]"
>
{showAll ? "Show Less" : "Show More"}
</button>
)}
</div>
)}
</>
);
};
export default Schedule;

View File

@@ -1,11 +0,0 @@
.next,
.prev {
width: 30px;
height: 30px;
border-radius: 100%;
background-color: white;
color: black;
font-size: 13px;
padding: 10px;
z-index: 10;
}

View File

@@ -1,73 +0,0 @@
import Suggestion from '../suggestion/Suggestion';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons';
import useSearch from '@/src/hooks/useSearch';
import { useNavigate } from 'react-router-dom';
function MobileSearch() {
const navigate = useNavigate();
const {
isSearchVisible,
searchValue,
setSearchValue,
isFocused,
setIsFocused,
debouncedValue,
suggestionRefs,
addSuggestionRef,
} = useSearch();
const handleSearchClick = () => {
if (searchValue.trim() && window.innerWidth <= 600) {
navigate(`/search?keyword=${encodeURIComponent(searchValue)}`);
}
};
return (
<>
{isSearchVisible && (
<div className="flex w-full mt-2 relative custom-md:hidden ">
<input
type="text"
className="bg-white px-4 py-2 text-black focus:outline-none w-full rounded-l-md"
placeholder="Search anime..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => {
setTimeout(() => {
const isInsideSuggestionBox = suggestionRefs.current.some(
(ref) => ref && ref.contains(document.activeElement),
);
if (!isInsideSuggestionBox) {
setIsFocused(false);
}
}, 100);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSearchClick();
}
}}
/>
<button className="flex items-center justify-center p-2 bg-white rounded-r-md"
onClick={handleSearchClick}
>
<FontAwesomeIcon
icon={faMagnifyingGlass}
className="text-black text-lg"
/>
</button>
{searchValue.trim() && isFocused && (
<div
ref={addSuggestionRef}
className="absolute z-[100000] top-full w-full"
>
<Suggestion keyword={debouncedValue} className="w-full" />
</div>
)}
</div>
)}
</>
);
}
export default MobileSearch;

View File

@@ -1,77 +0,0 @@
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Suggestion from "../suggestion/Suggestion";
import useSearch from "@/src/hooks/useSearch";
import { useNavigate } from "react-router-dom";
function WebSearch() {
const navigate = useNavigate();
const {
setIsSearchVisible,
searchValue,
setSearchValue,
isFocused,
setIsFocused,
debouncedValue,
suggestionRefs,
addSuggestionRef,
} = useSearch();
const handleSearchClick = () => {
if (window.innerWidth <= 600) {
setIsSearchVisible((prev) => !prev);
}
if (searchValue.trim() && window.innerWidth > 600) {
navigate(`/search?keyword=${encodeURIComponent(searchValue)}`);
}
};
return (
<div className="flex items-center relative w-[380px] max-[600px]:w-fit">
<input
type="text"
className="bg-white px-4 py-2 text-black focus:outline-none w-full max-[600px]:hidden"
placeholder="Search anime..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => {
setTimeout(() => {
const isInsideSuggestionBox = suggestionRefs.current.some(
(ref) => ref && ref.contains(document.activeElement),
);
if (!isInsideSuggestionBox) {
setIsFocused(false);
}
}, 100);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (searchValue.trim()) {
navigate(`/search?keyword=${encodeURIComponent(searchValue)}`);
}
}
}}
/>
<button
className="bg-white p-2 max-[600px]:bg-transparent focus:outline-none max-[600px]:p-0"
onClick={handleSearchClick}
>
<FontAwesomeIcon
icon={faMagnifyingGlass}
className="text-lg text-black hover:text-[#ffbade] max-[600px]:text-white max-[600px]:text-2xl max-[575px]:text-xl max-[600px]:mt-[7px]"
/>
</button>
{searchValue.trim() && isFocused && (
<div
ref={addSuggestionRef}
className="absolute z-[100000] top-full w-full"
>
<Suggestion keyword={debouncedValue} className="w-full" />
</div>
)}
</div>
);
}
export default WebSearch;

View File

@@ -1,9 +0,0 @@
.servers {
border-bottom: 1px dashed #35373d;
}
.servers:only-child {
border-bottom: none;
}
.servers:last-child {
border-bottom: none;
}

View File

@@ -1,187 +0,0 @@
/* eslint-disable react/prop-types */
import {
faClosedCaptioning,
faFile,
faMicrophone,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import BouncingLoader from "../ui/bouncingloader/Bouncingloader";
import "./Servers.css";
import { useEffect } from "react";
function Servers({
servers,
activeEpisodeNum,
activeServerId,
setActiveServerId,
serverLoading,
setActiveServerType,
setActiveServerName,
}) {
const subServers =
servers?.filter((server) => server.type === "sub") || [];
const dubServers =
servers?.filter((server) => server.type === "dub") || [];
const rawServers =
servers?.filter((server) => server.type === "raw") || [];
useEffect(() => {
const savedServerName = localStorage.getItem("server_name");
if (savedServerName) {
const matchingServer = servers?.find(
(server) => server.serverName === savedServerName,
);
if (matchingServer) {
setActiveServerId(matchingServer.data_id);
setActiveServerType(matchingServer.type);
} else if (servers && servers.length > 0) {
setActiveServerId(servers[0].data_id);
setActiveServerType(servers[0].type);
}
} else if (servers && servers.length > 0) {
setActiveServerId(servers[0].data_id);
setActiveServerType(servers[0].type);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [servers]);
const handleServerSelect = (server) => {
setActiveServerId(server.data_id);
setActiveServerType(server.type);
setActiveServerName(server.serverName);
localStorage.setItem("server_name", server.serverName);
localStorage.setItem("server_type", server.type);
};
return (
<div className="relative bg-[#11101A] p-4 w-full min-h-[100px] flex justify-center items-center max-[1200px]:bg-[#14151A]">
{serverLoading ? (
<div className="w-full h-full rounded-lg flex justify-center items-center max-[600px]:rounded-none">
<BouncingLoader />
</div>
) : servers ? (
<div className="w-full h-full rounded-lg grid grid-cols-[minmax(0,30%),minmax(0,70%)] overflow-hidden max-[800px]:grid-cols-[minmax(0,40%),minmax(0,60%)] max-[600px]:flex max-[600px]:flex-col max-[600px]:rounded-none">
<div className="h-full bg-[#ffbade] px-6 text-black flex flex-col justify-center items-center gap-y-2 max-[600px]:bg-transparent max-[600px]:h-1/2 max-[600px]:text-white max-[600px]:mb-4">
<p className="text-center leading-5 font-medium text-[14px]">
You are watching <br />
<span className="font-semibold max-[600px]:text-[#ffbade]">
Episode {activeEpisodeNum}
</span>
</p>
<p className="leading-5 text-[14px] font-medium text-center">
If the current server doesn&apos;t work, please try other servers
beside.
</p>
</div>
<div className="bg-[#201F31] flex flex-col max-[600px]:h-full">
{rawServers.length > 0 && (
<div
className={`servers px-2 flex items-center flex-wrap ml-2 max-[600px]:py-2 ${
dubServers.length === 0 || subServers.length === 0
? "h-1/2"
: "h-full"
}`}
>
<div className="flex items-center gap-x-2">
<FontAwesomeIcon
icon={faFile}
className="text-[#ffbade] text-[13px]"
/>
<p className="font-bold text-[14px]">RAW:</p>
</div>
<div className="flex gap-x-[7px] ml-8 flex-wrap">
{rawServers.map((item, index) => (
<div
key={index}
className={`px-6 py-[5px] rounded-lg cursor-pointer ${
activeServerId === item?.data_id
? "bg-[#ffbade] text-black"
: "bg-[#373646] text-white"
} max-[700px]:px-3`}
onClick={() => handleServerSelect(item)}
>
<p className="text-[13px] font-semibold">
{item.serverName}
</p>
</div>
))}
</div>
</div>
)}
{subServers.length > 0 && (
<div
className={`servers px-2 flex items-center flex-wrap ml-2 max-[600px]:py-2 ${
dubServers.length === 0 ? "h-1/2" : "h-full"
}`}
>
<div className="flex items-center gap-x-2">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[#ffbade] text-[13px]"
/>
<p className="font-bold text-[14px]">SUB:</p>
</div>
<div className="flex gap-x-[7px] ml-8 flex-wrap">
{subServers.map((item, index) => (
<div
key={index}
className={`px-6 py-[5px] rounded-lg cursor-pointer ${
activeServerId === item?.data_id
? "bg-[#ffbade] text-black"
: "bg-[#373646] text-white"
} max-[700px]:px-3`}
onClick={() => handleServerSelect(item)}
>
<p className="text-[13px] font-semibold">
{item.serverName}
</p>
</div>
))}
</div>
</div>
)}
{dubServers.length > 0 && (
<div
className={`servers px-2 flex items-center flex-wrap ml-2 max-[600px]:py-2 ${
subServers.length === 0 ? "h-1/2 " : "h-full"
}`}
>
<div className="flex items-center gap-x-3">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[#ffbade] text-[13px]"
/>
<p className="font-bold text-[14px]">DUB:</p>
</div>
<div className="flex gap-x-[7px] ml-8 flex-wrap">
{dubServers.map((item, index) => (
<div
key={index}
className={`px-6 py-[5px] rounded-lg cursor-pointer ${
activeServerId === item?.data_id
? "bg-[#ffbade] text-black"
: "bg-[#373646] text-white"
} max-[700px]:px-3`}
onClick={() => handleServerSelect(item)}
>
<p className="text-[13px] font-semibold">
{item.serverName}
</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
) : (
<p className="text-center font-medium text-[15px] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 pointer-events-none">
Could not load servers <br />
Either reload or try again after sometime
</p>
)}
</div>
);
}
export default Servers;

View File

@@ -1,141 +0,0 @@
import { FaChevronLeft } from "react-icons/fa";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFilm, faRandom } from "@fortawesome/free-solid-svg-icons";
import { useLanguage } from "@/src/context/LanguageContext";
import { useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
import {
cleanupScrollbar,
toggleScrollbar,
} from "@/src/helper/toggleScrollbar";
const Sidebar = ({ isOpen, onClose }) => {
const { language, toggleLanguage } = useLanguage();
const location = useLocation();
useEffect(() => {
toggleScrollbar(isOpen);
return () => {
cleanupScrollbar();
};
}, [isOpen]);
useEffect(() => {
onClose();
}, [location]);
return (
<>
{isOpen && (
<div
className={`fixed top-0 left-0 bottom-0 right-0 w-screen h-screen transform transition-all duration-400 ease-in-out ${
isOpen ? "backdrop-blur-lg" : "backdrop-blur-none"
}`}
onClick={onClose}
style={{ zIndex: 1000000, background: "rgba(32, 31, 49, .8)" }}
/>
)}
<div
className={`fixed h-full top-0 left-0 z-50 flex transition-transform duration-300 ease-in-out ${
isOpen ? "translate-x-0" : "-translate-x-full"
}`}
style={{ zIndex: 1000200 }}
>
<div
className="bg-white/10 w-[260px] py-8 h-full flex flex-col items-start max-[575px]:w-56 overflow-y-auto sidebar"
style={{
zIndex: 300,
borderRight: "1px solid rgba(0, 0, 0, .1)",
}}
>
<div className="px-4 w-full">
<button
onClick={onClose}
className="w-full text-white flex items-baseline h-fit gap-x-1 z-[100] px-3 py-2 bg-[#4f4d6e] rounded-3xl"
>
<FaChevronLeft className="text-sm font-bold" />
<p>Close menu</p>
</button>
</div>
<div className="flex gap-x-7 w-full py-3 justify-center px-auto mt-8 bg-black/10 max-[575px]:gap-x-4 lg:hidden">
{[
{ icon: faRandom, label: "Random" },
{ icon: faFilm, label: "Movie" },
].map((item, index) => (
<Link
to={`/${item.label}`}
key={index}
className="flex flex-col gap-y-1 items-center"
>
<FontAwesomeIcon
icon={item.icon}
className="text-[#ffbade] text-xl font-bold max-[575px]:text-[15px]"
/>
<p className="text-[15px] max-[575px]:text-[13px]">
{item.label}
</p>
</Link>
))}
<div className="flex flex-col gap-y-1 items-center w-auto justify-center">
<div className="flex">
{["EN", "JP"].map((lang, index) => (
<button
key={lang}
onClick={() => toggleLanguage(lang)}
className={`px-1 py-[1px] text-xs font-bold ${
index === 0 ? "rounded-l-[3px]" : "rounded-r-[3px]"
} ${
language === lang
? "bg-[#ffbade] text-black"
: "bg-gray-600 text-white"
} max-[575px]:text-[9px] max-[575px]:py-0`}
>
{lang}
</button>
))}
</div>
<div className="w-full">
<p className="whitespace-nowrap text-[15px] max-[575px]:text-[13px]">
Anime name
</p>
</div>
</div>
</div>
<ul className="text-white mt-8 w-full">
{[
{ name: "Home", path: "/home" },
{ name: "Subbed Anime", path: "/subbed-anime" },
{ name: "Dubbed Anime", path: "/dubbed-anime" },
{ name: "Most Popular", path: "/most-popular" },
{ name: "Movies", path: "/movie" },
{ name: "TV Series", path: "/tv" },
{ name: "OVAs", path: "/ova" },
{ name: "ONAs", path: "/ona" },
{ name: "Specials", path: "/special" },
{
name: "Join Telegram",
path: "https://t.me/zenime_discussion",
},
].map((item, index) => (
<li
key={index}
className="py-4 w-full font-semibold"
style={{ borderBottom: "1px solid rgba(255, 255, 255, .08)" }}
>
<Link
to={item.path}
className="px-4 hover:text-[#ffbade] hover:cursor-pointer w-fit line-clamp-1"
>
{item.name}
</Link>
</li>
))}
</ul>
</div>
</div>
</>
);
};
export default Sidebar;

View File

@@ -1,142 +0,0 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faClosedCaptioning,
faMicrophone,
} from "@fortawesome/free-solid-svg-icons";
import { useLanguage } from "@/src/context/LanguageContext";
import { Link, useNavigate } from "react-router-dom";
import useToolTipPosition from "@/src/hooks/useToolTipPosition";
import Qtip from "../qtip/Qtip";
function Sidecard({ data, label, className, limit }) {
const { language } = useLanguage();
const navigate = useNavigate();
const [showAll, setShowAll] = useState(false);
const [hoverTimeout, setHoverTimeout] = useState(null);
const handleMouseEnter = (item, index) => {
const timeout = setTimeout(() => {
setHoveredItem(item.id + index);
}, 400);
setHoverTimeout(timeout);
};
const handleMouseLeave = () => {
clearTimeout(hoverTimeout);
setHoveredItem(null);
};
const toggleShowAll = () => {
setShowAll((prev) => !prev);
};
const displayedData = limit
? data.slice(0, limit)
: showAll
? data
: data.slice(0, 6);
const [hoveredItem, setHoveredItem] = useState(null);
const { tooltipPosition, tooltipHorizontalPosition, cardRefs } =
useToolTipPosition(hoveredItem, data);
return (
<div className={`flex flex-col space-y-6 ${className}`}>
<h1 className="font-bold text-2xl text-[#ffbade]">{label}</h1>
<div className="flex flex-col space-y-4 bg-[#2B2A3C] p-4 pt-8">
{data &&
displayedData.map((item, index) => (
<div
key={index}
className="flex items-center gap-x-4"
ref={(el) => (cardRefs.current[index] = el)}
>
<div
style={{
borderBottom:
index + 1 < displayedData.length
? "1px solid rgba(255, 255, 255, .075)"
: "none",
}}
className="flex pb-4 relative container items-center"
>
{hoveredItem === item.id + index &&
window.innerWidth > 1024 && (
<div
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition} ${
tooltipPosition === "top-1/2"
? "translate-y-[50px]"
: "translate-y-[-50px]"
} z-[100000] transform transition-all duration-300 ease-in-out ${
hoveredItem === item.id + index
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-2"
}`}
>
<Qtip id={item.id} />
</div>
)}
<img
src={`https://wsrv.nl/?url=${item.poster}`}
alt={item.title}
className="flex-shrink-0 w-[60px] h-[75px] rounded-md object-cover cursor-pointer"
onClick={() => navigate(`/watch/${item.id}`)}
onMouseEnter={() => handleMouseEnter(item, index)}
onMouseLeave={handleMouseLeave}
/>
<div className="flex flex-col ml-4 space-y-2">
<Link
to={`/${item.id}`}
className="text-[1em] font-[500] hover:cursor-pointer hover:text-[#ffbade] transform transition-all ease-out line-clamp-1 max-[478px]:line-clamp-2 max-[478px]:text-[14px]"
onClick={() =>
window.scrollTo({ top: 0, behavior: "smooth" })
}
>
{language === "EN" ? item.title : item.japanese_title}
</Link>
<div className="flex flex-wrap items-center w-fit space-x-1 max-[320px]:gap-y-2">
{item.tvInfo?.sub && (
<div className="flex space-x-1 justify-center items-center bg-[#B0E3AF] rounded-[4px] px-[4px] text-black py-[2px]">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.sub}
</p>
</div>
)}
{item.tvInfo?.dub && (
<div className="flex space-x-1 justify-center items-center bg-[#B9E7FF] rounded-[4px] px-[8px] text-black py-[2px]">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.dub}
</p>
</div>
)}
{item.tvInfo?.showType && (
<div className="flex items-center gap-x-2">
<div className="dot ml-[4px]"></div>
<p className="text-[15px] font-light">
{item.tvInfo.showType}
</p>
</div>
)}
</div>
</div>
</div>
</div>
))}
{!limit && data.length > 6 && (
<button
className="w-full bg-[#555462d3] py-3 mt-4 hover:bg-[#555462] rounded-md font-bold transform transition-all ease-out"
onClick={toggleShowAll}
>
{showAll ? "Show less" : "Show more"}
</button>
)}
</div>
</div>
);
}
export default React.memo(Sidecard);

View File

@@ -1,227 +0,0 @@
/* Base styles */
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
color: white;
}
/* Container and background */
.splash-container {
min-height: 100vh;
width: 100%;
position: relative;
background: url('/splash.jpg') no-repeat center center fixed;
background-size: cover;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0 30px;
}
.splash-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.content-wrapper {
position: relative;
z-index: 2;
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 140px;
}
/* Logo */
.logo-container {
margin-bottom: 30px;
}
.logo {
height: 75px;
width: auto;
}
/* Search */
.search-container {
width: 100%;
max-width: 500px;
position: relative;
margin-bottom: 24px;
}
.search-input {
width: 100%;
padding: 14px 48px 14px 20px;
background: rgba(17, 17, 17, 0.75);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: white;
font-size: 16px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: rgba(255, 255, 255, 0.3);
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search-button {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
padding: 0;
font-size: 18px;
transition: color 0.2s;
}
.search-button:hover {
color: white;
}
/* Enter button */
.enter-button {
background: white;
color: black;
padding: 12px 24px;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
margin: 8px 0 60px;
transition: background-color 0.2s;
}
.enter-button:hover {
background: #ffbade;
}
/* FAQ Section */
.faq-section {
width: 100%;
max-width: 700px;
}
.faq-title {
font-size: 32px;
font-weight: 700;
text-align: center;
margin-bottom: 40px;
color: white;
}
.faq-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.faq-item {
background: #141414;
border-radius: 12px;
overflow: hidden;
border: 1px solid #1a1a1a;
}
.faq-question {
width: 100%;
padding: 18px 24px;
display: flex;
justify-content: space-between;
align-items: center;
background: none;
border: none;
color: white;
font-size: 17px;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
}
.faq-question:hover {
background: #1a1a1a;
}
.faq-toggle {
font-size: 16px;
color: white;
opacity: 0.8;
transition: transform 0.2s ease;
}
.faq-toggle.rotate {
transform: rotate(180deg);
}
.faq-answer {
padding: 0 24px 18px;
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
font-size: 15px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.content-wrapper {
padding-top: 100px;
}
.logo {
height: 60px;
}
.search-input {
padding: 12px 40px 12px 16px;
font-size: 15px;
}
.faq-title {
font-size: 24px;
margin-bottom: 24px;
}
}
@media (max-width: 480px) {
.content-wrapper {
padding-top: 80px;
}
.logo {
height: 50px;
}
.search-input {
padding: 12px 36px 12px 14px;
font-size: 14px;
}
.enter-button {
padding: 10px 20px;
font-size: 14px;
}
.faq-question {
padding: 16px;
font-size: 15px;
}
.faq-answer {
padding: 0 16px 16px;
font-size: 14px;
}
}

View File

@@ -1,107 +0,0 @@
import { useState, useCallback } from "react";
import { Link, useNavigate } from "react-router-dom";
import "./SplashScreen.css";
import logoTitle from "@/src/config/logoTitle";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMagnifyingGlass, faChevronDown } from "@fortawesome/free-solid-svg-icons";
const FAQ_ITEMS = [
{
question: "Is JustAnime safe?",
answer: "Yes, JustAnime is completely safe to use. We ensure all content is properly scanned and secured for our users."
},
{
question: "What makes JustAnime the best site to watch anime free online?",
answer: "JustAnime offers high-quality streaming, a vast library of anime, no intrusive ads, and a user-friendly interface - all completely free."
},
{
question: "How do I request an anime?",
answer: "You can submit anime requests through our contact form or by reaching out to our support team."
}
];
function SplashScreen() {
const navigate = useNavigate();
const [search, setSearch] = useState("");
const [expandedFaq, setExpandedFaq] = useState(null);
const handleSearchSubmit = useCallback(() => {
const trimmedSearch = search.trim();
if (!trimmedSearch) return;
const queryParam = encodeURIComponent(trimmedSearch);
navigate(`/search?keyword=${queryParam}`);
}, [search, navigate]);
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Enter") {
handleSearchSubmit();
}
},
[handleSearchSubmit]
);
const toggleFaq = (index) => {
setExpandedFaq(expandedFaq === index ? null : index);
};
return (
<div className="splash-container">
<div className="splash-overlay"></div>
<div className="content-wrapper">
<div className="logo-container">
<img src="/logo.png" alt={logoTitle} className="logo" />
</div>
<div className="search-container">
<input
type="text"
placeholder="Search anime..."
className="search-input"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button
className="search-button"
onClick={handleSearchSubmit}
aria-label="Search"
>
<FontAwesomeIcon icon={faMagnifyingGlass} />
</button>
</div>
<Link to="/home" className="enter-button">
Enter Homepage
</Link>
<div className="faq-section">
<h2 className="faq-title">Frequently Asked Questions</h2>
<div className="faq-list">
{FAQ_ITEMS.map((item, index) => (
<div key={index} className="faq-item">
<button
className="faq-question"
onClick={() => toggleFaq(index)}
>
<span>{item.question}</span>
<FontAwesomeIcon
icon={faChevronDown}
className={`faq-toggle ${expandedFaq === index ? 'rotate' : ''}`}
/>
</button>
{expandedFaq === index && (
<div className="faq-answer">
{item.answer}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
);
}
export default SplashScreen;

View File

@@ -1,68 +0,0 @@
.swiper {
width: 100%;
}
.swiper-slide {
font-size: 18px;
display: -webkit-box;
display: -ms-flexbox;
display: -webkit-flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-box-align: center;
-ms-flex-align: center;
}
.button-prev,
.button-next {
width: 40px;
height: 40px;
color: white;
background-color: #383747;
border-radius: 7px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s ease-out;
}
.button-prev:hover,
.button-next:hover {
background-color: #ffbade;
color: #383747;
}
.button-prev::after {
font-family: "Font Awesome 5 Free";
content: "\f053";
font-weight: 900;
font-size: 14px;
}
.button-next::after {
font-family: "Font Awesome 5 Free";
content: "\f054";
font-weight: 900;
font-size: 14px;
}
.swiper-horizontal > .swiper-pagination-bullets {
display: none;
}
.swiper-pagination-bullet-active {
background-color: rgb(239, 213, 22) !important;
}
@media only screen and (max-width: 575px) {
.swiper-horizontal > .swiper-pagination-bullets {
/* bottom: var(--swiper-pagination-bottom, 8px); */
bottom: 0;
right: 10px !important ;
left: auto !important;
width: 20px !important;
bottom: 5px !important;
display: flex !important;
gap: 18px;
align-items: center;
justify-content: center;
height: 80%;
flex-direction: column;
}
}

View File

@@ -1,54 +0,0 @@
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Autoplay } from "swiper/modules";
import "swiper/css";
import "swiper/css/autoplay";
import "swiper/css/navigation";
import "./Spotlight.css";
import Banner from "../banner/Banner";
const Spotlight = ({ spotlights }) => {
return (
<>
<div className="relative h-[600px] max-[1390px]:h-[530px] max-[1300px]:h-[500px] max-md:h-[420px]">
<div className="absolute right-[10px] bottom-0 flex flex-col space-y-2 z-10 max-[575px]:hidden">
<div className="button-next"></div>
<div className="button-prev"></div>
</div>
{spotlights && spotlights.length > 0 ? (
<>
<Swiper
spaceBetween={0}
slidesPerView={1}
loop={true}
allowTouchMove={false}
navigation={{
nextEl: ".button-next",
prevEl: ".button-prev",
}}
autoplay={{
delay: 3000,
disableOnInteraction: false,
}}
modules={[Navigation, Autoplay]}
className="h-[600px] max-[1390px]:h-full"
style={{
"--swiper-pagination-bullet-inactive-color": "#ffffff",
"--swiper-pagination-bullet-inactive-opacity": "1",
}}
>
{spotlights.map((item, index) => (
<SwiperSlide className="text-black relative" key={index}>
<Banner item={item} index={index} />
</SwiperSlide>
))}
</Swiper>
</>
) : (
<p>No spotlights to show.</p>
)}
</div>
</>
);
};
export default Spotlight;

View File

@@ -1,115 +0,0 @@
import getSearchSuggestion from "@/src/utils/getSearchSuggestion.utils";
import { useEffect, useState } from "react";
import BouncingLoader from "../ui/bouncingloader/Bouncingloader";
import { FaChevronRight } from "react-icons/fa";
import { Link } from "react-router-dom";
function Suggestion({ keyword, className }) {
const [suggestion, setSuggestion] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [hasFetched, setHasFetched] = useState(false);
useEffect(() => {
const fetchSearchSuggestion = async () => {
if (!keyword) return;
setLoading(true);
setHasFetched(false);
try {
const data = await getSearchSuggestion(keyword);
setSuggestion(data);
setHasFetched(true);
} catch (err) {
console.error("Error fetching search suggestion info:", err);
setError(err);
} finally {
setLoading(false);
}
};
fetchSearchSuggestion();
}, [keyword]);
return (
<div
className={`bg-[#2d2b44] ${className} flex ${
loading ? "justify-center py-7" : "justify-start"
} ${!suggestion ? "p-3" : "justify-start"} items-center`}
style={{ boxShadow: "0 20px 20px rgba(0, 0, 0, .3)" }}
>
{loading ? (
<BouncingLoader />
) : error && !suggestion ? (
<div>Error loading suggestions</div>
) : suggestion && hasFetched ? (
<div className="w-full flex flex-col pt-2 overflow-y-auto">
{suggestion.map((item, index) => (
<Link
to={`/${item.id}`}
key={index}
className="group py-2 flex items-start gap-x-3 hover:bg-[#3c3a5e] cursor-pointer px-[10px]"
style={{
borderBottom:
index === suggestion.length - 1
? "none"
: "1px dashed rgba(255, 255, 255, .075)",
}}
>
<img
src={`https://wsrv.nl/?url=${item.poster}`}
className="w-[50px] h-[75px] flex-shrink-0 object-cover"
alt=""
onError={(e) => {
e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg";
}}
/>
<div className="flex flex-col gap-y-[2px]">
{item?.title && (
<h1 className="line-clamp-1 leading-5 font-bold text-[15px] group-hover:text-[#ffbade]">
{item.title || "N/A"}
</h1>
)}
{item?.japanese_title && (
<h1 className="line-clamp-1 leading-5 text-[13px] font-light text-[#aaaaaa]">
{item.japanese_title || "N/A"}
</h1>
)}
{(item?.releaseDate || item?.showType || item?.duration) && (
<div className="flex gap-x-[5px] items-center w-full justify-start mt-[4px]">
<p className="leading-5 text-[13px] font-light text-[#aaaaaa]">
{item.releaseDate || "N/A"}
</p>
<span className="dot"></span>
<p className="leading-5 text-[13px] font-medium group-hover:text-[#ffbade]">
{item.showType || "N/A"}
</p>
<span className="dot"></span>
<p className="leading-5 text-[13px] font-light text-[#aaaaaa]">
{item.duration || "N/A"}
</p>
</div>
)}
</div>
</Link>
))}
{!loading && hasFetched && (
<Link
className="w-full flex py-4 justify-center items-center bg-[#ffbade]"
to={`/search?keyword=${encodeURIComponent(keyword)}`}
>
<div className="flex w-fit items-center gap-x-2">
<p className="text-[17px] font-light text-black">
View all results
</p>
<FaChevronRight className="text-black text-[12px] font-black mt-[2px]" />
</div>
</Link>
)}
</div>
) : hasFetched ? (
<p className="text-[17px]">No results found!</p>
) : null}
</div>
);
}
export default Suggestion;

View File

@@ -1,176 +0,0 @@
import React, { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faClosedCaptioning,
faMicrophone,
} from "@fortawesome/free-solid-svg-icons";
import { useLanguage } from "@/src/context/LanguageContext";
import { Link, useNavigate } from "react-router-dom";
import useToolTipPosition from "@/src/hooks/useToolTipPosition";
import Qtip from "../qtip/Qtip";
function Topten({ data, className }) {
const { language } = useLanguage();
const [activePeriod, setActivePeriod] = useState("today");
const [hoveredItem, setHoveredItem] = useState(null);
const [hoverTimeout, setHoverTimeout] = useState(null);
const navigate = useNavigate();
const handlePeriodChange = (period) => {
setActivePeriod(period);
};
const handleNavigate = (id) => {
navigate(`/${id}`);
window.scrollTo({ top: 0, behavior: "smooth" });
};
const currentData =
activePeriod === "today"
? data.today
: activePeriod === "week"
? data.week
: data.month;
const { tooltipPosition, tooltipHorizontalPosition, cardRefs } =
useToolTipPosition(hoveredItem, currentData);
const handleMouseEnter = (item, index) => {
if (hoverTimeout) clearTimeout(hoverTimeout);
setHoveredItem(item.id + index);
};
const handleMouseLeave = () => {
setHoverTimeout(
setTimeout(() => {
setHoveredItem(null);
}, 300) // Small delay to prevent flickering
);
};
return (
<div className={`flex flex-col space-y-6 ${className}`}>
<div className="flex justify-between items-center max-[350px]:flex-col max-[350px]:gap-y-2 max-[350px]:items-start">
<h1 className="font-bold text-2xl text-[#ffbade]">Top 10</h1>
<ul className="flex justify-between w-fit bg-[#373646] rounded-[4px] text-sm font-bold">
{["today", "week", "month"].map((period) => (
<li
key={period}
className={`cursor-pointer p-2 px-3 ${
activePeriod === period
? "bg-[#ffbade] text-[#555462]"
: "text-white hover:text-[#ffbade]"
} ${period === "today" ? "rounded-l-[4px]" : ""} ${
period === "month" ? "rounded-r-[4px]" : ""
}`}
onClick={() => handlePeriodChange(period)}
>
{period.charAt(0).toUpperCase() + period.slice(1)}
</li>
))}
</ul>
</div>
<div className="flex flex-col space-y-4 bg-[#2B2A3C] p-4 pt-8">
{currentData &&
currentData.map((item, index) => (
<div
key={index}
className="flex items-center gap-x-4"
ref={(el) => (cardRefs.current[index] = el)}
>
<h1
className={`font-bold text-2xl ${
index < 3
? "pb-1 text-white border-b-[3px] border-[#ffbade]"
: "text-[#777682]"
} max-[350px]:hidden`}
>
{`${index + 1 < 10 ? "0" : ""}${index + 1}`}
</h1>
<div
style={{
borderBottom:
index + 1 < 10
? "1px solid rgba(255, 255, 255, .075)"
: "none",
}}
className="flex pb-4 relative container items-center"
>
{/* Image with tooltip behavior */}
<img
src={`https://wsrv.nl/?url=${item.poster}`}
alt={item.title}
className="w-[60px] h-[75px] rounded-md object-cover flex-shrink-0 cursor-pointer"
onClick={() => navigate(`/watch/${item.id}`)}
onMouseEnter={() => handleMouseEnter(item, index)}
onMouseLeave={handleMouseLeave}
/>
{/* Tooltip positioned near image */}
{hoveredItem === item.id + index &&
window.innerWidth > 1024 && (
<div
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition}
${
tooltipPosition === "top-1/2"
? "translate-y-[50px]"
: "translate-y-[-50px]"
}
z-[100000] transform transition-all duration-300 ease-in-out
${
hoveredItem === item.id + index
? "opacity-100 translate-y-0"
: "opacity-0 translate-y-2"
}`}
onMouseEnter={() => {
if (hoverTimeout) clearTimeout(hoverTimeout);
}}
onMouseLeave={handleMouseLeave}
>
<Qtip id={item.id} />
</div>
)}
<div className="flex flex-col ml-4 space-y-2">
<Link
to={`/${item.id}`}
className="text-[1em] font-[500] hover:cursor-pointer hover:text-[#ffbade] transform transition-all ease-out line-clamp-1 max-[478px]:line-clamp-2 max-[478px]:text-[14px]"
onClick={() => handleNavigate(item.id)}
>
{language === "EN" ? item.title : item.japanese_title}
</Link>
<div className="flex flex-wrap items-center w-fit space-x-1 max-[350px]:gap-y-[3px]">
{item.tvInfo?.sub && (
<div className="flex space-x-1 justify-center items-center bg-[#B0E3AF] rounded-[4px] px-[4px] text-black py-[2px]">
<FontAwesomeIcon
icon={faClosedCaptioning}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.sub}
</p>
</div>
)}
{item.tvInfo?.dub && (
<div className="flex space-x-1 justify-center items-center bg-[#B9E7FF] rounded-[4px] px-[8px] text-black py-[2px]">
<FontAwesomeIcon
icon={faMicrophone}
className="text-[12px]"
/>
<p className="text-[12px] font-bold">
{item.tvInfo.dub}
</p>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
export default React.memo(Topten);

View File

@@ -1,77 +0,0 @@
import { Pagination, Navigation } from "swiper/modules";
import { Swiper, SwiperSlide } from "swiper/react";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
import { useLanguage } from "@/src/context/LanguageContext";
import { Link, useNavigate } from "react-router-dom";
const Trending = ({ trending }) => {
const { language } = useLanguage();
const navigate = useNavigate();
return (
<div className="mt-6 max-[1200px]:px-4 max-md:px-0">
<h1 className="text-[#ffbade] text-2xl font-bold max-md:pl-4">
Trending
</h1>
<div className="pr-[60px] relative mx-auto overflow-hidden z-[1] mt-6 max-[759px]:pr-0">
<Swiper
className="w-full h-full"
slidesPerView={3}
spaceBetween={2}
breakpoints={{
479: { spaceBetween: 15 },
575: { spaceBetween: 15 },
640: { slidesPerView: 3, spaceBetween: 15 },
900: { slidesPerView: 4, spaceBetween: 15 },
1300: { slidesPerView: 6, spaceBetween: 15 },
}}
modules={[Pagination, Navigation]}
navigation={{
nextEl: ".btn-next",
prevEl: ".btn-prev",
}}
>
{trending &&
trending.map((item, idx) => (
<SwiperSlide
key={idx}
className="text-center flex text-[18px] justify-center items-center"
onClick={() => navigate(`/watch/${item.id}`)}
>
<div className="w-full h-auto pb-[115%] relative inline-block overflow-hidden max-[575px]:pb-[150%]">
<div className="absolute left-0 top-0 bottom-0 overflow-hidden w-[40px] text-center font-semibold bg-[#201F31] max-[575px]:top-0 max-[575px]:h-[30px] max-[575px]:z-[9] max-[575px]:bg-white">
<span className="absolute left-0 right-0 bottom-0 text-[24px] leading-[1.1em] text-center z-[9] transform -rotate-90 max-[575px]:transform max-[575px]:rotate-0 max-[575px]:text-[#111] max-[575px]:text-[18px] max-[575px]:leading-[30px]">
{item.number}
</span>
<div className="w-[150px] h-fit text-left transform -rotate-90 absolute bottom-[100px] left-[-55px] leading-[40px] text-ellipsis whitespace-nowrap overflow-hidden text-white text-[16px] font-medium">
{language === "EN" ? item.title : item.japanese_title}
</div>
</div>
<Link
to={`/${item.id}`}
className="inline-block bg-[#2a2c31] absolute w-auto left-[40px] right-0 top-0 bottom-0 max-[575px]:left-0 max-[575px]:top-0 max-[575px]:bottom-0"
>
<img
src={`https://wsrv.nl/?url=${item.poster}`}
alt={item.title}
className="block w-full h-full object-cover hover:cursor-pointer"
title={item.title}
/>
</Link>
</div>
</SwiperSlide>
))}
</Swiper>
<div className="absolute top-0 right-0 bottom-0 w-[45px] flex flex-col space-y-2 max-[759px]:hidden">
<div className="btn-next bg-[#383747] h-[50%] flex justify-center items-center rounded-[8px] cursor-pointer transition-all duration-300 ease-out hover:bg-[#ffbade] hover:text-[#383747]">
<FaChevronRight />
</div>
<div className="btn-prev bg-[#383747] h-[50%] flex justify-center items-center rounded-[8px] cursor-pointer transition-all duration-300 ease-out hover:bg-[#ffbade] hover:text-[#383747]">
<FaChevronLeft />
</div>
</div>
</div>
</div>
);
};
export default Trending;

View File

@@ -1,23 +0,0 @@
@keyframes shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
.shimmer-effect {
background: linear-gradient(
to right,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.2) 20%,
rgba(255, 255, 255, 0.3) 40%,
rgba(255, 255, 255, 0.2) 60%,
rgba(255, 255, 255, 0.1) 80%,
rgba(0, 0, 0, 0.03) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
}

View File

@@ -1,16 +0,0 @@
import { cn } from "@/lib/utils";
import './Skeleton.css';
function Skeleton({ className, animation=true, ...props }) {
return (
<div
className={cn("bg-gray-400 rounded-3xl",
animation ? "shimmer-effect" : "",
className
)}
{...props}
/>
);
}
export { Skeleton };

View File

@@ -1,45 +0,0 @@
.bouncing-loading > div {
width: 18px;
height: 18px;
background-color: #858490;
border-radius: 100%;
display: inline-block;
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
.bouncing-loading .span1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.bouncing-loading .span2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes sk-bouncedelay {
0%,
100%,
80% {
-webkit-transform: scale(0);
}
40% {
-webkit-transform: scale(1);
}
}
@keyframes sk-bouncedelay {
0%,
100%,
80% {
-webkit-transform: scale(0);
transform: scale(0);
}
40% {
-webkit-transform: scale(1);
transform: scale(1);
}
}

View File

@@ -1,12 +0,0 @@
import "./Bouncingloader.css"
const BouncingLoader = () => {
return (
<div className="bouncing-loading flex gap-x-[5px]">
<div className="span1"></div>
<div className="span2"></div>
<div className="span3"></div>
</div>
);
};
export default BouncingLoader;

View File

@@ -1,100 +0,0 @@
import { useState } from "react";
import { FaChevronRight } from "react-icons/fa";
import VoiceactorList from "../voiceactorlist/VoiceactorList";
function Voiceactor({ animeInfo, className }) {
const [showVoiceActors, setShowVoiceActors] = useState(false);
return (
<div className={`w-full mt-8 flex flex-col gap-y-4 ${className}`}>
<div className="flex justify-between items-center">
<h1 className="font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px] capitalize">
Characters & Voice Actors
</h1>
<button className="flex w-fit items-baseline h-fit rounded-3xl gap-x-1 group">
<p
className="text-white text-[12px] font-semibold h-fit leading-0"
onClick={() => {
setShowVoiceActors(true);
}}
>
View more
</p>
<FaChevronRight className="text-white text-[10px]" />
</button>
</div>
<div className="w-full grid grid-cols-3 max-[1024px]:grid-cols-2 max-[758px]:grid-cols-1 gap-4">
{animeInfo.charactersVoiceActors.slice(0, 6).map((character, index) => (
<div
key={index}
className="flex justify-between items-center px-3 py-4 rounded-md bg-[#373646]"
>
{character.character && (
<div className="w-[50%] float-left overflow-hidden max-[350px]:w-[45%]">
<div className="w-full flex gap-x-3">
{character.character.poster && (
<img
src={character.character.poster}
title={character.character.name || "Character"}
alt={character.character.name || "Character"}
onError={(e) => {
e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg";
}}
className="w-[45px] h-[45px] flex-shrink-0 rounded-full object-cover"
loading="lazy"
/>
)}
<div className="flex justify-center flex-col">
{character.character.name && (
<h4 className="text-[13px] text-left leading-[1.3em] font-[400] mb-0 overflow-hidden -webkit-box -webkit-line-clamp-2 -webkit-box-orient-vertical">
{character.character.name}
</h4>
)}
{character.character.cast && (
<p className="text-[11px] mt-[3px]">
{character.character.cast}
</p>
)}
</div>
</div>
</div>
)}
{character.voiceActors.length > 0 && character.voiceActors[0] && (
<div className="w-[50%] float-right overflow-hidden max-[350px]:w-[45%]">
<div className="w-full flex justify-end gap-x-2">
<div className="flex flex-col justify-center ">
{character.voiceActors[0].name && (
<span className="text-[13px] text-right leading-[1.3em] font-[400] mb-0 overflow-hidden -webkit-box -webkit-line-clamp-2 -webkit-box-orient-vertical w-fit">
{character.voiceActors[0].name}
</span>
)}
</div>
{character.voiceActors[0].poster && (
<img
src={character.voiceActors[0].poster}
title={character.voiceActors[0].name || "Voice Actor"}
alt={character.voiceActors[0].name || "Voice Actor"}
loading="lazy"
onError={(e) => {
e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg";
}}
className="w-[45px] h-[45px] rounded-full object-cover grayscale hover:grayscale-0 hover:cursor-pointer flex-shrink-0 transition-all duration-300 ease-in-out"
/>
)}
</div>
</div>
)}
</div>
))}
</div>
{showVoiceActors && (
<VoiceactorList
id={animeInfo.id}
isOpen={showVoiceActors}
onClose={() => setShowVoiceActors(false)}
/>
)}
</div>
);
}
export default Voiceactor;

View File

@@ -1,175 +0,0 @@
import { useState, useEffect } from "react";
import {
faAngleDoubleLeft,
faAngleDoubleRight,
faChevronLeft,
faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import fetchVoiceActorInfo from "@/src/utils/getVoiceActor.utils";
import VoiceActorlistLoader from "../Loader/VoiceActorlist.loader";
import { useNavigate } from "react-router-dom";
import Error from "../error/Error";
import {
cleanupScrollbar,
toggleScrollbar,
} from "@/src/helper/toggleScrollbar";
import PageSlider from "../pageslider/PageSlider";
function VoiceactorList({ id, isOpen, onClose }) {
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [error, setError] = useState(null);
const [VoiceactorList, setVoiceactorList] = useState([]);
const navigate = useNavigate();
useEffect(() => {
toggleScrollbar(isOpen);
return () => {
cleanupScrollbar();
};
}, [isOpen]);
useEffect(() => {
const fetchCategoryInfo = async () => {
setLoading(true);
try {
const data = await fetchVoiceActorInfo(id, page);
setVoiceactorList(data.data);
setTotalPages(data.totalPages);
setLoading(false);
} catch (err) {
setError(err);
console.error("Error fetching category info:", err);
}
};
fetchCategoryInfo();
}, [page]);
if (error) {
navigate("/error-page");
return <Error />;
}
if (!VoiceactorList) {
navigate("/404-not-found-page");
return null;
}
return (
<div
className="fixed top-0 left-0 w-screen h-screen overflow-y-auto bg-black/80 z-50 flex justify-center py-10 max-[575px]:py-3"
style={{
zIndex: 1000000,
pointerEvents: "auto",
}}
>
<div
className={`w-[920px] h-fit flex flex-col relative backdrop-blur-[10px] rounded-lg p-6 bg-white/10 ${
loading ? "h-fit" : ""
} max-[1000px]:w-[80vw] max-md:w-[90vw] max-[480px]:p-3`}
style={{
pointerEvents: "auto",
}}
>
{!loading && (
<h2 className="text-2xl font-bold col-span-2 max-[480px]:text-lg">
Characters & Voice Actors
</h2>
)}
{loading ? (
<VoiceActorlistLoader />
) : (
<div className="w-full grid grid-cols-2 gap-4 mt-5 max-[1000px]:grid-cols-1">
{VoiceactorList.map((item, index) => (
<div
key={index}
className="flex p-4 items-center justify-between py-2 bg-[#444445] rounded-lg h-[80px] max-[480px]:p-1 max-[480px]:bg-transparent max-[480px]:rounded-none max-[480px]:border-b-[1px] border-dotted max-[480px]:h-[60px] max-[480px]:pb-4"
>
<div className="flex gap-x-2 items-center w-[50%] overflow-hidden">
<img
src={item.character.poster}
className="w-[45px] h-[45px] rounded-full flex-shrink-0 object-cover hover:cursor-pointer max-[480px]:w-[30px] max-[480px]:h-[30px]"
loading="lazy"
onError={(e) => {
e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg";
}}
/>
<div className="flex flex-col text-left gap-y-1 w-full">
{item.character.name && (
<h1 className="text-[13px] font-semibold max-[480px]:text-[11px]">
{item.character.name}
</h1>
)}
{item.character.cast && (
<p className="text-[12px] font-light max-[480px]:text-[10px]">
{item.character.cast}
</p>
)}
</div>
</div>
{item.voiceActors &&
item.voiceActors.length > 0 &&
(item.voiceActors.length > 1 ? (
<div className="flex flex-wrap gap-x-[4px] items-center justify-end w-[50%] max-sm:flex-nowrap max-sm:overflow-auto max-[350px]:justify-start max-sm:py-3">
{item.voiceActors.map((data, index) => (
<img
key={index}
src={data.poster}
className="w-[41px] h-[41px] opacity-70 cursor-pointer rounded-full flex-shrink-0 object-cover grayscale hover:grayscale-0 hover:opacity-100 max-[480px]:w-[30px] max-[480px]:h-[30px] transition-all duration-300 ease-in-out"
title={data.name}
style={{
border: "4px solid rgba(105, 108, 117, 0.8)",
}}
onError={(e) => {
e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg";
}}
/>
))}
</div>
) : (
<div className="flex items-center justify-end gap-x-2 w-[50%] overflow-hidden max-[480px]:flex-wrap max-[480px]:flex-col-reverse max-[480px]:items-end max-[480px]:gap-y-1">
{item?.voiceActors[0]?.name && (
<p className="text-right text-[13px] max-[480px]:text-[11px]">
{item.voiceActors[0].name}
</p>
)}
<img
src={item.voiceActors[0].poster}
alt=""
title={item.voiceActors.name}
loading="lazy"
className="w-[45px] h-[45px] rounded-full opacity-70 flex-shrink-0 object-cover grayscale hover:grayscale-0 hover:opacity-100 max-[480px]:w-[30px] max-[480px]:h-[30px] transition-all duration-300 ease-in-out"
onError={(e) => {
e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg";
}}
/>
</div>
))}
</div>
))}
</div>
)}
<div
className="bg-white w-[30px] h-[30px] p-2 rounded-full text-3xl absolute z-[1000] top-[-14px] right-[-14px] hover:text-[#FFBADE] cursor-pointer transform transition-all ease-in-out duration-300 flex items-center justify-center hover:bg-[#ffbade] max-md:top-0 max-md:right-0 max-md:rounded-none max-md:rounded-bl-lg max-md:rounded-tr-lg"
onClick={onClose}
>
<button className="text-black mb-[6px] font-semibold">&times;</button>
</div>
<PageSlider
page={page}
totalPages={totalPages}
handlePageChange={setPage}
start={true}
style={{
marginTop: "10px",
}}
/>
</div>
</div>
);
}
export default VoiceactorList;

View File

@@ -1,97 +0,0 @@
import { faBackward, faForward } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react";
const ToggleButton = ({ label, isActive, onClick }) => (
<button className="flex gap-x-2" onClick={onClick}>
<h1 className="capitalize text-[13px]">{label}</h1>
<span
className={`capitalize text-[13px] ${
isActive ? "text-[#ffbade]" : "text-red-500"
}`}
>
{isActive ? "on" : "off"}
</span>
</button>
);
export default function WatchControls({
autoPlay,
setAutoPlay,
autoSkipIntro,
setAutoSkipIntro,
autoNext,
setAutoNext,
episodeId,
episodes = [],
onButtonClick,
}) {
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(
episodes?.findIndex(
(episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId
)
);
useEffect(() => {
if (episodes?.length > 0) {
const newIndex = episodes.findIndex(
(episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId
);
setCurrentEpisodeIndex(newIndex);
}
}, [episodeId, episodes]);
return (
<div className="bg-[#11101A] w-full flex justify-between flex-wrap px-4 pt-4 max-[1200px]:bg-[#14151A] max-[375px]:flex-col max-[375px]:gap-y-2">
<div className="flex gap-x-4 flex-wrap">
<ToggleButton
label="auto play"
isActive={autoPlay}
onClick={() => setAutoPlay((prev) => !prev)}
/>
<ToggleButton
label="auto skip intro"
isActive={autoSkipIntro}
onClick={() => setAutoSkipIntro((prev) => !prev)}
/>
<ToggleButton
label="auto next"
isActive={autoNext}
onClick={() => setAutoNext((prev) => !prev)}
/>
</div>
<div className="flex gap-x-6 max-[575px]:gap-x-4 max-[375px]:justify-end">
<button
onClick={() => {
if (currentEpisodeIndex > 0) {
onButtonClick(
episodes[currentEpisodeIndex - 1].id.match(/ep=(\d+)/)?.[1]
);
}
}}
disabled={currentEpisodeIndex <= 0}
>
<FontAwesomeIcon
icon={faBackward}
className="text-[20px] max-[575px]:text-[16px] text-white"
/>
</button>
<button
onClick={() => {
if (currentEpisodeIndex < episodes?.length - 1) {
onButtonClick(
episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]
);
}
}}
disabled={currentEpisodeIndex >= episodes?.length - 1}
>
<FontAwesomeIcon
icon={faForward}
className="text-[20px] max-[575px]:text-[16px] text-white"
/>
</button>
</div>
</div>
);
}

View File

@@ -1,3 +0,0 @@
const logoTitle="Zen!me"
export default logoTitle;

View File

@@ -1,3 +0,0 @@
const website_name = "JustAnime";
export default website_name;

View File

@@ -1,31 +0,0 @@
import { createContext, useContext, useState, useEffect } from 'react';
import getHomeInfo from '../utils/getHomeInfo.utils.js';
const HomeInfoContext = createContext();
export const HomeInfoProvider = ({ children }) => {
const [homeInfo, setHomeInfo] = useState(null);
const [homeInfoLoading, setHomeInfoLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchHomeInfo = async () => {
try {
const data = await getHomeInfo();
setHomeInfo(data);
} catch (err) {
console.error("Error fetching home info:", err);
setError(err);
} finally {
setHomeInfoLoading(false);
}
};
fetchHomeInfo();
}, []);
return (
<HomeInfoContext.Provider value={{ homeInfo, homeInfoLoading, error }}>
{children}
</HomeInfoContext.Provider>
);
};
export const useHomeInfo = () => useContext(HomeInfoContext);

View File

@@ -1,27 +0,0 @@
import { createContext, useContext, useState, useEffect } from 'react';
const LanguageContext = createContext();
export const LanguageProvider = ({ children }) => {
const [language, setLanguage] = useState(() => {
const storedLanguage = localStorage.getItem('language');
return storedLanguage ? storedLanguage : 'EN';
});
useEffect(() => {
localStorage.setItem('language', language);
}, [language]);
const toggleLanguage = (lang) => {
setLanguage(lang);
};
return (
<LanguageContext.Provider value={{ language, toggleLanguage }}>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = () => {
return useContext(LanguageContext);
};

View File

@@ -1,13 +0,0 @@
import { createContext, useContext, useState } from 'react';
const SearchContext = createContext();
export function SearchProvider({ children }) {
const [isSearchVisible, setIsSearchVisible] = useState(false);
return (
<SearchContext.Provider value={{ isSearchVisible, setIsSearchVisible }}>
{children}
</SearchContext.Provider>
);
}
export const useSearchContext = () => useContext(SearchContext);

View File

@@ -1,32 +0,0 @@
export function toggleScrollbar(isOpen) {
const getScrollbarWidth = () => {
return window.innerWidth - document.documentElement.clientWidth;
};
const body = document.body;
if (isOpen) {
const scrollbarWidth = getScrollbarWidth();
body.style.paddingRight = `${scrollbarWidth}px`;
body.classList.add("overflow-y-hidden");
const style = document.createElement("style");
style.id = "hide-scrollbar";
style.innerHTML = `::-webkit-scrollbar { display: none; }`;
document.head.appendChild(style);
} else {
body.style.paddingRight = "0";
body.classList.remove("overflow-y-hidden");
const styleElement = document.getElementById("hide-scrollbar");
if (styleElement) {
styleElement.remove();
}
}
}
export function cleanupScrollbar() {
const body = document.body;
body.style.paddingRight = "0";
body.classList.remove("overflow-y-hidden");
const styleElement = document.getElementById("hide-scrollbar");
if (styleElement) {
styleElement.remove();
}
}

View File

@@ -1,64 +0,0 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { useLocation } from "react-router-dom";
import { useSearchContext } from "@/src/context/SearchContext";
const useSearch = () => {
const { isSearchVisible, setIsSearchVisible } = useSearchContext();
const [searchValue, setSearchValue] = useState("");
const [isFocused, setIsFocused] = useState(false);
const [debouncedValue, setDebouncedValue] = useState("");
const suggestionRefs = useRef([]);
const location = useLocation();
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(searchValue);
}, 500);
return () => {
clearTimeout(timer);
};
}, [searchValue]);
useEffect(() => {
setIsSearchVisible(false);
setSearchValue("");
setDebouncedValue("");
// setIsFocused(false);
}, [location, setIsSearchVisible]);
useEffect(() => {
const handleClickOutside = (event) => {
const isInsideSuggestionBox = suggestionRefs.current.some(
(ref) => ref && ref.contains(event.target)
);
const isInsideInput = document.activeElement === event.target;
if (!isInsideSuggestionBox && !isInsideInput) {
setIsFocused(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const addSuggestionRef = useCallback((ref) => {
if (ref && !suggestionRefs.current.includes(ref)) {
suggestionRefs.current.push(ref);
}
}, []);
return {
isSearchVisible,
setIsSearchVisible,
searchValue,
setSearchValue,
isFocused,
setIsFocused,
debouncedValue,
suggestionRefs,
addSuggestionRef,
};
};
export default useSearch;

View File

@@ -1,49 +0,0 @@
import { useEffect, useRef, useState } from "react";
const useToolTipPosition = (hoveredItem, data) => {
const cardRefs = useRef([]);
const [tooltipPosition, setTooltipPosition] = useState("top-1/2");
const [tooltipHorizontalPosition, setTooltipHorizontalPosition] =
useState("left-1/2");
const updateToolTipPosition = () => {
if (hoveredItem !== null) {
const refIndex = data.findIndex(
(item, index) => item.id + index === hoveredItem
);
const ref = cardRefs.current[refIndex];
if (ref) {
const { top, height, left, width } = ref.getBoundingClientRect();
const adjustedTop = top + height / 2 - 64;
const bottomY = window.innerHeight - adjustedTop;
if (adjustedTop < bottomY) {
setTooltipPosition("top-1/2");
} else {
setTooltipPosition("bottom-1/2");
}
const adjustedLeft = left + width / 2;
const spaceRight = window.innerWidth - adjustedLeft;
if (spaceRight > 320) {
setTooltipHorizontalPosition("left-1/2");
} else {
setTooltipHorizontalPosition("right-1/2");
}
}
}
};
useEffect(() => {
updateToolTipPosition();
const handleScroll = () => {
updateToolTipPosition();
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [hoveredItem, data]);
return { tooltipPosition, tooltipHorizontalPosition, cardRefs };
};
export default useToolTipPosition;

View File

@@ -1,269 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useState, useEffect, useRef } from "react";
import getAnimeInfo from "@/src/utils/getAnimeInfo.utils";
import getEpisodes from "@/src/utils/getEpisodes.utils";
import getNextEpisodeSchedule from "../utils/getNextEpisodeSchedule.utils";
import getServers from "../utils/getServers.utils";
import getStreamInfo from "../utils/getStreamInfo.utils";
export const useWatch = (animeId, initialEpisodeId) => {
const [error, setError] = useState(null);
const [buffering, setBuffering] = useState(true);
const [streamInfo, setStreamInfo] = useState(null);
const [animeInfo, setAnimeInfo] = useState(null);
const [episodes, setEpisodes] = useState(null);
const [animeInfoLoading, setAnimeInfoLoading] = useState(false);
const [totalEpisodes, setTotalEpisodes] = useState(null);
const [seasons, setSeasons] = useState(null);
const [servers, setServers] = useState(null);
const [streamUrl, setStreamUrl] = useState(null);
const [isFullOverview, setIsFullOverview] = useState(false);
const [subtitles, setSubtitles] = useState([]);
const [thumbnail, setThumbnail] = useState(null);
const [intro, setIntro] = useState(null);
const [outro, setOutro] = useState(null);
const [episodeId, setEpisodeId] = useState(null);
const [activeEpisodeNum, setActiveEpisodeNum] = useState(null);
const [activeServerId, setActiveServerId] = useState(null);
const [activeServerType, setActiveServerType] = useState(null);
const [activeServerName, setActiveServerName] = useState(null);
const [serverLoading, setServerLoading] = useState(true);
const [nextEpisodeSchedule, setNextEpisodeSchedule] = useState(null);
const isServerFetchInProgress = useRef(false);
const isStreamFetchInProgress = useRef(false);
useEffect(() => {
setEpisodes(null);
setEpisodeId(null);
setActiveEpisodeNum(null);
setServers(null);
setActiveServerId(null);
setStreamInfo(null);
setStreamUrl(null);
setSubtitles([]);
setThumbnail(null);
setIntro(null);
setOutro(null);
setBuffering(true);
setServerLoading(true);
setError(null);
setAnimeInfo(null);
setSeasons(null);
setTotalEpisodes(null);
setAnimeInfoLoading(true);
isServerFetchInProgress.current = false;
isStreamFetchInProgress.current = false;
}, [animeId]);
useEffect(() => {
const fetchInitialData = async () => {
try {
setAnimeInfoLoading(true);
const [animeData, episodesData] = await Promise.all([
getAnimeInfo(animeId, false),
getEpisodes(animeId),
]);
setAnimeInfo(animeData?.data);
setSeasons(animeData?.seasons);
setEpisodes(episodesData?.episodes);
setTotalEpisodes(episodesData?.totalEpisodes);
const newEpisodeId =
initialEpisodeId ||
(episodesData?.episodes?.length > 0
? episodesData.episodes[0].id.match(/ep=(\d+)/)?.[1]
: null);
setEpisodeId(newEpisodeId);
} catch (err) {
console.error("Error fetching initial data:", err);
setError(err.message || "An error occurred.");
} finally {
setAnimeInfoLoading(false);
}
};
fetchInitialData();
}, [animeId]);
useEffect(() => {
const fetchNextEpisodeSchedule = async () => {
try {
const data = await getNextEpisodeSchedule(animeId);
setNextEpisodeSchedule(data);
} catch (err) {
console.error("Error fetching next episode schedule:", err);
}
};
fetchNextEpisodeSchedule();
}, [animeId]);
useEffect(() => {
if (!episodes || !episodeId) {
setActiveEpisodeNum(null);
return;
}
const activeEpisode = episodes.find((episode) => {
const match = episode.id.match(/ep=(\d+)/);
return match && match[1] === episodeId;
});
const newActiveEpisodeNum = activeEpisode ? activeEpisode.episode_no : null;
if (activeEpisodeNum !== newActiveEpisodeNum) {
setActiveEpisodeNum(newActiveEpisodeNum);
}
}, [episodeId, episodes]);
useEffect(() => {
if (!episodeId || !episodes || isServerFetchInProgress.current) return;
const fetchServers = async () => {
isServerFetchInProgress.current = true;
setServerLoading(true);
try {
const data = await getServers(animeId, episodeId);
console.log(data);
const filteredServers = data?.filter(
(server) =>
server.serverName === "HD-1" ||
server.serverName === "HD-2" ||
server.serverName === "HD-3"
);
if (filteredServers.some((s) => s.type === "sub")) {
filteredServers.push({
type: "sub",
data_id: "69696969",
server_id: "41",
serverName: "HD-4",
});
}
if (filteredServers.some((s) => s.type === "dub")) {
filteredServers.push({
type: "dub",
data_id: "96969696",
server_id: "42",
serverName: "HD-4",
});
}
const savedServerName = localStorage.getItem("server_name");
const savedServerType = localStorage.getItem("server_type");
let initialServer =
data.find(
(s) =>
s.serverName === savedServerName && s.type === savedServerType
) ||
data.find((s) => s.serverName === savedServerName) ||
data.find((s) => s.type === savedServerType) ||
data.find(
(s) => s.serverName === "HD-1" && s.type === savedServerType
) ||
data.find(
(s) => s.serverName === "HD-2" && s.type === savedServerType
) ||
data.find(
(s) => s.serverName === "HD-3" && s.type === savedServerType
) ||
data.find(
(s) => s.serverName === "HD-4" && s.type === savedServerType
) ||
filteredServers[0];
setServers(filteredServers);
setActiveServerType(initialServer?.type);
setActiveServerName(initialServer?.serverName);
setActiveServerId(initialServer?.data_id);
} catch (error) {
console.error("Error fetching servers:", error);
setError(error.message || "An error occurred.");
} finally {
setServerLoading(false);
isServerFetchInProgress.current = false;
}
};
fetchServers();
}, [episodeId, episodes]);
// Fetch stream info only when episodeId, activeServerId, and servers are ready
useEffect(() => {
if (
!episodeId ||
!activeServerId ||
!servers ||
isServerFetchInProgress.current ||
isStreamFetchInProgress.current
)
return;
if (
(activeServerName?.toLowerCase() === "hd-1"
|| activeServerName?.toLowerCase() === "hd-2"|| activeServerName?.toLowerCase() === "hd-3"|| activeServerName?.toLowerCase() === "hd-4")
&&
!serverLoading
) {
setBuffering(false);
return;
}
const fetchStreamInfo = async () => {
isStreamFetchInProgress.current = true;
setBuffering(true);
try {
const server = servers.find((srv) => srv.data_id === activeServerId);
if (server) {
const data = await getStreamInfo(
animeId,
episodeId,
server.serverName.toLowerCase(),
server.type.toLowerCase()
);
setStreamInfo(data);
setStreamUrl(data?.streamingLink?.link?.file || null);
setIntro(data?.streamingLink?.intro || null);
setOutro(data?.streamingLink?.outro || null);
const subtitles =
data?.streamingLink?.tracks
?.filter((track) => track.kind === "captions")
.map(({ file, label }) => ({ file, label })) || [];
setSubtitles(subtitles);
const thumbnailTrack = data?.streamingLink?.tracks?.find(
(track) => track.kind === "thumbnails" && track.file
);
if (thumbnailTrack) setThumbnail(thumbnailTrack.file);
} else {
setError("No server found with the activeServerId.");
}
} catch (err) {
console.error("Error fetching stream info:", err);
setError(err.message || "An error occurred.");
} finally {
setBuffering(false);
isStreamFetchInProgress.current = false;
}
};
fetchStreamInfo();
}, [episodeId, activeServerId, servers]);
return {
error,
buffering,
serverLoading,
streamInfo,
animeInfo,
episodes,
nextEpisodeSchedule,
animeInfoLoading,
totalEpisodes,
seasons,
servers,
streamUrl,
isFullOverview,
setIsFullOverview,
subtitles,
thumbnail,
intro,
outro,
episodeId,
setEpisodeId,
activeEpisodeNum,
setActiveEpisodeNum,
activeServerId,
setActiveServerId,
activeServerType,
setActiveServerType,
activeServerName,
setActiveServerName,
};
};

View File

@@ -1,34 +0,0 @@
import { useState, useEffect } from "react";
export default function useWatchControl() {
const [autoPlay, setAutoPlay] = useState(
() => JSON.parse(localStorage.getItem("autoPlay")) || false
);
const [autoSkipIntro, setAutoSkipIntro] = useState(
() => JSON.parse(localStorage.getItem("autoSkipIntro")) || false
);
const [autoNext, setAutoNext] = useState(
() => JSON.parse(localStorage.getItem("autoNext")) || false
);
useEffect(() => {
localStorage.setItem("autoPlay", JSON.stringify(autoPlay));
}, [autoPlay]);
useEffect(() => {
localStorage.setItem("autoSkipIntro", JSON.stringify(autoSkipIntro));
}, [autoSkipIntro]);
useEffect(() => {
localStorage.setItem("autoNext", JSON.stringify(autoNext));
}, [autoNext]);
return {
autoPlay,
setAutoPlay,
autoSkipIntro,
setAutoSkipIntro,
autoNext,
setAutoNext,
};
}

View File

@@ -1,126 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #201f31;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow-y: scroll;
}
.scrollbar-visible {
scrollbar-width: auto;
scrollbar-color: #888 #333;
}
.scrollbar-visible::-webkit-scrollbar {
width: 20px;
}
.scrollbar-visible::-webkit-scrollbar-thumb {
background-color: #888;
}
.scrollbar-visible::-webkit-scrollbar-track {
background: black;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
::-webkit-scrollbar {
width: 16px;
}
::-webkit-scrollbar-track {
background: #23222c;
}
::-webkit-scrollbar-thumb {
background: #65646a;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.is-visible {
opacity: 1;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
.dot {
width: 4px;
height: 4px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
display: inline-block;
}

View File

@@ -1,13 +0,0 @@
import { LanguageProvider } from './context/LanguageContext';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<LanguageProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</LanguageProvider>
);

View File

@@ -1,82 +0,0 @@
import website_name from "@/src/config/website.js";
import Spotlight from "@/src/components/spotlight/Spotlight.jsx";
import Trending from "@/src/components/trending/Trending.jsx";
import Cart from "@/src/components/cart/Cart.jsx";
import CategoryCard from "@/src/components/categorycard/CategoryCard.jsx";
import Genre from "@/src/components/genres/Genre.jsx";
import Topten from "@/src/components/topten/Topten.jsx";
import Loader from "@/src/components/Loader/Loader.jsx";
import Error from "@/src/components/error/Error.jsx";
import { useHomeInfo } from "@/src/context/HomeInfoContext.jsx";
import Schedule from "@/src/components/schedule/Schedule";
import ContinueWatching from "@/src/components/continue/ContinueWatching";
function Home() {
const { homeInfo, homeInfoLoading, error } = useHomeInfo();
if (homeInfoLoading) return <Loader type="home" />;
if (error) return <Error />;
if (!homeInfo) return <Error error="404" />;
return (
<>
<div className="px-4 w-full max-[1200px]:px-0">
<Spotlight spotlights={homeInfo.spotlights} />
<ContinueWatching />
<Trending trending={homeInfo.trending} />
<div className="mt-10 flex gap-6 max-[1200px]:px-4 max-[1200px]:grid max-[1200px]:grid-cols-2 max-[1200px]:mt-12 max-[1200px]:gap-y-10 max-[680px]:grid-cols-1">
<Cart
label="Top Airing"
data={homeInfo.top_airing}
path="top-airing"
/>
<Cart
label="Most Popular"
data={homeInfo.most_popular}
path="most-popular"
/>
<Cart
label="Most Favorite"
data={homeInfo.most_favorite}
path="most-favorite"
/>
<Cart
label="Latest Completed"
data={homeInfo.latest_completed}
path="completed"
/>
</div>
<div className="w-full grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex flex-col max-[1200px]:px-4">
<div>
<CategoryCard
label="Latest Episode"
data={homeInfo.latest_episode}
className={"mt-[60px]"}
path="recently-updated"
limit={12}
/>
<CategoryCard
label={`New On ${website_name}`}
data={homeInfo.recently_added}
className={"mt-[60px]"}
path="recently-added"
limit={12}
/>
<Schedule />
<CategoryCard
label="Top Upcoming"
data={homeInfo.top_upcoming}
className={"mt-[30px]"}
path="top-upcoming"
limit={12}
/>
</div>
<div className="w-full mt-[60px]">
<Genre data={homeInfo.genres} />
<Topten data={homeInfo.topten} className={"mt-12"} />
</div>
</div>
</div>
</>
);
}
export default Home;

View File

@@ -1,118 +0,0 @@
import { useEffect, useState } from "react";
import { useSearchParams, Link } from "react-router-dom";
import getCategoryInfo from "@/src/utils/getCategoryInfo.utils";
import CategoryCard from "@/src/components/categorycard/CategoryCard";
import Loader from "@/src/components/Loader/Loader";
import Error from "@/src/components/error/Error";
import PageSlider from "@/src/components/pageslider/PageSlider";
function AtoZ({ path }) {
const [searchParams, setSearchParams] = useSearchParams();
const [categoryInfo, setCategoryInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [totalPages, setTotalPages] = useState(0);
const page = parseInt(searchParams.get("page")) || 1;
const currentLetter = path.split("/").pop() || "";
useEffect(() => {
const fetchAtoZInfo = async () => {
setLoading(true);
try {
const data = await getCategoryInfo(path, page);
setCategoryInfo(data.data);
setTotalPages(data.totalPages);
setLoading(false);
} catch (err) {
setError(err);
setLoading(false);
console.error("Error fetching category info:", err);
}
};
fetchAtoZInfo();
window.scrollTo(0, 0);
}, [path, page]);
if (loading) return <Loader type="AtoZ" />;
if (error) {
return <Error />;
}
if (!categoryInfo) {
return null;
}
const handlePageChange = (newPage) => {
setSearchParams({ page: newPage });
};
return (
<div className="max-w-[1260px] mx-auto px-[15px] flex flex-col mt-[64px] max-md:mt-[50px]">
<ul className="flex gap-x-2 mt-[50px] items-center w-fit max-[1200px]:hidden">
<li className="flex gap-x-3 items-center">
<Link to="/home" className="text-white hover:text-[#FFBADE] text-[17px]">
Home
</Link>
<div className="dot mt-[1px] bg-white"></div>
</li>
<li className="font-light">A-Z List</li>
</ul>
<div className="flex flex-col gap-y-5 mt-6">
<h1 className="font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px]">
Sort By Letters
</h1>
<div className="flex gap-x-[7px] flex-wrap justify-start gap-y-2 max-md:justify-start">
{[
"All",
"#",
"0-9",
...Array.from({ length: 26 }, (_, i) =>
String.fromCharCode(65 + i)
),
].map((item, index) => {
const linkPath =
item.toLowerCase() === "all"
? ""
: item === "#"
? "other"
: item;
const isActive =
(currentLetter === "az-list" && item.toLowerCase() === "all") ||
(currentLetter === "other" && item === "#") ||
currentLetter === item.toLowerCase();
return (
<Link
to={`/az-list/${linkPath}`}
key={index}
className={`text-md bg-[#373646] py-1 px-4 rounded-md font-bold hover:text-black hover:bg-[#FFBADE] hover:cursor-pointer transition-all ease-out ${
isActive ? "text-black bg-[#FFBADE]" : ""
}`}
>
{item}
</Link>
);
})}
</div>
</div>
<div className="w-full flex flex-col gap-y-8">
<div>
{categoryInfo && categoryInfo.length > 0 && (
<CategoryCard
data={categoryInfo}
limit={categoryInfo.length}
showViewMore={false}
className="mt-0"
cardStyle="max-[1400px]:h-[35vw]"
/>
)}
<PageSlider
page={page}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
</div>
</div>
</div>
);
}
export default AtoZ;

View File

@@ -1,416 +0,0 @@
import getAnimeInfo from "@/src/utils/getAnimeInfo.utils";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faPlay,
faClosedCaptioning,
faMicrophone,
} from "@fortawesome/free-solid-svg-icons";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import website_name from "@/src/config/website";
import CategoryCard from "@/src/components/categorycard/CategoryCard";
import Sidecard from "@/src/components/sidecard/Sidecard";
import Loader from "@/src/components/Loader/Loader";
import Error from "@/src/components/error/Error";
import { useLanguage } from "@/src/context/LanguageContext";
import { useHomeInfo } from "@/src/context/HomeInfoContext";
import Voiceactor from "@/src/components/voiceactor/Voiceactor";
function InfoItem({ label, value, isProducer = true }) {
return (
value && (
<div className="text-[14px] font-bold">
{`${label}: `}
<span className="font-light">
{Array.isArray(value) ? (
value.map((item, index) =>
isProducer ? (
<Link
to={`/producer/${item
.replace(/[&'"^%$#@!()+=<>:;,.?/\\|{}[\]`~*_]/g, "")
.split(" ")
.join("-")
.replace(/-+/g, "-")}`}
key={index}
className="cursor-pointer hover:text-[#ffbade]"
>
{item}
{index < value.length - 1 && ", "}
</Link>
) : (
<span key={index} className="cursor-pointer">
{item}
</span>
)
)
) : isProducer ? (
<Link
to={`/producer/${value
.replace(/[&'"^%$#@!()+=<>:;,.?/\\|{}[\]`~*_]/g, "")
.split(" ")
.join("-")
.replace(/-+/g, "-")}`}
className="cursor-pointer hover:text-[#ffbade]"
>
{value}
</Link>
) : (
<span className="cursor-pointer">{value}</span>
)}
</span>
</div>
)
);
}
function Tag({ bgColor, index, icon, text }) {
return (
<div
className={`flex space-x-1 justify-center items-center px-[4px] py-[1px] text-black font-bold text-[13px] ${
index === 0 ? "rounded-l-[4px]" : "rounded-none"
}`}
style={{ backgroundColor: bgColor }}
>
{icon && <FontAwesomeIcon icon={icon} className="text-[12px]" />}
<p className="text-[12px]">{text}</p>
</div>
);
}
function AnimeInfo({ random = false }) {
const { language } = useLanguage();
const { id: paramId } = useParams();
const id = random ? null : paramId;
const [isFull, setIsFull] = useState(false);
const [animeInfo, setAnimeInfo] = useState(null);
const [seasons, setSeasons] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { homeInfo } = useHomeInfo();
const { id: currentId } = useParams();
const navigate = useNavigate();
useEffect(() => {
if (id === "404-not-found-page") {
console.log("404 got!");
return null;
} else {
const fetchAnimeInfo = async () => {
setLoading(true);
try {
const data = await getAnimeInfo(id, random);
setSeasons(data?.seasons);
setAnimeInfo(data.data);
} catch (err) {
console.error("Error fetching anime info:", err);
setError(err);
} finally {
setLoading(false);
}
};
fetchAnimeInfo();
window.scrollTo({ top: 0, behavior: "smooth" });
}
}, [id, random]);
useEffect(() => {
if (animeInfo && location.pathname === `/${animeInfo.id}`) {
document.title = `Watch ${animeInfo.title} English Sub/Dub online Free on ${website_name}`;
}
return () => {
document.title = `${website_name} | Free anime streaming platform`;
};
}, [animeInfo]);
if (loading) return <Loader type="animeInfo" />;
if (error) {
return <Error />;
}
if (!animeInfo) {
navigate("/404-not-found-page");
return undefined;
}
const { title, japanese_title, poster, animeInfo: info } = animeInfo;
const tags = [
{
condition: info.tvInfo?.rating,
bgColor: "#ffffff",
text: info.tvInfo.rating,
},
{
condition: info.tvInfo?.quality,
bgColor: "#FFBADE",
text: info.tvInfo.quality,
},
{
condition: info.tvInfo?.sub,
icon: faClosedCaptioning,
bgColor: "#B0E3AF",
text: info.tvInfo.sub,
},
{
condition: info.tvInfo?.dub,
icon: faMicrophone,
bgColor: "#B9E7FF",
text: info.tvInfo.dub,
},
];
return (
<>
<div className="relative grid grid-cols-[minmax(0,75%),minmax(0,25%)] h-fit w-full overflow-hidden text-white mt-[64px] max-[1200px]:flex max-[1200px]:flex-col max-md:mt-[50px]">
<img
src={`https://wsrv.nl/?url=${poster}`}
alt={`${title} Poster`}
className="absolute inset-0 object-cover w-full h-full filter grayscale blur-lg z-[-900]"
/>
<div className="flex items-start z-10 px-14 py-[70px] bg-[#252434] bg-opacity-70 gap-x-8 max-[1024px]:px-6 max-[1024px]:py-10 max-[1024px]:gap-x-4 max-[575px]:flex-col max-[575px]:items-center max-[575px]:justify-center">
<div className="relative w-[180px] h-[270px] max-[575px]:w-[140px] max-[575px]:h-[200px] flex-shrink-0">
<img
src={`https://wsrv.nl/?url=${poster}`}
alt={`${title} Poster`}
className="w-full h-full object-cover object-center flex-shrink-0"
/>
{animeInfo.adultContent && (
<div className="text-white px-2 rounded-md bg-[#FF5700] absolute top-2 left-2 flex items-center justify-center text-[14px] font-bold">
18+
</div>
)}
</div>
<div className="flex flex-col ml-4 gap-y-5 max-[575px]:items-center max-[575px]:justify-center max-[575px]:mt-6 max-[1200px]:ml-0">
<ul className="flex gap-x-2 items-center w-fit max-[1200px]:hidden">
{[
["Home", "home"],
[info.tvInfo?.showType, info.tvInfo?.showType],
].map(([text, link], index) => (
<li key={index} className="flex gap-x-3 items-center">
<Link
to={`/${link}`}
className="text-white hover:text-[#FFBADE] text-[15px] font-semibold"
>
{text}
</Link>
<div className="dot mt-[1px] bg-white"></div>
</li>
))}
<p className="font-light text-[15px] text-gray-300 line-clamp-1 max-[575px]:leading-5">
{language === "EN" ? title : japanese_title}
</p>
</ul>
<h1 className="text-4xl font-semibold max-[1200px]:text-3xl max-[575px]:text-2xl max-[575px]:text-center max-[575px]:leading-7">
{language === "EN" ? title : japanese_title}
</h1>
<div className="flex flex-wrap w-fit gap-x-[2px] mt-3 max-[575px]:mx-auto max-[575px]:mt-0 gap-y-[3px] max-[320px]:justify-center">
{tags.map(
({ condition, icon, bgColor, text }, index) =>
condition && (
<Tag
key={index}
index={index}
bgColor={bgColor}
icon={icon}
text={text}
/>
)
)}
<div className="flex w-fit items-center ml-1">
{[info.tvInfo?.showType, info.tvInfo?.duration].map(
(item, index) =>
item && (
<div
key={index}
className="px-1 h-fit flex items-center gap-x-2 w-fit"
>
<div className="dot mt-[2px]"></div>
<p className="text-[14px]">{item}</p>
</div>
)
)}
</div>
</div>
{animeInfo?.animeInfo?.Status?.toLowerCase() !== "not-yet-aired" ? (
<Link
to={`/watch/${animeInfo.id}`}
className="flex gap-x-2 px-6 py-2 bg-[#FFBADE] w-fit text-black items-center rounded-3xl mt-5"
>
<FontAwesomeIcon
icon={faPlay}
className="text-[14px] mt-[1px]"
/>
<p className="text-lg font-medium">Watch Now</p>
</Link>
) : (
<div className="flex gap-x-2 px-6 py-2 bg-[#FFBADE] w-fit text-black items-center rounded-3xl mt-5">
<p className="text-lg font-medium">Not released</p>
</div>
)}
{info?.Overview && (
<div className="text-[14px] mt-2 max-[575px]:hidden">
{info.Overview.length > 270 ? (
<>
{isFull
? info.Overview
: `${info.Overview.slice(0, 270)}...`}
<span
className="text-[13px] font-bold hover:cursor-pointer"
onClick={() => setIsFull(!isFull)}
>
{isFull ? "- Less" : "+ More"}
</span>
</>
) : (
info.Overview
)}
</div>
)}
<p className="text-[14px] max-[575px]:hidden">
{`${website_name} is the best site to watch `}
<span className="font-bold">{title}</span>
{` SUB online, or you can even watch `}
<span className="font-bold">{title}</span>
{` DUB in HD quality.`}
</p>
<div className="flex gap-x-4 items-center mt-4 max-[575px]:w-full max-[575px]:justify-center max-[320px]:hidden">
<img
src="https://i.postimg.cc/d34WWyNQ/share-icon.gif"
alt="Share Anime"
className="w-[60px] h-auto rounded-full max-[1024px]:w-[40px]"
/>
<div className="flex flex-col w-fit">
<p className="text-[15px] font-bold text-[#FFBADE]">
Share Anime
</p>
<p className="text-[16px] text-white">to your friends</p>
</div>
</div>
</div>
</div>
<div className="bg-[#4c4b57c3] flex items-center px-8 max-[1200px]:py-10 max-[1200px]:bg-[#363544e0] max-[575px]:p-4">
<div className="w-full flex flex-col h-fit gap-y-3">
{info?.Overview && (
<div className="custom-xl:hidden max-h-[150px] overflow-hidden">
<p className="text-[13px] font-bold">Overview:</p>
<div className="max-h-[110px] mt-2 overflow-y-scroll">
<p className="text-[14px] font-light">{info.Overview}</p>
</div>
</div>
)}
{[
{ label: "Japanese", value: info?.Japanese },
{ label: "Synonyms", value: info?.Synonyms },
{ label: "Aired", value: info?.Aired },
{ label: "Premiered", value: info?.Premiered },
{ label: "Duration", value: info?.Duration },
{ label: "Status", value: info?.Status },
{ label: "MAL Score", value: info?.["MAL Score"] },
].map(({ label, value }, index) => (
<InfoItem
key={index}
label={label}
value={value}
isProducer={false}
/>
))}
{info?.Genres && (
<div className="flex gap-x-2 py-2 custom-xl:border-t custom-xl:border-b custom-xl:border-white/20 max-[1200px]:border-none">
<p>Genres:</p>
<div className="flex flex-wrap gap-2">
{info.Genres.map((genre, index) => (
<Link
to={`/genre/${genre.split(" ").join("-")}`}
key={index}
className="text-[14px] font-semibold px-2 py-[1px] border border-gray-400 rounded-2xl hover:text-[#ffbade]"
>
{genre}
</Link>
))}
</div>
</div>
)}
{[
{ label: "Studios", value: info?.Studios },
{ label: "Producers", value: info?.Producers },
].map(({ label, value }, index) => (
<InfoItem key={index} label={label} value={value} />
))}
<p className="text-[14px] mt-4 custom-xl:hidden">
{`${website_name} is the best site to watch `}
<span className="font-bold">{title}</span>
{` SUB online, or you can even watch `}
<span className="font-bold">{title}</span>
{` DUB in HD quality.`}
</p>
</div>
</div>
</div>
<div className="w-full px-4 grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex flex-col">
<div>
{seasons?.length > 0 && (
<div className="flex flex-col gap-y-7 mt-8">
<h1 className="w-fit text-2xl text-[#ffbade] max-[478px]:text-[18px] font-bold">
More Seasons
</h1>
<div className="flex flex-wrap gap-4 max-[575px]:grid max-[575px]:grid-cols-3 max-[575px]:gap-3 max-[480px]:grid-cols-2">
{seasons.map((season, index) => (
<Link
to={`/${season.id}`}
key={index}
className={`relative w-[20%] h-[60px] rounded-lg overflow-hidden cursor-pointer group ${
currentId === String(season.id)
? "border border-[#ffbade]"
: ""
} max-[1200px]:w-[140px] max-[575px]:w-full`}
>
<p
className={`text-[13px] text-center font-bold absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full px-2 z-30 line-clamp-2 group-hover:text-[#ffbade] ${
currentId === String(season.id)
? "text-[#ffbade]"
: "text-white"
}`}
>
{season.season}
</p>
<div className="absolute inset-0 z-10 bg-[url('https://i.postimg.cc/pVGY6RXd/thumb.png')] bg-repeat"></div>
<img
src={season.season_poster}
alt=""
className="w-full h-full object-cover blur-[3px] opacity-50"
/>
</Link>
))}
</div>
</div>
)}
{animeInfo?.charactersVoiceActors.length > 0 && (
<Voiceactor animeInfo={animeInfo} />
)}
{animeInfo.recommended_data.length > 0 && (
<CategoryCard
label="Recommended for you"
data={animeInfo.recommended_data}
limit={animeInfo.recommended_data.length}
showViewMore={false}
className={"mt-8"}
/>
)}
</div>
<div>
{animeInfo.related_data.length > 0 && (
<Sidecard
label="Related Anime"
data={animeInfo.related_data}
className="mt-8"
/>
)}
{homeInfo && homeInfo.most_popular && (
<Sidecard
label="Most Popular"
data={homeInfo.most_popular.slice(0, 10)}
className="mt-[40px]"
limit={10}
/>
)}
</div>
</div>
</>
);
}
export default AnimeInfo;

View File

@@ -1,111 +0,0 @@
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import getCategoryInfo from "@/src/utils/getCategoryInfo.utils";
import CategoryCard from "@/src/components/categorycard/CategoryCard";
import Genre from "@/src/components/genres/Genre";
import Topten from "@/src/components/topten/Topten";
import Loader from "@/src/components/Loader/Loader";
import Error from "@/src/components/error/Error";
import { useNavigate } from "react-router-dom";
import { useHomeInfo } from "@/src/context/HomeInfoContext";
import PageSlider from "@/src/components/pageslider/PageSlider";
import SidecardLoader from "@/src/components/Loader/Sidecard.loader";
function Category({ path, label }) {
const [searchParams, setSearchParams] = useSearchParams();
const [categoryInfo, setCategoryInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [totalPages, setTotalPages] = useState(0);
const page = parseInt(searchParams.get("page")) || 1;
const { homeInfo, homeInfoLoading } = useHomeInfo();
const navigate = useNavigate();
useEffect(() => {
const fetchCategoryInfo = async () => {
setLoading(true);
try {
const data = await getCategoryInfo(path, page);
setCategoryInfo(data.data);
setTotalPages(data.totalPages);
setLoading(false);
} catch (err) {
setError(err);
console.error("Error fetching category info:", err);
}
};
fetchCategoryInfo();
window.scrollTo(0, 0);
}, [path, page]);
if (loading) return <Loader type="category" />;
if (error) {
navigate("/error-page");
return <Error />;
}
if (!categoryInfo) {
navigate("/404-not-found-page");
return null;
}
const handlePageChange = (newPage) => {
setSearchParams({ page: newPage });
};
return (
<div className="w-full flex flex-col gap-y-4 mt-[64px] max-md:mt-[50px]">
<div className="w-full flex gap-x-4 items-center bg-[#191826] p-5 max-[575px]:px-3 max-[320px]:hidden">
<img
src="https://i.postimg.cc/d34WWyNQ/share-icon.gif"
alt="Share Anime"
className="w-[60px] h-auto rounded-full max-[1024px]:w-[40px] max-[575px]:hidden"
/>
<div className="flex flex-col w-fit">
<p className="text-[15px] font-bold text-[#FFBADE]">Share Anime</p>
<p className="text-[16px] text-white">to your friends</p>
</div>
</div>
{categoryInfo ? (
<div className="w-full px-4 grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex max-[1200px]:flex-col max-[1200px]:gap-y-10">
{page > totalPages ? (
<p className="font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px] max-[300px]:leading-6">
You came a long way, go back <br className="max-[300px]:hidden" />
nothing is here
</p>
) : (
<div>
{categoryInfo && categoryInfo.length > 0 && (
<CategoryCard
label={label.split("/").pop()}
data={categoryInfo}
showViewMore={false}
className={"mt-0"}
categoryPage={true}
path={path}
/>
)}
<PageSlider
page={page}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
</div>
)}
<div className="w-full flex flex-col gap-y-10">
{homeInfoLoading ? (
<SidecardLoader />
) : (
<>
{homeInfo && homeInfo.topten && (
<Topten data={homeInfo.topten} className="mt-0" />
)}
{homeInfo?.genres && <Genre data={homeInfo.genres} />}
</>
)}
</div>
</div>
) : (
<Error />
)}
</div>
);
}
export default Category;

View File

@@ -1,74 +0,0 @@
import CategoryCard from '@/src/components/categorycard/CategoryCard';
import Genre from '@/src/components/genres/Genre';
import CategoryCardLoader from '@/src/components/Loader/CategoryCard.loader';
import SidecardLoader from '@/src/components/Loader/Sidecard.loader';
import PageSlider from '@/src/components/pageslider/PageSlider';
import Sidecard from '@/src/components/sidecard/Sidecard';
import { useHomeInfo } from '@/src/context/HomeInfoContext';
import getSearch from '@/src/utils/getSearch.utils';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
function Search() {
const { homeInfo, homeInfoLoading } = useHomeInfo();
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get("keyword");
const page = parseInt(searchParams.get("page"), 10) || 1;
const [searchData, setSearchData] = useState(null);
const [totalPages, setTotalPages] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchSearch = async () => {
setLoading(true);
try {
const data = await getSearch(keyword,page);
setSearchData(data.data);
setTotalPages(data.totalPage);
setLoading(false);
} catch (err) {
console.error("Error fetching anime info:", err);
setError(err);
setLoading(false);
}
};
fetchSearch();
window.scrollTo({ top: 0, behavior: 'smooth' });
}, [keyword, page]);
const handlePageChange = (newPage) => {
setSearchParams({ keyword, page: newPage });
};
return (
<div className='w-full px-4 mt-[128px] grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex max-[1200px]:flex-col max-[1200px]:gap-y-10 max-custom-md:mt-[80px] max-[478px]:mt-[60px]'>
{loading ? (
<CategoryCardLoader className={"max-[478px]:mt-2"} />
) : page > totalPages ? <p className='font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px] max-[300px]:leading-6'>You came a long way, go back <br className='max-[300px]:hidden' />nothing is here</p> : searchData && searchData.length > 0 ? (
<div>
<CategoryCard
label={`Search results for: ${keyword}`}
data={searchData}
showViewMore={false}
className={"mt-0"}
/>
<PageSlider page={page} totalPages={totalPages} handlePageChange={handlePageChange} />
</div>
) : error ? <p className='font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px]'>Couldn&apos;t get search result please try again</p> : (
<h1 className='font-bold text-2xl text-[#ffbade] max-[478px]:text-[18px]'>{`Search results for: ${keyword}`}</h1>
)}
<div className="w-full flex flex-col gap-y-10">
{homeInfoLoading ? (
<SidecardLoader />
) : (
<>
{homeInfo?.most_popular && <Sidecard data={homeInfo.most_popular} className="mt-0" label="Most Popular" />}
{homeInfo?.genres && <Genre data={homeInfo.genres} />}
</>
)}
</div>
</div>
);
}
export default Search;

View File

@@ -1,541 +0,0 @@
/* eslint-disable react/prop-types */
import { useEffect, useRef, useState } from "react";
import { useLocation, useParams, Link, useNavigate } from "react-router-dom";
import { useLanguage } from "@/src/context/LanguageContext";
import { useHomeInfo } from "@/src/context/HomeInfoContext";
import { useWatch } from "@/src/hooks/useWatch";
import BouncingLoader from "@/src/components/ui/bouncingloader/Bouncingloader";
import IframePlayer from "@/src/components/player/IframePlayer";
import Episodelist from "@/src/components/episodelist/Episodelist";
import website_name from "@/src/config/website";
import Sidecard from "@/src/components/sidecard/Sidecard";
import CategoryCard from "@/src/components/categorycard/CategoryCard";
import {
faClosedCaptioning,
faMicrophone,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Servers from "@/src/components/servers/Servers";
import CategoryCardLoader from "@/src/components/Loader/CategoryCard.loader";
import { Skeleton } from "@/src/components/ui/Skeleton/Skeleton";
import SidecardLoader from "@/src/components/Loader/Sidecard.loader";
import Voiceactor from "@/src/components/voiceactor/Voiceactor";
import Watchcontrols from "@/src/components/watchcontrols/Watchcontrols";
import useWatchControl from "@/src/hooks/useWatchControl";
import Player from "@/src/components/player/Player";
export default function Watch() {
const location = useLocation();
const navigate = useNavigate();
const { id: animeId } = useParams();
const queryParams = new URLSearchParams(location.search);
let initialEpisodeId = queryParams.get("ep");
const [tags, setTags] = useState([]);
const { language } = useLanguage();
const { homeInfo } = useHomeInfo();
const isFirstSet = useRef(true);
const [showNextEpisodeSchedule, setShowNextEpisodeSchedule] = useState(true);
const {
// error,
buffering,
streamInfo,
streamUrl,
animeInfo,
episodes,
nextEpisodeSchedule,
animeInfoLoading,
totalEpisodes,
isFullOverview,
intro,
outro,
subtitles,
thumbnail,
setIsFullOverview,
activeEpisodeNum,
seasons,
episodeId,
setEpisodeId,
activeServerId,
setActiveServerId,
servers,
serverLoading,
activeServerType,
setActiveServerType,
activeServerName,
setActiveServerName
} = useWatch(animeId, initialEpisodeId);
const {
autoPlay,
setAutoPlay,
autoSkipIntro,
setAutoSkipIntro,
autoNext,
setAutoNext,
} = useWatchControl();
useEffect(() => {
if (!episodes || episodes.length === 0) return;
const isValidEpisode = episodes.some(ep => {
const epNumber = ep.id.split('ep=')[1];
return epNumber === episodeId;
});
// If missing or invalid episodeId, fallback to first
if (!episodeId || !isValidEpisode) {
const fallbackId = episodes[0].id.match(/ep=(\d+)/)?.[1];
if (fallbackId && fallbackId !== episodeId) {
setEpisodeId(fallbackId);
}
return;
}
const newUrl = `/watch/${animeId}?ep=${episodeId}`;
if (isFirstSet.current) {
navigate(newUrl, { replace: true });
isFirstSet.current = false;
} else {
navigate(newUrl);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [episodeId, animeId, navigate, episodes]);
// Update document title
useEffect(() => {
if (animeInfo) {
document.title = `Watch ${animeInfo.title} English Sub/Dub online Free on ${website_name}`;
}
return () => {
document.title = `${website_name} | Free anime streaming platform`;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animeId]);
// Redirect if no episodes
useEffect(() => {
if (totalEpisodes !== null && totalEpisodes === 0) {
navigate(`/${animeId}`);
}
}, [streamInfo, episodeId, animeId, totalEpisodes, navigate]);
useEffect(() => {
const adjustHeight = () => {
if (window.innerWidth > 1200) {
const player = document.querySelector(".player");
const episodes = document.querySelector(".episodes");
if (player && episodes) {
episodes.style.height = `${player.clientHeight}px`;
}
} else {
const episodes = document.querySelector(".episodes");
if (episodes) {
episodes.style.height = "auto";
}
}
};
adjustHeight();
window.addEventListener("resize", adjustHeight);
return () => {
window.removeEventListener("resize", adjustHeight);
};
});
function Tag({ bgColor, index, icon, text }) {
return (
<div
className={`flex space-x-1 justify-center items-center px-[4px] py-[1px] text-black font-semibold text-[13px] ${
index === 0 ? "rounded-l-[4px]" : "rounded-none"
}`}
style={{ backgroundColor: bgColor }}
>
{icon && <FontAwesomeIcon icon={icon} className="text-[12px]" />}
<p className="text-[12px]">{text}</p>
</div>
);
}
useEffect(() => {
setTags([
{
condition: animeInfo?.animeInfo?.tvInfo?.rating,
bgColor: "#ffffff",
text: animeInfo?.animeInfo?.tvInfo?.rating,
},
{
condition: animeInfo?.animeInfo?.tvInfo?.quality,
bgColor: "#FFBADE",
text: animeInfo?.animeInfo?.tvInfo?.quality,
},
{
condition: animeInfo?.animeInfo?.tvInfo?.sub,
icon: faClosedCaptioning,
bgColor: "#B0E3AF",
text: animeInfo?.animeInfo?.tvInfo?.sub,
},
{
condition: animeInfo?.animeInfo?.tvInfo?.dub,
icon: faMicrophone,
bgColor: "#B9E7FF",
text: animeInfo?.animeInfo?.tvInfo?.dub,
},
]);
}, [animeId, animeInfo]);
return (
<div className="w-full h-fit flex flex-col justify-center items-center relative">
<div className="w-full relative max-[1400px]:px-[30px] max-[1200px]:px-[80px] max-[1024px]:px-0">
<img
src={
!animeInfoLoading
? `https://wsrv.nl/?url=${animeInfo?.poster}`
: "https://i.postimg.cc/rFZnx5tQ/2-Kn-Kzog-md.webp"
}
alt={`${animeInfo?.title} Poster`}
className="absolute inset-0 w-full h-full object-cover filter grayscale z-[-900]"
/>
<div className="absolute inset-0 bg-[#3a3948] bg-opacity-80 backdrop-blur-md z-[-800]"></div>
<div className="relative z-10 px-4 pb-[50px] grid grid-cols-[minmax(0,75%),minmax(0,25%)] w-full h-full mt-[128px] max-[1400px]:flex max-[1400px]:flex-col max-[1200px]:mt-[64px] max-[1024px]:px-0 max-md:mt-[50px]">
{animeInfo && (
<ul className="flex absolute left-4 top-[-40px] gap-x-2 items-center w-fit max-[1200px]:hidden">
{[
["Home", "home"],
[animeInfo?.showType, animeInfo?.showType],
].map(([text, link], index) => (
<li key={index} className="flex gap-x-3 items-center">
<Link
to={`/${link}`}
className="text-white hover:text-[#FFBADE] text-[15px] font-semibold"
>
{text}
</Link>
<div className="dot mt-[1px] bg-white"></div>
</li>
))}
<p className="font-light text-[15px] text-gray-300 line-clamp-1 max-[575px]:leading-5">
Watching{" "}
{language === "EN"
? animeInfo?.title
: animeInfo?.japanese_title}
</p>
</ul>
)}
<div className="flex w-full min-h-fit max-[1200px]:flex-col-reverse">
<div className="episodes w-[35%] bg-[#191826] flex justify-center items-center max-[1400px]:w-[380px] max-[1200px]:w-full max-[1200px]:h-full max-[1200px]:min-h-[100px]">
{!episodes ? (
<BouncingLoader />
) : (
<Episodelist
episodes={episodes}
currentEpisode={episodeId}
onEpisodeClick={(id) => setEpisodeId(id)}
totalEpisodes={totalEpisodes}
/>
)}
</div>
<div className="player w-full h-fit bg-black flex flex-col">
<div className="w-full relative h-[480px] max-[1400px]:h-[40vw] max-[1200px]:h-[48vw] max-[1024px]:h-[58vw] max-[600px]:h-[65vw]">
{!buffering ? (( activeServerName.toLowerCase()==="hd-1" || activeServerName.toLowerCase()==="hd-2" || activeServerName.toLowerCase()==="hd-3" || activeServerName.toLowerCase()==="hd-4") ?
<IframePlayer
animeId={animeId}
episodeId={episodeId}
servertype={activeServerType}
serverName={activeServerName}
animeInfo={animeInfo}
episodeNum={activeEpisodeNum}
episodes={episodes}
playNext={(id) => setEpisodeId(id)}
autoNext={autoNext}
/>:<Player
streamUrl={streamUrl}
subtitles={subtitles}
intro={intro}
outro={outro}
serverName={activeServerName.toLowerCase()}
thumbnail={thumbnail}
autoSkipIntro={autoSkipIntro}
autoPlay={autoPlay}
autoNext={autoNext}
episodeId={episodeId}
episodes={episodes}
playNext={(id) => setEpisodeId(id)}
animeInfo={animeInfo}
episodeNum={activeEpisodeNum}
streamInfo={streamInfo}
/>
) : (
<div className="absolute inset-0 flex justify-center items-center bg-black bg-opacity-50">
<BouncingLoader />
</div>
)}
<p className="text-center underline font-medium text-[15px] absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none">
{!buffering && !activeServerType ? (
servers ? (
<>
Probably this server is down, try other servers
<br />
Either reload or try again after sometime
</>
) : (
<>
Probably streaming server is down
<br />
Either reload or try again after sometime
</>
)
) : null}
</p>
</div>
{!buffering && (
<Watchcontrols
autoPlay={autoPlay}
setAutoPlay={setAutoPlay}
autoSkipIntro={autoSkipIntro}
setAutoSkipIntro={setAutoSkipIntro}
autoNext={autoNext}
setAutoNext={setAutoNext}
episodes={episodes}
totalEpisodes={totalEpisodes}
episodeId={episodeId}
onButtonClick={(id) => setEpisodeId(id)}
/>
)}
<Servers
servers={servers}
activeEpisodeNum={activeEpisodeNum}
activeServerId={activeServerId}
setActiveServerId={setActiveServerId}
serverLoading={serverLoading}
setActiveServerType={setActiveServerType}
activeServerType={activeServerType}
setActiveServerName={setActiveServerName}
/>
{seasons?.length > 0 && (
<div className="flex flex-col gap-y-2 bg-[#11101A] p-4">
<h1 className="w-fit text-lg max-[478px]:text-[18px] font-semibold">
Watch more seasons of this anime
</h1>
<div className="flex flex-wrap gap-4 max-[575px]:grid max-[575px]:grid-cols-3 max-[575px]:gap-3 max-[480px]:grid-cols-2">
{seasons.map((season, index) => (
<Link
to={`/${season.id}`}
key={index}
className={`relative w-[20%] h-[60px] rounded-lg overflow-hidden cursor-pointer group ${
animeId === String(season.id)
? "border border-[#ffbade]"
: ""
} max-[1200px]:w-[140px] max-[575px]:w-full`}
>
<p
className={`text-[13px] text-center font-bold absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-full px-2 z-30 line-clamp-2 group-hover:text-[#ffbade] ${
animeId === String(season.id)
? "text-[#ffbade]"
: "text-white"
}`}
>
{season.season}
</p>
<div className="absolute inset-0 z-10 bg-[url('https://i.postimg.cc/pVGY6RXd/thumb.png')] bg-repeat"></div>
<img
src={`https://wsrv.nl/?url=${season.season_poster}`}
alt=""
className="w-full h-full object-cover blur-[3px] opacity-50"
/>
</Link>
))}
</div>
</div>
)}
{nextEpisodeSchedule?.nextEpisodeSchedule &&
showNextEpisodeSchedule && (
<div className="p-4">
<div className="w-full px-4 rounded-md bg-[#0088CC] flex items-center justify-between gap-x-2">
<div className="w-full h-fit">
<span className="text-[18px]">🚀</span>
{" Estimated the next episode will come at "}
<span className="text-[13.4px] font-medium">
{new Date(
new Date(
nextEpisodeSchedule.nextEpisodeSchedule
).getTime() -
new Date().getTimezoneOffset() * 60000
).toLocaleDateString("en-GB", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
})}
</span>
</div>
<span
className="text-[25px] h-fit font-extrabold text-[#80C4E6] mb-1 cursor-pointer"
onClick={() => setShowNextEpisodeSchedule(false)}
>
×
</span>
</div>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-y-4 items-start ml-8 max-[1400px]:ml-0 max-[1400px]:mt-10 max-[1400px]:flex-row max-[1400px]:gap-x-6 max-[1024px]:px-[30px] max-[1024px]:mt-8 max-[500px]:mt-4 max-[500px]:px-4">
{animeInfo && animeInfo?.poster ? (
<img
src={`https://wsrv.nl/?url=${animeInfo?.poster}`}
alt=""
className="w-[100px] h-[150px] object-cover max-[500px]:w-[70px] max-[500px]:h-[90px]"
/>
) : (
<Skeleton className="w-[100px] h-[150px] rounded-none" />
)}
<div className="flex flex-col gap-y-4 justify-start">
{animeInfo && animeInfo?.title ? (
<p className="text-[26px] font-medium leading-6 max-[500px]:text-[18px]">
{language ? animeInfo?.title : animeInfo?.japanese_title}
</p>
) : (
<Skeleton className="w-[170px] h-[20px] rounded-xl" />
)}
<div className="flex flex-wrap w-fit gap-x-[2px] gap-y-[3px]">
{animeInfo ? (
tags.map(
({ condition, icon, bgColor, text }, index) =>
condition && (
<Tag
key={index}
index={index}
bgColor={bgColor}
icon={icon}
text={text}
/>
)
)
) : (
<Skeleton className="w-[70px] h-[20px] rounded-xl" />
)}
<div className="flex w-fit items-center ml-1">
{[
animeInfo?.animeInfo?.tvInfo?.showType,
animeInfo?.animeInfo?.tvInfo?.duration,
].map(
(item, index) =>
item && (
<div
key={index}
className="px-1 h-fit flex items-center gap-x-2 w-fit"
>
<div className="dot mt-[2px]"></div>
<p className="text-[14px]">{item}</p>
</div>
)
)}
</div>
</div>
{animeInfo ? (
animeInfo?.animeInfo?.Overview && (
<div className="max-h-[150px] overflow-hidden">
<div className="max-h-[110px] mt-2 overflow-y-auto">
<p className="text-[14px] font-[400]">
{animeInfo?.animeInfo?.Overview.length > 270 ? (
<>
{isFullOverview
? animeInfo?.animeInfo?.Overview
: `${animeInfo?.animeInfo?.Overview.slice(
0,
270
)}...`}
<span
className="text-[13px] font-bold hover:cursor-pointer"
onClick={() => setIsFullOverview(!isFullOverview)}
>
{isFullOverview ? "- Less" : "+ More"}
</span>
</>
) : (
animeInfo?.animeInfo?.Overview
)}
</p>
</div>
</div>
)
) : (
<div className="flex flex-col gap-y-2">
<Skeleton className="w-[200px] h-[10px] rounded-xl" />
<Skeleton className="w-[160px] h-[10px] rounded-xl" />
<Skeleton className="w-[100px] h-[10px] rounded-xl" />
<Skeleton className="w-[80px] h-[10px] rounded-xl" />
</div>
)}
<p className="text-[14px] max-[575px]:hidden">
{`${website_name} is the best site to watch `}
<span className="font-bold">
{language ? animeInfo?.title : animeInfo?.japanese_title}
</span>
{` SUB online, or you can even watch `}
<span className="font-bold">
{language ? animeInfo?.title : animeInfo?.japanese_title}
</span>
{` DUB in HD quality.`}
</p>
<Link
to={`/${animeId}`}
className="w-fit text-[13px] bg-white rounded-[12px] px-[10px] py-1 text-black"
>
View detail
</Link>
</div>
</div>
</div>
</div>
<div className="w-full flex gap-x-4 items-center bg-[#191826] p-5 max-[575px]:px-3 max-[320px]:hidden">
<img
src="https://i.postimg.cc/d34WWyNQ/share-icon.gif"
alt="Share Anime"
className="w-[60px] h-auto rounded-full max-[1024px]:w-[40px] max-[575px]:hidden"
/>
<div className="flex flex-col w-fit">
<p className="text-[15px] font-bold text-[#FFBADE]">Share Anime</p>
<p className="text-[16px] text-white">to your friends</p>
</div>
</div>
<div className="w-full px-4 grid grid-cols-[minmax(0,75%),minmax(0,25%)] gap-x-6 max-[1200px]:flex flex-col">
<div className="mt-[15px] flex flex-col gap-y-7">
{animeInfo?.charactersVoiceActors.length > 0 && (
<Voiceactor animeInfo={animeInfo} className="!mt-0" />
)}
{animeInfo?.recommended_data.length > 0 ? (
<CategoryCard
label="Recommended for you"
data={animeInfo?.recommended_data}
limit={animeInfo?.recommended_data.length}
showViewMore={false}
/>
) : (
<CategoryCardLoader className={"mt-[15px]"} />
)}
</div>
<div>
{animeInfo && animeInfo.related_data ? (
<Sidecard
label="Related Anime"
data={animeInfo.related_data}
className="mt-[15px]"
/>
) : (
<SidecardLoader className={"mt-[25px]"} />
)}
{homeInfo && homeInfo.most_popular && (
<Sidecard
label="Most Popular"
data={homeInfo.most_popular.slice(0, 10)}
className="mt-[15px]"
limit={10}
/>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,89 +0,0 @@
export const categoryRoutes = [
"genre/action",
"genre/adventure",
"genre/cars",
"genre/comedy",
"genre/dementia",
"genre/demons",
"genre/drama",
"genre/ecchi",
"genre/fantasy",
"genre/game",
"genre/harem",
"genre/historical",
"genre/horror",
"genre/isekai",
"genre/josei",
"genre/kids",
"genre/magic",
"genre/martial-arts",
"genre/mecha",
"genre/military",
"genre/music",
"genre/mystery",
"genre/parody",
"genre/police",
"genre/psychological",
"genre/romance",
"genre/samurai",
"genre/school",
"genre/sci-fi",
"genre/seinen",
"genre/shoujo",
"genre/shoujo-ai",
"genre/shounen",
"genre/shounen-ai",
"genre/slice-of-life",
"genre/space",
"genre/sports",
"genre/super-power",
"genre/supernatural",
"genre/thriller",
"genre/vampire",
"top-airing",
"most-popular",
"most-favorite",
"completed",
"recently-updated",
"recently-added",
"top-upcoming",
"subbed-anime",
"dubbed-anime",
"movie",
"special",
"ova",
"ona",
"tv",
];
export const azRoute = [
"az-list",
"az-list/other",
"az-list/0-9",
"az-list/a",
"az-list/b",
"az-list/c",
"az-list/d",
"az-list/e",
"az-list/f",
"az-list/g",
"az-list/h",
"az-list/i",
"az-list/j",
"az-list/k",
"az-list/l",
"az-list/m",
"az-list/n",
"az-list/o",
"az-list/p",
"az-list/q",
"az-list/r",
"az-list/s",
"az-list/t",
"az-list/u",
"az-list/v",
"az-list/w",
"az-list/x",
"az-list/y",
"az-list/z",
];

View File

@@ -1,18 +0,0 @@
import axios from "axios";
export default async function fetchAnimeInfo(id, random = false) {
const api_url = import.meta.env.VITE_API_URL;
try {
if (random) {
const id = await axios.get(`${api_url}/random/id`);
const response = await axios.get(`${api_url}/info?id=${id.data.results}`);
return response.data.results;
} else {
const response = await axios.get(`${api_url}/info?id=${id}`);
return response.data.results;
}
} catch (error) {
console.error("Error fetching anime info:", error);
return error;
}
}

Some files were not shown because too many files have changed in this diff Show More