- Introduced TrainingCoachPage for coaching-related training sessions. - Updated app routing to include a new route for the TrainingCoachPage. - Enhanced TrainingPlanningPage with a link to navigate to the new coaching page. - Incremented version numbers for TrainingPlanningPage, TrainingUnitRunPage, and added version for TrainingCoachPage. - Integrated ExercisePeekModal for improved exercise selection and visibility in both TrainingPlanningPage and TrainingUnitRunPage.
150 lines
5.0 KiB
JavaScript
150 lines
5.0 KiB
JavaScript
/**
|
|
* Schnellansicht einer Übung aus dem Katalog (ohne die Planungsseite zu verlassen).
|
|
*/
|
|
import React, { useEffect, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import api from '../utils/api'
|
|
import { sanitizeTrainerHtml } from '../utils/htmlUtils'
|
|
|
|
function HtmlBlock({ html, className = '' }) {
|
|
if (!html || !String(html).trim()) return null
|
|
const safe = sanitizeTrainerHtml(html)
|
|
return (
|
|
<div className={`rich-text-content ${className}`} dangerouslySetInnerHTML={{ __html: safe }} />
|
|
)
|
|
}
|
|
|
|
function TagMini({ exercise }) {
|
|
const parts = []
|
|
;(exercise.focus_areas || []).slice(0, 5).forEach((f) => {
|
|
parts.push(f.name)
|
|
})
|
|
if (parts.length === 0) return null
|
|
return (
|
|
<div className="exercise-tag-row" style={{ marginTop: '10px' }}>
|
|
{parts.map((p, i) => (
|
|
<span key={i} className="exercise-tag exercise-tag--accent">
|
|
{p}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function ExercisePeekModal({ open, exerciseId, onClose, titleFallback }) {
|
|
const [loading, setLoading] = useState(false)
|
|
const [err, setErr] = useState(null)
|
|
const [exercise, setExercise] = useState(null)
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setExercise(null)
|
|
setErr(null)
|
|
return
|
|
}
|
|
if (!exerciseId) {
|
|
setErr('Keine Übung gewählt')
|
|
return
|
|
}
|
|
let cancelled = false
|
|
;(async () => {
|
|
setLoading(true)
|
|
setErr(null)
|
|
try {
|
|
const data = await api.getExercise(exerciseId)
|
|
if (!cancelled) setExercise(data)
|
|
} catch (e) {
|
|
if (!cancelled) setErr(e.message || 'Laden fehlgeschlagen')
|
|
} finally {
|
|
if (!cancelled) setLoading(false)
|
|
}
|
|
})()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [open, exerciseId])
|
|
|
|
if (!open) return null
|
|
|
|
return (
|
|
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && onClose()}>
|
|
<div
|
|
className="admin-modal-sheet"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="exercise-peek-title"
|
|
style={{
|
|
maxWidth: '620px',
|
|
width: '100%',
|
|
maxHeight: '88vh',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="admin-modal-sheet__header">
|
|
<h3 id="exercise-peek-title" className="admin-modal-sheet__title">
|
|
{loading ? '…' : exercise?.title || titleFallback || `Übung #${exerciseId}`}
|
|
</h3>
|
|
<button type="button" className="btn btn-secondary admin-modal-sheet__close" onClick={onClose}>
|
|
Schließen
|
|
</button>
|
|
</div>
|
|
<div style={{ overflowY: 'auto', padding: '1rem', flex: 1 }}>
|
|
{loading && (
|
|
<div style={{ textAlign: 'center', color: 'var(--text2)' }}>
|
|
<div className="spinner" />
|
|
<p style={{ marginTop: '0.65rem' }}>Laden…</p>
|
|
</div>
|
|
)}
|
|
{!loading && err && <p style={{ color: 'var(--danger)' }}>{err}</p>}
|
|
{!loading && exercise && (
|
|
<>
|
|
{exercise.summary && (
|
|
<div style={{ fontSize: '0.95rem', color: 'var(--text2)' }}>
|
|
<HtmlBlock html={exercise.summary} />
|
|
</div>
|
|
)}
|
|
<TagMini exercise={exercise} />
|
|
{(exercise.goal || exercise.preparation || exercise.execution || exercise.trainer_notes) && (
|
|
<hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '1rem 0' }} />
|
|
)}
|
|
{exercise.goal && (
|
|
<>
|
|
<h4 style={{ fontSize: '0.85rem', color: 'var(--text3)', marginBottom: 6 }}>Ziel</h4>
|
|
<HtmlBlock html={exercise.goal} />
|
|
</>
|
|
)}
|
|
{exercise.preparation && (
|
|
<>
|
|
<h4 style={{ fontSize: '0.85rem', color: 'var(--text3)', margin: '14px 0 6px' }}>Vorbereitung</h4>
|
|
<HtmlBlock html={exercise.preparation} />
|
|
</>
|
|
)}
|
|
{exercise.execution && (
|
|
<>
|
|
<h4 style={{ fontSize: '0.85rem', color: 'var(--text3)', margin: '14px 0 6px' }}>Ablauf</h4>
|
|
<HtmlBlock html={exercise.execution} />
|
|
</>
|
|
)}
|
|
{exercise.trainer_notes && (
|
|
<>
|
|
<h4 style={{ fontSize: '0.85rem', color: 'var(--text3)', margin: '14px 0 6px' }}>Trainer-Hinweise</h4>
|
|
<HtmlBlock html={exercise.trainer_notes} />
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
{exerciseId && (
|
|
<div style={{ padding: '0 1rem 1rem', flexShrink: 0 }}>
|
|
<Link to={`/exercises/${exerciseId}`} className="btn btn-secondary" style={{ width: '100%', textDecoration: 'none', textAlign: 'center', display: 'block' }}>
|
|
Vollständige Übungsseite öffnen
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|