HELP-1: in-app German user guide (/hilfe, accordion, 9 sections)
HilfePage.tsx: accordion via <details>/<summary> (native, accessible, mobile-first); 9 sections in du-Form: Erste Schritte, Rennmaeuse, Becken, Wuerfe, Genetik, Stammbaum, Abgabe-Ablauf, Statistik, Datensicherung. All UI labels imported from de.ts (rename-safe); SCHECKE/Rex warnings verbatim from genetics.warnings. de.ts: de.nav.help + de.hilfe (title, subtitle, 9 section keys) AppShell.tsx: Hilfe (heart question mark) added to SECONDARY nav (Mehr-sheet on phone, sidebar on desktop) App.tsx: Route path=hilfe -> <HilfePage /> hilfe.css: accordion, hint boxes, warning colour; piggybacks on existing CSS vars e2e/hilfe.spec.ts: 3 smoke tests x 2 viewports = 6 new tests -- Hilfe reachable via nav -- First accordion section expands on click -- SCHECKE warning visible in Genetik section Full evidence: dotnet test 18/18, npm test 47/47, npm run build ok, npm run e2e 54/54 (48 existing + 6 new hilfe tests). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
36
gerbil-manager-web/e2e/hilfe.spec.ts
Normal file
36
gerbil-manager-web/e2e/hilfe.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* HELP-1: Hilfe-Seite — Smoke-Test (erreichbar + Accordion öffnet sich).
|
||||
* Kein Mock-API nötig (rein statische Seite).
|
||||
*/
|
||||
import { de, expect, gotoSection, test } from './fixtures'
|
||||
|
||||
const th = de.hilfe
|
||||
|
||||
test.describe('Hilfe', () => {
|
||||
test('Hilfe-Seite ist über die Navigation erreichbar', async ({ page }) => {
|
||||
await gotoSection(page, de.nav.help)
|
||||
await expect(page.getByRole('heading', { name: th.title })).toBeVisible()
|
||||
await expect(page.getByText(th.subtitle)).toBeVisible()
|
||||
})
|
||||
|
||||
test('Akkordeon-Abschnitt öffnet sich per Tippen', async ({ page }) => {
|
||||
await page.goto('/hilfe')
|
||||
const firstSummary = page.getByText(th.sections.ersteSchritte)
|
||||
await expect(firstSummary).toBeVisible()
|
||||
|
||||
// Abschnitt ist zunächst geschlossen — Inhalt nicht sichtbar
|
||||
const sectionBody = page.getByText('Empfohlene Reihenfolge')
|
||||
await expect(sectionBody).not.toBeVisible()
|
||||
|
||||
// Tippen öffnet den Abschnitt
|
||||
await firstSummary.click()
|
||||
await expect(sectionBody).toBeVisible()
|
||||
})
|
||||
|
||||
test('SCHECKE-Warnung ist im Genetik-Abschnitt enthalten', async ({ page }) => {
|
||||
await page.goto('/hilfe')
|
||||
// Öffne Genetik-Abschnitt
|
||||
await page.getByText(th.sections.genetik).click()
|
||||
await expect(page.getByText('Schecke × Schecke', { exact: false })).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,7 @@ import WurfFormPage from './pages/WurfFormPage'
|
||||
import NotFoundPage from './pages/NotFoundPage'
|
||||
import StammbaumPage from './pages/StammbaumPage'
|
||||
import StatistikPage from './pages/StatistikPage'
|
||||
import HilfePage from './pages/HilfePage'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -54,6 +55,7 @@ export default function App() {
|
||||
<Route path="genetik" element={<GenetikPage />} />
|
||||
<Route path="abgabe" element={<AbgabePage />} />
|
||||
<Route path="statistik" element={<StatistikPage />} />
|
||||
<Route path="hilfe" element={<HilfePage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -23,6 +23,7 @@ const SECONDARY: NavItem[] = [
|
||||
{ to: '/kontakte', label: de.nav.contacts, icon: '📇' },
|
||||
{ to: '/abgabe', label: de.nav.forSale, icon: '🏡' },
|
||||
{ to: '/statistik', label: de.nav.statistics, icon: '📊' },
|
||||
{ to: '/hilfe', label: de.nav.help, icon: '❓' },
|
||||
]
|
||||
|
||||
const linkClass = ({ isActive }: { isActive: boolean }) =>
|
||||
|
||||
196
gerbil-manager-web/src/pages/HilfePage.tsx
Normal file
196
gerbil-manager-web/src/pages/HilfePage.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* HELP-1: In-App-Anleitung für die Züchterin (nicht-technisch, du-Form).
|
||||
* Accordion via <details>/<summary> — kein JS-State nötig, barrierefrei, mobiltauglich.
|
||||
* Texte referenzieren nur Bezeichnungen aus de.ts (bleiben bei Umbenennung korrekt).
|
||||
*/
|
||||
import { de } from '../strings/de'
|
||||
import './hilfe.css'
|
||||
|
||||
const t = de.hilfe
|
||||
const nav = de.nav
|
||||
const g = de.pages.gerbils
|
||||
const l = de.pages.litters
|
||||
const b = de.pages.becken
|
||||
const st = de.pages.stammbaum
|
||||
const ge = de.pages.genetik
|
||||
const ab = de.pages.abgabe
|
||||
const stat = de.pages.statistik
|
||||
const scheckeWarning = de.genetics.warnings.SCHECKE_LETHAL
|
||||
const rexWarning = de.genetics.warnings.REX_SEMI_LETHAL
|
||||
|
||||
interface Section {
|
||||
id: string
|
||||
title: string
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
const sections: Section[] = [
|
||||
{
|
||||
id: 'erste-schritte',
|
||||
title: t.sections.ersteSchritte,
|
||||
content: (
|
||||
<>
|
||||
<p>Willkommen im <strong>{de.app.title}</strong>! Diese Anleitung führt dich durch die wichtigsten Bereiche.</p>
|
||||
<p>Die Navigation erreichst du unten (Smartphone) oder links (Laptop). Mit <strong>„{nav.more}"</strong> öffnest du alle weiteren Bereiche.</p>
|
||||
<p>Empfohlene Reihenfolge beim ersten Mal:</p>
|
||||
<ol>
|
||||
<li>Becken anlegen (wo leben deine Tiere?)</li>
|
||||
<li>Rennmäuse anlegen mit Foto</li>
|
||||
<li>Würfe eintragen</li>
|
||||
<li>Genetik erkunden</li>
|
||||
</ol>
|
||||
<p className="hilfe-hint">Tipp: Lege zuerst die Becken an — dann kannst du beim Erfassen der Tiere direkt das richtige Becken auswählen.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'tiere',
|
||||
title: t.sections.tiere,
|
||||
content: (
|
||||
<>
|
||||
<p>Unter <strong>„{nav.gerbils}"</strong> siehst du alle deine Rennmäuse. Mit <strong>„{g.newButton}"</strong> legst du ein neues Tier an.</p>
|
||||
<p>Pflichtfelder sind <strong>„{g.fields.name}"</strong> und <strong>„{g.fields.gender}"</strong>. Optional kannst du Geburtsdatum, Farbschlag, Genotyp, Becken und Herkunft eintragen.</p>
|
||||
<p>Auf der Detailseite findest du drei Tabs:</p>
|
||||
<ul>
|
||||
<li><strong>„{g.detail.tabs.photos}"</strong> — Fotos hochladen; das erste Foto wird als Profilfoto angezeigt.</li>
|
||||
<li><strong>„{g.detail.tabs.health}"</strong> — Tierarztbesuche, Behandlungen und Untersuchungen eintragen.</li>
|
||||
<li><strong>„{g.detail.tabs.weight}"</strong> — Gewicht in Gramm erfassen; nach zwei Einträgen erscheint eine Verlaufskurve.</li>
|
||||
</ul>
|
||||
<p>Mit den Filtern oben in der Liste kannst du nach Status, Geschlecht und Farbschlag filtern oder nach Name suchen.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'becken',
|
||||
title: t.sections.becken,
|
||||
content: (
|
||||
<>
|
||||
<p>Unter <strong>„{nav.enclosures}"</strong> verwaltest du deine Gehege. Mit <strong>„{b.newButton}"</strong> legst du ein neues Becken an (nur Name ist Pflicht).</p>
|
||||
<p>Die Detailseite zeigt dir die aktuellen <strong>„{b.occupancy.title}"</strong> — mit Links direkt zur Tierakte. So siehst du auf einen Blick, wer wo lebt.</p>
|
||||
<p>Ein Becken lässt sich nur löschen, wenn es leer ist — das schützt vor versehentlichem Datenverlust.</p>
|
||||
<p>Unter <strong>„{nav.contacts}"</strong> kannst du Züchter, Käufer und andere Kontakte anlegen. Diese lassen sich dann als Herkunft oder Abnehmer bei Tieren eintragen.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'wuerfe',
|
||||
title: t.sections.wuerfe,
|
||||
content: (
|
||||
<>
|
||||
<p>Unter <strong>„{nav.litters}"</strong> erfasst du Würfe. Mit <strong>„{l.newButton}"</strong> trägst du Vater, Mutter, Wurfdatum und Wurfstärke ein.</p>
|
||||
<p>Das <strong>„{l.fields.expectedGoHomeDate}"</strong> wird automatisch auf 35 Tage nach dem Wurfdatum vorgeschlagen — du kannst es anpassen.</p>
|
||||
<p>Auf der Wurf-Detailseite siehst du unter <strong>„{l.detail.registered}"</strong> die bereits erfassten Jungtiere. Mit <strong>„{l.detail.registerJuvenile}"</strong> legst du ein neues Tier an, das direkt mit diesem Wurf verknüpft ist.</p>
|
||||
<p>Im Tab <strong>„{l.tabs.pairs}"</strong> siehst du alle Verpaarungen aus deinen Würfen auf einen Blick — mit Anzahl Würfe und Datum des letzten Wurfes.</p>
|
||||
<p className="hilfe-hint">Tipp: Wenn du auf der Genetik-Seite eine Probeverpaarung berechnest, kannst du mit <strong>„Probeverpaarung"</strong> direkt zur Genetikseite springen und das Tierpaar vorauswählen.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'genetik',
|
||||
title: t.sections.genetik,
|
||||
content: (
|
||||
<>
|
||||
<p>Unter <strong>„{nav.genetics}"</strong> berechnest du, welche Farbschläge bei einer Verpaarung möglich sind.</p>
|
||||
<p>Wähle <strong>„{ge.father}"</strong> und <strong>„{ge.mother}"</strong> aus deinen Tieren oder gib die Genotypen manuell ein. Klicke dann auf <strong>„{ge.run}"</strong>.</p>
|
||||
<p>Das Ergebnis zeigt alle möglichen Farbschläge mit Wahrscheinlichkeit — du kannst zwischen Anzeige nach Farbschlag und nach Genotyp wechseln.</p>
|
||||
<p><strong>Wichtige Warnungen:</strong></p>
|
||||
<ul>
|
||||
<li className="hilfe-warning"><strong>Schecke × Schecke:</strong> {scheckeWarning}</li>
|
||||
<li className="hilfe-warning"><strong>Rex × Rex:</strong> {rexWarning}</li>
|
||||
</ul>
|
||||
<p>Diese Warnungen erscheinen automatisch, wenn die entsprechende Kombination erkannt wird. Bitte beachte sie beim Zuchtentscheid.</p>
|
||||
<p>Den Genotyp eines Tieres trägst du auf der Tier-Detailseite unter <strong>„{g.detail.masterData}"</strong> ein. Format: z. B. <code>Aa CC Dd EE GG Pp Spsp rere</code>. Unbekannte Stellen als <code>?</code>.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'stammbaum',
|
||||
title: t.sections.stammbaum,
|
||||
content: (
|
||||
<>
|
||||
<p>Auf der Tier-Detailseite öffnest du mit <strong>„{st.openButton}"</strong> den interaktiven Stammbaum.</p>
|
||||
<p><strong>Navigieren:</strong> {st.tapHint}</p>
|
||||
<p><strong>Zoomen:</strong> Mit den Schaltflächen <strong>„{st.zoomIn}"</strong> / <strong>„{st.zoomOut}"</strong> oder per Pinch-Geste auf dem Smartphone. <strong>„{st.zoomFit}"</strong> passt die Ansicht an den Bildschirm an.</p>
|
||||
<p><strong>Drucken / PDF:</strong> Mit <strong>„{st.print}"</strong> öffnest du die Druckansicht (Ahnentafel). Wähle im Druckdialog „Als PDF speichern", um eine Datei zu erzeugen.</p>
|
||||
<p>Der <strong>„{st.inbreeding.label}"</strong> wird automatisch für das angezeigte Tier berechnet und oben angezeigt.</p>
|
||||
<p className="hilfe-hint">Tipp: Je mehr Elterntiere und Würfe du erfasst hast, desto tiefer reicht der Stammbaum.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'abgabe',
|
||||
title: t.sections.abgabe,
|
||||
content: (
|
||||
<>
|
||||
<p>Der Abgabe-Ablauf läuft in mehreren Schritten:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Tier zur Abgabe stellen:</strong> Öffne die Tierakte und klicke auf <strong>„{ab.markAction}"</strong>. Das Tier wechselt in den Status <em>„{de.pages.gerbils.statusLabels.ForSale}"</em>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Gruppen bilden:</strong> Auf der <strong>„{nav.forSale}"</strong>-Seite werden Tiere nach Becken vorgruppiert. Du kannst Tiere zwischen Gruppen verschieben oder neue Gruppen anlegen.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Inserat erstellen:</strong> Fülle für jede Gruppe den emotionalen Einzeiler und den Charakter-Text aus.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Text kopieren & Fotos herunterladen:</strong> Mit <strong>„{ab.export.copyText}"</strong> kommt der fertige Inseratstext in die Zwischenablage. Mit <strong>„{ab.export.downloadPhotos}"</strong> lädst du alle Fotos der Gruppe als ZIP herunter.
|
||||
</li>
|
||||
<li>
|
||||
<strong>KI-Unterstützung (optional):</strong> Mit <strong>„{ab.ai.improve}"</strong> kann der Text automatisch verbessert werden — dazu ist ein API-Schlüssel nötig.
|
||||
</li>
|
||||
</ol>
|
||||
<p className="hilfe-hint">Tipp: Bearbeite jede Gruppe einzeln — Text und Fotos für eine Gruppe gleichzeitig zusammenstellen, dann weiter zur nächsten.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'statistik',
|
||||
title: t.sections.statistik,
|
||||
content: (
|
||||
<>
|
||||
<p>Unter <strong>„{nav.statistics}"</strong> findest du Auswertungen über deinen Bestand:</p>
|
||||
<ul>
|
||||
<li><strong>„{stat.littersPerYear}"</strong> — wie viele Würfe pro Jahr.</li>
|
||||
<li><strong>„{stat.avgLitterSize}"</strong> — durchschnittliche Wurfstärke pro Jahr.</li>
|
||||
<li><strong>„{stat.farbschlag}"</strong> — wie viele aktive Tiere welchen Farbschlag haben.</li>
|
||||
<li><strong>„{stat.population}"</strong> — wie sich dein Bestand über die Zeit entwickelt hat.</li>
|
||||
<li><strong>„{stat.losses}"</strong> — verstorbene Tiere nach Jahr.</li>
|
||||
</ul>
|
||||
<p>Die Statistiken werden automatisch aus deinen Daten berechnet. Je vollständiger deine Einträge (Geburtsdaten, Farbschläge, Wurfstärken), desto aussagekräftiger die Auswertungen.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'datensicherung',
|
||||
title: t.sections.datensicherung,
|
||||
content: (
|
||||
<>
|
||||
<p>Deine Daten liegen sicher auf Julian's Server zu Hause — kein Cloud-Konto nötig.</p>
|
||||
<p>Julian erstellt automatisch jeden Nacht um 3 Uhr ein Backup der Datenbank und aller Fotos. Bei einem Problem kann er den Stand eines beliebigen Tages wiederherstellen.</p>
|
||||
<p className="hilfe-hint">Wenn du versehentlich Daten gelöscht hast, wende dich an Julian. Er kann aus einem der letzten 7 Tages-Backups wiederherstellen.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export default function HilfePage() {
|
||||
return (
|
||||
<section className="page hilfe-page">
|
||||
<h2>{t.title}</h2>
|
||||
<p className="hilfe-subtitle">{t.subtitle}</p>
|
||||
|
||||
<div className="hilfe-accordion">
|
||||
{sections.map((s) => (
|
||||
<details key={s.id} className="hilfe-section">
|
||||
<summary className="hilfe-section-summary">
|
||||
<span className="hilfe-section-title">{s.title}</span>
|
||||
<span className="hilfe-section-chevron" aria-hidden="true">▾</span>
|
||||
</summary>
|
||||
<div className="hilfe-section-body">{s.content}</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
105
gerbil-manager-web/src/pages/hilfe.css
Normal file
105
gerbil-manager-web/src/pages/hilfe.css
Normal file
@@ -0,0 +1,105 @@
|
||||
.hilfe-page {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.hilfe-subtitle {
|
||||
color: var(--color-muted, #666);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Accordion */
|
||||
.hilfe-accordion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hilfe-section {
|
||||
border: 1px solid var(--color-border, #ddd);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface, #fff);
|
||||
}
|
||||
|
||||
.hilfe-section-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.875rem 1rem;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hilfe-section-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hilfe-section-summary:hover {
|
||||
background: var(--color-hover, #f5f5f5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.hilfe-section[open] > .hilfe-section-summary {
|
||||
border-bottom: 1px solid var(--color-border, #ddd);
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.hilfe-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.hilfe-section-chevron {
|
||||
font-size: 1.1rem;
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hilfe-section[open] .hilfe-section-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.hilfe-section-body {
|
||||
padding: 1rem 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.hilfe-section-body p {
|
||||
margin: 0 0 0.75rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hilfe-section-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hilfe-section-body ul,
|
||||
.hilfe-section-body ol {
|
||||
margin: 0.25rem 0 0.75rem 1.25rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hilfe-section-body li {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.hilfe-section-body code {
|
||||
font-family: monospace;
|
||||
background: var(--color-hover, #f5f5f5);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Hint boxes */
|
||||
.hilfe-hint {
|
||||
background: var(--color-info-bg, #eff6ff);
|
||||
border-left: 3px solid var(--color-info, #3b82f6);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0 4px 4px 0;
|
||||
font-size: 0.93rem;
|
||||
}
|
||||
|
||||
.hilfe-warning {
|
||||
color: var(--color-warning-text, #92400e);
|
||||
}
|
||||
@@ -26,6 +26,8 @@ export const de = {
|
||||
openMenu: 'Menü öffnen',
|
||||
closeMenu: 'Menü schließen',
|
||||
mainNavigation: 'Hauptnavigation',
|
||||
// HELP-1
|
||||
help: 'Hilfe',
|
||||
},
|
||||
pages: {
|
||||
home: {
|
||||
@@ -473,6 +475,22 @@ export const de = {
|
||||
},
|
||||
},
|
||||
},
|
||||
// ── HELP-1: In-App-Anleitung ──
|
||||
hilfe: {
|
||||
title: 'Hilfe & Anleitung',
|
||||
subtitle: 'Kurzanleitung für den Rennmaus-Manager',
|
||||
sections: {
|
||||
ersteSchritte: 'Erste Schritte',
|
||||
tiere: 'Rennmäuse anlegen & Fotos',
|
||||
becken: 'Becken',
|
||||
wuerfe: 'Würfe & Jungtiere',
|
||||
genetik: 'Probeverpaarung & Genetik',
|
||||
stammbaum: 'Stammbaum',
|
||||
abgabe: 'Abgabe-Ablauf',
|
||||
statistik: 'Statistik',
|
||||
datensicherung: 'Datensicherung',
|
||||
},
|
||||
},
|
||||
api: {
|
||||
errors: {
|
||||
network: 'Verbindung zum Server fehlgeschlagen. Bitte später erneut versuchen.',
|
||||
|
||||
Reference in New Issue
Block a user