refactor(island): ♻️ extract universal CardList
from specific BlogCardList
This commit is contained in:
parent
8b716a382f
commit
80ad700c4c
|
@ -1,75 +1,38 @@
|
|||
import { BlogCard, BlogProps } from ':components/BlogBlocks.tsx'
|
||||
import Suspense 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'
|
||||
|
||||
function fillList(
|
||||
list: Signal<JSX.Element[]>,
|
||||
{ limit, ac }: { limit?: number; ac?: AbortController },
|
||||
) {
|
||||
;(async () => {
|
||||
const newsList = requestApiStream<void, BlogProps>(
|
||||
'news/fetchAll',
|
||||
'GET',
|
||||
)
|
||||
|
||||
for await (const news of newsList) {
|
||||
list.value = [
|
||||
...list.value,
|
||||
BlogCard({ ...news, lastUpdate: new Date(news.lastUpdate) }),
|
||||
]
|
||||
if (limit && list.value.length >= limit) break
|
||||
}
|
||||
ac?.abort()
|
||||
})()
|
||||
}
|
||||
import CardList from ':islands/CardList.tsx'
|
||||
import type { Ref } from 'preact'
|
||||
|
||||
export default function BlogCardList(
|
||||
{ limit, usePlaceholder, useObserver }: {
|
||||
usePlaceholder?: boolean
|
||||
limit?: number
|
||||
usePlaceholder?: boolean
|
||||
useObserver?: boolean
|
||||
},
|
||||
) {
|
||||
const list: Signal<JSX.Element[]> = useSignal<JSX.Element[]>([])
|
||||
const ac = new AbortController()
|
||||
const ref = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && useObserver) {
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
fillList(list, { limit, ac })
|
||||
observer.disconnect()
|
||||
}
|
||||
}, {
|
||||
rootMargin: '300px',
|
||||
})
|
||||
//@ts-expect-error Need to investigate why it's work
|
||||
observer.observe(ref.current.base)
|
||||
} else {
|
||||
fillList(list, { limit, ac })
|
||||
}
|
||||
})
|
||||
|
||||
if (limit && usePlaceholder) {
|
||||
const placeholders = Array
|
||||
.from({ length: limit })
|
||||
.map((_, index) => (
|
||||
<Suspense
|
||||
loader={<Placeholder ref={index === 0 ? ref : undefined} />}
|
||||
if (usePlaceholder) {
|
||||
return (
|
||||
<CardList
|
||||
apiRoute='news/fetchAll'
|
||||
builder={builder}
|
||||
limit={limit}
|
||||
placeholder={Placeholder}
|
||||
fallback={Fallback}
|
||||
signal={ac.signal}
|
||||
>
|
||||
{updateFromList(list, index)}
|
||||
</Suspense>
|
||||
))
|
||||
return <>{placeholders}</>
|
||||
useObserver={useObserver}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{list}</>
|
||||
return (
|
||||
<CardList
|
||||
apiRoute='news/fetchAll'
|
||||
builder={builder}
|
||||
limit={limit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function builder(news: BlogProps) {
|
||||
return BlogCard({ ...news, lastUpdate: new Date(news.lastUpdate) })
|
||||
}
|
||||
|
||||
function Placeholder({ ref }: { ref?: Ref<HTMLDivElement> | undefined }) {
|
||||
|
@ -93,18 +56,3 @@ function Fallback() {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
102
islands/CardList.tsx
Normal file
102
islands/CardList.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
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, ...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, { limit, ac })
|
||||
observer.disconnect()
|
||||
}
|
||||
}, {
|
||||
rootMargin: '300px',
|
||||
})
|
||||
observer.observe(ref.current)
|
||||
} else {
|
||||
fillList(list, builder, { 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>,
|
||||
{ limit, ac }: { limit?: number; ac?: AbortController },
|
||||
) {
|
||||
;(async () => {
|
||||
const propsList = requestApiStream<void, ApiResponse>(
|
||||
'news/fetchAll',
|
||||
'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
|
||||
}
|
|
@ -16,7 +16,7 @@ function RenderError(
|
|||
)
|
||||
}
|
||||
|
||||
type Fallback = ({ error }: { error: Error }) => JSX.Element
|
||||
export type Fallback = ({ error }: { error: Error }) => JSX.Element
|
||||
|
||||
export default function Suspense(
|
||||
{ loader, fallback, signal, children }: {
|
||||
|
|
|
@ -19,7 +19,7 @@ export default function Home() {
|
|||
<h2>Nos actus</h2>
|
||||
<AutoGrid columnWidth='15rem' style={{ alignItems: 'center' }}>
|
||||
<>
|
||||
<BlogCardList limit={4} usePlaceholder={true} />
|
||||
<BlogCardList limit={4} usePlaceholder={true} useObserver={true} />
|
||||
<a href='/blog' class='cta'>Voir plus</a>
|
||||
</>
|
||||
</AutoGrid>
|
||||
|
|
Loading…
Reference in a new issue