personal website

biew

+70 -42
+38 -11
src/components/sections/Vibes.tsx
··· 2 2 3 3 const BATCH_SIZE = 20 4 4 5 + function LazyMedia({ src }: { src: string }) { 6 + const [visible, setVisible] = useState(false) 7 + const ref = useRef<HTMLDivElement>(null) 8 + const isVideo = src.endsWith(".mp4") || src.endsWith(".mov") || src.endsWith(".webm") 9 + 10 + useEffect(() => { 11 + const el = ref.current 12 + if (!el) return 13 + const observer = new IntersectionObserver( 14 + ([entry]) => { 15 + if (entry.isIntersecting) { 16 + setVisible(true) 17 + observer.disconnect() 18 + } 19 + }, 20 + { rootMargin: "300px" } 21 + ) 22 + observer.observe(el) 23 + return () => observer.disconnect() 24 + }, []) 25 + 26 + return ( 27 + <div ref={ref} className="vibe-item"> 28 + {visible && ( 29 + isVideo ? ( 30 + <video src={src} className="vibe-media" controls loop muted preload="none" /> 31 + ) : ( 32 + <img src={src} alt="" className="vibe-media" decoding="async" /> 33 + ) 34 + )} 35 + </div> 36 + ) 37 + } 38 + 5 39 export function Vibes() { 6 40 const [allVibes, setAllVibes] = useState<string[]>([]) 7 41 const [visibleCount, setVisibleCount] = useState(BATCH_SIZE) ··· 24 58 setVisibleCount((c) => Math.min(c + BATCH_SIZE, allVibes.length)) 25 59 } 26 60 }, 27 - { rootMargin: "400px" } 61 + { rootMargin: "600px" } 28 62 ) 29 63 30 64 observer.observe(sentinel) 31 65 return () => observer.disconnect() 32 66 }, [allVibes.length]) 33 67 34 - const visible = allVibes.slice(0, visibleCount) 35 - 36 68 return ( 37 69 <div className="vibes-container"> 38 - {visible.map((vibe, index) => { 39 - const isVideo = vibe.endsWith(".mp4") || vibe.endsWith(".mov") || vibe.endsWith(".webm") 40 - return isVideo ? ( 41 - <video key={vibe} src={vibe} className="vibe-item" controls loop muted preload="none" /> 42 - ) : ( 43 - <img key={vibe} src={vibe} alt="" className="vibe-item" loading="lazy" decoding="async" /> 44 - ) 45 - })} 70 + {allVibes.slice(0, visibleCount).map((vibe) => ( 71 + <LazyMedia key={vibe} src={vibe} /> 72 + ))} 46 73 {visibleCount < allVibes.length && <div ref={sentinelRef} style={{ height: 1 }} />} 47 74 </div> 48 75 )
+32 -31
styles/globals.css
··· 123 123 124 124 /* Vibes gallery */ 125 125 .vibes-container { 126 - display: flex; 127 - flex-wrap: wrap; 128 - justify-content: center; 129 - align-items: flex-start; 130 - gap: 10px; 131 - padding: 5rem 2rem 2rem; 126 + columns: 4 180px; 127 + column-gap: 8px; 128 + padding: 5rem 1.5rem 2rem; 132 129 } 133 130 134 131 .vibe-item { 135 - max-width: 25%; 136 - height: auto; 137 - object-fit: contain; 138 - filter: contrast(1.05); 139 - transition: transform 0.2s; 132 + break-inside: avoid; 133 + margin-bottom: 8px; 140 134 border: 3px solid #fff; 141 - margin-bottom: 10px; 135 + display: block; 136 + transition: transform 0.2s; 137 + --rotate: 0deg; 138 + transform: rotate(var(--rotate)); 139 + min-height: 40px; 142 140 } 143 141 144 142 .vibe-item:hover { 145 - transform: scale(1.05) rotate(calc(var(--hover-rotate) * 1deg)); 143 + transform: rotate(var(--rotate)) scale(1.04); 146 144 z-index: 10; 147 - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); 145 + position: relative; 146 + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5); 148 147 } 149 148 150 - /* Random rotations for chaotic effect */ 151 - .vibe-item:nth-child(11n+1) { --hover-rotate: 3; } 152 - .vibe-item:nth-child(7n+2) { --hover-rotate: -4; } 153 - .vibe-item:nth-child(5n+3) { --hover-rotate: 2; } 154 - .vibe-item:nth-child(13n+4) { --hover-rotate: -3; } 155 - .vibe-item:nth-child(17n+5) { --hover-rotate: 5; } 156 - .vibe-item:nth-child(19n+6) { --hover-rotate: -2; } 149 + .vibe-media { 150 + width: 100%; 151 + height: auto; 152 + display: block; 153 + filter: contrast(1.05); 154 + } 157 155 158 - /* Random margins for messiness */ 159 - .vibe-item:nth-child(8n) { margin-top: 15px; } 160 - .vibe-item:nth-child(12n) { margin-left: 10px; } 161 - .vibe-item:nth-child(9n) { margin-right: 10px; } 162 - .vibe-item:nth-child(15n) { margin-bottom: 20px; } 156 + /* Random slight rotations */ 157 + .vibe-item:nth-child(11n+1) { --rotate: 0.8deg; } 158 + .vibe-item:nth-child(7n+2) { --rotate: -1.2deg; } 159 + .vibe-item:nth-child(5n+3) { --rotate: 0.5deg; } 160 + .vibe-item:nth-child(13n+4) { --rotate: -0.7deg; } 161 + .vibe-item:nth-child(17n+5) { --rotate: 1.4deg; } 162 + .vibe-item:nth-child(19n+6) { --rotate: -0.4deg; } 163 + .vibe-item:nth-child(23n+7) { --rotate: 1deg; } 164 + .vibe-item:nth-child(3n+8) { --rotate: -1.5deg; } 163 165 164 - @media (max-width: 768px) { 165 - .vibe-item { 166 - max-width: 45%; 167 - } 168 - } 166 + /* Irregular margins for messiness */ 167 + .vibe-item:nth-child(8n) { margin-bottom: 16px; } 168 + .vibe-item:nth-child(12n) { margin-bottom: 4px; } 169 + .vibe-item:nth-child(6n) { margin-bottom: 20px; } 169 170 170 171 /* Glassmorphism utilities */ 171 172 @layer utilities {