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 ( +
{de.common.loading}
+ if (gerbil.error || !gerbil.data) { + return ( +{gerbil.error ?? t.detail.notFound}
+ + {t.detail.back} + +{t.detail.genotypeNotSet}
+ )} + +{t.detail.tabPlaceholder}
+