FEAT: Merge Anja Sch. into Anja Schaumburg, and keep infinite scroll layout pages
This commit is contained in:
Binary file not shown.
@@ -1,10 +1,10 @@
|
|||||||
/** FEAT-2: Kontakte-Liste — Gridify-Namenssuche + Infinite Scroll, Link auf Detailseite. */
|
/** FEAT-2: Kontakte-Liste — Gridify-Namenssuche + Infinite Scroll, Link auf Detailseite. */
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
import { listContactsPaged } from '../api/contacts'
|
import { listContactsPaged } from '../api/contacts'
|
||||||
import { condition } from '../api/gridify'
|
import { condition } from '../api/gridify'
|
||||||
import { useInfiniteList } from '../hooks/useInfiniteList'
|
import { useInfiniteList, useInfiniteSentinel } from '../hooks/useInfiniteList'
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
@@ -21,21 +21,8 @@ export default function KontaktePage() {
|
|||||||
(page) => listContactsPaged({ filter, orderBy: 'name,id', page, pageSize: PAGE_SIZE }),
|
(page) => listContactsPaged({ filter, orderBy: 'name,id', page, pageSize: PAGE_SIZE }),
|
||||||
`${filter ?? ''}`,
|
`${filter ?? ''}`,
|
||||||
)
|
)
|
||||||
const { items, total, hasMore, loading, loadingMore, loadMore } = list
|
const sentinelRef = useInfiniteSentinel(list)
|
||||||
|
const { items, total, hasMore, loading, loadingMore } = list
|
||||||
const sentinelRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
useEffect(() => {
|
|
||||||
const el = sentinelRef.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 (
|
return (
|
||||||
<section className="page">
|
<section className="page">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** FEAT-13: Abgabeverträge — Liste mit Download/Löschen (/vertraege). */
|
/** FEAT-13: Abgabeverträge — Liste mit Download/Löschen (/vertraege). */
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
import { contractFileUrl, deleteContract, listContracts } from '../api/contracts'
|
import { contractFileUrl, deleteContract, listContracts } from '../api/contracts'
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { Link } from 'react-router-dom'
|
|||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
import { listLitters } from '../api/litters'
|
import { listLitters } from '../api/litters'
|
||||||
import { listGerbils } from '../api/gerbils'
|
import { listGerbils } from '../api/gerbils'
|
||||||
import { andFilter, condition, escapeGridifyValue, type GridifyQuery } from '../api/gridify'
|
import { andFilter, condition, escapeGridifyValue } from '../api/gridify'
|
||||||
import type { Litter } from '../api/types'
|
import type { Litter } from '../api/types'
|
||||||
import { useApi } from '../hooks/useApi'
|
import { useApi } from '../hooks/useApi'
|
||||||
|
import { useInfiniteList, useInfiniteSentinel } from '../hooks/useInfiniteList'
|
||||||
import { formatDate } from '../format/labels'
|
import { formatDate } from '../format/labels'
|
||||||
import { FilterPanel } from '../components/FilterPanel'
|
import { FilterPanel } from '../components/FilterPanel'
|
||||||
|
|
||||||
@@ -13,7 +14,8 @@ const PAGE_SIZE = 20
|
|||||||
|
|
||||||
type Tab = 'litters' | 'pairs'
|
type Tab = 'litters' | 'pairs'
|
||||||
type SortKey = 'dateDesc' | 'dateAsc'
|
type SortKey = 'dateDesc' | 'dateAsc'
|
||||||
const SORT_ORDER_BY: Record<SortKey, string> = { dateDesc: 'date desc', dateAsc: 'date' }
|
// Trailing `id` → total order, so OFFSET pagination is stable for infinite scroll.
|
||||||
|
const SORT_ORDER_BY: Record<SortKey, string> = { dateDesc: 'date desc,id', dateAsc: 'date,id' }
|
||||||
|
|
||||||
function yearOptions(): number[] {
|
function yearOptions(): number[] {
|
||||||
const current = new Date().getFullYear()
|
const current = new Date().getFullYear()
|
||||||
@@ -50,7 +52,6 @@ export default function WuerfeListPage() {
|
|||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [year, setYear] = useState('')
|
const [year, setYear] = useState('')
|
||||||
const [sort, setSort] = useState<SortKey>('dateDesc')
|
const [sort, setSort] = useState<SortKey>('dateDesc')
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
|
|
||||||
// Parent names (shared by both tabs).
|
// Parent names (shared by both tabs).
|
||||||
const gerbils = useApi(() => listGerbils({ page: 1, pageSize: 1000, orderBy: 'name' }), [])
|
const gerbils = useApi(() => listGerbils({ page: 1, pageSize: 1000, orderBy: 'name' }), [])
|
||||||
@@ -68,8 +69,11 @@ export default function WuerfeListPage() {
|
|||||||
year && condition({ field: 'date', op: '<=', value: `${year}-12-31` }),
|
year && condition({ field: 'date', op: '<=', value: `${year}-12-31` }),
|
||||||
)
|
)
|
||||||
const orderBy = SORT_ORDER_BY[sort]
|
const orderBy = SORT_ORDER_BY[sort]
|
||||||
const query: GridifyQuery = { filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE }
|
const litters = useInfiniteList(
|
||||||
const litters = useApi(() => listLitters(query), [filter, orderBy, page])
|
(page) => listLitters({ filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE }),
|
||||||
|
`${filter}|${orderBy}`,
|
||||||
|
)
|
||||||
|
const littersSentinelRef = useInfiniteSentinel(litters)
|
||||||
|
|
||||||
// Pairs tab: all litters, grouped client-side.
|
// Pairs tab: all litters, grouped client-side.
|
||||||
const allLitters = useApi(
|
const allLitters = useApi(
|
||||||
@@ -88,9 +92,7 @@ export default function WuerfeListPage() {
|
|||||||
)
|
)
|
||||||
}, [pairs, search, nameById])
|
}, [pairs, search, nameById])
|
||||||
|
|
||||||
const total = litters.data?.totalCount ?? 0
|
const items = litters.items
|
||||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
|
||||||
const items = litters.data?.items ?? []
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page">
|
<section className="page">
|
||||||
@@ -131,10 +133,7 @@ export default function WuerfeListPage() {
|
|||||||
className="input"
|
className="input"
|
||||||
placeholder={t.searchPlaceholder}
|
placeholder={t.searchPlaceholder}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
setSearch(e.target.value)
|
|
||||||
setPage(1)
|
|
||||||
}}
|
|
||||||
aria-label="Wurf suchen"
|
aria-label="Wurf suchen"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -143,17 +142,13 @@ export default function WuerfeListPage() {
|
|||||||
setSearch('')
|
setSearch('')
|
||||||
setYear('')
|
setYear('')
|
||||||
setSort('dateDesc')
|
setSort('dateDesc')
|
||||||
setPage(1)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>{t.filterYear}</span>
|
<span>{t.filterYear}</span>
|
||||||
<select
|
<select
|
||||||
value={year}
|
value={year}
|
||||||
onChange={(e) => {
|
onChange={(e) => setYear(e.target.value)}
|
||||||
setYear(e.target.value)
|
|
||||||
setPage(1)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="">{t.allYears}</option>
|
<option value="">{t.allYears}</option>
|
||||||
{yearOptions().map((y) => (
|
{yearOptions().map((y) => (
|
||||||
@@ -204,28 +199,14 @@ export default function WuerfeListPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{/* Infinite-scroll sentinel + "loading more" indicator. */}
|
||||||
<nav className="pager" aria-label={de.common.pageNav}>
|
{litters.hasMore && (
|
||||||
<button
|
<div ref={littersSentinelRef} aria-hidden="true" style={{ height: 1 }} />
|
||||||
type="button"
|
)}
|
||||||
className="btn"
|
{litters.loadingMore && (
|
||||||
disabled={page <= 1}
|
<p className="muted" style={{ textAlign: 'center', padding: '0.75rem 0' }}>
|
||||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
{de.common.loading}
|
||||||
>
|
</p>
|
||||||
{de.common.previous}
|
|
||||||
</button>
|
|
||||||
<span className="muted">
|
|
||||||
{de.common.page} {page} {de.common.of} {totalPages}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn"
|
|
||||||
disabled={page >= totalPages}
|
|
||||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
|
||||||
>
|
|
||||||
{de.common.next}
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ def get_normalized_contact_name(name):
|
|||||||
"blackforestgv": "Black Forest",
|
"blackforestgv": "Black Forest",
|
||||||
"topol": "Topolino",
|
"topol": "Topolino",
|
||||||
"ankeb": "Anke Busch",
|
"ankeb": "Anke Busch",
|
||||||
|
"anjasch": "Anja Schaumburg",
|
||||||
}
|
}
|
||||||
|
|
||||||
if n in norm_map:
|
if n in norm_map:
|
||||||
|
|||||||
Reference in New Issue
Block a user