diff --git a/gerbil-manager-web/src/index.css b/gerbil-manager-web/src/index.css index 9d7e786..77ed128 100644 --- a/gerbil-manager-web/src/index.css +++ b/gerbil-manager-web/src/index.css @@ -347,3 +347,87 @@ textarea { gap: 1rem; margin: 1.5rem 0; } + +/* ── Detail page ─────────────────────────────────────────────── */ + +.head-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.def-list { + margin: 0.5rem 0 1.5rem; +} + +.def-row { + display: grid; + grid-template-columns: 1fr; + gap: 0.1rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--color-border); +} + +.def-row dt { + font-size: 0.8rem; + color: var(--color-text-muted); +} + +.def-row dd { + margin: 0; +} + +.def-row code { + background: var(--color-accent-soft); + padding: 0.1rem 0.4rem; + border-radius: 0.3rem; + font-size: 0.95em; +} + +@media (min-width: 768px) { + .def-row { + grid-template-columns: 14rem 1fr; + gap: 1rem; + align-items: baseline; + } +} + +.tabs { + display: flex; + gap: 0.25rem; + border-bottom: 1px solid var(--color-border); + margin-top: 1rem; +} + +.tab { + background: none; + border: none; + border-bottom: 2px solid transparent; + padding: 0.6rem 0.9rem; + font: inherit; + color: var(--color-text-muted); + cursor: pointer; + min-height: 44px; +} + +.tab--active { + color: var(--color-accent); + border-bottom-color: var(--color-accent); + font-weight: 600; +} + +.tab-panel { + padding: 1rem 0; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} diff --git a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx new file mode 100644 index 0000000..262449f --- /dev/null +++ b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx @@ -0,0 +1,146 @@ +import { useMemo, useState, type ReactNode } from 'react' +import { Link, useParams } from 'react-router-dom' +import { de } from '../strings/de' +import { getGerbil } from '../api/gerbils' +import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups' +import { useApi } from '../hooks/useApi' +import { formatDate, genderLabel, statusLabel } from '../format/labels' +import { fromDisplayString, genotypeToFarbschlag, toDisplayString } from '../genetics' + +type DetailTab = 'photos' | 'health' | 'weight' + +/** Parse a stored genotype string and derive its normalized form + Farbschlag. */ +function describeGenotype(genotype: string | null): { display: string; farbschlag: string } | null { + if (!genotype || !genotype.trim()) return null + try { + const g = fromDisplayString(genotype) + return { display: toDisplayString(g), farbschlag: genotypeToFarbschlag(g) } + } catch { + // Unparseable: show the raw value, no derived Farbschlag. + return { display: genotype, farbschlag: de.genetics.unknownFarbschlag } + } +} + +function Row({ label, value }: { label: string; value: ReactNode }) { + return ( +
+
{label}
+
{value ?? '—'}
+
+ ) +} + +export default function GerbilDetailPage() { + const t = de.pages.gerbils + const { id = '' } = useParams() + const [tab, setTab] = useState('photos') + + const gerbil = useApi(() => getGerbil(id), [id]) + const colorVarieties = useApi(() => listColorVarieties(), []) + const enclosures = useApi(() => listEnclosures(), []) + const contacts = useApi(() => listContacts(), []) + const litters = useApi(() => listLitters(), []) + + const colorName = useMemo( + () => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])), + [colorVarieties.data], + ) + const enclosureName = useMemo( + () => new Map((enclosures.data ?? []).map((e) => [e.id, e.name])), + [enclosures.data], + ) + const contactName = useMemo( + () => new Map((contacts.data ?? []).map((c) => [c.id, c.name])), + [contacts.data], + ) + const litterName = useMemo( + () => new Map((litters.data ?? []).map((l) => [l.id, l.name])), + [litters.data], + ) + + if (gerbil.loading) return

{de.common.loading}

+ if (gerbil.error || !gerbil.data) { + return ( +
+

{gerbil.error ?? t.detail.notFound}

+ + {t.detail.back} + +
+ ) + } + + const g = gerbil.data + const geno = describeGenotype(g.genotype) + const lookup = (map: Map, key: string | null) => (key ? (map.get(key) ?? '—') : '—') + + return ( +
+
+
+

{g.name}

+ {statusLabel(g.status)} +
+
+ + {t.detail.edit} + + + {t.detail.back} + +
+
+ +

{t.detail.masterData}

+
+ + + + {g.status === 'Deceased' && ( + <> + + + + )} + {g.status === 'GivenAway' && ( + + )} + + + + + + +
+ +

{t.detail.genetics}

+ {geno ? ( +
+ {geno.display}} /> + +
+ ) : ( +

{t.detail.genotypeNotSet}

+ )} + +

Weitere Daten

+
+ {(['photos', 'health', 'weight'] as const).map((key) => ( + + ))} +
+
+

{t.detail.tabPlaceholder}

+
+
+ ) +}