FEAT-3: extract reusable BreedingResultView from GenetikPage (no behavior change)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
gerbil-manager-web/src/components/BreedingResultView.tsx
Normal file
93
gerbil-manager-web/src/components/BreedingResultView.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { de } from '../strings/de'
|
||||||
|
import type { BreedingResult } from '../genetics'
|
||||||
|
|
||||||
|
function warningText(code: string): string {
|
||||||
|
const map = de.genetics.warnings as Record<string, string>
|
||||||
|
return map[code] ?? code
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BreedingResultViewProps {
|
||||||
|
result: BreedingResult
|
||||||
|
/** Heading shown above the Farbschlag cards. Defaults to "Mögliche Nachkommen". */
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a GEN-1 breeding result: warnings (SCHECKE_LETHAL prominent),
|
||||||
|
* offspring grouped byFarbschlag (percent + exact fraction), and a collapsible
|
||||||
|
* per-genotype table. Shared by the Genetik page (FEAT-5) and the Wurf
|
||||||
|
* workspace (FEAT-3) where it shows the expected distribution for a pairing.
|
||||||
|
*/
|
||||||
|
export default function BreedingResultView({ result, title }: BreedingResultViewProps) {
|
||||||
|
const t = de.pages.genetik
|
||||||
|
const [showGenotypes, setShowGenotypes] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="results">
|
||||||
|
{result.warnings.length > 0 && (
|
||||||
|
<div className="warnings">
|
||||||
|
<h4>{t.warningsTitle}</h4>
|
||||||
|
{result.warnings.map((w) => (
|
||||||
|
<div key={w.code} className="alert alert--warning">
|
||||||
|
<span>{warningText(w.code)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h4>{title ?? t.resultsTitle}</h4>
|
||||||
|
{result.byFarbschlag.length === 0 ? (
|
||||||
|
<p className="muted">{t.noOffspring}</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ul className="farbschlag-cards">
|
||||||
|
{result.byFarbschlag.map((f) => (
|
||||||
|
<li key={f.farbschlag} className="farbschlag-card">
|
||||||
|
<span className="farbschlag-card__name">{f.farbschlag}</span>
|
||||||
|
<span className="farbschlag-card__prob">
|
||||||
|
{f.probability.percent}
|
||||||
|
<small> ({f.probability.text})</small>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="link-btn"
|
||||||
|
onClick={() => setShowGenotypes((s) => !s)}
|
||||||
|
aria-expanded={showGenotypes}
|
||||||
|
>
|
||||||
|
{showGenotypes ? t.hideGenotypes : t.showGenotypes}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showGenotypes && (
|
||||||
|
<table className="genotype-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t.genotypeLabel}</th>
|
||||||
|
<th>{t.genotypePreview}</th>
|
||||||
|
<th>{t.probability}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{result.offspring.map((o) => (
|
||||||
|
<tr key={o.genotype}>
|
||||||
|
<td>
|
||||||
|
<code>{o.genotype}</code>
|
||||||
|
</td>
|
||||||
|
<td>{o.farbschlag}</td>
|
||||||
|
<td>
|
||||||
|
{o.probability.percent} <small>({o.probability.text})</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,20 +6,15 @@ import { getGerbil } from '../api/gerbils'
|
|||||||
import { useApi } from '../hooks/useApi'
|
import { useApi } from '../hooks/useApi'
|
||||||
import ParentSelector, { type ParentValue } from '../components/ParentSelector'
|
import ParentSelector, { type ParentValue } from '../components/ParentSelector'
|
||||||
import AnimalPicker from '../components/AnimalPicker'
|
import AnimalPicker from '../components/AnimalPicker'
|
||||||
|
import BreedingResultView from '../components/BreedingResultView'
|
||||||
import { isValidGenotype } from '../format/genotypeText'
|
import { isValidGenotype } from '../format/genotypeText'
|
||||||
|
|
||||||
const EMPTY_PARENT: ParentValue = { genotype: '', animalName: null }
|
const EMPTY_PARENT: ParentValue = { genotype: '', animalName: null }
|
||||||
|
|
||||||
function warningText(code: string): string {
|
|
||||||
const map = de.genetics.warnings as Record<string, string>
|
|
||||||
return map[code] ?? code
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GenetikPage() {
|
export default function GenetikPage() {
|
||||||
const t = de.pages.genetik
|
const t = de.pages.genetik
|
||||||
const [father, setFather] = useState<ParentValue>(EMPTY_PARENT)
|
const [father, setFather] = useState<ParentValue>(EMPTY_PARENT)
|
||||||
const [mother, setMother] = useState<ParentValue>(EMPTY_PARENT)
|
const [mother, setMother] = useState<ParentValue>(EMPTY_PARENT)
|
||||||
const [showGenotypes, setShowGenotypes] = useState(false)
|
|
||||||
|
|
||||||
// Pre-fill a parent when arriving from an animal's detail page (?parent=<id>).
|
// Pre-fill a parent when arriving from an animal's detail page (?parent=<id>).
|
||||||
// Gender decides the side: female -> Mutter, otherwise Vater.
|
// Gender decides the side: female -> Mutter, otherwise Vater.
|
||||||
@@ -78,74 +73,7 @@ export default function GenetikPage() {
|
|||||||
|
|
||||||
{!bothValid && <p className="muted">{t.needBoth}</p>}
|
{!bothValid && <p className="muted">{t.needBoth}</p>}
|
||||||
|
|
||||||
{result && (
|
{result && <BreedingResultView result={result} />}
|
||||||
<div className="results">
|
|
||||||
{result.warnings.length > 0 && (
|
|
||||||
<div className="warnings">
|
|
||||||
<h3>{t.warningsTitle}</h3>
|
|
||||||
{result.warnings.map((w) => (
|
|
||||||
<div key={w.code} className="alert alert--warning">
|
|
||||||
<span>{warningText(w.code)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h3>{t.resultsTitle}</h3>
|
|
||||||
{result.byFarbschlag.length === 0 ? (
|
|
||||||
<p className="muted">{t.noOffspring}</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h4 className="muted">{t.byFarbschlag}</h4>
|
|
||||||
<ul className="farbschlag-cards">
|
|
||||||
{result.byFarbschlag.map((f) => (
|
|
||||||
<li key={f.farbschlag} className="farbschlag-card">
|
|
||||||
<span className="farbschlag-card__name">{f.farbschlag}</span>
|
|
||||||
<span className="farbschlag-card__prob">
|
|
||||||
{f.probability.percent}
|
|
||||||
<small> ({f.probability.text})</small>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="link-btn"
|
|
||||||
onClick={() => setShowGenotypes((s) => !s)}
|
|
||||||
aria-expanded={showGenotypes}
|
|
||||||
>
|
|
||||||
{showGenotypes ? t.hideGenotypes : t.showGenotypes}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showGenotypes && (
|
|
||||||
<table className="genotype-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{t.genotypeLabel}</th>
|
|
||||||
<th>{t.genotypePreview}</th>
|
|
||||||
<th>{t.probability}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{result.offspring.map((o) => (
|
|
||||||
<tr key={o.genotype}>
|
|
||||||
<td>
|
|
||||||
<code>{o.genotype}</code>
|
|
||||||
</td>
|
|
||||||
<td>{o.farbschlag}</td>
|
|
||||||
<td>
|
|
||||||
{o.probability.percent} <small>({o.probability.text})</small>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user