- de.ts: alle nutzer-sichtbaren 'Becken'-Strings → 'Gehege' (nav, fields, titles, buttons, validation, delete, hilfe sections) - AppShell.tsx: route /becken → /gehege, icon 🛁 → 🏜️ (Wüsten-Habitat) - App.tsx: <Route path="gehege"> + <Route path="becken/*"> redirect (bewahrt Bookmarks; BeckenRedirect leitet Unterseiten 1:1 weiter) - BeckenPage/DetailPage/FormPage: interne Links /becken → /gehege - HilfePage.tsx: hartcodierte 'Becken'-Prosa → 'Gehege' - e2e/becken-kontakte.spec.ts: goto('/becken') → goto('/gehege') Gate: build ✓ eslint ✓ vitest 82/82 ✓ e2e 120/120 ✓
110 lines
3.2 KiB
TypeScript
110 lines
3.2 KiB
TypeScript
/** FEAT-2: Becken-Liste — Gridify-Namenssuche + Paging, Link auf Detailseite. */
|
|
import { useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { de } from '../strings/de'
|
|
import { listEnclosuresPaged } from '../api/enclosures'
|
|
import { condition, type GridifyQuery } from '../api/gridify'
|
|
import { useApi } from '../hooks/useApi'
|
|
|
|
const PAGE_SIZE = 20
|
|
|
|
export default function BeckenPage() {
|
|
const t = de.pages.becken
|
|
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 enclosures = useApi(() => listEnclosuresPaged(query), [filter, page])
|
|
|
|
const total = enclosures.data?.totalCount ?? 0
|
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
|
const items = enclosures.data?.items ?? []
|
|
|
|
return (
|
|
<section className="page">
|
|
<header className="page-head">
|
|
<div>
|
|
<h2>{t.title}</h2>
|
|
{enclosures.data && (
|
|
<p className="muted">
|
|
{total} {t.countLabel}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<Link to="/gehege/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>
|
|
|
|
{enclosures.loading && <p className="muted">{de.common.loading}</p>}
|
|
{enclosures.error && (
|
|
<div className="alert alert--error">
|
|
<span>{enclosures.error}</span>
|
|
<button type="button" className="btn" onClick={enclosures.reload}>
|
|
{de.common.retry}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!enclosures.loading && !enclosures.error && items.length === 0 && (
|
|
<p className="muted">{t.empty}</p>
|
|
)}
|
|
|
|
{items.length > 0 && (
|
|
<ul className="card-list">
|
|
{items.map((e) => (
|
|
<li key={e.id}>
|
|
<Link to={`/gehege/${e.id}`} className="gerbil-card">
|
|
<span className="gerbil-card__name">{e.name}</span>
|
|
<span className="gerbil-card__meta">{e.notes ?? ''}</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>
|
|
)
|
|
}
|