- de.ts: nameless = '(ohne Namen)' (neuer Platzhalter-String) - GerbilsPage: g.name || t.nameless in Karten-Name + Checkbox-Aria - GerbilDetailPage: g.name || nameless im h2-Header; g.causeOfDeath/notes via || null (leer-string-sicher wie Herkunft) - AnimalPicker: g.name || nameless im Picker-Button e2e: +1 nameless-stub in mock-data; 2 neue Tests (Liste + Detail-Header) Gate: vitest 82/82, e2e 130/130, tsc clean
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import { useState } from 'react'
|
|
import { de } from '../strings/de'
|
|
|
|
const nameless = de.pages.gerbils.nameless
|
|
import { listGerbils } from '../api/gerbils'
|
|
import { andFilter, condition } from '../api/gridify'
|
|
import type { Gender, Gerbil, Paged } from '../api/types'
|
|
import { useApi } from '../hooks/useApi'
|
|
|
|
const EMPTY_PAGE: Paged<Gerbil> = { items: [], totalCount: 0, page: 1, pageSize: 0 }
|
|
|
|
export interface AnimalPickerProps {
|
|
/** Restrict results to this gender (Vater = male, Mutter = female). */
|
|
gender: Gender
|
|
onPick: (gerbil: Gerbil) => void
|
|
}
|
|
|
|
/** Search existing animals (gender-filtered) and pick one as a breeding parent. */
|
|
export default function AnimalPicker({ gender, onPick }: AnimalPickerProps) {
|
|
const t = de.pages.genetik
|
|
const [search, setSearch] = useState('')
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const trimmed = search.trim()
|
|
const filter = andFilter(
|
|
trimmed && condition({ field: 'name', op: 'contains', value: trimmed }),
|
|
condition({ field: 'gender', op: '==', value: gender }),
|
|
)
|
|
const result = useApi(
|
|
() => (trimmed ? listGerbils({ filter, orderBy: 'name', page: 1, pageSize: 8 }) : Promise.resolve(EMPTY_PAGE)),
|
|
[filter, trimmed],
|
|
)
|
|
|
|
const items = result.data?.items ?? []
|
|
|
|
return (
|
|
<div className="animal-picker">
|
|
<input
|
|
type="search"
|
|
className="input"
|
|
placeholder={t.pickAnimalSearch}
|
|
value={search}
|
|
onChange={(e) => {
|
|
setSearch(e.target.value)
|
|
setOpen(true)
|
|
}}
|
|
onFocus={() => setOpen(true)}
|
|
aria-label={t.pickAnimal}
|
|
/>
|
|
{open && trimmed !== '' && (
|
|
<ul className="animal-picker__results">
|
|
{result.loading && <li className="muted animal-picker__hint">{de.common.loading}</li>}
|
|
{!result.loading && items.length === 0 && (
|
|
<li className="muted animal-picker__hint">{t.pickAnimalNone}</li>
|
|
)}
|
|
{items.map((g) => (
|
|
<li key={g.id}>
|
|
<button
|
|
type="button"
|
|
className="animal-picker__item"
|
|
onClick={() => {
|
|
onPick(g)
|
|
setSearch('')
|
|
setOpen(false)
|
|
}}
|
|
>
|
|
{g.name || nameless}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|