({
books, currentIndex,
getCoverUrl, handleCoverError,
onCardClick, onFavorite, onNavigate,
}: BookShelf3DProps)
| 20 | const VISIBLE_RANGE = 3; // Show ±3 books from center |
| 21 | |
| 22 | export default function BookShelf3D({ |
| 23 | books, currentIndex, |
| 24 | getCoverUrl, handleCoverError, |
| 25 | onCardClick, onFavorite, onNavigate, |
| 26 | }: BookShelf3DProps) { |
| 27 | const [hoveredCenter, setHoveredCenter] = useState(false); |
| 28 | const [favAnimating, setFavAnimating] = useState(false); |
| 29 | const wheelCooldown = useRef(false); |
| 30 | |
| 31 | const handleWheel = useCallback((e: React.WheelEvent) => { |
| 32 | if (wheelCooldown.current) return; |
| 33 | wheelCooldown.current = true; |
| 34 | if (e.deltaY > 0 || e.deltaX > 0) { |
| 35 | onNavigate('right'); |
| 36 | } else if (e.deltaY < 0 || e.deltaX < 0) { |
| 37 | onNavigate('left'); |
| 38 | } |
| 39 | setTimeout(() => { wheelCooldown.current = false; }, 300); |
| 40 | }, [onNavigate]); |
| 41 | |
| 42 | const handleFavoriteClick = (e: React.MouseEvent) => { |
| 43 | e.stopPropagation(); |
| 44 | const book = books[currentIndex]; |
| 45 | if (!book || favAnimating) return; |
| 46 | setFavAnimating(true); |
| 47 | onFavorite(book); |
| 48 | setTimeout(() => setFavAnimating(false), 600); |
| 49 | }; |
| 50 | |
| 51 | const centerBook = books[currentIndex]; |
| 52 | const canGoLeft = currentIndex > 0; |
| 53 | const canGoRight = currentIndex < books.length - 1; |
| 54 | |
| 55 | return ( |
| 56 | <div |
| 57 | onWheel={handleWheel} |
| 58 | style={{ |
| 59 | position: 'relative', |
| 60 | width: '100%', |
| 61 | display: 'flex', |
| 62 | flexDirection: 'column', |
| 63 | alignItems: 'center', |
| 64 | gap: '0px', |
| 65 | userSelect: 'none', |
| 66 | }} |
| 67 | > |
| 68 | {/* === Upper section: 3D Bookshelf === */} |
| 69 | <div style={{ |
| 70 | position: 'relative', |
| 71 | width: '100%', |
| 72 | height: 'calc(100vh - 360px)', |
| 73 | minHeight: '320px', |
| 74 | maxHeight: '460px', |
| 75 | display: 'flex', |
| 76 | flexDirection: 'column', |
| 77 | alignItems: 'center', |
| 78 | justifyContent: 'flex-end', |
| 79 | perspective: '1200px', |
nothing calls this directly
no test coverage detected