All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Bumped APP_VERSION to 0.8.132 and updated the changelog to reflect recent changes. - Removed unused imports and refactored the ExerciseFormPage, ExercisesListPage, and TrainingPlanningPage for improved code clarity and maintainability. - Enhanced the overall structure of the components by eliminating redundant code and optimizing imports.
591 lines
20 KiB
JavaScript
591 lines
20 KiB
JavaScript
import React, { useState, useEffect, useMemo, useCallback, useRef, lazy, Suspense } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import api from '../../utils/api'
|
|
import { useAuth } from '../../context/AuthContext'
|
|
import { activeClubMemberships, getTenantClubDependencyKey } from '../../utils/activeClub'
|
|
import PageSectionNav from '../PageSectionNav'
|
|
import ExerciseListCard from './ExerciseListCard'
|
|
import ExerciseListFilterModal from './ExerciseListFilterModal'
|
|
import ExerciseListBulkModal from './ExerciseListBulkModal'
|
|
import ExerciseListSearchBar from './ExerciseListSearchBar'
|
|
import ExerciseListBulkToolbar from './ExerciseListBulkToolbar'
|
|
import { buildExerciseListFilterChips } from '../../utils/exerciseListFilterChips'
|
|
import { applyDashboardExerciseListUrl, buildExerciseListQueryBase } from '../../utils/exerciseListQuery'
|
|
import { useExerciseListCatalogsAndQuery } from '../../hooks/useExerciseListCatalogsAndQuery'
|
|
import {
|
|
INITIAL_EXERCISE_LIST_FILTERS,
|
|
mergeExerciseListPrefsFromApi,
|
|
compactExerciseListPrefsPayload,
|
|
} from '../../constants/exerciseListFilters'
|
|
|
|
const ExerciseProgressionGraphPanel = lazy(() => import('../ExerciseProgressionGraphPanel'))
|
|
|
|
const BULK_MAX_IDS = 500
|
|
const EXERCISES_PAGE_TABS = [
|
|
{ id: 'list', label: 'Liste' },
|
|
{ id: 'progression', label: 'Progressionsgraphen' },
|
|
]
|
|
|
|
function ExercisesListPageRoot() {
|
|
const { user, checkAuth } = useAuth()
|
|
const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
|
const isSuperadmin = user?.role === 'superadmin'
|
|
const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
|
|
|
|
const [mineOnly, setMineOnly] = useState(() => {
|
|
try {
|
|
const sp = new URLSearchParams(window.location.search)
|
|
return sp.get('mine') === '1' || sp.get('created_by_me') === '1'
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
|
|
const [searchInput, setSearchInput] = useState('')
|
|
const [aiSearchInput, setAiSearchInput] = useState('')
|
|
const [debouncedSearch, setDebouncedSearch] = useState('')
|
|
const [debouncedAiSearch, setDebouncedAiSearch] = useState('')
|
|
const [filters, setFilters] = useState(() => ({ ...INITIAL_EXERCISE_LIST_FILTERS }))
|
|
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
|
const [savingExercisePrefs, setSavingExercisePrefs] = useState(false)
|
|
const [pageTab, setPageTab] = useState('list')
|
|
const prefsAppliedRef = useRef(false)
|
|
|
|
const [selectedIds, setSelectedIds] = useState(() => new Set())
|
|
const [bulkModalOpen, setBulkModalOpen] = useState(false)
|
|
const [bulkVisibility, setBulkVisibility] = useState('')
|
|
const [bulkStatus, setBulkStatus] = useState('')
|
|
const [bulkClubSelect, setBulkClubSelect] = useState('')
|
|
const [bulkClubManual, setBulkClubManual] = useState('')
|
|
const [bulkSubmitting, setBulkSubmitting] = useState(false)
|
|
const [bulkPatchFocusAreas, setBulkPatchFocusAreas] = useState(false)
|
|
const [bulkFocusAreaIds, setBulkFocusAreaIds] = useState([])
|
|
const [bulkPatchStyleDirections, setBulkPatchStyleDirections] = useState(false)
|
|
const [bulkStyleDirectionIds, setBulkStyleDirectionIds] = useState([])
|
|
const [bulkPatchTrainingTypes, setBulkPatchTrainingTypes] = useState(false)
|
|
const [bulkTrainingTypeIds, setBulkTrainingTypeIds] = useState([])
|
|
const [bulkPatchTargetGroups, setBulkPatchTargetGroups] = useState(false)
|
|
const [bulkTargetGroupIds, setBulkTargetGroupIds] = useState([])
|
|
|
|
useEffect(() => {
|
|
if (!user?.id) return
|
|
if (prefsAppliedRef.current) return
|
|
const merged = mergeExerciseListPrefsFromApi(user.exercise_list_prefs)
|
|
setFilters(applyDashboardExerciseListUrl(merged))
|
|
try {
|
|
const sp = new URLSearchParams(window.location.search)
|
|
if (sp.get('mine') === '1' || sp.get('created_by_me') === '1') setMineOnly(true)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
prefsAppliedRef.current = true
|
|
}, [user?.id, user?.exercise_list_prefs])
|
|
|
|
useEffect(() => {
|
|
if (!user?.id) prefsAppliedRef.current = false
|
|
}, [user?.id])
|
|
|
|
useEffect(() => {
|
|
const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400)
|
|
return () => clearTimeout(t)
|
|
}, [searchInput])
|
|
|
|
useEffect(() => {
|
|
const t = setTimeout(() => setDebouncedAiSearch(aiSearchInput.trim()), 400)
|
|
return () => clearTimeout(t)
|
|
}, [aiSearchInput])
|
|
|
|
useEffect(() => {
|
|
if (!filterModalOpen) return
|
|
const onKey = (e) => {
|
|
if (e.key === 'Escape') setFilterModalOpen(false)
|
|
}
|
|
window.addEventListener('keydown', onKey)
|
|
return () => window.removeEventListener('keydown', onKey)
|
|
}, [filterModalOpen])
|
|
|
|
const queryBase = useMemo(
|
|
() => buildExerciseListQueryBase(filters, debouncedSearch, debouncedAiSearch, mineOnly),
|
|
[filters, debouncedSearch, debouncedAiSearch, mineOnly]
|
|
)
|
|
|
|
const {
|
|
catalogs,
|
|
catalogsReady,
|
|
exercises,
|
|
setExercises,
|
|
listFetching,
|
|
loadingMore,
|
|
hasMore,
|
|
loadMore,
|
|
} = useExerciseListCatalogsAndQuery({ queryBase, pageTab, tenantClubDepKey })
|
|
|
|
useEffect(() => {
|
|
setSelectedIds(new Set())
|
|
}, [queryBase])
|
|
|
|
const focusOptions = useMemo(
|
|
() =>
|
|
catalogs.focusAreas.map((fa) => ({
|
|
id: fa.id,
|
|
label: `${fa.icon || ''} ${fa.name || ''}`.trim(),
|
|
})),
|
|
[catalogs.focusAreas]
|
|
)
|
|
const styleOptions = useMemo(
|
|
() => catalogs.styleDirections.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
|
|
[catalogs.styleDirections]
|
|
)
|
|
const trainingTypeOptions = useMemo(
|
|
() => catalogs.trainingTypes.map((t) => ({ id: t.id, label: t.name || String(t.id) })),
|
|
[catalogs.trainingTypes]
|
|
)
|
|
const targetGroupOptions = useMemo(
|
|
() => catalogs.targetGroups.map((g) => ({ id: g.id, label: g.name || String(g.id) })),
|
|
[catalogs.targetGroups]
|
|
)
|
|
const skillOptions = useMemo(
|
|
() => catalogs.skills.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
|
|
[catalogs.skills]
|
|
)
|
|
const visibilityOptions = useMemo(
|
|
() => [
|
|
{ id: 'private', label: 'Privat' },
|
|
{ id: 'club', label: 'Verein' },
|
|
{ id: 'official', label: 'Offiziell' },
|
|
],
|
|
[]
|
|
)
|
|
const statusOptions = useMemo(
|
|
() => [
|
|
{ id: 'draft', label: 'Entwurf' },
|
|
{ id: 'in_review', label: 'In Prüfung' },
|
|
{ id: 'approved', label: 'Freigegeben' },
|
|
{ id: 'archived', label: 'Archiviert' },
|
|
],
|
|
[]
|
|
)
|
|
|
|
const filterChips = useMemo(
|
|
() =>
|
|
buildExerciseListFilterChips({
|
|
mineOnly,
|
|
setMineOnly,
|
|
filters,
|
|
setFilters,
|
|
focusOptions,
|
|
styleOptions,
|
|
trainingTypeOptions,
|
|
targetGroupOptions,
|
|
skillOptions,
|
|
visibilityOptions,
|
|
statusOptions,
|
|
}),
|
|
[
|
|
mineOnly,
|
|
filters,
|
|
focusOptions,
|
|
styleOptions,
|
|
trainingTypeOptions,
|
|
targetGroupOptions,
|
|
skillOptions,
|
|
visibilityOptions,
|
|
statusOptions,
|
|
]
|
|
)
|
|
|
|
/** Für Browser-/datalist-Vorschläge (aktuelle Treffer-Titel, begrenzt) */
|
|
const searchTitleSuggestions = useMemo(() => {
|
|
const titles = exercises.map((e) => (e.title || '').trim()).filter(Boolean)
|
|
return [...new Set(titles)].slice(0, 80)
|
|
}, [exercises])
|
|
|
|
const clubNameById = useMemo(() => {
|
|
const m = {}
|
|
for (const c of activeClubMemberships(user?.clubs)) {
|
|
if (c?.id != null) m[Number(c.id)] = c.name || `#${c.id}`
|
|
}
|
|
return m
|
|
}, [user?.clubs])
|
|
|
|
const effectiveClubId =
|
|
user?.effective_club_id != null && user.effective_club_id !== ''
|
|
? Number(user.effective_club_id)
|
|
: user?.active_club_id != null && user.active_club_id !== ''
|
|
? Number(user.active_club_id)
|
|
: null
|
|
|
|
const toggleSelect = useCallback((id) => {
|
|
setSelectedIds((prev) => {
|
|
const n = new Set(prev)
|
|
const nid = Number(id)
|
|
if (Number.isNaN(nid)) return prev
|
|
if (n.has(nid)) n.delete(nid)
|
|
else n.add(nid)
|
|
return n
|
|
})
|
|
}, [])
|
|
|
|
const clearSelection = useCallback(() => setSelectedIds(new Set()), [])
|
|
|
|
const toggleSelectAllPage = useCallback(() => {
|
|
setSelectedIds((prev) => {
|
|
const n = new Set(prev)
|
|
const allSel =
|
|
exercises.length > 0 && exercises.every((e) => n.has(Number(e.id)))
|
|
if (allSel) {
|
|
exercises.forEach((e) => n.delete(Number(e.id)))
|
|
} else {
|
|
exercises.forEach((e) => n.add(Number(e.id)))
|
|
}
|
|
return n
|
|
})
|
|
}, [exercises])
|
|
|
|
const allOnPageSelected = useMemo(
|
|
() => exercises.length > 0 && exercises.every((e) => selectedIds.has(Number(e.id))),
|
|
[exercises, selectedIds]
|
|
)
|
|
|
|
const bulkVisibilityOptions = useMemo(() => {
|
|
const base = [
|
|
{ id: '', label: '— nicht ändern —' },
|
|
{ id: 'private', label: 'Privat' },
|
|
{ id: 'club', label: 'Verein' },
|
|
]
|
|
if (isSuperadmin) base.push({ id: 'official', label: 'Offiziell (global)' })
|
|
return base
|
|
}, [isSuperadmin])
|
|
|
|
const handleDelete = async (exercise) => {
|
|
if (!confirm(`Übung "${exercise.title}" wirklich löschen?`)) return
|
|
try {
|
|
await api.deleteExercise(exercise.id)
|
|
setExercises((prev) => prev.filter((e) => e.id !== exercise.id))
|
|
} catch (err) {
|
|
alert('Fehler beim Löschen: ' + err.message)
|
|
}
|
|
}
|
|
|
|
const resetAllFilters = useCallback(() => {
|
|
setMineOnly(false)
|
|
setFilters({ ...INITIAL_EXERCISE_LIST_FILTERS })
|
|
}, [])
|
|
|
|
const handleSaveExerciseFilterPrefs = useCallback(async () => {
|
|
const uid = user?.id
|
|
if (!uid) {
|
|
alert('Nicht angemeldet.')
|
|
return
|
|
}
|
|
setSavingExercisePrefs(true)
|
|
try {
|
|
const payload = compactExerciseListPrefsPayload(filters)
|
|
await api.updateProfile(uid, { exercise_list_prefs: payload })
|
|
await checkAuth()
|
|
alert('Standardfilter für die Übungsliste gespeichert.')
|
|
} catch (e) {
|
|
alert('Speichern fehlgeschlagen: ' + (e.message || String(e)))
|
|
} finally {
|
|
setSavingExercisePrefs(false)
|
|
}
|
|
}, [user?.id, filters, checkAuth])
|
|
|
|
const openBulkModal = () => {
|
|
setBulkVisibility('')
|
|
setBulkStatus('')
|
|
setBulkClubSelect('')
|
|
setBulkClubManual('')
|
|
setBulkPatchFocusAreas(false)
|
|
setBulkFocusAreaIds([])
|
|
setBulkPatchStyleDirections(false)
|
|
setBulkStyleDirectionIds([])
|
|
setBulkPatchTrainingTypes(false)
|
|
setBulkTrainingTypeIds([])
|
|
setBulkPatchTargetGroups(false)
|
|
setBulkTargetGroupIds([])
|
|
setBulkModalOpen(true)
|
|
}
|
|
|
|
const handleBulkSubmit = async () => {
|
|
const anyRelationPatch =
|
|
bulkPatchFocusAreas ||
|
|
bulkPatchStyleDirections ||
|
|
bulkPatchTrainingTypes ||
|
|
bulkPatchTargetGroups
|
|
if (!bulkVisibility && !bulkStatus && !anyRelationPatch) {
|
|
alert(
|
|
'Bitte mindestens eine Änderung wählen (Sichtbarkeit, Status oder eine der Zuordnungen mit gesetztem Häkchen „ersetzen“).'
|
|
)
|
|
return
|
|
}
|
|
const ids = Array.from(selectedIds).filter((x) => Number.isFinite(x) && x > 0)
|
|
if (ids.length === 0) {
|
|
alert('Keine Übungen ausgewählt.')
|
|
return
|
|
}
|
|
if (ids.length > BULK_MAX_IDS) {
|
|
alert(`Maximal ${BULK_MAX_IDS} Übungen pro Vorgang. Bitte Auswahl oder mehrere Durchläufe verwenden.`)
|
|
return
|
|
}
|
|
const payload = { exercise_ids: ids }
|
|
if (bulkVisibility) payload.visibility = bulkVisibility
|
|
if (bulkStatus) payload.status = bulkStatus
|
|
if (bulkPatchFocusAreas) {
|
|
payload.focus_area_ids = bulkFocusAreaIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
|
}
|
|
if (bulkPatchStyleDirections) {
|
|
payload.style_direction_ids = bulkStyleDirectionIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
|
}
|
|
if (bulkPatchTrainingTypes) {
|
|
payload.training_type_ids = bulkTrainingTypeIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
|
}
|
|
if (bulkPatchTargetGroups) {
|
|
payload.target_group_ids = bulkTargetGroupIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
|
}
|
|
if (bulkVisibility === 'club') {
|
|
const manual = String(bulkClubManual || '').trim()
|
|
if (manual && /^\d+$/.test(manual)) payload.club_id = Number(manual)
|
|
else if (bulkClubSelect && /^\d+$/.test(String(bulkClubSelect))) {
|
|
payload.club_id = Number(bulkClubSelect)
|
|
}
|
|
}
|
|
setBulkSubmitting(true)
|
|
try {
|
|
const res = await api.bulkPatchExercisesMetadata(payload)
|
|
const updatedSet = new Set((res.updated || []).map((x) => Number(x)))
|
|
let resolvedClubId = null
|
|
if (bulkVisibility === 'club') {
|
|
if (payload.club_id != null) resolvedClubId = payload.club_id
|
|
else if (effectiveClubId != null && !Number.isNaN(effectiveClubId)) resolvedClubId = effectiveClubId
|
|
}
|
|
const clubLabel =
|
|
resolvedClubId != null ? clubNameById[resolvedClubId] || `Verein #${resolvedClubId}` : null
|
|
|
|
let nextPrimaryFocusName = null
|
|
if (bulkPatchFocusAreas) {
|
|
const faNums = bulkFocusAreaIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
|
|
if (faNums.length > 0) {
|
|
const opt = focusOptions.find((o) => Number(o.id) === Number(faNums[0]))
|
|
nextPrimaryFocusName = String(opt?.label ?? '').trim() || String(faNums[0])
|
|
}
|
|
}
|
|
|
|
setExercises((prev) =>
|
|
prev.map((e) => {
|
|
if (!updatedSet.has(Number(e.id))) return e
|
|
const next = { ...e }
|
|
if (bulkVisibility) {
|
|
next.visibility = bulkVisibility
|
|
next.club_id = bulkVisibility === 'club' ? resolvedClubId : null
|
|
next.club_name = bulkVisibility === 'club' ? clubLabel : null
|
|
}
|
|
if (bulkStatus) next.status = bulkStatus
|
|
if (bulkPatchFocusAreas) {
|
|
if (nextPrimaryFocusName == null) delete next.focus_area
|
|
else next.focus_area = nextPrimaryFocusName
|
|
}
|
|
return next
|
|
})
|
|
)
|
|
|
|
let msg = `${res.updated_count ?? updatedSet.size} Übung(en) aktualisiert.`
|
|
if (res.failed_count) msg += `\n${res.failed_count} nicht geändert (siehe Details).`
|
|
if (Array.isArray(res.failed) && res.failed.length) {
|
|
msg +=
|
|
'\n\n' +
|
|
res.failed
|
|
.slice(0, 12)
|
|
.map((f) => `#${f.id}: ${f.detail}`)
|
|
.join('\n')
|
|
if (res.failed.length > 12) msg += '\n…'
|
|
}
|
|
alert(msg)
|
|
setBulkModalOpen(false)
|
|
clearSelection()
|
|
} catch (err) {
|
|
alert('Massenänderung fehlgeschlagen: ' + (err.message || String(err)))
|
|
} finally {
|
|
setBulkSubmitting(false)
|
|
}
|
|
}
|
|
|
|
if (!catalogsReady && pageTab === 'list') {
|
|
return (
|
|
<div className="app-page">
|
|
<div className="empty-state" style={{ padding: '2.5rem 1rem' }}>
|
|
<div className="spinner" />
|
|
<p className="muted" style={{ marginTop: '12px' }}>
|
|
Lade Kataloge…
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="app-page">
|
|
<div className="exercises-page__header">
|
|
<h1 className="page-title exercises-page__title">Übungen</h1>
|
|
{pageTab === 'list' ? (
|
|
<Link to="/exercises/new" className="btn btn-primary">
|
|
+ Neu
|
|
</Link>
|
|
) : (
|
|
<span aria-hidden="true" />
|
|
)}
|
|
</div>
|
|
|
|
<PageSectionNav
|
|
ariaLabel="Übungen Bereiche"
|
|
value={pageTab}
|
|
onChange={setPageTab}
|
|
items={EXERCISES_PAGE_TABS}
|
|
className="exercises-page-toolbar-tabs"
|
|
/>
|
|
|
|
{pageTab === 'progression' ? (
|
|
<Suspense
|
|
fallback={
|
|
<div className="card empty-state" style={{ padding: '2rem 1rem' }}>
|
|
<div className="spinner" />
|
|
<p className="muted" style={{ marginTop: '12px' }}>
|
|
Lade Progressionsgraphen…
|
|
</p>
|
|
</div>
|
|
}
|
|
>
|
|
<ExerciseProgressionGraphPanel />
|
|
</Suspense>
|
|
) : (
|
|
<>
|
|
<ExerciseListSearchBar
|
|
searchTitleSuggestions={searchTitleSuggestions}
|
|
searchInput={searchInput}
|
|
onSearchInputChange={setSearchInput}
|
|
aiSearchInput={aiSearchInput}
|
|
onAiSearchInputChange={setAiSearchInput}
|
|
mineOnly={mineOnly}
|
|
onToggleMineOnly={() => setMineOnly((v) => !v)}
|
|
onOpenFilter={() => setFilterModalOpen(true)}
|
|
filterChips={filterChips}
|
|
onResetAllFilters={resetAllFilters}
|
|
exerciseCount={exercises.length}
|
|
allOnPageSelected={allOnPageSelected}
|
|
onToggleSelectAllPage={toggleSelectAllPage}
|
|
/>
|
|
|
|
<ExerciseListBulkToolbar
|
|
selectedCount={selectedIds.size}
|
|
bulkMaxIds={BULK_MAX_IDS}
|
|
onClearSelection={clearSelection}
|
|
onOpenBulkModal={openBulkModal}
|
|
/>
|
|
|
|
<ExerciseListFilterModal
|
|
open={filterModalOpen}
|
|
onClose={() => setFilterModalOpen(false)}
|
|
filters={filters}
|
|
setFilters={setFilters}
|
|
focusOptions={focusOptions}
|
|
styleOptions={styleOptions}
|
|
trainingTypeOptions={trainingTypeOptions}
|
|
targetGroupOptions={targetGroupOptions}
|
|
skillOptions={skillOptions}
|
|
visibilityOptions={visibilityOptions}
|
|
statusOptions={statusOptions}
|
|
savingExercisePrefs={savingExercisePrefs}
|
|
onSaveStandard={handleSaveExerciseFilterPrefs}
|
|
onResetAll={resetAllFilters}
|
|
/>
|
|
|
|
<ExerciseListBulkModal
|
|
open={bulkModalOpen}
|
|
onClose={() => setBulkModalOpen(false)}
|
|
onSubmit={handleBulkSubmit}
|
|
bulkSubmitting={bulkSubmitting}
|
|
selectedCount={selectedIds.size}
|
|
bulkMaxIds={BULK_MAX_IDS}
|
|
user={user}
|
|
isPlatformAdmin={isPlatformAdmin}
|
|
statusOptions={statusOptions}
|
|
bulkVisibilityOptions={bulkVisibilityOptions}
|
|
focusOptions={focusOptions}
|
|
styleOptions={styleOptions}
|
|
trainingTypeOptions={trainingTypeOptions}
|
|
targetGroupOptions={targetGroupOptions}
|
|
bulkVisibility={bulkVisibility}
|
|
setBulkVisibility={setBulkVisibility}
|
|
bulkStatus={bulkStatus}
|
|
setBulkStatus={setBulkStatus}
|
|
bulkClubSelect={bulkClubSelect}
|
|
setBulkClubSelect={setBulkClubSelect}
|
|
bulkClubManual={bulkClubManual}
|
|
setBulkClubManual={setBulkClubManual}
|
|
bulkPatchFocusAreas={bulkPatchFocusAreas}
|
|
setBulkPatchFocusAreas={setBulkPatchFocusAreas}
|
|
bulkFocusAreaIds={bulkFocusAreaIds}
|
|
setBulkFocusAreaIds={setBulkFocusAreaIds}
|
|
bulkPatchStyleDirections={bulkPatchStyleDirections}
|
|
setBulkPatchStyleDirections={setBulkPatchStyleDirections}
|
|
bulkStyleDirectionIds={bulkStyleDirectionIds}
|
|
setBulkStyleDirectionIds={setBulkStyleDirectionIds}
|
|
bulkPatchTrainingTypes={bulkPatchTrainingTypes}
|
|
setBulkPatchTrainingTypes={setBulkPatchTrainingTypes}
|
|
bulkTrainingTypeIds={bulkTrainingTypeIds}
|
|
setBulkTrainingTypeIds={setBulkTrainingTypeIds}
|
|
bulkPatchTargetGroups={bulkPatchTargetGroups}
|
|
setBulkPatchTargetGroups={setBulkPatchTargetGroups}
|
|
bulkTargetGroupIds={bulkTargetGroupIds}
|
|
setBulkTargetGroupIds={setBulkTargetGroupIds}
|
|
/>
|
|
|
|
{listFetching && exercises.length === 0 ? (
|
|
<div className="card empty-state" style={{ padding: '2rem 1rem' }}>
|
|
<div className="spinner" />
|
|
<p className="muted" style={{ marginTop: '12px' }}>
|
|
Lade Übungen…
|
|
</p>
|
|
</div>
|
|
) : exercises.length === 0 ? (
|
|
<div className="card">
|
|
<p className="exercises-empty-text">Keine Übungen gefunden.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{listFetching ? (
|
|
<p className="exercises-meta-line exercises-meta-line--muted">Aktualisiere Treffer…</p>
|
|
) : null}
|
|
<p className="exercises-meta-line">
|
|
{exercises.length} angezeigt
|
|
{hasMore ? ' · es gibt weitere Einträge' : ''}
|
|
</p>
|
|
<div className="exercises-list-grid" data-testid="exercises-list-grid">
|
|
{exercises.map((exercise) => (
|
|
<ExerciseListCard
|
|
key={exercise.id}
|
|
exercise={exercise}
|
|
user={user}
|
|
selectedIds={selectedIds}
|
|
toggleSelect={toggleSelect}
|
|
onDelete={handleDelete}
|
|
/>
|
|
))}
|
|
</div>
|
|
{hasMore && (
|
|
<div className="exercises-load-more">
|
|
<button type="button" className="btn btn-secondary" disabled={loadingMore} onClick={loadMore}>
|
|
{loadingMore ? 'Laden…' : 'Mehr laden'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ExercisesListPageRoot
|