210 lines
6.1 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
async function getData() {
15
const response = await fetch('/api/data').then(d => d.json())
16
searcher = microsearch(response, ['title', 'link'])
17
return response
18
}
19
20
if (typeof window != 'undefined') {
21
const data = await getData()
22
sites$.value = data.toSorted((x, y) => x.title.localeCompare(y.title))
23
}
24
25
const recents = computed(() =>
26
sites$.value
27
.toSorted(
28
(x, y) => new Date(y.addedOn).getTime() - new Date(x.addedOn).getTime()
29
)
30
.slice(0, 4)
31
)
32
33
const containerWidth = signal(900)
34
const offset = { value: 8 }
35
const columns = signal(3)
36
37
const bentoPositions = computed(() => {
38
const withPositions = []
39
for (let index in sites$.value) {
40
const indAsNum = Number(index)
41
const d = sites$.value[indAsNum]
42
const prevLeft = withPositions[indAsNum - 1]
43
? withPositions[indAsNum - 1].left
44
: 0
45
const prevWidth = withPositions[indAsNum - 1]
46
? withPositions[indAsNum - 1].width
47
: 0
48
49
const originalHeight = d.dimensions.height ?? 543
50
const originalWidth = d.dimensions.width ?? 1039
51
52
const ratio = originalHeight / originalWidth
53
const widthByContainer = containerWidth.value / columns.value - offset.value
54
const heightByContainer = widthByContainer * ratio
55
56
const prevByCol = withPositions[indAsNum - columns.value]
57
let top = 0
58
let left = prevLeft + prevWidth + (indAsNum === 0 ? 0 : offset.value / 2)
59
60
if (prevByCol) {
61
top = prevByCol.top + prevByCol.height + offset.value
62
}
63
64
const prevRow = Math.floor((indAsNum - 1) / columns.value)
65
const currentRow = Math.floor(indAsNum / columns.value)
66
67
let columnCountBroken = currentRow > prevRow ? true : false
68
if (columnCountBroken) {
69
left = 0
70
}
71
72
withPositions.push({
73
top,
74
left,
75
height: heightByContainer,
76
width: widthByContainer,
77
})
78
}
79
return withPositions
80
})
81
82
export default () => {
83
return (
84
<div
85
class="p-10 mx-auto max-w-4xl"
86
ref={node => {
87
if (!node) return
88
let minWidth = 250
89
const resizer = debounce(() => {
90
let usableMinWidth = minWidth
91
const computedStyle = getComputedStyle(node)
92
const widthWithoutPadding =
93
node.getBoundingClientRect().width -
94
(parseFloat(computedStyle.paddingLeft) +
95
parseFloat(computedStyle.paddingRight))
96
97
const colsPossible = Math.floor(widthWithoutPadding / minWidth)
98
const un_Width = offset.value * colsPossible * 2
99
let colsWithOffset = Math.floor(
100
(widthWithoutPadding - un_Width) / minWidth
101
)
102
103
if (colsWithOffset <= 1) {
104
usableMinWidth = widthWithoutPadding - un_Width
105
colsWithOffset = 1
106
}
107
108
containerWidth.value = widthWithoutPadding - un_Width
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="h-full 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