feat(exercise): add support for exercise variants in ExerciseFullContent and related components
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.
This commit is contained in:
Lars 2026-05-11 12:44:09 +02:00
parent 1ce6d929ce
commit fb8837574e
3 changed files with 49 additions and 8 deletions

View File

@ -1,5 +1,6 @@
/** /**
* Voller Katalog-Inhalt einer Übung (Lesemodus für Coach/Mobile). * 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 React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
@ -51,9 +52,9 @@ function metaParts(exercise) {
} }
/** /**
* @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number }} props * @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null }} props
*/ */
export default function ExerciseFullContent({ exercise, loading, error, exerciseId }) { export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId }) {
if (loading) { if (loading) {
return ( return (
<div style={{ textAlign: 'center', padding: '1rem' }}> <div style={{ textAlign: 'center', padding: '1rem' }}>
@ -70,8 +71,41 @@ export default function ExerciseFullContent({ exercise, loading, error, exercise
const resolvedId = exercise.id ?? exerciseId const resolvedId = exercise.id ?? exerciseId
const meta = metaParts(exercise) 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 ( return (
<div className="exercise-coach-catalog" style={{ fontSize: '0.93rem', lineHeight: 1.5 }}> <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> <h2 style={{ margin: '0 0 8px', fontSize: '1.2rem', lineHeight: 1.35 }}>{exercise.title}</h2>
{meta.length > 0 && ( {meta.length > 0 && (
<p className="exercise-meta-line" style={{ marginBottom: '10px', color: 'var(--text3)', fontSize: '0.86rem' }}> <p className="exercise-meta-line" style={{ marginBottom: '10px', color: 'var(--text3)', fontSize: '0.86rem' }}>

View File

@ -400,7 +400,7 @@ export default function TrainingCoachPage() {
return () => { return () => {
cancelled = true cancelled = true
} }
}, [step, currentEntry?.item?.exercise_id, currentEntry?.item?.item_type]) }, [step, currentEntry?.item?.exercise_id, currentEntry?.item?.exercise_variant_id, currentEntry?.item?.item_type])
const handleSaveDebrief = async () => { const handleSaveDebrief = async () => {
setSaveOk(null) setSaveOk(null)
@ -739,6 +739,7 @@ export default function TrainingCoachPage() {
error={catalogError} error={catalogError}
exercise={catalogExercise} exercise={catalogExercise}
exerciseId={currentEntry?.item?.exercise_id ?? null} exerciseId={currentEntry?.item?.exercise_id ?? null}
variantId={currentEntry?.item?.exercise_variant_id ?? null}
/> />
</div> </div>
</> </>

View File

@ -33,7 +33,7 @@ export default function TrainingUnitRunPage() {
const [loadError, setLoadError] = useState(null) const [loadError, setLoadError] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [checked, setChecked] = useState(() => new Set()) const [checked, setChecked] = useState(() => new Set())
const [peekExerciseId, setPeekExerciseId] = useState(null) const [peekCtx, setPeekCtx] = useState(null)
const loadChecked = useCallback((uid) => { const loadChecked = useCallback((uid) => {
try { try {
@ -143,9 +143,10 @@ export default function TrainingUnitRunPage() {
return ( return (
<div className="training-run-page app-page" style={{ paddingBottom: '2rem' }}> <div className="training-run-page app-page" style={{ paddingBottom: '2rem' }}>
<ExercisePeekModal <ExercisePeekModal
open={peekExerciseId != null} open={peekCtx != null}
exerciseId={peekExerciseId} exerciseId={peekCtx?.exerciseId}
onClose={() => setPeekExerciseId(null)} variantId={peekCtx?.variantId ?? undefined}
onClose={() => setPeekCtx(null)}
/> />
<nav className="training-run-toolbar no-print" style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}> <nav className="training-run-toolbar no-print" style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
@ -314,7 +315,12 @@ export default function TrainingUnitRunPage() {
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
style={{ fontSize: '0.82rem', margin: 0 }} style={{ fontSize: '0.82rem', margin: 0 }}
onClick={() => setPeekExerciseId(it.exercise_id)} onClick={() =>
setPeekCtx({
exerciseId: it.exercise_id,
variantId: it.exercise_variant_id != null ? Number(it.exercise_variant_id) : null,
})
}
> >
Katalog (Popup) Katalog (Popup)
</button> </button>