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