import React, { useEffect, useMemo, useState } from 'react' import { useParams, useLocation } from 'react-router-dom' import api from '../utils/api' import PageReturnButton from '../components/PageReturnButton' import NavStateLink from '../components/NavStateLink' import { EXERCISES_LIST_PATH, buildCurrentLocationReturnContext, } from '../utils/navReturnContext' import ExerciseRichTextBlock from '../components/ExerciseRichTextBlock' import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaStrip' import CombinationPlanBracket from '../components/CombinationPlanBracket' import ExercisePeekModal from '../components/ExercisePeekModal' import { formatSkillLevelSlug } from '../constants/skillLevels' function TagRow({ exercise }) { const tags = [] ;(exercise.focus_areas || []).forEach((f) => { tags.push({ key: `fa-${f.id}`, label: f.name, accent: !!f.is_primary }) }) ;(exercise.training_styles || []).forEach((t) => { tags.push({ key: `ts-${t.id}`, label: t.name, accent: false }) }) ;(exercise.training_types || []).forEach((t) => { tags.push({ key: `tt-${t.id}`, label: t.name, accent: false }) }) ;(exercise.target_groups || []).forEach((g) => { tags.push({ key: `tg-${g.id}`, label: g.name, accent: !!g.is_primary }) }) if (tags.length === 0) return null return (
{tags.map((t) => ( {t.label} ))}
) } function metaParts(exercise) { const parts = [] if (exercise.duration_min != null || exercise.duration_max != null) { const a = exercise.duration_min const b = exercise.duration_max if (a != null && b != null && a !== b) parts.push(`${a}–${b} Min.`) else if (a != null) parts.push(`ca. ${a} Min.`) else if (b != null) parts.push(`ca. ${b} Min.`) } if (exercise.group_size_min != null || exercise.group_size_max != null) { const a = exercise.group_size_min const b = exercise.group_size_max if (a != null && b != null && a !== b) parts.push(`Gruppe ${a}–${b}`) else if (a != null) parts.push(`Gruppe ab ${a}`) else if (b != null) parts.push(`Gruppe bis ${b}`) } return parts } function ExerciseDetailPage() { const { id } = useParams() const location = useLocation() const editReturnContext = useMemo( () => buildCurrentLocationReturnContext(location, 'Zurück zur Übung'), [location] ) const [exercise, setExercise] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) /** Schnellansicht für eingebettete Einzelübungen (Kombination) — ohne Route zu verlassen */ const [embeddedPeekExerciseId, setEmbeddedPeekExerciseId] = useState(null) useEffect(() => { let cancelled = false const load = async () => { setLoading(true) setError(null) try { const data = await api.getExercise(id) if (!cancelled) setExercise(data) } catch (err) { if (!cancelled) setError(err) } finally { if (!cancelled) setLoading(false) } } if (id) load() return () => { cancelled = true } }, [id]) if (loading) { return (

Laden...

) } if (error) { const msg = error.message || String(error) return (

Übung

{msg}

) } if (!exercise) return null const meta = metaParts(exercise) const isCombinationDetail = (exercise.exercise_kind || '').toLowerCase().trim() === 'combination' && Array.isArray(exercise.combination_slots) && exercise.combination_slots.length > 0 const catalogMethodProfileForBracket = exercise.method_profile && typeof exercise.method_profile === 'object' && !Array.isArray(exercise.method_profile) ? exercise.method_profile : {} return (
setEmbeddedPeekExerciseId(null)} />
Bearbeiten

{exercise.title}

{exercise.summary && (
)}
{exercise.visibility} {exercise.status} {exercise.club_name && {exercise.club_name}}
{meta.length > 0 &&

{meta.join(' · ')}

}
{isCombinationDetail ? (

Ablauf und Stationen

Katalog‑Ablauf mit Archetyp, Zeiten und Stationen. Station bzw. Einzelübung antippen öffnet eine Schnellansicht mit Kurztext und Ablauf, ohne diese Seite zu verlassen. Die vollständige Übungsseite liegt im Popup unten als Link.

setEmbeddedPeekExerciseId(Number(exerciseId))} />
) : null} {exercise.goal && (

Ziel

)} {(exercise.equipment || []).length > 0 && (

Material & Aufbau

    {exercise.equipment.map((x, i) => (
  • {x}
  • ))}
)} {exercise.preparation && (

Vorbereitung

)} {exercise.execution && (

Ablauf

)} {exercise.trainer_notes && (

Hinweise für Trainer

)} {(exercise.skills || []).length > 0 && (

Fähigkeiten

{exercise.skills.map((s) => { const rl = formatSkillLevelSlug(s.required_level) const tl = formatSkillLevelSlug(s.target_level) const lvl = rl || tl ? ` (${[rl, tl].filter(Boolean).join(' → ')})` : '' return ( {s.skill_name} {s.intensity ? ` · ${s.intensity}` : ''} {lvl} ) })}
)} {(exercise.variants || []).length > 0 && (

Varianten

{exercise.variants.map((v) => { const dur = v.duration_min != null || v.duration_max != null ? v.duration_min != null && v.duration_max != null && v.duration_min !== v.duration_max ? `${v.duration_min}–${v.duration_max} Min.` : v.duration_min != null ? `ca. ${v.duration_min} Min.` : `bis ca. ${v.duration_max} Min.` : null const diffLabel = v.difficulty_adjustment === 'easier' ? 'einfacher' : v.difficulty_adjustment === 'harder' ? 'schwerer' : v.difficulty_adjustment === 'same' ? 'gleiche Schwierigkeit' : v.difficulty_adjustment === 'adapted' ? 'angepasst' : null const equip = Array.isArray(v.equipment_changes) && v.equipment_changes.length > 0 ? v.equipment_changes.join(', ') : null return (
{v.variant_name} {(dur || diffLabel || equip || v.progression_level != null) && (
{[dur, diffLabel, equip && `Material: ${equip}`, v.progression_level != null && `Progression ${v.progression_level}`] .filter(Boolean) .join(' · ')}
)} {v.description &&

{v.description}

} {v.execution_changes && (
)}
) })}
)}
) } export default ExerciseDetailPage