mirror of
https://github.com/JustAnimeCore/JustAnime.git
synced 2026-04-17 13:51:44 +00:00
fixed, made it working
This commit is contained in:
69
index.html
69
index.html
@@ -1,10 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="https://pl27909393.effectivegatecpm.com/c3/0b/f8/c30bf8d3c1ffabac1cad25285ad62c97.js"></script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<link rel="shortcut icon" href="favicon.png" type="image/x-icon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Mobile & PWA Meta Tags -->
|
||||
<meta name="theme-color" content="#1a1a2e" />
|
||||
<meta name="msapplication-navbutton-color" content="#1a1a2e" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="JustAnime" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#1a1a2e" />
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
@@ -47,47 +59,50 @@
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "VideoObject",
|
||||
"name": "JustAnime - Watch Anime Free",
|
||||
"@type": "WebSite",
|
||||
"name": "JustAnime",
|
||||
"alternateName": ["JustAnime.to", "Just Anime"],
|
||||
"url": "https://justanime.to",
|
||||
"description": "JustAnime offers free streaming of English-subbed and dubbed anime series and movies. No account needed and no ads!",
|
||||
"thumbnailUrl": "https://github.com/tejaspanchall/JustAnime/blob/main/public/home.PNG",
|
||||
"uploadDate": "2024-11-08",
|
||||
"contentUrl": "https://justanime.to",
|
||||
"duration": "PT30M",
|
||||
"interactionStatistic": {
|
||||
"@type": "InteractionCounter",
|
||||
"interactionType": { "@type": "WatchAction" },
|
||||
"userInteractionCount": 50000
|
||||
},
|
||||
"author": {
|
||||
"@type": "Individual",
|
||||
"name": "itzzzme",
|
||||
"url": "https://justanime.to"
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": {
|
||||
"@type": "EntryPoint",
|
||||
"urlTemplate": "https://justanime.to/search?keyword={search_term_string}"
|
||||
},
|
||||
"query-input": "required name=search_term_string"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Individual",
|
||||
"name": "itzzzme",
|
||||
"@type": "Organization",
|
||||
"name": "JustAnime",
|
||||
"url": "https://justanime.to",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://github.com/tejaspanchall/JustAnime/blob/main/public/logo.png"
|
||||
"url": "https://justanime.to/logo.png",
|
||||
"width": 512,
|
||||
"height": 512
|
||||
},
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"email": "justanimexyz@gmail.com",
|
||||
"contactType": "customer service"
|
||||
}
|
||||
},
|
||||
"potentialAction": {
|
||||
"@type": "WatchAction",
|
||||
"target": "https://justanime.to"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="canonical" href="https://justanime.to" />
|
||||
|
||||
<!-- Preload Critical Resources -->
|
||||
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
|
||||
<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" /></noscript>
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="//cdnjs.cloudflare.com" />
|
||||
<link rel="dns-prefetch" href="//www.googletagmanager.com" />
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -20,6 +20,7 @@ import SplashScreen from "./components/splashscreen/SplashScreen";
|
||||
import Terms from "./pages/terms/Terms";
|
||||
import DMCA from "./pages/dmca/DMCA";
|
||||
import Contact from "./pages/contact/Contact";
|
||||
import DiscordPopup from "./components/DiscordPopup";
|
||||
|
||||
function App() {
|
||||
const location = useLocation();
|
||||
@@ -75,6 +76,7 @@ function App() {
|
||||
</main>
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
<DiscordPopup />
|
||||
</div>
|
||||
</HomeInfoProvider>
|
||||
);
|
||||
|
||||
105
src/components/DiscordPopup.jsx
Normal file
105
src/components/DiscordPopup.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { FaDiscord, FaTelegram } from "react-icons/fa";
|
||||
|
||||
const DiscordPopup = () => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [shouldRender, setShouldRender] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if the user has opted out of seeing the popup
|
||||
const isHidden = localStorage.getItem("hideDiscordPopup");
|
||||
if (isHidden) return;
|
||||
|
||||
// Set a timer for 2 minutes (120,000 ms)
|
||||
const timer = setTimeout(() => {
|
||||
setShouldRender(true);
|
||||
// Brief delay to trigger entrance animation
|
||||
setTimeout(() => setIsVisible(true), 10);
|
||||
}, 60000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
// Wait for animation to finish before removing from DOM
|
||||
setTimeout(() => setShouldRender(false), 300);
|
||||
};
|
||||
|
||||
const handleNeverShowAgain = () => {
|
||||
localStorage.setItem("hideDiscordPopup", "true");
|
||||
handleClose();
|
||||
};
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 z-[9999] flex items-center justify-center p-4 transition-all duration-300 ease-in-out ${isVisible ? "opacity-100 backdrop-blur-md pointer-events-auto" : "opacity-0 backdrop-blur-none pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`bg-[#1a1a1a] border border-[#2a2a2a] rounded-2xl shadow-[0_0_50px_-12px_rgba(0,0,0,0.8)] overflow-hidden max-w-sm w-full transition-all duration-300 transform ${isVisible ? "scale-100 translate-y-0" : "scale-95 translate-y-4"
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-5 border-b border-[#2a2a2a] bg-[#1a1a1a]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 bg-[#5865F2] text-white rounded-xl shadow-md">
|
||||
<FaDiscord className="text-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-white text-[15px] leading-tight">Join Our Community</span>
|
||||
<span className="text-[10px] text-gray-400 uppercase tracking-widest font-bold mt-0.5">Discord & Telegram</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-1.5 hover:bg-[#2a2a2a] rounded-full transition-colors text-gray-400 hover:text-white"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 bg-[#1a1a1a]">
|
||||
<p className="text-[13px] text-gray-400 leading-relaxed font-medium">
|
||||
Join our official channels for early updates, announcements, and to connect with other fans!
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
<a
|
||||
href="https://discord.gg/your-invite-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 w-full bg-[#5865F2] hover:bg-[#4759d8] text-white py-2.5 px-4 rounded-xl font-bold transition-all transform active:scale-[0.97] shadow-lg"
|
||||
>
|
||||
<FaDiscord className="text-lg" />
|
||||
JOIN DISCORD
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://tinyurl.com/JustAnimeZone"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 w-full bg-[#26A5E4] hover:bg-[#2295ce] text-white py-2.5 px-4 rounded-xl font-bold transition-all transform active:scale-[0.97] shadow-lg"
|
||||
>
|
||||
<FaTelegram className="text-lg" />
|
||||
JOIN TELEGRAM
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleNeverShowAgain}
|
||||
className="mt-5 w-full text-[11px] text-gray-500 hover:text-white underline-offset-4 hover:underline transition-colors py-1 font-semibold tracking-wide"
|
||||
>
|
||||
NEVER SHOW AGAIN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscordPopup;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useLanguage } from "@/src/context/LanguageContext";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
import "./Banner.css";
|
||||
|
||||
function Banner({ item, index }) {
|
||||
@@ -16,19 +17,19 @@ function Banner({ item, index }) {
|
||||
<section className="spotlight w-full h-full relative rounded-2xl overflow-hidden">
|
||||
<img
|
||||
src={`${item.poster}`}
|
||||
alt={item.title}
|
||||
alt={getSafeTitle(item.title, language, item.japanese_title)}
|
||||
className="absolute inset-0 object-cover w-full h-full rounded-2xl"
|
||||
/>
|
||||
<div className="spotlight-overlay absolute inset-0 z-[1] rounded-2xl"></div>
|
||||
|
||||
|
||||
<div className="absolute flex flex-col left-0 bottom-[40px] w-[55%] p-4 z-[2] max-[1390px]:w-[45%] max-[1390px]:bottom-[40px] max-[1300px]:w-[600px] max-[1120px]:w-[60%] max-md:w-[90%] max-md:bottom-[20px] max-[300px]:w-full">
|
||||
<p className="text-[#ffbade] font-semibold text-[20px] w-fit max-[1300px]:text-[15px]">
|
||||
#{index + 1} Spotlight
|
||||
</p>
|
||||
<h3 className="text-white line-clamp-2 text-5xl font-bold mt-4 text-left max-[1390px]:text-[45px] max-[1300px]:text-3xl max-[1300px]:mt-3 max-md:text-2xl max-md:mt-1 max-[575px]:text-[22px] max-sm:leading-6 max-sm:w-[80%] max-[320px]:w-full">
|
||||
{language === "EN" ? item.title : item.japanese_title}
|
||||
{getSafeTitle(item.title, language, item.japanese_title)}
|
||||
</h3>
|
||||
|
||||
|
||||
{/* Mobile Buttons */}
|
||||
<div className="hidden max-md:flex max-md:mt-3 max-md:gap-x-3 max-md:w-full">
|
||||
<Link
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
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 getSafeTitle from "@/src/utils/getSafetitle";
|
||||
import "./CategoryCard.css";
|
||||
|
||||
const CategoryCard = React.memo(
|
||||
({
|
||||
@@ -23,7 +24,7 @@ const CategoryCard = React.memo(
|
||||
}) => {
|
||||
const { language } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
if (limit) {
|
||||
data = data.slice(0, limit);
|
||||
}
|
||||
@@ -55,7 +56,7 @@ const CategoryCard = React.memo(
|
||||
if (
|
||||
JSON.stringify(prev.firstRow) !== JSON.stringify(newItems.firstRow) ||
|
||||
JSON.stringify(prev.remainingItems) !==
|
||||
JSON.stringify(newItems.remainingItems)
|
||||
JSON.stringify(newItems.remainingItems)
|
||||
) {
|
||||
return newItems;
|
||||
}
|
||||
@@ -90,11 +91,10 @@ const CategoryCard = React.memo(
|
||||
<>
|
||||
{categoryPage && (
|
||||
<div
|
||||
className={`grid grid-cols-4 gap-x-3 gap-y-8 transition-all duration-300 ease-in-out ${
|
||||
categoryPage && itemsToRender.firstRow.length > 0
|
||||
? "mt-8 max-[758px]:hidden"
|
||||
: ""
|
||||
}`}
|
||||
className={`grid grid-cols-4 gap-x-3 gap-y-8 transition-all duration-300 ease-in-out ${categoryPage && itemsToRender.firstRow.length > 0
|
||||
? "mt-8 max-[758px]:hidden"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{itemsToRender.firstRow.map((item, index) => (
|
||||
<div
|
||||
@@ -107,17 +107,16 @@ const CategoryCard = React.memo(
|
||||
className="inline-block bg-gray-900 absolute left-0 top-0 w-full h-full group hover:cursor-pointer"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`${
|
||||
path === "top-upcoming"
|
||||
? `/${item.id}`
|
||||
: `/watch/${item.id}`
|
||||
`${path === "top-upcoming"
|
||||
? `/${item.id}`
|
||||
: `/watch/${item.id}`
|
||||
}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={`${item.poster}`}
|
||||
alt={item.title}
|
||||
alt={getSafeTitle(item.title, language, item.japanese_title)}
|
||||
className="block w-full h-full object-cover transition-all duration-500 ease-in-out group-hover:scale-105 group-hover:blur-sm"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center">
|
||||
@@ -131,10 +130,10 @@ const CategoryCard = React.memo(
|
||||
</div>
|
||||
{(item.tvInfo?.rating === "18+" ||
|
||||
item?.adultContent === true) && (
|
||||
<div className="text-white px-2 py-0.5 rounded-lg bg-red-600 absolute top-3 left-3 flex items-center justify-center text-[12px] font-bold">
|
||||
18+
|
||||
</div>
|
||||
)}
|
||||
<div className="text-white px-2 py-0.5 rounded-lg bg-red-600 absolute top-3 left-3 flex items-center justify-center text-[12px] font-bold">
|
||||
18+
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-3 pb-2 bg-gradient-to-t from-black/80 via-black/50 to-transparent">
|
||||
<div className="flex items-center justify-start w-full space-x-1.5 z-[100] flex-wrap gap-y-1.5">
|
||||
{item.tvInfo?.sub && (
|
||||
@@ -177,9 +176,9 @@ const CategoryCard = React.memo(
|
||||
{(item.tvInfo?.duration || item.duration) && (
|
||||
<div className="bg-[#2a2a2a] text-white rounded-[2px] px-2 py-1 text-[11px] font-medium">
|
||||
{item.tvInfo?.duration === "m" ||
|
||||
item.tvInfo?.duration === "?" ||
|
||||
item.duration === "m" ||
|
||||
item.duration === "?"
|
||||
item.tvInfo?.duration === "?" ||
|
||||
item.duration === "m" ||
|
||||
item.duration === "?"
|
||||
? "N/A"
|
||||
: item.tvInfo?.duration || item.duration || "N/A"}
|
||||
</div>
|
||||
@@ -191,7 +190,7 @@ const CategoryCard = React.memo(
|
||||
to={`/${item.id}`}
|
||||
className="text-white font-semibold mt-3 item-title hover:text-white hover:cursor-pointer line-clamp-1"
|
||||
>
|
||||
{language === "EN" ? item.title : item.japanese_title}
|
||||
{getSafeTitle(item.title, language, item.japanese_title)}
|
||||
</Link>
|
||||
{item.description && (
|
||||
<div className="line-clamp-3 text-[13px] font-light text-gray-400 mt-3 max-[1200px]:hidden">
|
||||
@@ -214,17 +213,16 @@ const CategoryCard = React.memo(
|
||||
className="inline-block bg-gray-900 absolute left-0 top-0 w-full h-full group hover:cursor-pointer"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`${
|
||||
path === "top-upcoming"
|
||||
? `/${item.id}`
|
||||
: `/watch/${item.id}`
|
||||
`${path === "top-upcoming"
|
||||
? `/${item.id}`
|
||||
: `/watch/${item.id}`
|
||||
}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={`${item.poster}`}
|
||||
alt={item.title}
|
||||
alt={getSafeTitle(item.title, language, item.japanese_title)}
|
||||
className="block w-full h-full object-cover transition-all duration-500 ease-in-out group-hover:scale-105 group-hover:blur-sm"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center">
|
||||
@@ -238,10 +236,10 @@ const CategoryCard = React.memo(
|
||||
</div>
|
||||
{(item.tvInfo?.rating === "18+" ||
|
||||
item?.adultContent === true) && (
|
||||
<div className="text-white px-2 py-0.5 rounded-lg bg-red-600 absolute top-3 left-3 flex items-center justify-center text-[12px] font-bold">
|
||||
18+
|
||||
</div>
|
||||
)}
|
||||
<div className="text-white px-2 py-0.5 rounded-lg bg-red-600 absolute top-3 left-3 flex items-center justify-center text-[12px] font-bold">
|
||||
18+
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/80 via-black/50 to-transparent">
|
||||
<div className="flex items-center justify-start w-full space-x-1 max-[478px]:space-x-0.5 z-[100] flex-wrap gap-y-1">
|
||||
{item.tvInfo?.sub && (
|
||||
@@ -284,9 +282,9 @@ const CategoryCard = React.memo(
|
||||
{(item.tvInfo?.duration || item.duration) && (
|
||||
<div className="bg-[#2a2a2a] text-white rounded-[2px] px-1.5 py-0.5 text-[10px] font-medium max-[478px]:py-0.5 max-[478px]:px-1 max-[478px]:hidden">
|
||||
{item.tvInfo?.duration === "m" ||
|
||||
item.tvInfo?.duration === "?" ||
|
||||
item.duration === "m" ||
|
||||
item.duration === "?"
|
||||
item.tvInfo?.duration === "?" ||
|
||||
item.duration === "m" ||
|
||||
item.duration === "?"
|
||||
? "N/A"
|
||||
: item.tvInfo?.duration || item.duration || "N/A"}
|
||||
</div>
|
||||
@@ -298,7 +296,7 @@ const CategoryCard = React.memo(
|
||||
to={`/${item.id}`}
|
||||
className="text-white font-semibold mt-3 item-title hover:text-white hover:cursor-pointer line-clamp-1"
|
||||
>
|
||||
{language === "EN" ? item.title : item.japanese_title}
|
||||
{getSafeTitle(item.title, language, item.japanese_title)}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
|
||||
const ContinueWatching = () => {
|
||||
const [watchList, setWatchList] = useState([]);
|
||||
@@ -92,9 +93,9 @@ const ContinueWatching = () => {
|
||||
>
|
||||
<img
|
||||
src={`${item?.poster}`}
|
||||
alt={item?.title}
|
||||
alt={getSafeTitle(item?.title, language, item?.japanese_title)}
|
||||
className="block w-full h-full object-cover transition-all duration-500 ease-in-out group-hover:scale-105 group-hover:blur-sm"
|
||||
title={item?.title}
|
||||
title={getSafeTitle(item?.title, language, item?.japanese_title)}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center">
|
||||
@@ -113,9 +114,7 @@ const ContinueWatching = () => {
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-3 pb-2 bg-gradient-to-t from-black/90 via-black/60 to-transparent">
|
||||
<p className="text-white text-[15px] font-bold text-left truncate mb-1.5 max-[450px]:text-sm drop-shadow-lg">
|
||||
{language === "EN"
|
||||
? item?.title
|
||||
: item?.japanese_title}
|
||||
{getSafeTitle(item?.title, language, item?.japanese_title)}
|
||||
</p>
|
||||
<p className="text-gray-200 text-[13px] font-semibold text-left max-[450px]:text-[12px] drop-shadow-md">
|
||||
Episode {item.episodeNum}
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
import logoTitle from "@/src/config/logoTitle.js";
|
||||
import website_name from "@/src/config/website.js";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FaDiscord, FaTelegram } from "react-icons/fa";
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<footer className="w-full mt-16">
|
||||
{/* Logo Section */}
|
||||
<div className="max-w-[1920px] mx-auto px-4">
|
||||
<div className="flex justify-center sm:justify-start items-center gap-6">
|
||||
<img
|
||||
src="/footer.png"
|
||||
alt={logoTitle}
|
||||
className="h-[100px] w-[200px] object-contain"
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center gap-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<img
|
||||
src="/footer.png"
|
||||
alt={logoTitle}
|
||||
className="h-[100px] w-[200px] object-contain"
|
||||
/>
|
||||
<div className="flex items-center gap-4 border-l border-white/10 pl-6 h-10">
|
||||
<a
|
||||
href="https://discord.gg/your-invite-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/40 hover:text-[#5865F2] transition-all hover:scale-110"
|
||||
>
|
||||
<FaDiscord size={28} />
|
||||
</a>
|
||||
<a
|
||||
href="https://t.me/your-telegram-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/40 hover:text-[#26A5E4] transition-all hover:scale-110"
|
||||
>
|
||||
<FaTelegram size={28} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -59,11 +59,13 @@ export default function Player({
|
||||
animeInfo,
|
||||
episodeNum,
|
||||
streamInfo,
|
||||
serverName,
|
||||
}) {
|
||||
const artRef = useRef(null);
|
||||
const leftAtRef = useRef(0);
|
||||
const leftAtRef = useRef(0);
|
||||
const proxy = import.meta.env.VITE_PROXY_URL;
|
||||
const m3u8proxy = import.meta.env.VITE_M3U8_PROXY_URL?.split(",") || [];
|
||||
const m3u8proxyHD3 = import.meta.env.VITE_M3U8_PROXY_HD3;
|
||||
const [currentEpisodeIndex, setCurrentEpisodeIndex] = useState(
|
||||
episodes?.findIndex(
|
||||
(episode) => episode.id.match(/ep=(\d+)/)?.[1] === episodeId
|
||||
@@ -117,11 +119,11 @@ export default function Player({
|
||||
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]
|
||||
);
|
||||
art.pause();
|
||||
if (currentEpisodeIndex < episodes?.length - 1 && autoNext) {
|
||||
playNext(
|
||||
episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -131,11 +133,11 @@ export default function Player({
|
||||
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]
|
||||
);
|
||||
art.pause();
|
||||
if (currentEpisodeIndex < episodes?.length - 1 && autoNext) {
|
||||
playNext(
|
||||
episodes[currentEpisodeIndex + 1].id.match(/ep=(\d+)/)?.[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -211,18 +213,21 @@ export default function Player({
|
||||
if (!streamUrl || !artRef.current) return;
|
||||
const iframeUrl = streamInfo?.streamingLink?.iframe;
|
||||
const headers = {};
|
||||
headers.referer=new URL(iframeUrl).origin+"/";
|
||||
console.log(m3u8proxy[Math.floor(Math.random() * m3u8proxy?.length)] +
|
||||
encodeURIComponent(streamUrl) +
|
||||
"&headers=" +
|
||||
encodeURIComponent(JSON.stringify(headers)));
|
||||
headers.referer = new URL(iframeUrl).origin + "/";
|
||||
const finalProxy = (serverName === "hd-3" && m3u8proxyHD3)
|
||||
? m3u8proxyHD3
|
||||
: m3u8proxy[Math.floor(Math.random() * m3u8proxy?.length)];
|
||||
console.log(finalProxy +
|
||||
encodeURIComponent(streamUrl) +
|
||||
"&headers=" +
|
||||
encodeURIComponent(JSON.stringify(headers)));
|
||||
|
||||
const art = new Artplayer({
|
||||
url:
|
||||
m3u8proxy[Math.floor(Math.random() * m3u8proxy?.length)] +
|
||||
finalProxy +
|
||||
encodeURIComponent(streamUrl) +
|
||||
"&headers=" +
|
||||
encodeURIComponent(JSON.stringify(headers)),
|
||||
"&headers=" +
|
||||
encodeURIComponent(JSON.stringify(headers)),
|
||||
container: artRef.current,
|
||||
type: "m3u8",
|
||||
autoplay: autoPlay,
|
||||
|
||||
@@ -9,8 +9,11 @@ import {
|
||||
faMicrophone,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Link } from "react-router-dom";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
import { useLanguage } from "@/src/context/LanguageContext";
|
||||
|
||||
function Qtip({ id }) {
|
||||
const { language } = useLanguage();
|
||||
const [qtip, setQtip] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
@@ -38,7 +41,7 @@ function Qtip({ id }) {
|
||||
) : (
|
||||
<div className="w-full flex flex-col justify-start gap-y-2">
|
||||
<h1 className="text-xl font-semibold text-white text-[13px] leading-6">
|
||||
{qtip.title}
|
||||
{getSafeTitle(qtip.title, language, qtip.japaneseTitle)}
|
||||
</h1>
|
||||
<div className="w-full flex items-center relative mt-2">
|
||||
{qtip?.rating && (
|
||||
|
||||
@@ -36,12 +36,14 @@ function Servers({
|
||||
setActiveServerId(matchingServer.data_id);
|
||||
setActiveServerType(matchingServer.type);
|
||||
} else if (servers && servers.length > 0) {
|
||||
setActiveServerId(servers[0].data_id);
|
||||
setActiveServerType(servers[0].type);
|
||||
const defaultServer = servers.find(s => s.serverName === "HD-2") || servers[0];
|
||||
setActiveServerId(defaultServer.data_id);
|
||||
setActiveServerType(defaultServer.type);
|
||||
}
|
||||
} else if (servers && servers.length > 0) {
|
||||
setActiveServerId(servers[0].data_id);
|
||||
setActiveServerType(servers[0].type);
|
||||
const defaultServer = servers.find(s => s.serverName === "HD-2") || servers[0];
|
||||
setActiveServerId(defaultServer.data_id);
|
||||
setActiveServerType(defaultServer.type);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [servers]);
|
||||
@@ -77,11 +79,10 @@ function Servers({
|
||||
</div>
|
||||
<div className="bg-[#1f1f1f] flex flex-col max-[600px]:rounded-lg max-[600px]:p-2">
|
||||
{rawServers.length > 0 && (
|
||||
<div className={`servers px-2 flex items-center flex-wrap gap-y-1 ml-2 max-[600px]:py-1.5 max-[600px]:px-1 max-[600px]:ml-0 ${
|
||||
dubServers.length === 0 || subServers.length === 0
|
||||
<div className={`servers px-2 flex items-center flex-wrap gap-y-1 ml-2 max-[600px]:py-1.5 max-[600px]:px-1 max-[600px]:ml-0 ${dubServers.length === 0 || subServers.length === 0
|
||||
? "h-1/2"
|
||||
: "h-full"
|
||||
}`}>
|
||||
}`}>
|
||||
<div className="flex items-center gap-x-2 min-w-[65px]">
|
||||
<FontAwesomeIcon
|
||||
icon={faFile}
|
||||
@@ -93,11 +94,10 @@ function Servers({
|
||||
{rawServers.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`px-6 py-[5px] rounded-lg cursor-pointer ${
|
||||
activeServerId === item?.data_id
|
||||
className={`px-6 py-[5px] rounded-lg cursor-pointer ${activeServerId === item?.data_id
|
||||
? "bg-[#e0e0e0] text-black"
|
||||
: "bg-[#373737] text-white"
|
||||
} max-[700px]:px-3 max-[600px]:px-2 max-[600px]:py-1`}
|
||||
} max-[700px]:px-3 max-[600px]:px-2 max-[600px]:py-1`}
|
||||
onClick={() => handleServerSelect(item)}
|
||||
>
|
||||
<p className="text-[13px] font-semibold max-[600px]:text-[12px]">
|
||||
@@ -109,9 +109,8 @@ function Servers({
|
||||
</div>
|
||||
)}
|
||||
{subServers.length > 0 && (
|
||||
<div className={`servers px-2 flex items-center flex-wrap gap-y-1 ml-2 max-[600px]:py-1.5 max-[600px]:px-1 max-[600px]:ml-0 ${
|
||||
dubServers.length === 0 ? "h-1/2" : "h-full"
|
||||
}`}>
|
||||
<div className={`servers px-2 flex items-center flex-wrap gap-y-1 ml-2 max-[600px]:py-1.5 max-[600px]:px-1 max-[600px]:ml-0 ${dubServers.length === 0 ? "h-1/2" : "h-full"
|
||||
}`}>
|
||||
<div className="flex items-center gap-x-2 min-w-[65px]">
|
||||
<FontAwesomeIcon
|
||||
icon={faClosedCaptioning}
|
||||
@@ -123,11 +122,10 @@ function Servers({
|
||||
{subServers.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`px-6 py-[5px] rounded-lg cursor-pointer ${
|
||||
activeServerId === item?.data_id
|
||||
className={`px-6 py-[5px] rounded-lg cursor-pointer ${activeServerId === item?.data_id
|
||||
? "bg-[#e0e0e0] text-black"
|
||||
: "bg-[#373737] text-white"
|
||||
} max-[700px]:px-3 max-[600px]:px-2 max-[600px]:py-1`}
|
||||
} max-[700px]:px-3 max-[600px]:px-2 max-[600px]:py-1`}
|
||||
onClick={() => handleServerSelect(item)}
|
||||
>
|
||||
<p className="text-[13px] font-semibold max-[600px]:text-[12px]">
|
||||
@@ -139,9 +137,8 @@ function Servers({
|
||||
</div>
|
||||
)}
|
||||
{dubServers.length > 0 && (
|
||||
<div className={`servers px-2 flex items-center flex-wrap gap-y-1 ml-2 max-[600px]:py-1.5 max-[600px]:px-1 max-[600px]:ml-0 ${
|
||||
subServers.length === 0 ? "h-1/2" : "h-full"
|
||||
}`}>
|
||||
<div className={`servers px-2 flex items-center flex-wrap gap-y-1 ml-2 max-[600px]:py-1.5 max-[600px]:px-1 max-[600px]:ml-0 ${subServers.length === 0 ? "h-1/2" : "h-full"
|
||||
}`}>
|
||||
<div className="flex items-center gap-x-2 min-w-[65px]">
|
||||
<FontAwesomeIcon
|
||||
icon={faMicrophone}
|
||||
@@ -153,11 +150,10 @@ function Servers({
|
||||
{dubServers.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`px-6 py-[5px] rounded-lg cursor-pointer ${
|
||||
activeServerId === item?.data_id
|
||||
className={`px-6 py-[5px] rounded-lg cursor-pointer ${activeServerId === item?.data_id
|
||||
? "bg-[#e0e0e0] text-black"
|
||||
: "bg-[#373737] text-white"
|
||||
} max-[700px]:px-3 max-[600px]:px-2 max-[600px]:py-1`}
|
||||
} max-[700px]:px-3 max-[600px]:px-2 max-[600px]:py-1`}
|
||||
onClick={() => handleServerSelect(item)}
|
||||
>
|
||||
<p className="text-[13px] font-semibold max-[600px]:text-[12px]">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useLanguage } from "@/src/context/LanguageContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import useToolTipPosition from "@/src/hooks/useToolTipPosition";
|
||||
import Qtip from "../qtip/Qtip";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
|
||||
function Sidecard({ data, label, className }) {
|
||||
const { language } = useLanguage();
|
||||
@@ -50,27 +51,25 @@ function Sidecard({ data, label, className }) {
|
||||
<div className="flex items-start gap-3 p-2 rounded-lg transition-colors hover:bg-[#1f1f1f]">
|
||||
{hoveredItem === item.id + index && window.innerWidth > 1024 && (
|
||||
<div
|
||||
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition} ${
|
||||
tooltipPosition === "top-1/2"
|
||||
? "translate-y-[50px]"
|
||||
: "translate-y-[-50px]"
|
||||
} z-[100000] transform transition-all duration-300 ease-in-out ${
|
||||
hoveredItem === item.id + index
|
||||
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition} ${tooltipPosition === "top-1/2"
|
||||
? "translate-y-[50px]"
|
||||
: "translate-y-[-50px]"
|
||||
} z-[100000] transform transition-all duration-300 ease-in-out ${hoveredItem === item.id + index
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 translate-y-2"
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<Qtip id={item.id} />
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
src={`${item.poster}`}
|
||||
alt={item.title}
|
||||
alt={getSafeTitle(item.title, language, item.japanese_title)}
|
||||
className="w-[50px] h-[70px] rounded object-cover"
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5 flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-gray-200 group-hover:text-white transition-colors line-clamp-1">
|
||||
{language === "EN" ? item.title : item.japanese_title}
|
||||
{getSafeTitle(item.title, language, item.japanese_title)}
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{item.tvInfo?.sub && (
|
||||
|
||||
@@ -3,8 +3,11 @@ import { useEffect, useState } from "react";
|
||||
import BouncingLoader from "../ui/bouncingloader/Bouncingloader";
|
||||
import { FaChevronRight } from "react-icons/fa";
|
||||
import { Link } from "react-router-dom";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
import { useLanguage } from "@/src/context/LanguageContext";
|
||||
|
||||
function Suggestion({ keyword, className, onSuggestionClick }) {
|
||||
const { language } = useLanguage();
|
||||
const [suggestion, setSuggestion] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
@@ -31,10 +34,9 @@ function Suggestion({ keyword, className, onSuggestionClick }) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-zinc-900 ${className} flex ${
|
||||
loading ? "justify-center py-4" : "justify-start"
|
||||
} ${!suggestion ? "p-2" : "justify-start"} items-center rounded-lg`}
|
||||
style={{
|
||||
className={`bg-zinc-900 ${className} flex ${loading ? "justify-center py-4" : "justify-start"
|
||||
} ${!suggestion ? "p-2" : "justify-start"} items-center rounded-lg`}
|
||||
style={{
|
||||
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.05)"
|
||||
}}
|
||||
@@ -61,20 +63,15 @@ function Suggestion({ keyword, className, onSuggestionClick }) {
|
||||
<img
|
||||
src={`${item.poster}`}
|
||||
className="w-[45px] h-[65px] flex-shrink-0 object-cover rounded-md shadow-lg"
|
||||
alt=""
|
||||
alt={getSafeTitle(item.title, language, item.japanese_title)}
|
||||
onError={(e) => {
|
||||
e.target.src = "https://i.postimg.cc/HnHKvHpz/no-avatar.jpg";
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-[2px]">
|
||||
{item?.title && (
|
||||
{(item?.title || item?.japanese_title) && (
|
||||
<h1 className="line-clamp-1 leading-5 font-semibold text-[14px] text-gray-100 group-hover:text-white">
|
||||
{item.title || "N/A"}
|
||||
</h1>
|
||||
)}
|
||||
{item?.japanese_title && (
|
||||
<h1 className="line-clamp-1 leading-4 text-[12px] font-normal text-gray-400">
|
||||
{item.japanese_title || "N/A"}
|
||||
{getSafeTitle(item.title, language, item.japanese_title)}
|
||||
</h1>
|
||||
)}
|
||||
{(item?.releaseDate || item?.showType || item?.duration) && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useLanguage } from "@/src/context/LanguageContext";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import useToolTipPosition from "@/src/hooks/useToolTipPosition";
|
||||
import Qtip from "../qtip/Qtip";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
|
||||
function Topten({ data, className }) {
|
||||
const { language } = useLanguage();
|
||||
@@ -29,8 +30,8 @@ function Topten({ data, className }) {
|
||||
activePeriod === "today"
|
||||
? data.today
|
||||
: activePeriod === "week"
|
||||
? data.week
|
||||
: data.month;
|
||||
? data.week
|
||||
: data.month;
|
||||
|
||||
const { tooltipPosition, tooltipHorizontalPosition, cardRefs } =
|
||||
useToolTipPosition(hoveredItem, currentData);
|
||||
@@ -56,11 +57,10 @@ function Topten({ data, className }) {
|
||||
{["today", "week", "month"].map((period) => (
|
||||
<li
|
||||
key={period}
|
||||
className={`cursor-pointer p-1.5 px-4 transition-all duration-200 ${
|
||||
activePeriod === period
|
||||
? "bg-white text-black font-medium"
|
||||
: "text-gray-400 hover:text-white hover:bg-[#2a2a2a]"
|
||||
}`}
|
||||
className={`cursor-pointer p-1.5 px-4 transition-all duration-200 ${activePeriod === period
|
||||
? "bg-white text-black font-medium"
|
||||
: "text-gray-400 hover:text-white hover:bg-[#2a2a2a]"
|
||||
}`}
|
||||
onClick={() => handlePeriodChange(period)}
|
||||
>
|
||||
{period.charAt(0).toUpperCase() + period.slice(1)}
|
||||
@@ -78,11 +78,10 @@ function Topten({ data, className }) {
|
||||
ref={(el) => (cardRefs.current[index] = el)}
|
||||
>
|
||||
<h1
|
||||
className={`font-bold text-2xl transition-colors ${
|
||||
index < 3
|
||||
? "text-white border-b-2 border-white pb-0.5"
|
||||
: "text-gray-600"
|
||||
} max-[350px]:hidden`}
|
||||
className={`font-bold text-2xl transition-colors ${index < 3
|
||||
? "text-white border-b-2 border-white pb-0.5"
|
||||
: "text-gray-600"
|
||||
} max-[350px]:hidden`}
|
||||
>
|
||||
{`${index + 1 < 10 ? "0" : ""}${index + 1}`}
|
||||
</h1>
|
||||
@@ -97,7 +96,7 @@ function Topten({ data, className }) {
|
||||
>
|
||||
<img
|
||||
src={`${item.poster}`}
|
||||
alt={item.title}
|
||||
alt={getSafeTitle(item.title, language, item.japanese_title)}
|
||||
className="w-[55px] h-[70px] rounded-lg object-cover flex-shrink-0 cursor-pointer shadow-md transition-transform duration-200 group-hover:scale-[1.02]"
|
||||
onClick={() => navigate(`/watch/${item.id}`)}
|
||||
onMouseEnter={() => handleMouseEnter(item, index)}
|
||||
@@ -109,17 +108,15 @@ function Topten({ data, className }) {
|
||||
window.innerWidth > 1024 && (
|
||||
<div
|
||||
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition}
|
||||
${
|
||||
tooltipPosition === "top-1/2"
|
||||
${tooltipPosition === "top-1/2"
|
||||
? "translate-y-[50px]"
|
||||
: "translate-y-[-50px]"
|
||||
}
|
||||
}
|
||||
z-[100000] transform transition-all duration-300 ease-in-out
|
||||
${
|
||||
hoveredItem === item.id + index
|
||||
${hoveredItem === item.id + index
|
||||
? "opacity-100 translate-y-0"
|
||||
: "opacity-0 translate-y-2"
|
||||
}`}
|
||||
}`}
|
||||
onMouseEnter={() => {
|
||||
if (hoverTimeout) clearTimeout(hoverTimeout);
|
||||
}}
|
||||
@@ -135,7 +132,7 @@ function Topten({ data, className }) {
|
||||
className="text-[0.95em] font-medium text-gray-200 hover:text-white transform transition-all ease-out line-clamp-1 max-[478px]:line-clamp-2 max-[478px]:text-[14px]"
|
||||
onClick={() => handleNavigate(item.id)}
|
||||
>
|
||||
{language === "EN" ? item.title : item.japanese_title}
|
||||
{getSafeTitle(item.title, language, item.japanese_title)}
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center w-fit space-x-2 max-[350px]:gap-y-[3px]">
|
||||
{item.tvInfo?.sub && (
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
faMicrophone,
|
||||
faFire
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
|
||||
const Trending = ({ trending, className }) => {
|
||||
const { language } = useLanguage();
|
||||
@@ -29,7 +30,7 @@ const Trending = ({ trending, className }) => {
|
||||
<div className="relative">
|
||||
<img
|
||||
src={item.poster}
|
||||
alt={item.title}
|
||||
alt={getSafeTitle(item.title, language, item.japanese_title)}
|
||||
className="w-[50px] h-[70px] rounded object-cover"
|
||||
/>
|
||||
<div className="absolute top-0 left-0 bg-white/90 text-black text-xs font-bold px-1.5 rounded-br">
|
||||
@@ -38,7 +39,7 @@ const Trending = ({ trending, className }) => {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-gray-200 group-hover:text-white transition-colors line-clamp-2">
|
||||
{language === "EN" ? item.title : item.japanese_title}
|
||||
{getSafeTitle(item.title, language, item.japanese_title)}
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{item.tvInfo?.sub && (
|
||||
|
||||
@@ -119,7 +119,7 @@ export const useWatch = (animeId, initialEpisodeId) => {
|
||||
try {
|
||||
const data = await getServers(animeId, episodeId);
|
||||
console.log(data);
|
||||
|
||||
|
||||
const filteredServers = data?.filter(
|
||||
(server) =>
|
||||
server.serverName === "HD-1" ||
|
||||
@@ -148,6 +148,7 @@ export const useWatch = (animeId, initialEpisodeId) => {
|
||||
filteredServers.find(s => s.serverName === savedServerName && s.type === savedServerType) ||
|
||||
filteredServers.find(s => s.serverName === savedServerName) ||
|
||||
filteredServers.find(s => s.type === savedServerType && ["HD-1", "HD-2", "HD-3", "HD-4"].includes(s.serverName)) ||
|
||||
filteredServers.find(s => s.serverName === "HD-2") ||
|
||||
filteredServers[0];
|
||||
|
||||
setServers(filteredServers);
|
||||
@@ -175,8 +176,8 @@ export const useWatch = (animeId, initialEpisodeId) => {
|
||||
)
|
||||
return;
|
||||
if (
|
||||
(activeServerName?.toLowerCase() === "hd-1" || activeServerName?.toLowerCase() === "hd-4")
|
||||
&&
|
||||
(activeServerName?.toLowerCase() === "hd-1" || activeServerName?.toLowerCase() === "hd-4")
|
||||
&&
|
||||
!serverLoading
|
||||
) {
|
||||
setBuffering(false);
|
||||
@@ -191,7 +192,7 @@ export const useWatch = (animeId, initialEpisodeId) => {
|
||||
const data = await getStreamInfo(
|
||||
animeId,
|
||||
episodeId,
|
||||
server.serverName.toLowerCase()==="hd-3"?"hd-1":server.serverName.toLowerCase(),
|
||||
server.serverName.toLowerCase() === "hd-3" ? "hd-1" : server.serverName.toLowerCase(),
|
||||
server.type.toLowerCase()
|
||||
);
|
||||
setStreamInfo(data);
|
||||
|
||||
@@ -15,6 +15,7 @@ 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";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
|
||||
function InfoItem({ label, value, isProducer = true }) {
|
||||
return (
|
||||
@@ -111,12 +112,13 @@ function AnimeInfo({ random = false }) {
|
||||
}, [id, random]);
|
||||
useEffect(() => {
|
||||
if (animeInfo && location.pathname === `/${animeInfo.id}`) {
|
||||
document.title = `Watch ${animeInfo.title} English Sub/Dub online Free on ${website_name}`;
|
||||
const safeTitle = getSafeTitle(animeInfo.title, language, animeInfo.japanese_title);
|
||||
document.title = `Watch ${safeTitle} English Sub/Dub online Free on ${website_name}`;
|
||||
}
|
||||
return () => {
|
||||
document.title = `${website_name} | Free anime streaming platform`;
|
||||
};
|
||||
}, [animeInfo]);
|
||||
}, [animeInfo, language]);
|
||||
if (loading) return <Loader type="animeInfo" />;
|
||||
if (error) {
|
||||
return <Error />;
|
||||
@@ -126,6 +128,8 @@ function AnimeInfo({ random = false }) {
|
||||
return undefined;
|
||||
}
|
||||
const { title, japanese_title, poster, animeInfo: info } = animeInfo;
|
||||
const safeTitle = getSafeTitle(title, language, japanese_title);
|
||||
|
||||
const tags = [
|
||||
{
|
||||
condition: info.tvInfo?.rating,
|
||||
@@ -165,7 +169,7 @@ function AnimeInfo({ random = false }) {
|
||||
<div className="relative w-[130px] xs:w-[150px] aspect-[2/3] rounded-xl overflow-hidden shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
|
||||
<img
|
||||
src={`${poster}`}
|
||||
alt={`${title} Poster`}
|
||||
alt={`${safeTitle} Poster`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{animeInfo.adultContent && (
|
||||
@@ -181,7 +185,7 @@ function AnimeInfo({ random = false }) {
|
||||
{/* Title */}
|
||||
<div className="space-y-0.5">
|
||||
<h1 className="text-lg xs:text-xl font-bold tracking-tight truncate">
|
||||
{language === "EN" ? title : japanese_title}
|
||||
{safeTitle}
|
||||
</h1>
|
||||
{language === "EN" && japanese_title && (
|
||||
<p className="text-white/50 text-[11px] xs:text-xs truncate">JP Title: {japanese_title}</p>
|
||||
@@ -310,7 +314,7 @@ function AnimeInfo({ random = false }) {
|
||||
<div className="relative w-[220px] lg:w-[260px] aspect-[2/3] rounded-2xl overflow-hidden shadow-[0_8px_32px_rgba(0,0,0,0.3)]">
|
||||
<img
|
||||
src={`${poster}`}
|
||||
alt={`${title} Poster`}
|
||||
alt={`${safeTitle} Poster`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{animeInfo.adultContent && (
|
||||
@@ -326,7 +330,7 @@ function AnimeInfo({ random = false }) {
|
||||
{/* Title */}
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl lg:text-4xl font-bold tracking-tight truncate">
|
||||
{language === "EN" ? title : japanese_title}
|
||||
{safeTitle}
|
||||
</h1>
|
||||
{language === "EN" && japanese_title && (
|
||||
<p className="text-white/50 text-sm lg:text-base truncate">JP Title: {japanese_title}</p>
|
||||
@@ -454,42 +458,38 @@ function AnimeInfo({ random = false }) {
|
||||
<Link
|
||||
to={`/${season.id}`}
|
||||
key={index}
|
||||
className={`relative w-full aspect-[3/1] sm:aspect-[3/1] rounded-lg overflow-hidden cursor-pointer group ${
|
||||
currentId === String(season.id)
|
||||
className={`relative w-full aspect-[3/1] sm:aspect-[3/1] rounded-lg overflow-hidden cursor-pointer group ${currentId === String(season.id)
|
||||
? "ring-2 ring-white/40 shadow-lg shadow-white/10"
|
||||
: ""
|
||||
}`}
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={season.season_poster}
|
||||
alt={season.season}
|
||||
className={`w-full h-full object-cover scale-150 ${
|
||||
currentId === String(season.id)
|
||||
className={`w-full h-full object-cover scale-150 ${currentId === String(season.id)
|
||||
? "opacity-50"
|
||||
: "opacity-40"
|
||||
}`}
|
||||
}`}
|
||||
/>
|
||||
{/* Dots Pattern Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
backgroundImage: `url('data:image/svg+xml,<svg width="3" height="3" viewBox="0 0 3 3" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="1.5" cy="1.5" r="0.5" fill="white" fill-opacity="0.25"/></svg>')`,
|
||||
backgroundSize: '3px 3px'
|
||||
}}
|
||||
/>
|
||||
{/* Dark Gradient Overlay */}
|
||||
<div className={`absolute inset-0 z-20 bg-gradient-to-r ${
|
||||
currentId === String(season.id)
|
||||
<div className={`absolute inset-0 z-20 bg-gradient-to-r ${currentId === String(season.id)
|
||||
? "from-black/50 to-transparent"
|
||||
: "from-black/40 to-transparent"
|
||||
}`} />
|
||||
}`} />
|
||||
{/* Title Container */}
|
||||
<div className="absolute inset-0 z-30 flex items-center justify-center">
|
||||
<p className={`text-[14px] sm:text-[16px] md:text-[18px] font-bold text-center px-2 sm:px-4 transition-colors duration-300 ${
|
||||
currentId === String(season.id)
|
||||
<p className={`text-[14px] sm:text-[16px] md:text-[18px] font-bold text-center px-2 sm:px-4 transition-colors duration-300 ${currentId === String(season.id)
|
||||
? "text-white"
|
||||
: "text-white/90 group-hover:text-white"
|
||||
}`}>
|
||||
}`}>
|
||||
{season.season}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import SidecardLoader from "@/src/components/Loader/Sidecard.loader";
|
||||
import Watchcontrols from "@/src/components/watchcontrols/Watchcontrols";
|
||||
import useWatchControl from "@/src/hooks/useWatchControl";
|
||||
import Player from "@/src/components/player/Player";
|
||||
import getSafeTitle from "@/src/utils/getSafetitle";
|
||||
|
||||
export default function Watch() {
|
||||
const location = useLocation();
|
||||
@@ -76,12 +77,12 @@ export default function Watch() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!episodes || episodes.length === 0) return;
|
||||
|
||||
|
||||
const isValidEpisode = episodes.some(ep => {
|
||||
const epNumber = ep.id.split('ep=')[1];
|
||||
return epNumber === episodeId;
|
||||
return epNumber === episodeId;
|
||||
});
|
||||
|
||||
|
||||
// If missing or invalid episodeId, fallback to first
|
||||
if (!episodeId || !isValidEpisode) {
|
||||
const fallbackId = episodes[0].id.match(/ep=(\d+)/)?.[1];
|
||||
@@ -90,7 +91,7 @@ export default function Watch() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const newUrl = `/watch/${animeId}?ep=${episodeId}`;
|
||||
if (isFirstSet.current) {
|
||||
navigate(newUrl, { replace: true });
|
||||
@@ -98,19 +99,23 @@ export default function Watch() {
|
||||
} else {
|
||||
navigate(newUrl);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [episodeId, animeId, navigate, episodes]);
|
||||
|
||||
|
||||
|
||||
// ... inside Watch component ...
|
||||
// Update document title
|
||||
useEffect(() => {
|
||||
if (animeInfo) {
|
||||
document.title = `Watch ${animeInfo.title} English Sub/Dub online Free on ${website_name}`;
|
||||
const safeTitle = getSafeTitle(animeInfo.title, language, animeInfo.japanese_title);
|
||||
document.title = `Watch ${safeTitle} 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]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [animeId, animeInfo, language]);
|
||||
|
||||
// Redirect if no episodes
|
||||
useEffect(() => {
|
||||
@@ -128,7 +133,7 @@ export default function Watch() {
|
||||
const videoHeight = videoContainerRef.current.offsetHeight;
|
||||
const controlsHeight = controlsRef.current.offsetHeight;
|
||||
const totalHeight = videoHeight + controlsHeight;
|
||||
|
||||
|
||||
// Apply the combined height to episodes container
|
||||
episodesRef.current.style.height = `${totalHeight}px`;
|
||||
}
|
||||
@@ -143,15 +148,15 @@ export default function Watch() {
|
||||
const initialTimer = setTimeout(() => {
|
||||
adjustHeight();
|
||||
}, 500);
|
||||
|
||||
|
||||
// Set up resize listener
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
|
||||
|
||||
// Create MutationObserver to monitor player changes
|
||||
const observer = new MutationObserver(() => {
|
||||
setTimeout(adjustHeight, 100);
|
||||
});
|
||||
|
||||
|
||||
// Start observing both video container and controls
|
||||
if (videoContainerRef.current) {
|
||||
observer.observe(videoContainerRef.current, {
|
||||
@@ -160,7 +165,7 @@ export default function Watch() {
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (controlsRef.current) {
|
||||
observer.observe(controlsRef.current, {
|
||||
attributes: true,
|
||||
@@ -168,10 +173,10 @@ export default function Watch() {
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Set up additional interval for continuous adjustments
|
||||
const intervalId = setInterval(adjustHeight, 1000);
|
||||
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
clearTimeout(initialTimer);
|
||||
@@ -184,9 +189,8 @@ export default function Watch() {
|
||||
function Tag({ bgColor, index, icon, text }) {
|
||||
return (
|
||||
<div
|
||||
className={`flex space-x-1 justify-center items-center px-[4px] py-[1px] text-black font-semibold text-[13px] ${
|
||||
index === 0 ? "rounded-l-[4px]" : "rounded-none"
|
||||
}`}
|
||||
className={`flex space-x-1 justify-center items-center px-[4px] py-[1px] text-black font-semibold text-[13px] ${index === 0 ? "rounded-l-[4px]" : "rounded-none"
|
||||
}`}
|
||||
style={{ backgroundColor: bgColor }}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="text-[12px]" />}
|
||||
@@ -361,42 +365,38 @@ export default function Watch() {
|
||||
<Link
|
||||
to={`/${season.id}`}
|
||||
key={index}
|
||||
className={`relative w-full aspect-[3/1] rounded-lg overflow-hidden cursor-pointer group ${
|
||||
animeId === String(season.id)
|
||||
? "ring-2 ring-white/40 shadow-lg shadow-white/10"
|
||||
: ""
|
||||
}`}
|
||||
className={`relative w-full aspect-[3/1] rounded-lg overflow-hidden cursor-pointer group ${animeId === String(season.id)
|
||||
? "ring-2 ring-white/40 shadow-lg shadow-white/10"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={season.season_poster}
|
||||
alt={season.season}
|
||||
className={`w-full h-full object-cover scale-150 ${
|
||||
animeId === String(season.id)
|
||||
? "opacity-50"
|
||||
: "opacity-40 group-hover:opacity-50 transition-opacity"
|
||||
}`}
|
||||
className={`w-full h-full object-cover scale-150 ${animeId === String(season.id)
|
||||
? "opacity-50"
|
||||
: "opacity-40 group-hover:opacity-50 transition-opacity"
|
||||
}`}
|
||||
/>
|
||||
{/* Dots Pattern Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
backgroundImage: `url('data:image/svg+xml,<svg width="3" height="3" viewBox="0 0 3 3" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="1.5" cy="1.5" r="0.5" fill="white" fill-opacity="0.25"/></svg>')`,
|
||||
backgroundSize: '3px 3px'
|
||||
}}
|
||||
/>
|
||||
{/* Dark Gradient Overlay */}
|
||||
<div className={`absolute inset-0 z-20 bg-gradient-to-r ${
|
||||
animeId === String(season.id)
|
||||
? "from-black/50 to-transparent"
|
||||
: "from-black/40 to-transparent"
|
||||
}`} />
|
||||
<div className={`absolute inset-0 z-20 bg-gradient-to-r ${animeId === String(season.id)
|
||||
? "from-black/50 to-transparent"
|
||||
: "from-black/40 to-transparent"
|
||||
}`} />
|
||||
{/* Title Container */}
|
||||
<div className="absolute inset-0 z-30 flex items-center justify-center">
|
||||
<p className={`text-[14px] font-bold text-center px-2 transition-colors duration-300 ${
|
||||
animeId === String(season.id)
|
||||
? "text-white"
|
||||
: "text-white/90 group-hover:text-white"
|
||||
}`}>
|
||||
<p className={`text-[14px] font-bold text-center px-2 transition-colors duration-300 ${animeId === String(season.id)
|
||||
? "text-white"
|
||||
: "text-white/90 group-hover:text-white"
|
||||
}`}>
|
||||
{season.season}
|
||||
</p>
|
||||
</div>
|
||||
@@ -438,12 +438,12 @@ export default function Watch() {
|
||||
)}
|
||||
<div className="flex flex-col gap-y-4 flex-1 max-[600px]:gap-y-2">
|
||||
{animeInfo && animeInfo?.title ? (
|
||||
<Link
|
||||
<Link
|
||||
to={`/${animeId}`}
|
||||
className="group"
|
||||
>
|
||||
<h1 className="text-[28px] font-medium text-white leading-tight group-hover:text-gray-300 transition-colors max-[600px]:text-[20px]">
|
||||
{language ? animeInfo?.title : animeInfo?.japanese_title}
|
||||
{getSafeTitle(animeInfo.title, language, animeInfo.japanese_title)}
|
||||
</h1>
|
||||
<div className="flex items-center gap-1.5 mt-1 text-gray-400 text-sm group-hover:text-white transition-colors max-[600px]:text-[12px] max-[600px]:mt-0.5">
|
||||
<span>View Details</span>
|
||||
@@ -502,42 +502,38 @@ export default function Watch() {
|
||||
<Link
|
||||
to={`/${season.id}`}
|
||||
key={index}
|
||||
className={`relative w-full aspect-[3/1] rounded-lg overflow-hidden cursor-pointer group ${
|
||||
animeId === String(season.id)
|
||||
? "ring-2 ring-white/40 shadow-lg shadow-white/10"
|
||||
: ""
|
||||
}`}
|
||||
className={`relative w-full aspect-[3/1] rounded-lg overflow-hidden cursor-pointer group ${animeId === String(season.id)
|
||||
? "ring-2 ring-white/40 shadow-lg shadow-white/10"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={season.season_poster}
|
||||
alt={season.season}
|
||||
className={`w-full h-full object-cover scale-150 ${
|
||||
animeId === String(season.id)
|
||||
? "opacity-50"
|
||||
: "opacity-40 group-hover:opacity-50 transition-opacity"
|
||||
}`}
|
||||
className={`w-full h-full object-cover scale-150 ${animeId === String(season.id)
|
||||
? "opacity-50"
|
||||
: "opacity-40 group-hover:opacity-50 transition-opacity"
|
||||
}`}
|
||||
/>
|
||||
{/* Dots Pattern Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
backgroundImage: `url('data:image/svg+xml,<svg width="3" height="3" viewBox="0 0 3 3" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="1.5" cy="1.5" r="0.5" fill="white" fill-opacity="0.25"/></svg>')`,
|
||||
backgroundSize: '3px 3px'
|
||||
}}
|
||||
/>
|
||||
{/* Dark Gradient Overlay */}
|
||||
<div className={`absolute inset-0 z-20 bg-gradient-to-r ${
|
||||
animeId === String(season.id)
|
||||
? "from-black/50 to-transparent"
|
||||
: "from-black/40 to-transparent"
|
||||
}`} />
|
||||
<div className={`absolute inset-0 z-20 bg-gradient-to-r ${animeId === String(season.id)
|
||||
? "from-black/50 to-transparent"
|
||||
: "from-black/40 to-transparent"
|
||||
}`} />
|
||||
{/* Title Container */}
|
||||
<div className="absolute inset-0 z-30 flex items-center justify-center">
|
||||
<p className={`text-[14px] sm:text-[16px] font-bold text-center px-2 sm:px-4 transition-colors duration-300 ${
|
||||
animeId === String(season.id)
|
||||
? "text-white"
|
||||
: "text-white/90 group-hover:text-white"
|
||||
}`}>
|
||||
<p className={`text-[14px] sm:text-[16px] font-bold text-center px-2 sm:px-4 transition-colors duration-300 ${animeId === String(season.id)
|
||||
? "text-white"
|
||||
: "text-white/90 group-hover:text-white"
|
||||
}`}>
|
||||
{season.season}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
18
src/utils/getSafetitle.js
Normal file
18
src/utils/getSafetitle.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default function getSafeTitle(title, language = 'EN', jpTitle = '') {
|
||||
if (!title) return '';
|
||||
|
||||
// If title is already a string, return it
|
||||
if (typeof title === 'string') return title;
|
||||
|
||||
// If title is an object, extract based on language preference
|
||||
if (typeof title === 'object') {
|
||||
if (language === 'EN') {
|
||||
return title.english || title.romaji || title.userPreferred || title.native || jpTitle || 'Unknown Title';
|
||||
} else {
|
||||
// For JP/other languages, prefer native or romaji
|
||||
return title.native || title.romaji || title.userPreferred || title.english || jpTitle || 'Unknown Title';
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown Title';
|
||||
}
|
||||
Reference in New Issue
Block a user