2026-04-24 14:41:52 +02:00
# Frontend Routing & Navigation Specification
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
**Version:** 1.3
**Datum:** 2026-05-20
2026-04-24 14:41:52 +02:00
**Status:** DRAFT - Awaiting Review
2026-04-28 16:18:25 +02:00
**Autor:** Claude Code
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
**Änderungen v1.3:** Übungsformular Tab-Navigation unter `/exercises/:id/edit` (Stammdaten … Medien & Mehr); Freigabelevel als UI-Begriff
2026-05-05 08:30:48 +02:00
**Änderungen v1.2:** Übersicht **Übungen** : Tabs Liste \| Progressionsgraphen auf `/exercises` ; Progressions-Editor ohne neue Routen (Panel + Formularblock unter `/exercises/:id/edit` )
2026-04-28 16:18:25 +02:00
**Änderungen v1.1:** Übungsvarianten-Bearbeitung nur unter `/exercises/:id/edit` (keine VariantFormPage-Routen)
2026-04-24 14:41:52 +02:00
---
## 1. Route-Struktur
### 1.1 Route-Übersicht
```
2026-05-05 08:30:48 +02:00
/exercises → ExercisesListPage — Tabs: **Liste** \| **Progressionsgraphen** (`ExerciseProgressionGraphPanel`)
2026-04-24 14:41:52 +02:00
/exercises/new → ExerciseFormPage (Create)
/exercises/{id} → ExerciseDetailPage (Accordion-Layout)
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
/exercises/{id}/edit → ExerciseFormPage (Edit: Registerkarten + Varianten inline + Progressionsgraph)
2026-04-24 14:41:52 +02:00
/exercise-blocks → ExerciseBlocksListPage (Meine Blocks)
/exercise-blocks/new → ExerciseBlockFormPage (Create)
/exercise-blocks/{id} → ExerciseBlockDetailPage (mit BlockEditor)
/exercise-blocks/{id}/edit → ExerciseBlockFormPage (Edit)
/admin/exercises/catalog → AdminCatalogPage (Fokusbereiche, Stile, Zielgruppen, Altersgruppen)
/admin/exercises/skills → AdminSkillsPage (Fähigkeiten-Matrix)
/admin/exercises/methods → AdminMethodsPage (Methoden-Katalog)
```
**URL-Parameter-Konventionen:**
- IDs immer numerisch: `/exercises/42` (nicht `/exercises/uuid` )
- Query-Parameter für Filter: `/exercises?focus_area=1&visibility=club`
- Pagination: `/exercises?limit=50&offset=100`
- Sortierung: `/exercises?sort=created_at&order=desc`
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
### 1.2 Übungsformular – Registerkarten (`/exercises/new`, `/exercises/:id/edit`)
**Implementierung:** `ExerciseFormPageRoot.jsx` + `ExerciseFormLayout.jsx` (`ExerciseFormTabBar`, `ExerciseFormPanel` ).
| Tab-ID | Label | Verfügbarkeit |
|--------|-------|---------------|
| `stammdaten` | Stammdaten | immer |
| `anleitung` | Anleitung | immer |
| `einordnung` | Einordnung | immer |
| `kombination` | Kombination | nur `exercise_kind=combination` |
| `varianten` | Varianten | Edit-Modus; nicht bei Kombination; disabled bei Neuanlage |
| `medien` | Medien & Mehr | Edit-Modus; disabled bei Neuanlage |
**UX-Regeln:**
- Nur ein Panel sichtbar (`activeFormTab`); Navigation über `PageSectionNav` .
- **Freigabelevel** (Feld `visibility` ) in Stammdaten — Konstante `EXERCISE_VISIBILITY_FIELD_LABEL` .
- Varianten-Änderungen werden mit **Speichern** in der Aktionsleiste persistiert (`persistPendingVariantChanges`); Button „Variante anlegen“ optional sofort.
- Kein URL-Hash pro Tab (Tab-State nur lokal).
2026-04-24 14:41:52 +02:00
---
## 2. Navigation-Patterns
### 2.1 Haupt-Navigation (Desktop + Tablet)
**Primäre Nav (Top-Level):**
```
┌─────────────────────────────────────────┐
│ Shinkan │ Übungen │ Planung │ Admin │
└─────────────────────────────────────────┘
```
**Übungen-Submenu (Dropdown):**
```
Übungen
├─ Alle Übungen → /exercises
├─ Neue Übung → /exercises/new
├─ Meine Blocks → /exercise-blocks
├─ Neuer Block → /exercise-blocks/new
└─ Katalog verwalten → /admin/exercises/catalog (nur Admin)
```
### 2.2 Mobile Navigation (Hamburger-Menu)
```
☰ Menu
├─ Übungen
│ ├─ Alle anzeigen
│ ├─ Neue Übung
│ └─ Filter & Suche
├─ Planung
└─ Admin
└─ Katalog
```
### 2.3 Breadcrumbs (Detail/Edit-Seiten)
**Pattern:**
```
2026-04-28 16:18:25 +02:00
Home → Übungen → [Übungsname] → Bearbeiten (Varianten im gleichen Formular)
2026-04-24 14:41:52 +02:00
```
**Implementation:**
- Breadcrumbs dynamisch aus Route-Parametern generiert
- Letztes Element (aktuelle Seite) nicht klickbar
- Max. 4 Ebenen, dann `...` für gekürzte Pfade
---
## 3. State Management
### 3.1 URL-State (Query-Parameter)
**Filter-State in URL persistieren:**
```javascript
// ExercisesListPage.jsx
const [filters, setFilters] = useState({
focus_area: parseInt(searchParams.get('focus_area')) || null,
visibility: searchParams.get('visibility') || null,
status: searchParams.get('status') || null,
search: searchParams.get('search') || '',
limit: parseInt(searchParams.get('limit')) || 50,
offset: parseInt(searchParams.get('offset')) || 0,
})
// Bei Filter-Änderung → URL aktualisieren
const updateFilters = (newFilters) => {
const params = new URLSearchParams()
Object.entries(newFilters).forEach(([key, val]) => {
if (val) params.set(key, val)
})
navigate(`/exercises?${params.toString()}`)
}
```
**Vorteile:**
- Sharable URLs (Filter-Zustand übertragbar)
- Browser-Back funktioniert korrekt
- Keine verlorenen Filter beim Reload
### 3.2 Local State (React useState)
**Form-State (nicht in URL):**
```javascript
// ExerciseFormPage.jsx
const [formData, setFormData] = useState({
title: '',
summary: '',
goal: '',
execution: '',
// ... weitere Felder
})
```
**UI-State (nicht in URL):**
```javascript
// ExerciseDetailPage.jsx
const [expandedSections, setExpandedSections] = useState({
basics: true,
execution: false,
variants: false,
media: false,
})
```
### 3.3 Context (Global State)
**Katalog-Daten (einmalig laden, global verfügbar):**
```javascript
// CatalogContext.jsx
const CatalogContext = createContext()
export function CatalogProvider({ children }) {
const [focusAreas, setFocusAreas] = useState([])
const [trainingStyles, setTrainingStyles] = useState([])
const [targetGroups, setTargetGroups] = useState([])
const [ageGroups, setAgeGroups] = useState([])
const [skills, setSkills] = useState([])
useEffect(() => {
// Kataloge einmalig beim App-Start laden
Promise.all([
api.getFocusAreas(),
api.getTrainingStyles(),
api.getTargetGroups(),
api.getAgeGroups(),
api.getSkills(),
]).then(([fa, ts, tg, ag, sk]) => {
setFocusAreas(fa)
setTrainingStyles(ts)
setTargetGroups(tg)
setAgeGroups(ag)
setSkills(sk)
})
}, [])
return (
< CatalogContext.Provider value = {{
focusAreas, trainingStyles, targetGroups, ageGroups, skills
}}>
{children}
< / CatalogContext.Provider >
)
}
export const useCatalog = () => useContext(CatalogContext)
```
**Usage in Komponenten:**
```javascript
// ExerciseFormPage.jsx
const { focusAreas, trainingStyles } = useCatalog()
```
---
## 4. Deep Linking
### 4.1 Detail-Seite mit Accordion-State
**Problem:** Link teilen → Empfänger soll direkt zur "Varianten"-Sektion springen
**Lösung:** Hash-Fragment in URL:
```
/exercises/42#variants
/exercises/42#media
```
**Implementation:**
```javascript
// ExerciseDetailPage.jsx
useEffect(() => {
const hash = window.location.hash.replace('#', '')
if (hash & & expandedSections[hash] !== undefined) {
setExpandedSections(prev => ({ ...prev, [hash]: true }))
// Scroll zur Sektion
setTimeout(() => {
document.getElementById(hash)?.scrollIntoView({ behavior: 'smooth' })
}, 100)
}
}, [])
```
### 4.2 Externe Verknüpfung aus Trainingsplan
**Szenario:** Trainingsplan-Editor soll Übung verlinken
**URL-Format:**
```
/exercises/42?from=training_plan& plan_id=7
```
**Navigation-Logik:**
```javascript
// ExerciseDetailPage.jsx
const searchParams = useSearchParams()
const returnUrl = searchParams.get('from') === 'training_plan'
? `/training-plans/${searchParams.get('plan_id')}`
: '/exercises'
// "Zurück"-Button zeigt entsprechendes Ziel
< button onClick = {() = > navigate(returnUrl)}>
← {returnUrl.includes('training') ? 'Zum Trainingsplan' : 'Zur Übersicht'}
< / button >
```
---
## 5. Navigation Guards
### 5.1 Unsaved Changes Warning
**Pattern:**
```javascript
// ExerciseFormPage.jsx
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// Bei Form-Änderung
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }))
setHasUnsavedChanges(true)
}
// Browser-Warning bei Reload/Close
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasUnsavedChanges) {
e.preventDefault()
e.returnValue = ''
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [hasUnsavedChanges])
// React-Router Warning bei Navigation
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
hasUnsavedChanges & &
currentLocation.pathname !== nextLocation.pathname
)
useEffect(() => {
if (blocker.state === 'blocked') {
const confirmed = window.confirm(
'Du hast ungespeicherte Änderungen. Seite wirklich verlassen?'
)
if (confirmed) {
blocker.proceed()
} else {
blocker.reset()
}
}
}, [blocker])
```
### 5.2 Permission-Check (Admin-Routes)
**Pattern:**
```javascript
// ProtectedRoute.jsx
function ProtectedRoute({ children, requiredRole = 'admin' }) {
const { profile } = useAuth()
if (!profile) {
return < Navigate to = "/login" / >
}
if (profile.role !== requiredRole) {
return < Navigate to = "/exercises" replace / >
}
return children
}
// App.jsx
< Route path = "/admin/exercises/*" element = {
< ProtectedRoute requiredRole = "admin" >
< AdminExerciseRoutes / >
< / ProtectedRoute >
} />
```
---
## 6. Performance-Optimierung
### 6.1 Lazy Loading von Routen
**Pattern:**
```javascript
// App.jsx
const ExercisesListPage = lazy(() => import('./pages/ExercisesListPage'))
const ExerciseDetailPage = lazy(() => import('./pages/ExerciseDetailPage'))
const ExerciseFormPage = lazy(() => import('./pages/ExerciseFormPage'))
function App() {
return (
< Suspense fallback = {<LoadingSpinner / > }>
< Routes >
< Route path = "/exercises" element = {<ExercisesListPage / > } />
< Route path = "/exercises/:id" element = {<ExerciseDetailPage / > } />
< Route path = "/exercises/:id/edit" element = {<ExerciseFormPage / > } />
< / Routes >
< / Suspense >
)
}
```
**Vorteil:** Initial Bundle kleiner, Routen nur bei Bedarf nachgeladen
### 6.2 Prefetching von Detail-Daten
**Pattern:**
```javascript
// ExercisesListPage.jsx - Hover-Prefetch
const handleCardHover = (exerciseId) => {
// Prefetch Detail-Daten beim Hover
queryClient.prefetchQuery(['exercise', exerciseId], () =>
api.getExercise(exerciseId)
)
}
< ExerciseCard
exercise={exercise}
onMouseEnter={() => handleCardHover(exercise.id)}
onClick={() => navigate(`/exercises/${exercise.id}`)}
/>
```
**Vorteil:** Detail-Seite lädt sofort, da Daten bereits im Cache
---
## 7. Error-Handling
### 7.1 404 - Exercise Not Found
**Pattern:**
```javascript
// ExerciseDetailPage.jsx
const { data: exercise, error, isLoading } = useQuery(
['exercise', exerciseId],
() => api.getExercise(exerciseId)
)
if (error?.response?.status === 404) {
return (
< NotFoundPage
title="Übung nicht gefunden"
message="Diese Übung existiert nicht oder wurde gelöscht."
backLink="/exercises"
backLabel="Zur Übersicht"
/>
)
}
```
### 7.2 403 - No Permission
**Pattern:**
```javascript
if (error?.response?.status === 403) {
return (
< ForbiddenPage
title="Keine Berechtigung"
message="Du hast keinen Zugriff auf diese Übung."
backLink="/exercises"
/>
)
}
```
### 7.3 Network Error
**Pattern:**
```javascript
if (error?.message === 'Network Error') {
return (
< ErrorPage
title="Verbindungsfehler"
message="Server nicht erreichbar. Bitte prüfe deine Internetverbindung."
retryAction={() => refetch()}
/>
)
}
```
---
## 8. Mobile-Spezifische Navigation
### 8.1 Bottom Navigation (Mobile)
**Pattern:**
```
┌─────────────────────────────────────┐
│ │
│ (Content Area) │
│ │
├─────────────────────────────────────┤
│ 🏠 Home │ 📋 Übungen │ 📅 Plan │
└─────────────────────────────────────┘
```
**Implementation:**
```javascript
// MobileBottomNav.jsx
< nav className = "mobile-bottom-nav" >
< NavLink to = "/" className = {({ isActive } ) = > isActive ? 'active' : ''}>
< Icon name = "home" / >
< span > Home< / span >
< / NavLink >
< NavLink to = "/exercises" >
< Icon name = "list" / >
< span > Übungen< / span >
< / NavLink >
< NavLink to = "/training-plans" >
< Icon name = "calendar" / >
< span > Pläne< / span >
< / NavLink >
< / nav >
```
**CSS:**
```css
.mobile-bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: var(--surface);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-around;
z-index: 100;
}
@media (min-width: 768px) {
.mobile-bottom-nav {
display: none; /* Desktop nutzt Top-Nav */
}
}
```
### 8.2 Swipe-Navigation (Detail-Seite)
**Pattern:** Swipe left/right zwischen Übungen (gleicher Filter)
**Implementation:**
```javascript
// ExerciseDetailPage.jsx
const { exercises } = useExercisesList() // aus Context/Query
const currentIndex = exercises.findIndex(e => e.id === parseInt(exerciseId))
const swipeHandlers = useSwipeable({
onSwipedLeft: () => {
const nextExercise = exercises[currentIndex + 1]
if (nextExercise) navigate(`/exercises/${nextExercise.id}`)
},
onSwipedRight: () => {
const prevExercise = exercises[currentIndex - 1]
if (prevExercise) navigate(`/exercises/${prevExercise.id}`)
},
preventDefaultTouchmoveEvent: true,
trackMouse: false,
})
< div { . . . swipeHandlers } >
{/* Exercise Detail Content */}
< / div >
```
---
## 9. Accessibility
### 9.1 Skip Links
**Pattern:**
```javascript
// App.jsx
< a href = "#main-content" className = "skip-link" >
Zum Hauptinhalt springen
< / a >
< main id = "main-content" >
{/* Content */}
< / main >
```
**CSS:**
```css
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--accent);
color: white;
padding: 8px;
z-index: 1000;
}
.skip-link:focus {
top: 0;
}
```
### 9.2 Focus Management
**Pattern:** Bei Navigation → Focus auf Hauptüberschrift setzen
```javascript
// ExerciseDetailPage.jsx
const titleRef = useRef(null)
useEffect(() => {
titleRef.current?.focus()
}, [exerciseId])
< h1 ref = {titleRef} tabIndex = {-1} >
{exercise.title}
< / h1 >
```
### 9.3 ARIA Live-Region für Filter
**Pattern:**
```javascript
// ExercisesListPage.jsx
< div aria-live = "polite" aria-atomic = "true" className = "sr-only" >
{exercises.length} Übungen gefunden
< / div >
```
---
## 10. Testing-Strategie
### 10.1 Route-Tests (Playwright)
```javascript
// exercises.routing.spec.js
test('Navigation zu Detail-Seite funktioniert', async ({ page }) => {
await page.goto('/exercises')
const firstCard = page.locator('.exercise-card').first()
await firstCard.click()
await expect(page).toHaveURL(/\/exercises\/\d+/)
await expect(page.locator('h1')).toBeVisible()
})
test('Filter-State bleibt in URL erhalten', async ({ page }) => {
await page.goto('/exercises?focus_area=1')
await page.reload()
const url = new URL(page.url())
expect(url.searchParams.get('focus_area')).toBe('1')
})
```
### 10.2 Navigation-Guard-Tests
```javascript
test('Unsaved Changes Warning', async ({ page }) => {
await page.goto('/exercises/new')
await page.fill('[name="title"]', 'Test Übung')
page.on('dialog', dialog => {
expect(dialog.message()).toContain('ungespeicherte')
dialog.accept()
})
await page.goto('/exercises')
})
```
---
## 11. Route-Konfiguration (React Router)
### 11.1 Vollständige Route-Definitionen
```javascript
// App.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
function App() {
return (
< BrowserRouter >
< CatalogProvider >
< Routes >
{/* Exercises Routes */}
< Route path = "/exercises" >
< Route index element = {<ExercisesListPage / > } />
< Route path = "new" element = {<ExerciseFormPage / > } />
< Route path = ":id" element = {<ExerciseDetailPage / > } />
< Route path = ":id/edit" element = {<ExerciseFormPage / > } />
2026-04-28 16:18:25 +02:00
{/* Varianten: Bearbeitung inline in ExerciseFormPage, keine eigenen Routen */}
2026-04-24 14:41:52 +02:00
< / Route >
{/* Exercise Blocks Routes */}
< Route path = "/exercise-blocks" >
< Route index element = {<ExerciseBlocksListPage / > } />
< Route path = "new" element = {<ExerciseBlockFormPage / > } />
< Route path = ":id" element = {<ExerciseBlockDetailPage / > } />
< Route path = ":id/edit" element = {<ExerciseBlockFormPage / > } />
< / Route >
{/* Admin Routes */}
< Route path = "/admin/exercises" element = {
< ProtectedRoute requiredRole = "admin" >
< Outlet / >
< / ProtectedRoute >
}>
< Route path = "catalog" element = {<AdminCatalogPage / > } />
< Route path = "skills" element = {<AdminSkillsPage / > } />
< Route path = "methods" element = {<AdminMethodsPage / > } />
< / Route >
{/* Redirects */}
< Route path = "/" element = {<Navigate to = "/exercises" replace / > } />
< Route path = "*" element = {<NotFoundPage / > } />
< / Routes >
< / CatalogProvider >
< / BrowserRouter >
)
}
```
---
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
**Version:** 1.3
**Letzte Änderung:** 2026-05-20
2026-05-05 08:30:48 +02:00
**Status:** REVIEWED - Pending Implementation
Enhance Exercise Management and AI Integration
- Updated the exercise form to include a tabbed navigation structure, improving user experience with sections for Stammdaten, Anleitung, Einordnung, Varianten, and Medien & Mehr.
- Introduced the concept of **Freigabelevel** (visibility level) in the UI, replacing previous terminology for clarity and consistency across components.
- Implemented new AI endpoints for exercise suggestions and regeneration, allowing for dynamic content generation without direct database writes.
- Removed the legacy `is_primary` flag from exercise skills in the UI, ensuring that intensity levels (`niedrig`, `mittel`, `hoch`) are the primary focus for skill management.
- Enhanced the variant management process with improved saving mechanisms and UI updates to reflect changes more intuitively.
2026-05-22 07:52:31 +02:00
**Review-Änderungen:** Formular-Registerkarten; Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher)