# 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 (
    <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.2  
**Letzte Änderung:** 2026-04-30  
**Status:** REVIEWED - Pending Implementation  
**Review-Änderungen:** Progressionsgraphen-UI (Tabs, Formularblock); Exercise Blocks Routes + Navigation (früher)
