This commit is contained in:
tejaspanchall
2025-05-28 23:11:20 +05:30
commit 00797f0c96
52 changed files with 15166 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { fetchSchedule } from '@/lib/api';
export default function AnimeCalendar() {
const [selectedDay, setSelectedDay] = useState(getCurrentDayIndex());
const [scheduleData, setScheduleData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Add custom scrollbar styles
useEffect(() => {
// Add custom styles for the calendar scrollbar
const style = document.createElement('style');
style.textContent = `
.schedule-scrollbar::-webkit-scrollbar {
width: 4px;
}
.schedule-scrollbar::-webkit-scrollbar-track {
background: var(--card);
}
.schedule-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--border);
border-radius: 4px;
}
`;
document.head.appendChild(style);
// Cleanup function
return () => {
document.head.removeChild(style);
};
}, []);
// Get current day index (0-6, Sunday is 0)
function getCurrentDayIndex() {
const dayIndex = new Date().getDay();
return dayIndex; // Sunday is 0, Monday is 1, etc.
}
// Get current date info for the header
const getCurrentDateInfo = () => {
const today = new Date();
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// Calculate the date for the selected day
const currentDayIndex = today.getDay();
let daysDiff = selectedDay - currentDayIndex;
// Always get the previous occurrence (or today if it's the current day)
if (daysDiff > 0) {
daysDiff -= 7; // Go back to previous week
}
const selectedDate = new Date(today);
selectedDate.setDate(today.getDate() + daysDiff);
return {
day: dayNames[selectedDay],
date: selectedDate.getDate(),
month: monthNames[selectedDate.getMonth()]
};
};
const dateInfo = getCurrentDateInfo();
// Generate week days for the calendar
const days = [
{ label: 'Mon', value: 1 },
{ label: 'Tue', value: 2 },
{ label: 'Wed', value: 3 },
{ label: 'Thu', value: 4 },
{ label: 'Fri', value: 5 },
{ label: 'Sat', value: 6 },
{ label: 'Sun', value: 0 },
];
useEffect(() => {
async function loadScheduleData() {
setIsLoading(true);
try {
// Get the date for the selected day
const today = new Date();
const currentDayIndex = today.getDay();
let daysDiff = selectedDay - currentDayIndex;
if (daysDiff > 0) {
daysDiff -= 7;
}
const selectedDate = new Date(today);
selectedDate.setDate(today.getDate() + daysDiff);
// Format date as YYYY-MM-DD
const formattedDate = selectedDate.toISOString().split('T')[0];
// Fetch schedule data for the selected date
const data = await fetchSchedule(formattedDate);
if (data && data.scheduledAnimes) {
// Process and sort the scheduled animes by time
const processedData = data.scheduledAnimes
.map(anime => ({
id: anime.id,
title: anime.name,
japaneseTitle: anime.jname,
time: anime.time,
airingTimestamp: anime.airingTimestamp,
secondsUntilAiring: anime.secondsUntilAiring
}))
.sort((a, b) => {
// Convert time strings to comparable values (assuming 24-hour format)
const timeA = a.time.split(':').map(Number);
const timeB = b.time.split(':').map(Number);
return (timeA[0] * 60 + timeA[1]) - (timeB[0] * 60 + timeB[1]);
});
setScheduleData(processedData);
} else {
setScheduleData([]);
}
} catch (error) {
console.error('Error loading schedule data:', error);
setScheduleData([]);
} finally {
setIsLoading(false);
}
}
loadScheduleData();
}, [selectedDay]);
return (
<div className="mb-10 bg-[var(--card)] border border-[var(--border)] rounded-lg overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-[var(--border)]">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">Release Calendar</h2>
<div className="text-sm text-[var(--text-muted)]">
{dateInfo.month} {dateInfo.date}
</div>
</div>
{/* Day selector */}
<div className="flex justify-between">
{days.map((day) => (
<button
key={day.value}
onClick={() => setSelectedDay(day.value)}
className={`
flex-1 py-2 text-sm font-medium rounded-md transition-colors
${selectedDay === day.value
? 'bg-white text-[var(--background)]'
: 'text-[var(--text-muted)] hover:text-white'
}
`}
>
{day.label}
</button>
))}
</div>
</div>
{/* Schedule list */}
<div className="min-h-[375px] max-h-[490px] overflow-y-auto schedule-scrollbar">
{isLoading ? (
<div className="flex items-center justify-center h-[375px]">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-white/20 border-t-white"></div>
</div>
) : scheduleData.length > 0 ? (
<div className="pt-3.5 space-y-2">
{scheduleData.map((anime) => (
<Link
href={`/anime/${anime.id}`}
key={anime.id}
className="block px-3.5 py-3 hover:bg-white/5 transition-colors"
>
<div className="flex items-center gap-3">
{/* Time */}
<div className="w-16 text-sm font-medium text-[var(--text-muted)]">
{anime.time}
</div>
{/* Anime info */}
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-white line-clamp-1">
{anime.title}
</h3>
{anime.japaneseTitle && (
<p className="text-xs text-[var(--text-muted)] line-clamp-1">
{anime.japaneseTitle}
</p>
)}
</div>
</div>
</Link>
))}
</div>
) : (
<div className="flex items-center justify-center h-[375px] text-[var(--text-muted)] text-sm">
No releases scheduled
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,96 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useState } from 'react';
export default function AnimeCard({ anime, isRecent }) {
const [imageError, setImageError] = useState(false);
if (!anime) return null;
const handleImageError = () => {
console.log("Image error for:", anime.name);
setImageError(true);
};
// 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}`;
return (
<div className="anime-card w-full flex flex-col">
{/* Image card linking to watch page */}
<Link
href={watchLink}
className="block w-full rounded-lg overflow-hidden transition-transform duration-300 hover:scale-[1.02] group"
prefetch={false}
>
<div className="relative aspect-[2/3] rounded-lg overflow-hidden bg-gray-900 shadow-lg">
{/* Hover overlay */}
<div className="absolute inset-0 bg-black opacity-0 group-hover:opacity-60 transition-opacity duration-300 z-[3]"></div>
{/* Play button triangle - appears on hover */}
<div className="absolute inset-0 flex items-center justify-center z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-16 h-16 text-white drop-shadow-lg">
<path d="M8 5v14l11-7z" />
</svg>
</div>
<Image
src={imageSrc}
alt={anime.name || 'Anime'}
fill
className="object-cover rounded-lg"
onError={handleImageError}
sizes="(max-width: 768px) 50vw, (max-width: 1200px) 33vw, 20vw"
unoptimized={true}
priority={false}
/>
{/* Badges in bottom left */}
<div className="absolute bottom-2 left-2 flex space-x-1 z-10">
{/* Episode badges */}
{anime.episodes && (
<>
{anime.episodes.sub > 0 && (
<div className="bg-black/70 text-white text-[10px] px-1.5 py-0.5 rounded">
SUB {anime.episodes.sub}
</div>
)}
{anime.episodes.dub > 0 && (
<div className="bg-black/70 text-white text-[10px] px-1.5 py-0.5 rounded">
DUB {anime.episodes.dub}
</div>
)}
</>
)}
{/* Type badge */}
{anime.type && (
<div className="bg-black/70 text-white text-[10px] px-1.5 py-0.5 rounded">
{anime.type}
</div>
)}
</div>
</div>
</Link>
{/* Title linking to info page */}
<Link
href={infoLink}
className="block mt-2"
prefetch={false}
>
<h3 className="text-sm font-medium text-white line-clamp-2 hover:text-[var(--primary)] transition-colors">
{anime.name}
</h3>
</Link>
</div>
);
}

View File

@@ -0,0 +1,435 @@
'use client';
import { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
export default function AnimeDetails({ anime }) {
const [isExpanded, setIsExpanded] = useState(false);
const [activeVideo, setActiveVideo] = useState(null);
console.log('AnimeDetails received:', anime);
if (!anime?.info) {
console.error('Invalid anime data structure:', anime);
return null;
}
const { info, moreInfo, relatedAnime, recommendations, mostPopular, seasons } = anime;
// Helper function to render anime cards
const renderAnimeCards = (animeList, title) => {
if (!animeList || animeList.length === 0) return null;
return (
<div className="mt-8">
<h3 className="text-xl font-semibold text-white mb-4 text-center md:text-left">{title}</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{animeList.map((item, index) => (
<Link key={index} href={`/anime/${item.id}`} className="block group">
<div className="bg-[var(--card)] rounded-lg overflow-hidden transition-transform hover:scale-105">
<div className="relative aspect-[3/4]">
<Image
src={item.poster}
alt={item.name}
fill
sizes="(max-width: 768px) 50vw, 20vw"
className="object-cover"
/>
</div>
<div className="p-2">
<p className="text-white text-sm font-medium line-clamp-2">{item.name}</p>
</div>
</div>
</Link>
))}
</div>
</div>
);
};
// Helper function to render seasons
const renderSeasons = () => {
if (!seasons || seasons.length === 0) return null;
return (
<div className="mt-8">
<h3 className="text-xl font-semibold text-white mb-4 text-center md:text-left">Seasons</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{seasons.map((season, index) => (
<Link key={index} href={`/anime/${season.id}`} className="block group">
<div className={`${season.isCurrent ? 'border-2 border-[var(--primary)]' : ''} bg-[var(--card)] rounded-lg overflow-hidden transition-transform hover:scale-105`}>
<div className="relative aspect-[3/4]">
<Image
src={season.poster}
alt={season.name}
fill
sizes="(max-width: 768px) 50vw, 20vw"
className="object-cover"
/>
{season.isCurrent && (
<div className="absolute top-2 right-2 bg-[var(--primary)] text-black text-xs px-2 py-1 rounded-full">
Current
</div>
)}
</div>
<div className="p-2">
<p className="text-white text-sm font-medium line-clamp-2">{season.title || season.name}</p>
</div>
</div>
</Link>
))}
</div>
</div>
);
};
// Video modal for promotional videos
const VideoModal = ({ video, onClose }) => {
if (!video) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-80 z-50 flex items-center justify-center p-4">
<div className="relative max-w-4xl w-full bg-[var(--card)] rounded-lg overflow-hidden">
<button
onClick={onClose}
className="absolute top-3 right-3 z-10 bg-black bg-opacity-50 rounded-full p-1"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="aspect-video w-full">
<iframe
src={video.source}
title={video.title || "Promotional Video"}
allowFullScreen
className="w-full h-full"
></iframe>
</div>
{video.title && (
<div className="p-3">
<p className="text-white font-medium">{video.title}</p>
</div>
)}
</div>
</div>
);
};
return (
<div className="relative">
{/* Video Modal */}
{activeVideo && <VideoModal video={activeVideo} onClose={() => setActiveVideo(null)} />}
{/* Background Image with Gradient Overlay */}
<div className="absolute inset-0 h-[250px] md:h-[400px] overflow-hidden -z-10">
{info.poster && (
<>
<Image
src={info.poster}
alt={info.name}
fill
className="object-cover opacity-10 blur-sm"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-[var(--background)] to-[var(--background)]"></div>
</>
)}
</div>
{/* Main Content */}
<div className="container mx-auto px-4 md:px-4 pt-6 md:pt-8">
<div className="flex flex-col md:flex-row md:gap-8">
{/* Left Column - Poster and Mobile Title */}
<div className="w-full md:w-1/4 lg:w-1/4">
<div className="bg-[var(--card)] rounded-xl md:rounded-lg overflow-hidden shadow-xl max-w-[180px] mx-auto md:max-w-none">
<div className="relative aspect-[3/4] w-full">
<Image
src={info.poster}
alt={info.name}
fill
className="object-cover"
priority
/>
</div>
</div>
{/* Mobile Title Section */}
<div className="md:hidden mt-4 text-center">
<h1 className="text-2xl font-bold text-white mb-2">{info.name}</h1>
{info.jname && (
<h2 className="text-base text-gray-400">{info.jname}</h2>
)}
</div>
{/* Mobile Quick Info */}
<div className="md:hidden mt-4 flex flex-wrap justify-center gap-2">
{info.stats?.rating && (
<div className="flex items-center bg-[var(--card)] px-3 py-1.5 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 text-yellow-400 mr-1"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
<span className="text-white font-medium">{info.stats.rating}</span>
</div>
)}
{moreInfo?.status && (
<div className="bg-[var(--card)] px-3 py-1.5 rounded-full text-sm text-white">
{moreInfo.status}
</div>
)}
{info.stats?.type && (
<div className="bg-[var(--card)] px-3 py-1.5 rounded-full text-sm text-white">
{info.stats.type}
</div>
)}
{info.stats?.episodes && (
<div className="bg-[var(--card)] px-3 py-1.5 rounded-full text-sm text-white">
{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}`}
</div>
)}
</div>
</div>
{/* Right Column - Details */}
<div className="w-full md:w-3/4 lg:w-3/4 mt-6 md:mt-0">
<div className="flex flex-col gap-5 md:gap-6">
{/* Desktop Title Section */}
<div className="hidden md:block">
<h1 className="text-3xl lg:text-4xl font-bold text-white mb-2">{info.name}</h1>
{info.jname && (
<h2 className="text-lg text-gray-400">{info.jname}</h2>
)}
</div>
{/* Desktop Quick Info */}
<div className="hidden md:flex flex-wrap gap-3">
{info.stats?.rating && (
<div className="flex items-center bg-[var(--card)] px-3 py-1 rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 text-yellow-400 mr-1"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
<span className="text-white font-medium">{info.stats.rating}</span>
</div>
)}
{moreInfo?.status && (
<div className="bg-[var(--card)] px-3 py-1 rounded-full text-sm text-white">
{moreInfo.status}
</div>
)}
{info.stats?.type && (
<div className="bg-[var(--card)] px-3 py-1 rounded-full text-sm text-white">
{info.stats.type}
</div>
)}
{info.stats?.episodes && (
<div className="bg-[var(--card)] px-3 py-1 rounded-full text-sm text-white">
{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}`}
</div>
)}
{info.stats?.quality && (
<div className="bg-[var(--card)] px-3 py-1 rounded-full text-sm text-white">
{info.stats.quality}
</div>
)}
{info.stats?.duration && (
<div className="bg-[var(--card)] px-3 py-1 rounded-full text-sm text-white">
{info.stats.duration}
</div>
)}
</div>
{/* Synopsis */}
<div className="bg-[var(--card)] rounded-xl md:rounded-lg p-4 md:p-6">
<h3 className="text-lg md:text-xl font-semibold text-white mb-3">Synopsis</h3>
<div className="relative">
<p className={`text-gray-300 leading-relaxed text-sm md:text-base ${!isExpanded ? 'line-clamp-4' : ''}`}>
{info.description || 'No description available for this anime.'}
</p>
{info.description && info.description.length > 100 && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-[var(--primary)] hover:underline text-sm mt-2 font-medium"
>
{isExpanded ? 'Show Less' : 'Read More'}
</button>
)}
</div>
</div>
{/* Watch Button */}
{info.stats?.episodes && (info.stats.episodes.sub > 0 || info.stats.episodes.dub > 0) && (
<div className="flex items-center gap-4">
<Link
href={`/watch/${info.id}?ep=1`}
className="bg-[#ffffff] text-[var(--background)] px-6 py-3 rounded-xl md:rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center font-medium text-base w-full md:w-auto"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="h-5 w-5 mr-2"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z" />
</svg>
<span>Start Watching</span>
</Link>
</div>
)}
{/* Promotional Videos */}
{info.promotionalVideos && info.promotionalVideos.length > 0 && (
<div className="bg-[var(--card)] rounded-xl md:rounded-lg p-4 md:p-6">
<h3 className="text-lg md:text-xl font-semibold text-white mb-3">Promotional Videos</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{info.promotionalVideos.map((video, index) => (
<div
key={index}
className="relative aspect-video cursor-pointer group overflow-hidden rounded-lg"
onClick={() => setActiveVideo(video)}
>
<div className="absolute inset-0 bg-black bg-opacity-30 group-hover:bg-opacity-20 transition-all duration-200 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-[var(--primary)] flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clipRule="evenodd" />
</svg>
</div>
</div>
<Image
src={video.thumbnail || '/images/video-placeholder.jpg'}
alt={video.title || `Promotional Video ${index + 1}`}
fill
className="object-cover"
/>
</div>
))}
</div>
</div>
)}
{/* Additional Info */}
<div className="space-y-5 md:space-y-6">
{/* Genres */}
{moreInfo?.genres && moreInfo.genres.length > 0 && (
<div>
<h3 className="text-white font-medium mb-3 text-center md:text-left">Genres</h3>
<div className="overflow-x-auto hide-scrollbar">
<div className="flex flex-wrap md:flex-nowrap gap-2 justify-center md:justify-start pb-1">
{moreInfo.genres.map((genre, index) => (
<Link
key={index}
href={`/genre/${genre.toLowerCase()}`}
className="px-3 py-1.5 bg-[var(--card)] text-gray-300 text-sm rounded-full whitespace-nowrap hover:text-white transition-colors"
>
{genre}
</Link>
))}
</div>
</div>
</div>
)}
{/* Studios */}
{moreInfo?.studios && (
<div>
<h3 className="text-white font-medium mb-3 text-center md:text-left">Studios</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
<div className="px-3 py-1.5 bg-[var(--card)] text-gray-300 text-sm rounded-full">
{moreInfo.studios}
</div>
</div>
</div>
)}
{/* Aired Date */}
{moreInfo?.aired && (
<div>
<h3 className="text-white font-medium mb-3 text-center md:text-left">Aired</h3>
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
<div className="px-3 py-1.5 bg-[var(--card)] text-gray-300 text-sm rounded-full">
{moreInfo.aired}
</div>
</div>
</div>
)}
{/* Character & Voice Actors */}
{info.characterVoiceActor && info.characterVoiceActor.length > 0 && (
<div>
<h3 className="text-white font-medium mb-3 text-center md:text-left">Characters & Voice Actors</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-4">
{info.characterVoiceActor.map((item, index) => (
<div key={index} className="bg-[var(--card)] p-3 rounded-lg flex items-center gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<div className="relative w-10 h-10 rounded-full overflow-hidden">
<Image
src={item.character.poster}
alt={item.character.name}
fill
className="object-cover"
/>
</div>
<div className="min-w-0">
<p className="text-sm text-white truncate">{item.character.name}</p>
<p className="text-xs text-gray-400 truncate">{item.character.cast}</p>
</div>
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<div className="relative w-10 h-10 rounded-full overflow-hidden">
<Image
src={item.voiceActor.poster}
alt={item.voiceActor.name}
fill
className="object-cover"
/>
</div>
<div className="min-w-0">
<p className="text-sm text-white truncate">{item.voiceActor.name}</p>
<p className="text-xs text-gray-400 truncate">{item.voiceActor.cast}</p>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
{/* Seasons Section */}
{renderSeasons()}
{/* Related Anime Section */}
{renderAnimeCards(relatedAnime, 'Related Anime')}
{/* Recommendations Section */}
{renderAnimeCards(recommendations, 'You May Also Like')}
{/* Most Popular Section */}
{renderAnimeCards(mostPopular, 'Most Popular')}
</div>
</div>
);
}

View File

@@ -0,0 +1,597 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { fetchGenres } from '@/lib/api';
import { ChevronDownIcon, CheckIcon, XMarkIcon } from '@heroicons/react/24/outline';
// Helper function to capitalize first letter of each word
const capitalizeFirstLetter = (string) => {
if (!string) return '';
return string.split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join(' ');
};
export default function AnimeFilters({
selectedGenre,
onGenreChange,
yearFilter,
onYearChange,
sortOrder,
onSortChange,
showGenreFilter = true,
searchQuery = '',
onSearchChange,
selectedSeasons = [],
onSeasonChange,
selectedTypes = [],
onTypeChange,
selectedStatus = [],
onStatusChange,
selectedLanguages = [],
onLanguageChange
}) {
const [genres, setGenres] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [dropdowns, setDropdowns] = useState({
genre: false,
season: false,
year: false,
type: false,
status: false,
language: false,
sort: false
});
const dropdownRefs = useRef({
genre: null,
season: null,
year: null,
type: null,
status: null,
language: null,
sort: null
});
// Available years for filter (current year down to 2000 and 'older')
const currentYear = new Date().getFullYear();
const years = ['all', ...Array.from({ length: currentYear - 1999 }, (_, i) => (currentYear - i).toString()), 'older'];
// Seasons data
const seasons = ['Winter', 'Spring', 'Summer', 'Fall'];
// Types data
const types = ['TV', 'Movie', 'OVA', 'ONA', 'Special'];
// Status data
const statuses = ['Ongoing', 'Completed', 'Upcoming'];
// Languages data
const languages = ['Subbed', 'Dubbed', 'Chinese', 'English'];
// Fetch genres on component mount
useEffect(() => {
const getGenres = async () => {
if (!showGenreFilter) return;
try {
setIsLoading(true);
const genreData = await fetchGenres();
// Capitalize each genre
const capitalizedGenres = genreData ? genreData.map(capitalizeFirstLetter) : [];
setGenres(capitalizedGenres);
} catch (error) {
console.error('Error fetching genres:', error);
setError('Failed to load genres. Please try again later.');
} finally {
setIsLoading(false);
}
};
getGenres();
}, [showGenreFilter]);
// Toggle dropdown visibility
const toggleDropdown = (dropdown) => {
setDropdowns(prev => {
// Close other dropdowns when opening one
const newState = {
genre: false,
season: false,
year: false,
type: false,
status: false,
language: false,
sort: false,
[dropdown]: !prev[dropdown]
};
return newState;
});
};
// Initialize refs for each dropdown
useEffect(() => {
dropdownRefs.current = {
genre: dropdownRefs.current.genre,
season: dropdownRefs.current.season,
year: dropdownRefs.current.year,
type: dropdownRefs.current.type,
status: dropdownRefs.current.status,
language: dropdownRefs.current.language,
sort: dropdownRefs.current.sort
};
}, []);
// Close all dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
// Check if the click was outside all dropdown containers
let isOutside = true;
Object.keys(dropdownRefs.current).forEach(key => {
if (dropdownRefs.current[key] && dropdownRefs.current[key].contains(event.target)) {
isOutside = false;
}
});
if (isOutside) {
setDropdowns({
genre: false,
season: false,
year: false,
type: false,
status: false,
language: false,
sort: false
});
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Prevent dropdown from closing when selecting an item in multiselect
const keepDropdownOpen = (e, dropdown) => {
e.stopPropagation();
// Don't toggle the dropdown state on item click for multi-select dropdowns
};
const handleClearGenre = (e) => {
e.stopPropagation();
if (onGenreChange) {
onGenreChange(null);
}
};
// Toggle multi-select filter
const handleMultiSelectToggle = (type, value, onChange) => {
if (!onChange) return;
let updatedSelection;
if (type.includes(value)) {
updatedSelection = type.filter(item => item !== value);
} else {
updatedSelection = [...type, value];
}
onChange(updatedSelection);
};
// Modify the onClick handlers for each button to prevent event propagation
const handleGenreSelect = (e, genre) => {
e.stopPropagation();
if (onGenreChange) {
onGenreChange(genre);
// Close genre dropdown after selection since it's a single select
setDropdowns(prev => ({ ...prev, genre: false }));
}
};
const handleYearSelect = (e, year) => {
e.stopPropagation();
if (onYearChange) {
onYearChange(year);
// Close year dropdown after selection since it's a single select
setDropdowns(prev => ({ ...prev, year: false }));
}
};
const handleSortSelect = (e, sort) => {
e.stopPropagation();
if (onSortChange) {
onSortChange(sort);
// Close sort dropdown after selection since it's a single select
setDropdowns(prev => ({ ...prev, sort: false }));
}
};
const handleMultiSelect = (e, type, value, onChange, dropdown) => {
e.stopPropagation();
let updatedSelection;
if (type.includes(value)) {
updatedSelection = type.filter(item => item !== value);
} else {
updatedSelection = [...type, value];
}
if (onChange) {
onChange(updatedSelection);
// Keep dropdown open for multiselect to allow multiple selections
// Without closing the dropdown
}
};
// Add clear filter handlers
const clearAllFilters = (e) => {
e.stopPropagation();
if (onGenreChange) onGenreChange(null);
if (onYearChange) onYearChange('all');
if (onSortChange) onSortChange('default');
if (onSeasonChange) onSeasonChange([]);
if (onTypeChange) onTypeChange([]);
if (onStatusChange) onStatusChange([]);
if (onLanguageChange) onLanguageChange([]);
};
const clearGenre = (e) => {
e.stopPropagation();
if (onGenreChange) onGenreChange(null);
};
const clearYear = (e) => {
e.stopPropagation();
if (onYearChange) onYearChange('all');
};
const clearSort = (e) => {
e.stopPropagation();
if (onSortChange) onSortChange('default');
};
const clearSeasons = (e) => {
e.stopPropagation();
if (onSeasonChange) onSeasonChange([]);
};
const clearTypes = (e) => {
e.stopPropagation();
if (onTypeChange) onTypeChange([]);
};
const clearStatus = (e) => {
e.stopPropagation();
if (onStatusChange) onStatusChange([]);
};
const clearLanguages = (e) => {
e.stopPropagation();
if (onLanguageChange) onLanguageChange([]);
};
// Get display text for filters
const getYearDisplayText = () => {
if (yearFilter === 'all') return 'Year';
if (yearFilter === 'older') return 'Before 2000';
return yearFilter;
};
const getSortDisplayText = () => {
switch (sortOrder) {
case 'title-asc': return 'Title (A-Z)';
case 'title-desc': return 'Title (Z-A)';
case 'year-desc': return 'Newest First';
case 'year-asc': return 'Oldest First';
default: return 'Default';
}
};
// Check if any filter is active
const isAnyFilterActive = () => {
return selectedGenre !== null ||
yearFilter !== 'all' ||
sortOrder !== 'default' ||
selectedSeasons.length > 0 ||
selectedTypes.length > 0 ||
selectedStatus.length > 0 ||
selectedLanguages.length > 0;
};
return (
<div className="p-3">
<div className="flex flex-wrap gap-3">
{/* Genre Filter */}
<div className="relative flex-1 min-w-[160px]" ref={el => dropdownRefs.current.genre = el}>
<button
onClick={() => toggleDropdown('genre')}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg bg-[#141414] hover:bg-[#1a1a1a] active:bg-[#1f1f1f] border border-white/[0.04] group transition-colors"
>
<span className="text-[13px] font-medium text-white/80">
{selectedGenre ? selectedGenre : 'Genre'}
</span>
<div className="flex items-center">
<XMarkIcon
className={`w-3.5 h-3.5 text-white/60 mr-1 hover:text-white ${!selectedGenre ? 'opacity-40' : 'opacity-100'}`}
onClick={clearGenre}
/>
<ChevronDownIcon className={`w-3.5 h-3.5 text-white/60 transition-transform ${dropdowns.genre ? 'rotate-180' : ''}`} />
</div>
</button>
{dropdowns.genre && (
<div className="absolute z-50 w-full mt-2 py-1 bg-[#141414] rounded-lg border border-white/[0.04] shadow-xl">
<div className="max-h-[250px] overflow-y-auto custom-scrollbar">
{genres.map((genre) => (
<button
key={genre}
onClick={(e) => handleGenreSelect(e, genre)}
className={`w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors ${
selectedGenre === genre ? 'text-white font-medium' : 'text-white/70'
}`}
>
{genre}
</button>
))}
</div>
</div>
)}
</div>
{/* Year Filter */}
<div className="relative flex-1 min-w-[160px]" ref={el => dropdownRefs.current.year = el}>
<button
onClick={() => toggleDropdown('year')}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg bg-[#141414] hover:bg-[#1a1a1a] active:bg-[#1f1f1f] border border-white/[0.04] group transition-colors"
>
<span className="text-[13px] font-medium text-white/80">
{getYearDisplayText()}
</span>
<div className="flex items-center">
<XMarkIcon
className={`w-3.5 h-3.5 text-white/60 mr-1 hover:text-white ${yearFilter === 'all' ? 'opacity-40' : 'opacity-100'}`}
onClick={clearYear}
/>
<ChevronDownIcon className={`w-3.5 h-3.5 text-white/60 transition-transform ${dropdowns.year ? 'rotate-180' : ''}`} />
</div>
</button>
{dropdowns.year && (
<div className="absolute z-50 w-full mt-2 py-1 bg-[#141414] rounded-lg border border-white/[0.04] shadow-xl">
<div className="max-h-[250px] overflow-y-auto custom-scrollbar">
{years.map((year) => (
<button
key={year}
onClick={(e) => handleYearSelect(e, year)}
className={`w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors ${
yearFilter === year ? 'text-white font-medium' : 'text-white/70'
}`}
>
{year === 'older' ? 'Before 2000' : year === 'all' ? 'All Years' : year}
</button>
))}
</div>
</div>
)}
</div>
{/* Season Filter */}
<div className="relative flex-1 min-w-[160px]" ref={el => dropdownRefs.current.season = el}>
<button
onClick={() => toggleDropdown('season')}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg bg-[#141414] hover:bg-[#1a1a1a] active:bg-[#1f1f1f] border border-white/[0.04] group transition-colors"
>
<span className="text-[13px] font-medium text-white/80">
{selectedSeasons.length > 0 ? `${selectedSeasons.length} Selected` : 'Season'}
</span>
<div className="flex items-center">
<XMarkIcon
className={`w-3.5 h-3.5 text-white/60 mr-1 hover:text-white ${selectedSeasons.length === 0 ? 'opacity-40' : 'opacity-100'}`}
onClick={clearSeasons}
/>
<ChevronDownIcon className={`w-3.5 h-3.5 text-white/60 transition-transform ${dropdowns.season ? 'rotate-180' : ''}`} />
</div>
</button>
{dropdowns.season && (
<div onClick={(e) => keepDropdownOpen(e, 'season')} className="absolute z-50 w-full mt-2 py-1 bg-[#141414] rounded-lg border border-white/[0.04] shadow-xl">
{seasons.map((season) => (
<button
key={season}
onClick={(e) => handleMultiSelect(e, selectedSeasons, season, onSeasonChange, 'season')}
className="w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors flex items-center justify-between"
>
<span className={`text-[13px] ${selectedSeasons.includes(season) ? 'text-white font-medium' : 'text-white/70'}`}>
{season}
</span>
{selectedSeasons.includes(season) && (
<CheckIcon className="w-4 h-4 text-primary" />
)}
</button>
))}
</div>
)}
</div>
{/* Format Filter */}
<div className="relative flex-1 min-w-[160px]" ref={el => dropdownRefs.current.type = el}>
<button
onClick={() => toggleDropdown('type')}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg bg-[#141414] hover:bg-[#1a1a1a] active:bg-[#1f1f1f] border border-white/[0.04] group transition-colors"
>
<span className="text-[13px] font-medium text-white/80">
{selectedTypes.length > 0 ? `${selectedTypes.length} Selected` : 'Format'}
</span>
<div className="flex items-center">
<XMarkIcon
className={`w-3.5 h-3.5 text-white/60 mr-1 hover:text-white ${selectedTypes.length === 0 ? 'opacity-40' : 'opacity-100'}`}
onClick={clearTypes}
/>
<ChevronDownIcon className={`w-3.5 h-3.5 text-white/60 transition-transform ${dropdowns.type ? 'rotate-180' : ''}`} />
</div>
</button>
{dropdowns.type && (
<div onClick={(e) => keepDropdownOpen(e, 'type')} className="absolute z-50 w-full mt-2 py-1 bg-[#141414] rounded-lg border border-white/[0.04] shadow-xl">
{types.map((type) => (
<button
key={type}
onClick={(e) => handleMultiSelect(e, selectedTypes, type, onTypeChange, 'type')}
className="w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors flex items-center justify-between"
>
<span className={`text-[13px] ${selectedTypes.includes(type) ? 'text-white font-medium' : 'text-white/70'}`}>
{type}
</span>
{selectedTypes.includes(type) && (
<CheckIcon className="w-4 h-4 text-primary" />
)}
</button>
))}
</div>
)}
</div>
{/* Status Filter */}
<div className="relative flex-1 min-w-[160px]" ref={el => dropdownRefs.current.status = el}>
<button
onClick={() => toggleDropdown('status')}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg bg-[#141414] hover:bg-[#1a1a1a] active:bg-[#1f1f1f] border border-white/[0.04] group transition-colors"
>
<span className="text-[13px] font-medium text-white/80">
{selectedStatus.length > 0 ? `${selectedStatus.length} Selected` : 'Status'}
</span>
<div className="flex items-center">
<XMarkIcon
className={`w-3.5 h-3.5 text-white/60 mr-1 hover:text-white ${selectedStatus.length === 0 ? 'opacity-40' : 'opacity-100'}`}
onClick={clearStatus}
/>
<ChevronDownIcon className={`w-3.5 h-3.5 text-white/60 transition-transform ${dropdowns.status ? 'rotate-180' : ''}`} />
</div>
</button>
{dropdowns.status && (
<div onClick={(e) => keepDropdownOpen(e, 'status')} className="absolute z-50 w-full mt-2 py-1 bg-[#141414] rounded-lg border border-white/[0.04] shadow-xl">
{statuses.map((status) => (
<button
key={status}
onClick={(e) => handleMultiSelect(e, selectedStatus, status, onStatusChange, 'status')}
className="w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors flex items-center justify-between"
>
<span className={`text-[13px] ${selectedStatus.includes(status) ? 'text-white font-medium' : 'text-white/70'}`}>
{status}
</span>
{selectedStatus.includes(status) && (
<CheckIcon className="w-4 h-4 text-primary" />
)}
</button>
))}
</div>
)}
</div>
{/* Language Filter */}
<div className="relative flex-1 min-w-[160px]" ref={el => dropdownRefs.current.language = el}>
<button
onClick={() => toggleDropdown('language')}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg bg-[#141414] hover:bg-[#1a1a1a] active:bg-[#1f1f1f] border border-white/[0.04] group transition-colors"
>
<span className="text-[13px] font-medium text-white/80">
{selectedLanguages.length > 0 ? `${selectedLanguages.length} Selected` : 'Language'}
</span>
<div className="flex items-center">
<XMarkIcon
className={`w-3.5 h-3.5 text-white/60 mr-1 hover:text-white ${selectedLanguages.length === 0 ? 'opacity-40' : 'opacity-100'}`}
onClick={clearLanguages}
/>
<ChevronDownIcon className={`w-3.5 h-3.5 text-white/60 transition-transform ${dropdowns.language ? 'rotate-180' : ''}`} />
</div>
</button>
{dropdowns.language && (
<div onClick={(e) => keepDropdownOpen(e, 'language')} className="absolute z-50 w-full mt-2 py-1 bg-[#141414] rounded-lg border border-white/[0.04] shadow-xl">
{languages.map((language) => (
<button
key={language}
onClick={(e) => handleMultiSelect(e, selectedLanguages, language, onLanguageChange, 'language')}
className="w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors flex items-center justify-between"
>
<span className={`text-[13px] ${selectedLanguages.includes(language) ? 'text-white font-medium' : 'text-white/70'}`}>
{language}
</span>
{selectedLanguages.includes(language) && (
<CheckIcon className="w-4 h-4 text-primary" />
)}
</button>
))}
</div>
)}
</div>
{/* Sort Filter */}
<div className="relative flex-1 min-w-[160px]" ref={el => dropdownRefs.current.sort = el}>
<button
onClick={() => toggleDropdown('sort')}
className="flex items-center justify-between w-full px-4 py-2 rounded-lg bg-[#141414] hover:bg-[#1a1a1a] active:bg-[#1f1f1f] border border-white/[0.04] group transition-colors"
>
<span className="text-[13px] font-medium text-white/80">
{getSortDisplayText()}
</span>
<div className="flex items-center">
<XMarkIcon
className={`w-3.5 h-3.5 text-white/60 mr-1 hover:text-white ${sortOrder === 'default' ? 'opacity-40' : 'opacity-100'}`}
onClick={clearSort}
/>
<ChevronDownIcon className={`w-3.5 h-3.5 text-white/60 transition-transform ${dropdowns.sort ? 'rotate-180' : ''}`} />
</div>
</button>
{dropdowns.sort && (
<div className="absolute z-50 w-full mt-2 py-1 bg-[#141414] rounded-lg border border-white/[0.04] shadow-xl">
<button
onClick={(e) => handleSortSelect(e, 'default')}
className={`w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors ${
sortOrder === 'default' ? 'text-white font-medium' : 'text-white/70'
}`}
>
Default
</button>
<button
onClick={(e) => handleSortSelect(e, 'title-asc')}
className={`w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors ${
sortOrder === 'title-asc' ? 'text-white font-medium' : 'text-white/70'
}`}
>
Title (A-Z)
</button>
<button
onClick={(e) => handleSortSelect(e, 'title-desc')}
className={`w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors ${
sortOrder === 'title-desc' ? 'text-white font-medium' : 'text-white/70'
}`}
>
Title (Z-A)
</button>
<button
onClick={(e) => handleSortSelect(e, 'year-desc')}
className={`w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors ${
sortOrder === 'year-desc' ? 'text-white font-medium' : 'text-white/70'
}`}
>
Newest First
</button>
<button
onClick={(e) => handleSortSelect(e, 'year-asc')}
className={`w-full px-4 py-1.5 text-left hover:bg-white/[0.03] transition-colors ${
sortOrder === 'year-asc' ? 'text-white font-medium' : 'text-white/70'
}`}
>
Oldest First
</button>
</div>
)}
</div>
{/* Clear All Button - Always visible */}
<button
onClick={clearAllFilters}
className={`flex items-center justify-center gap-1 px-4 py-2 rounded-lg bg-[#1a1a1a] hover:bg-[#2a2a2a] border border-white/[0.04] transition-colors ${!isAnyFilterActive() ? 'opacity-50' : 'opacity-100'}`}
>
<XMarkIcon className="w-3.5 h-3.5 text-white/80" />
<span className="text-[13px] font-medium text-white/80">Clear All</span>
</button>
</div>
</div>
);
}

101
src/components/AnimeInfo.js Normal file
View File

@@ -0,0 +1,101 @@
import Image from 'next/image';
export default function AnimeInfo({ anime }) {
return (
<div className="bg-[#1a1a1a] rounded-xl shadow-2xl overflow-hidden">
<div className="relative">
{/* Banner Image - You'll need to add bannerImage to your anime object */}
<div className="w-full h-48 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-t from-[#1a1a1a] to-transparent z-10" />
{anime.bannerImage ? (
<Image
src={anime.bannerImage}
alt=""
fill
className="object-cover opacity-50"
/>
) : (
<div className="w-full h-full bg-gradient-to-r from-gray-800 to-gray-900" />
)}
</div>
{/* Content */}
<div className="relative z-20 -mt-24 px-6 pb-6">
<div className="flex flex-col md:flex-row gap-6">
{/* Cover Image */}
<div className="relative w-40 md:w-48 flex-shrink-0">
<div className="aspect-[2/3] relative rounded-lg overflow-hidden shadow-xl border-4 border-[#1a1a1a]">
<Image
src={anime.coverImage}
alt={anime.title}
fill
className="object-cover"
/>
</div>
</div>
{/* Details */}
<div className="flex-grow">
{/* Title and Alternative Titles */}
<div className="mb-4">
<h1 className="text-2xl md:text-3xl font-bold text-white mb-2">
{anime.title}
</h1>
{anime.alternativeTitles && (
<div className="text-gray-400 text-sm">
{anime.alternativeTitles.join(' • ')}
</div>
)}
</div>
{/* Metadata Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-[#242424] rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Status</div>
<div className="text-white font-medium">{anime.status}</div>
</div>
<div className="bg-[#242424] rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Episodes</div>
<div className="text-white font-medium">{anime.totalEpisodes}</div>
</div>
<div className="bg-[#242424] rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Season</div>
<div className="text-white font-medium">{anime.season} {anime.year}</div>
</div>
<div className="bg-[#242424] rounded-lg p-3">
<div className="text-gray-400 text-xs mb-1">Studio</div>
<div className="text-white font-medium">{anime.studio}</div>
</div>
</div>
{/* Genres */}
{anime.genres && (
<div className="mb-6">
<div className="text-gray-400 text-sm mb-2">Genres</div>
<div className="flex flex-wrap gap-2">
{anime.genres.map((genre) => (
<span
key={genre}
className="px-3 py-1 rounded-full bg-[#242424] text-white text-sm hover:bg-[#2a2a2a] transition-colors cursor-pointer"
>
{genre}
</span>
))}
</div>
</div>
)}
{/* Synopsis */}
<div>
<div className="text-gray-400 text-sm mb-2">Synopsis</div>
<div className="text-gray-300 text-sm leading-relaxed">
{anime.synopsis}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,85 @@
'use client';
import React, { useState } from 'react';
import AnimeCard from './AnimeCard';
import Link from 'next/link';
const tabs = [
{ id: 'topAiring', label: 'TOP AIRING' },
{ id: 'popular', label: 'POPULAR' },
{ id: 'latestCompleted', label: 'LATEST COMPLETED' }
];
export default function AnimeTabs({ topAiring = [], popular = [], latestCompleted = [] }) {
const [activeTab, setActiveTab] = useState('topAiring');
const getActiveList = () => {
switch (activeTab) {
case 'topAiring':
return topAiring;
case 'popular':
return popular;
case 'latestCompleted':
return latestCompleted;
default:
return [];
}
};
const getViewAllLink = () => {
switch (activeTab) {
case 'topAiring':
return '/top-airing';
case 'popular':
return '/most-popular';
case 'latestCompleted':
return '/latest-completed';
default:
return '/';
}
};
return (
<div className="mb-10">
{/* Tabs Navigation */}
<div className="flex items-center mb-6 border-b border-[var(--border)] overflow-x-auto scrollbar-none">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-3 sm:px-6 py-3 text-xs sm:text-sm font-medium transition-colors relative whitespace-nowrap flex-shrink-0 ${
activeTab === tab.id
? 'text-white'
: 'text-[var(--text-muted)] hover:text-white'
}`}
>
{tab.label}
{activeTab === tab.id && (
<div className="absolute bottom-0 left-0 w-full h-0.5 bg-[var(--primary)]"></div>
)}
</button>
))}
<Link
href={getViewAllLink()}
className="text-[var(--text-muted)] hover:text-white text-xs sm:text-sm transition-colors flex items-center ml-auto px-3 sm:px-6 py-3 whitespace-nowrap flex-shrink-0"
prefetch={false}
>
<span>View All</span>
<svg className="ml-1 w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path>
</svg>
</Link>
</div>
{/* Anime Grid */}
<div className="grid grid-cols-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{getActiveList().slice(0, 18).map((anime, index) => (
<AnimeCard
key={anime.id + '-' + index}
anime={anime}
/>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,220 @@
import { useState, useMemo, useEffect } from 'react';
export default function EpisodeList({ episodes, currentEpisode, onEpisodeClick, isDub = false }) {
const [currentPage, setCurrentPage] = useState(1);
const [isGridView, setIsGridView] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [activeEpisodeId, setActiveEpisodeId] = useState(null);
const episodesPerPage = 100;
// Update active episode when currentEpisode changes
useEffect(() => {
if (currentEpisode?.episodeId) {
setActiveEpisodeId(currentEpisode.episodeId);
}
}, [currentEpisode]);
// Sync with URL to identify current episode
useEffect(() => {
const checkCurrentEpisode = () => {
const path = window.location.pathname;
const match = path.match(/\/watch\/(.+)$/);
if (match) {
const episodeId = match[1];
setActiveEpisodeId(episodeId);
// Find the episode and update page
const episode = episodes.find(ep => ep.episodeId === episodeId);
if (episode) {
const pageNumber = Math.ceil(episode.number / episodesPerPage);
setCurrentPage(pageNumber);
}
}
};
checkCurrentEpisode();
}, [episodes, window.location.pathname]);
const filteredEpisodes = useMemo(() => {
if (!searchQuery) return episodes;
const query = searchQuery.toLowerCase();
return episodes.filter(episode =>
episode.number.toString().includes(query) ||
(episode.title && episode.title.toLowerCase().includes(query))
);
}, [episodes, searchQuery]);
const totalPages = Math.ceil(filteredEpisodes.length / episodesPerPage);
const indexOfLastEpisode = currentPage * episodesPerPage;
const indexOfFirstEpisode = indexOfLastEpisode - episodesPerPage;
const currentEpisodes = filteredEpisodes.slice(indexOfFirstEpisode, indexOfLastEpisode);
const getPageRange = (pageNum) => {
const start = (pageNum - 1) * episodesPerPage + 1;
const end = Math.min(pageNum * episodesPerPage, filteredEpisodes.length);
return `${start}-${end}`;
};
const isCurrentEpisode = (episode) => {
return episode.episodeId === activeEpisodeId;
};
const handleEpisodeSelect = (episode, e) => {
e.preventDefault();
if (onEpisodeClick) {
onEpisodeClick(episode.episodeId);
}
setActiveEpisodeId(episode.episodeId);
};
// Scroll active episode into view when page changes or active episode changes
useEffect(() => {
if (activeEpisodeId) {
const activeElement = document.querySelector(`[data-episode-id="${activeEpisodeId}"]`);
if (activeElement) {
activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
}, [activeEpisodeId, currentPage]);
return (
<div className="bg-[#1a1a1a] rounded-xl shadow-2xl overflow-hidden h-[calc(90vh-6rem)]">
{/* Header */}
<div className="bg-[#242424] p-3 border-b border-gray-800 sticky top-0 z-40">
<div className="flex items-center gap-3">
<div className="relative flex-grow max-w-lg">
<input
type="text"
placeholder="Search episodes by name or number..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
className="w-full bg-[#2a2a2a] text-white text-sm rounded-lg px-4 py-1.5 pl-9 focus:outline-none focus:ring-2 focus:ring-[var(--primary)] placeholder-gray-500"
/>
<svg
className="absolute left-2.5 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<div className="flex items-center gap-2">
<select
value={currentPage}
onChange={(e) => setCurrentPage(Number(e.target.value))}
className="bg-[#2a2a2a] text-white text-sm rounded-lg px-2 py-1.5 border border-gray-700 focus:outline-none focus:ring-2 focus:ring-[var(--primary)] min-w-[90px]"
>
{[...Array(totalPages)].map((_, index) => (
<option key={index + 1} value={index + 1}>
{getPageRange(index + 1)}
</option>
))}
</select>
<button
onClick={() => setIsGridView(!isGridView)}
className="p-1.5 rounded-lg text-gray-400 hover:text-white transition-colors bg-[#2a2a2a] hover:bg-[#333333]"
title={isGridView ? "Switch to List View" : "Switch to Grid View"}
>
{isGridView ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
)}
</button>
</div>
</div>
</div>
{/* Episodes Container */}
<div className="overflow-y-auto h-[calc(100%-4rem)] scroll-smooth" id="episodes-container">
<div className="p-4">
{isGridView ? (
// Grid View
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
{currentEpisodes.map((episode) => (
<button
key={episode.number}
data-episode-id={episode.episodeId}
onClick={(e) => handleEpisodeSelect(episode, e)}
className={`group relative ${
isCurrentEpisode(episode)
? 'bg-[#2a2a2a] ring-2 ring-white z-30'
: 'bg-[#2a2a2a] hover:bg-[#333333]'
} rounded-lg transition-all duration-300 ease-out transform hover:scale-[1.02] hover:z-10`}
>
<div className="aspect-w-16 aspect-h-9">
<div className={`flex items-center justify-center text-white p-1.5 ${
isCurrentEpisode(episode) ? 'text-base font-bold' : 'text-sm font-medium'
}`}>
<span>{episode.number}</span>
</div>
</div>
{isCurrentEpisode(episode) && (
<div className="absolute -top-1 -right-1 bg-white rounded-full p-0.5">
<svg className="w-3 h-3 text-[#2a2a2a]" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</div>
)}
</button>
))}
</div>
) : (
// List View
<div className="flex flex-col gap-1">
{currentEpisodes.map((episode) => (
<button
key={episode.number}
data-episode-id={episode.episodeId}
onClick={(e) => handleEpisodeSelect(episode, e)}
className={`group flex items-center gap-3 py-2 px-3 rounded-lg transition-all duration-300 w-full text-left ${
isCurrentEpisode(episode)
? 'bg-[#2a2a2a] ring-2 ring-white z-30'
: 'bg-[#2a2a2a] hover:bg-[#333333]'
}`}
>
<div className={`flex-shrink-0 w-8 h-8 rounded-lg ${
isCurrentEpisode(episode)
? 'bg-black/20'
: 'bg-black/20'
} flex items-center justify-center`}>
<span className={`${
isCurrentEpisode(episode)
? 'text-base font-bold'
: 'text-sm font-medium'
} text-white`}>{episode.number}</span>
</div>
<div className="flex-grow min-w-0">
<div className="text-sm text-white font-medium truncate">
{episode.title || `Episode ${episode.number}`}
</div>
</div>
{isCurrentEpisode(episode) && (
<div className="flex-shrink-0">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</div>
)}
</button>
))}
</div>
)}
</div>
</div>
</div>
);
}

230
src/components/GenreBar.js Normal file
View File

@@ -0,0 +1,230 @@
'use client';
import Link from 'next/link';
import { useState, useEffect, useRef } from 'react';
import { fetchGenres } from '@/lib/api';
export default function GenreBar() {
const [genres, setGenres] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [showLeftButton, setShowLeftButton] = useState(true); // Always show left button initially
const [showRightButton, setShowRightButton] = useState(true);
const [isMobile, setIsMobile] = useState(false);
const scrollContainerRef = useRef(null);
const containerRef = useRef(null);
const [visibleGenres, setVisibleGenres] = useState(14);
// Function to capitalize first letter
const capitalizeFirstLetter = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
};
// Predefined genres exactly as specified
const defaultGenres = [
"Action", "Adventure", "Comedy", "Drama", "Ecchi", "Fantasy",
"Horror", "Mahou Shoujo", "Mecha", "Music", "Mystery", "Psychological",
"Romance", "Sci-Fi", "Slice of Life", "Sports", "Supernatural", "Thriller"
];
// Handle long names on mobile
const getMobileGenreName = (genre) => {
// Abbreviate long genre names for mobile view
switch(genre) {
case "Psychological": return "Psycho";
case "Mahou Shoujo": return "Mahou";
case "Supernatural": return "Super";
case "Slice of Life": return "SoL";
default: return genre;
}
};
// Detect mobile devices
useEffect(() => {
const checkIfMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkIfMobile();
window.addEventListener('resize', checkIfMobile);
return () => window.removeEventListener('resize', checkIfMobile);
}, []);
// Calculate the number of genres that fit in the container
useEffect(() => {
const calculateVisibleGenres = () => {
const container = containerRef.current;
if (container) {
const containerWidth = container.offsetWidth;
// Approximate width of each genre button
const genreButtonWidth = isMobile ? 72 : 88; // Slightly larger on mobile to fit text
const visibleCount = Math.floor((containerWidth - 80) / genreButtonWidth);
// Minimum genres visible (smaller minimum on mobile)
setVisibleGenres(Math.max(visibleCount, isMobile ? 4 : 8));
}
};
calculateVisibleGenres();
window.addEventListener('resize', calculateVisibleGenres);
return () => {
window.removeEventListener('resize', calculateVisibleGenres);
};
}, [isMobile]);
useEffect(() => {
// Force scroll position slightly to the right initially
// to ensure there are genres on both sides for scrolling
setTimeout(() => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollLeft = 40; // Start slightly scrolled
// Trigger scroll event to update button states
scrollContainerRef.current.dispatchEvent(new Event('scroll'));
}
}, 100);
setGenres(defaultGenres);
setIsLoading(false);
}, []);
// Check scroll position to determine button visibility
useEffect(() => {
const handleScroll = () => {
if (scrollContainerRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
// Show left button if not at the start
setShowLeftButton(scrollLeft > 5);
// Show right button if not at the end
setShowRightButton(scrollLeft < scrollWidth - clientWidth - 5);
}
};
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
// Initial check
handleScroll();
return () => {
scrollContainer.removeEventListener('scroll', handleScroll);
};
}
}, []);
// Scroll left/right functions
const scrollLeft = () => {
if (scrollContainerRef.current) {
const scrollAmount = isMobile ? -80 : -200;
scrollContainerRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
};
const scrollRight = () => {
if (scrollContainerRef.current) {
const scrollAmount = isMobile ? 80 : 200;
scrollContainerRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' });
}
};
// Mobile-specific styles
const mobileButtonStyle = {
padding: '0.15rem 0.5rem',
fontSize: '0.65rem',
height: '1.5rem',
minWidth: '4rem',
maxWidth: '5.5rem',
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
};
if (isLoading) {
return (
<div className="relative w-full overflow-hidden">
<div className="flex space-x-2 md:space-x-4 py-2 animate-pulse justify-between px-4 md:px-8">
{[...Array(isMobile ? 5 : visibleGenres)].map((_, i) => (
<div key={i} className="h-6 md:h-7 bg-[#1f1f1f] rounded-md flex-1 max-w-[100px] min-w-[60px] md:min-w-[80px]"></div>
))}
</div>
</div>
);
}
return (
<div className="relative w-full" ref={containerRef}>
{/* Left fade effect */}
<div className="absolute left-0 top-0 h-full w-6 md:w-16 z-10 pointer-events-none"
style={{ background: 'linear-gradient(to right, var(--background) 30%, transparent 100%)' }}>
</div>
{/* Left scroll button - only visible when not at the leftmost position */}
{showLeftButton && (
<button
onClick={scrollLeft}
className="absolute left-0 top-1/2 transform -translate-y-1/2 z-20 bg-[var(--background)] bg-opacity-40 backdrop-blur-sm rounded-full p-0.5 md:p-1 shadow-lg transition-opacity"
aria-label="Scroll left"
style={isMobile ? { left: '2px', padding: '2px' } : {}}
>
<svg className="w-3.5 h-3.5 md:w-5 md:h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7"></path>
</svg>
</button>
)}
{/* Scrollable genre container */}
<div className="w-full">
<div
ref={scrollContainerRef}
className="flex py-1.5 md:py-2 overflow-x-auto scrollbar-hide px-5 md:px-8"
style={{
scrollbarWidth: 'none',
msOverflowStyle: 'none',
display: 'grid',
gridAutoFlow: 'column',
gridAutoColumns: `minmax(${Math.floor(100 / (isMobile ? 4.5 : visibleGenres))}%, ${Math.floor(100 / (isMobile ? 3 : visibleGenres - 2))}%)`,
gap: isMobile ? '6px' : '8px',
scrollSnapType: isMobile ? 'x mandatory' : 'none',
WebkitOverflowScrolling: 'touch' // For smoother scrolling on iOS
}}
>
{genres.map((genre) => (
<Link
key={genre}
href={`/search?genre=${genre.toLowerCase()}`}
className="bg-[#1f1f1f] text-white rounded-md hover:bg-white/20 transition-colors text-xs font-medium flex items-center justify-center h-6 md:h-7 px-1 md:px-2 scroll-snap-align-start"
style={isMobile ? mobileButtonStyle : {}}
title={genre} // Add tooltip showing full name
>
{isMobile ? getMobileGenreName(genre) : genre}
</Link>
))}
</div>
</div>
{/* Right fade effect */}
<div className="absolute right-0 top-0 h-full w-6 md:w-16 z-10 pointer-events-none"
style={{ background: 'linear-gradient(to left, var(--background) 30%, transparent 100%)' }}>
</div>
{/* Right scroll button - only visible when not at the rightmost position */}
{showRightButton && (
<button
onClick={scrollRight}
className="absolute right-0 top-1/2 transform -translate-y-1/2 z-20 bg-[var(--background)] bg-opacity-40 backdrop-blur-sm rounded-full p-0.5 md:p-1 shadow-lg transition-opacity"
aria-label="Scroll right"
style={isMobile ? { right: '2px', padding: '2px' } : {}}
>
<svg className="w-3.5 h-3.5 md:w-5 md:h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,93 @@
'use client';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import { fetchGenres } from '@/lib/api';
export default function GenreList() {
const [genres, setGenres] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [showAll, setShowAll] = useState(false);
useEffect(() => {
async function loadGenres() {
try {
const genreData = await fetchGenres();
setGenres(genreData || []);
} catch (error) {
console.error("Error fetching genres:", error);
} finally {
setIsLoading(false);
}
}
loadGenres();
}, []);
// Predefined popular genres if API doesn't return them
const defaultGenres = [
"Action", "Adventure", "Comedy", "Drama", "Fantasy",
"Horror", "Mystery", "Romance", "Sci-Fi", "Slice of Life",
"Supernatural", "Thriller", "Isekai", "Mecha", "Sports"
];
// Use fetched genres or fallback to default genres
const displayGenres = genres.length > 0 ? genres : defaultGenres;
// Show only first 12 genres if not showing all
const visibleGenres = showAll ? displayGenres : displayGenres.slice(0, 12);
if (isLoading) {
return (
<div className="mb-10 bg-[var(--card)] border border-[var(--border)] rounded-lg overflow-hidden">
<div className="p-4 border-b border-[var(--border)]">
<h2 className="text-lg font-semibold text-white">Genres</h2>
</div>
<div className="p-4 grid grid-cols-2 sm:grid-cols-3 gap-3 animate-pulse">
{[...Array(12)].map((_, i) => (
<div key={i} className="h-10 bg-[var(--border)] rounded"></div>
))}
</div>
</div>
);
}
return (
<div className="mb-10 bg-[var(--card)] border border-[var(--border)] rounded-lg overflow-hidden">
<div className="p-4 border-b border-[var(--border)]">
<h2 className="text-lg font-semibold text-white">Genres</h2>
</div>
<div className="p-4 grid grid-cols-2 sm:grid-cols-3 gap-3">
{visibleGenres.map((genre) => (
<Link
key={genre}
href={`/search?genre=${genre.toLowerCase()}`}
className="bg-[var(--background)] hover:bg-white/10 text-white text-sm text-center py-2 px-3 rounded transition-colors truncate"
>
{genre}
</Link>
))}
</div>
{displayGenres.length > 12 && (
<div className="p-4 pt-0 text-center">
<button
onClick={() => setShowAll(!showAll)}
className="text-white/70 hover:text-white text-sm transition-colors inline-flex items-center"
>
<span>{showAll ? 'Show Less' : 'Show All'}</span>
<svg
className={`ml-1 w-4 h-4 transition-transform duration-300 ${showAll ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
</div>
)}
</div>
);
}

703
src/components/Navbar.js Normal file
View File

@@ -0,0 +1,703 @@
'use client';
import Link from 'next/link';
import { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import {
fetchSearchSuggestions,
fetchMostPopular,
fetchTopAiring,
fetchRecentEpisodes,
fetchMostFavorite,
fetchTopUpcoming
} from '@/lib/api';
export default function Navbar() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [searchSuggestions, setSearchSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isRandomLoading, setIsRandomLoading] = useState(false);
const suggestionRef = useRef(null);
const searchInputRef = useRef(null);
const router = useRouter();
// Enhanced fallback suggestions with popular anime
const fallbackSuggestions = [
{
id: "naruto",
title: "Naruto",
image: "https://cdn.myanimelist.net/images/anime/13/17405.jpg",
type: "TV",
year: 2002,
episodes: 220,
rating: 79
},
{
id: "one-piece",
title: "One Piece",
image: "https://cdn.myanimelist.net/images/anime/6/73245.jpg",
type: "TV",
year: 1999,
episodes: 1000,
rating: 85
},
{
id: "attack-on-titan",
title: "Attack on Titan",
image: "https://cdn.myanimelist.net/images/anime/10/47347.jpg",
type: "TV",
year: 2013,
episodes: 75,
rating: 90
},
{
id: "demon-slayer",
title: "Demon Slayer",
image: "https://cdn.myanimelist.net/images/anime/1286/99889.jpg",
type: "TV",
year: 2019,
episodes: 26,
rating: 86
},
{
id: "my-hero-academia",
title: "My Hero Academia",
image: "https://cdn.myanimelist.net/images/anime/10/78745.jpg",
type: "TV",
year: 2016,
episodes: 113,
rating: 82
},
{
id: "jujutsu-kaisen",
title: "Jujutsu Kaisen",
image: "https://cdn.myanimelist.net/images/anime/1171/109222.jpg",
type: "TV",
year: 2020,
episodes: 24,
rating: 88
},
{
id: "tokyo-revengers",
title: "Tokyo Revengers",
image: "https://cdn.myanimelist.net/images/anime/1839/122012.jpg",
type: "TV",
year: 2021,
episodes: 24,
rating: 80
},
{
id: "death-note",
title: "Death Note",
image: "https://cdn.myanimelist.net/images/anime/9/9453.jpg",
type: "TV",
year: 2006,
episodes: 37,
rating: 90
}
];
// Track scroll position
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 10) {
setIsScrolled(true);
} else {
setIsScrolled(false);
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Update suggestions when search query changes
useEffect(() => {
const updateSuggestions = async () => {
// Only search if we have at least 2 characters
if (searchQuery.trim().length >= 2) {
setIsLoading(true);
setShowSuggestions(true); // Always show the suggestions container when typing
try {
console.log(`Fetching suggestions for: ${searchQuery}`);
const apiSuggestions = await fetchSearchSuggestions(searchQuery);
console.log('API returned:', apiSuggestions);
if (Array.isArray(apiSuggestions) && apiSuggestions.length > 0) {
// Take top 5 results
setSearchSuggestions(apiSuggestions.slice(0, 5));
} else {
// Use fallback with pattern matching
getFilteredFallbackSuggestions(searchQuery);
}
} catch (error) {
console.error('Error in search component:', error);
// Use fallback with pattern matching
getFilteredFallbackSuggestions(searchQuery);
} finally {
setIsLoading(false);
}
} else {
setSearchSuggestions([]);
setShowSuggestions(false);
}
};
// Helper function to get filtered fallback suggestions
const getFilteredFallbackSuggestions = (query) => {
const filtered = fallbackSuggestions.filter(anime =>
anime.title.toLowerCase().includes(query.toLowerCase())
);
if (filtered.length > 0) {
setSearchSuggestions(filtered.slice(0, 5));
} else {
// Create a generic suggestion based on the search query
setSearchSuggestions([{
id: query.toLowerCase().replace(/\s+/g, '-'),
title: `Search for "${query}"`,
type: "SEARCH"
}]);
}
};
const debounceTimer = setTimeout(() => {
updateSuggestions();
}, 300); // 300ms debounce time
return () => clearTimeout(debounceTimer);
}, [searchQuery]);
// Close suggestions when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (
suggestionRef.current &&
!suggestionRef.current.contains(event.target) &&
!searchInputRef.current?.contains(event.target)
) {
setShowSuggestions(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleSearch = (e) => {
e.preventDefault();
// Navigate to search page regardless if search is empty or not
router.push(searchQuery.trim() ? `/search?q=${encodeURIComponent(searchQuery)}` : '/search');
setSearchQuery('');
setShowSuggestions(false);
setIsMenuOpen(false);
};
// Handle suggestion item click
const handleAnimeClick = (id) => {
router.push(`/anime/${id}`);
setSearchQuery('');
setShowSuggestions(false);
setIsMenuOpen(false);
};
// Handle search by query click
const handleSearchByQueryClick = () => {
router.push(`/search?q=${encodeURIComponent(searchQuery)}`);
setSearchQuery('');
setShowSuggestions(false);
setIsMenuOpen(false);
};
// Helper function to render clear button
const renderClearButton = () => {
if (searchQuery) {
return (
<button
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white"
onClick={() => setSearchQuery('')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
);
}
return null;
};
// Function to handle input focus
const handleInputFocus = () => {
if (searchQuery.trim().length >= 2) {
setShowSuggestions(true);
}
};
// Function to handle random anime click
const handleRandomAnimeClick = async () => {
setIsRandomLoading(true);
try {
// Randomly select a category to fetch from
const categories = [
{ name: 'Most Popular', fetch: fetchMostPopular },
{ name: 'Top Airing', fetch: fetchTopAiring },
{ name: 'Recent Episodes', fetch: fetchRecentEpisodes },
{ name: 'Most Favorite', fetch: fetchMostFavorite },
{ name: 'Top Upcoming', fetch: fetchTopUpcoming }
];
// Select a random category
const randomCategoryIndex = Math.floor(Math.random() * categories.length);
const selectedCategory = categories[randomCategoryIndex];
console.log(`Fetching random anime from: ${selectedCategory.name}`);
// Fetch anime from the selected category - use a random page number to get more variety
const randomPage = Math.floor(Math.random() * 5) + 1; // Random page between 1-5
const animeList = await selectedCategory.fetch(randomPage);
if (animeList && animeList.results && animeList.results.length > 0) {
// Skip the first few results as they tend to be more popular
const skipCount = Math.min(5, Math.floor(animeList.results.length / 3));
let availableAnime = animeList.results.slice(skipCount);
if (availableAnime.length === 0) {
// If we've filtered out everything, use the original list
availableAnime = animeList.results;
}
// Get a random index
const randomAnimeIndex = Math.floor(Math.random() * availableAnime.length);
// Get the random anime ID
const randomAnimeId = availableAnime[randomAnimeIndex].id;
console.log(`Selected random anime: ${availableAnime[randomAnimeIndex].title} (ID: ${randomAnimeId})`);
// Navigate to the anime page
router.push(`/anime/${randomAnimeId}`);
} else {
console.error('No anime found to select randomly from');
// Fallback to most popular if the chosen category fails, but use a higher page number
const fallbackPage = Math.floor(Math.random() * 5) + 2; // Pages 2-6 for more obscure options
const fallbackList = await fetchMostPopular(fallbackPage);
if (fallbackList && fallbackList.results && fallbackList.results.length > 0) {
const randomIndex = Math.floor(Math.random() * fallbackList.results.length);
const randomAnimeId = fallbackList.results[randomIndex].id;
router.push(`/anime/${randomAnimeId}`);
}
}
} catch (error) {
console.error('Error fetching random anime:', error);
} finally {
setIsRandomLoading(false);
}
};
return (
<nav className={`fixed w-full z-20 transition-all duration-300 ${
isScrolled
? 'backdrop-blur-xl shadow-md bg-[#0a0a0a]/80'
: 'bg-transparent'
}`}>
<div className="flex items-center justify-between h-16 px-2 sm:px-4 md:px-[4rem]">
{/* Logo */}
<div className="flex-shrink-0">
<Link href="/home" className="flex items-center" prefetch={false}>
<img src="/Logo.png" alt="JustAnime Logo" className="h-[38px]" />
</Link>
</div>
{/* Search Bar - Desktop */}
<div className="hidden sm:flex flex-1 max-w-lg mx-auto">
<form onSubmit={handleSearch} className="flex items-center w-full">
<div className="relative w-full">
<input
ref={searchInputRef}
type="text"
placeholder="Search Anime"
className="bg-[#1a1a1a] text-white pl-10 pr-8 py-2 rounded-md focus:outline-none w-full"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={handleInputFocus}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
{renderClearButton()}
{/* Search Suggestions Dropdown */}
{showSuggestions && (
<div
ref={suggestionRef}
className="absolute mt-2 w-full bg-[#121212] rounded-md shadow-lg z-30 border border-gray-700 overflow-hidden"
>
{isLoading ? (
<div className="px-4 py-3 text-sm text-gray-400 flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</div>
) : searchSuggestions.length > 0 ? (
<>
<ul className="list-none m-0 p-0 w-full">
{searchSuggestions.map((suggestion, index) => (
<li key={index}>
<a
href="#"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (suggestion.type === 'SEARCH') {
handleSearchByQueryClick();
} else {
handleAnimeClick(suggestion.id);
}
}}
className="block p-2.5 text-sm text-white hover:bg-gray-700 cursor-pointer border-b border-gray-700 last:border-0 transition-colors duration-150 bg-[#121212] active:bg-gray-600"
>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-10 h-14 bg-gray-800 overflow-hidden rounded">
{suggestion.image ? (
<img
src={suggestion.image}
alt={suggestion.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-700 flex items-center justify-center">
<span className="text-xs text-gray-500">No img</span>
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{suggestion.title}</div>
<div className="flex flex-wrap gap-2 mt-1 text-xs">
{suggestion.year && (
<span className="text-gray-400">{suggestion.year}</span>
)}
{suggestion.type && suggestion.type !== 'SEARCH' && (
<span className="bg-gray-800 px-2 py-0.5 rounded text-gray-300">{suggestion.type}</span>
)}
{suggestion.type === 'SEARCH' && (
<span className="bg-blue-900 px-2 py-0.5 rounded text-blue-200">Search</span>
)}
{suggestion.episodes && (
<span className="flex items-center text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 4v16M17 4v16M3 8h18M3 16h18" />
</svg>
{suggestion.episodes}
</span>
)}
{suggestion.rating && (
<span className="flex items-center text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 mr-1 text-yellow-500" fill="currentColor" viewBox="0 0 24 24" stroke="none">
<path d="M12 2l2.4 7.4H22l-6 4.6 2.3 7-6.3-4.6L5.7 21l2.3-7-6-4.6h7.6z" />
</svg>
{suggestion.rating}
</span>
)}
</div>
</div>
</div>
</a>
</li>
))}
</ul>
<a
href="#"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleSearchByQueryClick();
}}
className="block p-2 border-t border-gray-700 bg-[#121212] hover:bg-gray-700 cursor-pointer transition-colors duration-150 active:bg-gray-600 text-center"
>
<div className="text-sm text-gray-300 hover:text-white py-2 flex items-center justify-center">
<span>VIEW ALL</span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</a>
</>
) : (
<div className="px-4 py-3 text-sm text-gray-400">
No results found
</div>
)}
</div>
)}
</div>
{/* Search Button */}
<button
type="submit"
className="bg-[#1a1a1a] text-white p-2 rounded-md transition-colors duration-200 focus:outline-none ml-2 h-[38px] w-[38px] flex items-center justify-center cursor-pointer"
aria-label="Search"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
{/* Random Anime Button */}
<div className="ml-2">
<button
type="button"
onClick={handleRandomAnimeClick}
disabled={isRandomLoading}
className="bg-[#1a1a1a] text-white p-2 rounded-md transition-colors duration-200 focus:outline-none h-[38px] w-[38px] flex items-center justify-center cursor-pointer"
aria-label="Random Anime"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" className="bi bi-shuffle" viewBox="0 0 16 16">
<path fillRule="evenodd" d="M0 3.5A.5.5 0 0 1 .5 3H1c2.202 0 3.827 1.24 4.874 2.418.49.552.865 1.102 1.126 1.532.26-.43.636-.98 1.126-1.532C9.173 4.24 10.798 3 13 3v1c-1.798 0-3.173 1.01-4.126 2.082A9.624 9.624 0 0 0 7.556 8a9.624 9.624 0 0 0 1.317 1.918C9.828 10.99 11.204 12 13 12v1c-2.202 0-3.827-1.24-4.874-2.418A10.595 10.595 0 0 1 7 9.05c-.26.43-.636.98-1.126 1.532C4.827 11.76 3.202 13 1 13H.5a.5.5 0 0 1 0-1H1c1.798 0 3.173-1.01 4.126-2.082A9.624 9.624 0 0 0 6.444 8a9.624 9.624 0 0 0-1.317-1.918C4.172 5.01 2.796 4 1 4H.5a.5.5 0 0 1-.5-.5z"/>
<path d="M13 5.466V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192zm0 9v-3.932a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192z"/>
</svg>
</button>
</div>
</form>
</div>
{/* Login Button - Desktop */}
<div className="hidden sm:block flex-shrink-0">
<Link
href="#"
className="bg-white text-black px-4 py-2 rounded-md hover:bg-gray-200"
prefetch={false}
>
Login
</Link>
</div>
{/* Mobile Menu Button */}
<div className="sm:hidden flex items-center ml-2">
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-[#1a1a1a] focus:outline-none"
>
<svg
className={`${isMenuOpen ? 'hidden' : 'block'} h-6 w-6`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg
className={`${isMenuOpen ? 'block' : 'hidden'} h-6 w-6`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Mobile menu */}
{isMenuOpen && (
<div className="sm:hidden absolute top-16 inset-x-0 bg-[var(--card)] shadow-lg border-t border-[var(--border)] z-10">
<div className="px-4 pt-4 pb-6 space-y-4">
<div className="mb-4">
<form onSubmit={handleSearch} className="flex items-center">
<div className="relative flex-1">
<input
type="text"
placeholder="Search Anime"
className="bg-[#1a1a1a] text-white pl-10 pr-8 py-2 rounded-md focus:outline-none w-full text-sm"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={handleInputFocus}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
{/* Mobile Clear Button */}
{searchQuery && (
<button
type="button"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white"
onClick={() => setSearchQuery('')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
{/* Mobile Search Suggestions */}
{showSuggestions && (
<div
className="absolute mt-2 w-full bg-[#121212] rounded-md shadow-lg z-30 border border-gray-700 overflow-hidden"
>
{isLoading ? (
<div className="px-4 py-3 text-sm text-gray-400 flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</div>
) : searchSuggestions.length > 0 ? (
<>
<ul className="list-none m-0 p-0 w-full">
{searchSuggestions.map((suggestion, index) => (
<li key={index}>
<a
href="#"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (suggestion.type === 'SEARCH') {
handleSearchByQueryClick();
} else {
handleAnimeClick(suggestion.id);
}
}}
className="block p-2.5 text-sm text-white hover:bg-gray-700 cursor-pointer border-b border-gray-700 last:border-0 transition-colors duration-150 bg-[#121212] active:bg-gray-600"
>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-10 h-14 bg-gray-800 overflow-hidden rounded">
{suggestion.image ? (
<img
src={suggestion.image}
alt={suggestion.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-700 flex items-center justify-center">
<span className="text-xs text-gray-500">No img</span>
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{suggestion.title}</div>
<div className="flex flex-wrap gap-2 mt-1 text-xs">
{suggestion.year && (
<span className="text-gray-400">{suggestion.year}</span>
)}
{suggestion.type && suggestion.type !== 'SEARCH' && (
<span className="bg-gray-800 px-2 py-0.5 rounded text-gray-300">{suggestion.type}</span>
)}
{suggestion.type === 'SEARCH' && (
<span className="bg-blue-900 px-2 py-0.5 rounded text-blue-200">Search</span>
)}
{suggestion.episodes && (
<span className="flex items-center text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 4v16M17 4v16M3 8h18M3 16h18" />
</svg>
{suggestion.episodes}
</span>
)}
{suggestion.rating && (
<span className="flex items-center text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 mr-1 text-yellow-500" fill="currentColor" viewBox="0 0 24 24" stroke="none">
<path d="M12 2l2.4 7.4H22l-6 4.6 2.3 7-6.3-4.6L5.7 21l2.3-7-6-4.6h7.6z" />
</svg>
{suggestion.rating}
</span>
)}
</div>
</div>
</div>
</a>
</li>
))}
</ul>
<a
href="#"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleSearchByQueryClick();
setIsMenuOpen(false);
}}
className="block p-2 border-t border-gray-700 bg-[#121212] hover:bg-gray-700 cursor-pointer transition-colors duration-150 active:bg-gray-600 text-center"
>
<div className="text-sm text-gray-300 hover:text-white py-2 flex items-center justify-center">
<span>VIEW ALL</span>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</a>
</>
) : (
<div className="px-4 py-3 text-sm text-gray-400">
No results found
</div>
)}
</div>
)}
</div>
{/* Search Button - Mobile */}
<button
type="submit"
className="bg-[#1a1a1a] text-white p-2 rounded-md transition-colors duration-200 focus:outline-none ml-2 h-[34px] w-[34px] flex items-center justify-center cursor-pointer"
aria-label="Search"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
{/* Random Anime Button - Mobile */}
<button
type="button"
onClick={handleRandomAnimeClick}
disabled={isRandomLoading}
className="bg-[#1a1a1a] text-white p-2 rounded-md transition-colors duration-200 focus:outline-none ml-2 h-[34px] w-[34px] flex items-center justify-center cursor-pointer"
aria-label="Random Anime"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" className="bi bi-shuffle" viewBox="0 0 16 16">
<path fillRule="evenodd" d="M0 3.5A.5.5 0 0 1 .5 3H1c2.202 0 3.827 1.24 4.874 2.418.49.552.865 1.102 1.126 1.532.26-.43.636-.98 1.126-1.532C9.173 4.24 10.798 3 13 3v1c-1.798 0-3.173 1.01-4.126 2.082A9.624 9.624 0 0 0 7.556 8a9.624 9.624 0 0 0 1.317 1.918C9.828 10.99 11.204 12 13 12v1c-2.202 0-3.827-1.24-4.874-2.418A10.595 10.595 0 0 1 7 9.05c-.26.43-.636.98-1.126 1.532C4.827 11.76 3.202 13 1 13H.5a.5.5 0 0 1 0-1H1c1.798 0 3.173-1.01 4.126-2.082A9.624 9.624 0 0 0 6.444 8a9.624 9.624 0 0 0-1.317-1.918C4.172 5.01 2.796 4 1 4H.5a.5.5 0 0 1-.5-.5z"/>
<path d="M13 5.466V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192zm0 9v-3.932a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192z"/>
</svg>
</button>
</form>
</div>
<div className="pt-4 border-t border-[var(--border)]">
<Link
href="#"
className="block px-3 py-2 text-base font-medium text-white bg-[var(--primary)] hover:bg-opacity-90 rounded-md"
onClick={() => setIsMenuOpen(false)}
prefetch={false}
>
Login
</Link>
</div>
</div>
</div>
)}
</nav>
);
}

View File

@@ -0,0 +1,85 @@
'use client';
import Navbar from './Navbar';
const Footer = () => {
return (
<footer className="bg-[#0a0a0a] text-gray-400 py-6 border-t border-gray-800">
<div className="px-2 sm:px-4 md:px-[4rem] mx-auto">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex flex-col items-center md:items-start md:flex-row gap-6 w-full md:w-auto">
<div className="flex w-full justify-center items-center md:justify-start md:w-auto">
<div className="flex-1 flex justify-end pr-2">
<img src="/Logo.png" alt="JustAnime Logo" className="h-8" />
</div>
<div className="h-8 w-px bg-gray-700 md:hidden"></div>
<div className="flex-1 flex items-center pl-2 md:hidden">
<div className="flex items-center space-x-3">
<a href="#" className="text-white hover:text-gray-300 transition-colors">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>
</a>
<a href="#" className="text-white hover:text-gray-300 transition-colors">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
</div>
</div>
</div>
<div className="flex items-center space-x-4 md:hidden">
<a href="/terms" className="hover:text-white transition-colors">Terms & Privacy</a>
<a href="/dmca" className="hover:text-white transition-colors">DMCA</a>
<a href="/contacts" className="hover:text-white transition-colors">Contacts</a>
</div>
<p className="text-xs max-w-md text-center md:text-left">This website does not retain any files on its server. Rather, it solely provides links to media content hosted by third-party services.</p>
<div className="flex items-center space-x-4 md:hidden mt-4 hidden">
<a href="#" className="text-white hover:text-gray-300 transition-colors">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>
</a>
<a href="#" className="text-white hover:text-gray-300 transition-colors">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
</div>
</div>
<div className="hidden md:flex items-center gap-6">
<div className="flex items-center space-x-4">
<a href="/terms" className="hover:text-white transition-colors">Terms & Privacy</a>
<a href="/dmca" className="hover:text-white transition-colors">DMCA</a>
<a href="/contacts" className="hover:text-white transition-colors">Contacts</a>
</div>
<div className="flex items-center space-x-4">
<a href="#" className="text-white hover:text-gray-300 transition-colors">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>
</a>
<a href="#" className="text-white hover:text-gray-300 transition-colors">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
</div>
</div>
</div>
</div>
</footer>
);
};
export default function SharedLayout({ children }) {
return (
<>
<Navbar />
<main className="pt-16 flex-grow">
{children}
</main>
<Footer />
</>
);
}

View File

@@ -0,0 +1,226 @@
'use client';
import React, { useEffect, useState } 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 Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/effect-fade';
const SpotlightCarousel = ({ items = [] }) => {
const [isClient, setIsClient] = useState(false);
// Handle hydration mismatch
useEffect(() => {
setIsClient(true);
}, []);
// If no items or not on client yet, show loading state
if (!isClient || !items.length) {
return (
<div className="w-full h-[250px] md:h-[450px] bg-[var(--card)] rounded-xl animate-pulse flex items-center justify-center mb-6 md:mb-10">
<div className="text-center">
<div className="h-10 w-40 bg-[var(--border)] rounded mx-auto mb-4"></div>
<div className="h-4 w-60 bg-[var(--border)] rounded mx-auto"></div>
</div>
</div>
);
}
return (
<div className="w-full mb-6 md:mb-10 spotlight-carousel">
<Swiper
modules={[Autoplay, Navigation, Pagination, EffectFade]}
slidesPerView={1}
effect="fade"
navigation
pagination={{ clickable: true }}
autoplay={{
delay: 5000,
disableOnInteraction: false,
}}
loop={true}
className="rounded-xl overflow-hidden"
>
{items.map((anime, index) => (
<SwiperSlide key={`spotlight-${anime.id}-${index}`}>
<div className="relative w-full h-[250px] md:h-[450px]">
{/* Background Image */}
<Image
src={anime.banner || anime.poster || '/LandingPage.jpg'}
alt={anime.name || 'Anime spotlight'}
fill
priority={index < 2}
className="object-cover"
/>
{/* Gradient Overlay */}
<div
className="absolute inset-0"
style={{
background: `
linear-gradient(to right,
rgba(10,10,10,0.9) 0%,
rgba(10,10,10,0.6) 25%,
rgba(10,10,10,0.3) 40%,
rgba(10,10,10,0) 60%),
linear-gradient(to top,
rgba(10,10,10,0.95) 0%,
rgba(10,10,10,0.7) 15%,
rgba(10,10,10,0.3) 30%,
rgba(10,10,10,0) 50%)
`
}}
></div>
{/* Content Area */}
<div className="absolute inset-0 flex flex-col justify-end p-3 pb-12 md:p-8">
<div className="flex flex-col md:flex-row md:items-end md:justify-between">
{/* Left Side Content */}
<div className="max-w-2xl">
{/* Metadata first - Minimal boxed design */}
<div className="flex items-center mb-2 md:mb-3 text-xs md:text-xs space-x-1.5 md:space-x-1.5">
{anime.otherInfo?.map((info, i) => (
<span
key={i}
className="inline-block px-2 md:px-1.5 py-1 md:py-0.5 bg-white/10 text-white/80 rounded-sm"
>
{info}
</span>
))}
{anime.episodes && (
<>
{anime.episodes.sub > 0 && (
<span className="inline-block px-2 md:px-1.5 py-1 md:py-0.5 bg-white/10 text-white/80 rounded-sm">
SUB {anime.episodes.sub}
</span>
)}
{anime.episodes.dub > 0 && (
<span className="inline-block px-2 md:px-1.5 py-1 md:py-0.5 bg-white/10 text-white/80 rounded-sm">
DUB {anime.episodes.dub}
</span>
)}
</>
)}
</div>
{/* Title second */}
<h2 className="text-lg md:text-4xl font-bold mb-2 md:mb-2 line-clamp-2 md:line-clamp-none">
{anime.name || 'Anime Title'}
</h2>
{/* Japanese Title */}
{anime.jname && (
<h3 className="text-sm md:text-lg text-white/70 mb-2 line-clamp-1">
{anime.jname}
</h3>
)}
{/* Description third - hidden on mobile, shown on desktop with exactly 3 lines */}
<p className="hidden md:block text-base line-clamp-3 text-white/90 max-h-[4.5rem] overflow-hidden">
{anime.description || 'No description available.'}
</p>
</div>
{/* 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={`/anime/${anime.id}`}
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">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clipRule="evenodd" />
</svg>
<span>WATCH NOW</span>
</Link>
<Link
href={`/anime/${anime.id}`}
className="text-white border border-white/30 hover:bg-white/10 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" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>DETAILS</span>
</Link>
</div>
</div>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
<style jsx global>{`
.spotlight-carousel .swiper-button-next,
.spotlight-carousel .swiper-button-prev {
color: white;
background: rgba(0, 0, 0, 0.3);
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 768px) {
.spotlight-carousel .swiper-button-next,
.spotlight-carousel .swiper-button-prev {
width: 40px;
height: 40px;
}
}
.spotlight-carousel .swiper-button-next:after,
.spotlight-carousel .swiper-button-prev:after {
font-size: 12px;
}
@media (min-width: 768px) {
.spotlight-carousel .swiper-button-next:after,
.spotlight-carousel .swiper-button-prev:after {
font-size: 18px;
}
}
.spotlight-carousel .swiper-pagination {
bottom: 12px !important;
}
.spotlight-carousel .swiper-pagination-bullet {
background: white;
opacity: 0.5;
width: 4px;
height: 4px;
margin: 0 3px !important;
}
@media (min-width: 768px) {
.spotlight-carousel .swiper-pagination {
bottom: 20px !important;
}
.spotlight-carousel .swiper-pagination-bullet {
width: 6px;
height: 6px;
margin: 0 4px !important;
}
}
.spotlight-carousel .swiper-pagination-bullet-active {
background: white;
opacity: 1;
}
`}</style>
</div>
);
};
export default SpotlightCarousel;

159
src/components/TopLists.js Normal file
View File

@@ -0,0 +1,159 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import Image from 'next/image';
export default function TopLists({ topToday = [], topWeek = [], topMonth = [] }) {
const [activeTab, setActiveTab] = useState('today');
const tabs = [
{ id: 'today', label: 'Today', data: topToday },
{ id: 'week', label: 'Week', data: topWeek },
{ id: 'month', label: 'Month', data: topMonth },
];
// Add custom scrollbar styles
useEffect(() => {
// Add custom styles for the toplists scrollbar
const style = document.createElement('style');
style.textContent = `
.toplists-scrollbar::-webkit-scrollbar {
width: 4px;
}
.toplists-scrollbar::-webkit-scrollbar-track {
background: var(--card);
}
.toplists-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--border);
border-radius: 4px;
}
`;
document.head.appendChild(style);
// Cleanup function
return () => {
document.head.removeChild(style);
};
}, []);
// Find the active tab data
const activeTabData = tabs.find(tab => tab.id === activeTab)?.data || [];
return (
<div className="mb-10 bg-[var(--card)] border border-[var(--border)] rounded-lg overflow-hidden">
<div className="p-4 border-b border-[var(--border)] flex justify-between items-center">
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[var(--text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.783-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
<h2 className="text-lg font-semibold text-white">Top 10 Anime</h2>
</div>
</div>
{/* Tabs */}
<div className="grid grid-cols-3 border-b border-[var(--border)]">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-3 transition-colors text-sm font-medium ${
activeTab === tab.id
? 'text-white bg-[var(--background)] border-b-2 border-[var(--border)]'
: 'text-[var(--text-muted)] hover:bg-[var(--background)]'
}`}
>
{tab.label}
</button>
))}
</div>
{/* List content */}
<div className="p-4">
{activeTabData.length === 0 ? (
<div className="py-8 text-center text-[var(--text-muted)]">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mx-auto mb-3 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.783-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
<p className="text-sm">No data available</p>
<p className="text-xs mt-1 opacity-70">Check another tab or come back later</p>
</div>
) : (
<div className="space-y-2 min-h-[375px] max-h-[490px] overflow-y-auto pr-1 toplists-scrollbar">
{activeTabData.slice(0, 10).map((anime, index) => (
<Link
href={`/anime/${anime.id}`}
key={anime.id}
className="block p-3 rounded hover:bg-white/5 transition-colors border border-[var(--border)] bg-[var(--card)] relative overflow-hidden"
>
{/* Top rank highlight for top 3 */}
{index < 3 && (
<div className="absolute top-0 left-0 w-1 h-full opacity-70"
style={{
background: index === 0 ? 'linear-gradient(to bottom, #303030, #1a1a1a)' :
index === 1 ? 'linear-gradient(to bottom, #282828, #181818)' :
'linear-gradient(to bottom, #202020, #161616)'
}}
/>
)}
<div className="flex items-center">
{/* Rank number with monochrome styling */}
<div className="flex-shrink-0 w-8 flex items-center justify-center mr-3">
<span
className={`flex items-center justify-center w-7 h-7 rounded-lg text-sm font-bold ${
index === 0 ? 'bg-white/20 text-white' :
index === 1 ? 'bg-white/15 text-white' :
index === 2 ? 'bg-white/10 text-white' :
'bg-[var(--background)] text-white/70 border border-[var(--border)]'
}`}
>
{anime.rank || index + 1}
</span>
</div>
{/* Anime thumbnail with subtle shadow */}
<div className="flex-shrink-0 w-12 h-16 relative rounded overflow-hidden mr-3 shadow-md">
<Image
src={anime.poster || '/images/placeholder.png'}
alt={anime.name}
fill
className="object-cover"
unoptimized={true}
/>
</div>
{/* Info */}
<div className="flex-1 min-w-0">
{/* Title */}
<div className="mb-1">
<h3 className="text-sm font-medium text-white line-clamp-1">
{anime.name}
</h3>
</div>
{/* Episodes if available */}
{anime.episodes && (
<div className="flex items-center mb-1">
{anime.episodes.sub > 0 && (
<span className="text-xs bg-[var(--background)] text-[var(--text-muted)] px-1.5 py-0.5 rounded">
SUB {anime.episodes.sub}
</span>
)}
{anime.episodes.dub > 0 && (
<span className="text-xs bg-[var(--background)] text-[var(--text-muted)] px-1.5 py-0.5 rounded ml-1">
DUB {anime.episodes.dub}
</span>
)}
</div>
)}
</div>
</div>
</Link>
))}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,55 @@
'use client';
import React from 'react';
import Link from 'next/link';
import Image from 'next/image';
export default function TrendingList({ trendingAnime = [] }) {
return (
<div className="mb-10 bg-[var(--card)] border border-[var(--border)] rounded-lg overflow-hidden">
<div className="p-4 border-b border-[var(--border)] flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2 text-[var(--text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<h2 className="text-lg font-semibold text-white">Trending Now</h2>
</div>
<div className="min-h-[375px] max-h-[490px] overflow-y-auto toplists-scrollbar">
<div className="pt-3.5 space-y-2">
{trendingAnime.slice(0, 10).map((anime, index) => (
<Link
href={`/anime/${anime.id}`}
key={anime.id || index}
className="block px-3.5 py-3 hover:bg-white/5 transition-colors relative overflow-hidden"
>
<div className="flex items-center gap-3">
{/* Rank number */}
<div className="flex items-center justify-center w-8 text-lg font-bold text-[var(--text-muted)]">
#{index + 1}
</div>
{/* Anime image */}
<div className="relative w-[45px] h-[60px] flex-shrink-0">
<Image
src={anime.image || '/placeholder.png'}
alt={anime.title}
className="rounded object-cover"
fill
sizes="45px"
/>
</div>
{/* Anime info */}
<div className="flex items-center flex-1 min-w-0">
<h3 className="text-sm font-medium text-white line-clamp-2">
{anime.title}
</h3>
</div>
</div>
</Link>
))}
</div>
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff