FEAT-14b: trait vocabulary (de.character), Gerbil/sale-ad types, Charakterbogen component

This commit is contained in:
2026-06-06 09:08:46 +02:00
parent f858a02f50
commit 5b11780074
6 changed files with 157 additions and 0 deletions

View File

@@ -20,6 +20,9 @@ export interface SaleAdAnimal {
farbschlag: string | null
dateOfBirth: string | null
notes: string | null
/** FEAT-14: German trait LABELS (human-readable for the prompt), not keys. */
traits?: string[]
characterNote?: string | null
}
export interface SaleAdRequest {

View File

@@ -34,6 +34,12 @@ export interface Gerbil {
/** Compact GEN-1 genotype string, e.g. "Aa CC Dd EE GG Pp Spsp rere" (or "?"-wildcards). */
genotype: string | null
notes: string | null
/**
* FEAT-14: Charakterbogen. Selected trait KEYS (see de.character.traits).
* Optional until FEAT-14a backend exposes the fields; treat absent as [].
*/
characterTraits?: string[] | null
characterNote?: string | null
}
/** Payload for POST /gerbils. */
@@ -52,6 +58,8 @@ export interface CreateGerbil {
receiverContactId?: string | null
genotype?: string | null
notes?: string | null
characterTraits?: string[] | null
characterNote?: string | null
}
/** Payload for PUT /gerbils/{id} (all optional / partial update). */

View File

@@ -0,0 +1,62 @@
import { de } from '../strings/de'
import { ALL_TRAITS } from '../format/traits'
import './charakterbogen.css'
export interface CharakterbogenProps {
/** Selected trait keys. */
traits: string[]
note: string
onTraitsChange: (traits: string[]) => void
onNoteChange: (note: string) => void
}
/**
* FEAT-14: character sheet — a checkbox grid of traits + a free note.
* Controlled & reusable: rendered on the animal detail page (persisted) and in
* the Abgabe listing composer (feeds the AI sale-text). German labels from
* de.character.traits; the stored value is the trait KEY.
*/
export default function Charakterbogen({
traits,
note,
onTraitsChange,
onNoteChange,
}: CharakterbogenProps) {
const t = de.character
const selected = new Set(traits)
const toggle = (key: string) => {
const next = new Set(selected)
if (next.has(key)) next.delete(key)
else next.add(key)
// Preserve the vocabulary order for stable output.
onTraitsChange(ALL_TRAITS.filter((tr) => next.has(tr.key)).map((tr) => tr.key))
}
return (
<div className="charakterbogen">
<ul className="trait-grid">
{ALL_TRAITS.map((tr) => (
<li key={tr.key}>
<label className="trait-chip">
<input
type="checkbox"
checked={selected.has(tr.key)}
onChange={() => toggle(tr.key)}
/>
<span>{tr.label}</span>
</label>
</li>
))}
</ul>
<label className="field">
<span>{t.noteLabel}</span>
<textarea
value={note}
placeholder={t.notePlaceholder}
onChange={(e) => onNoteChange(e.target.value)}
/>
</label>
</div>
)
}

View File

@@ -0,0 +1,41 @@
/* FEAT-14 Charakterbogen — trait checkbox grid (mobile-first). */
.charakterbogen .trait-grid {
list-style: none;
margin: 0 0 0.75rem;
padding: 0;
display: grid;
grid-template-columns: 1fr;
gap: 0.3rem;
}
@media (min-width: 480px) {
.charakterbogen .trait-grid {
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 768px) {
.charakterbogen .trait-grid {
grid-template-columns: 1fr 1fr 1fr;
}
}
.trait-chip {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.6rem;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
background: var(--color-surface);
cursor: pointer;
min-height: 44px;
}
.trait-chip input {
width: 1.1rem;
height: 1.1rem;
min-height: 0;
flex: 0 0 auto;
}

View File

@@ -0,0 +1,15 @@
/** FEAT-14: map character trait KEYS (stored) <-> German LABELS (de.character.traits). */
import { de } from '../strings/de'
export const ALL_TRAITS = de.character.traits
const LABEL_BY_KEY = new Map<string, string>(de.character.traits.map((t) => [t.key, t.label]))
export function traitLabel(key: string): string {
return LABEL_BY_KEY.get(key) ?? key
}
/** Selected keys -> German labels (for display + the AI sale-text prompt). */
export function traitLabels(keys: readonly string[] | null | undefined): string[] {
return (keys ?? []).map(traitLabel)
}

View File

@@ -625,6 +625,34 @@ export const de = {
},
unknownFarbschlag: 'Unbekannter Farbschlag',
},
// ── FEAT-14 (Kevin): Charakterbogen — Eigenschaften + Notiz, speist den KI-Verkaufstext ──
// Traits: stabile KEYS (gespeichert) + deutsche LABELS (UI + KI-Prompt).
// Erweitern = eine Zeile in der Liste; Julian verfeinert die Auswahl.
character: {
sectionTitle: 'Charakter & Eigenschaften',
noteLabel: 'Notizen zum Charakter',
notePlaceholder: 'Ein paar Sätze zum Charakter dieses Tieres …',
save: 'Charakter speichern',
saved: 'Charakter gespeichert.',
none: 'Noch keine Eigenschaften ausgewählt.',
traits: [
{ key: 'zutraulich', label: 'zutraulich' },
{ key: 'handzahm', label: 'handzahm' },
{ key: 'neugierig', label: 'neugierig' },
{ key: 'aufgeschlossen', label: 'aufgeschlossen' },
{ key: 'ruhig', label: 'ruhig / ausgeglichen' },
{ key: 'lebhaft', label: 'lebhaft / aktiv' },
{ key: 'verschmust', label: 'verschmust' },
{ key: 'eigenstaendig', label: 'eigenständig' },
{ key: 'anfaengergeeignet', label: 'anfängergeeignet' },
{ key: 'futterfreudig', label: 'futterfreudig' },
{ key: 'buddelt', label: 'buddelt gern' },
{ key: 'klettert', label: 'klettert gern' },
{ key: 'laufrad', label: 'läuft gern im Laufrad' },
{ key: 'vertraeglich', label: 'gut verträglich' },
{ key: 'schreckhaft', label: 'schreckhaft' },
],
},
} as const
export type Strings = typeof de