feat: implement exercise filter modal and enhance filtering logic
- Added a new exercise filter modal with improved layout and styling for better user experience. - Introduced initial filter state and logic to count active filter groups, enhancing the filtering capabilities on the ExercisesListPage. - Updated CSS styles to support the new modal and improve overall UI consistency. - Implemented keyboard accessibility for closing the filter modal, enhancing usability.
This commit is contained in:
parent
fd2009294b
commit
0d4ad9a2c8
|
|
@ -1537,6 +1537,67 @@ a.analysis-split__nav-item {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.exercise-filter-modal.admin-modal-sheet {
|
||||||
|
max-width: min(920px, calc(100vw - 16px));
|
||||||
|
}
|
||||||
|
.exercise-filter-modal .admin-modal-sheet__body.exercise-filter-modal__scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.exercise-filter-modal__footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.exercise-search-bar__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.exercise-filter-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.exercise-filter-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.exercise-filters-modal-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.exercise-filter-level-row {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.exercise-filter-level-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Reifegradmodell-Admin: klare Schritte, responsives Raster */
|
/* Reifegradmodell-Admin: klare Schritte, responsives Raster */
|
||||||
.admin-matrix-alert {
|
.admin-matrix-alert {
|
||||||
border: 1px solid var(--danger);
|
border: 1px solid var(--danger);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,37 @@
|
||||||
import React, { useState, useEffect, useMemo } from 'react'
|
import React, { useState, useEffect, useMemo, useCallback } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
|
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
|
||||||
import SearchableSelect from '../components/SearchableSelect'
|
|
||||||
import MultiSelectCombo from '../components/MultiSelectCombo'
|
import MultiSelectCombo from '../components/MultiSelectCombo'
|
||||||
|
|
||||||
const PAGE_SIZE = 100
|
const PAGE_SIZE = 100
|
||||||
const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)
|
const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)
|
||||||
|
|
||||||
|
const INITIAL_FILTERS = {
|
||||||
|
focus_area_ids: [],
|
||||||
|
style_direction_ids: [],
|
||||||
|
training_type_ids: [],
|
||||||
|
target_group_ids: [],
|
||||||
|
skill_ids: [],
|
||||||
|
skill_min_level: '',
|
||||||
|
skill_max_level: '',
|
||||||
|
visibility_any: [],
|
||||||
|
status_any: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
function countActiveFilterGroups(f) {
|
||||||
|
let n = 0
|
||||||
|
if (f.focus_area_ids?.length) n++
|
||||||
|
if (f.style_direction_ids?.length) n++
|
||||||
|
if (f.training_type_ids?.length) n++
|
||||||
|
if (f.target_group_ids?.length) n++
|
||||||
|
if (f.skill_ids?.length) n++
|
||||||
|
if (f.skill_min_level || f.skill_max_level) n++
|
||||||
|
if (f.visibility_any?.length) n++
|
||||||
|
if (f.status_any?.length) n++
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
function ExercisesListPage() {
|
function ExercisesListPage() {
|
||||||
const [exercises, setExercises] = useState([])
|
const [exercises, setExercises] = useState([])
|
||||||
const [catalogs, setCatalogs] = useState({
|
const [catalogs, setCatalogs] = useState({
|
||||||
|
|
@ -26,17 +50,10 @@ function ExercisesListPage() {
|
||||||
const [aiSearchInput, setAiSearchInput] = useState('')
|
const [aiSearchInput, setAiSearchInput] = useState('')
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||||
const [debouncedAiSearch, setDebouncedAiSearch] = useState('')
|
const [debouncedAiSearch, setDebouncedAiSearch] = useState('')
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState(() => ({ ...INITIAL_FILTERS }))
|
||||||
focus_area_ids: [],
|
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||||
style_direction_ids: [],
|
|
||||||
training_type_ids: [],
|
const activeFilterGroups = useMemo(() => countActiveFilterGroups(filters), [filters])
|
||||||
target_group_ids: [],
|
|
||||||
skill_ids: [],
|
|
||||||
skill_min_level: '',
|
|
||||||
skill_max_level: '',
|
|
||||||
visibility_any: [],
|
|
||||||
status_any: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400)
|
const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400)
|
||||||
|
|
@ -48,6 +65,15 @@ function ExercisesListPage() {
|
||||||
return () => clearTimeout(t)
|
return () => clearTimeout(t)
|
||||||
}, [aiSearchInput])
|
}, [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 focusOptions = useMemo(
|
const focusOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
catalogs.focusAreas.map((fa) => ({
|
catalogs.focusAreas.map((fa) => ({
|
||||||
|
|
@ -72,10 +98,6 @@ function ExercisesListPage() {
|
||||||
() => catalogs.skills.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
|
() => catalogs.skills.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
|
||||||
[catalogs.skills]
|
[catalogs.skills]
|
||||||
)
|
)
|
||||||
const levelFilterOptions = useMemo(
|
|
||||||
() => LEVEL_FILTER_OPTS.map((o) => ({ id: o.level, label: o.label })),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
const visibilityOptions = useMemo(
|
const visibilityOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ id: 'private', label: 'Privat' },
|
{ id: 'private', label: 'Privat' },
|
||||||
|
|
@ -204,6 +226,8 @@ function ExercisesListPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetAllFilters = useCallback(() => setFilters({ ...INITIAL_FILTERS }), [])
|
||||||
|
|
||||||
if (!catalogsReady || loading) {
|
if (!catalogsReady || loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
|
@ -231,122 +255,188 @@ function ExercisesListPage() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card exercise-filters-compact" style={{ marginBottom: '12px' }}>
|
<div className="card exercise-search-bar" style={{ marginBottom: '12px' }}>
|
||||||
<p style={{ gridColumn: '1 / -1', fontSize: '12px', color: 'var(--text3)', margin: '0 0 4px' }}>
|
<label className="form-label">Volltextsuche (Titel, Ziel, …)</label>
|
||||||
Filter untereinander werden mit <strong>UND</strong> kombiniert. Mehrere Einträge in einem Feld mit{' '}
|
<input
|
||||||
<strong>ODER</strong> (die Übung muss mindestens eine der gewählten Zuordnungen erfüllen).
|
type="search"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="Suchbegriffe…"
|
||||||
|
value={searchInput}
|
||||||
|
onChange={(e) => setSearchInput(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
style={{ marginBottom: '10px' }}
|
||||||
|
/>
|
||||||
|
<label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="form-input"
|
||||||
|
placeholder="zweiter Begriff — zusätzliche Volltextsuche (ODER)"
|
||||||
|
value={aiSearchInput}
|
||||||
|
onChange={(e) => setAiSearchInput(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<div className="exercise-search-bar__actions">
|
||||||
|
<button type="button" className="btn btn-secondary exercise-filter-trigger" onClick={() => setFilterModalOpen(true)}>
|
||||||
|
Filter
|
||||||
|
{activeFilterGroups > 0 ? (
|
||||||
|
<span className="exercise-filter-badge" aria-hidden>
|
||||||
|
{activeFilterGroups}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
{activeFilterGroups > 0 ? (
|
||||||
|
<button type="button" className="btn" onClick={resetAllFilters}>
|
||||||
|
Filter löschen
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: '10px', marginBottom: 0 }}>
|
||||||
|
Vereins-/Trainerfilter folgen später. Fachliche Filter über „Filter“ – zwischen Feldern UND, Auswahl mehrerer Werte je Feld mit ODER.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ gridColumn: '1 / -1' }}>
|
|
||||||
<label className="form-label">Volltextsuche (Titel, Ziel, …)</label>
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="Suchbegriffe…"
|
|
||||||
value={searchInput}
|
|
||||||
onChange={(e) => setSearchInput(e.target.value)}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ gridColumn: '1 / -1' }}>
|
|
||||||
<label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label>
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="zweiter Begriff — aktuell zusätzliche Volltextsuche (ODER); später KI"
|
|
||||||
value={aiSearchInput}
|
|
||||||
onChange={(e) => setAiSearchInput(e.target.value)}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: '6px' }}>
|
|
||||||
Standardfilter aus Verein und Trainerrolle folgen später; die KI-Nutzung der zweiten Zeile
|
|
||||||
kommt mit einem eigenen Endpunkt.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Fokus (mehrere möglich)</label>
|
|
||||||
<MultiSelectCombo
|
|
||||||
value={filters.focus_area_ids}
|
|
||||||
onChange={(v) => setFilters({ ...filters, focus_area_ids: v })}
|
|
||||||
options={focusOptions}
|
|
||||||
placeholder="Fokus suchen oder „▼ Alle“ …"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Stilrichtung (mehrere möglich)</label>
|
|
||||||
<MultiSelectCombo
|
|
||||||
value={filters.style_direction_ids}
|
|
||||||
onChange={(v) => setFilters({ ...filters, style_direction_ids: v })}
|
|
||||||
options={styleOptions}
|
|
||||||
placeholder="Stilrichtung suchen …"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Trainingsstil (mehrere möglich)</label>
|
|
||||||
<MultiSelectCombo
|
|
||||||
value={filters.training_type_ids}
|
|
||||||
onChange={(v) => setFilters({ ...filters, training_type_ids: v })}
|
|
||||||
options={trainingTypeOptions}
|
|
||||||
placeholder="Trainingsstil suchen …"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Zielgruppe (mehrere möglich)</label>
|
|
||||||
<MultiSelectCombo
|
|
||||||
value={filters.target_group_ids}
|
|
||||||
onChange={(v) => setFilters({ ...filters, target_group_ids: v })}
|
|
||||||
options={targetGroupOptions}
|
|
||||||
placeholder="Zielgruppe suchen …"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Fähigkeit (mehrere möglich)</label>
|
|
||||||
<MultiSelectCombo
|
|
||||||
value={filters.skill_ids}
|
|
||||||
onChange={(v) => setFilters({ ...filters, skill_ids: v })}
|
|
||||||
options={skillOptions}
|
|
||||||
placeholder="Fähigkeit suchen …"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Fähigkeit Stufe von</label>
|
|
||||||
<SearchableSelect
|
|
||||||
value={filters.skill_min_level}
|
|
||||||
onChange={(v) => setFilters({ ...filters, skill_min_level: v })}
|
|
||||||
options={levelFilterOptions}
|
|
||||||
allLabel="egal (min)"
|
|
||||||
filterPlaceholder="Stufe suchen…"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Fähigkeit Stufe bis</label>
|
|
||||||
<SearchableSelect
|
|
||||||
value={filters.skill_max_level}
|
|
||||||
onChange={(v) => setFilters({ ...filters, skill_max_level: v })}
|
|
||||||
options={levelFilterOptions}
|
|
||||||
allLabel="egal (max)"
|
|
||||||
filterPlaceholder="Stufe suchen…"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Sichtbarkeit (mehrere möglich)</label>
|
|
||||||
<MultiSelectCombo
|
|
||||||
value={filters.visibility_any}
|
|
||||||
onChange={(v) => setFilters({ ...filters, visibility_any: v })}
|
|
||||||
options={visibilityOptions}
|
|
||||||
placeholder="Sichtbarkeit wählen …"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="form-label">Status (mehrere möglich)</label>
|
|
||||||
<MultiSelectCombo
|
|
||||||
value={filters.status_any}
|
|
||||||
onChange={(v) => setFilters({ ...filters, status_any: v })}
|
|
||||||
options={statusOptions}
|
|
||||||
placeholder="Status wählen …"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{filterModalOpen && (
|
||||||
|
<div
|
||||||
|
className="admin-modal-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) setFilterModalOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="admin-modal-sheet exercise-filter-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="exercise-filter-modal-title"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="admin-modal-sheet__header">
|
||||||
|
<h3 id="exercise-filter-modal-title" className="admin-modal-sheet__title">
|
||||||
|
Übungen filtern
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary admin-modal-sheet__close"
|
||||||
|
onClick={() => setFilterModalOpen(false)}
|
||||||
|
>
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="admin-modal-sheet__body exercise-filter-modal__scroll">
|
||||||
|
<p style={{ fontSize: '13px', color: 'var(--text2)', marginTop: 0, marginBottom: '14px' }}>
|
||||||
|
Zwischen den Bereichen gilt <strong>UND</strong>. Innerhalb eines Bereichs werden mehrere Einträge mit{' '}
|
||||||
|
<strong>ODER</strong> verknüpft (die Übung muss mindestens eine gewählte Zuordnung erfüllen).
|
||||||
|
</p>
|
||||||
|
<div className="exercise-filters-modal-grid">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Fokus</label>
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={filters.focus_area_ids}
|
||||||
|
onChange={(v) => setFilters({ ...filters, focus_area_ids: v })}
|
||||||
|
options={focusOptions}
|
||||||
|
placeholder="Fokus suchen oder „▼ Alle“ …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Stilrichtung</label>
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={filters.style_direction_ids}
|
||||||
|
onChange={(v) => setFilters({ ...filters, style_direction_ids: v })}
|
||||||
|
options={styleOptions}
|
||||||
|
placeholder="Stilrichtung suchen …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Trainingsstil</label>
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={filters.training_type_ids}
|
||||||
|
onChange={(v) => setFilters({ ...filters, training_type_ids: v })}
|
||||||
|
options={trainingTypeOptions}
|
||||||
|
placeholder="Trainingsstil suchen …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Zielgruppe</label>
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={filters.target_group_ids}
|
||||||
|
onChange={(v) => setFilters({ ...filters, target_group_ids: v })}
|
||||||
|
options={targetGroupOptions}
|
||||||
|
placeholder="Zielgruppe suchen …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Fähigkeit</label>
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={filters.skill_ids}
|
||||||
|
onChange={(v) => setFilters({ ...filters, skill_ids: v })}
|
||||||
|
options={skillOptions}
|
||||||
|
placeholder="Fähigkeit suchen …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="exercise-filter-level-row">
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Fähigkeit Stufe von</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={filters.skill_min_level}
|
||||||
|
onChange={(e) => setFilters({ ...filters, skill_min_level: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">egal</option>
|
||||||
|
{LEVEL_FILTER_OPTS.map((o) => (
|
||||||
|
<option key={o.value} value={String(o.level)}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">bis</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={filters.skill_max_level}
|
||||||
|
onChange={(e) => setFilters({ ...filters, skill_max_level: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">egal</option>
|
||||||
|
{LEVEL_FILTER_OPTS.map((o) => (
|
||||||
|
<option key={`m-${o.value}`} value={String(o.level)}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Sichtbarkeit</label>
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={filters.visibility_any}
|
||||||
|
onChange={(v) => setFilters({ ...filters, visibility_any: v })}
|
||||||
|
options={visibilityOptions}
|
||||||
|
placeholder="Sichtbarkeit wählen …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="form-label">Status</label>
|
||||||
|
<MultiSelectCombo
|
||||||
|
value={filters.status_any}
|
||||||
|
onChange={(v) => setFilters({ ...filters, status_any: v })}
|
||||||
|
options={statusOptions}
|
||||||
|
placeholder="Status wählen …"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="exercise-filter-modal__footer">
|
||||||
|
<button type="button" className="btn" onClick={resetAllFilters}>
|
||||||
|
Alle Filter zurücksetzen
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={() => setFilterModalOpen(false)}>
|
||||||
|
Fertig
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{exercises.length === 0 ? (
|
{exercises.length === 0 ? (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user