From a74bc1c76f30345e6de49cfd853d59375bd2f55f Mon Sep 17 00:00:00 2001 From: Gulum Date: Sun, 7 Jun 2026 03:01:37 +0200 Subject: [PATCH] STAMMBAUM-AVATAR: Tier-Foto als Avatar in Pedigree-Karte, GerbilIcon-Fallback (profilePhotoUrl-Contract) --- gerbil-manager-web/e2e/mock-data.ts | 3 +- gerbil-manager-web/e2e/stammbaum.spec.ts | 17 +++++++++ gerbil-manager-web/src/api/types.ts | 2 ++ .../src/components/GerbilIcon.tsx | 35 +++++++++++++++++++ .../src/pages/StammbaumPage.tsx | 25 ++++++++++--- gerbil-manager-web/src/pages/stammbaum.css | 8 +++++ 6 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 gerbil-manager-web/src/components/GerbilIcon.tsx diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index 74544ef..f552cde 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -108,6 +108,7 @@ function gerbil( notes: null, // BESTAND-FILTER: default = eigener Bestand; einzelne externe Ahnen unten gesetzt. isResident: true, + profilePhotoUrl: null, } } @@ -120,7 +121,7 @@ export function seedDb(): MockDb { originContactId: 'con-meier', originBreeder: 'Clan-Kleine-Chaoten', }, - { ...gerbil('fridolin', 'Fridolin', 'male', '2023-05-01', 'w-fridolin', 'cv-schwarz', 'aa CC DD EE GG PP spsp rere'), enclosureId: 'enc-gross', originBreeder: 'Zoohandlung Meier' }, + { ...gerbil('fridolin', 'Fridolin', 'male', '2023-05-01', 'w-fridolin', 'cv-schwarz', 'aa CC DD EE GG PP spsp rere'), enclosureId: 'enc-gross', originBreeder: 'Zoohandlung Meier', profilePhotoUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' }, gerbil('luna', 'Luna', 'female', '2023-08-15', 'w-luna', 'cv-gold', 'AA CC DD EE GG pp spsp rere'), gerbil('balu', 'Balu', 'male', '2021-04-20', 'w-balu', 'cv-agouti'), gerbil('maja', 'Maja', 'female', '2021-06-11', null, 'cv-schwarz-schecke', 'aa CC DD EE GG PP Spsp rere'), diff --git a/gerbil-manager-web/e2e/stammbaum.spec.ts b/gerbil-manager-web/e2e/stammbaum.spec.ts index 2f55793..fb45e6b 100644 --- a/gerbil-manager-web/e2e/stammbaum.spec.ts +++ b/gerbil-manager-web/e2e/stammbaum.spec.ts @@ -137,3 +137,20 @@ test('Kein Würfe-Panel wenn Wurzeltier keine Würfe hat (STAMMBAUM-LITTERS)', a await expect(page.locator('.pedigree-card').first()).toBeVisible() await expect(page.locator('.stammbaum-litters-panel')).not.toBeVisible() }) + +test('Tier mit Foto zeigt Avatar-Bild, Tier ohne Foto zeigt Rennmaus-Icon (STAMMBAUM-AVATAR)', async ({ page }) => { + skipUnlessMock() + // Fridolin hat profilePhotoUrl (data URI) → sichtbar + // Krümel hat profilePhotoUrl=null → kein img, SVG-Icon sichtbar + await page.goto('/rennmaeuse/kruemel/stammbaum') + await expect(page.locator('.pedigree-card').first()).toBeVisible() + + // Fridolin ist Vater von Krümel → seine Karte enthält ein + const fridolinCard = page.locator('.pedigree-card').filter({ hasText: 'Fridolin' }) + await expect(fridolinCard.locator('img.pedigree-card__avatar')).toBeVisible() + + // Krümel (root) hat kein Foto → SVG-Icon im Foto-Slot + const rootCard = page.locator('.pedigree-card.pedigree-card--root') + await expect(rootCard.locator('img.pedigree-card__avatar')).not.toBeAttached() + await expect(rootCard.locator('svg[role="img"]')).toBeVisible() +}) diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts index e9b75f9..6e6b976 100644 --- a/gerbil-manager-web/src/api/types.ts +++ b/gerbil-manager-web/src/api/types.ts @@ -56,6 +56,8 @@ export interface Gerbil { isResident?: boolean /** FORM-FIELDS-1: true=gehörlos, false=hörend, null=unbekannt. */ isDeaf?: boolean | null + /** STAMMBAUM-AVATAR: URL des Profilfotos (erstes Foto nach sortOrder); null = kein Foto. API-relativ oder absolut. */ + profilePhotoUrl?: string | null } /** Payload for POST /gerbils. */ diff --git a/gerbil-manager-web/src/components/GerbilIcon.tsx b/gerbil-manager-web/src/components/GerbilIcon.tsx new file mode 100644 index 0000000..7209852 --- /dev/null +++ b/gerbil-manager-web/src/components/GerbilIcon.tsx @@ -0,0 +1,35 @@ +import type { CSSProperties } from 'react' + +interface Props { + size?: string | number + style?: CSSProperties + className?: string +} + +/** Reusable gerbil silhouette icon — upright sitting pose, long tail with tuft. fill=currentColor. */ +export default function GerbilIcon({ size = '1em', style, className }: Props) { + return ( + + {/* body — sitting upright */} + + {/* ear */} + + {/* hind paw */} + + {/* long curved tail with tuft */} + + {/* front paw */} + + + ) +} diff --git a/gerbil-manager-web/src/pages/StammbaumPage.tsx b/gerbil-manager-web/src/pages/StammbaumPage.tsx index 120051c..f3ce695 100644 --- a/gerbil-manager-web/src/pages/StammbaumPage.tsx +++ b/gerbil-manager-web/src/pages/StammbaumPage.tsx @@ -21,13 +21,14 @@ import { Link, useNavigate, useParams } from 'react-router-dom' import Tree from 'react-d3-tree' import type { CustomNodeElementProps, Point, RawNodeDatum } from 'react-d3-tree' import { de } from '../strings/de' -import { ApiError } from '../api/client' +import { API_BASE_URL, ApiError } from '../api/client' import { listLitters } from '../api/litters' import { listColorVarieties } from '../api/lookups' import { getInbreedingCoefficient } from '../api/pedigree' import type { Gender, Gerbil, Litter } from '../api/types' import { useApi } from '../hooks/useApi' import { formatDate, genderLabel } from '../format/labels' +import GerbilIcon from '../components/GerbilIcon' import { UNKNOWN_FARBSCHLAG, fromDisplayString, genotypeToFarbschlag, toDisplayString } from '../genetics' import { DEFAULT_GENERATIONS, @@ -380,6 +381,14 @@ function PedigreeCard({ const g = node.gerbil const chip = farbschlag ? chipColorFor(farbschlag) : null const year = g.dateOfBirth ? g.dateOfBirth.slice(0, 4) : null + const [imgError, setImgError] = useState(false) + useEffect(() => setImgError(false), [g.id]) + const photoUrl = + !imgError && g.profilePhotoUrl + ? /^(https?:|data:|\/\/)/.test(g.profilePhotoUrl) + ? g.profilePhotoUrl + : `${API_BASE_URL}${g.profilePhotoUrl}` + : null return (
- {/* Foto-Platzhalter — echte Fotos kommen mit FEAT-6. */} -