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