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