Files
GerbilManager/gerbil-manager-web/src/components/AnimalPicker.tsx
Gulum 55c298ad1f UI-POLISH-1: namenlose Tiere + Leerwert-Konsistenz
- 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
2026-06-06 17:32:26 +02:00

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>
)
}