FEAT-2: Kontakte pages - list (Gridify search+paging), detail w/ linked animals (Herkunft/Abnehmer), form, delete w/ 409 conflict message

This commit is contained in:
2026-06-06 00:09:43 +02:00
parent d8e94f6d05
commit 92c930c165
3 changed files with 367 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
/** FEAT-2: Kontakte-Liste — Gridify-Namenssuche + Paging, Link auf Detailseite. */
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { de } from '../strings/de'
import { listContactsPaged } from '../api/contacts'
import { condition, type GridifyQuery } from '../api/gridify'
import { useApi } from '../hooks/useApi'
const PAGE_SIZE = 20
export default function KontaktePage() {
const t = de.pages.kontakte
const [search, setSearch] = useState('')
const [page, setPage] = useState(1)
const filter = search.trim()
? condition({ field: 'name', op: 'contains', value: search.trim() })
: undefined
const query: GridifyQuery = { filter, orderBy: 'name', page, pageSize: PAGE_SIZE }
const contacts = useApi(() => listContactsPaged(query), [filter, page])
const total = contacts.data?.totalCount ?? 0
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
const items = contacts.data?.items ?? []
return (
<section className="page">
<header className="page-head">
<div>
<h2>{t.title}</h2>
{contacts.data && (
<p className="muted">
{total} {t.countLabel}
</p>
)}
</div>
<Link to="/kontakte/neu" className="btn btn--primary">
+ {t.newButton}
</Link>
</header>
<div className="filters">
<input
type="search"
className="input"
placeholder={t.searchPlaceholder}
value={search}
onChange={(e) => {
setSearch(e.target.value)
setPage(1)
}}
aria-label={t.fields.name}
/>
</div>
{contacts.loading && <p className="muted">{de.common.loading}</p>}
{contacts.error && (
<div className="alert alert--error">
<span>{contacts.error}</span>
<button type="button" className="btn" onClick={contacts.reload}>
{de.common.retry}
</button>
</div>
)}
{!contacts.loading && !contacts.error && items.length === 0 && (
<p className="muted">{t.empty}</p>
)}
{items.length > 0 && (
<ul className="card-list">
{items.map((c) => (
<li key={c.id}>
<Link to={`/kontakte/${c.id}`} className="gerbil-card">
<span className="gerbil-card__name">{c.name}</span>
<span className="gerbil-card__meta">{c.contactInfo ?? ''}</span>
</Link>
</li>
))}
</ul>
)}
{totalPages > 1 && (
<nav className="pager" aria-label="Seitennavigation">
<button
type="button"
className="btn"
disabled={page <= 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
{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>
)}
</section>
)
}