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