feat(exercise-detail): enhance combination exercise display with candidate links and bracket visualization
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 59s
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 59s
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
d50bed428b
commit
00edc7a93d
|
|
@ -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 (
|
||||
<div className="exercise-detail-shell" style={{ padding: '12px 12px 24px' }}>
|
||||
<div style={{ marginBottom: '12px', display: 'flex', justifyContent: 'space-between', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
|
|
@ -137,39 +174,34 @@ function ExerciseDetailPage() {
|
|||
{meta.length > 0 && <p className="exercise-meta-line">{meta.join(' · ')}</p>}
|
||||
</div>
|
||||
|
||||
{(exercise.exercise_kind || '').toLowerCase().trim() === 'combination' &&
|
||||
Array.isArray(exercise.combination_slots) &&
|
||||
exercise.combination_slots.length > 0 && (
|
||||
{isCombinationDetail ? (
|
||||
<section className="card exercise-detail-section">
|
||||
<h2>Stationen und Übungspools</h2>
|
||||
{exercise.method_archetype ? (
|
||||
<p style={{ fontSize: '14px', color: 'var(--text2)', marginTop: 0 }}>
|
||||
Archetyp: <code>{String(exercise.method_archetype)}</code>
|
||||
<h2>Ablauf und Stationen</h2>
|
||||
<p style={{ marginTop: 0, fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.45 }}>
|
||||
Katalog‑Ablauf mit Archetyp, Zeiten und Stationen — dieselbe Darstellung wie in der Planung und Vorschau.
|
||||
</p>
|
||||
) : null}
|
||||
<ol style={{ paddingLeft: '1.25rem', marginBottom: 0 }}>
|
||||
{sortCombinationSlotsForDisplay(exercise.combination_slots).map((s, idx) => (
|
||||
<li key={`${s.slot_index}-${idx}-${(s.title || '').slice(0, 8)}`} style={{ marginBottom: '10px' }}>
|
||||
<strong>{(s.title || '').trim() || `Station ${idx + 1}`}</strong>
|
||||
<ul style={{ margin: '4px 0 0', paddingLeft: '1.2rem' }}>
|
||||
{(s.candidates && s.candidates.length
|
||||
? s.candidates
|
||||
: (s.candidate_exercise_ids || []).map((id) => ({
|
||||
exercise_id: id,
|
||||
title: null,
|
||||
}))
|
||||
).map((c) => (
|
||||
<div className="training-run-combo-embed">
|
||||
<CombinationPlanBracket
|
||||
methodArchetype={String(exercise.method_archetype || '').trim()}
|
||||
methodProfile={catalogMethodProfileForBracket}
|
||||
combinationSlots={exercise.combination_slots}
|
||||
planningAdjusted={false}
|
||||
/>
|
||||
</div>
|
||||
{combinationCandidateLinks.length > 0 ? (
|
||||
<div style={{ marginTop: '14px', fontSize: '0.88rem' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: '6px', color: 'var(--text2)' }}>Verknüpfte Einzelübungen</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '1.2rem' }}>
|
||||
{combinationCandidateLinks.map((c) => (
|
||||
<li key={c.exercise_id}>
|
||||
<Link to={`/exercises/${c.exercise_id}`}>Übung #{c.exercise_id}</Link>
|
||||
{c.title ? ` — ${c.title}` : ''}
|
||||
<Link to={`/exercises/${c.exercise_id}`}>{c.title || `Übung #${c.exercise_id}`}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{exercise.goal && (
|
||||
<section className="card exercise-detail-section">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user