104 lines
3 KiB
TypeScript
104 lines
3 KiB
TypeScript
import Suspense, { Fallback } from ':islands/Suspens.tsx'
|
|
import { requestApiStream } from ':src/utils.ts'
|
|
import { Signal, useSignal } from '@preact/signals'
|
|
import type { JSX, Ref } from 'preact'
|
|
import { useEffect, useRef } from 'preact/hooks'
|
|
|
|
export type Builder<T> = (props: T) => JSX.Element
|
|
|
|
export type CardListProps<ApiResponse, RefType = null> = {
|
|
apiRoute: string
|
|
builder: Builder<ApiResponse>
|
|
limit?: number
|
|
} | {
|
|
apiRoute: string
|
|
builder: Builder<ApiResponse>
|
|
limit?: number
|
|
useObserver?: boolean
|
|
placeholder: ({ ref }: { ref: Ref<RefType> | undefined }) => JSX.Element
|
|
fallback: Fallback
|
|
}
|
|
|
|
export default function CardList<ApiResponse, RefType = null>(
|
|
{ limit, builder, apiRoute, ...props }: CardListProps<ApiResponse, RefType>,
|
|
) {
|
|
const list: Signal<JSX.Element[]> = useSignal<JSX.Element[]>([])
|
|
const ac = new AbortController()
|
|
const ref = useRef(null)
|
|
|
|
const useObserver = 'useObserver' in props ? props.useObserver : false
|
|
const placeholder = 'placeholder' in props ? props.placeholder : false
|
|
const fallback = 'fallback' in props ? props.fallback : false
|
|
|
|
useEffect(() => {
|
|
if (ref.current && useObserver) {
|
|
const observer = new IntersectionObserver(([entry]) => {
|
|
if (entry.isIntersecting) {
|
|
fillList(list, builder, apiRoute, { limit, ac })
|
|
observer.disconnect()
|
|
}
|
|
}, {
|
|
rootMargin: '300px',
|
|
})
|
|
observer.observe(ref.current)
|
|
} else {
|
|
fillList(list, builder, apiRoute, { limit, ac })
|
|
}
|
|
})
|
|
|
|
if (limit && placeholder && fallback) {
|
|
const placeholders = Array
|
|
.from({ length: limit })
|
|
.map((_, index) => (
|
|
<Suspense
|
|
loader={placeholder({ ref: index === 0 ? ref : undefined })}
|
|
fallback={fallback}
|
|
signal={ac.signal}
|
|
>
|
|
{updateFromList(list, index)}
|
|
</Suspense>
|
|
))
|
|
return <>{placeholders}</>
|
|
}
|
|
|
|
return <>{list}</>
|
|
}
|
|
|
|
function fillList<ApiResponse>(
|
|
list: Signal<JSX.Element[]>,
|
|
builder: Builder<ApiResponse>,
|
|
apiRoute: string,
|
|
{ limit, ac }: { limit?: number; ac?: AbortController },
|
|
) {
|
|
;(async () => {
|
|
const propsList = requestApiStream<void, ApiResponse>(
|
|
apiRoute,
|
|
'GET',
|
|
)
|
|
|
|
for await (const props of propsList) {
|
|
list.value = [
|
|
...list.value,
|
|
builder(props),
|
|
]
|
|
if (limit && list.value.length >= limit) break
|
|
}
|
|
ac?.abort()
|
|
})()
|
|
}
|
|
|
|
function updateFromList(
|
|
list: Signal<JSX.Element[]>,
|
|
index: number,
|
|
): Promise<JSX.Element> {
|
|
const { promise, resolve } = Promise.withResolvers<JSX.Element>()
|
|
list.subscribe((value: JSX.Element[]) => {
|
|
const selected = value.at(index)
|
|
if (selected) {
|
|
resolve(selected)
|
|
}
|
|
})
|
|
|
|
return promise
|
|
}
|