diff --git a/src/app/latest-completed/page.js b/src/app/latest-completed/page.js index adb06a6..8f97593 100644 --- a/src/app/latest-completed/page.js +++ b/src/app/latest-completed/page.js @@ -264,7 +264,7 @@ export default function LatestCompletedPage() { <>
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => ( - + ))}
diff --git a/src/app/most-popular/page.js b/src/app/most-popular/page.js index 283ba3f..d546f9d 100644 --- a/src/app/most-popular/page.js +++ b/src/app/most-popular/page.js @@ -264,7 +264,7 @@ export default function MostPopularPage() { <>
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => ( - + ))}
diff --git a/src/app/search/[query]/page.js b/src/app/search/[query]/page.js index b1e7649..7c147cc 100644 --- a/src/app/search/[query]/page.js +++ b/src/app/search/[query]/page.js @@ -270,7 +270,7 @@ export default function SearchPage() { <>
{filteredResults.map((anime) => ( - + ))}
diff --git a/src/app/search/page.js b/src/app/search/page.js index c127099..dd5d63f 100644 --- a/src/app/search/page.js +++ b/src/app/search/page.js @@ -391,7 +391,7 @@ function SearchResults() { <>
{filteredList.map((anime) => ( - + ))}
diff --git a/src/app/top-airing/page.js b/src/app/top-airing/page.js index 06cb7f8..afff6f5 100644 --- a/src/app/top-airing/page.js +++ b/src/app/top-airing/page.js @@ -235,7 +235,7 @@ export default function TopAiringPage() { <>
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => ( - + ))}
diff --git a/src/app/watch/[episodeId]/page.js b/src/app/watch/[episodeId]/page.js index b2c73d1..4bbcf1b 100644 --- a/src/app/watch/[episodeId]/page.js +++ b/src/app/watch/[episodeId]/page.js @@ -68,12 +68,33 @@ export default function WatchPage() { console.log('[Watch] Raw episodeId from URL:', episodeId); // Extract animeId from the episodeId parameter - // The new format is: anime-name?ep=episode-number - const [baseId, queryString] = episodeId.split('?'); + // Handle different possible formats: + // 1. anime-name?ep=episode-number (standard format) + // 2. anime-name-episode-number (legacy format) - if (baseId) { - setAnimeId(baseId); - console.log('[Watch] Extracted anime ID:', baseId); + let extractedAnimeId; + let episodeNumber; + + if (episodeId.includes('?ep=')) { + // Format: anime-name?ep=episode-number + const [baseId, queryString] = episodeId.split('?'); + extractedAnimeId = baseId; + episodeNumber = queryString.replace('ep=', ''); + console.log(`[Watch] Format detected: standard (anime-name?ep=episode-number)`); + } else if (episodeId.includes('-')) { + // Format: anime-name-episode-number + const match = episodeId.match(/^(.*?)-(\d+)$/); + if (match) { + extractedAnimeId = match[1]; + episodeNumber = match[2]; + console.log(`[Watch] Format detected: legacy (anime-name-episode-number)`); + } + } + + if (extractedAnimeId) { + setAnimeId(extractedAnimeId); + console.log('[Watch] Extracted anime ID:', extractedAnimeId); + console.log('[Watch] Extracted episode number:', episodeNumber); } else { console.warn('[Watch] Could not extract anime ID from episode ID:', episodeId); } @@ -283,32 +304,35 @@ export default function WatchPage() { // Find current episode in episode list // Handle both formats: anime-name?ep=episode-number or anime-name-episode-number const findCurrentEpisode = () => { - // Extract episode number from the URL - const [, queryString] = currentEpisodeId.split('?'); - let currentEpisodeNumber; + // First, try to find the episode by direct ID match + const directMatch = episodesData.episodes.find(ep => ep.id === currentEpisodeId); + if (directMatch) { + console.log('[Watch] Found episode by direct ID match:', directMatch.number); + return directMatch; + } - if (queryString && queryString.startsWith('ep=')) { - // If it's in the format anime-name?ep=episode-number - currentEpisodeNumber = queryString.replace('ep=', ''); - console.log('[Watch] Current episode number from ?ep= format:', currentEpisodeNumber); - } else { - // If it's in the format anime-name-episode-number - const match = currentEpisodeId.match(/-(\d+)$/); - if (match && match[1]) { - currentEpisodeNumber = match[1]; - console.log('[Watch] Current episode number from dash format:', currentEpisodeNumber); + // As a fallback, try to match by episode number + // Extract episode number from the URL if it's in the format anime-id?ep=number + if (currentEpisodeId.includes('?ep=')) { + const [, queryString] = currentEpisodeId.split('?'); + if (queryString) { + const episodeNumber = queryString.replace('ep=', ''); + console.log('[Watch] Trying to find by episode number:', episodeNumber); + + const numberMatch = episodesData.episodes.find(ep => + ep.number && ep.number.toString() === episodeNumber.toString() + ); + + if (numberMatch) { + console.log('[Watch] Found episode by number:', numberMatch.number); + return numberMatch; + } } } - if (currentEpisodeNumber) { - // Try to find the episode by number - return episodesData.episodes.find(ep => - ep.number && ep.number.toString() === currentEpisodeNumber.toString() - ); - } - - // If no match by number, try to match by full ID - return episodesData.episodes.find(ep => ep.id === currentEpisodeId); + // If no match found, return first episode as fallback + console.warn('[Watch] Could not find matching episode, falling back to first episode'); + return episodesData.episodes[0]; }; const episode = findCurrentEpisode(); @@ -377,6 +401,11 @@ export default function WatchPage() { const handleEpisodeClick = (newEpisodeId) => { if (newEpisodeId !== currentEpisodeId) { + console.log(`[Watch] Episode clicked, ID: ${newEpisodeId}`); + + // Use the episode ID directly as it should already be in the correct format + // from the API response (animeId?ep=episodeNumber) + // Update the URL using history API const newUrl = `/watch/${encodeURIComponent(newEpisodeId)}`; window.history.pushState({ episodeId: newEpisodeId }, '', newUrl); diff --git a/src/components/AnimeCard.js b/src/components/AnimeCard.js index 4143b15..4736b54 100644 --- a/src/components/AnimeCard.js +++ b/src/components/AnimeCard.js @@ -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 ( -
+
{/* Image card linking to watch page */} { + 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) && ( 0 || info.stats.episodes.dub > 0) && ( ))}
diff --git a/src/components/EpisodeList.js b/src/components/EpisodeList.js index 46c783c..fb5e478 100644 --- a/src/components/EpisodeList.js +++ b/src/components/EpisodeList.js @@ -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 diff --git a/src/components/SpotlightCarousel.js b/src/components/SpotlightCarousel.js index 8210baa..c30f153 100644 --- a/src/components/SpotlightCarousel.js +++ b/src/components/SpotlightCarousel.js @@ -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 (
{ }} 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) => ( @@ -131,7 +229,7 @@ const SpotlightCarousel = ({ items = [] }) => { {/* Buttons - Below title on mobile, right side on desktop */}
diff --git a/src/lib/api.js b/src/lib/api.js index 52fc204..8d6efae 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -463,6 +463,9 @@ export const fetchAnimeEpisodes = async (animeId) => { return { episodes: [] }; } + // API already returns episodeId in correct format (animeId?ep=episodeNumber) + // So we simply use it directly without any formatting + return { episodes: data.data.episodes || [], totalEpisodes: data.data.totalEpisodes || 0 @@ -480,20 +483,11 @@ export const fetchEpisodeServers = async (episodeId) => { return { servers: [] }; } - // Make sure the episodeId is properly formatted - // If it has a number suffix without ?ep= format, reformat it - let formattedEpisodeId = episodeId; - if (!episodeId.includes('?ep=')) { - // Extract the anime ID and episode number - const match = episodeId.match(/^(.*?)-(\d+)$/); - if (match) { - const [, animeId, episodeNumber] = match; - formattedEpisodeId = `${animeId}?ep=${episodeNumber}`; - console.log(`[API] Reformatted episode ID from ${episodeId} to ${formattedEpisodeId}`); - } - } + console.log(`[API] Processing episode ID: ${episodeId}`); - const apiUrl = `${API_BASE_URL}/episode/servers?animeEpisodeId=${encodeURIComponent(formattedEpisodeId)}`; + // episodeId should already be in the correct format (animeId?ep=episodeNumber) + // from the API response, so we use it directly + const apiUrl = `${API_BASE_URL}/episode/servers?animeEpisodeId=${encodeURIComponent(episodeId)}`; console.log(`[API Call] Fetching servers from: ${apiUrl}`); const response = await fetch(apiUrl, { @@ -547,22 +541,13 @@ export const fetchEpisodeSources = async (episodeId, dub = false, server = 'hd-2 return { sources: [] }; } - // Make sure the episodeId is properly formatted - // If it has a number suffix without ?ep= format, reformat it - let formattedEpisodeId = episodeId; - if (!episodeId.includes('?ep=')) { - // Extract the anime ID and episode number - const match = episodeId.match(/^(.*?)-(\d+)$/); - if (match) { - const [, animeId, episodeNumber] = match; - formattedEpisodeId = `${animeId}?ep=${episodeNumber}`; - console.log(`[API] Reformatted episode ID from ${episodeId} to ${formattedEpisodeId}`); - } - } + console.log(`[API] Processing episode ID for sources: ${episodeId}`); + // episodeId should already be in the correct format (animeId?ep=episodeNumber) + // from the API response, so we use it directly const category = dub ? 'dub' : 'sub'; const serverName = server || 'hd-2'; // Default to hd-2 if server is null or empty - const apiUrl = `${API_BASE_URL}/episode/sources?animeEpisodeId=${encodeURIComponent(formattedEpisodeId)}&category=${category}&server=${serverName}`; + const apiUrl = `${API_BASE_URL}/episode/sources?animeEpisodeId=${encodeURIComponent(episodeId)}&category=${category}&server=${serverName}`; console.log(`[API Call] Fetching sources from: ${apiUrl}`); const response = await fetch(apiUrl, {