shinkan-jinkendo/frontend/src/pages/ExerciseDetailPage.jsx

249 lines
8.4 KiB
React
Raw Normal View History

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