shinkan-jinkendo/frontend/src/components/ExerciseFullContent.jsx
Lars fb8837574e
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
feat(exercise): add support for exercise variants in ExerciseFullContent and related components
- 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.
2026-05-11 12:44:09 +02:00

172 lines
7.3 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'
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 &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>
)
}