watch page progress

This commit is contained in:
Tejas Panchal
2025-07-30 20:55:12 +05:30
parent c0c1e93660
commit ef1746a716
4 changed files with 196 additions and 156 deletions

View File

@@ -137,9 +137,9 @@ function Episodelist({
}, [activeEpisodeId, episodes]); }, [activeEpisodeId, episodes]);
return ( return (
<div className="relative flex flex-col w-full h-full max-[1200px]:max-h-[500px]"> <div className="flex flex-col w-full h-full">
<div className="sticky top-0 z-10 flex flex-col gap-y-[5px] justify-start px-4 py-5 bg-[#1a1a1a] border-b border-[#2a2a2a]"> <div className="sticky top-0 z-10 flex flex-col gap-y-[5px] justify-start px-4 py-3 bg-[#1a1a1a] border-b border-[#2a2a2a]">
<h1 className="text-[14px] font-semibold text-white mb-2">Episodes</h1> <h1 className="text-[14px] font-semibold text-white mb-1">Episodes</h1>
{totalEpisodes > 100 && ( {totalEpisodes > 100 && (
<div className="w-full flex gap-x-4 items-center max-[1200px]:justify-between"> <div className="w-full flex gap-x-4 items-center max-[1200px]:justify-between">
<div className="min-w-fit flex text-[13px]"> <div className="min-w-fit flex text-[13px]">
@@ -198,7 +198,7 @@ function Episodelist({
</div> </div>
)} )}
</div> </div>
<div ref={listContainerRef} className="w-full h-full overflow-y-auto bg-[#1a1a1a]"> <div ref={listContainerRef} className="w-full flex-1 overflow-y-auto bg-[#1a1a1a]">
<div <div
className={`${ className={`${
totalEpisodes > 30 totalEpisodes > 30

View File

@@ -9,10 +9,9 @@ import { Link, useNavigate } from "react-router-dom";
import useToolTipPosition from "@/src/hooks/useToolTipPosition"; import useToolTipPosition from "@/src/hooks/useToolTipPosition";
import Qtip from "../qtip/Qtip"; import Qtip from "../qtip/Qtip";
function Sidecard({ data, label, className, limit }) { function Sidecard({ data, label, className }) {
const { language } = useLanguage(); const { language } = useLanguage();
const navigate = useNavigate(); const navigate = useNavigate();
const [showAll, setShowAll] = useState(false);
const [hoverTimeout, setHoverTimeout] = useState(null); const [hoverTimeout, setHoverTimeout] = useState(null);
const handleMouseEnter = (item, index) => { const handleMouseEnter = (item, index) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@@ -24,40 +23,23 @@ function Sidecard({ data, label, className, limit }) {
clearTimeout(hoverTimeout); clearTimeout(hoverTimeout);
setHoveredItem(null); setHoveredItem(null);
}; };
const toggleShowAll = () => {
setShowAll((prev) => !prev);
};
const displayedData = limit
? data.slice(0, limit)
: showAll
? data
: data.slice(0, 6);
const [hoveredItem, setHoveredItem] = useState(null); const [hoveredItem, setHoveredItem] = useState(null);
const { tooltipPosition, tooltipHorizontalPosition, cardRefs } = const { tooltipPosition, tooltipHorizontalPosition, cardRefs } =
useToolTipPosition(hoveredItem, data); useToolTipPosition(hoveredItem, data);
return ( return (
<div className={`flex flex-col space-y-6 ${className}`}> <div className={`flex flex-col ${className}`}>
<h1 className="font-bold text-2xl text-[#ffbade]">{label}</h1> <div className="flex flex-col space-y-2 max-h-[600px] overflow-y-auto pr-2 scrollbar-thin scrollbar-track-[#1a1a1a] scrollbar-thumb-[#2a2a2a] hover:scrollbar-thumb-[#333] scrollbar-thumb-rounded">
<div className="flex flex-col space-y-4 bg-[#2B2A3C] p-4 pt-8">
{data && {data &&
displayedData.map((item, index) => ( data.map((item, index) => (
<div <div
key={index} key={index}
className="flex items-center gap-x-4" className="group"
ref={(el) => (cardRefs.current[index] = el)} ref={(el) => (cardRefs.current[index] = el)}
> >
<div <div className="flex items-start gap-3 p-2 rounded-lg transition-colors hover:bg-[#1f1f1f]">
style={{ {hoveredItem === item.id + index && window.innerWidth > 1024 && (
borderBottom:
index + 1 < displayedData.length
? "1px solid rgba(255, 255, 255, .075)"
: "none",
}}
className="flex pb-4 relative container items-center"
>
{hoveredItem === item.id + index &&
window.innerWidth > 1024 && (
<div <div
className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition} ${ className={`absolute ${tooltipPosition} ${tooltipHorizontalPosition} ${
tooltipPosition === "top-1/2" tooltipPosition === "top-1/2"
@@ -75,65 +57,52 @@ function Sidecard({ data, label, className, limit }) {
<img <img
src={`${item.poster}`} src={`${item.poster}`}
alt={item.title} alt={item.title}
className="flex-shrink-0 w-[60px] h-[75px] rounded-md object-cover cursor-pointer" className="w-[50px] h-[70px] rounded object-cover cursor-pointer transition-transform group-hover:scale-105"
onClick={() => navigate(`/watch/${item.id}`)} onClick={() => navigate(`/watch/${item.id}`)}
onMouseEnter={() => handleMouseEnter(item, index)} onMouseEnter={() => handleMouseEnter(item, index)}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
/> />
<div className="flex flex-col ml-4 space-y-2"> <div className="flex flex-col gap-1.5 flex-1 min-w-0">
<Link <Link
to={`/${item.id}`} to={`/${item.id}`}
className="text-[1em] font-[500] hover:cursor-pointer hover:text-[#ffbade] transform transition-all ease-out line-clamp-1 max-[478px]:line-clamp-2 max-[478px]:text-[14px]" className="text-sm font-medium text-gray-200 hover:text-white transition-colors line-clamp-1"
onClick={() => onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
window.scrollTo({ top: 0, behavior: "smooth" })
}
> >
{language === "EN" ? item.title : item.japanese_title} {language === "EN" ? item.title : item.japanese_title}
</Link> </Link>
<div className="flex flex-wrap items-center w-fit space-x-1 max-[320px]:gap-y-2"> <div className="flex flex-wrap items-center gap-2">
{item.tvInfo?.sub && ( {item.tvInfo?.sub && (
<div className="flex space-x-1 justify-center items-center bg-[#B0E3AF] rounded-[4px] px-[4px] text-black py-[2px]"> <div className="flex items-center gap-1 px-1.5 py-0.5 bg-[#2a2a2a] rounded text-gray-300">
<FontAwesomeIcon <FontAwesomeIcon
icon={faClosedCaptioning} icon={faClosedCaptioning}
className="text-[12px]" className="text-[10px]"
/> />
<p className="text-[12px] font-bold"> <span className="text-[10px] font-medium">
{item.tvInfo.sub} {item.tvInfo.sub}
</p> </span>
</div> </div>
)} )}
{item.tvInfo?.dub && ( {item.tvInfo?.dub && (
<div className="flex space-x-1 justify-center items-center bg-[#B9E7FF] rounded-[4px] px-[8px] text-black py-[2px]"> <div className="flex items-center gap-1 px-1.5 py-0.5 bg-[#2a2a2a] rounded text-gray-300">
<FontAwesomeIcon <FontAwesomeIcon
icon={faMicrophone} icon={faMicrophone}
className="text-[12px]" className="text-[10px]"
/> />
<p className="text-[12px] font-bold"> <span className="text-[10px] font-medium">
{item.tvInfo.dub} {item.tvInfo.dub}
</p> </span>
</div> </div>
)} )}
{item.tvInfo?.showType && ( {item.tvInfo?.showType && (
<div className="flex items-center gap-x-2"> <span className="text-xs text-gray-400">
<div className="dot ml-[4px]"></div>
<p className="text-[15px] font-light">
{item.tvInfo.showType} {item.tvInfo.showType}
</p> </span>
</div>
)} )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
))} ))}
{!limit && data.length > 6 && (
<button
className="w-full bg-[#555462d3] py-3 mt-4 hover:bg-[#555462] rounded-md font-bold transform transition-all ease-out"
onClick={toggleShowAll}
>
{showAll ? "Show less" : "Show more"}
</button>
)}
</div> </div>
</div> </div>
); );

View File

@@ -3,14 +3,17 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const ToggleButton = ({ label, isActive, onClick }) => ( const ToggleButton = ({ label, isActive, onClick }) => (
<button className="flex gap-x-2" onClick={onClick}> <button
<h1 className="capitalize text-[13px]">{label}</h1> className="flex items-center text-xs px-2 py-0.5 rounded transition-colors hover:bg-[#2a2a2a]"
onClick={onClick}
>
<span className="text-gray-300">{label}</span>
<span <span
className={`capitalize text-[13px] ${ className={`ml-1.5 ${
isActive ? "text-[#ffbade]" : "text-red-500" isActive ? "text-white" : "text-gray-500"
}`} }`}
> >
{isActive ? "on" : "off"} {isActive ? "ON" : "OFF"}
</span> </span>
</button> </button>
); );
@@ -42,25 +45,25 @@ export default function WatchControls({
}, [episodeId, episodes]); }, [episodeId, episodes]);
return ( return (
<div className="bg-[#11101A] w-full flex justify-between flex-wrap px-4 pt-4 max-[1200px]:bg-[#14151A] max-[375px]:flex-col max-[375px]:gap-y-2"> <div className="w-full flex justify-between items-center px-3 py-2 border-b border-gray-800">
<div className="flex gap-x-4 flex-wrap"> <div className="flex gap-x-2">
<ToggleButton <ToggleButton
label="auto play" label="Auto Play"
isActive={autoPlay} isActive={autoPlay}
onClick={() => setAutoPlay((prev) => !prev)} onClick={() => setAutoPlay((prev) => !prev)}
/> />
<ToggleButton <ToggleButton
label="auto skip intro" label="Skip Intro"
isActive={autoSkipIntro} isActive={autoSkipIntro}
onClick={() => setAutoSkipIntro((prev) => !prev)} onClick={() => setAutoSkipIntro((prev) => !prev)}
/> />
<ToggleButton <ToggleButton
label="auto next" label="Auto Next"
isActive={autoNext} isActive={autoNext}
onClick={() => setAutoNext((prev) => !prev)} onClick={() => setAutoNext((prev) => !prev)}
/> />
</div> </div>
<div className="flex gap-x-6 max-[575px]:gap-x-4 max-[375px]:justify-end"> <div className="flex items-center gap-x-2">
<button <button
onClick={() => { onClick={() => {
if (currentEpisodeIndex > 0) { if (currentEpisodeIndex > 0) {
@@ -70,11 +73,13 @@ export default function WatchControls({
} }
}} }}
disabled={currentEpisodeIndex <= 0} disabled={currentEpisodeIndex <= 0}
className={`w-7 h-7 flex items-center justify-center rounded transition-colors ${
currentEpisodeIndex <= 0
? "text-gray-600 cursor-not-allowed"
: "text-gray-300 hover:text-white"
}`}
> >
<FontAwesomeIcon <FontAwesomeIcon icon={faBackward} className="text-[14px]" />
icon={faBackward}
className="text-[20px] max-[575px]:text-[16px] text-white"
/>
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -85,11 +90,13 @@ export default function WatchControls({
} }
}} }}
disabled={currentEpisodeIndex >= episodes?.length - 1} disabled={currentEpisodeIndex >= episodes?.length - 1}
className={`w-7 h-7 flex items-center justify-center rounded transition-colors ${
currentEpisodeIndex >= episodes?.length - 1
? "text-gray-600 cursor-not-allowed"
: "text-gray-300 hover:text-white"
}`}
> >
<FontAwesomeIcon <FontAwesomeIcon icon={faForward} className="text-[14px]" />
icon={faForward}
className="text-[20px] max-[575px]:text-[16px] text-white"
/>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -72,6 +72,10 @@ export default function Watch() {
autoNext, autoNext,
setAutoNext, setAutoNext,
} = useWatchControl(); } = useWatchControl();
const playerRef = useRef(null);
const videoContainerRef = useRef(null);
const controlsRef = useRef(null);
const episodesRef = useRef(null);
useEffect(() => { useEffect(() => {
if (!episodes || episodes.length === 0) return; if (!episodes || episodes.length === 0) return;
@@ -119,27 +123,67 @@ export default function Watch() {
}, [streamInfo, episodeId, animeId, totalEpisodes, navigate]); }, [streamInfo, episodeId, animeId, totalEpisodes, navigate]);
useEffect(() => { useEffect(() => {
// Function to adjust the height of episodes list to match only video + controls
const adjustHeight = () => { const adjustHeight = () => {
if (window.innerWidth > 1200) { if (window.innerWidth > 1200) {
const player = document.querySelector(".player"); if (videoContainerRef.current && controlsRef.current && episodesRef.current) {
const episodes = document.querySelector(".episodes"); // Calculate combined height of video container and controls
if (player && episodes) { const videoHeight = videoContainerRef.current.offsetHeight;
episodes.style.height = `${player.clientHeight}px`; const controlsHeight = controlsRef.current.offsetHeight;
const totalHeight = videoHeight + controlsHeight;
// Apply the combined height to episodes container
episodesRef.current.style.height = `${totalHeight}px`;
} }
} else { } else {
const episodes = document.querySelector(".episodes"); if (episodesRef.current) {
if (episodes) { episodesRef.current.style.height = 'auto';
episodes.style.height = "auto";
} }
} }
}; };
// Initial adjustment with delay to ensure player is fully rendered
const initialTimer = setTimeout(() => {
adjustHeight(); adjustHeight();
window.addEventListener("resize", adjustHeight); }, 500);
return () => {
window.removeEventListener("resize", adjustHeight); // 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, {
attributes: true,
childList: true,
subtree: true
});
}
if (controlsRef.current) {
observer.observe(controlsRef.current, {
attributes: true,
childList: true,
subtree: true
});
}
// Set up additional interval for continuous adjustments
const intervalId = setInterval(adjustHeight, 1000);
// Clean up
return () => {
clearTimeout(initialTimer);
clearInterval(intervalId);
observer.disconnect();
window.removeEventListener('resize', adjustHeight);
};
}, [buffering, activeServerType, activeServerName, episodeId, streamUrl, episodes]);
function Tag({ bgColor, index, icon, text }) { function Tag({ bgColor, index, icon, text }) {
return ( return (
<div <div
@@ -182,13 +226,13 @@ export default function Watch() {
}, [animeId, animeInfo]); }, [animeId, animeInfo]);
return ( return (
<div className="w-full min-h-screen bg-[#0a0a0a]"> <div className="w-full min-h-screen bg-[#0a0a0a]">
<div className="w-full max-w-[1920px] mx-auto px-6 pt-[128px] pb-12 max-[1200px]:pt-[64px] max-[1024px]:px-4 max-md:pt-[50px]"> <div className="w-full max-w-[1920px] mx-auto px-6 pt-[80px] pb-12 max-[1200px]:pt-[48px] max-[1024px]:px-4 max-md:pt-[32px]">
<div className="grid grid-cols-[minmax(0,70%),minmax(0,30%)] gap-6 w-full h-full max-[1200px]:flex max-[1200px]:flex-col"> <div className="grid grid-cols-[minmax(0,70%),minmax(0,30%)] gap-6 w-full h-full max-[1200px]:flex max-[1200px]:flex-col">
{/* Left Column - Player, Controls, Servers */} {/* Left Column - Player, Controls, Servers */}
<div className="flex flex-col w-full gap-6"> <div className="flex flex-col w-full gap-6">
<div className="player w-full h-fit bg-black flex flex-col rounded-xl overflow-hidden"> <div ref={playerRef} className="player w-full h-fit bg-black flex flex-col rounded-xl overflow-hidden">
{/* Video Container */} {/* Video Container */}
<div className="w-full relative aspect-video bg-black"> <div ref={videoContainerRef} className="w-full relative aspect-video bg-black">
{!buffering ? (["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ? {!buffering ? (["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ?
<IframePlayer <IframePlayer
episodeId={episodeId} episodeId={episodeId}
@@ -241,9 +285,9 @@ export default function Watch() {
</div> </div>
{/* Controls Section */} {/* Controls Section */}
<div className="bg-[#0f0f0f]"> <div className="bg-[#121212]">
{!buffering && ( {!buffering && (
<div className="border-b border-[#272727]"> <div ref={controlsRef}>
<Watchcontrols <Watchcontrols
autoPlay={autoPlay} autoPlay={autoPlay}
setAutoPlay={setAutoPlay} setAutoPlay={setAutoPlay}
@@ -260,8 +304,7 @@ export default function Watch() {
)} )}
{/* Title and Server Selection */} {/* Title and Server Selection */}
<div className="p-3"> <div className="px-3 py-2">
<div> <div>
<Servers <Servers
servers={servers} servers={servers}
@@ -326,9 +369,20 @@ export default function Watch() {
)} )}
<div className="flex flex-col gap-y-4 flex-1"> <div className="flex flex-col gap-y-4 flex-1">
{animeInfo && animeInfo?.title ? ( {animeInfo && animeInfo?.title ? (
<h1 className="text-[28px] font-medium text-white leading-tight"> <Link
to={`/${animeId}`}
className="group"
>
<h1 className="text-[28px] font-medium text-white leading-tight group-hover:text-gray-300 transition-colors">
{language ? animeInfo?.title : animeInfo?.japanese_title} {language ? animeInfo?.title : animeInfo?.japanese_title}
</h1> </h1>
<div className="flex items-center gap-1.5 mt-1 text-gray-400 text-sm group-hover:text-white transition-colors">
<span>View Details</span>
<svg className="w-4 h-4 transform group-hover:translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</Link>
) : ( ) : (
<Skeleton className="w-[170px] h-[20px] rounded-xl" /> <Skeleton className="w-[170px] h-[20px] rounded-xl" />
)} )}
@@ -366,12 +420,6 @@ export default function Watch() {
)} )}
</p> </p>
)} )}
<Link
to={`/${animeId}`}
className="w-fit px-4 py-2 bg-[#2a2a2a] hover:bg-[#333] text-gray-300 hover:text-white rounded-md transition-all text-sm font-medium mt-2"
>
View Details
</Link>
</div> </div>
</div> </div>
</div> </div>
@@ -380,22 +428,47 @@ export default function Watch() {
{seasons?.length > 0 && ( {seasons?.length > 0 && (
<div className="p-6 bg-[#141414] rounded-lg"> <div className="p-6 bg-[#141414] rounded-lg">
<h2 className="text-xl font-semibold mb-4 text-white">More Seasons</h2> <h2 className="text-xl font-semibold mb-4 text-white">More Seasons</h2>
<div className="grid grid-cols-5 gap-4 max-[1200px]:grid-cols-4 max-[900px]:grid-cols-3 max-[600px]:grid-cols-2"> <div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 sm:gap-4">
{seasons.map((season, index) => ( {seasons.map((season, index) => (
<Link <Link
to={`/${season.id}`} to={`/${season.id}`}
key={index} key={index}
className={`relative aspect-[2/3] rounded-lg overflow-hidden group ${ className={`relative w-full aspect-[3/1] rounded-lg overflow-hidden cursor-pointer group ${
animeId === String(season.id) ? "ring-2 ring-white" : "" animeId === String(season.id)
? "ring-2 ring-white/40 shadow-lg shadow-white/10"
: ""
}`} }`}
> >
<img <img
src={`${season.season_poster}`} src={season.season_poster}
alt="" alt={season.season}
className="w-full h-full object-cover transition-transform group-hover:scale-110" className={`w-full h-full object-cover scale-150 ${
animeId === String(season.id)
? "opacity-50"
: "opacity-40 group-hover:opacity-50 transition-opacity"
}`}
/> />
<div className="absolute inset-0 bg-black bg-opacity-60 flex items-center justify-center p-3 group-hover:bg-opacity-40 transition-all"> {/* Dots Pattern Overlay */}
<p className="text-center text-sm font-medium"> <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"
}`} />
{/* 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"
}`}>
{season.season} {season.season}
</p> </p>
</div> </div>
@@ -404,37 +477,12 @@ export default function Watch() {
</div> </div>
</div> </div>
)} )}
{/* Recommended Section */}
{animeInfo?.recommended_data.length > 0 && (
<div className="p-6 bg-[#141414] rounded-lg">
<h2 className="text-xl font-semibold mb-4 text-white">Recommended</h2>
<CategoryCard
data={animeInfo?.recommended_data}
limit={animeInfo?.recommended_data.length}
showViewMore={false}
/>
</div>
)}
{/* Related Anime Section */}
{animeInfo && animeInfo.related_data ? (
<div className="p-6 bg-[#141414] rounded-lg">
<h2 className="text-xl font-semibold mb-4 text-white">Related Anime</h2>
<Sidecard
data={animeInfo.related_data}
className="!mt-0"
/>
</div>
) : (
<div className="mt-6">
<SidecardLoader />
</div>
)}
</div> </div>
{/* Right Column - Episodes */} {/* Right Column - Episodes and Related */}
<div className="episodes h-full bg-[#141414] rounded-lg overflow-hidden"> <div className="flex flex-col gap-6 h-full">
{/* Episodes Section */}
<div ref={episodesRef} className="episodes flex-shrink-0 bg-[#141414] rounded-lg overflow-hidden">
{!episodes ? ( {!episodes ? (
<div className="h-full flex items-center justify-center"> <div className="h-full flex items-center justify-center">
<BouncingLoader /> <BouncingLoader />
@@ -448,6 +496,22 @@ export default function Watch() {
/> />
)} )}
</div> </div>
{/* Related Anime Section */}
{animeInfo && animeInfo.related_data ? (
<div className="bg-[#141414] rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4 text-white">Related Anime</h2>
<Sidecard
data={animeInfo.related_data}
className="!mt-0"
/>
</div>
) : (
<div className="mt-6">
<SidecardLoader />
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>