ChunDe's picture
fix: Prevent thumbnail drag from triggering upload mode and fix canvas corner radius
72660a6
raw
history blame
6.13 kB
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; // Show more initially to ensure scrolling
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);
// Filter Huggys based on search query
const filteredHuggys = useMemo(() => searchHuggys(searchQuery), [searchQuery]);
// Get the Huggys to display (limited by displayCount)
const displayedHuggys = filteredHuggys.slice(0, displayCount);
const hasMore = displayCount < filteredHuggys.length;
// Check if content overflows, if not load more automatically
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer || !hasMore) return;
const checkOverflow = () => {
const { scrollHeight, clientHeight } = scrollContainer;
// If content doesn't overflow (no scrollbar), load more
if (scrollHeight <= clientHeight && hasMore) {
setDisplayCount(prev => Math.min(prev + LOAD_MORE_COUNT, filteredHuggys.length));
}
};
// Check after images load
const timer = setTimeout(checkOverflow, 100);
return () => clearTimeout(timer);
}, [displayCount, hasMore, filteredHuggys.length]);
// Infinite scroll: load more when scrolled near bottom
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
// Load more when scrolled to 80% of content
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));
};
// Reset display count when search changes
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>
</>
);
}