249 lines
8.4 KiB
React
249 lines
8.4 KiB
React
|
|
import React, { useEffect, useState } from 'react'
|
||
|
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||
|
|
import api from '../utils/api'
|
||
|
|
|
||
|
|
const API_BASE = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '')
|
||
|
|
|
||
|
|
function resolveMediaUrl(filePath) {
|
||
|
|
if (!filePath) return null
|
||
|
|
if (filePath.startsWith('http://') || filePath.startsWith('https://')) return filePath
|
||
|
|
const p = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||
|
|
return `${API_BASE}${p}`
|
||
|
|
}
|
||
|
|
|
||
|
|
function MediaBlock({ media }) {
|
||
|
|
if (media.embed_url) {
|
||
|
|
return (
|
||
|
|
<div style={{ marginTop: '0.5rem' }}>
|
||
|
|
<a href={media.embed_url} target="_blank" rel="noreferrer">
|
||
|
|
{media.embed_url}
|
||
|
|
</a>
|
||
|
|
{media.embed_platform && (
|
||
|
|
<span style={{ color: 'var(--text2)', marginLeft: '0.5rem', fontSize: '0.8rem' }}>
|
||
|
|
({media.embed_platform})
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
const src = resolveMediaUrl(media.file_path)
|
||
|
|
if (!src) return null
|
||
|
|
if (media.media_type === 'image' || (media.mime_type && media.mime_type.startsWith('image/'))) {
|
||
|
|
return (
|
||
|
|
<img
|
||
|
|
src={src}
|
||
|
|
alt={media.title || media.original_filename || ''}
|
||
|
|
style={{ maxWidth: '100%', borderRadius: '8px', marginTop: '0.5rem' }}
|
||
|
|
/>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
if (media.media_type === 'video' || (media.mime_type && media.mime_type.startsWith('video/'))) {
|
||
|
|
return <video src={src} controls style={{ width: '100%', marginTop: '0.5rem', borderRadius: '8px' }} />
|
||
|
|
}
|
||
|
|
return (
|
||
|
|
<a href={src} target="_blank" rel="noreferrer" style={{ display: 'inline-block', marginTop: '0.5rem' }}>
|
||
|
|
{media.title || media.original_filename || 'Datei öffnen'}
|
||
|
|
</a>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
function ExerciseDetailPage() {
|
||
|
|
const { id } = useParams()
|
||
|
|
const navigate = useNavigate()
|
||
|
|
const [exercise, setExercise] = useState(null)
|
||
|
|
const [error, setError] = useState(null)
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let cancelled = false
|
||
|
|
const load = async () => {
|
||
|
|
setLoading(true)
|
||
|
|
setError(null)
|
||
|
|
try {
|
||
|
|
const data = await api.getExercise(id)
|
||
|
|
if (!cancelled) setExercise(data)
|
||
|
|
} catch (err) {
|
||
|
|
if (!cancelled) setError(err)
|
||
|
|
} finally {
|
||
|
|
if (!cancelled) setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (id) load()
|
||
|
|
return () => {
|
||
|
|
cancelled = true
|
||
|
|
}
|
||
|
|
}, [id])
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||
|
|
<div className="spinner"></div>
|
||
|
|
<p>Laden...</p>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (error) {
|
||
|
|
const msg = error.message || String(error)
|
||
|
|
return (
|
||
|
|
<div style={{ padding: '2rem', maxWidth: '720px', margin: '0 auto' }}>
|
||
|
|
<div className="card">
|
||
|
|
<h2>Übung</h2>
|
||
|
|
<p style={{ color: 'var(--danger)' }}>{msg}</p>
|
||
|
|
<button type="button" className="btn btn-secondary" onClick={() => navigate('/exercises')}>
|
||
|
|
Zur Übersicht
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!exercise) return null
|
||
|
|
|
||
|
|
const chips = (items, labelKey = 'name') =>
|
||
|
|
(items || []).length ? (items || []).map((x) => x[labelKey]).join(', ') : '—'
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ padding: '2rem' }}>
|
||
|
|
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||
|
|
<div style={{ marginBottom: '1rem' }}>
|
||
|
|
<button type="button" className="btn btn-secondary" onClick={() => navigate('/exercises')}>
|
||
|
|
← Übersicht
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'flex',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
alignItems: 'flex-start',
|
||
|
|
gap: '1rem',
|
||
|
|
flexWrap: 'wrap',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<h1 style={{ margin: 0 }}>{exercise.title}</h1>
|
||
|
|
<Link to={`/exercises/${exercise.id}/edit`} className="btn btn-primary">
|
||
|
|
Bearbeiten
|
||
|
|
</Link>
|
||
|
|
</div>
|
||
|
|
{exercise.summary && (
|
||
|
|
<p style={{ color: 'var(--text2)', marginTop: '1rem' }}>{exercise.summary}</p>
|
||
|
|
)}
|
||
|
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginTop: '1rem' }}>
|
||
|
|
<span className="badge">{exercise.visibility}</span>
|
||
|
|
<span className="badge">{exercise.status}</span>
|
||
|
|
{exercise.club_name && <span className="badge">{exercise.club_name}</span>}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<section className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Zuordnung</h2>
|
||
|
|
<p>
|
||
|
|
<strong>Fokusbereiche:</strong> {chips(exercise.focus_areas)}
|
||
|
|
</p>
|
||
|
|
<p>
|
||
|
|
<strong>Stilrichtungen:</strong> {chips(exercise.training_styles)}
|
||
|
|
</p>
|
||
|
|
<p>
|
||
|
|
<strong>Zielgruppen:</strong> {chips(exercise.target_groups)}
|
||
|
|
</p>
|
||
|
|
<p>
|
||
|
|
<strong>Altersgruppen:</strong>{' '}
|
||
|
|
{(exercise.age_groups || []).length ? exercise.age_groups.join(', ') : '—'}
|
||
|
|
</p>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{exercise.goal && (
|
||
|
|
<section className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Ziel</h2>
|
||
|
|
<p style={{ whiteSpace: 'pre-wrap' }}>{exercise.goal}</p>
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{exercise.execution && (
|
||
|
|
<section className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Durchführung</h2>
|
||
|
|
<p style={{ whiteSpace: 'pre-wrap' }}>{exercise.execution}</p>
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{(exercise.preparation || exercise.trainer_notes) && (
|
||
|
|
<section className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Trainer</h2>
|
||
|
|
{exercise.preparation && (
|
||
|
|
<>
|
||
|
|
<h3 style={{ fontSize: '1rem' }}>Vorbereitung</h3>
|
||
|
|
<p style={{ whiteSpace: 'pre-wrap' }}>{exercise.preparation}</p>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
{exercise.trainer_notes && (
|
||
|
|
<>
|
||
|
|
<h3 style={{ fontSize: '1rem' }}>Hinweise</h3>
|
||
|
|
<p style={{ whiteSpace: 'pre-wrap' }}>{exercise.trainer_notes}</p>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{exercise.equipment && exercise.equipment.length > 0 && (
|
||
|
|
<section className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Material</h2>
|
||
|
|
<ul>
|
||
|
|
{exercise.equipment.map((x, i) => (
|
||
|
|
<li key={i}>{x}</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{(exercise.skills || []).length > 0 && (
|
||
|
|
<section className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Fähigkeiten</h2>
|
||
|
|
<ul style={{ paddingLeft: '1.25rem' }}>
|
||
|
|
{exercise.skills.map((s) => (
|
||
|
|
<li key={s.id}>
|
||
|
|
{s.skill_name}
|
||
|
|
{s.skill_category ? ` (${s.skill_category})` : ''}
|
||
|
|
{s.is_primary ? ' · primär' : ''}
|
||
|
|
{s.intensity ? ` · ${s.intensity}` : ''}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{(exercise.variants || []).length > 0 && (
|
||
|
|
<section className="card" style={{ marginBottom: '1rem' }}>
|
||
|
|
<h2 style={{ marginTop: 0 }}>Varianten</h2>
|
||
|
|
{exercise.variants.map((v) => (
|
||
|
|
<div key={v.id} style={{ marginBottom: '1rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border)' }}>
|
||
|
|
<strong>{v.variant_name}</strong>
|
||
|
|
{v.description && <p style={{ color: 'var(--text2)' }}>{v.description}</p>}
|
||
|
|
{v.execution_changes && (
|
||
|
|
<p style={{ whiteSpace: 'pre-wrap' }}>{v.execution_changes}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{(exercise.media || []).length > 0 && (
|
||
|
|
<section className="card">
|
||
|
|
<h2 style={{ marginTop: 0 }}>Medien</h2>
|
||
|
|
{exercise.media.map((m) => (
|
||
|
|
<div key={m.id} style={{ marginBottom: '1.5rem' }}>
|
||
|
|
<strong>{m.title || m.original_filename || m.media_type}</strong>
|
||
|
|
{m.description && <p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>{m.description}</p>}
|
||
|
|
<MediaBlock media={m} />
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default ExerciseDetailPage
|