100 lines
3.6 KiB
TypeScript
100 lines
3.6 KiB
TypeScript
|
|
import { useEffect, useRef, useState } from 'react'
|
||
|
|
import type { BackendArt } from '../../functions/BackendTypes'
|
||
|
|
import { useScrollRoot } from '../../functions/Context'
|
||
|
|
import { routeIntercept } from '../../functions/Route'
|
||
|
|
import { CDN_BASE } from '../../functions/Backend'
|
||
|
|
import './styles/LayoutBrowser.css'
|
||
|
|
|
||
|
|
interface PropsForLayoutBrowser {
|
||
|
|
items: BackendArt[]
|
||
|
|
position: number
|
||
|
|
onEndReached?: () => void
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface RecoverForLayoutBrowser {
|
||
|
|
position: number
|
||
|
|
items: BackendArt[]
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function LayoutBrowser({ items, position, onEndReached }: PropsForLayoutBrowser) {
|
||
|
|
const [columnCount, setColumnCount] = useState(3)
|
||
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
||
|
|
const scrollRoot = useScrollRoot()
|
||
|
|
const didRestore = useRef(false)
|
||
|
|
|
||
|
|
// Endless Scrolling
|
||
|
|
useEffect(() => {
|
||
|
|
if (!onEndReached || !scrollRoot) return
|
||
|
|
const onScroll = () => {
|
||
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollRoot
|
||
|
|
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||
|
|
onEndReached()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
scrollRoot.addEventListener('scroll', onScroll)
|
||
|
|
return () => scrollRoot.removeEventListener('scroll', onScroll)
|
||
|
|
}, [onEndReached, scrollRoot])
|
||
|
|
|
||
|
|
// Restore Scrolling
|
||
|
|
useEffect(() => {
|
||
|
|
if (!scrollRoot || didRestore.current) return
|
||
|
|
|
||
|
|
// avoid race conditions
|
||
|
|
const raf = requestAnimationFrame(() => {
|
||
|
|
scrollRoot.scrollTo({ top: position })
|
||
|
|
didRestore.current = true
|
||
|
|
})
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
cancelAnimationFrame(raf)
|
||
|
|
}
|
||
|
|
}, [scrollRoot, position, items])
|
||
|
|
|
||
|
|
// Calculate Column Count
|
||
|
|
useEffect(() => {
|
||
|
|
const el = containerRef.current
|
||
|
|
if (!el) return
|
||
|
|
const ro = new ResizeObserver(([entry]) => {
|
||
|
|
setColumnCount(Math.max(1, Math.floor(entry.contentRect.width / 160)))
|
||
|
|
})
|
||
|
|
ro.observe(el)
|
||
|
|
return () => ro.disconnect()
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const columns: BackendArt[][] = Array.from({ length: columnCount }, () => [])
|
||
|
|
items.forEach((item, i) => columns[i % columnCount].push(item))
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="layout-browser" ref={containerRef}>
|
||
|
|
{columns.map((column, columnIdx) => (
|
||
|
|
<div key={columnIdx} className="column">
|
||
|
|
{column.map((item, itemIdx) => {
|
||
|
|
const animationOrder = Math.min(columnIdx + itemIdx * columnCount, 25)
|
||
|
|
const animationDelay = `calc(${animationOrder} * var(--animation-step-delay))`
|
||
|
|
return (
|
||
|
|
<a
|
||
|
|
className="item"
|
||
|
|
href={`/art/${item.id}`}
|
||
|
|
onClick={(e) =>
|
||
|
|
routeIntercept(e, item, {
|
||
|
|
position: scrollRoot?.scrollTop ?? 0,
|
||
|
|
items: items,
|
||
|
|
} as RecoverForLayoutBrowser)
|
||
|
|
}>
|
||
|
|
<img
|
||
|
|
style={{ animationDelay }}
|
||
|
|
className="preview animation-fall-in"
|
||
|
|
src={`${CDN_BASE}/${item.id}/preview.avif`}
|
||
|
|
/>
|
||
|
|
<div className="metadata">
|
||
|
|
<div className="title">{item.title}</div>
|
||
|
|
</div>
|
||
|
|
</a>
|
||
|
|
)
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|