diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..995c7d7 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +#Refer https://github.com/itzzzme/anime-api to host your backend API +VITE_API_URL=/api + +#Refer this gist to setup proxy server https://gist.github.com/itzzzme/180813be2c7b45eedc8ce8344c8dea3b +VITE_PROXY_URL=/?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?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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4855fb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# 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.* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..85f6de2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d2b28f --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +

+

+ + AnimeHi + +
+

Zenime - Ad free anime streaming platform

+

+ + Github Stars + + Github Issues + + Github Forks + +

+

+

+ Zenime is an open-source anime streaming service that uses custom 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. +

+ +
+View more Features + +### 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 + +
+ +## Previews + +
+ Home Page +
+ View more screenshots +
+ AnimeInfo Page + AnimeInfo Page +
+ Searchbar + Searchbar +
+ Character & Voice Actors + Character & Voice Actors +
+ Watch Page + Watch Page +
+
+
+ +## 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 .env.example 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 Zenime 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 Zenime 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. + +

Made by itzzzme +🫰

diff --git a/components.json b/components.json new file mode 100644 index 0000000..d16853e --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$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" + } +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..238d2e4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +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 }, + ], + }, + }, +] diff --git a/index.html b/index.html new file mode 100644 index 0000000..ea57811 --- /dev/null +++ b/index.html @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + JustAnime | Free Anime Streaming Platform + + + + + + + +
+ + + diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..0d81cf7 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + } + } + \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..b20bf01 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,6 @@ +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge" + +export function cn(...inputs) { + return twMerge(clsx(inputs)); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0abc694 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "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" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..a7bbe4a Binary files /dev/null and b/public/favicon.png differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..ae75320 Binary files /dev/null and b/public/logo.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..1e85586 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-Agent: * +Allow: / + +Sitemap: https://zenime.site/sitemap.xml \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..87923c4 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,2430 @@ + + + + https://zenime.site/ + 2024-11-08T15:50:46+00:00 + 1.00 + + + https://zenime.site/top-upcoming + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/movie + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/tv + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/most-popular + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/top-airing + 2024-11-08T15:50:46+00:00 + 0.80 + + + + https://zenime.site/search?keyword=Dandadan + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=Blue%20Box + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=Haikyu!!%20Movie%3A%20Battle%20of%20the%20Garbage%20Dump + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=One%20Piece + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=Dragon%20Ball%20Daima + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=Solo%20Leveling + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=Re%3AZERO%20-Starting%20Life%20in%20Another%20World-%20Season%203 + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=Attack%20on%20Titan + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=Bleach%3A%20Thousand-Year%20Blood%20War%20-%20The%20Conflict + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/search?keyword=My%20Hero%20Academia%20Season%207 + 2024-11-08T15:50:46+00:00 + 0.80 + + + https://zenime.site/basilisk-1307 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/subbed-anime + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dubbed-anime + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ova + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ona + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/special + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/app-download + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/action + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/adventure + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/cars + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/comedy + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/dementia + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/demons + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/drama + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/ecchi + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/fantasy + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/game + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/harem + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/historical + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/horror + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/isekai + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/josei + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/kids + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/magic + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/marial-arts + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/mecha + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/military + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/music + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/mystery + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/parody + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/police + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/psychological + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/romance + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/samurai + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/school + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/sci-fi + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/seinen + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/shoujo + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/shoujo-ai + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/shounen + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/shounen-ai + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/slice-of-life + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/space + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/sports + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/super-power + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/supernatural + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/thriller + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genre/vampire + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/filter + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/ranma-1-2-19335 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ranma-1-2-19335 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/the-elusive-samurai-19233 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-elusive-samurai-19233 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/no-longer-allowed-in-another-world-19247 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/no-longer-allowed-in-another-world-19247 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/why-does-nobody-remember-me-in-this-world-19240 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/why-does-nobody-remember-me-in-this-world-19240 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-100 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-100 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/the-strongest-magician-in-the-demon-lords-army-was-a-human-19238 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-strongest-magician-in-the-demon-lords-army-was-a-human-19238 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/pseudo-harem-19246 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/pseudo-harem-19246 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/demon-slayer-kimetsu-no-yaiba-hashira-training-arc-19107 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/demon-slayer-kimetsu-no-yaiba-hashira-training-arc-19107 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/wind-breaker-19136 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/wind-breaker-19136 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/bleach-806 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/bleach-806 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dandadan-19319 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-lock-season-2-19318 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-box-19326 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/bleach-thousand-year-blood-war-the-conflict-19322 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/rurouni-kenshin-kyoto-disturbance-19340 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/rezero-starting-life-in-another-world-season-3-19301 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-star-season-2-19256 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/good-bye-dragon-life-19347 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/case-closed-323 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/naruto-shippuden-355 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/jujutsu-kaisen-2nd-season-18413 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/black-clover-2404 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/chainsaw-man-17406 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/jujutsu-kaisen-tv-534 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/demon-slayer-kimetsu-no-yaiba-swordsmith-village-arc-18056 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/most-favorite + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/look-back-19083 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/i-parry-everything-19229 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/high-card-the-flowers-bloom-19410 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/jellyfish-cant-swim-in-the-night-19124 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/level-1-demon-lord-and-one-room-hero-18465 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/completed + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/recently-updated + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/hamidashi-creative-19368?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/hamidashi-creative-19368 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/25-dimensional-seduction-19245?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/25-dimensional-seduction-19245 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/another-journey-to-the-west-19402?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/another-journey-to-the-west-19402 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/rurouni-kenshin-kyoto-disturbance-19340?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/trillion-game-19362?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/trillion-game-19362 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/good-bye-dragon-life-19347?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/365-days-to-the-wedding-19332?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/365-days-to-the-wedding-19332 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/mechanical-arms-19354?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/mechanical-arms-19354 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/look-back-19083?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/dandadan-19319?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/loner-life-in-another-world-19337?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/loner-life-in-another-world-19337 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/kinoko-inu-19373?w=latest + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/kinoko-inu-19373 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/recently-added + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/look-back-19083 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/i-parry-everything-19229 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/high-card-the-flowers-bloom-19410 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/jellyfish-cant-swim-in-the-night-19124 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/level-1-demon-lord-and-one-room-hero-18465 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/future-folktales-8383 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/future-folktales-8383 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/mission-yozakura-family-19133 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/mission-yozakura-family-19133 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/da-shen-xian-17405 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/da-shen-xian-17405 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/da-shen-xian-17526 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/da-shen-xian-17526 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/crayon-shin-chan-movie-31-chounouryoku-daikessen-tobe-tobe-temakizushi-18427 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/crayon-shin-chan-movie-31-chounouryoku-daikessen-tobe-tobe-temakizushi-18427 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/crayon-shin-chan-movie-30-mononoke-ninja-chinpuuden-19408 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/crayon-shin-chan-movie-30-mononoke-ninja-chinpuuden-19408 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/cardfight-vanguard-divinez-season-2-19206 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/cardfight-vanguard-divinez-season-2-19206 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/rakshasa-street-4th-season-19411 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/murder-mystery-of-the-dead-19405 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/kagaku-x-bouken-survival-19376 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/genjitsu-no-yohane-sunshine-in-the-mirror-movie-19394 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/give-it-all-19393 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/solo-leveling-reawakening-19392 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/attack-on-titan-the-last-attack-19391 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/gintama-on-theater-2d-kintama-hen-19389 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/beastars-final-season-19385 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/pinkfong-gwa-hogi-sae-chingu-ninimo-19384 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/pochaazu-19383 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/kumarba-season-2-19382 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/is-it-wrong-to-try-to-pick-up-girls-in-a-dungeon-v-19323 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/haikyu-movie-battle-of-the-garbage-dump-18922 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-hero-academia-season-7-19146 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dragon-ball-daima-19328 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/other + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/0-9 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/A + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/B + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/C + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/D + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/E + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/F + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/G + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/H + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/I + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/J + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/K + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/L + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/M + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/N + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/O + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/P + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/Q + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/R + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/S + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/T + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/U + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/V + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/W + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/X + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/Y + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/az-list/Z + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/spy-x-family-code-white-19291 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/be-forever-yamato-rebel-3199-19192 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/idolish7-movie-live-4bit-beyond-the-period-19176 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-lock-episode-nagi-19085 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-oni-girl-19076 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/kuramerukagari-19075 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/rascal-does-not-dream-of-a-sister-venturing-out-18934 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dead-dead-demons-dededede-destruction-18925 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/oomuro-ke-dear-sisters-18916 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ya-boy-kongming-road-to-summer-sonia-18913 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/mobile-suit-gundam-seed-freedom-18910 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/psycho-pass-movie-providence-18715 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/white-snake-2-the-tribulation-of-the-green-snake-18682 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/detective-conan-movie-26-black-iron-submarine-18655 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-hero-academia-ua-heroes-battle-18629 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/digimon-adventure-02-movie-18562 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-imaginary-18561 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/i-am-what-i-am-18555 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-summer-18553 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/boonie-bears-back-to-earth-18549 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/boonie-bears-the-wild-life-18548 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/free-movie-5-the-final-stroke-kouhen-18504 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/maboroshi-18434 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/sound-euphonium-ensemble-contest-arc-18433 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/kaina-of-the-great-snow-sea-star-sage-18432 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/resident-evil-death-island-18429 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/detective-conan-movie-the-story-of-haibara-ai-black-iron-mystery-train-18412 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tsurune-movie-hajimari-no-issha-18411 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/backflip-the-movie-18405 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/mobile-suit-gundam-cucuruz-doans-island-18400 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/movie?page=2 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/movie?page=3 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/movie?page=32 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-log-fish-man-island-saga-19404 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tower-of-god-season-2-workshop-battle-19400 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/punirunes-2-19381 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/okaimono-panda-19379 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/asatir-2-mirai-no-mukashi-banashi-19378 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/neko-ni-tensei-shita-ojisan-19375 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/hyakushou-kizoku-2nd-season-19374 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/puniru-is-a-kawaii-slime-19372 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tonbo-season-2-19370 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-idolm-at-ster-shiny-colors-2nd-season-19369 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/a-herbivorous-dragon-of-5000-years-gets-unfairly-villainized-2nd-season-19367 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/a-story-of-a-girl-that-was-unable-to-become-a-mage-19366 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-prince-of-tennis-u-17-world-cup-semifinal-19365 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/kamierabi-godapp-season-2-19364 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/negative-positive-angler-19363 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/haigakura-19361 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/acro-trip-19360 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/a-terrified-teacher-at-ghoul-school-19359 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/touhai-ura-rate-mahjong-touhai-roku-19358 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/magilumiere-co-ltd-19357 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/let-this-grieving-soul-retire-19356 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/how-i-attended-an-all-guys-mixer-19355 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-most-notorious-talker-runs-the-worlds-greatest-clan-19353 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/love-live-superstar-3rd-season-19352 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/about-the-movement-of-the-earth-19351 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-wolves-of-mibu-19350 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/demon-lord-retry-r-19349 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/nina-the-starry-bride-19348 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/after-school-hanako-kun-part-2-19346 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tv?page=2 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tv?page=3 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tv?page=113 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/boruto-naruto-next-generations-8143 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/naruto-677 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/spy-x-family-17977 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/solo-leveling-18718 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/hunter-x-hunter-2 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-lock-17889 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/demon-slayer-entertainment-district-arc-17483 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/demon-slayer-kimetsu-no-yaiba-47 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/attack-on-titan-112 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-hero-academia-season-6-18154 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/hells-paradise-18332 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dragon-ball-z-325 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-eminence-in-shadow-17473 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dragon-ball-super-1692 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/attack-on-titan-final-season-part-2-17753 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/death-note-60 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/mashle-magic-and-muscles-18339 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/attack-on-titan-season-3-85 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/i-got-a-cheat-skill-in-another-world-and-became-unrivaled-in-the-real-world-too-18343 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dragon-ball-509 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/attack-on-titan-final-season-part-1-15548 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/classroom-of-the-elite-2nd-season-18076 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/fairy-tail-930 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-hero-academia-5th-season-15666 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/bleach-thousandyear-blood-war-the-separation-18420 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-star-18330 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/vinland-saga-2nd-season-18239 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/that-time-i-got-reincarnated-as-a-slime-season-3-19109 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/jujutsu-kaisen-0-movie-17763 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/most-popular?page=2 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/most-popular?page=3 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/most-popular?page=50 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/fairy-tail-100-years-quest-19253 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/super-dragon-ball-heroes-9688 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/pokemon-horizons-the-series-18397 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/seirei-gensouki-spirit-chronicles-season-2-19320 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-healer-who-was-banished-from-his-party-is-in-fact-the-strongest-19345 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/shangri-la-frontier-season-2-19324 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/as-a-reincarnated-aristocrat-ill-use-my-appraisal-skill-to-rise-in-the-world-season-2-19329 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/arifureta-from-commonplace-to-worlds-strongest-season-3-19321 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/super-dragon-ball-heroes-big-bang-mission-17970 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/yakuza-fiance-19336 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/battle-through-the-heavens-5th-season-18119 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ill-become-a-villainess-who-goes-down-in-history-19334 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/demon-lord-2099-19339 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/you-are-ms-servant-19331 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/shin-chan-1058 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-do-over-damsel-conquers-the-dragon-emperor-19341 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tying-the-knot-with-an-amagami-sister-19338 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/beyblade-x-18632 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/soul-land-2-peerless-tang-sect-18416 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/swallowed-star-2nd-season-18018 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/top-airing?page=2 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/top-airing?page=3 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/top-airing?page=10 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/dandadan-19319 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dandadan-19319 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-giant-18260 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-giant-18260 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-gender-3372 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-gender-3372 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-reflection-17485 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-reflection-17485 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-box-19326 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-box-19326 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-period-17427 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-period-17427 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-lock-17889 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-lock-17889 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-dragon-6179 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-dragon-6179 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-seed-3431 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-seed-3431 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/grand-blue-dreaming-139 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/grand-blue-dreaming-139 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/perfect-blue-127 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/perfect-blue-127 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-literature-893 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-literature-893 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-wolves-of-mibu-19350 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-wolves-of-mibu-19350 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-seed-beyond-6048 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-seed-beyond-6048 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/the-blue-orchestra-18359 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-blue-orchestra-18359 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/bb-fish-10438 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/bb-fish-10438 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/sweet-blue-flowers-2862 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/sweet-blue-flowers-2862 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/armed-blue-gunvolt-8786 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/armed-blue-gunvolt-8786 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-seed-omake-4193 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-seed-omake-4193 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-exorcist-1198 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-exorcist-1198 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-drop-4694 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-drop-4694 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-blink-4540 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-blink-4540 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/sky-blue-3412 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/sky-blue-3412 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/ginga-no-uo-ursa-minor-blue-7064 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ginga-no-uo-ursa-minor-blue-7064 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-dragon-the-seven-dragons-of-the-heavens-5453 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-dragon-the-seven-dragons-of-the-heavens-5453 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-lock-season-2-19318 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-lock-season-2-19318 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/aoki-honoo-10273 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/aoki-honoo-10273 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/801-tts-airbats-5741 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/801-tts-airbats-5741 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/ao-no-exorcist-kyoto-fujouou-hen-ova-2896 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ao-no-exorcist-kyoto-fujouou-hen-ova-2896 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-archive-the-animation-19125 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-archive-the-animation-19125 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-lock-episode-nagi-19085 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-lock-episode-nagi-19085 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/world-war-blue-8602 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/world-war-blue-8602 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/her-blue-sky-1418 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/her-blue-sky-1418 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-legend-shoot-1822 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-legend-shoot-1822 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-legend-shoot-4518 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-legend-shoot-4518 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/combat-mecha-xabungle-2768 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/combat-mecha-xabungle-2768 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blue-exorcist-kyoto-saga-1628 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blue-exorcist-kyoto-saga-1628 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/search?keyword=Blue%20Box&page=2 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/search?keyword=Blue%20Box&page=3 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/haikyu-movie-battle-of-the-garbage-dump-18922 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/haikyu-movie-battle-of-the-garbage-dump-18922 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/knights-of-the-zodiac-saint-seiya-battle-for-sanctuary-18135 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/knights-of-the-zodiac-saint-seiya-battle-for-sanctuary-18135 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/persona-3-the-movie-1-spring-of-birth-1244 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/persona-3-the-movie-1-spring-of-birth-1244 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/persona-3-the-movie-4-winter-of-rebirth-500 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/persona-3-the-movie-4-winter-of-rebirth-500 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/haikyu-76 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/haikyu-76 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/ex-driver-the-movie-5019 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/ex-driver-the-movie-5019 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/dirty-pair-project-eden-3340 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/dirty-pair-project-eden-3340 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/the-last-naruto-the-movie-882 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/the-last-naruto-the-movie-882 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/boruto-naruto-the-movie-1391 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/boruto-naruto-the-movie-1391 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/love-live-the-school-idol-movie-572 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/love-live-the-school-idol-movie-572 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/banner-of-the-stars-945 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/banner-of-the-stars-945 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/recycle-of-the-penguindrum-17796 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/recycle-of-the-penguindrum-17796 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/haikyu-2nd-season-29 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/haikyu-2nd-season-29 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/haikyu-3rd-season-18 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/haikyu-3rd-season-18 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/my-hero-academia-the-movie-3-world-heroes-mission-17334 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-hero-academia-the-movie-3-world-heroes-mission-17334 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/naruto-shippuuden-movie-6-road-to-ninja-1066 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/naruto-shippuuden-movie-6-road-to-ninja-1066 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/persona-3-the-movie-3-falling-down-1230 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/persona-3-the-movie-3-falling-down-1230 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/love-live-sunshine-the-school-idol-movie-over-the-rainbow-1211 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/love-live-sunshine-the-school-idol-movie-over-the-rainbow-1211 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/banner-of-the-stars-ii-753 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/banner-of-the-stars-ii-753 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/black-butler-book-of-the-atlantic-224 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/black-butler-book-of-the-atlantic-224 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/magi-the-labyrinth-of-magic-425 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/magi-the-labyrinth-of-magic-425 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/magi-the-kingdom-of-magic-208 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/magi-the-kingdom-of-magic-208 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/patlabor-the-movie-1464 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/patlabor-the-movie-1464 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/persona-3-the-movie-2-midsummer-knights-dream-1088 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/persona-3-the-movie-2-midsummer-knights-dream-1088 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/my-hero-academia-the-movie-2-heroes-rising-383 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/my-hero-academia-the-movie-2-heroes-rising-383 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/battle-of-clay-10620 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/battle-of-clay-10620 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/tales-of-luminaria-the-fateful-crossroad-17962 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/tales-of-luminaria-the-fateful-crossroad-17962 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/growlanser-iv-wayfarer-of-time-8906 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/growlanser-iv-wayfarer-of-time-8906 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/blackfox-4434 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/blackfox-4434 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/mobile-police-patlabor-2-the-movie-696 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/mobile-police-patlabor-2-the-movie-696 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/garouden-the-way-of-the-lone-wolf-19165 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/garouden-the-way-of-the-lone-wolf-19165 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/saint-seiya-the-movie-evil-goddess-eris-4181 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/saint-seiya-the-movie-evil-goddess-eris-4181 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/kabaneri-of-the-iron-fortress-the-battle-of-unato-933 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/kabaneri-of-the-iron-fortress-the-battle-of-unato-933 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/saint-seiya-knights-of-the-zodiac-battle-for-sanctuary-part-2-19090 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/saint-seiya-knights-of-the-zodiac-battle-for-sanctuary-part-2-19090 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/soul-hunter-3019 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/soul-hunter-3019 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/highschool-of-the-dead-drifters-of-the-dead-5101 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/highschool-of-the-dead-drifters-of-the-dead-5101 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/search?keyword=Haikyu!!%20Movie:%20Battle%20of%20the%20Garbage%20Dump&page=2 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/search?keyword=Haikyu!!%20Movie:%20Battle%20of%20the%20Garbage%20Dump&page=3 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/search?keyword=Haikyu!!%20Movie:%20Battle%20of%20the%20Garbage%20Dump&page=76 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-movie-1-3096 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-movie-1-3096 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-100 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-fan-letter-19406 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-fan-letter-19406 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-film-red-18236 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-film-red-18236 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-the-movie-13-film-gold-550 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-the-movie-13-film-gold-550 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-room-9215 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-room-9215 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-room-second-season-7392 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-room-second-season-7392 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-room-third-season-6959 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-room-third-season-6959 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-episode-of-skypiea-3097 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-episode-of-skypiea-3097 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-log-fish-man-island-saga-19404 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/one-piece-log-fish-man-island-saga-19404 + 2024-11-08T15:50:46+00:00 + 0.64 + + + https://zenime.site/watch/one-piece-3d-gekisou-trap-coaster-3400 + 2024-11-08T15:50:46+00:00 + 0.64 + + + + \ No newline at end of file diff --git a/public/splash.jpg b/public/splash.jpg new file mode 100644 index 0000000..12b0bc5 Binary files /dev/null and b/public/splash.jpg differ diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..d608b5b --- /dev/null +++ b/src/App.css @@ -0,0 +1,27 @@ +* { + 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; +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..65102a4 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,73 @@ +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 ( + +
+
+ {!isSplashScreen && } + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Render category routes */} + {categoryRoutes.map((path) => ( + + } + /> + ))} + {/* Render A to Z routes */} + {azRoute.map((path) => ( + } + /> + ))} + } /> + } /> + {/* Catch-all route for 404 */} + } /> + + {!isSplashScreen &&
} +
+
+
+ ); +} + +export default App; diff --git a/src/components/Loader/AnimeInfo.loader.jsx b/src/components/Loader/AnimeInfo.loader.jsx new file mode 100644 index 0000000..3a20c22 --- /dev/null +++ b/src/components/Loader/AnimeInfo.loader.jsx @@ -0,0 +1,58 @@ +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) => ) +); + +function AnimeInfoLoader() { + return ( + <> +
+ +
+ +
+
    + +
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+ + +
+ + ); +} +export default AnimeInfoLoader; diff --git a/src/components/Loader/AtoZ.loader.jsx b/src/components/Loader/AtoZ.loader.jsx new file mode 100644 index 0000000..8c9ba62 --- /dev/null +++ b/src/components/Loader/AtoZ.loader.jsx @@ -0,0 +1,26 @@ +import { Skeleton } from "../ui/Skeleton/Skeleton"; +import CategoryCardLoader from "./CategoryCard.loader"; + +const SkeletonItems = ({ count, className }) => ( + [...Array(count)].map((_, index) => ) +); + +function AtoZLoader() { + return ( +
+
    + + +
+
+ +
+ +
+
+ +
+ ); +} + +export default AtoZLoader; diff --git a/src/components/Loader/Cart.loader.jsx b/src/components/Loader/Cart.loader.jsx new file mode 100644 index 0000000..2467415 --- /dev/null +++ b/src/components/Loader/Cart.loader.jsx @@ -0,0 +1,27 @@ +import { Skeleton } from "../ui/Skeleton/Skeleton" +const SkeletonItems = ({ count, className }) => ( + [...Array(count)].map((_, index) => ) +); +function CartLoader() { + return ( +
+ +
+ {[...Array(5)].map((item, index) => ( +
+ +
+ +
+ +
+
+
+ ))} +
+ +
+ ) +} + +export default CartLoader \ No newline at end of file diff --git a/src/components/Loader/Category.loader.jsx b/src/components/Loader/Category.loader.jsx new file mode 100644 index 0000000..45a6b21 --- /dev/null +++ b/src/components/Loader/Category.loader.jsx @@ -0,0 +1,23 @@ +import { Skeleton } from "../ui/Skeleton/Skeleton" +import CategoryCardLoader from "./CategoryCard.loader" +import SidecardLoader from "./Sidecard.loader" + +function CategoryLoader() { + return ( +
+
+ +
+ + +
+
+
+ + +
+
+ ) +} + +export default CategoryLoader \ No newline at end of file diff --git a/src/components/Loader/CategoryCard.loader.jsx b/src/components/Loader/CategoryCard.loader.jsx new file mode 100644 index 0000000..c170069 --- /dev/null +++ b/src/components/Loader/CategoryCard.loader.jsx @@ -0,0 +1,35 @@ +import { Skeleton } from "../ui/Skeleton/Skeleton"; + +function CategoryCardLoader({ className, showLabelSkeleton = true }) { + return ( +
+ {showLabelSkeleton && ( + + )} +
+ {[...Array(12)].map((_, index) => ( +
+
+ +
+ + +
+
+ +
+ + +
+
+ ))} +
+
+ ); +} + +export default CategoryCardLoader; diff --git a/src/components/Loader/Home.loader.jsx b/src/components/Loader/Home.loader.jsx new file mode 100644 index 0000000..21f8491 --- /dev/null +++ b/src/components/Loader/Home.loader.jsx @@ -0,0 +1,32 @@ +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 ( +
+ + +
+ + + + +
+
+
+ + + +
+
+ + +
+
+
+ ); +} + +export default HomeLoader; diff --git a/src/components/Loader/Loader.jsx b/src/components/Loader/Loader.jsx new file mode 100644 index 0000000..e6a7886 --- /dev/null +++ b/src/components/Loader/Loader.jsx @@ -0,0 +1,24 @@ +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 ; + case "animeInfo": + return ; + case "category": + return ; + case "producer": + return ; + case "AtoZ": + return ; + default: + return
; + } +}; + +export default Loader; diff --git a/src/components/Loader/Producer.loader.jsx b/src/components/Loader/Producer.loader.jsx new file mode 100644 index 0000000..b48b908 --- /dev/null +++ b/src/components/Loader/Producer.loader.jsx @@ -0,0 +1,15 @@ +import CategoryCardLoader from "./CategoryCard.loader"; +import SidecardLoader from "./Sidecard.loader"; + +function ProducerLoader() { + return ( +
+
+ + +
+
+ ); +} + +export default ProducerLoader; diff --git a/src/components/Loader/Sidecard.loader.jsx b/src/components/Loader/Sidecard.loader.jsx new file mode 100644 index 0000000..8f9d7d5 --- /dev/null +++ b/src/components/Loader/Sidecard.loader.jsx @@ -0,0 +1,26 @@ +import { Skeleton } from "../ui/Skeleton/Skeleton"; +function SidecardLoader({ className }) { + return ( +
+ +
+ {[...Array(10)].map((_, index) => ( +
+
+ +
+ +
+ + +
+
+
+
+ ))} +
+
+ ); +} + +export default SidecardLoader; diff --git a/src/components/Loader/Spotlight.loader.jsx b/src/components/Loader/Spotlight.loader.jsx new file mode 100644 index 0000000..583d74c --- /dev/null +++ b/src/components/Loader/Spotlight.loader.jsx @@ -0,0 +1,34 @@ +import { Skeleton } from "../ui/Skeleton/Skeleton" +const SkeletonItems = ({ count, className }) => ( + [...Array(count)].map((_, index) => ) +); +function SpotlightLoader() { + return ( +
+
+ + +
+ +
+ +
+ +
+
+
+
+ + + +
+
+ + +
+
+
+ ) +} + +export default SpotlightLoader \ No newline at end of file diff --git a/src/components/Loader/Trending.loader.jsx b/src/components/Loader/Trending.loader.jsx new file mode 100644 index 0000000..bf777db --- /dev/null +++ b/src/components/Loader/Trending.loader.jsx @@ -0,0 +1,34 @@ +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 ( +
+ +
+ {[...Array(count)].map((_, index) => ( +
+ +
+ ))} +
+
+ ); +} + +export default TrendingLoader; diff --git a/src/components/Loader/VoiceActorlist.loader.jsx b/src/components/Loader/VoiceActorlist.loader.jsx new file mode 100644 index 0000000..f4b975e --- /dev/null +++ b/src/components/Loader/VoiceActorlist.loader.jsx @@ -0,0 +1,21 @@ +import { Skeleton } from "../ui/Skeleton/Skeleton" + +function VoiceActorlistLoader() { + return ( +
+ {[...Array(10)].map((_, index) => ( +
+
+ +
+ + +
+
+
+ ))} +
+ ) +} + +export default VoiceActorlistLoader \ No newline at end of file diff --git a/src/components/banner/Banner.css b/src/components/banner/Banner.css new file mode 100644 index 0000000..599a1b4 --- /dev/null +++ b/src/components/banner/Banner.css @@ -0,0 +1,133 @@ +.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% + ); + } +} diff --git a/src/components/banner/Banner.jsx b/src/components/banner/Banner.jsx new file mode 100644 index 0000000..aa9aaf5 --- /dev/null +++ b/src/components/banner/Banner.jsx @@ -0,0 +1,136 @@ +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 ( +
+ {item.title} +
+
+

+ #{index + 1} Spotlight +

+

+ {language === "EN" ? item.title : item.japanese_title} +

+
+ {item.tvInfo && ( + <> + {item.tvInfo.showType && ( +
+ +

+ {item.tvInfo.showType} +

+
+ )} + + {item.tvInfo.duration && ( +
+ +

+ {item.tvInfo.duration} +

+
+ )} + + {item.tvInfo.releaseDate && ( +
+ +

+ {item.tvInfo.releaseDate} +

+
+ )} + +
+ {item.tvInfo.quality && ( +
+ {item.tvInfo.quality} +
+ )} +
+ {item.tvInfo.episodeInfo?.sub && ( +
+ +

+ {item.tvInfo.episodeInfo.sub} +

+
+ )} + + {item.tvInfo.episodeInfo?.dub && ( +
+ +

+ {item.tvInfo.episodeInfo.dub} +

+
+ )} +
+
+ + )} +
+

+ {item.description} +

+
+ + +

+ Detail +

+ + +
+
+
+ ); +} + +export default Banner; diff --git a/src/components/cart/Cart.css b/src/components/cart/Cart.css new file mode 100644 index 0000000..ff2b750 --- /dev/null +++ b/src/components/cart/Cart.css @@ -0,0 +1,10 @@ +.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; +} diff --git a/src/components/cart/Cart.jsx b/src/components/cart/Cart.jsx new file mode 100644 index 0000000..0ceefe7 --- /dev/null +++ b/src/components/cart/Cart.jsx @@ -0,0 +1,132 @@ +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 ( +
+

+ {label} +

+
+ {data && + data.slice(0, 5).map((item, index) => ( +
(cardRefs.current[index] = el)} + > + {item.title} navigate(`/watch/${item.id}`)} + onMouseEnter={() => handleImageEnter(item, index)} + onMouseLeave={handleImageLeave} + /> + + {hoveredItem === item.id + index && window.innerWidth > 1024 && ( +
{ + if (hoverTimeout) clearTimeout(hoverTimeout); + }} + onMouseLeave={handleImageLeave} + > + +
+ )} + +
+ + {language === "EN" ? item.title : item.japanese_title} + +
+ {item.tvInfo?.sub && ( +
+ +

{item.tvInfo.sub}

+
+ )} + + {item.tvInfo?.dub && ( +
+ +

{item.tvInfo.dub}

+
+ )} +
+
+

+ {item.tvInfo.showType} +

+
+
+
+
+ ))} + +

