From c4cddc9223eed9b8a02cb89dc73f643402a792e5 Mon Sep 17 00:00:00 2001 From: tejaspanchall Date: Thu, 5 Jun 2025 19:49:53 +0530 Subject: [PATCH] anime info (desktop) --- src/components/AnimeDetails.js | 575 +++++++++++++++------------------ src/components/AnimeInfo.js | 101 ------ src/components/AnimeRow.js | 131 ++++++++ src/components/SeasonCard.js | 56 ++++ src/components/SeasonRow.js | 162 ++++++++++ 5 files changed, 610 insertions(+), 415 deletions(-) delete mode 100644 src/components/AnimeInfo.js create mode 100644 src/components/AnimeRow.js create mode 100644 src/components/SeasonCard.js create mode 100644 src/components/SeasonRow.js diff --git a/src/components/AnimeDetails.js b/src/components/AnimeDetails.js index 4609b15..04141a9 100644 --- a/src/components/AnimeDetails.js +++ b/src/components/AnimeDetails.js @@ -1,12 +1,17 @@ 'use client'; -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import Image from 'next/image'; import Link from 'next/link'; +import AnimeRow from './AnimeRow'; +import SeasonRow from './SeasonRow'; 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 synopsisRef = useRef(null); console.log('AnimeDetails received:', anime); @@ -15,85 +20,28 @@ export default function AnimeDetails({ anime }) { return null; } - const { info, moreInfo, relatedAnime, recommendations, mostPopular, seasons } = anime; + const { info, moreInfo, relatedAnime, recommendations, seasons } = anime; + const hasCharacters = info.characterVoiceActor?.length > 0 || info.charactersVoiceActors?.length > 0; + const hasVideos = info.promotionalVideos && info.promotionalVideos.length > 0; + + // Check if synopsis overflows when component mounts or when content changes + useEffect(() => { + if (synopsisRef.current) { + const element = synopsisRef.current; + setSynopsisOverflows(element.scrollHeight > element.clientHeight); + } + }, [info.description, activeTab]); - // Helper function to render anime cards - const renderAnimeCards = (animeList, title) => { - if (!animeList || animeList.length === 0) return null; - - return ( -
-

{title}

-
- {animeList.map((item, index) => ( - -
-
- {item.name} -
-
-

{item.name}

-
-
- - ))} -
-
- ); - }; - - // Helper function to render seasons - const renderSeasons = () => { - if (!seasons || seasons.length === 0) return null; - - return ( -
-

Seasons

-
- {seasons.map((season, index) => ( - -
-
- {season.name} - {season.isCurrent && ( -
- Current -
- )} -
-
-

{season.title || season.name}

-
-
- - ))} -
-
- ); - }; - // Video modal for promotional videos const VideoModal = ({ video, onClose }) => { if (!video) return null; return ( -
-
+
+
- - {video.title && ( -
-

{video.title}

-
- )}
); }; + // Format status with aired date + const getStatusWithAired = () => { + let status = moreInfo?.status || ''; + if (moreInfo?.aired) { + status += ` (${moreInfo.aired})`; + } + return status; + }; + return (
{/* Video Modal */} {activeVideo && setActiveVideo(null)} />} {/* Background Image with Gradient Overlay */} -
+
{info.poster && ( <> {info.name} -
+
)}
{/* Main Content */} -
-
- {/* Left Column - Poster and Mobile Title */} -
-
+
+ {/* Header Section - Title and Basic Info */} +
+ {/* Poster */} +
+
+ + {/* Watch Button - Mobile & Desktop */} + {info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && ( + + + + + Start Watching + + )} +
- {/* Mobile Title Section */} -
-

{info.name}

