219 lines
6.0 kB
1
import { get } from '@dumbjs/pick/get'
2
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow'
3
import { useMeta, useTitle } from 'adex/head'
4
5
import { computed, signal } from '@preact/signals'
6
import { Image } from '../components/Image'
7
8
/**
9
* @type {ReturnType<typeof microsearch>}
10
*/
11
let searcher
12
13
const sites$ = signal([])
14
15
const MIN_CARD_WIDTH = 280
16
17
async function getData() {
18
const response = await fetch('/api/data').then(d => d.json())
19
searcher = microsearch(response, ['title', 'link'])
20
return response
21
}
22
23
if (typeof window != 'undefined') {
24
;(async () => {
25
const data = await getData()
26
sites$.value = data.sort((x, y) => x.title.localeCompare(y.title))
27
})()
28
}
29
30
const recents = computed(() =>
31
sites$.value
32
.toSorted(
33
(x, y) => new Date(y.addedOn).getTime() - new Date(x.addedOn).getTime()
34
)
35
.slice(0, 4)
36
)
37
38
const containerWidth = signal(900)
39
const offset = { value: 8 }
40
const columns = signal(3)
41
42
const bentoPositions = computed(() => {
43
const sites = sites$.value
44
const containerW = containerWidth.value
45
const cols = columns.value
46
const offsetVal = offset.value
47
const count = sites.length
48
const cardWidth = containerW / cols
49
const withPositions = new Array(count)
50
51
for (let i = 0; i < count; i++) {
52
const d = sites[i]
53
const originalHeight = d.dimensions.height ?? 543
54
const originalWidth = d.dimensions.width ?? 1039
55
const ratio = originalHeight / originalWidth
56
const heightByContainer = cardWidth * ratio
57
58
let left = 0
59
let top = 0
60
61
// Calculate horizontal position:
62
if (i % cols !== 0) {
63
const prev = withPositions[i - 1]
64
left = prev.left + prev.width + offsetVal / 2
65
} else {
66
left = 0
67
}
68
69
// Calculate vertical position if not in the first row:
70
if (i >= cols) {
71
const prevByCol = withPositions[i - cols]
72
top = prevByCol.top + prevByCol.height + offsetVal
73
}
74
75
withPositions[i] = {
76
top,
77
left,
78
height: heightByContainer,
79
width: cardWidth,
80
}
81
}
82
return withPositions
83
})
84
85
function useDefaultHead() {
86
useTitle('minweb.site | Minimal Websites Gallery')
87
useMeta({
88
name: 'viewport',
89
content: 'width=device-width, initial-scale=1.0',
90
})
91
}
92
93
export default () => {
94
useDefaultHead()
95
96
return (
97
<div
98
class="p-2 mx-auto sm:p-5 md:p-10 max-w-screen"
99
ref={node => {
100
if (!node) return
101
const resizer = debounce(() => {
102
const computedStyle = getComputedStyle(node)
103
const widthWithoutPadding =
104
node.getBoundingClientRect().width -
105
(parseFloat(computedStyle.paddingLeft) +
106
parseFloat(computedStyle.paddingRight))
107
108
const negationWidthForOffset = offset.value
109
let colsWithOffset = Math.floor(
110
(widthWithoutPadding - negationWidthForOffset) / MIN_CARD_WIDTH
111
)
112
113
if (colsWithOffset <= 1) {
114
colsWithOffset = 1
115
}
116
117
containerWidth.value = widthWithoutPadding - negationWidthForOffset
118
columns.value = colsWithOffset
119
}, 350)
120
121
window.addEventListener('resize', () => {
122
resizer()
123
})
124
125
resizer()
126
}}
127
>
128
<div class="flex justify-end w-full">
129
<ul class="flex gap-2 items-center mx-2 font-sans text-xs">
130
<li>
131
<a
132
class="text-zinc-600 hover:underline hover:underline-offset-4 hover:text-black"
133
href="https://github.com/barelyhuman/minweb-public-data?tab=readme-ov-file#add-another-site"
134
>
135
Add your site?
136
</a>
137
</li>
138
</ul>
139
140
<h1 class="font-sans text-sm text-zinc-400">minweb.site</h1>
141
</div>
142
<div class="my-24">
143
<h2 class="font-semibold">Recent</h2>
144
<ul class="flex flex-col gap-4 mt-8 w-full">
145
{recents.value.map(d => {
146
return (
147
<li class="w-full text-zinc-500">
148
<a
149
href={d.link}
150
class="relative w-full transition duration-300 link"
151
>
152
{d.title}
153
<span class="font-sans italic absolute top-0 text-[8px] text-emerald-400 -right-99 min-w-44">
154
{formatDistanceToNow(new Date(d.addedOn), {
155
addSuffix: true,
156
})}
157
</span>
158
</a>
159
</li>
160
)
161
})}
162
</ul>
163
</div>
164
<div class="my-24">
165
<h2 class="font-semibold">Gallery</h2>
166
<div class="relative gap-2 mt-4 w-full">
167
{sites$.value.map((d, ind) => {
168
const pos = Object.fromEntries(
169
Object.entries(bentoPositions.value[ind]).map(d => [
170
d[0],
171
d[1] + 'px',
172
])
173
)
174
return (
175
<div
176
class="inline-flex absolute justify-center items-center hover:cursor-pointer text-zinc-500"
177
style={{
178
...pos,
179
}}
180
>
181
<a href={d.link} class="transition-all transition hover:px-1">
182
<Image
183
src={d.imageURL}
184
className="rounded-md"
185
classNameOnLoad="border-2 border-black"
186
/>
187
</a>
188
</div>
189
)
190
})}
191
</div>
192
</div>
193
</div>
194
)
195
}
196
197
function microsearch(collection, paths) {
198
const index = collection.map(d => paths.map(p => get(d, p)))
199
return term => {
200
return index
201
.map((d, index) => {
202
return [d, index]
203
})
204
.filter(val =>
205
val[0].find(t => t.toLowerCase().includes(term.toLowerCase()))
206
)
207
.map(matches => collection[matches[1]])
208
}
209
}
210
211
function debounce(fn, delay) {
212
let handler
213
return (...args) => {
214
if (handler) clearTimeout(handler)
215
handler = setTimeout(() => {
216
fn(...args)
217
}, delay)
218
}
219
}
220