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 = (props: T) => JSX.Element export type CardListProps = { apiRoute: string builder: Builder limit?: number } | { apiRoute: string builder: Builder limit?: number useObserver?: boolean placeholder: ({ ref }: { ref: Ref | undefined }) => JSX.Element fallback: Fallback } export default function CardList( { limit, builder, apiRoute, ...props }: CardListProps, ) { const list: Signal = useSignal([]) 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) => ( {updateFromList(list, index)} )) return <>{placeholders} } return <>{list} } function fillList( list: Signal, builder: Builder, apiRoute: string, { limit, ac }: { limit?: number; ac?: AbortController }, ) { ;(async () => { const propsList = requestApiStream( 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, index: number, ): Promise { const { promise, resolve } = Promise.withResolvers() list.subscribe((value: JSX.Element[]) => { const selected = value.at(index) if (selected) { resolve(selected) } }) return promise }