Some checks failed
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 1s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Has been cancelled
- 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.
700 lines
18 KiB
Markdown
700 lines
18 KiB
Markdown
# Frontend Routing & Navigation Specification
|
||
|
||
**Version:** 1.3
|
||
**Datum:** 2026-05-20
|
||
**Status:** DRAFT - Awaiting Review
|
||
**Autor:** Claude Code
|
||
**Änderungen v1.3:** Übungsformular Tab-Navigation unter `/exercises/:id/edit` (Stammdaten … Medien & Mehr); Freigabelevel als UI-Begriff
|
||
**Änderungen v1.2:** Übersicht **Übungen**: Tabs Liste \| Progressionsgraphen auf `/exercises`; Progressions-Editor ohne neue Routen (Panel + Formularblock unter `/exercises/:id/edit`)
|
||
**Änderungen v1.1:** Übungsvarianten-Bearbeitung nur unter `/exercises/:id/edit` (keine VariantFormPage-Routen)
|
||
|
||
---
|
||
|
||
## 1. Route-Struktur
|
||
|
||
### 1.1 Route-Übersicht
|
||
|
||
```
|
||
/exercises → ExercisesListPage — Tabs: **Liste** \| **Progressionsgraphen** (`ExerciseProgressionGraphPanel`)
|
||
/exercises/new → ExerciseFormPage (Create)
|
||
/exercises/{id} → ExerciseDetailPage (Accordion-Layout)
|
||
/exercises/{id}/edit → ExerciseFormPage (Edit: Registerkarten + Varianten inline + Progressionsgraph)
|
||
|
||
/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`
|
||
|
||
### 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).
|
||
|
||
---
|
||
|
||
## 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:**
|
||
```
|
||
Home → Übungen → [Übungsname] → Bearbeiten (Varianten im gleichen Formular)
|
||
```
|
||
|
||
**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 />} />
|
||
{/* Varianten: Bearbeitung inline in ExerciseFormPage, keine eigenen Routen */}
|
||
</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>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**Version:** 1.3
|
||
**Letzte Änderung:** 2026-05-20
|
||
**Status:** REVIEWED - Pending Implementation
|
||
**Review-Änderungen:** Formular-Registerkarten; Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher)
|