232 lines
7.7 KiB
React
232 lines
7.7 KiB
React
|
|
import React, { useState, useEffect } from 'react'
|
||
|
|
import { Link } from 'react-router-dom'
|
||
|
|
import api from '../utils/api'
|
||
|
|
|
||
|
|
function ExercisesListPage() {
|
||
|
|
const [exercises, setExercises] = useState([])
|
||
|
|
const [focusAreas, setFocusAreas] = useState([])
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
const [filters, setFilters] = useState({
|
||
|
|
focus_area: '',
|
||
|
|
visibility: '',
|
||
|
|
status: '',
|
||
|
|
})
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadData()
|
||
|
|
}, [filters])
|
||
|
|
|
||
|
|
const loadData = async () => {
|
||
|
|
try {
|
||
|
|
const [exercisesData, focusAreasData] = await Promise.all([
|
||
|
|
api.listExercises(filters),
|
||
|
|
api.listFocusAreas(),
|
||
|
|
])
|
||
|
|
setExercises(exercisesData)
|
||
|
|
setFocusAreas(focusAreasData)
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to load data:', err)
|
||
|
|
alert('Fehler beim Laden: ' + err.message)
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDelete = async (exercise) => {
|
||
|
|
if (!confirm(`Übung "${exercise.title}" wirklich löschen?`)) return
|
||
|
|
try {
|
||
|
|
await api.deleteExercise(exercise.id)
|
||
|
|
await loadData()
|
||
|
|
} catch (err) {
|
||
|
|
alert('Fehler beim Löschen: ' + err.message)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (loading) {
|
||
|
|
return (
|
||
|
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||
|
|
<div className="spinner"></div>
|
||
|
|
<p>Laden...</p>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ padding: '2rem' }}>
|
||
|
|
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'flex',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
alignItems: 'center',
|
||
|
|
marginBottom: '1.5rem',
|
||
|
|
flexWrap: 'wrap',
|
||
|
|
gap: '0.75rem',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<h1>Übungen</h1>
|
||
|
|
<Link to="/exercises/new" className="btn btn-primary">
|
||
|
|
+ Neue Übung
|
||
|
|
</Link>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'grid',
|
||
|
|
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||
|
|
gap: '1rem',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div>
|
||
|
|
<label className="form-label">Fokusbereich</label>
|
||
|
|
<select
|
||
|
|
className="form-input"
|
||
|
|
value={filters.focus_area}
|
||
|
|
onChange={(e) => setFilters({ ...filters, focus_area: e.target.value })}
|
||
|
|
>
|
||
|
|
<option value="">Alle</option>
|
||
|
|
{focusAreas.map((fa) => (
|
||
|
|
<option key={fa.id} value={fa.id}>
|
||
|
|
{fa.icon} {fa.name}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="form-label">Sichtbarkeit</label>
|
||
|
|
<select
|
||
|
|
className="form-input"
|
||
|
|
value={filters.visibility}
|
||
|
|
onChange={(e) => setFilters({ ...filters, visibility: e.target.value })}
|
||
|
|
>
|
||
|
|
<option value="">Alle</option>
|
||
|
|
<option value="private">Privat</option>
|
||
|
|
<option value="club">Verein</option>
|
||
|
|
<option value="official">Offiziell</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="form-label">Status</label>
|
||
|
|
<select
|
||
|
|
className="form-input"
|
||
|
|
value={filters.status}
|
||
|
|
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
|
||
|
|
>
|
||
|
|
<option value="">Alle</option>
|
||
|
|
<option value="draft">Entwurf</option>
|
||
|
|
<option value="in_review">In Prüfung</option>
|
||
|
|
<option value="approved">Freigegeben</option>
|
||
|
|
<option value="archived">Archiviert</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{exercises.length === 0 ? (
|
||
|
|
<div className="card">
|
||
|
|
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||
|
|
Keine Übungen gefunden. Lege jetzt deine erste Übung an!
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: 'grid',
|
||
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
||
|
|
gap: '1rem',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{exercises.map((exercise) => (
|
||
|
|
<div key={exercise.id} className="card">
|
||
|
|
<div style={{ marginBottom: '1rem' }}>
|
||
|
|
<h3 style={{ marginBottom: '0.5rem' }}>
|
||
|
|
<Link
|
||
|
|
to={`/exercises/${exercise.id}`}
|
||
|
|
style={{ color: 'inherit', textDecoration: 'none' }}
|
||
|
|
>
|
||
|
|
{exercise.title}
|
||
|
|
</Link>
|
||
|
|
</h3>
|
||
|
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
fontSize: '0.75rem',
|
||
|
|
padding: '0.25rem 0.5rem',
|
||
|
|
borderRadius: '4px',
|
||
|
|
background: 'var(--surface2)',
|
||
|
|
color: 'var(--text2)',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{exercise.focus_area || 'Ohne Fokus'}
|
||
|
|
</span>
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
fontSize: '0.75rem',
|
||
|
|
padding: '0.25rem 0.5rem',
|
||
|
|
borderRadius: '4px',
|
||
|
|
background:
|
||
|
|
exercise.visibility === 'official' ? 'var(--accent)' : 'var(--surface2)',
|
||
|
|
color: exercise.visibility === 'official' ? 'white' : 'var(--text2)',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{exercise.visibility}
|
||
|
|
</span>
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
fontSize: '0.75rem',
|
||
|
|
padding: '0.25rem 0.5rem',
|
||
|
|
borderRadius: '4px',
|
||
|
|
background: exercise.status === 'approved' ? '#2ea44f' : 'var(--surface2)',
|
||
|
|
color: exercise.status === 'approved' ? 'white' : 'var(--text2)',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{exercise.status}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{exercise.summary && (
|
||
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem' }}>
|
||
|
|
{exercise.summary}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto', flexWrap: 'wrap' }}>
|
||
|
|
<Link
|
||
|
|
to={`/exercises/${exercise.id}`}
|
||
|
|
className="btn btn-secondary"
|
||
|
|
style={{ flex: '1 1 100px', textAlign: 'center' }}
|
||
|
|
>
|
||
|
|
Ansehen
|
||
|
|
</Link>
|
||
|
|
<Link
|
||
|
|
to={`/exercises/${exercise.id}/edit`}
|
||
|
|
className="btn btn-secondary"
|
||
|
|
style={{ flex: '1 1 100px', textAlign: 'center' }}
|
||
|
|
>
|
||
|
|
Bearbeiten
|
||
|
|
</Link>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="btn"
|
||
|
|
style={{
|
||
|
|
flex: '1 1 100px',
|
||
|
|
background: 'var(--danger)',
|
||
|
|
color: 'white',
|
||
|
|
border: 'none',
|
||
|
|
}}
|
||
|
|
onClick={() => handleDelete(exercise)}
|
||
|
|
>
|
||
|
|
Löschen
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default ExercisesListPage
|