shinkan-jinkendo/frontend/src/components/ExercisePickerModal.jsx
Lars c6a7d668c5
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 22s
feat: add quick create draft functionality in ExercisePickerModal
- Introduced a quick create draft feature allowing users to create private exercise drafts directly from the ExercisePickerModal.
- Added state management for quick create inputs including title and summary, with validation for minimum title length.
- Updated the Dashboard to display a preview of private exercise drafts, enhancing user visibility of pending exercises.
- Enabled quick create functionality in TrainingFrameworkProgramEditPage and TrainingPlanningPage for streamlined exercise management.
2026-05-07 09:20:19 +02:00

725 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Übungssuche mit Volltext-, KI-/Semantikfeld (aktuell gleiche Engine wie Suche) und erweiterten Filtern.
* Paginierung bis max. 100 Treffer pro Request (API-Limit).
*/
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import api from '../utils/api'
import { useAuth } from '../context/AuthContext'
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
import {
INITIAL_EXERCISE_LIST_FILTERS,
mergeExerciseListPrefsFromApi,
splitMnCatalogRules,
splitScalarCatalogRules,
} from '../constants/exerciseListFilters'
import MultiSelectCombo from './MultiSelectCombo'
import ExerciseFocusRulePicker from './ExerciseFocusRulePicker'
import CatalogRulePicker from './CatalogRulePicker'
const PAGE_SIZE = 100
const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)
const INITIAL_FILTERS = { ...INITIAL_EXERCISE_LIST_FILTERS }
/** Stub-Ziel für API-Validator (mind. Ziel oder Durchführung); Nutzer ergänzt Details in der Übungsbearbeitung. */
const QUICK_CREATE_GOAL_PLACEHOLDER =
'Aus der Trainingsplanung angelegt — bitte Ziel und Durchführung in der Übungsbearbeitung ergänzen.'
export default function ExercisePickerModal({
open,
onClose,
onSelectExercise,
multiSelect = false,
onSelectExercises = null,
enableQuickCreateDraft = false,
}) {
const { user } = useAuth()
const [catalogs, setCatalogs] = useState({
focusAreas: [],
styleDirections: [],
trainingTypes: [],
targetGroups: [],
skills: [],
})
const [catalogsReady, setCatalogsReady] = useState(false)
const [searchInput, setSearchInput] = useState('')
const [aiSearchInput, setAiSearchInput] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [debouncedAi, setDebouncedAi] = useState('')
const [filters, setFilters] = useState(() => ({ ...INITIAL_FILTERS }))
const [filterOpen, setFilterOpen] = useState(false)
const [list, setList] = useState([])
const [loading, setLoading] = useState(false)
const [loadingMore, setLoadingMore] = useState(false)
const [offset, setOffset] = useState(0)
const [hasMore, setHasMore] = useState(false)
const [multiPicked, setMultiPicked] = useState([])
const [quickOpen, setQuickOpen] = useState(false)
const [quickTitle, setQuickTitle] = useState('')
const [quickSummary, setQuickSummary] = useState('')
const [quickSaving, setQuickSaving] = useState(false)
const toggleMultiPick = (ex) => {
setMultiPicked((prev) =>
prev.some((p) => p.id === ex.id) ? prev.filter((p) => p.id !== ex.id) : [...prev, ex]
)
}
useEffect(() => {
const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 350)
return () => clearTimeout(t)
}, [searchInput])
useEffect(() => {
const t = setTimeout(() => setDebouncedAi(aiSearchInput.trim()), 350)
return () => clearTimeout(t)
}, [aiSearchInput])
useEffect(() => {
if (!open) return
let cancelled = false
;(async () => {
try {
const [fa, sd, tt, tg, sk] = await Promise.all([
api.listFocusAreas(),
api.listStyleDirections(),
api.listTrainingTypes(),
api.listTargetGroups(),
api.listSkills(),
])
if (!cancelled) {
setCatalogs({
focusAreas: fa,
styleDirections: sd,
trainingTypes: tt,
targetGroups: tg,
skills: sk,
})
setCatalogsReady(true)
}
} catch (e) {
console.error(e)
if (!cancelled) setCatalogsReady(true)
}
})()
return () => {
cancelled = true
}
}, [open])
useEffect(() => {
if (!open) {
setSearchInput('')
setAiSearchInput('')
setDebouncedSearch('')
setDebouncedAi('')
setFilters({ ...INITIAL_FILTERS })
setFilterOpen(false)
setList([])
setOffset(0)
setHasMore(false)
setMultiPicked([])
setQuickOpen(false)
setQuickTitle('')
setQuickSummary('')
setQuickSaving(false)
return
}
setFilters(mergeExerciseListPrefsFromApi(user?.exercise_list_prefs))
}, [open, user?.exercise_list_prefs])
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 queryBase = useMemo(() => {
const q = {}
const n = (v) => (v === '' || v == null ? undefined : Number(v))
const ids = (arr) =>
Array.isArray(arr) && arr.length ? arr.map((x) => Number(x)).filter((x) => !Number.isNaN(x)) : undefined
const fMn = splitMnCatalogRules(filters.focus_rules)
if (fMn.includeIds.length) q.focus_area_must_include_ids = fMn.includeIds
if (fMn.excludeIds.length) q.focus_area_must_exclude_ids = fMn.excludeIds
if (filters.focus_only_without) q.focus_only_without_focus_areas = true
const fa = ids(filters.focus_area_ids)
if (fa?.length) q.focus_area_ids = fa
const sdMn = splitMnCatalogRules(filters.style_direction_rules)
if (sdMn.includeIds.length) q.style_direction_must_include_ids = sdMn.includeIds
if (sdMn.excludeIds.length) q.style_direction_must_exclude_ids = sdMn.excludeIds
const sdLegacy = ids(filters.style_direction_ids)
if (sdLegacy?.length) q.style_direction_ids = sdLegacy
const ttMn = splitMnCatalogRules(filters.training_type_rules)
if (ttMn.includeIds.length) q.training_type_must_include_ids = ttMn.includeIds
if (ttMn.excludeIds.length) q.training_type_must_exclude_ids = ttMn.excludeIds
const ttLegacy = ids(filters.training_type_ids)
if (ttLegacy?.length) q.training_type_ids = ttLegacy
const tgMn = splitMnCatalogRules(filters.target_group_rules)
if (tgMn.includeIds.length) q.target_group_must_include_ids = tgMn.includeIds
if (tgMn.excludeIds.length) q.target_group_must_exclude_ids = tgMn.excludeIds
const tgLegacy = ids(filters.target_group_ids)
if (tgLegacy?.length) q.target_group_ids = tgLegacy
const visMn = splitScalarCatalogRules(filters.visibility_rules)
if (visMn.includeVals.length) q.visibility_any = visMn.includeVals
if (visMn.excludeVals.length) q.visibility_exclude_any = visMn.excludeVals
const stMn = splitScalarCatalogRules(filters.status_rules)
if (stMn.includeVals.length) q.status_any = stMn.includeVals
if (stMn.excludeVals.length) q.status_exclude_any = stMn.excludeVals
const sk = ids(filters.skill_ids)
if (sk?.length) q.skill_ids = sk
if (filters.skill_min_level) q.skill_min_level = n(filters.skill_min_level)
if (filters.skill_max_level) q.skill_max_level = n(filters.skill_max_level)
if (filters.exclude_without_focus) q.exclude_without_focus = true
if (filters.include_archived) q.include_archived = true
if (debouncedSearch) q.search = debouncedSearch
if (debouncedAi) q.ai_search = debouncedAi
return q
}, [filters, debouncedSearch, debouncedAi])
const reload = useCallback(async () => {
if (!open || !catalogsReady) return
setLoading(true)
setOffset(0)
try {
const batch = await api.listExercises({
...queryBase,
include_archived: true,
include_variants: true,
limit: PAGE_SIZE,
offset: 0,
})
setList(Array.isArray(batch) ? batch : [])
setHasMore(batch?.length === PAGE_SIZE)
setOffset(batch?.length ?? 0)
} catch (e) {
console.error(e)
alert(e.message || 'Laden fehlgeschlagen')
setList([])
setHasMore(false)
} finally {
setLoading(false)
}
}, [open, catalogsReady, queryBase])
useEffect(() => {
reload()
}, [reload])
const loadMore = async () => {
if (!hasMore || loadingMore || loading) return
setLoadingMore(true)
try {
const batch = await api.listExercises({
...queryBase,
include_archived: true,
include_variants: true,
limit: PAGE_SIZE,
offset,
})
setList((prev) => [...prev, ...(Array.isArray(batch) ? batch : [])])
setHasMore(batch?.length === PAGE_SIZE)
setOffset((o) => o + (batch?.length ?? 0))
} catch (e) {
console.error(e)
alert(e.message || 'Mehr laden fehlgeschlagen')
} finally {
setLoadingMore(false)
}
}
const resetFilters = () => setFilters({ ...INITIAL_FILTERS })
const submitQuickCreate = async () => {
const title = (quickTitle || '').trim()
if (title.length < 3) {
alert('Titel: mindestens 3 Zeichen.')
return
}
const summaryRaw = (quickSummary || '').trim()
setQuickSaving(true)
try {
const created = await api.createExercise({
title,
summary: summaryRaw || null,
goal: QUICK_CREATE_GOAL_PLACEHOLDER,
execution: null,
visibility: 'private',
status: 'draft',
equipment: [],
focus_areas_multi: [],
training_styles_multi: [],
training_types_multi: [],
target_groups_multi: [],
age_groups: [],
skills: [],
club_id: null,
})
if (!created?.id) {
throw new Error('Anlegen fehlgeschlagen')
}
if (multiSelect && typeof onSelectExercises === 'function') {
await Promise.resolve(onSelectExercises([created]))
} else if (typeof onSelectExercise === 'function') {
await Promise.resolve(onSelectExercise(created))
}
onClose()
} catch (e) {
console.error(e)
alert(e.message || 'Übung konnte nicht angelegt werden')
} finally {
setQuickSaving(false)
}
}
if (!open) return null
return (
<div
className="admin-modal-backdrop"
role="presentation"
onClick={(e) => {
if (e.target === e.currentTarget) onClose()
}}
>
<div
className="admin-modal-sheet"
role="dialog"
aria-modal="true"
style={{ maxWidth: '920px', width: '100%', maxHeight: '92vh', display: 'flex', flexDirection: 'column' }}
onClick={(e) => e.stopPropagation()}
>
<div className="admin-modal-sheet__header">
<h3 className="admin-modal-sheet__title">
{multiSelect ? 'Übungen auswählen' : 'Übung auswählen'}
</h3>
<button type="button" className="btn btn-secondary admin-modal-sheet__close" onClick={onClose}>
Schließen
</button>
</div>
{enableQuickCreateDraft ? (
<div
style={{
padding: '10px 1rem 12px',
borderBottom: '1px solid var(--border)',
flexShrink: 0,
background: 'var(--surface2)',
}}
>
<button
type="button"
className="btn btn-secondary"
style={{ width: '100%' }}
onClick={() => setQuickOpen((v) => !v)}
aria-expanded={quickOpen}
>
{quickOpen ? 'Neue Übung ausblenden' : 'Neue Übung anlegen (Entwurf, privat)'}
</button>
{quickOpen ? (
<div style={{ marginTop: '12px', display: 'grid', gap: '10px' }}>
<p style={{ margin: 0, fontSize: '13px', color: 'var(--text2)', lineHeight: 1.45 }}>
Wird mit Sichtbarkeit <strong>privat</strong> und Status <strong>Entwurf</strong> gespeichert und
erscheint auf dem Dashboard zum Weiterbearbeiten. Nach dem Speichern wird die Übung direkt in den
Ablauf übernommen.
</p>
<div>
<label className="form-label" htmlFor="ex-picker-quick-title">
Titel
</label>
<input
id="ex-picker-quick-title"
type="text"
className="form-input"
value={quickTitle}
onChange={(e) => setQuickTitle(e.target.value)}
autoComplete="off"
minLength={3}
maxLength={300}
placeholder="z.B. Partnerübung Abwehr"
/>
</div>
<div>
<label className="form-label" htmlFor="ex-picker-quick-summary">
Kurzbeschreibung
</label>
<textarea
id="ex-picker-quick-summary"
className="form-input"
rows={3}
value={quickSummary}
onChange={(e) => setQuickSummary(e.target.value)}
placeholder="Optional: grobe Idee, Kontext aus der Planung …"
/>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', justifyContent: 'flex-end' }}>
<button
type="button"
className="btn btn-primary"
disabled={quickSaving || (quickTitle || '').trim().length < 3}
onClick={submitQuickCreate}
>
{quickSaving ? 'Wird angelegt…' : 'Entwurf anlegen und übernehmen'}
</button>
</div>
</div>
) : null}
</div>
) : null}
<div style={{ padding: '0 1rem 0.75rem', borderBottom: '1px solid var(--border)', flexShrink: 0 }}>
<div style={{ display: 'grid', gap: '0.65rem' }}>
<div>
<label className="form-label">Volltextsuche</label>
<input
type="search"
className="form-input"
placeholder="Stichwort, Titelfragment…"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
autoComplete="off"
/>
</div>
<div>
<label className="form-label">
Semantisch /{' '}
<span title="aktuell gleiche Datenbanksuche wie Volltext; später KI-Verfeinerung möglich">KI-Feld</span>
</label>
<input
type="search"
className="form-input"
placeholder="zweites Suchkonzept oder Umschreibung…"
value={aiSearchInput}
onChange={(e) => setAiSearchInput(e.target.value)}
autoComplete="off"
/>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', alignItems: 'center' }}>
<button type="button" className="btn btn-secondary" onClick={() => setFilterOpen(!filterOpen)}>
{filterOpen ? 'Filter ausblenden' : 'Erweiterte Filter'}
</button>
<button type="button" className="btn" onClick={resetFilters}>
Filter zurücksetzen
</button>
{loading && <span style={{ fontSize: '13px', color: 'var(--text2)' }}>Suche läuft</span>}
</div>
{filterOpen && (
<div style={{ marginTop: '0.35rem', fontSize: '13px', color: 'var(--text2)' }}>
<p style={{ margin: '0 0 12px 0' }}>
Felder gelten mit <strong>UND</strong>. Kataloge: mehrere + = alle zutreffend; schließt aus.
Sichtbarkeit/Status: mehrere + = eine davon (ODER); blendet aus.
</p>
<ExerciseFocusRulePicker
focusOptions={focusOptions}
focusRules={filters.focus_rules}
focusOnlyWithout={filters.focus_only_without}
legacyFocusAreaIds={filters.focus_area_ids}
onPatch={(patch) => setFilters((f) => ({ ...f, ...patch }))}
/>
<div className="exercise-filters-modal-grid exercise-filters-modal-grid--catalog" style={{ marginTop: 12 }}>
<CatalogRulePicker
label="Stilrichtung"
hint="+ alle nötig (UND). verbietet Zuordnung."
options={styleOptions}
rules={filters.style_direction_rules}
rulesFieldName="style_direction_rules"
placeholder="Stil …"
onPatch={(patch) => setFilters((f) => ({ ...f, ...patch }))}
/>
<CatalogRulePicker
label="Trainingsstil"
hint="+ alle nötig (UND). verbietet Zuordnung."
options={trainingTypeOptions}
rules={filters.training_type_rules}
rulesFieldName="training_type_rules"
placeholder="Trainingsstil …"
onPatch={(patch) => setFilters((f) => ({ ...f, ...patch }))}
/>
<CatalogRulePicker
label="Zielgruppe"
hint="+ alle nötig (UND). verbietet Zuordnung."
options={targetGroupOptions}
rules={filters.target_group_rules}
rulesFieldName="target_group_rules"
placeholder="Gruppe …"
onPatch={(patch) => setFilters((f) => ({ ...f, ...patch }))}
/>
</div>
<div style={{ marginTop: 12 }}>
<label className="form-label">Fähigkeit</label>
<MultiSelectCombo
value={filters.skill_ids}
onChange={(v) => setFilters((f) => ({ ...f, skill_ids: v }))}
options={skillOptions}
placeholder="Fähigkeit …"
/>
<div className="exercise-filter-skill-levels-row" style={{ marginTop: 8 }}>
<select
className="form-input exercise-filter-level-select"
title="von"
value={filters.skill_min_level}
onChange={(e) => setFilters((f) => ({ ...f, skill_min_level: e.target.value }))}
>
<option value="">Stufe von</option>
{LEVEL_FILTER_OPTS.map((o) => (
<option key={o.value} value={String(o.level)}>
{o.level}
</option>
))}
</select>
<select
className="form-input exercise-filter-level-select"
title="bis"
value={filters.skill_max_level}
onChange={(e) => setFilters((f) => ({ ...f, skill_max_level: e.target.value }))}
>
<option value="">Stufe bis</option>
{LEVEL_FILTER_OPTS.map((o) => (
<option key={`m-${o.value}`} value={String(o.level)}>
{o.level}
</option>
))}
</select>
</div>
</div>
<div className="exercise-filters-modal-grid exercise-filters-modal-grid--two exercise-filters-modal-grid--catalog" style={{ marginTop: 12 }}>
<CatalogRulePicker
label="Sichtbarkeit"
options={visibilityOptions}
rules={filters.visibility_rules}
rulesFieldName="visibility_rules"
idKind="string"
placeholder="Sichtbarkeit …"
onPatch={(patch) => setFilters((f) => ({ ...f, ...patch }))}
/>
<CatalogRulePicker
label="Status"
options={statusOptions}
rules={filters.status_rules}
rulesFieldName="status_rules"
idKind="string"
placeholder="Status …"
onPatch={(patch) => setFilters((f) => ({ ...f, ...patch }))}
/>
</div>
<div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 10 }}>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
cursor: filters.focus_only_without ? 'not-allowed' : 'pointer',
opacity: filters.focus_only_without ? 0.55 : 1,
}}
>
<input
type="checkbox"
disabled={!!filters.focus_only_without}
checked={!!filters.exclude_without_focus}
onChange={(e) =>
setFilters((f) => ({
...f,
exclude_without_focus: e.target.checked,
...(e.target.checked ? { focus_only_without: false } : {}),
}))
}
/>
<span>Ohne Fokus ausblenden</span>
</label>
<p style={{ margin: 0, fontSize: '12px', color: 'var(--text2)' }}>
Hinweis: Für die Planung werden archivierte Übungen bei der Suche immer mit eingeschlossen (bestehende
Zuordnungen).
</p>
</div>
</div>
)}
</div>
</div>
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '12px 1rem' }}>
{!catalogsReady || (loading && list.length === 0) ? (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<div className="spinner" />
</div>
) : list.length === 0 ? (
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>Keine Treffer.</p>
) : (
<>
<p style={{ fontSize: '13px', color: 'var(--text2)', marginBottom: 10 }}>
{list.length} angezeigt{hasMore ? ' · weiter unten „Mehr laden“' : ''}
</p>
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{list.map((ex) => {
const picked = multiPicked.some((p) => p.id === ex.id)
const rowInner = (
<>
<strong style={{ display: 'block' }}>{ex.title}</strong>
{(ex.summary || '').trim().length > 0 && (
<span style={{ fontSize: '12px', color: 'var(--text2)' }}>
{(ex.summary || '').length > 120
? `${(ex.summary || '').slice(0, 120)}`
: ex.summary}
</span>
)}
{ex.focus_area && (
<span className="exercise-tag exercise-tag--accent" style={{ marginTop: 6 }}>
{ex.focus_area}
</span>
)}
</>
)
if (multiSelect) {
return (
<li key={ex.id}>
<label
className="tu-ex-picker-multi-row"
style={{
display: 'flex',
gap: '10px',
alignItems: 'flex-start',
width: '100%',
textAlign: 'left',
padding: '10px 12px',
marginBottom: 8,
borderRadius: '8px',
border: picked ? '2px solid var(--accent)' : '1px solid var(--border)',
background: 'var(--surface2)',
cursor: 'pointer',
boxSizing: 'border-box',
}}
>
<input
type="checkbox"
checked={picked}
onChange={() => toggleMultiPick(ex)}
style={{ marginTop: '0.35rem', flexShrink: 0 }}
aria-label={ex.title ? `Auswahl: ${ex.title}` : 'Auswahl'}
/>
<div style={{ flex: 1, minWidth: 0 }}>{rowInner}</div>
</label>
</li>
)
}
return (
<li key={ex.id}>
<button
type="button"
onClick={() => {
onSelectExercise(ex)
onClose()
}}
style={{
width: '100%',
textAlign: 'left',
padding: '10px 12px',
marginBottom: 8,
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--surface2)',
cursor: 'pointer',
}}
>
{rowInner}
</button>
</li>
)
})}
</ul>
{hasMore && (
<div style={{ textAlign: 'center', marginTop: 12 }}>
<button type="button" className="btn btn-secondary" disabled={loadingMore} onClick={loadMore}>
{loadingMore ? 'Laden…' : 'Mehr laden'}
</button>
</div>
)}
{multiSelect && typeof onSelectExercises === 'function' ? (
<div
className="exercise-picker-multi-footer"
style={{
position: 'sticky',
bottom: 0,
marginTop: 16,
paddingTop: 12,
borderTop: '1px solid var(--border)',
background: 'var(--surface)',
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<span style={{ fontSize: '0.92rem', color: 'var(--text2)' }}>
{multiPicked.length} ausgewählt
</span>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
<button
type="button"
className="btn btn-secondary"
onClick={() => setMultiPicked([])}
disabled={!multiPicked.length}
>
Auswahl leeren
</button>
<button
type="button"
className="btn btn-primary"
disabled={!multiPicked.length}
onClick={() => {
onSelectExercises([...multiPicked])
onClose()
}}
>
Übernehmen
</button>
</div>
</div>
) : null}
</>
)}
</div>
</div>
</div>
)
}