212 lines
5.8 kB
1
import { useEffect, useState } from 'preact/hooks'
2
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow'
3
import { get } from '@dumbjs/pick/get'
4
5
import { signal } from '@preact/signals'
6
import { computed } from '@preact/signals'
7
import { Component } from 'preact'
8
9
/**
10
* @type {ReturnType<typeof microsearch>}
11
*/
12
let searcher
13
14
const sites$ = signal([])
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 = { value: 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 - offset.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
console.log({ indAsNum, prevByCol })
63
if (prevByCol) {
64
top = prevByCol.top + prevByCol.height + offset.value / 2
65
}
66
67
const prevRow = Math.floor((indAsNum - 1) / columns.value)
68
const currentRow = Math.floor(indAsNum / columns.value)
69
70
let columnCountBroken = currentRow > prevRow ? true : false
71
if (columnCountBroken) {
72
left = 0
73
}
74
75
withPositions.push({
76
top,
77
left,
78
height: heightByContainer,
79
width: widthByContainer,
80
})
81
}
82
return withPositions
83
})
84
85
export default () => {
86
return (
87
<div class="p-10 mx-auto max-w-4xl">
88
<div class="flex justify-end w-full">
89
<ul class="flex gap-2 items-center mx-2 font-sans text-xs">
90
<li>
91
<a
92
class="text-zinc-600 hover:underline hover:underline-offset-4 hover:text-white"
93
href="https://github.com/barelyhuman/minweb-public-data?tab=readme-ov-file#add-another-site"
94
>
95
Add your site?
96
</a>
97
</li>
98
</ul>
99
100
<h1 class="font-sans text-sm text-zinc-400">minweb.site</h1>
101
</div>
102
<div class="my-24">
103
<h2 class="font-semibold">Recent</h2>
104
<ul class="flex flex-col gap-4 mt-8 w-full">
105
{recents.value.map(d => {
106
return (
107
<li class="w-full text-zinc-500">
108
<a
109
href={d.link}
110
class="relative w-full transition duration-300 link"
111
>
112
{d.title}
113
<span class="font-sans italic absolute top-0 text-[8px] text-emerald-400 -right-99 min-w-44">
114
{formatDistanceToNow(new Date(d.addedOn), {
115
addSuffix: true,
116
})}
117
</span>
118
</a>
119
</li>
120
)
121
})}
122
</ul>
123
</div>
124
<div class="my-24">
125
<h2 class="font-semibold">Gallery</h2>
126
<div class="relative gap-2 mt-4 w-full">
127
{sites$.value.map((d, ind) => {
128
const pos = Object.fromEntries(
129
Object.entries(bentoPositions.value[ind]).map(d => [
130
d[0],
131
d[1] + 'px',
132
])
133
)
134
return (
135
<div
136
class="inline-flex absolute justify-center items-center text-zinc-500"
137
style={{
138
...pos,
139
}}
140
>
141
<Image
142
src={d.imageURL}
143
className="h-full rounded-md"
144
classNameOnLoad="border border-black"
145
/>
146
</div>
147
)
148
})}
149
</div>
150
</div>
151
</div>
152
)
153
}
154
155
function microsearch(collection, paths) {
156
const index = collection.map(d => paths.map(p => get(d, p)))
157
return term => {
158
return index
159
.map((d, index) => {
160
return [d, index]
161
})
162
.filter(val =>
163
val[0].find(t => t.toLowerCase().includes(term.toLowerCase()))
164
)
165
.map(matches => collection[matches[1]])
166
}
167
}
168
169
class Image extends Component {
170
state = {
171
loaded: false,
172
}
173
174
inview(entries, observer) {
175
entries.forEach(entry => {
176
if (!entry.intersectionRatio) return
177
178
entry.target.addEventListener('load', this.loading.bind(this))
179
entry.target.src = this.props.src
180
observer.unobserve(entry.target)
181
})
182
}
183
184
loading(event) {
185
if (event.target.complete)
186
this.setState({
187
loaded: true,
188
})
189
}
190
191
componentDidMount() {
192
this.setState({
193
loaded: false,
194
})
195
196
const observer = new IntersectionObserver(this.inview.bind(this))
197
198
observer.observe(this.element)
199
}
200
201
render() {
202
const { loaded } = this.state
203
const classList = (this.props.class ?? this.props.className)
204
.split(' ')
205
.filter(Boolean)
206
.concat(loaded ? this.props.classNameOnLoad.split(' ') : [])
207
.join(' ')
208
return (
209
<img className={classList} ref={element => (this.element = element)} />
210
)
211
}
212
}
213