forked from phc/website
feat: poisson user page with fuzzy search
This commit is contained in:
+3
-3
@@ -19,9 +19,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
integrations: [preact(), mdx()],
|
||||
adapter: node({
|
||||
mode: 'standalone',
|
||||
}),
|
||||
// adapter: node({
|
||||
// mode: 'standalone',
|
||||
// }),
|
||||
output: 'hybrid',
|
||||
outDir: './out/astro',
|
||||
build: {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"astro": "^4.15.11",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"drizzle-orm": "^0.29.4",
|
||||
"fuse.js": "^7.0.0",
|
||||
"katex": "^0.16.9",
|
||||
"preact": "^10.19.6",
|
||||
"typescript": "^5.3.3"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useComputed, useSignal, type ReadonlySignal } from '@preact/signals'
|
||||
import type { JSX } from 'preact/jsx-runtime'
|
||||
|
||||
export const ShowMore = <T extends any>({
|
||||
items,
|
||||
pageSize,
|
||||
children,
|
||||
}: {
|
||||
items: ReadonlySignal<T[]>
|
||||
pageSize: number
|
||||
children: (item: T) => JSX.Element
|
||||
}) => {
|
||||
const $shownItems = useSignal(pageSize)
|
||||
|
||||
const $paginatedItems = useComputed(() => {
|
||||
return items.value.slice(0, $shownItems.value)
|
||||
})
|
||||
|
||||
console.log($paginatedItems.value)
|
||||
|
||||
return (
|
||||
<>
|
||||
{$paginatedItems.value.map(children)}
|
||||
<div class="show-more">
|
||||
{$shownItems.value < items.value.length && (
|
||||
<button onClick={() => ($shownItems.value += pageSize)}>Mostra altri</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+46
-17
@@ -1,5 +1,8 @@
|
||||
import { useSignal } from '@preact/signals'
|
||||
import { useComputed, useSignal } from '@preact/signals'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import { trottleDebounce } from './lib/util'
|
||||
import { ShowMore } from './Paginate'
|
||||
|
||||
type User = {
|
||||
uid: string
|
||||
@@ -20,37 +23,63 @@ function applyPatches(users: User[]) {
|
||||
}
|
||||
|
||||
export const UtentiPage = () => {
|
||||
const utentiSignal = useSignal<User[]>([])
|
||||
const $utentiData = useSignal<User[]>([])
|
||||
const $fuse = useSignal<Fuse<User> | null>(null)
|
||||
|
||||
const $searchText = useSignal('')
|
||||
const $searchResults = useComputed(() =>
|
||||
$searchText.value.trim().length > 0
|
||||
? ($fuse.value?.search($searchText.value).map(result => result.item) ?? [])
|
||||
: $utentiData.value,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://poisson.phc.dm.unipi.it/users.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
applyPatches(data)
|
||||
utentiSignal.value = data
|
||||
|
||||
$utentiData.value = data
|
||||
$fuse.value = new Fuse(data, {
|
||||
keys: ['gecos'],
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="search">
|
||||
<input type="text" placeholder="Cerca un utente Poisson..." />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cerca un utente Poisson..."
|
||||
onInput={e => ($searchText.value = e.currentTarget.value)}
|
||||
value={$searchText.value}
|
||||
/>
|
||||
<span class="material-symbols-outlined">search</span>
|
||||
</div>
|
||||
<div class="search-results">
|
||||
{utentiSignal.value.map(user => (
|
||||
<div class="search-result">
|
||||
<div class="icon">
|
||||
<span class="material-symbols-outlined">person</span>
|
||||
</div>
|
||||
<div class="text">{user.gecos}</div>
|
||||
<div class="right">
|
||||
<a href={`https://poisson.phc.dm.unipi.it/~${user.uid}`}>
|
||||
<span class="material-symbols-outlined">globe</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{$searchResults.value ? (
|
||||
<ShowMore items={$searchResults} pageSize={10}>
|
||||
{poissonUser => (
|
||||
console.log(poissonUser),
|
||||
(
|
||||
<div class="search-result">
|
||||
<div class="icon">
|
||||
<span class="material-symbols-outlined">person</span>
|
||||
</div>
|
||||
<div class="text">{poissonUser.gecos}</div>
|
||||
<div class="right">
|
||||
<a href={`https://poisson.phc.dm.unipi.it/~${poissonUser.uid}`}>
|
||||
<span class="material-symbols-outlined">globe</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</ShowMore>
|
||||
) : (
|
||||
<>Nessun risultato</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export const trottleDebounce = <T extends any[], R>(
|
||||
fn: (...args: T) => R,
|
||||
delay: number,
|
||||
options: { leading?: boolean; trailing?: boolean } = {},
|
||||
): ((...args: T) => R | undefined) => {
|
||||
let lastCall = 0
|
||||
let lastResult: R | undefined
|
||||
let lastArgs: T | undefined
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
|
||||
const leading = options.leading ?? true
|
||||
const trailing = options.trailing ?? true
|
||||
|
||||
return (...args: T): R | undefined => {
|
||||
lastArgs = args
|
||||
if (leading && Date.now() - lastCall >= delay) {
|
||||
lastCall = Date.now()
|
||||
lastResult = fn(...args)
|
||||
} else {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
if (trailing && lastArgs) {
|
||||
lastCall = Date.now()
|
||||
lastResult = fn(...lastArgs)
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
console.log(lastResult)
|
||||
return lastResult
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
# Questo documento è utilizzato nella homepage del sito nella card principale
|
||||
title: Cos'è il PHC?
|
||||
---
|
||||
|
||||
|
||||
@@ -594,4 +594,8 @@
|
||||
max-width: 25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.show-more {
|
||||
place-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,8 @@
|
||||
}
|
||||
|
||||
.utenti {
|
||||
background: #ffffe4;
|
||||
|
||||
main {
|
||||
justify-self: center;
|
||||
|
||||
@@ -250,6 +252,14 @@
|
||||
padding: 5rem 0;
|
||||
|
||||
gap: 5rem;
|
||||
|
||||
.search-result {
|
||||
background: #ffeabc;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #ffd270;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user