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:
2026-06-06 00:23:53 +02:00
parent 9c0a2fec56
commit e12588c338
2 changed files with 95 additions and 74 deletions

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

View File

@@ -6,20 +6,15 @@ import { getGerbil } from '../api/gerbils'
import { useApi } from '../hooks/useApi'
import ParentSelector, { type ParentValue } from '../components/ParentSelector'
import AnimalPicker from '../components/AnimalPicker'
import BreedingResultView from '../components/BreedingResultView'
import { isValidGenotype } from '../format/genotypeText'
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() {
const t = de.pages.genetik
const [father, setFather] = 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>).
// Gender decides the side: female -> Mutter, otherwise Vater.
@@ -78,74 +73,7 @@ export default function GenetikPage() {
{!bothValid && <p className="muted">{t.needBoth}</p>}
{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>
)}
{result && <BreedingResultView result={result} />}
</section>
)
}