# Frontend Routing & Navigation Specification **Version:** 1.2 **Datum:** 2026-04-30 **Status:** DRAFT - Awaiting Review **Autor:** Claude Code **Ä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 inkl. Varianten-Editor inline + Block 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` --- ## 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 ( {children} ) } 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 ``` --- ## 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 } if (profile.role !== requiredRole) { return } return children } // App.jsx } /> ``` --- ## 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 ( }> } /> } /> } /> ) } ``` **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) ) } 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 ( ) } ``` ### 7.2 403 - No Permission **Pattern:** ```javascript if (error?.response?.status === 403) { return ( ) } ``` ### 7.3 Network Error **Pattern:** ```javascript if (error?.message === 'Network Error') { return ( refetch()} /> ) } ``` --- ## 8. Mobile-Spezifische Navigation ### 8.1 Bottom Navigation (Mobile) **Pattern:** ``` ┌─────────────────────────────────────┐ │ │ │ (Content Area) │ │ │ ├─────────────────────────────────────┤ │ 🏠 Home │ 📋 Übungen │ 📅 Plan │ └─────────────────────────────────────┘ ``` **Implementation:** ```javascript // MobileBottomNav.jsx ``` **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, })
{/* Exercise Detail Content */}
``` --- ## 9. Accessibility ### 9.1 Skip Links **Pattern:** ```javascript // App.jsx Zum Hauptinhalt springen
{/* Content */}
``` **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])

{exercise.title}

``` ### 9.3 ARIA Live-Region für Filter **Pattern:** ```javascript // ExercisesListPage.jsx
{exercises.length} Übungen gefunden
``` --- ## 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 ( {/* Exercises Routes */} } /> } /> } /> } /> {/* Varianten: Bearbeitung inline in ExerciseFormPage, keine eigenen Routen */} {/* Exercise Blocks Routes */} } /> } /> } /> } /> {/* Admin Routes */} }> } /> } /> } /> {/* Redirects */} } /> } /> ) } ``` --- **Version:** 1.2 **Letzte Änderung:** 2026-04-30 **Status:** REVIEWED - Pending Implementation **Review-Änderungen:** Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher)