personal website

adf

+15 -101
+10 -64
src/components/sections/Vibes.tsx
··· 1 - import { useEffect, useRef, useState } from "react" 2 - 3 - const BATCH_SIZE = 20 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 - } 1 + import { useEffect, useState } from "react" 38 2 39 3 export function Vibes() { 40 - const [allVibes, setAllVibes] = useState<string[]>([]) 41 - const [visibleCount, setVisibleCount] = useState(BATCH_SIZE) 42 - const sentinelRef = useRef<HTMLDivElement>(null) 4 + const [vibes, setVibes] = useState<string[]>([]) 43 5 44 6 useEffect(() => { 45 7 fetch("/vibes/manifest.json") 46 - .then((res) => res.json()) 47 - .then(setAllVibes) 48 - .catch(() => setAllVibes([])) 8 + .then((r) => r.json()) 9 + .then(setVibes) 10 + .catch(() => {}) 49 11 }, []) 50 12 51 - useEffect(() => { 52 - const sentinel = sentinelRef.current 53 - if (!sentinel) return 54 - 55 - const observer = new IntersectionObserver( 56 - (entries) => { 57 - if (entries[0].isIntersecting) { 58 - setVisibleCount((c) => Math.min(c + BATCH_SIZE, allVibes.length)) 59 - } 60 - }, 61 - { rootMargin: "600px" } 62 - ) 63 - 64 - observer.observe(sentinel) 65 - return () => observer.disconnect() 66 - }, [allVibes.length]) 67 - 68 13 return ( 69 14 <div className="vibes-container"> 70 - {allVibes.slice(0, visibleCount).map((vibe) => ( 71 - <LazyMedia key={vibe} src={vibe} /> 72 - ))} 73 - {visibleCount < allVibes.length && <div ref={sentinelRef} style={{ height: 1 }} />} 15 + {vibes.map((src) => 16 + /\.(mp4|mov|webm)$/i.test(src) 17 + ? <video key={src} src={src} controls loop muted preload="none" /> 18 + : <img key={src} src={src} alt="" loading="lazy" /> 19 + )} 74 20 </div> 75 21 ) 76 22 }
+5 -37
styles/globals.css
··· 123 123 124 124 /* Vibes gallery */ 125 125 .vibes-container { 126 - columns: 4 180px; 126 + columns: 3 200px; 127 127 column-gap: 8px; 128 - padding: 5rem 1.5rem 2rem; 129 - } 130 - 131 - .vibe-item { 132 - break-inside: avoid; 133 - margin-bottom: 8px; 134 - border: 3px solid #fff; 135 - display: block; 136 - transition: transform 0.2s; 137 - --rotate: 0deg; 138 - transform: rotate(var(--rotate)); 139 - min-height: 40px; 140 - } 141 - 142 - .vibe-item:hover { 143 - transform: rotate(var(--rotate)) scale(1.04); 144 - z-index: 10; 145 - position: relative; 146 - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5); 128 + padding: 5rem 1rem 2rem; 147 129 } 148 130 149 - .vibe-media { 131 + .vibes-container img, 132 + .vibes-container video { 150 133 width: 100%; 151 134 height: auto; 152 135 display: block; 153 - filter: contrast(1.05); 136 + margin-bottom: 8px; 154 137 } 155 - 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; } 165 - 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; } 170 138 171 139 /* Glassmorphism utilities */ 172 140 @layer utilities {