- 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 ✓
92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
/** FEAT-2: Becken anlegen/bearbeiten — Name (Pflicht) + Notizen. */
|
|
import { useState, type FormEvent } from 'react'
|
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
|
import { de } from '../strings/de'
|
|
import { createEnclosure, getEnclosure, updateEnclosure, type CreateEnclosure } from '../api/enclosures'
|
|
import { useApi, useMutation } from '../hooks/useApi'
|
|
|
|
interface FormState {
|
|
name: string
|
|
notes: string
|
|
}
|
|
|
|
const EMPTY: FormState = { name: '', notes: '' }
|
|
|
|
/** "" -> null, sonst der Wert. */
|
|
const nn = (s: string): string | null => (s.trim() === '' ? null : s)
|
|
|
|
export default function BeckenFormPage() {
|
|
const t = de.pages.becken
|
|
const navigate = useNavigate()
|
|
const { id } = useParams()
|
|
const isEdit = Boolean(id)
|
|
|
|
const [form, setForm] = useState<FormState>(EMPTY)
|
|
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({})
|
|
const [initializedFor, setInitializedFor] = useState<string | null>(null)
|
|
|
|
const existing = useApi(() => (id ? getEnclosure(id) : Promise.resolve(null)), [id])
|
|
|
|
// Vorbefüllen im Bearbeiten-Modus (adjust-state-during-render, wie FEAT-1).
|
|
if (existing.data && initializedFor !== existing.data.id) {
|
|
setInitializedFor(existing.data.id)
|
|
setForm({ name: existing.data.name, notes: existing.data.notes ?? '' })
|
|
}
|
|
|
|
const mutation = useMutation((body: CreateEnclosure) =>
|
|
isEdit && id ? updateEnclosure(id, body) : createEnclosure(body),
|
|
)
|
|
|
|
async function onSubmit(e: FormEvent) {
|
|
e.preventDefault()
|
|
if (form.name.trim() === '') {
|
|
setErrors({ name: t.validation.nameRequired })
|
|
return
|
|
}
|
|
setErrors({})
|
|
const result = await mutation.run({ name: form.name.trim(), notes: nn(form.notes) })
|
|
if (result.ok) navigate(`/gehege/${result.value.id}`)
|
|
// On failure mutation.error drives the inline alert; run() never throws.
|
|
}
|
|
|
|
if (isEdit && existing.loading) return <p className="muted">{de.common.loading}</p>
|
|
|
|
return (
|
|
<section className="page">
|
|
<h2>{isEdit ? t.form.editTitle : t.form.createTitle}</h2>
|
|
|
|
<form className="form" onSubmit={onSubmit} noValidate>
|
|
<label className="field">
|
|
<span>{t.fields.name} *</span>
|
|
<input
|
|
className="input"
|
|
value={form.name}
|
|
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
|
aria-invalid={Boolean(errors.name)}
|
|
/>
|
|
{errors.name && <small className="error-text">{errors.name}</small>}
|
|
</label>
|
|
|
|
<label className="field">
|
|
<span>{t.fields.notes}</span>
|
|
<textarea
|
|
value={form.notes}
|
|
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
|
/>
|
|
</label>
|
|
|
|
{mutation.error && <div className="alert alert--error">{mutation.error}</div>}
|
|
|
|
<div className="form-actions">
|
|
<button type="submit" className="btn btn--primary" disabled={mutation.pending}>
|
|
{mutation.pending ? t.form.saving : t.form.save}
|
|
</button>
|
|
<Link to={isEdit && id ? `/gehege/${id}` : '/gehege'} className="btn">
|
|
{t.form.cancel}
|
|
</Link>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
)
|
|
}
|