mirror of
https://github.com/JustAnimeCore/JustAnime.git
synced 2026-04-17 22:01:45 +00:00
fresh
This commit is contained in:
703
src/components/Navbar.js
Normal file
703
src/components/Navbar.js
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user