mirror of
https://github.com/JustAnimeCore/JustAnime.git
synced 2026-04-17 22:01:45 +00:00
need to correct but atleast gives correct episodeId
This commit is contained in:
@@ -264,7 +264,7 @@ export default function LatestCompletedPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
|
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
|
||||||
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => (
|
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => (
|
||||||
<AnimeCard key={anime.id} anime={anime} />
|
<AnimeCard key={anime.id} anime={anime} isRecent={true} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export default function MostPopularPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
|
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
|
||||||
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => (
|
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => (
|
||||||
<AnimeCard key={anime.id} anime={anime} />
|
<AnimeCard key={anime.id} anime={anime} isRecent={true} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export default function SearchPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
{filteredResults.map((anime) => (
|
{filteredResults.map((anime) => (
|
||||||
<AnimeCard key={anime.id} anime={anime} />
|
<AnimeCard key={anime.id} anime={anime} isRecent={true} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ function SearchResults() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4 mb-8">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4 mb-8">
|
||||||
{filteredList.map((anime) => (
|
{filteredList.map((anime) => (
|
||||||
<AnimeCard key={anime.id} anime={anime} />
|
<AnimeCard key={anime.id} anime={anime} isRecent={true} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export default function TopAiringPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
|
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-4">
|
||||||
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => (
|
{(filteredList.length > 0 ? filteredList : animeList).map((anime) => (
|
||||||
<AnimeCard key={anime.id} anime={anime} />
|
<AnimeCard key={anime.id} anime={anime} isRecent={true} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -68,12 +68,33 @@ export default function WatchPage() {
|
|||||||
console.log('[Watch] Raw episodeId from URL:', episodeId);
|
console.log('[Watch] Raw episodeId from URL:', episodeId);
|
||||||
|
|
||||||
// Extract animeId from the episodeId parameter
|
// Extract animeId from the episodeId parameter
|
||||||
// The new format is: anime-name?ep=episode-number
|
// Handle different possible formats:
|
||||||
const [baseId, queryString] = episodeId.split('?');
|
// 1. anime-name?ep=episode-number (standard format)
|
||||||
|
// 2. anime-name-episode-number (legacy format)
|
||||||
|
|
||||||
if (baseId) {
|
let extractedAnimeId;
|
||||||
setAnimeId(baseId);
|
let episodeNumber;
|
||||||
console.log('[Watch] Extracted anime ID:', baseId);
|
|
||||||
|
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 {
|
} else {
|
||||||
console.warn('[Watch] Could not extract anime ID from episode ID:', episodeId);
|
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
|
// Find current episode in episode list
|
||||||
// Handle both formats: anime-name?ep=episode-number or anime-name-episode-number
|
// Handle both formats: anime-name?ep=episode-number or anime-name-episode-number
|
||||||
const findCurrentEpisode = () => {
|
const findCurrentEpisode = () => {
|
||||||
// Extract episode number from the URL
|
// First, try to find the episode by direct ID match
|
||||||
const [, queryString] = currentEpisodeId.split('?');
|
const directMatch = episodesData.episodes.find(ep => ep.id === currentEpisodeId);
|
||||||
let currentEpisodeNumber;
|
if (directMatch) {
|
||||||
|
console.log('[Watch] Found episode by direct ID match:', directMatch.number);
|
||||||
|
return directMatch;
|
||||||
|
}
|
||||||
|
|
||||||
if (queryString && queryString.startsWith('ep=')) {
|
// As a fallback, try to match by episode number
|
||||||
// If it's in the format anime-name?ep=episode-number
|
// Extract episode number from the URL if it's in the format anime-id?ep=number
|
||||||
currentEpisodeNumber = queryString.replace('ep=', '');
|
if (currentEpisodeId.includes('?ep=')) {
|
||||||
console.log('[Watch] Current episode number from ?ep= format:', currentEpisodeNumber);
|
const [, queryString] = currentEpisodeId.split('?');
|
||||||
} else {
|
if (queryString) {
|
||||||
// If it's in the format anime-name-episode-number
|
const episodeNumber = queryString.replace('ep=', '');
|
||||||
const match = currentEpisodeId.match(/-(\d+)$/);
|
console.log('[Watch] Trying to find by episode number:', episodeNumber);
|
||||||
if (match && match[1]) {
|
|
||||||
currentEpisodeNumber = match[1];
|
const numberMatch = episodesData.episodes.find(ep =>
|
||||||
console.log('[Watch] Current episode number from dash format:', currentEpisodeNumber);
|
ep.number && ep.number.toString() === episodeNumber.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (numberMatch) {
|
||||||
|
console.log('[Watch] Found episode by number:', numberMatch.number);
|
||||||
|
return numberMatch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentEpisodeNumber) {
|
// If no match found, return first episode as fallback
|
||||||
// Try to find the episode by number
|
console.warn('[Watch] Could not find matching episode, falling back to first episode');
|
||||||
return episodesData.episodes.find(ep =>
|
return episodesData.episodes[0];
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const episode = findCurrentEpisode();
|
const episode = findCurrentEpisode();
|
||||||
@@ -377,6 +401,11 @@ export default function WatchPage() {
|
|||||||
|
|
||||||
const handleEpisodeClick = (newEpisodeId) => {
|
const handleEpisodeClick = (newEpisodeId) => {
|
||||||
if (newEpisodeId !== currentEpisodeId) {
|
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
|
// Update the URL using history API
|
||||||
const newUrl = `/watch/${encodeURIComponent(newEpisodeId)}`;
|
const newUrl = `/watch/${encodeURIComponent(newEpisodeId)}`;
|
||||||
window.history.pushState({ episodeId: newEpisodeId }, '', newUrl);
|
window.history.pushState({ episodeId: newEpisodeId }, '', newUrl);
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { fetchAnimeEpisodes } from '@/lib/api';
|
||||||
|
|
||||||
export default function AnimeCard({ anime, isRecent }) {
|
export default function AnimeCard({ anime, isRecent }) {
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const [firstEpisodeId, setFirstEpisodeId] = useState(null);
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const timerRef = useRef(null);
|
||||||
|
|
||||||
if (!anime) return null;
|
if (!anime) return null;
|
||||||
|
|
||||||
@@ -14,17 +18,60 @@ export default function AnimeCard({ anime, isRecent }) {
|
|||||||
setImageError(true);
|
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
|
// Get image URL with fallback
|
||||||
const imageSrc = imageError ? '/images/placeholder.png' : anime.poster;
|
const imageSrc = imageError ? '/images/placeholder.png' : anime.poster;
|
||||||
|
|
||||||
// Generate appropriate links
|
// Generate appropriate links
|
||||||
const infoLink = `/anime/${anime.id}`;
|
const infoLink = `/anime/${anime.id}`;
|
||||||
const watchLink = isRecent
|
|
||||||
? `/watch/${anime.id}?ep=${anime.episodes?.sub || anime.episodes?.dub || 1}`
|
// Build the watch URL based on the first episode ID or fallback
|
||||||
: `/anime/${anime.id}`;
|
const watchLink = isRecent ? (
|
||||||
|
firstEpisodeId
|
||||||
|
? `/watch/${firstEpisodeId}`
|
||||||
|
: `/watch/${anime.id}?ep=${anime.episodes?.sub || anime.episodes?.dub || 1}`
|
||||||
|
) : infoLink;
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Image card linking to watch page */}
|
||||||
<Link
|
<Link
|
||||||
href={watchLink}
|
href={watchLink}
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import Image from 'next/image';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import AnimeRow from './AnimeRow';
|
import AnimeRow from './AnimeRow';
|
||||||
import SeasonRow from './SeasonRow';
|
import SeasonRow from './SeasonRow';
|
||||||
|
import { fetchAnimeEpisodes } from '@/lib/api';
|
||||||
|
|
||||||
export default function AnimeDetails({ anime }) {
|
export default function AnimeDetails({ anime }) {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [activeVideo, setActiveVideo] = useState(null);
|
const [activeVideo, setActiveVideo] = useState(null);
|
||||||
const [activeTab, setActiveTab] = useState('synopsis');
|
const [activeTab, setActiveTab] = useState('synopsis');
|
||||||
const [synopsisOverflows, setSynopsisOverflows] = useState(false);
|
const [synopsisOverflows, setSynopsisOverflows] = useState(false);
|
||||||
|
const [firstEpisodeId, setFirstEpisodeId] = useState(null);
|
||||||
|
const [isLoadingEpisodes, setIsLoadingEpisodes] = useState(false);
|
||||||
const synopsisRef = useRef(null);
|
const synopsisRef = useRef(null);
|
||||||
|
|
||||||
// Check if synopsis overflows when component mounts or when content changes
|
// Check if synopsis overflows when component mounts or when content changes
|
||||||
@@ -21,6 +24,29 @@ export default function AnimeDetails({ anime }) {
|
|||||||
}
|
}
|
||||||
}, [anime?.info?.description, activeTab]);
|
}, [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) {
|
if (!anime?.info) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -29,6 +55,11 @@ export default function AnimeDetails({ anime }) {
|
|||||||
const hasCharacters = info.characterVoiceActor?.length > 0 || info.charactersVoiceActors?.length > 0;
|
const hasCharacters = info.characterVoiceActor?.length > 0 || info.charactersVoiceActors?.length > 0;
|
||||||
const hasVideos = info.promotionalVideos && info.promotionalVideos.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
|
// Video modal for promotional videos
|
||||||
const VideoModal = ({ video, onClose }) => {
|
const VideoModal = ({ video, onClose }) => {
|
||||||
if (!video) return null;
|
if (!video) return null;
|
||||||
@@ -208,7 +239,7 @@ export default function AnimeDetails({ anime }) {
|
|||||||
{/* Watch Button - Full Width on Mobile */}
|
{/* Watch Button - Full Width on Mobile */}
|
||||||
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
|
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
|
||||||
<Link
|
<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"
|
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
|
<svg
|
||||||
@@ -244,7 +275,7 @@ export default function AnimeDetails({ anime }) {
|
|||||||
{/* Watch Button - Desktop */}
|
{/* Watch Button - Desktop */}
|
||||||
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
|
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
|
||||||
<Link
|
<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"
|
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
|
<svg
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default function AnimeTabs({ topAiring = [], popular = [], latestComplete
|
|||||||
<AnimeCard
|
<AnimeCard
|
||||||
key={anime.id + '-' + index}
|
key={anime.id + '-' + index}
|
||||||
anime={anime}
|
anime={anime}
|
||||||
|
isRecent={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,21 +52,11 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
};
|
};
|
||||||
}, [episodes, episodesPerPage]);
|
}, [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) => {
|
const normalizeEpisodeId = (id) => {
|
||||||
if (!id) return '';
|
// Simply return the ID as-is since the API already provides the correct format
|
||||||
|
return id || '';
|
||||||
// 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredEpisodes = useMemo(() => {
|
const filteredEpisodes = useMemo(() => {
|
||||||
@@ -97,9 +87,11 @@ export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick,
|
|||||||
const handleEpisodeSelect = (episode, e) => {
|
const handleEpisodeSelect = (episode, e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (onEpisodeClick && episode.id) {
|
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);
|
onEpisodeClick(episode.id);
|
||||||
|
setActiveEpisodeId(episode.id);
|
||||||
}
|
}
|
||||||
setActiveEpisodeId(episode.id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scroll active episode into view when page changes or active episode changes
|
// Scroll active episode into view when page changes or active episode changes
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import { Autoplay, Navigation, Pagination, EffectFade } from 'swiper/modules';
|
import { Autoplay, Navigation, Pagination, EffectFade } from 'swiper/modules';
|
||||||
|
import { fetchAnimeEpisodes } from '@/lib/api';
|
||||||
|
|
||||||
// Import Swiper styles
|
// Import Swiper styles
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
@@ -14,12 +15,88 @@ import 'swiper/css/effect-fade';
|
|||||||
|
|
||||||
const SpotlightCarousel = ({ items = [] }) => {
|
const SpotlightCarousel = ({ items = [] }) => {
|
||||||
const [isClient, setIsClient] = useState(false);
|
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
|
// Handle hydration mismatch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true);
|
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 no items or not on client yet, show loading state
|
||||||
if (!isClient || !items.length) {
|
if (!isClient || !items.length) {
|
||||||
return (
|
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 (
|
return (
|
||||||
<div className="w-full mb-6 md:mb-10 spotlight-carousel">
|
<div className="w-full mb-6 md:mb-10 spotlight-carousel">
|
||||||
<Swiper
|
<Swiper
|
||||||
@@ -46,6 +130,20 @@ const SpotlightCarousel = ({ items = [] }) => {
|
|||||||
}}
|
}}
|
||||||
loop={true}
|
loop={true}
|
||||||
className="rounded-xl overflow-hidden"
|
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) => (
|
{items.map((anime, index) => (
|
||||||
<SwiperSlide key={`spotlight-${anime.id}-${index}`}>
|
<SwiperSlide key={`spotlight-${anime.id}-${index}`}>
|
||||||
@@ -131,7 +229,7 @@ const SpotlightCarousel = ({ items = [] }) => {
|
|||||||
{/* Buttons - Below title on mobile, right side on desktop */}
|
{/* 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">
|
<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
|
<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"
|
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">
|
<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">
|
||||||
|
|||||||
@@ -463,6 +463,9 @@ export const fetchAnimeEpisodes = async (animeId) => {
|
|||||||
return { episodes: [] };
|
return { episodes: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API already returns episodeId in correct format (animeId?ep=episodeNumber)
|
||||||
|
// So we simply use it directly without any formatting
|
||||||
|
|
||||||
return {
|
return {
|
||||||
episodes: data.data.episodes || [],
|
episodes: data.data.episodes || [],
|
||||||
totalEpisodes: data.data.totalEpisodes || 0
|
totalEpisodes: data.data.totalEpisodes || 0
|
||||||
@@ -480,20 +483,11 @@ export const fetchEpisodeServers = async (episodeId) => {
|
|||||||
return { servers: [] };
|
return { servers: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the episodeId is properly formatted
|
console.log(`[API] Processing episode ID: ${episodeId}`);
|
||||||
// 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
console.log(`[API Call] Fetching servers from: ${apiUrl}`);
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
@@ -547,22 +541,13 @@ export const fetchEpisodeSources = async (episodeId, dub = false, server = 'hd-2
|
|||||||
return { sources: [] };
|
return { sources: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the episodeId is properly formatted
|
console.log(`[API] Processing episode ID for sources: ${episodeId}`);
|
||||||
// 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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 category = dub ? 'dub' : 'sub';
|
||||||
const serverName = server || 'hd-2'; // Default to hd-2 if server is null or empty
|
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}`);
|
console.log(`[API Call] Fetching sources from: ${apiUrl}`);
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
|
|||||||
Reference in New Issue
Block a user