dptxa-proxy / src /components /EpisodeList.jsx
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
7.06 kB
import { Play, CheckCircle2 } from 'lucide-react';
import { useState, useEffect } from 'react';
export default function EpisodeList({ episodes, currentTap, movieSlug }) {
const [serverIndex, setServerIndex] = useState(0);
const [selectedChunkIndex, setSelectedChunkIndex] = useState(0);
const [watched, setWatched] = useState([]);
// Tự động đồng bộ server và tab (chunk) chứa tập đang phát (currentTap)
useEffect(() => {
if (currentTap && episodes?.length) {
const foundServerIdx = episodes.findIndex(server =>
server.server_data?.some(ep => ep.name === currentTap)
);
if (foundServerIdx !== -1) {
setServerIndex(foundServerIdx);
const targetServer = episodes[foundServerIdx];
const targetEpisodes = targetServer?.server_data || [];
const epIdx = targetEpisodes.findIndex(ep => ep.name === currentTap);
if (epIdx !== -1) {
const targetChunkIdx = Math.floor(epIdx / 25);
setSelectedChunkIndex(targetChunkIdx);
}
}
}
}, [currentTap, episodes]);
// Tải lịch sử xem phim
useEffect(() => {
try {
const history = JSON.parse(localStorage.getItem('txa_watch_history') || '{}');
if (history[movieSlug]?.watched) {
setWatched(history[movieSlug].watched);
}
} catch (e) {
console.error('Failed to load watch history');
}
}, [movieSlug]);
if (!episodes?.length) return null;
const currentServer = episodes[serverIndex] || episodes[0];
const allEpisodes = currentServer?.server_data || [];
// Chia danh sách tập phim thành các nhóm tối đa 25 tập
const chunkSize = 25;
const chunks = [];
for (let i = 0; i < allEpisodes.length; i += chunkSize) {
chunks.push(allEpisodes.slice(i, i + chunkSize));
}
// Đảm bảo selectedChunkIndex không vượt quá phạm vi mới sau khi đổi server
const activeChunkIndex = selectedChunkIndex < chunks.length ? selectedChunkIndex : 0;
const visibleEpisodes = chunks[activeChunkIndex] || [];
return (
<div suppressHydrationWarning className="glass rounded-[32px] p-6 shadow-2xl backdrop-blur-2xl border border-white/5 space-y-6">
{/* Tiêu đề & Chọn Server / Chọn Tab 25 tập */}
<div suppressHydrationWarning className="flex flex-col gap-4 border-b border-white/5 pb-6">
<div suppressHydrationWarning className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div suppressHydrationWarning className="flex items-center gap-3">
<span className="h-8 w-2 rounded-full bg-primary shadow-[0_0_15px_rgba(229,9,20,0.5)]" />
<h3 className="text-xl font-black uppercase tracking-tighter text-white">
Danh sách tập
</h3>
</div>
{/* Các Server phát */}
{episodes.length > 1 && (
<div suppressHydrationWarning className="flex flex-wrap gap-2">
{episodes.map((server, idx) => (
<button
key={idx}
onClick={() => {
setServerIndex(idx);
setSelectedChunkIndex(0); // Reset về tab đầu tiên khi đổi server
}}
className={`rounded-2xl px-5 py-2.5 text-xs font-black uppercase tracking-widest transition-all cursor-pointer ${
serverIndex === idx
? 'bg-primary text-white shadow-[0_0_20px_rgba(229,9,20,0.4)] scale-105'
: 'glass text-gray-400 hover:text-white hover:bg-white/5'
}`}
>
{server.server_name}
</button>
))}
</div>
)}
</div>
{/* Các Tab phân trang 25 tập (chỉ hiện khi có trên 25 tập) */}
{chunks.length > 1 && (
<div suppressHydrationWarning className="flex flex-wrap gap-2 mt-2 pt-4 border-t border-white/5">
{chunks.map((chunk, idx) => {
const startIdx = idx * chunkSize + 1;
const endIdx = Math.min((idx + 1) * chunkSize, allEpisodes.length);
// Tạo nhãn tab trực quan theo tên tập hoặc index số
const getLabel = () => {
const sName = chunk[0]?.name;
const eName = chunk[chunk.length - 1]?.name;
const sNum = parseInt(sName);
const eNum = parseInt(eName);
if (!isNaN(sNum) && !isNaN(eNum)) {
return `Tập ${sNum} - ${eNum}`;
}
return `${sName} - ${eName}`;
};
const isActive = activeChunkIndex === idx;
return (
<button
key={idx}
onClick={() => setSelectedChunkIndex(idx)}
className={`rounded-xl px-4 py-2 text-xs font-black transition-all cursor-pointer ${
isActive
? 'bg-yellow-500 text-dark shadow-[0_0_15px_rgba(234,179,8,0.3)] scale-105'
: 'bg-white/5 text-gray-400 hover:text-white hover:bg-white/10'
}`}
>
{getLabel()}
</button>
);
})}
</div>
)}
</div>
{/* Grid danh sách tập */}
<div suppressHydrationWarning className="grid grid-cols-3 gap-3 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10">
{visibleEpisodes.map((ep) => {
const isWatched = watched.includes(ep.name);
const isActive = ep.name === currentTap;
return (
<a
key={ep.name}
href={`/xem/${movieSlug}?tap=${ep.name}`}
className={`group relative flex flex-col items-center justify-center rounded-2xl py-4 transition-all duration-300 ${
isActive
? 'bg-primary shadow-[0_0_25px_rgba(229,9,20,0.3)] ring-2 ring-primary scale-105 z-10'
: 'glass hover:bg-white/10 hover:translate-y-[-2px]'
}`}
>
<span className={`text-sm font-black tracking-tight ${isActive ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>
{ep.name.toLowerCase().includes('tập') ? ep.name : `Tập ${ep.name}`}
</span>
{isWatched && !isActive && (
<div className="absolute -right-1 -top-1 rounded-full bg-green-500 p-0.5 text-white shadow-lg ring-2 ring-[#0f0f0f]">
<CheckCircle2 size={10} fill="currentColor" className="text-white" />
</div>
)}
{isActive && (
<div className="mt-1">
<Play size={10} fill="white" className="text-white animate-pulse" />
</div>
)}
</a>
);
})}
</div>
</div>
);
}