|
|
import { useState, useMemo, useRef, useEffect } from 'react'; |
|
|
import { searchHuggys, Huggy } from '../../data/huggys'; |
|
|
|
|
|
interface HuggyMenuProps { |
|
|
onSelectHuggy: (huggy: Huggy) => void; |
|
|
onClose: () => void; |
|
|
} |
|
|
|
|
|
const INITIAL_DISPLAY_COUNT = 12; |
|
|
const LOAD_MORE_COUNT = 6; |
|
|
|
|
|
export default function HuggyMenu({ onSelectHuggy, onClose }: HuggyMenuProps) { |
|
|
const [searchQuery, setSearchQuery] = useState(''); |
|
|
const [displayCount, setDisplayCount] = useState(INITIAL_DISPLAY_COUNT); |
|
|
const [loadingImages, setLoadingImages] = useState<Set<string>>(new Set()); |
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null); |
|
|
|
|
|
|
|
|
const filteredHuggys = useMemo(() => searchHuggys(searchQuery), [searchQuery]); |
|
|
|
|
|
|
|
|
const displayedHuggys = filteredHuggys.slice(0, displayCount); |
|
|
const hasMore = displayCount < filteredHuggys.length; |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const scrollContainer = scrollContainerRef.current; |
|
|
if (!scrollContainer || !hasMore) return; |
|
|
|
|
|
const checkOverflow = () => { |
|
|
const { scrollHeight, clientHeight } = scrollContainer; |
|
|
|
|
|
if (scrollHeight <= clientHeight && hasMore) { |
|
|
setDisplayCount(prev => Math.min(prev + LOAD_MORE_COUNT, filteredHuggys.length)); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const timer = setTimeout(checkOverflow, 100); |
|
|
return () => clearTimeout(timer); |
|
|
}, [displayCount, hasMore, filteredHuggys.length]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const scrollContainer = scrollContainerRef.current; |
|
|
if (!scrollContainer) return; |
|
|
|
|
|
const handleScroll = () => { |
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollContainer; |
|
|
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; |
|
|
|
|
|
|
|
|
if (scrollPercentage > 0.8 && hasMore) { |
|
|
setDisplayCount(prev => Math.min(prev + LOAD_MORE_COUNT, filteredHuggys.length)); |
|
|
} |
|
|
}; |
|
|
|
|
|
scrollContainer.addEventListener('scroll', handleScroll); |
|
|
return () => scrollContainer.removeEventListener('scroll', handleScroll); |
|
|
}, [hasMore, filteredHuggys.length]); |
|
|
|
|
|
const handleHuggyClick = (huggy: Huggy) => { |
|
|
onSelectHuggy(huggy); |
|
|
onClose(); |
|
|
}; |
|
|
|
|
|
const handleImageLoad = (huggyId: string) => { |
|
|
setLoadingImages(prev => { |
|
|
const newSet = new Set(prev); |
|
|
newSet.delete(huggyId); |
|
|
return newSet; |
|
|
}); |
|
|
}; |
|
|
|
|
|
const handleImageLoadStart = (huggyId: string) => { |
|
|
setLoadingImages(prev => new Set(prev).add(huggyId)); |
|
|
}; |
|
|
|
|
|
|
|
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
|
|
setSearchQuery(e.target.value); |
|
|
setDisplayCount(INITIAL_DISPLAY_COUNT); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<> |
|
|
{/* Backdrop */} |
|
|
<div |
|
|
className="fixed inset-0 z-10" |
|
|
onClick={onClose} |
|
|
/> |
|
|
|
|
|
{/* Huggy Menu */} |
|
|
<div |
|
|
className="huggy-menu fixed left-[107px] top-[20px] z-20 w-[340px] bg-[#f8f9fa] border border-[#3faee6] rounded-[10px] flex flex-col overflow-hidden shadow-lg" |
|
|
onDragStart={(e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
}} |
|
|
onDrag={(e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
}} |
|
|
onDragOver={(e) => { |
|
|
e.preventDefault(); |
|
|
e.stopPropagation(); |
|
|
}} |
|
|
> |
|
|
{/* Search Bar */} |
|
|
<div className="border-b border-[#ebebeb] p-[5px]"> |
|
|
<input |
|
|
type="text" |
|
|
placeholder="Search Huggy" |
|
|
value={searchQuery} |
|
|
onChange={handleSearchChange} |
|
|
className="w-full bg-transparent border-none outline-none text-[14px] text-[#999999] font-['Inter'] placeholder-[#999999]" |
|
|
autoFocus |
|
|
/> |
|
|
</div> |
|
|
|
|
|
{/* Huggy Grid - Scrollable */} |
|
|
<div |
|
|
ref={scrollContainerRef} |
|
|
className="overflow-y-auto p-[5px]" |
|
|
style={{ maxHeight: '430px' }} |
|
|
> |
|
|
{filteredHuggys.length === 0 ? ( |
|
|
<div className="text-center text-[#999999] text-[14px] py-8"> |
|
|
No Huggys found |
|
|
</div> |
|
|
) : ( |
|
|
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-[5px] p-[5px]"> |
|
|
{displayedHuggys.map((huggy) => ( |
|
|
<button |
|
|
key={huggy.id} |
|
|
onClick={() => handleHuggyClick(huggy)} |
|
|
onDragStart={(e) => e.preventDefault()} |
|
|
className="relative w-full aspect-square rounded-[5px] overflow-hidden hover:bg-[#e9ecef] transition-colors cursor-pointer border-none p-0" |
|
|
title={huggy.name} |
|
|
> |
|
|
{/* Shimmer Loading Placeholder */} |
|
|
{loadingImages.has(huggy.id) && ( |
|
|
<div className="absolute inset-0 skeleton-shimmer"></div> |
|
|
)} |
|
|
|
|
|
{/* Huggy Image */} |
|
|
<img |
|
|
src={huggy.thumbnail} |
|
|
alt={huggy.name} |
|
|
className={`w-full h-full object-cover transition-opacity duration-200 ${loadingImages.has(huggy.id) ? 'opacity-0' : 'opacity-100'}`} |
|
|
loading="lazy" |
|
|
onLoadStart={() => handleImageLoadStart(huggy.id)} |
|
|
onLoad={() => handleImageLoad(huggy.id)} |
|
|
onError={() => handleImageLoad(huggy.id)} |
|
|
draggable={false} |
|
|
/> |
|
|
</button> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
|
|
|
{/* Summary Footer */} |
|
|
<div className="border-t border-[#ebebeb] p-[5px] text-center"> |
|
|
<p className="text-[#999999] text-[12px]"> |
|
|
Showing {displayedHuggys.length} of {filteredHuggys.length} Huggys |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</> |
|
|
); |
|
|
} |
|
|
|