From f3f453439225887e0f05ed1cd30405d0dcbecbf8 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 13 Jun 2026 08:11:17 +0200 Subject: [PATCH] FEAT: Normalize Anke B./Anke Busch contact, and save other infinite scroll layout updates --- .../src/hooks/useInfiniteList.ts | 24 +++++++ gerbil-manager-web/src/pages/AnfragenPage.tsx | 55 ++++++--------- gerbil-manager-web/src/pages/BeckenPage.tsx | 68 +++++++------------ .../src/pages/VertraegeListPage.tsx | 41 ++++------- tools/import/merge_and_resolve.py | 1 + 5 files changed, 84 insertions(+), 105 deletions(-) diff --git a/gerbil-manager-web/src/hooks/useInfiniteList.ts b/gerbil-manager-web/src/hooks/useInfiniteList.ts index fe7d76a..32581a1 100644 --- a/gerbil-manager-web/src/hooks/useInfiniteList.ts +++ b/gerbil-manager-web/src/hooks/useInfiniteList.ts @@ -125,3 +125,27 @@ export function useInfiniteList( return { items, total, loading, loadingMore, error, hasMore, loadMore, reload } } + +/** + * Wires an IntersectionObserver to a sentinel element that triggers loadMore() as + * it scrolls into view. Render the returned ref on a small element shown only while + * `list.hasMore`, e.g. `{list.hasMore &&
}`. Re-subscribes + * when the load state changes so it keeps paging while the sentinel stays in view. + */ +export function useInfiniteSentinel(list: InfiniteListState) { + const ref = useRef(null) + const { hasMore, loading, loadingMore, loadMore } = list + useEffect(() => { + const el = ref.current + if (!el) return + const io = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !loading && !loadingMore) loadMore() + }, + { rootMargin: '300px 0px' }, + ) + io.observe(el) + return () => io.disconnect() + }, [hasMore, loading, loadingMore, loadMore]) + return ref +} diff --git a/gerbil-manager-web/src/pages/AnfragenPage.tsx b/gerbil-manager-web/src/pages/AnfragenPage.tsx index 36897ac..0567fe7 100644 --- a/gerbil-manager-web/src/pages/AnfragenPage.tsx +++ b/gerbil-manager-web/src/pages/AnfragenPage.tsx @@ -13,7 +13,8 @@ import { syncRequests, type RequestStatus, } from '../api/requests' -import { useApi, useMutation } from '../hooks/useApi' +import { useMutation } from '../hooks/useApi' +import { useInfiniteList, useInfiniteSentinel } from '../hooks/useInfiniteList' import { formatDateTime } from '../format/labels' import { FilterPanel } from '../components/FilterPanel' import './anfragen.css' @@ -31,19 +32,20 @@ export function StatusBadge({ status }: { status: RequestStatus }) { export default function AnfragenPage() { const t = de.pages.anfragen const [status, setStatus] = useState('') - const [page, setPage] = useState(1) const [notice, setNotice] = useState(null) - const requests = useApi( - () => + const requests = useInfiniteList( + (page) => listRequests({ page, pageSize: PAGE_SIZE, - orderBy: 'receivedAt desc', + orderBy: 'receivedAt desc,id', filter: status === '' ? undefined : `status==${status}`, }), - [page, status], + `${status}`, ) + const sentinelRef = useInfiniteSentinel(requests) + const { items, total, loading } = requests const sync = useMutation(() => syncRequests()) async function onSync() { @@ -63,7 +65,7 @@ export default function AnfragenPage() {

{t.title}

- {requests.data &&

{t.countText(requests.data.totalCount)}

} + {!loading &&

{t.countText(total)}

}