shinkan-jinkendo/frontend/src/components/ExerciseFullContent.jsx
Lars 4e654e50c0
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 11s
Test Suite / playwright-tests (push) Successful in 59s
feat(version): bump to 0.8.103 and enhance planning method profile integration
- Updated app version to 0.8.103, reflecting recent enhancements in training planning.
- Incremented database schema version to 20260512057, ensuring compatibility with new features.
- Introduced optional `planning_method_profile` for combination exercises, allowing for detailed planning and coaching support.
- Enhanced frontend components to manage and display planning method profiles effectively in the Training Unit Sections Editor and ExerciseFullContent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:21:33 +02:00

188 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Voller Katalog-Inhalt einer Übung (Lesemodus für Coach/Mobile).
* Optional: geplante Variante (`variantId`) — Beschreibung und Durchführungsänderungen oben.
*/
import React from 'react'
import { Link } from 'react-router-dom'
import ExerciseRichTextBlock from './ExerciseRichTextBlock'
import CombinationCoachSlots from './CombinationCoachSlots'
import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
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 (
<div className="exercise-tag-row">
{tags.map((t) => (
<span key={t.key} className={`exercise-tag${t.accent ? ' exercise-tag--accent' : ''}`}>
{t.label}
</span>
))}
</div>
)
}
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
}
/**
* @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null, planningComboMethodProfile?: object|null }} props
*/
export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId, planningComboMethodProfile }) {
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '1rem' }}>
<div className="spinner" />
<p style={{ marginTop: '10px', color: 'var(--text2)', fontSize: '0.9rem' }}>Übung aus Katalog laden…</p>
</div>
)
}
if (error) {
return <p style={{ color: 'var(--danger)', fontSize: '0.92rem', padding: '4px 0' }}>{error}</p>
}
if (!exercise) return null
const resolvedId = exercise.id ?? exerciseId
const meta = metaParts(exercise)
const variant =
variantId != null && variantId !== '' && Array.isArray(exercise.variants) && exercise.variants.length
? exercise.variants.find((v) => String(v.id) === String(variantId)) || null
: null
const isCombination =
String(exercise.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
const coachComboProfile = isCombination
? effectiveComboMethodProfile(exercise.method_profile, planningComboMethodProfile)
: null
return (
<div className="exercise-coach-catalog" style={{ fontSize: '0.93rem', lineHeight: 1.5 }}>
{variant ? (
<section
className="card"
style={{
marginBottom: '14px',
padding: '12px 14px',
borderLeft: '3px solid var(--accent)',
background: 'var(--surface2)',
}}
>
<h3 style={{ fontSize: '0.72rem', textTransform: 'uppercase', color: 'var(--text3)', margin: '0 0 6px', letterSpacing: '0.04em' }}>
Geplante Variante
</h3>
<p style={{ margin: '0 0 10px', fontSize: '1.05rem', fontWeight: 700 }}>{variant.variant_name || `Variante #${variant.id}`}</p>
{variant.description ? (
<div style={{ marginBottom: variant.execution_changes ? '12px' : 0 }}>
<h4 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px' }}>Zur Variante</h4>
<ExerciseRichTextBlock html={variant.description} exerciseId={resolvedId} media={exercise.media} />
</div>
) : null}
{variant.execution_changes ? (
<div>
<h4 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px' }}>Durchführung (Variante)</h4>
<ExerciseRichTextBlock html={variant.execution_changes} exerciseId={resolvedId} media={exercise.media} />
</div>
) : null}
</section>
) : null}
{isCombination && Array.isArray(exercise.combination_slots) ? (
<CombinationCoachSlots
combinationSlots={exercise.combination_slots}
methodArchetype={exercise.method_archetype}
methodProfile={coachComboProfile}
/>
) : null}
<h2 style={{ margin: '0 0 8px', fontSize: '1.2rem', lineHeight: 1.35 }}>{exercise.title}</h2>
{meta.length > 0 && (
<p className="exercise-meta-line" style={{ marginBottom: '10px', color: 'var(--text3)', fontSize: '0.86rem' }}>
{meta.join(' · ')}
</p>
)}
<TagRow exercise={exercise} />
{exercise.summary && (
<section className="card" style={{ marginTop: '12px', padding: '12px 14px' }}>
<h3 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px', letterSpacing: '0.04em' }}>
Kurzbeschreibung
</h3>
<ExerciseRichTextBlock html={exercise.summary} exerciseId={resolvedId} media={exercise.media} />
</section>
)}
{exercise.goal && (
<section className="card" style={{ marginTop: '10px', padding: '12px 14px' }}>
<h3 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px' }}>Ziel</h3>
<ExerciseRichTextBlock html={exercise.goal} exerciseId={resolvedId} media={exercise.media} />
</section>
)}
{(exercise.equipment || []).length > 0 && (
<section className="card" style={{ marginTop: '10px', padding: '12px 14px' }}>
<h3 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px' }}>
Material &amp; Aufbau
</h3>
<ul style={{ paddingLeft: '1.1rem', margin: 0 }}>
{exercise.equipment.map((x, i) => (
<li key={i}>{x}</li>
))}
</ul>
</section>
)}
{exercise.preparation && (
<section className="card" style={{ marginTop: '10px', padding: '12px 14px' }}>
<h3 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px' }}>Vorbereitung</h3>
<ExerciseRichTextBlock html={exercise.preparation} exerciseId={resolvedId} media={exercise.media} />
</section>
)}
{exercise.execution && (
<section className="card" style={{ marginTop: '10px', padding: '12px 14px' }}>
<h3 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px' }}>Ablauf</h3>
<ExerciseRichTextBlock html={exercise.execution} exerciseId={resolvedId} media={exercise.media} />
</section>
)}
{exercise.trainer_notes && (
<section className="card" style={{ marginTop: '10px', padding: '12px 14px' }}>
<h3 style={{ fontSize: '0.78rem', textTransform: 'uppercase', color: 'var(--text3)', marginBottom: '8px' }}>
Hinweise Trainer (Katalog)
</h3>
<ExerciseRichTextBlock html={exercise.trainer_notes} exerciseId={resolvedId} media={exercise.media} />
</section>
)}
{exerciseId != null && (
<p style={{ marginTop: '12px' }}>
<Link to={`/exercises/${exerciseId}`} style={{ fontSize: '0.86rem', color: 'var(--accent)' }}>
Volle Übungsseite im Browser
</Link>
</p>
)}
</div>
)
}