121 lines
3.6 kB
1
import { useEffect, useState } from 'preact/hooks'
2
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow'
3
import { get } from '@dumbjs/pick/get'
4
5
/**
6
* @type {ReturnType<typeof microsearch>}
7
*/
8
let searcher
9
10
async function getData() {
11
const response = await fetch('/api/data').then(d => d.json())
12
13
searcher = microsearch(response, ['title', 'link'])
14
15
return response
16
}
17
18
export default () => {
19
const [sites, setSites] = useState([])
20
const [searchTerm, setSearchTerm] = useState('')
21
22
useEffect(() => {
23
getData().then(d => setSites(d))
24
}, [])
25
26
const recents = sites
27
.toSorted(
28
(x, y) => new Date(y.addedOn).getTime() - new Date(x.addedOn).getTime()
29
)
30
.slice(0, 4)
31
32
const totalCount = sites.length
33
34
const filteredSites = (searchTerm ? searcher(searchTerm) : sites)
35
.toSorted((x, y) => x.title.localeCompare(y.title))
36
37
return (
38
<div class="p-10 mx-auto max-w-4xl">
39
<div class="flex justify-end w-full">
40
<ul class="flex gap-2 items-center mx-2 font-sans text-xs">
41
<li>
42
<a class="text-zinc-600 hover:underline hover:underline-offset-4 hover:text-white" href="https://github.com/barelyhuman/minweb-public-data?tab=readme-ov-file#add-another-site">Add your site?</a>
43
</li>
44
</ul>
45
46
<h1 class="font-sans text-sm text-zinc-400">minweb.site</h1>
47
</div>
48
<div class="my-24">
49
<h2 class="font-semibold">Recent</h2>
50
<ul class="flex flex-col gap-4 mt-8 w-full">
51
{recents.map(d => {
52
return (
53
<li class="w-full text-zinc-500">
54
<a
55
href={d.link}
56
class="relative w-full transition duration-300 link"
57
>
58
{d.title}
59
<span class="font-sans italic absolute top-0 text-[8px] text-emerald-400 -right-99 min-w-44">
60
{formatDistanceToNow(new Date(d.addedOn), {
61
addSuffix: true,
62
})}
63
</span>
64
</a>
65
</li>
66
)
67
})}
68
</ul>
69
</div>
70
71
<div class="my-24">
72
<div class="flex flex-wrap gap-2 justify-between">
73
<h2 class="font-semibold">
74
All <span class="text-zinc-500">( {totalCount} )</span>
75
</h2>
76
<div class="flex min-w-56">
77
<input
78
name="search"
79
class="transition-colors duration-300 input focus:ring-0 focus:border-emerald-400"
80
placeholder="search"
81
ref={node => {
82
if (!node) return
83
node.addEventListener('keyup', e => {
84
setSearchTerm(e.target.value)
85
})
86
}}
87
/>
88
</div>
89
</div>
90
<ul class="flex flex-col gap-4 mt-8">
91
{filteredSites.map(d => {
92
return (
93
<li class="w-full text-zinc-500">
94
<a
95
href={d.link}
96
class="relative w-full transition duration-300 link"
97
>
98
{d.title}
99
</a>
100
</li>
101
)
102
})}
103
</ul>
104
</div>
105
</div>
106
)
107
}
108
109
function microsearch(collection, paths) {
110
const index = collection.map(d => paths.map(p => get(d, p)))
111
return term => {
112
return index
113
.map((d, index) => {
114
return [d, index]
115
})
116
.filter(val =>
117
val[0].find(t => t.toLowerCase().includes(term.toLowerCase()))
118
)
119
.map(matches => collection[matches[1]])
120
}
121
}
122