GEN-2: surface Farbschlag images (FarbschlagImage component) in breeding results, animal detail, form picker
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
import type { BreedingResult } from '../genetics'
|
import type { BreedingResult } from '../genetics'
|
||||||
|
import FarbschlagImage from './FarbschlagImage'
|
||||||
|
|
||||||
function warningText(code: string): string {
|
function warningText(code: string): string {
|
||||||
const map = de.genetics.warnings as Record<string, string>
|
const map = de.genetics.warnings as Record<string, string>
|
||||||
@@ -44,6 +45,7 @@ export default function BreedingResultView({ result, title }: BreedingResultView
|
|||||||
<ul className="farbschlag-cards">
|
<ul className="farbschlag-cards">
|
||||||
{result.byFarbschlag.map((f) => (
|
{result.byFarbschlag.map((f) => (
|
||||||
<li key={f.farbschlag} className="farbschlag-card">
|
<li key={f.farbschlag} className="farbschlag-card">
|
||||||
|
<FarbschlagImage name={f.farbschlag} />
|
||||||
<span className="farbschlag-card__name">{f.farbschlag}</span>
|
<span className="farbschlag-card__name">{f.farbschlag}</span>
|
||||||
<span className="farbschlag-card__prob">
|
<span className="farbschlag-card__prob">
|
||||||
{f.probability.percent}
|
{f.probability.percent}
|
||||||
|
|||||||
20
gerbil-manager-web/src/components/FarbschlagImage.css
Normal file
20
gerbil-manager-web/src/components/FarbschlagImage.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.farbschlag-thumb {
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-surface);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* With a leading thumbnail, let the name grow so the probability stays right. */
|
||||||
|
.farbschlag-card .farbschlag-card__name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline image + name (e.g. on the animal detail Stammdaten row). */
|
||||||
|
.farbschlag-value {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
25
gerbil-manager-web/src/components/FarbschlagImage.tsx
Normal file
25
gerbil-manager-web/src/components/FarbschlagImage.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { farbschlagImageUrl, findVariety } from '../genetics'
|
||||||
|
import './FarbschlagImage.css'
|
||||||
|
|
||||||
|
export interface FarbschlagImageProps {
|
||||||
|
/** Variety name; may include " Schecke"/" Rex" modifiers (stripped for lookup). */
|
||||||
|
name: string
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Thumbnail for a Farbschlag, or null if the variety has no catalog image. */
|
||||||
|
export default function FarbschlagImage({ name, size = 40 }: FarbschlagImageProps) {
|
||||||
|
const baseName = name.replace(/\s+(Schecke|Rex)\b/g, '').trim()
|
||||||
|
const url = farbschlagImageUrl(findVariety(baseName)?.image ?? findVariety(name)?.image)
|
||||||
|
if (!url) return null
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className="farbschlag-thumb"
|
||||||
|
src={url}
|
||||||
|
alt={name}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { listColorVarieties, listContacts, listEnclosures, listLitters } from '.
|
|||||||
import { useApi } from '../hooks/useApi'
|
import { useApi } from '../hooks/useApi'
|
||||||
import { formatDate, genderLabel, statusLabel } from '../format/labels'
|
import { formatDate, genderLabel, statusLabel } from '../format/labels'
|
||||||
import { fromDisplayString, genotypeToFarbschlag, toDisplayString } from '../genetics'
|
import { fromDisplayString, genotypeToFarbschlag, toDisplayString } from '../genetics'
|
||||||
|
import FarbschlagImage from '../components/FarbschlagImage'
|
||||||
// FEAT-6 (Oscar): Tab-Inhalte + Profilfoto
|
// FEAT-6 (Oscar): Tab-Inhalte + Profilfoto
|
||||||
import GerbilHealthTab from '../components/GerbilHealthTab'
|
import GerbilHealthTab from '../components/GerbilHealthTab'
|
||||||
import GerbilPhotosTab from '../components/GerbilPhotosTab'
|
import GerbilPhotosTab from '../components/GerbilPhotosTab'
|
||||||
@@ -118,7 +119,19 @@ export default function GerbilDetailPage() {
|
|||||||
{g.status === 'GivenAway' && (
|
{g.status === 'GivenAway' && (
|
||||||
<Row label={t.fields.goHomeDate} value={formatDate(g.goHomeDate)} />
|
<Row label={t.fields.goHomeDate} value={formatDate(g.goHomeDate)} />
|
||||||
)}
|
)}
|
||||||
<Row label={t.fields.colorVariety} value={lookup(colorName, g.colorVarietyId)} />
|
<Row
|
||||||
|
label={t.fields.colorVariety}
|
||||||
|
value={
|
||||||
|
storedColorName ? (
|
||||||
|
<span className="farbschlag-value">
|
||||||
|
<FarbschlagImage name={storedColorName} size={32} />
|
||||||
|
{storedColorName}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'—'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Row label={t.fields.enclosure} value={lookup(enclosureName, g.enclosureId)} />
|
<Row label={t.fields.enclosure} value={lookup(enclosureName, g.enclosureId)} />
|
||||||
<Row label={t.fields.litter} value={lookup(litterName, g.litterId)} />
|
<Row label={t.fields.litter} value={lookup(litterName, g.litterId)} />
|
||||||
<Row label={t.fields.origin} value={lookup(contactName, g.originContactId)} />
|
<Row label={t.fields.origin} value={lookup(contactName, g.originContactId)} />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { GENDERS, GERBIL_STATUSES, type CreateGerbil, type Gender, type GerbilSt
|
|||||||
import { useApi, useMutation } from '../hooks/useApi'
|
import { useApi, useMutation } from '../hooks/useApi'
|
||||||
import { genderLabel, statusLabel } from '../format/labels'
|
import { genderLabel, statusLabel } from '../format/labels'
|
||||||
import { fromDisplayString } from '../genetics'
|
import { fromDisplayString } from '../genetics'
|
||||||
|
import FarbschlagImage from '../components/FarbschlagImage'
|
||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
name: string
|
name: string
|
||||||
@@ -173,6 +174,9 @@ export default function GerbilFormPage() {
|
|||||||
|
|
||||||
if (isEdit && existing.loading) return <p className="muted">{de.common.loading}</p>
|
if (isEdit && existing.loading) return <p className="muted">{de.common.loading}</p>
|
||||||
|
|
||||||
|
const selectedColorName =
|
||||||
|
(colorVarieties.data ?? []).find((cv) => cv.id === form.colorVarietyId)?.name ?? null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page">
|
<section className="page">
|
||||||
<h2>{isEdit ? t.form.editTitle : t.form.createTitle}</h2>
|
<h2>{isEdit ? t.form.editTitle : t.form.createTitle}</h2>
|
||||||
@@ -265,7 +269,11 @@ export default function GerbilFormPage() {
|
|||||||
|
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>{t.fields.colorVariety}</span>
|
<span>{t.fields.colorVariety}</span>
|
||||||
<select value={form.colorVarietyId} onChange={(e) => set('colorVarietyId', e.target.value)}>
|
<span className="farbschlag-value">
|
||||||
|
<select
|
||||||
|
value={form.colorVarietyId}
|
||||||
|
onChange={(e) => set('colorVarietyId', e.target.value)}
|
||||||
|
>
|
||||||
<option value="">{t.form.none}</option>
|
<option value="">{t.form.none}</option>
|
||||||
{(colorVarieties.data ?? []).map((cv) => (
|
{(colorVarieties.data ?? []).map((cv) => (
|
||||||
<option key={cv.id} value={cv.id}>
|
<option key={cv.id} value={cv.id}>
|
||||||
@@ -273,6 +281,8 @@ export default function GerbilFormPage() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{selectedColorName && <FarbschlagImage name={selectedColorName} size={36} />}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="field">
|
<label className="field">
|
||||||
|
|||||||
Reference in New Issue
Block a user