- {info.jname && ( -

{info.jname}

+ {/* Title and Metadata */} +
+ {/* Title Section */} +
+

+ {info.name} +

+ + {moreInfo?.japanese && ( +

{moreInfo.japanese}

+ )} + + {/* Synonyms */} + {moreInfo?.synonyms && ( +
+

{moreInfo.synonyms}

+
)}
- - {/* Mobile Quick Info */} -
+ + {/* Status Badges */} +
{info.stats?.rating && ( -
+
- {info.stats.rating} + {info.stats.rating}
)} + + {/* Status with Aired Date */} {moreInfo?.status && ( -
- {moreInfo.status} +
+ {getStatusWithAired()}
)} + {info.stats?.type && ( -
+
{info.stats.type}
)} + {info.stats?.episodes && ( -
+
{info.stats.episodes.sub > 0 && `SUB ${info.stats.episodes.sub}`} {info.stats.episodes.dub > 0 && info.stats.episodes.sub > 0 && ' | '} {info.stats.episodes.dub > 0 && `DUB ${info.stats.episodes.dub}`}
)} -
-
- - {/* Right Column - Details */} -
-
- {/* Desktop Title Section */} -
-

{info.name}

- {info.jname && ( -

{info.jname}

- )} -
- - {/* Desktop Quick Info */} -
- {info.stats?.rating && ( -
- - - - {info.stats.rating} -
- )} - {moreInfo?.status && ( -
- {moreInfo.status} -
- )} - {info.stats?.type && ( -
- {info.stats.type} -
- )} - {info.stats?.episodes && ( -
- {info.stats.episodes.sub > 0 && `SUB ${info.stats.episodes.sub}`} - {info.stats.episodes.dub > 0 && info.stats.episodes.sub > 0 && ' | '} - {info.stats.episodes.dub > 0 && `DUB ${info.stats.episodes.dub}`} -
- )} - {info.stats?.quality && ( -
- {info.stats.quality} -
- )} - {info.stats?.duration && ( -
- {info.stats.duration} -
- )} -
- - {/* Synopsis */} -
-

Synopsis

-
-

- {info.description || 'No description available for this anime.'} -

- {info.description && info.description.length > 100 && ( - - )} -
-
- - {/* Watch Button */} - {info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && ( -
- - - - - Start Watching - + + {info.stats?.quality && ( +
+ {info.stats.quality}
)} - - {/* Promotional Videos */} - {info.promotionalVideos && info.promotionalVideos.length > 0 && ( -
-

Promotional Videos

-
- {info.promotionalVideos.map((video, index) => ( -
setActiveVideo(video)} + + {info.stats?.duration && ( +
+ {info.stats.duration} +
+ )} +
+ + {/* Genres & Studios */} +
+ {/* Genres */} + {moreInfo?.genres && moreInfo.genres.length > 0 && ( +
+

Genres

+
+ {moreInfo.genres.map((genre, index) => ( + -
-
- - - -
-
- {video.title -
+ {genre} + ))}
)} - {/* Additional Info */} -
- {/* Genres */} - {moreInfo?.genres && moreInfo.genres.length > 0 && ( -
-

Genres

-
-
- {moreInfo.genres.map((genre, index) => ( - - {genre} - - ))} -
+ {/* Studios */} + {moreInfo?.studios && ( +
+

Studios

+
+
+ {moreInfo.studios}
- )} - - {/* Studios */} - {moreInfo?.studios && ( -
-

Studios

-
-
- {moreInfo.studios} -
-
-
- )} - - {/* Aired Date */} - {moreInfo?.aired && ( -
-

Aired

-
-
- {moreInfo.aired} -
-
-
- )} - - {/* Character & Voice Actors */} - {(info.characterVoiceActor?.length > 0 || info.charactersVoiceActors?.length > 0) && ( -
-

Characters & Voice Actors

-
- {(info.characterVoiceActor || info.charactersVoiceActors || []).map((item, index) => ( -
-
-
-
- {item.character.name} -
-
-

{item.character.name}

-

{item.character.cast}

-
-
-
-
-
-
- {item.voiceActor.name} -
-
-

{item.voiceActor.name}

-

{item.voiceActor.cast}

-
-
-
-
- ))} -
-
- )} -
+
+ )}
+ {/* Details Tabs - Synopsis, Characters, Videos */} +
+ {/* Tab Navigation */} +
+ + + {hasCharacters && ( + + )} + + {hasVideos && ( + + )} +
+ + {/* Tab Content */} +
+ {/* Synopsis Tab */} + {activeTab === 'synopsis' && ( +
+

+ {info.description || 'No description available for this anime.'} +

+ {synopsisOverflows && ( + + )} +
+ )} + + {/* Characters Tab */} + {activeTab === 'characters' && hasCharacters && ( +
+ {(info.characterVoiceActor || info.charactersVoiceActors || []).map((item, index) => ( +
+
+ {/* Character Image - No padding */} +
+ {item.character.name} +
+ + {/* Text content in the middle */} +
+
+ {/* Character Name */} +
+

{item.character.name}

+

{item.character.cast || 'Main'}

+
+ + {/* Voice Actor Name */} +
+

{item.voiceActor.name}

+

{item.voiceActor.cast || 'Japanese'}

+
+
+
+ + {/* Voice Actor Image - No padding */} +
+ {item.voiceActor.name} +
+
+
+ ))} +
+ )} + + {/* Videos Tab */} + {activeTab === 'videos' && hasVideos && ( +
+ {info.promotionalVideos.map((video, index) => ( +
setActiveVideo(video)} + > +
+
+ + + +
+
+ {video.title +
+ ))} +
+ )} +
+
+ {/* Seasons Section */} - {renderSeasons()} + {seasons && seasons.length > 0 && ( + + )} {/* Related Anime Section */} - {renderAnimeCards(relatedAnime, 'Related Anime')} + {relatedAnime && relatedAnime.length > 0 && ( + + )} {/* Recommendations Section */} - {renderAnimeCards(recommendations, 'You May Also Like')} - - {/* Most Popular Section */} - {renderAnimeCards(mostPopular, 'Most Popular')} + {recommendations && recommendations.length > 0 && ( + + )}
); diff --git a/src/components/AnimeInfo.js b/src/components/AnimeInfo.js deleted file mode 100644 index c1a0ba5..0000000 --- a/src/components/AnimeInfo.js +++ /dev/null @@ -1,101 +0,0 @@ -import Image from 'next/image'; - -export default function AnimeInfo({ anime }) { - return ( -
-
- {/* Banner Image - You'll need to add bannerImage to your anime object */} -
-
- {anime.bannerImage ? ( - - ) : ( -
- )} -
- - {/* Content */} -
-
- {/* Cover Image */} -
-
- {anime.title} -
-
- - {/* Details */} -
- {/* Title and Alternative Titles */} -
-

- {anime.title} -

- {anime.alternativeTitles && ( -
- {anime.alternativeTitles.join(' • ')} -
- )} -
- - {/* Metadata Grid */} -
-
-
Status
-
{anime.status}
-
-
-
Episodes
-
{anime.totalEpisodes}
-
-
-
Season
-
{anime.season} {anime.year}
-
-
-
Studio
-
{anime.studio}
-
-
- - {/* Genres */} - {anime.genres && ( -
-
Genres
-
- {anime.genres.map((genre) => ( - - {genre} - - ))} -
-
- )} - - {/* Synopsis */} -
-
Synopsis
-
- {anime.synopsis} -
-
-
-
-
-
-
- ); -} \ No newline at end of file diff --git a/src/components/AnimeRow.js b/src/components/AnimeRow.js new file mode 100644 index 0000000..e153fb6 --- /dev/null +++ b/src/components/AnimeRow.js @@ -0,0 +1,131 @@ +'use client'; + +import { useRef, useState, useEffect } from 'react'; +import AnimeCard from './AnimeCard'; + +export default function AnimeRow({ title, animeList }) { + const scrollContainerRef = useRef(null); + const contentRef = useRef(null); + const [showLeftButton, setShowLeftButton] = useState(false); + const [showRightButton, setShowRightButton] = useState(false); + + useEffect(() => { + if (!animeList || animeList.length <= 7) { + setShowRightButton(false); + return; + } + + setShowRightButton(true); + + const checkScroll = () => { + if (!scrollContainerRef.current) return; + + const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current; + setShowLeftButton(scrollLeft > 0); + setShowRightButton(scrollLeft + clientWidth < scrollWidth - 10); + }; + + const scrollContainer = scrollContainerRef.current; + scrollContainer.addEventListener('scroll', checkScroll); + + // Initial check + checkScroll(); + + return () => { + if (scrollContainer) { + scrollContainer.removeEventListener('scroll', checkScroll); + } + }; + }, [animeList]); + + const scroll = (direction) => { + if (!scrollContainerRef.current) return; + + const container = scrollContainerRef.current; + // Calculate single card width based on viewport + const isMobile = window.innerWidth < 640; // sm breakpoint in Tailwind + const cardsPerRow = isMobile ? 3 : 7; + const singleCardWidth = container.clientWidth / cardsPerRow; + + if (direction === 'left') { + container.scrollBy({ left: -singleCardWidth, behavior: 'smooth' }); + } else { + container.scrollBy({ left: singleCardWidth, behavior: 'smooth' }); + } + }; + + if (!animeList || animeList.length === 0) return null; + + // Create groups of cards for pagination - 3 for mobile, 7 for larger screens + const cardGroups = []; + const isMobileView = typeof window !== 'undefined' && window.innerWidth < 640; + const groupSize = isMobileView ? 3 : 7; + + for (let i = 0; i < animeList.length; i += groupSize) { + cardGroups.push(animeList.slice(i, i + groupSize)); + } + + return ( +
+
+

{title}

+
+ +
+ {showLeftButton && ( + + )} + +
+
+ {cardGroups.map((group, groupIndex) => ( +
+ {group.map((anime, index) => ( +
+ +
+ ))} + {/* Add empty placeholders if needed to ensure slots are filled */} + {Array.from({ length: (typeof window !== 'undefined' && window.innerWidth < 640) ? + Math.max(0, 3 - group.length) : + Math.max(0, 7 - group.length) }).map((_, index) => ( +
+ ))} +
+ ))} +
+
+ + {showRightButton && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/SeasonCard.js b/src/components/SeasonCard.js new file mode 100644 index 0000000..2123710 --- /dev/null +++ b/src/components/SeasonCard.js @@ -0,0 +1,56 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { useState } from 'react'; + +export default function SeasonCard({ season }) { + const [imageError, setImageError] = useState(false); + + if (!season) return null; + + const handleImageError = () => { + console.log("Image error for:", season.name); + setImageError(true); + }; + + // Get image URL with fallback + const imageSrc = imageError ? '/images/placeholder.png' : season.poster; + + // Generate link + const infoLink = `/anime/${season.id}`; + + return ( + +
+ {/* Background image with blur */} +
+
+ {season.name +
+ + {/* Content overlay */} +
+
+

+ {season.title || season.name} +

+
+
+
+ + ); +} \ No newline at end of file diff --git a/src/components/SeasonRow.js b/src/components/SeasonRow.js new file mode 100644 index 0000000..e54c4b7 --- /dev/null +++ b/src/components/SeasonRow.js @@ -0,0 +1,162 @@ +'use client'; + +import { useRef, useState, useEffect } from 'react'; +import SeasonCard from './SeasonCard'; + +export default function SeasonRow({ title, seasons }) { + const scrollContainerRef = useRef(null); + const contentRef = useRef(null); + const [showLeftButton, setShowLeftButton] = useState(false); + const [showRightButton, setShowRightButton] = useState(false); + + useEffect(() => { + if (!seasons || seasons.length <= 7) { + setShowRightButton(false); + return; + } + + setShowRightButton(true); + + const checkScroll = () => { + if (!scrollContainerRef.current) return; + + const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current; + setShowLeftButton(scrollLeft > 0); + setShowRightButton(scrollLeft + clientWidth < scrollWidth - 10); + }; + + const scrollContainer = scrollContainerRef.current; + scrollContainer.addEventListener('scroll', checkScroll); + + // Initial check + checkScroll(); + + return () => { + if (scrollContainer) { + scrollContainer.removeEventListener('scroll', checkScroll); + } + }; + }, [seasons]); + + // Updated effect to handle mobile view arrows + useEffect(() => { + if (!seasons) return; + + // Check if we're on mobile and have more than 3 seasons + const isMobileView = typeof window !== 'undefined' && window.innerWidth < 640; + const showArrowsOnMobile = isMobileView && seasons.length > 3; + + // On desktop, show arrows if more than 7 seasons + const showArrowsOnDesktop = !isMobileView && seasons.length > 7; + + if (showArrowsOnMobile || showArrowsOnDesktop) { + setShowRightButton(true); + } else { + setShowRightButton(false); + } + + // Listen for resize events to update arrow visibility + const handleResize = () => { + const isMobile = window.innerWidth < 640; + const showArrows = isMobile ? seasons.length > 3 : seasons.length > 7; + setShowRightButton(showArrows); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [seasons]); + + const scroll = (direction) => { + if (!scrollContainerRef.current) return; + + const container = scrollContainerRef.current; + // Calculate single card width based on viewport + const isMobile = window.innerWidth < 640; // sm breakpoint in Tailwind + const cardsPerRow = isMobile ? 3 : 7; + const singleCardWidth = container.clientWidth / cardsPerRow; + + if (direction === 'left') { + container.scrollBy({ left: -singleCardWidth, behavior: 'smooth' }); + } else { + container.scrollBy({ left: singleCardWidth, behavior: 'smooth' }); + } + }; + + if (!seasons || seasons.length === 0) return null; + + // Create groups of cards for pagination - 3 for mobile, 7 for larger screens + const seasonGroups = []; + const isMobileView = typeof window !== 'undefined' && window.innerWidth < 640; + const groupSize = isMobileView ? 3 : 7; + + for (let i = 0; i < seasons.length; i += groupSize) { + seasonGroups.push(seasons.slice(i, i + groupSize)); + } + + return ( +
+
+

{title || 'Seasons'}

+
+ +
+ {showLeftButton && ( + + )} + +
+
+ {seasonGroups.map((group, groupIndex) => ( +
+ {group.map((season, index) => ( +
+ +
+ ))} + {/* Add empty placeholders if needed to ensure slots are filled */} + {Array.from({ length: (typeof window !== 'undefined' && window.innerWidth < 640) ? + Math.max(0, 3 - group.length) : + Math.max(0, 7 - group.length) }).map((_, index) => ( +
+ ))} +
+ ))} +
+
+ + {showRightButton && ( + + )} +
+
+ ); +} \ No newline at end of file