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