+ View more +

+ + +
+
+ ); +} + +export default Cart; diff --git a/src/components/categorycard/CategoryCard.css b/src/components/categorycard/CategoryCard.css new file mode 100644 index 0000000..04411bc --- /dev/null +++ b/src/components/categorycard/CategoryCard.css @@ -0,0 +1,27 @@ +.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; +} diff --git a/src/components/categorycard/CategoryCard.jsx b/src/components/categorycard/CategoryCard.jsx new file mode 100644 index 0000000..00bd560 --- /dev/null +++ b/src/components/categorycard/CategoryCard.jsx @@ -0,0 +1,340 @@ +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 ( +
+
+

+ {label} +

+ {showViewMore && ( + +

+ View more +

+ + + )} +
+ <> + {categoryPage && ( +
0 + ? "mt-8 max-[758px]:hidden" + : "" + }`} + > + {itemsToRender.firstRow.map((item, index) => ( +
(cardRefs.current[index] = el)} + > +
+ navigate( + `${ + path === "top-upcoming" + ? `/${item.id}` + : `/watch/${item.id}` + }` + ) + } + onMouseEnter={() => handleMouseEnter(item, index)} + onMouseLeave={handleMouseLeave} + > + {hoveredItem === item.id + index && showPlay && ( + + )} + +
+
+ {item.title} +
+ {(item.tvInfo?.rating === "18+" || + item?.adultContent === true) && ( +
+ 18+ +
+ )} +
+ {item.tvInfo?.sub && ( +
+ +

+ {item.tvInfo.sub} +

+
+ )} + {item.tvInfo?.dub && ( +
+ +

+ {item.tvInfo.dub} +

+
+ )} + {item.tvInfo?.eps && ( +
+

+ {item.tvInfo.eps} +

+
+ )} +
+ {hoveredItem === item.id + index && + window.innerWidth > 1024 && ( +
+ +
+ )} +
+ + {language === "EN" ? item.title : item.japanese_title} + + {item.description && ( +
+ {item.description} +
+ )} +
+
+ {item.tvInfo.showType.split(" ").shift()} +
+
+
+ {item.tvInfo?.duration === "m" || + item.tvInfo?.duration === "?" || + item.duration === "m" || + item.duration === "?" + ? "N/A" + : item.tvInfo?.duration || item.duration || "N/A"} +
+
+
+ ))} +
+ )} +
+ {itemsToRender.remainingItems.map((item, index) => ( +
(cardRefs.current[index] = el)} + > +
+ navigate( + `${ + path === "top-upcoming" + ? `/${item.id}` + : `/watch/${item.id}` + }` + ) + } + onMouseEnter={() => handleMouseEnter(item, index)} + onMouseLeave={handleMouseLeave} + > + {hoveredItem === item.id + index && showPlay && ( + + )} +
+
+ {item.title} +
+ {(item.tvInfo?.rating === "18+" || + item?.adultContent === true) && ( +
+ 18+ +
+ )} +
+ {item.tvInfo?.sub && ( +
+ +

+ {item.tvInfo.sub} +

+
+ )} + {item.tvInfo?.dub && ( +
+ +

+ {item.tvInfo.dub} +

+
+ )} +
+ {hoveredItem === item.id + index && + window.innerWidth > 1024 && ( +
+ +
+ )} +
+ + {language === "EN" ? item.title : item.japanese_title} + +
+
+ {item.tvInfo.showType.split(" ").shift()} +
+
+
+ {item.tvInfo?.duration === "m" || + item.tvInfo?.duration === "?" || + item.duration === "m" || + item.duration === "?" + ? "N/A" + : item.tvInfo?.duration || item.duration || "N/A"} +
+
+
+ ))} +
+ +
+ ); + } +); + +CategoryCard.displayName = "CategoryCard"; + +export default CategoryCard; diff --git a/src/components/continue/ContinueWatching.jsx b/src/components/continue/ContinueWatching.jsx new file mode 100644 index 0000000..da8d369 --- /dev/null +++ b/src/components/continue/ContinueWatching.jsx @@ -0,0 +1,132 @@ +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 ( +
+
+
+ +

+ Continue Watching +

+
+ +
+ + +
+
+ +
+ + {memoizedWatchList.map((item, index) => ( + +
+ + + + {item?.title} +
+ +
+ + {item?.adultContent === true && ( +
+ 18+ +
+ )} +
+

+ {language === "EN" + ? item?.title + : item?.japanese_title} +

+

+ Episode {item.episodeNum} +

+
+
+
+ ))} +
+
+
+ ); +}; + +export default ContinueWatching; diff --git a/src/components/episodelist/Episodelist.css b/src/components/episodelist/Episodelist.css new file mode 100644 index 0000000..8fd6b62 --- /dev/null +++ b/src/components/episodelist/Episodelist.css @@ -0,0 +1,15 @@ +@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; +} diff --git a/src/components/episodelist/Episodelist.jsx b/src/components/episodelist/Episodelist.jsx new file mode 100644 index 0000000..cc86327 --- /dev/null +++ b/src/components/episodelist/Episodelist.jsx @@ -0,0 +1,303 @@ +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 ( +
+
+

List of episodes:

+ {totalEpisodes > 100 && ( +
+
+
setShowDropDown((prev) => !prev)} + className="text-white w-fit mt-1 text-[13px] relative cursor-pointer bg-[#0D0D15] flex justify-center items-center" + ref={dropDownRef} + > + +
+

+ EPS: {selectedRange[0]}-{selectedRange[1]} +

+ +
+ {showDropDown && ( +
+ {generateRangeOptions(totalEpisodes).map((item, index) => ( +
{ + handleRangeSelect(item); + setActiveRange(item); + }} + className={`hover:bg-gray-200 cursor-pointer text-black ${ + item === activeRange ? "bg-[#EFF0F4]" : "" + }`} + > +

+ EPS: {item} + {item === activeRange ? ( + + ) : null} +

+
+ ))} +
+ )} +
+
+
+ + +
+
+ )} +
+
+
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 ( +
{ + if (episodeNumber) { + onEpisodeClick(episodeNumber); + setActiveEpisodeId(episodeNumber); + setSearchedEpisode(null); + } + }} + > + + {index + selectedRange[0]} + +
+ ); + }) + : episodes?.map((item, index) => { + const episodeNumber = item?.id.match(/ep=(\d+)/)?.[1]; + const isActive = + activeEpisodeId === episodeNumber || + currentEpisode === episodeNumber; + const isSearched = searchedEpisode === item?.id; + + return ( +
{ + if (episodeNumber) { + onEpisodeClick(episodeNumber); + setActiveEpisodeId(episodeNumber); + setSearchedEpisode(null); + } + }} + > +

{index + 1}

+
+

+ {language === "EN" ? item?.title : item?.japanese_title} +

+ {isActive && ( + + )} +
+
+ ); + })} +
+
+
+ ); +} + +export default Episodelist; diff --git a/src/components/error/Error.jsx b/src/components/error/Error.jsx new file mode 100644 index 0000000..6776274 --- /dev/null +++ b/src/components/error/Error.jsx @@ -0,0 +1,21 @@ +import { FaChevronLeft } from "react-icons/fa" +import { useNavigate } from "react-router-dom" + +function Error({ error }) { + const navigate = useNavigate(); + return ( +
+
+ +

{error === "404" ? "404 Error" : "Error"}

+

Oops! We couldn't find this page.

+ +
+
+ ) +} + +export default Error \ No newline at end of file diff --git a/src/components/footer/Footer.jsx b/src/components/footer/Footer.jsx new file mode 100644 index 0000000..43404af --- /dev/null +++ b/src/components/footer/Footer.jsx @@ -0,0 +1,62 @@ +import logoTitle from "@/src/config/logoTitle.js"; +import website_name from "@/src/config/website.js"; +import { Link } from "react-router-dom"; + +function Footer() { + return ( +
+
+ {logoTitle} +
+
+
+

A-Z LIST

+

+ Searching anime order by alphabet name A to Z +

+
+
+ {[ + "All", + "#", + "0-9", + ...Array.from({ length: 26 }, (_, i) => + String.fromCharCode(65 + i) + ), + ].map((item, index) => ( + + {item} + + ))} +
+
+

+ {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. +

+

+ © {website_name}. All rights reserved. +

+
+
+
+ ); +} + +export default Footer; diff --git a/src/components/genres/Genre.jsx b/src/components/genres/Genre.jsx new file mode 100644 index 0000000..c74424d --- /dev/null +++ b/src/components/genres/Genre.jsx @@ -0,0 +1,54 @@ +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 ( +
+

Genres

+
+
+ {data && + (showAll ? data : data.slice(0, 24)).map((item, index) => { + const textColor = colors[index % colors.length]; + return ( + +
+ {item.charAt(0).toUpperCase() + item.slice(1)} +
+ + ); + })} +
+ +
+
+ ); +} + +export default React.memo(Genre); diff --git a/src/components/navbar/Navbar.jsx b/src/components/navbar/Navbar.jsx new file mode 100644 index 0000000..f6f1215 --- /dev/null +++ b/src/components/navbar/Navbar.jsx @@ -0,0 +1,147 @@ +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 ( + + + + + ); +} + +export default Navbar; diff --git a/src/components/pageslider/PageSlider.jsx b/src/components/pageslider/PageSlider.jsx new file mode 100644 index 0000000..c42ebe2 --- /dev/null +++ b/src/components/pageslider/PageSlider.jsx @@ -0,0 +1,76 @@ +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) => ( + + )); + }; + return ( +
+
+ {page > 1 && totalPages > 2 && ( + + )} + {page > 1 && ( + + )} + {renderPageNumbers()} + {page < totalPages && ( + + )} + {page < totalPages && totalPages > 2 && ( + + )} +
+
+ ) +} + +export default PageSlider \ No newline at end of file diff --git a/src/components/player/IframePlayer.jsx b/src/components/player/IframePlayer.jsx new file mode 100644 index 0000000..efbaab1 --- /dev/null +++ b/src/components/player/IframePlayer.jsx @@ -0,0 +1,148 @@ +/* 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 ( +
+ {/* Loader Overlay */} +
+ +
+ + +
+ ); +} diff --git a/src/components/player/Player.css b/src/components/player/Player.css new file mode 100644 index 0000000..87bd52b --- /dev/null +++ b/src/components/player/Player.css @@ -0,0 +1,59 @@ +.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; + } +} diff --git a/src/components/player/Player.jsx b/src/components/player/Player.jsx new file mode 100644 index 0000000..bcf29bb --- /dev/null +++ b/src/components/player/Player.jsx @@ -0,0 +1,494 @@ +/* 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
; +} \ No newline at end of file diff --git a/src/components/player/PlayerIcons.jsx b/src/components/player/PlayerIcons.jsx new file mode 100644 index 0000000..2410fa3 --- /dev/null +++ b/src/components/player/PlayerIcons.jsx @@ -0,0 +1,103 @@ +const backward10Icon = ` + + +`; + +const forward10Icon = ` + + + +`; + +const forwardIcon = ` + +`; + +const backwardIcon = ` + +`; + +const volumeIcon = ``; + +const muteIcon = ` + + +`; + +const captionIcon = ` + + +`; +const captionOffIcon = ``; + +const pipOffIcon = ` +`; + +const loadingIcon = ``; + +const pipIcon = ` + +`; + +const playIconLg = ``; + +const playIcon = ``; + +const pauseIcon = ``; + +const uploadIcon = ` + + +`; + +const settingsIcon = ` + + +`; + +const fullScreenOnIcon = ``; + +const fullScreenOffIcon = ``; + +const logo = `

+ Powered by + + Zen!me + +

+`; + +export { + backward10Icon, + forward10Icon, + backwardIcon, + forwardIcon, + playIcon, + playIconLg, + pauseIcon, + loadingIcon, + uploadIcon, + settingsIcon, + pipIcon, + pipOffIcon, + volumeIcon, + muteIcon, + captionIcon, + captionOffIcon, + fullScreenOnIcon, + fullScreenOffIcon, + logo, +}; diff --git a/src/components/player/artPlayerPluginVttThumbnail.js b/src/components/player/artPlayerPluginVttThumbnail.js new file mode 100644 index 0000000..4cb851c --- /dev/null +++ b/src/components/player/artPlayerPluginVttThumbnail.js @@ -0,0 +1,72 @@ +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", + }; + }; +} diff --git a/src/components/player/artPlayerPluinChaper.js b/src/components/player/artPlayerPluinChaper.js new file mode 100644 index 0000000..ba681d8 --- /dev/null +++ b/src/components/player/artPlayerPluinChaper.js @@ -0,0 +1,211 @@ +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 = ` +
+
+
+
+
+
+
+ `; + + let titleTimer = null; + let $chapters = []; + + const $progress = art.query(".art-control-progress"); + const $inner = art.query(".art-control-progress-inner"); + const $control = append($inner, '
'); + const $title = append($inner, '
'); + + 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; +} diff --git a/src/components/player/artplayerPluginUploadSubtitle.js b/src/components/player/artplayerPluginUploadSubtitle.js new file mode 100644 index 0000000..20a362d --- /dev/null +++ b/src/components/player/artplayerPluginUploadSubtitle.js @@ -0,0 +1,49 @@ +import { uploadIcon } from "./PlayerIcons"; + +export default function artplayerPluginUploadSubtitle() { + return (art) => { + const { getExt } = art.constructor.utils; + + art.setting.add({ + html: ` +
+ + +
+ `, + 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; + }); + }, + }); + }; +} diff --git a/src/components/player/autoSkip.js b/src/components/player/autoSkip.js new file mode 100644 index 0000000..38588b9 --- /dev/null +++ b/src/components/player/autoSkip.js @@ -0,0 +1,74 @@ +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(); + }, + }; + }; +} diff --git a/src/components/player/getChapterStyle.js b/src/components/player/getChapterStyle.js new file mode 100644 index 0000000..c558d89 --- /dev/null +++ b/src/components/player/getChapterStyle.js @@ -0,0 +1,82 @@ +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; +} diff --git a/src/components/player/getVttArray.js b/src/components/player/getVttArray.js new file mode 100644 index 0000000..6df8d10 --- /dev/null +++ b/src/components/player/getVttArray.js @@ -0,0 +1,101 @@ +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; +} diff --git a/src/components/player/pluginChapterStyle.js b/src/components/player/pluginChapterStyle.js new file mode 100644 index 0000000..1e945ce --- /dev/null +++ b/src/components/player/pluginChapterStyle.js @@ -0,0 +1,55 @@ +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); +} +`; diff --git a/src/components/producer/Producer.jsx b/src/components/producer/Producer.jsx new file mode 100644 index 0000000..90b7c10 --- /dev/null +++ b/src/components/producer/Producer.jsx @@ -0,0 +1,102 @@ +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 ; + if (error) { + navigate("/error-page"); + return ; + } + if (!producerInfo) { + navigate("/404-not-found-page"); + return null; + } + const handlePageChange = (newPage) => { + setSearchParams({ page: newPage }); + }; + + return ( +
+ {producerInfo ? ( +
+ {page > totalPages ? ( +

+ You came a long way, go back
+ nothing is here +

+ ) : ( +
+ {producerInfo && ( + + )} + +
+ )} +
+ {homeInfoLoading ? ( + + ) : ( + <> + {homeInfo && homeInfo.topten && ( + + )} + {homeInfo?.genres && } + + )} +
+
+ ) : ( + + )} +
+ ); +} +export default Producer; diff --git a/src/components/qtip/Qtip.jsx b/src/components/qtip/Qtip.jsx new file mode 100644 index 0000000..a3c4efe --- /dev/null +++ b/src/components/qtip/Qtip.jsx @@ -0,0 +1,159 @@ +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 ( +
+ {loading || error || !qtip ? ( + + ) : ( +
+

+ {qtip.title} +

+
+ {qtip?.rating && ( +
+ +

{qtip.rating}

+
+ )} +
+ {qtip?.quality && ( +
+

{qtip.quality}

+
+ )} +
+ {qtip?.subCount && ( +
+ +

{qtip.subCount}

+
+ )} + {qtip?.dubCount && ( +
+ +

{qtip.dubCount}

+
+ )} + {qtip?.episodeCount && ( +
+

+ {qtip.episodeCount} +

+
+ )} +
+ {qtip?.type && ( +
+

{qtip.type}

+
+ )} +
+
+ {qtip?.description && ( +

+ {qtip.description} +

+ )} +
+ {qtip?.japaneseTitle && ( +
+ + Japanese:  + + {qtip.japaneseTitle} +
+ )} + {qtip?.Synonyms && ( +
+ + Synonyms:  + + {qtip.Synonyms} +
+ )} + {qtip?.airedDate && ( +
+ Aired:  + {qtip.airedDate} +
+ )} + {qtip?.status && ( +
+ + Status:  + + {qtip.status} +
+ )} + {qtip?.genres && ( +
+ + Genres:  + + {qtip.genres.map((genre, index) => ( + + + {genre} + {index === qtip.genres.length - 1 ? "" : ","}  + + + ))} +
+ )} +
+ + +

Watch Now

+ +
+ )} +
+ ); +} + +export default Qtip; diff --git a/src/components/schedule/Schedule.jsx b/src/components/schedule/Schedule.jsx new file mode 100644 index 0000000..200f6a6 --- /dev/null +++ b/src/components/schedule/Schedule.jsx @@ -0,0 +1,241 @@ +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 ( + <> +
+
+
+ Estimated Schedule +
+

+ ({GMTOffset}) {currentTime.toLocaleDateString()}{" "} + {currentTime.toLocaleTimeString()} +

+
+
+
+
+ (swiperRef.current = swiper)} + > + {dates && + dates.map((date, index) => ( + +
(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" + }`} + > +
+ {date.dayname} +
+
+ {date.monthName} {date.day} +
+
+
+ ))} +
+ + +
+
+ {loading ? ( +
+ +
+ ) : !scheduleData || scheduleData.length === 0 ? ( +
+ No data to display +
+ ) : error ? ( +
+ Something went wrong +
+ ) : ( +
+ {(showAll + ? scheduleData + : Array.isArray(scheduleData) + ? scheduleData.slice(0, 7) + : [] + ).map((item, idx) => ( + +
+
+ {item.time || "N/A"} +
+

+ {item.title || "N/A"} +

+
+ + + ))} + {scheduleData.length > 7 && ( + + )} +
+ )} + + ); +}; + +export default Schedule; diff --git a/src/components/schedule/schedule.css b/src/components/schedule/schedule.css new file mode 100644 index 0000000..440906f --- /dev/null +++ b/src/components/schedule/schedule.css @@ -0,0 +1,11 @@ +.next, +.prev { + width: 30px; + height: 30px; + border-radius: 100%; + background-color: white; + color: black; + font-size: 13px; + padding: 10px; + z-index: 10; +} diff --git a/src/components/searchbar/MobileSearch.jsx b/src/components/searchbar/MobileSearch.jsx new file mode 100644 index 0000000..80a56d7 --- /dev/null +++ b/src/components/searchbar/MobileSearch.jsx @@ -0,0 +1,73 @@ +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 && ( +
+ 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(); + } + }} + /> + + {searchValue.trim() && isFocused && ( +
+ +
+ )} +
+ )} + + ); +} + +export default MobileSearch; diff --git a/src/components/searchbar/WebSearch.jsx b/src/components/searchbar/WebSearch.jsx new file mode 100644 index 0000000..8f36cd6 --- /dev/null +++ b/src/components/searchbar/WebSearch.jsx @@ -0,0 +1,77 @@ +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 ( +
+ 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)}`); + } + } + }} + /> + + {searchValue.trim() && isFocused && ( +
+ +
+ )} +
+ ); +} + +export default WebSearch; diff --git a/src/components/servers/Servers.css b/src/components/servers/Servers.css new file mode 100644 index 0000000..10bf0e4 --- /dev/null +++ b/src/components/servers/Servers.css @@ -0,0 +1,9 @@ +.servers { + border-bottom: 1px dashed #35373d; +} +.servers:only-child { + border-bottom: none; +} +.servers:last-child { + border-bottom: none; +} diff --git a/src/components/servers/Servers.jsx b/src/components/servers/Servers.jsx new file mode 100644 index 0000000..39f82bd --- /dev/null +++ b/src/components/servers/Servers.jsx @@ -0,0 +1,187 @@ +/* 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 ( +
+ {serverLoading ? ( +
+ +
+ ) : servers ? ( +
+
+

+ You are watching
+ + Episode {activeEpisodeNum} + +

+

+ If the current server doesn't work, please try other servers + beside. +

+
+
+ {rawServers.length > 0 && ( +
+
+ +

RAW:

+
+
+ {rawServers.map((item, index) => ( +
handleServerSelect(item)} + > +

+ {item.serverName} +

+
+ ))} +
+
+ )} + {subServers.length > 0 && ( +
+
+ +

SUB:

+
+
+ {subServers.map((item, index) => ( +
handleServerSelect(item)} + > +

+ {item.serverName} +

+
+ ))} +
+
+ )} + {dubServers.length > 0 && ( +
+
+ +

DUB:

+
+
+ {dubServers.map((item, index) => ( +
handleServerSelect(item)} + > +

+ {item.serverName} +

+
+ ))} +
+
+ )} +
+
+ ) : ( +

+ Could not load servers
+ Either reload or try again after sometime +

+ )} +
+ ); +} + +export default Servers; diff --git a/src/components/sidebar/Sidebar.jsx b/src/components/sidebar/Sidebar.jsx new file mode 100644 index 0000000..61a61c9 --- /dev/null +++ b/src/components/sidebar/Sidebar.jsx @@ -0,0 +1,141 @@ +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 && ( +
+ )} + +
+
+
+ +
+
+ {[ + { icon: faRandom, label: "Random" }, + { icon: faFilm, label: "Movie" }, + ].map((item, index) => ( + + +

+ {item.label} +

+ + ))} +
+
+ {["EN", "JP"].map((lang, index) => ( + + ))} +
+
+

+ Anime name +

+
+
+
+
    + {[ + { 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) => ( +
  • + + {item.name} + +
  • + ))} +
+
+
+ + ); +}; + +export default Sidebar; diff --git a/src/components/sidecard/Sidecard.jsx b/src/components/sidecard/Sidecard.jsx new file mode 100644 index 0000000..fb539da --- /dev/null +++ b/src/components/sidecard/Sidecard.jsx @@ -0,0 +1,142 @@ +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 ( +
+

{label}

+
+ {data && + displayedData.map((item, index) => ( +
(cardRefs.current[index] = el)} + > +
+ {hoveredItem === item.id + index && + window.innerWidth > 1024 && ( +
+ +
+ )} + {item.title} navigate(`/watch/${item.id}`)} + onMouseEnter={() => handleMouseEnter(item, index)} + onMouseLeave={handleMouseLeave} + /> +
+ + window.scrollTo({ top: 0, behavior: "smooth" }) + } + > + {language === "EN" ? item.title : item.japanese_title} + +
+ {item.tvInfo?.sub && ( +
+ +

+ {item.tvInfo.sub} +

+
+ )} + {item.tvInfo?.dub && ( +
+ +

+ {item.tvInfo.dub} +

+
+ )} + {item.tvInfo?.showType && ( +
+
+

+ {item.tvInfo.showType} +

+
+ )} +
+
+
+
+ ))} + {!limit && data.length > 6 && ( + + )} +
+
+ ); +} + +export default React.memo(Sidecard); diff --git a/src/components/splashscreen/SplashScreen.css b/src/components/splashscreen/SplashScreen.css new file mode 100644 index 0000000..7401cba --- /dev/null +++ b/src/components/splashscreen/SplashScreen.css @@ -0,0 +1,227 @@ +/* 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; + } +} diff --git a/src/components/splashscreen/SplashScreen.jsx b/src/components/splashscreen/SplashScreen.jsx new file mode 100644 index 0000000..b060ae0 --- /dev/null +++ b/src/components/splashscreen/SplashScreen.jsx @@ -0,0 +1,107 @@ +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 ( +
+
+
+
+ {logoTitle} +
+ +
+ setSearch(e.target.value)} + onKeyDown={handleKeyDown} + /> + +
+ + + Enter Homepage → + + +
+

Frequently Asked Questions

+
+ {FAQ_ITEMS.map((item, index) => ( +
+ + {expandedFaq === index && ( +
+ {item.answer} +
+ )} +
+ ))} +
+
+
+
+ ); +} + +export default SplashScreen; diff --git a/src/components/spotlight/Spotlight.css b/src/components/spotlight/Spotlight.css new file mode 100644 index 0000000..01d867e --- /dev/null +++ b/src/components/spotlight/Spotlight.css @@ -0,0 +1,68 @@ +.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; + } +} diff --git a/src/components/spotlight/Spotlight.jsx b/src/components/spotlight/Spotlight.jsx new file mode 100644 index 0000000..94e2e7a --- /dev/null +++ b/src/components/spotlight/Spotlight.jsx @@ -0,0 +1,54 @@ +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 ( + <> +
+
+
+
+
+ {spotlights && spotlights.length > 0 ? ( + <> + + {spotlights.map((item, index) => ( + + + + ))} + + + ) : ( +

No spotlights to show.

+ )} +
+ + ); +}; + +export default Spotlight; diff --git a/src/components/suggestion/Suggestion.jsx b/src/components/suggestion/Suggestion.jsx new file mode 100644 index 0000000..5cacb94 --- /dev/null +++ b/src/components/suggestion/Suggestion.jsx @@ -0,0 +1,115 @@ +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 ( +
+ {loading ? ( + + ) : error && !suggestion ? ( +
Error loading suggestions
+ ) : suggestion && hasFetched ? ( +
+ {suggestion.map((item, index) => ( + + { + e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg"; + }} + /> +
+ {item?.title && ( +

+ {item.title || "N/A"} +

+ )} + {item?.japanese_title && ( +

+ {item.japanese_title || "N/A"} +

+ )} + {(item?.releaseDate || item?.showType || item?.duration) && ( +
+

+ {item.releaseDate || "N/A"} +

+ +

+ {item.showType || "N/A"} +

+ +

+ {item.duration || "N/A"} +

+
+ )} +
+ + ))} + {!loading && hasFetched && ( + +
+

+ View all results +

+ +
+ + )} +
+ ) : hasFetched ? ( +

No results found!

+ ) : null} +
+ ); +} + +export default Suggestion; diff --git a/src/components/topten/Topten.jsx b/src/components/topten/Topten.jsx new file mode 100644 index 0000000..c0a6758 --- /dev/null +++ b/src/components/topten/Topten.jsx @@ -0,0 +1,176 @@ +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 ( +
+
+

Top 10

+
    + {["today", "week", "month"].map((period) => ( +
  • handlePeriodChange(period)} + > + {period.charAt(0).toUpperCase() + period.slice(1)} +
  • + ))} +
+
+ +
+ {currentData && + currentData.map((item, index) => ( +
(cardRefs.current[index] = el)} + > +

+ {`${index + 1 < 10 ? "0" : ""}${index + 1}`} +

+
+ {/* Image with tooltip behavior */} + {item.title} navigate(`/watch/${item.id}`)} + onMouseEnter={() => handleMouseEnter(item, index)} + onMouseLeave={handleMouseLeave} + /> + + {/* Tooltip positioned near image */} + {hoveredItem === item.id + index && + window.innerWidth > 1024 && ( +
{ + if (hoverTimeout) clearTimeout(hoverTimeout); + }} + onMouseLeave={handleMouseLeave} + > + +
+ )} + +
+ handleNavigate(item.id)} + > + {language === "EN" ? item.title : item.japanese_title} + +
+ {item.tvInfo?.sub && ( +
+ +

+ {item.tvInfo.sub} +

+
+ )} + {item.tvInfo?.dub && ( +
+ +

+ {item.tvInfo.dub} +

+
+ )} +
+
+
+
+ ))} +
+
+ ); +} + +export default React.memo(Topten); diff --git a/src/components/trending/Trending.jsx b/src/components/trending/Trending.jsx new file mode 100644 index 0000000..002ad54 --- /dev/null +++ b/src/components/trending/Trending.jsx @@ -0,0 +1,77 @@ +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 ( +
+

+ Trending +

+
+ + {trending && + trending.map((item, idx) => ( + navigate(`/watch/${item.id}`)} + > +
+
+ + {item.number} + +
+ {language === "EN" ? item.title : item.japanese_title} +
+
+ + {item.title} + +
+
+ ))} +
+
+
+ +
+
+ +
+
+
+
+ ); +}; + +export default Trending; diff --git a/src/components/ui/Skeleton/Skeleton.css b/src/components/ui/Skeleton/Skeleton.css new file mode 100644 index 0000000..d51e7a0 --- /dev/null +++ b/src/components/ui/Skeleton/Skeleton.css @@ -0,0 +1,23 @@ +@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; + } + \ No newline at end of file diff --git a/src/components/ui/Skeleton/Skeleton.jsx b/src/components/ui/Skeleton/Skeleton.jsx new file mode 100644 index 0000000..eab2eaa --- /dev/null +++ b/src/components/ui/Skeleton/Skeleton.jsx @@ -0,0 +1,16 @@ +import { cn } from "@/lib/utils"; +import './Skeleton.css'; + +function Skeleton({ className, animation=true, ...props }) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/src/components/ui/bouncingloader/Bouncingloader.css b/src/components/ui/bouncingloader/Bouncingloader.css new file mode 100644 index 0000000..c5b1723 --- /dev/null +++ b/src/components/ui/bouncingloader/Bouncingloader.css @@ -0,0 +1,45 @@ +.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); + } +} diff --git a/src/components/ui/bouncingloader/Bouncingloader.jsx b/src/components/ui/bouncingloader/Bouncingloader.jsx new file mode 100644 index 0000000..9c81444 --- /dev/null +++ b/src/components/ui/bouncingloader/Bouncingloader.jsx @@ -0,0 +1,12 @@ +import "./Bouncingloader.css" +const BouncingLoader = () => { + return ( +
+
+
+
+
+ ); +}; + +export default BouncingLoader; \ No newline at end of file diff --git a/src/components/voiceactor/Voiceactor.jsx b/src/components/voiceactor/Voiceactor.jsx new file mode 100644 index 0000000..bcc588e --- /dev/null +++ b/src/components/voiceactor/Voiceactor.jsx @@ -0,0 +1,100 @@ +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 ( +
+
+

+ Characters & Voice Actors +

+ +
+
+ {animeInfo.charactersVoiceActors.slice(0, 6).map((character, index) => ( +
+ {character.character && ( +
+
+ {character.character.poster && ( + {character.character.name { + 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" + /> + )} +
+ {character.character.name && ( +

+ {character.character.name} +

+ )} + {character.character.cast && ( +

+ {character.character.cast} +

+ )} +
+
+
+ )} + {character.voiceActors.length > 0 && character.voiceActors[0] && ( +
+
+
+ {character.voiceActors[0].name && ( + + {character.voiceActors[0].name} + + )} +
+ {character.voiceActors[0].poster && ( + {character.voiceActors[0].name { + 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" + /> + )} +
+
+ )} +
+ ))} +
+ {showVoiceActors && ( + setShowVoiceActors(false)} + /> + )} +
+ ); +} + +export default Voiceactor; diff --git a/src/components/voiceactorlist/VoiceactorList.jsx b/src/components/voiceactorlist/VoiceactorList.jsx new file mode 100644 index 0000000..43f951d --- /dev/null +++ b/src/components/voiceactorlist/VoiceactorList.jsx @@ -0,0 +1,175 @@ +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 ; + } + if (!VoiceactorList) { + navigate("/404-not-found-page"); + return null; + } + return ( +
+
+ {!loading && ( +

+ Characters & Voice Actors +

+ )} + + {loading ? ( + + ) : ( +
+ {VoiceactorList.map((item, index) => ( +
+
+ { + e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg"; + }} + /> +
+ {item.character.name && ( +

+ {item.character.name} +

+ )} + {item.character.cast && ( +

+ {item.character.cast} +

+ )} +
+
+ + {item.voiceActors && + item.voiceActors.length > 0 && + (item.voiceActors.length > 1 ? ( +
+ {item.voiceActors.map((data, index) => ( + { + e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg"; + }} + /> + ))} +
+ ) : ( +
+ {item?.voiceActors[0]?.name && ( +

+ {item.voiceActors[0].name} +

+ )} + { + e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg"; + }} + /> +
+ ))} +
+ ))} +
+ )} + +
+ +
+ + +
+
+ ); +} + +export default VoiceactorList; diff --git a/src/components/watchcontrols/Watchcontrols.jsx b/src/components/watchcontrols/Watchcontrols.jsx new file mode 100644 index 0000000..c5ca17e --- /dev/null +++ b/src/components/watchcontrols/Watchcontrols.jsx @@ -0,0 +1,97 @@ +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 }) => ( + +); + +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 ( +
+
+ setAutoPlay((prev) => !prev)} + /> + setAutoSkipIntro((prev) => !prev)} + /> + setAutoNext((prev) => !prev)} + /> +
+
+ + +
+
+ ); +} diff --git a/src/config/logoTitle.js b/src/config/logoTitle.js new file mode 100644 index 0000000..95b3110 --- /dev/null +++ b/src/config/logoTitle.js @@ -0,0 +1,3 @@ +const logoTitle="Zen!me" + +export default logoTitle; \ No newline at end of file diff --git a/src/config/website.js b/src/config/website.js new file mode 100644 index 0000000..d7cdf5d --- /dev/null +++ b/src/config/website.js @@ -0,0 +1,3 @@ +const website_name = "JustAnime"; + +export default website_name; \ No newline at end of file diff --git a/src/context/HomeInfoContext.jsx b/src/context/HomeInfoContext.jsx new file mode 100644 index 0000000..814e8b0 --- /dev/null +++ b/src/context/HomeInfoContext.jsx @@ -0,0 +1,31 @@ +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 ( + + {children} + + ); +}; + +export const useHomeInfo = () => useContext(HomeInfoContext); diff --git a/src/context/LanguageContext.jsx b/src/context/LanguageContext.jsx new file mode 100644 index 0000000..cec8067 --- /dev/null +++ b/src/context/LanguageContext.jsx @@ -0,0 +1,27 @@ +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 ( + + {children} + + ); +}; + +export const useLanguage = () => { + return useContext(LanguageContext); +}; diff --git a/src/context/SearchContext.jsx b/src/context/SearchContext.jsx new file mode 100644 index 0000000..36e738e --- /dev/null +++ b/src/context/SearchContext.jsx @@ -0,0 +1,13 @@ +import { createContext, useContext, useState } from 'react'; + +const SearchContext = createContext(); +export function SearchProvider({ children }) { + const [isSearchVisible, setIsSearchVisible] = useState(false); + + return ( + + {children} + + ); +} +export const useSearchContext = () => useContext(SearchContext); \ No newline at end of file diff --git a/src/helper/toggleScrollbar.js b/src/helper/toggleScrollbar.js new file mode 100644 index 0000000..77e0bdd --- /dev/null +++ b/src/helper/toggleScrollbar.js @@ -0,0 +1,32 @@ +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(); + } +} diff --git a/src/hooks/useSearch.js b/src/hooks/useSearch.js new file mode 100644 index 0000000..f094b86 --- /dev/null +++ b/src/hooks/useSearch.js @@ -0,0 +1,64 @@ +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; diff --git a/src/hooks/useToolTipPosition.js b/src/hooks/useToolTipPosition.js new file mode 100644 index 0000000..ae74331 --- /dev/null +++ b/src/hooks/useToolTipPosition.js @@ -0,0 +1,49 @@ +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; diff --git a/src/hooks/useWatch.js b/src/hooks/useWatch.js new file mode 100644 index 0000000..cc416bb --- /dev/null +++ b/src/hooks/useWatch.js @@ -0,0 +1,269 @@ +/* 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, + }; +}; diff --git a/src/hooks/useWatchControl.js b/src/hooks/useWatchControl.js new file mode 100644 index 0000000..6ae19f9 --- /dev/null +++ b/src/hooks/useWatchControl.js @@ -0,0 +1,34 @@ +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, + }; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..765f332 --- /dev/null +++ b/src/index.css @@ -0,0 +1,126 @@ +@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; +} diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..e5bd98c --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,13 @@ +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( + + + + + +); diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx new file mode 100644 index 0000000..789fb6a --- /dev/null +++ b/src/pages/Home/Home.jsx @@ -0,0 +1,82 @@ +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 ; + if (error) return ; + if (!homeInfo) return ; + return ( + <> +
+ + + +
+ + + + +
+
+
+ + + + +
+
+ + +
+
+
+ + ); +} + +export default Home; diff --git a/src/pages/a2z/AtoZ.jsx b/src/pages/a2z/AtoZ.jsx new file mode 100644 index 0000000..f2717e6 --- /dev/null +++ b/src/pages/a2z/AtoZ.jsx @@ -0,0 +1,118 @@ +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 ; + if (error) { + return ; + } + if (!categoryInfo) { + return null; + } + const handlePageChange = (newPage) => { + setSearchParams({ page: newPage }); + }; + + return ( +
+
    +
  • + + Home + +
    +
  • +
  • A-Z List
  • +
+
+

+ Sort By Letters +

+
+ {[ + "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 ( + + {item} + + ); + })} +
+
+
+
+ {categoryInfo && categoryInfo.length > 0 && ( + + )} + +
+
+
+ ); +} + +export default AtoZ; diff --git a/src/pages/animeInfo/AnimeInfo.jsx b/src/pages/animeInfo/AnimeInfo.jsx new file mode 100644 index 0000000..60b1e4f --- /dev/null +++ b/src/pages/animeInfo/AnimeInfo.jsx @@ -0,0 +1,416 @@ +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 && ( +
+ {`${label}: `} + + {Array.isArray(value) ? ( + value.map((item, index) => + isProducer ? ( + :;,.?/\\|{}[\]`~*_]/g, "") + .split(" ") + .join("-") + .replace(/-+/g, "-")}`} + key={index} + className="cursor-pointer hover:text-[#ffbade]" + > + {item} + {index < value.length - 1 && ", "} + + ) : ( + + {item} + + ) + ) + ) : isProducer ? ( + :;,.?/\\|{}[\]`~*_]/g, "") + .split(" ") + .join("-") + .replace(/-+/g, "-")}`} + className="cursor-pointer hover:text-[#ffbade]" + > + {value} + + ) : ( + {value} + )} + +
+ ) + ); +} + +function Tag({ bgColor, index, icon, text }) { + return ( +
+ {icon && } +

{text}

+
+ ); +} + +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 ; + if (error) { + return ; + } + 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 ( + <> +
+ {`${title} +
+
+ {`${title} + {animeInfo.adultContent && ( +
+ 18+ +
+ )} +
+
+
    + {[ + ["Home", "home"], + [info.tvInfo?.showType, info.tvInfo?.showType], + ].map(([text, link], index) => ( +
  • + + {text} + +
    +
  • + ))} +

    + {language === "EN" ? title : japanese_title} +

    +
+

+ {language === "EN" ? title : japanese_title} +

+
+ {tags.map( + ({ condition, icon, bgColor, text }, index) => + condition && ( + + ) + )} +
+ {[info.tvInfo?.showType, info.tvInfo?.duration].map( + (item, index) => + item && ( +
+
+

{item}

+
+ ) + )} +
+
+ {animeInfo?.animeInfo?.Status?.toLowerCase() !== "not-yet-aired" ? ( + + +

Watch Now

+ + ) : ( +
+

Not released

+
+ )} + {info?.Overview && ( +
+ {info.Overview.length > 270 ? ( + <> + {isFull + ? info.Overview + : `${info.Overview.slice(0, 270)}...`} + setIsFull(!isFull)} + > + {isFull ? "- Less" : "+ More"} + + + ) : ( + info.Overview + )} +
+ )} +

+ {`${website_name} is the best site to watch `} + {title} + {` SUB online, or you can even watch `} + {title} + {` DUB in HD quality.`} +

+
+ Share Anime +
+

+ Share Anime +

+

to your friends

+
+
+
+
+
+
+ {info?.Overview && ( +
+

Overview:

+
+

{info.Overview}

+
+
+ )} + {[ + { 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) => ( + + ))} + {info?.Genres && ( +
+

Genres:

+
+ {info.Genres.map((genre, index) => ( + + {genre} + + ))} +
+
+ )} + {[ + { label: "Studios", value: info?.Studios }, + { label: "Producers", value: info?.Producers }, + ].map(({ label, value }, index) => ( + + ))} +

+ {`${website_name} is the best site to watch `} + {title} + {` SUB online, or you can even watch `} + {title} + {` DUB in HD quality.`} +

+
+
+
+
+
+ {seasons?.length > 0 && ( +
+

+ More Seasons +

+
+ {seasons.map((season, index) => ( + +

+ {season.season} +

+
+ + + ))} +
+
+ )} + {animeInfo?.charactersVoiceActors.length > 0 && ( + + )} + {animeInfo.recommended_data.length > 0 && ( + + )} +
+
+ {animeInfo.related_data.length > 0 && ( + + )} + {homeInfo && homeInfo.most_popular && ( + + )} +
+
+ + ); +} + +export default AnimeInfo; diff --git a/src/pages/category/Category.jsx b/src/pages/category/Category.jsx new file mode 100644 index 0000000..326c61e --- /dev/null +++ b/src/pages/category/Category.jsx @@ -0,0 +1,111 @@ +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 ; + if (error) { + navigate("/error-page"); + return ; + } + if (!categoryInfo) { + navigate("/404-not-found-page"); + return null; + } + const handlePageChange = (newPage) => { + setSearchParams({ page: newPage }); + }; + + return ( +
+
+ Share Anime +
+

Share Anime

+

to your friends

+
+
+ {categoryInfo ? ( +
+ {page > totalPages ? ( +

+ You came a long way, go back
+ nothing is here +

+ ) : ( +
+ {categoryInfo && categoryInfo.length > 0 && ( + + )} + +
+ )} +
+ {homeInfoLoading ? ( + + ) : ( + <> + {homeInfo && homeInfo.topten && ( + + )} + {homeInfo?.genres && } + + )} +
+
+ ) : ( + + )} +
+ ); +} + +export default Category; diff --git a/src/pages/search/Search.jsx b/src/pages/search/Search.jsx new file mode 100644 index 0000000..f52033c --- /dev/null +++ b/src/pages/search/Search.jsx @@ -0,0 +1,74 @@ +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 ( +
+ {loading ? ( + + ) : page > totalPages ?

You came a long way, go back
nothing is here

: searchData && searchData.length > 0 ? ( +
+ + +
+ ) : error ?

Couldn't get search result please try again

: ( +

{`Search results for: ${keyword}`}

+ )} +
+ {homeInfoLoading ? ( + + ) : ( + <> + {homeInfo?.most_popular && } + {homeInfo?.genres && } + + )} +
+
+ ); +} + +export default Search; diff --git a/src/pages/watch/Watch.jsx b/src/pages/watch/Watch.jsx new file mode 100644 index 0000000..e9ee8c0 --- /dev/null +++ b/src/pages/watch/Watch.jsx @@ -0,0 +1,541 @@ +/* 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 ( +
+ {icon && } +

{text}

+
+ ); + } + + 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 ( +
+
+ {`${animeInfo?.title} +
+
+ {animeInfo && ( +
    + {[ + ["Home", "home"], + [animeInfo?.showType, animeInfo?.showType], + ].map(([text, link], index) => ( +
  • + + {text} + +
    +
  • + ))} +

    + Watching{" "} + {language === "EN" + ? animeInfo?.title + : animeInfo?.japanese_title} +

    +
+ )} +
+
+ {!episodes ? ( + + ) : ( + setEpisodeId(id)} + totalEpisodes={totalEpisodes} + /> + )} +
+
+
+ {!buffering ? (( activeServerName.toLowerCase()==="hd-1" || activeServerName.toLowerCase()==="hd-2" || activeServerName.toLowerCase()==="hd-3" || activeServerName.toLowerCase()==="hd-4") ? + setEpisodeId(id)} + autoNext={autoNext} + />: setEpisodeId(id)} + animeInfo={animeInfo} + episodeNum={activeEpisodeNum} + streamInfo={streamInfo} + /> + ) : ( +
+ +
+ )} +

+ {!buffering && !activeServerType ? ( + servers ? ( + <> + Probably this server is down, try other servers +
+ Either reload or try again after sometime + + ) : ( + <> + Probably streaming server is down +
+ Either reload or try again after sometime + + ) + ) : null} +

+
+ + {!buffering && ( + setEpisodeId(id)} + /> + )} + + {seasons?.length > 0 && ( +
+

+ Watch more seasons of this anime +

+
+ {seasons.map((season, index) => ( + +

+ {season.season} +

+
+ + + ))} +
+
+ )} + {nextEpisodeSchedule?.nextEpisodeSchedule && + showNextEpisodeSchedule && ( +
+
+
+ 🚀 + {" Estimated the next episode will come at "} + + {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, + })} + +
+ setShowNextEpisodeSchedule(false)} + > + × + +
+
+ )} +
+
+
+ {animeInfo && animeInfo?.poster ? ( + + ) : ( + + )} +
+ {animeInfo && animeInfo?.title ? ( +

+ {language ? animeInfo?.title : animeInfo?.japanese_title} +

+ ) : ( + + )} +
+ {animeInfo ? ( + tags.map( + ({ condition, icon, bgColor, text }, index) => + condition && ( + + ) + ) + ) : ( + + )} +
+ {[ + animeInfo?.animeInfo?.tvInfo?.showType, + animeInfo?.animeInfo?.tvInfo?.duration, + ].map( + (item, index) => + item && ( +
+
+

{item}

+
+ ) + )} +
+
+ {animeInfo ? ( + animeInfo?.animeInfo?.Overview && ( +
+
+

+ {animeInfo?.animeInfo?.Overview.length > 270 ? ( + <> + {isFullOverview + ? animeInfo?.animeInfo?.Overview + : `${animeInfo?.animeInfo?.Overview.slice( + 0, + 270 + )}...`} + setIsFullOverview(!isFullOverview)} + > + {isFullOverview ? "- Less" : "+ More"} + + + ) : ( + animeInfo?.animeInfo?.Overview + )} +

+
+
+ ) + ) : ( +
+ + + + +
+ )} +

