From 00edc7a93d9fd8450bc6c34c5bd184f9778ca5d4 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 13 May 2026 21:41:32 +0200 Subject: [PATCH] feat(exercise-detail): enhance combination exercise display with candidate links and bracket visualization - Introduced `flattenCombinationCandidateLinks` function to streamline the extraction of unique candidate exercises for combination details. - Updated the `ExerciseDetailPage` to conditionally render a `CombinationPlanBracket` for combination exercises, improving the visual representation of training runs. - Enhanced the UI to display linked individual exercises associated with combination slots, providing clearer navigation and context for users. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/ExerciseDetailPage.jsx | 98 +++++++++++++++-------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/frontend/src/pages/ExerciseDetailPage.jsx b/frontend/src/pages/ExerciseDetailPage.jsx index 9909f5e..3888320 100644 --- a/frontend/src/pages/ExerciseDetailPage.jsx +++ b/frontend/src/pages/ExerciseDetailPage.jsx @@ -3,6 +3,7 @@ import { Link, useNavigate, useParams, useLocation } from 'react-router-dom' import api from '../utils/api' import ExerciseRichTextBlock from '../components/ExerciseRichTextBlock' import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaStrip' +import CombinationPlanBracket from '../components/CombinationPlanBracket' import { formatSkillLevelSlug } from '../constants/skillLevels' import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes' @@ -51,6 +52,28 @@ function metaParts(exercise) { return parts } +/** Eindeutige Kandidaten-Übungen für Schnellnavigation unter der Klammerdarstellung */ +function flattenCombinationCandidateLinks(slots) { + const rows = [] + const seen = new Set() + sortCombinationSlotsForDisplay(slots || []).forEach((s) => { + const cands = + s.candidates && s.candidates.length + ? s.candidates + : (s.candidate_exercise_ids || []).map((id) => ({ + exercise_id: id, + title: null, + })) + cands.forEach((c) => { + const eid = c.exercise_id + if (eid == null || seen.has(eid)) return + seen.add(eid) + rows.push({ exercise_id: eid, title: (c.title || '').trim() || null }) + }) + }) + return rows +} + function ExerciseDetailPage() { const { id } = useParams() const navigate = useNavigate() @@ -108,6 +131,20 @@ function ExerciseDetailPage() { const meta = metaParts(exercise) const fromExerciseEdit = location.state?.fromExerciseEdit === true + const isCombinationDetail = + (exercise.exercise_kind || '').toLowerCase().trim() === 'combination' && + Array.isArray(exercise.combination_slots) && + exercise.combination_slots.length > 0 + const combinationCandidateLinks = isCombinationDetail + ? flattenCombinationCandidateLinks(exercise.combination_slots) + : [] + const catalogMethodProfileForBracket = + exercise.method_profile && + typeof exercise.method_profile === 'object' && + !Array.isArray(exercise.method_profile) + ? exercise.method_profile + : {} + return (
@@ -137,39 +174,34 @@ function ExerciseDetailPage() { {meta.length > 0 &&

{meta.join(' · ')}

}
- {(exercise.exercise_kind || '').toLowerCase().trim() === 'combination' && - Array.isArray(exercise.combination_slots) && - exercise.combination_slots.length > 0 && ( -
-

Stationen und Übungspools

- {exercise.method_archetype ? ( -

- Archetyp: {String(exercise.method_archetype)} -

- ) : null} -
    - {sortCombinationSlotsForDisplay(exercise.combination_slots).map((s, idx) => ( -
  1. - {(s.title || '').trim() || `Station ${idx + 1}`} -
      - {(s.candidates && s.candidates.length - ? s.candidates - : (s.candidate_exercise_ids || []).map((id) => ({ - exercise_id: id, - title: null, - })) - ).map((c) => ( -
    • - Übung #{c.exercise_id} - {c.title ? ` — ${c.title}` : ''} -
    • - ))} -
    -
  2. - ))} -
-
- )} + {isCombinationDetail ? ( +
+

Ablauf und Stationen

+

+ Katalog‑Ablauf mit Archetyp, Zeiten und Stationen — dieselbe Darstellung wie in der Planung und Vorschau. +

+
+ +
+ {combinationCandidateLinks.length > 0 ? ( +
+
Verknüpfte Einzelübungen
+
    + {combinationCandidateLinks.map((c) => ( +
  • + {c.title || `Übung #${c.exercise_id}`} +
  • + ))} +
+
+ ) : null} +
+ ) : null} {exercise.goal && (