FEAT-5: Probeverpaarung page — free genotype entry, client-side breed(), Farbschlag cards + warnings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,111 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { de } from '../strings/de'
|
||||
import { breed, fromDisplayString, type BreedingResult } from '../genetics'
|
||||
import ParentSelector, { type ParentValue } from '../components/ParentSelector'
|
||||
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)
|
||||
|
||||
const bothValid = isValidGenotype(father.genotype) && isValidGenotype(mother.genotype)
|
||||
|
||||
const result: BreedingResult | null = useMemo(() => {
|
||||
if (!bothValid) return null
|
||||
// breed() is pure & client-side; ParentSelector already validated both, so
|
||||
// fromDisplayString won't throw.
|
||||
return breed(fromDisplayString(father.genotype), fromDisplayString(mother.genotype))
|
||||
}, [bothValid, father.genotype, mother.genotype])
|
||||
|
||||
return (
|
||||
<section className="page">
|
||||
<h2>{t.title}</h2>
|
||||
<p className="muted">{t.subtitle}</p>
|
||||
|
||||
<div className="parents-grid">
|
||||
<ParentSelector label={t.father} value={father} onChange={setFather} />
|
||||
<ParentSelector label={t.mother} value={mother} onChange={setMother} />
|
||||
</div>
|
||||
|
||||
{!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>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user