+ {`${website_name} is the best site to watch `} + + {language ? animeInfo?.title : animeInfo?.japanese_title} + + {` SUB online, or you can even watch `} + + {language ? animeInfo?.title : animeInfo?.japanese_title} + + {` DUB in HD quality.`} +

+ + View detail + +
+
+
+
+
+ Share Anime +
+

Share Anime

+

to your friends

+
+
+
+
+ {animeInfo?.charactersVoiceActors.length > 0 && ( + + )} + {animeInfo?.recommended_data.length > 0 ? ( + + ) : ( + + )} +
+
+ {animeInfo && animeInfo.related_data ? ( + + ) : ( + + )} + {homeInfo && homeInfo.most_popular && ( + + )} +
+
+
+ ); +} diff --git a/src/utils/category.utils.js b/src/utils/category.utils.js new file mode 100644 index 0000000..dfc5d69 --- /dev/null +++ b/src/utils/category.utils.js @@ -0,0 +1,89 @@ +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", +]; diff --git a/src/utils/getAnimeInfo.utils.js b/src/utils/getAnimeInfo.utils.js new file mode 100644 index 0000000..8736365 --- /dev/null +++ b/src/utils/getAnimeInfo.utils.js @@ -0,0 +1,18 @@ +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; + } +} diff --git a/src/utils/getCategoryInfo.utils.js b/src/utils/getCategoryInfo.utils.js new file mode 100644 index 0000000..409631e --- /dev/null +++ b/src/utils/getCategoryInfo.utils.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +const getCategoryInfo = async (path,page) => { + const api_url = import.meta.env.VITE_API_URL; + try { + const response = await axios.get(`${api_url}/${path}?page=${page}`); + return response.data.results; + } catch (err) { + console.error("Error fetching genre info:", err); + return err; + } +}; + +export default getCategoryInfo; diff --git a/src/utils/getEpisodes.utils.js b/src/utils/getEpisodes.utils.js new file mode 100644 index 0000000..d98c5f6 --- /dev/null +++ b/src/utils/getEpisodes.utils.js @@ -0,0 +1,12 @@ +import axios from "axios"; + +export default async function getEpisodes(id) { + const api_url = import.meta.env.VITE_API_URL; + try { + const response = await axios.get(`${api_url}/episodes/${id}`); + return response.data.results; + } catch (error) { + console.error("Error fetching anime info:", error); + return error; + } +} diff --git a/src/utils/getHomeInfo.utils.js b/src/utils/getHomeInfo.utils.js new file mode 100644 index 0000000..655182b --- /dev/null +++ b/src/utils/getHomeInfo.utils.js @@ -0,0 +1,58 @@ +import axios from "axios"; + +const CACHE_KEY = "homeInfoCache"; +const CACHE_DURATION = 24 * 60 * 60 * 1000; + +export default async function getHomeInfo() { + const api_url = import.meta.env.VITE_API_URL; + + const currentTime = Date.now(); + const cachedData = JSON.parse(localStorage.getItem(CACHE_KEY)); + + if (cachedData && currentTime - cachedData.timestamp < CACHE_DURATION) { + return cachedData.data; + } + const response = await axios.get(`${api_url}`); + if ( + !response.data.results || + Object.keys(response.data.results).length === 0 + ) { + return null; + } + const { + spotlights, + trending, + topTen: topten, + today: todaySchedule, + topAiring: top_airing, + mostPopular: most_popular, + mostFavorite: most_favorite, + latestCompleted: latest_completed, + latestEpisode: latest_episode, + topUpcoming: top_upcoming, + recentlyAdded: recently_added, + genres, + } = response.data.results; + + const dataToCache = { + data: { + spotlights, + trending, + topten, + todaySchedule, + top_airing, + most_popular, + most_favorite, + latest_completed, + latest_episode, + top_upcoming, + recently_added, + genres, + }, + timestamp: currentTime, + }; + + localStorage.setItem(CACHE_KEY, JSON.stringify(dataToCache)); + + return dataToCache.data; +} diff --git a/src/utils/getNextEpisodeSchedule.utils.js b/src/utils/getNextEpisodeSchedule.utils.js new file mode 100644 index 0000000..e01cb5b --- /dev/null +++ b/src/utils/getNextEpisodeSchedule.utils.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +const getNextEpisodeSchedule = async (id) => { + const api_url = import.meta.env.VITE_API_URL; + try { + const response = await axios.get(`${api_url}/schedule/${id}`); + return response.data.results; + } catch (err) { + console.error("Error fetching next episode schedule:", err); + return err; + } +}; + +export default getNextEpisodeSchedule; diff --git a/src/utils/getProducer.utils.js b/src/utils/getProducer.utils.js new file mode 100644 index 0000000..8f4e5bc --- /dev/null +++ b/src/utils/getProducer.utils.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +const getProducer = async (producer, page) => { + const api_url = import.meta.env.VITE_API_URL; + try { + const response = await axios.get(`${api_url}/producer/${producer}?page=${page}`); + return response.data.results; + } catch (err) { + console.error("Error fetching genre info:", err); + return err; + } +}; + +export default getProducer; diff --git a/src/utils/getQtip.utils.js b/src/utils/getQtip.utils.js new file mode 100644 index 0000000..19c8026 --- /dev/null +++ b/src/utils/getQtip.utils.js @@ -0,0 +1,18 @@ +import axios from "axios"; + +const getQtip = async (id) => { + try { + let workerUrls = import.meta.env.VITE_WORKER_URL?.split(","); + let baseUrl = workerUrls?.length + ? workerUrls[Math.floor(Math.random() * workerUrls.length)] + : import.meta.env.VITE_API_URL; + if (!baseUrl) throw new Error("No API endpoint defined."); + const response = await axios.get(`${baseUrl}/qtip/${id.split("-").pop()}`); + return response.data.results; + } catch (err) { + console.error("Error fetching genre info:", err); + return null; + } +}; + +export default getQtip; diff --git a/src/utils/getScheduleInfo.utils.js b/src/utils/getScheduleInfo.utils.js new file mode 100644 index 0000000..1095c23 --- /dev/null +++ b/src/utils/getScheduleInfo.utils.js @@ -0,0 +1,12 @@ +import axios from "axios"; + +export default async function getSchedInfo(date) { + try { + const api_url = import.meta.env.VITE_API_URL; + const response = await axios.get(`${api_url}/schedule?date=${date}`); + return response.data.results; + } catch (error) { + console.error(error); + return error; + } +} diff --git a/src/utils/getSearch.utils.js b/src/utils/getSearch.utils.js new file mode 100644 index 0000000..48aafb9 --- /dev/null +++ b/src/utils/getSearch.utils.js @@ -0,0 +1,17 @@ +import axios from "axios"; + +const getSearch = async (keyword, page) => { + const api_url = import.meta.env.VITE_API_URL; + if (!page) page = 1; + try { + const response = await axios.get( + `${api_url}/search?keyword=${keyword}&page=${page}` + ); + return response.data.results; + } catch (err) { + console.error("Error fetching genre info:", err); + return err; + } +}; + +export default getSearch; diff --git a/src/utils/getSearchSuggestion.utils.js b/src/utils/getSearchSuggestion.utils.js new file mode 100644 index 0000000..0d1173d --- /dev/null +++ b/src/utils/getSearchSuggestion.utils.js @@ -0,0 +1,16 @@ +import axios from "axios"; + +const getSearchSuggestion = async (keyword) => { + const api_url = import.meta.env.VITE_API_URL; + try { + const response = await axios.get( + `${api_url}/search/suggest?keyword=${keyword}` + ); + return response.data.results; + } catch (err) { + console.error("Error fetching genre info:", err); + return err; + } +}; + +export default getSearchSuggestion; diff --git a/src/utils/getServers.utils.js b/src/utils/getServers.utils.js new file mode 100644 index 0000000..0ed3e14 --- /dev/null +++ b/src/utils/getServers.utils.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +export default async function getServers(animeId, episodeId) { + try { + const api_url = import.meta.env.VITE_API_URL; + const response = await axios.get( + `${api_url}/servers/${animeId}?ep=${episodeId}` + ); + return response.data.results; + } catch (error) { + console.error(error); + return error; + } +} diff --git a/src/utils/getStreamInfo.utils.js b/src/utils/getStreamInfo.utils.js new file mode 100644 index 0000000..d044e5c --- /dev/null +++ b/src/utils/getStreamInfo.utils.js @@ -0,0 +1,12 @@ +import axios from "axios"; + +export default async function getStreamInfo(animeId,episodeId,serverName,type) { + const api_url = import.meta.env.VITE_API_URL; + try { + const response = await axios.get(`${api_url}/stream?id=${animeId}?ep=${episodeId}&server=${serverName}&type=${type}`); + return response.data.results; + } catch (error) { + console.error("Error fetching stream info:", error); + return error; + } +} diff --git a/src/utils/getTopSearch.utils.js b/src/utils/getTopSearch.utils.js new file mode 100644 index 0000000..a1d1282 --- /dev/null +++ b/src/utils/getTopSearch.utils.js @@ -0,0 +1,32 @@ +import axios from "axios"; + +const getTopSearch = async () => { + try { + let workerUrls = import.meta.env.VITE_WORKER_URL?.split(","); + let baseUrl = workerUrls?.length + ? workerUrls[Math.floor(Math.random() * workerUrls.length)] + : import.meta.env.VITE_API_URL; + const storedData = localStorage.getItem("topSearch"); + if (storedData) { + const { data, timestamp } = JSON.parse(storedData); + if (Date.now() - timestamp <= 7 * 24 * 60 * 60 * 1000) { + return data; + } + } + const { data } = await axios.get(`${baseUrl}/top-search`); + const results = data?.results || []; + if (results.length) { + localStorage.setItem( + "topSearch", + JSON.stringify({ data: results, timestamp: Date.now() }) + ); + return results; + } + return []; + } catch (error) { + console.error("Error fetching top search data:", error); + return null; + } +}; + +export default getTopSearch; diff --git a/src/utils/getVoiceActor.utils.js b/src/utils/getVoiceActor.utils.js new file mode 100644 index 0000000..57afecf --- /dev/null +++ b/src/utils/getVoiceActor.utils.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +export default async function fetchVoiceActorInfo(id, page) { + const api_url = import.meta.env.VITE_API_URL; + try { + const response = await axios.get( + `${api_url}/character/list/${id}?page=${page}` + ); + return response.data.results; + } catch (error) { + console.error("Error fetching anime info:", error); + return error; + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..2725950 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,62 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + screens: { + "custom-md": "600px", + "custom-xl": "1200px", + "ultra-wide":"1660px", + }, + colors: { + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + chart: { + 1: "hsl(var(--chart-1))", + 2: "hsl(var(--chart-2))", + 3: "hsl(var(--chart-3))", + 4: "hsl(var(--chart-4))", + 5: "hsl(var(--chart-5))", + }, + }, + }, + }, + plugins: [require("tailwindcss-animate")], +}; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..2e3d156 --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..454ab3e --- /dev/null +++ b/vite.config.js @@ -0,0 +1,12 @@ +import path from "path" +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./"), + }, + }, +})