fixed, made it working

This commit is contained in:
Tejas Panchal
2026-02-15 06:01:51 +05:30
parent a80228f697
commit 530eb0d5a9
18 changed files with 406 additions and 252 deletions

View File

@@ -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>

View File

@@ -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>
);

View 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;

View File

@@ -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

View File

@@ -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>
))}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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 && (

View File

@@ -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]">

View File

@@ -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 && (

View File

@@ -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) && (

View File

@@ -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 && (

View File

@@ -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 && (

View File

@@ -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);

View File

@@ -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>

View File

@@ -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
View 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';
}