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 10s
Test Suite / playwright-tests (push) Successful in 55s
- Updated ExerciseFullContent to accept a new `variantId` prop and display variant-specific information. - Enhanced TrainingCoachPage to pass `variantId` when rendering ExerciseFullContent. - Refactored TrainingUnitRunPage to manage exercise context with variant support, including updates to modal handling. - Improved UI to show variant details, including name, description, and execution changes where applicable.
172 lines
7.3 KiB
JavaScript
172 lines
7.3 KiB
JavaScript
/**
|
||
* 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'
|
||
|
||
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 }} props
|
||
*/
|
||
export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId }) {
|
||
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
|
||
|
||
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}
|
||
<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 & 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>
|
||
)
|
||
}
|