need to correct but atleast gives correct episodeId

This commit is contained in:
tejaspanchall
2025-06-19 19:17:07 +05:30
parent 14f6ac8327
commit 730f203aa0
12 changed files with 265 additions and 82 deletions

View File

@@ -1,11 +1,15 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
import { fetchAnimeEpisodes } from '@/lib/api';
export default function AnimeCard({ anime, isRecent }) {
const [imageError, setImageError] = useState(false);
const [firstEpisodeId, setFirstEpisodeId] = useState(null);
const [isHovered, setIsHovered] = useState(false);
const timerRef = useRef(null);
if (!anime) return null;
@@ -14,17 +18,60 @@ export default function AnimeCard({ anime, isRecent }) {
setImageError(true);
};
// Fetch first episode ID when component is hovered
useEffect(() => {
const fetchFirstEpisode = async () => {
if (anime?.id && isHovered && !firstEpisodeId) {
try {
const response = await fetchAnimeEpisodes(anime.id);
if (response.episodes && response.episodes.length > 0) {
// Get the first episode's episodeId
setFirstEpisodeId(response.episodes[0].episodeId);
console.log(`[AnimeCard] First episode ID for ${anime.name}: ${response.episodes[0].episodeId}`);
}
} catch (error) {
console.error(`[AnimeCard] Error fetching episodes for ${anime.id}:`, error);
}
}
};
fetchFirstEpisode();
}, [anime?.id, isHovered, firstEpisodeId]);
const handleMouseEnter = () => {
// Clear any existing timers
if (timerRef.current) clearTimeout(timerRef.current);
// Set a small delay to prevent API calls for quick mouseovers
timerRef.current = setTimeout(() => {
setIsHovered(true);
}, 300); // Delay to prevent unnecessary API calls
};
const handleMouseLeave = () => {
// Clear the timer if the user moves the mouse away quickly
if (timerRef.current) clearTimeout(timerRef.current);
setIsHovered(false);
};
// Get image URL with fallback
const imageSrc = imageError ? '/images/placeholder.png' : anime.poster;
// Generate appropriate links
const infoLink = `/anime/${anime.id}`;
const watchLink = isRecent
? `/watch/${anime.id}?ep=${anime.episodes?.sub || anime.episodes?.dub || 1}`
: `/anime/${anime.id}`;
// Build the watch URL based on the first episode ID or fallback
const watchLink = isRecent ? (
firstEpisodeId
? `/watch/${firstEpisodeId}`
: `/watch/${anime.id}?ep=${anime.episodes?.sub || anime.episodes?.dub || 1}`
) : infoLink;
return (
<div className="anime-card w-full flex flex-col">
<div
className="anime-card w-full flex flex-col"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{/* Image card linking to watch page */}
<Link
href={watchLink}

View File

@@ -5,12 +5,15 @@ import Image from 'next/image';
import Link from 'next/link';
import AnimeRow from './AnimeRow';
import SeasonRow from './SeasonRow';
import { fetchAnimeEpisodes } from '@/lib/api';
export default function AnimeDetails({ anime }) {
const [isExpanded, setIsExpanded] = useState(false);
const [activeVideo, setActiveVideo] = useState(null);
const [activeTab, setActiveTab] = useState('synopsis');
const [synopsisOverflows, setSynopsisOverflows] = useState(false);
const [firstEpisodeId, setFirstEpisodeId] = useState(null);
const [isLoadingEpisodes, setIsLoadingEpisodes] = useState(false);
const synopsisRef = useRef(null);
// Check if synopsis overflows when component mounts or when content changes
@@ -21,6 +24,29 @@ export default function AnimeDetails({ anime }) {
}
}, [anime?.info?.description, activeTab]);
// Fetch first episode ID when component mounts
useEffect(() => {
const fetchFirstEpisode = async () => {
if (anime?.info?.id) {
setIsLoadingEpisodes(true);
try {
const response = await fetchAnimeEpisodes(anime.info.id);
if (response.episodes && response.episodes.length > 0) {
// Get the first episode's episodeId
setFirstEpisodeId(response.episodes[0].episodeId);
console.log(`[AnimeDetails] First episode ID: ${response.episodes[0].episodeId}`);
}
} catch (error) {
console.error('[AnimeDetails] Error fetching episodes:', error);
} finally {
setIsLoadingEpisodes(false);
}
}
};
fetchFirstEpisode();
}, [anime?.info?.id]);
if (!anime?.info) {
return null;
}
@@ -29,6 +55,11 @@ export default function AnimeDetails({ anime }) {
const hasCharacters = info.characterVoiceActor?.length > 0 || info.charactersVoiceActors?.length > 0;
const hasVideos = info.promotionalVideos && info.promotionalVideos.length > 0;
// Build the watch URL based on the first episode ID or fallback
const watchUrl = firstEpisodeId
? `/watch/${firstEpisodeId}`
: `/watch/${info.id}?ep=1`; // Fallback to old format if API fetch fails
// Video modal for promotional videos
const VideoModal = ({ video, onClose }) => {
if (!video) return null;
@@ -208,7 +239,7 @@ export default function AnimeDetails({ anime }) {
{/* Watch Button - Full Width on Mobile */}
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
<Link
href={`/watch/${info.id}?ep=1`}
href={watchUrl}
className="bg-[#ffffff] text-[var(--background)] px-4 py-2.5 rounded-xl mt-3 hover:opacity-90 transition-opacity flex items-center justify-center font-medium text-sm w-full shadow-lg"
>
<svg
@@ -244,7 +275,7 @@ export default function AnimeDetails({ anime }) {
{/* Watch Button - Desktop */}
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
<Link
href={`/watch/${info.id}?ep=1`}
href={watchUrl}
className="bg-[#ffffff] text-[var(--background)] px-6 py-3 rounded-xl mt-4 hover:opacity-90 transition-opacity flex items-center justify-center font-medium text-base w-full shadow-lg"
>
<svg

View File

@@ -77,6 +77,7 @@ export default function AnimeTabs({ topAiring = [], popular = [], latestComplete
<AnimeCard
key={anime.id + '-' + index}
anime={anime}
isRecent={true}
/>
))}
</div>

View File

@@ -52,21 +52,11 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
};
}, [episodes, episodesPerPage]);
// Helper function to normalize episode IDs for comparison
// Helper function for episode ID comparison
// The API returns episode IDs in the format: animeId?ep=episodeNumber
const normalizeEpisodeId = (id) => {
if (!id) return '';
// If it's already in ?ep= format
if (id.includes('?ep=')) return id;
// If it's in anime-name-number format
const match = id.match(/^(.*?)-(\d+)$/);
if (match) {
const [, animeId, episodeNumber] = match;
return `${animeId}?ep=${episodeNumber}`;
}
return id;
// Simply return the ID as-is since the API already provides the correct format
return id || '';
};
const filteredEpisodes = useMemo(() => {
@@ -97,9 +87,11 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
const handleEpisodeSelect = (episode, e) => {
e.preventDefault();
if (onEpisodeClick && episode.id) {
// Use the episode ID directly as it's already in the correct format from the API
console.log(`[EpisodeList] Selected episode: ${episode.number}, ID: ${episode.id}`);
onEpisodeClick(episode.id);
setActiveEpisodeId(episode.id);
}
setActiveEpisodeId(episode.id);
};
// Scroll active episode into view when page changes or active episode changes

View File

@@ -1,10 +1,11 @@
'use client';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Navigation, Pagination, EffectFade } from 'swiper/modules';
import { fetchAnimeEpisodes } from '@/lib/api';
// Import Swiper styles
import 'swiper/css';
@@ -14,12 +15,88 @@ import 'swiper/css/effect-fade';
const SpotlightCarousel = ({ items = [] }) => {
const [isClient, setIsClient] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const [autoplay, setAutoplay] = useState(true);
const [progress, setProgress] = useState(0);
const [episodeIds, setEpisodeIds] = useState({});
const intervalRef = useRef(null);
const progressIntervalRef = useRef(null);
// Handle hydration mismatch
useEffect(() => {
setIsClient(true);
}, []);
// Fetch first episode IDs for all spotlight items
useEffect(() => {
const fetchEpisodeData = async () => {
const episodeData = {};
for (const item of items) {
if (item.id) {
try {
const response = await fetchAnimeEpisodes(item.id);
if (response.episodes && response.episodes.length > 0) {
episodeData[item.id] = response.episodes[0].episodeId;
}
} catch (error) {
console.error(`[SpotlightCarousel] Error fetching episodes for ${item.id}:`, error);
}
}
}
setEpisodeIds(episodeData);
};
if (items && items.length > 0) {
fetchEpisodeData();
}
}, [items]);
// Autoplay functionality
useEffect(() => {
if (autoplay && items.length > 1) {
// Clear any existing intervals
if (intervalRef.current) clearInterval(intervalRef.current);
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
// Set up new intervals
setProgress(0);
progressIntervalRef.current = setInterval(() => {
setProgress(prev => {
const newProgress = prev + 1;
return newProgress <= 100 ? newProgress : prev;
});
}, 50); // Update every 50ms to get smooth progress
intervalRef.current = setTimeout(() => {
setCurrentIndex(prevIndex => (prevIndex + 1) % items.length);
setProgress(0);
}, 5000);
}
return () => {
if (intervalRef.current) clearTimeout(intervalRef.current);
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
};
}, [autoplay, currentIndex, items.length]);
const handleDotClick = (index) => {
setCurrentIndex(index);
setProgress(0);
// Reset autoplay timer when manually changing slides
if (intervalRef.current) clearTimeout(intervalRef.current);
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
if (autoplay) {
intervalRef.current = setTimeout(() => {
setCurrentIndex((index + 1) % items.length);
}, 5000);
}
};
const handleMouseEnter = () => setAutoplay(false);
const handleMouseLeave = () => setAutoplay(true);
// If no items or not on client yet, show loading state
if (!isClient || !items.length) {
return (
@@ -32,6 +109,13 @@ const SpotlightCarousel = ({ items = [] }) => {
);
}
const currentItem = items[currentIndex];
// Get the watch URL for the current item
const watchUrl = episodeIds[currentItem.id]
? `/watch/${episodeIds[currentItem.id]}`
: `/watch/${currentItem.id}?ep=1`; // Fallback to old format if API fetch fails
return (
<div className="w-full mb-6 md:mb-10 spotlight-carousel">
<Swiper
@@ -46,6 +130,20 @@ const SpotlightCarousel = ({ items = [] }) => {
}}
loop={true}
className="rounded-xl overflow-hidden"
onSlideChange={(swiper) => {
setCurrentIndex(swiper.realIndex);
setProgress(0);
// Reset autoplay timer when manually changing slides
if (intervalRef.current) clearTimeout(intervalRef.current);
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
if (autoplay) {
intervalRef.current = setTimeout(() => {
setCurrentIndex((swiper.realIndex + 1) % items.length);
}, 5000);
}
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{items.map((anime, index) => (
<SwiperSlide key={`spotlight-${anime.id}-${index}`}>
@@ -131,7 +229,7 @@ const SpotlightCarousel = ({ items = [] }) => {
{/* Buttons - Below title on mobile, right side on desktop */}
<div className="flex items-center space-x-2 md:space-x-4 mt-1 md:mt-0 md:absolute md:bottom-8 md:right-8">
<Link
href={`/watch/${anime.id}?ep=${anime.episodes?.sub || anime.episodes?.dub || 1}`}
href={watchUrl}
className="bg-white hover:bg-gray-200 text-[#0a0a0a] font-medium text-xs md:text-base px-3 md:px-6 py-1.5 md:py-2 rounded flex items-center space-x-1.5 md:space-x-2 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5 md:h-5 md:w-5" viewBox="0 0 20 20" fill="currentColor">