diff --git a/frontend/src/app.css b/frontend/src/app.css index 7760c9f..4e8a194 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1586,16 +1586,98 @@ a.analysis-split__nav-item { 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; +.exercise-filters-modal-grid--two { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } -@media (max-width: 480px) { - .exercise-filter-level-row { - grid-template-columns: 1fr; - } +.exercise-filter-chips-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin-top: 10px; +} +.exercise-filter-chip { + display: inline-flex; + align-items: center; + gap: 6px; + max-width: 100%; + padding: 5px 10px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--surface2); + font-size: 12px; + cursor: pointer; + font-family: inherit; + color: var(--text1); + text-align: left; +} +.exercise-filter-chip__text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: min(260px, 88vw); +} +.exercise-filter-chip__x { + flex-shrink: 0; + opacity: 0.65; + font-weight: 700; + line-height: 1; +} +.exercise-filter-section { + margin-bottom: 20px; +} +.exercise-filter-section--last { + margin-bottom: 0; +} +.exercise-filter-section-title { + margin: 0 0 10px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text3); + font-weight: 700; +} +.exercise-filter-skill-block { + border: 1px solid var(--border); + border-radius: 12px; + padding: 12px; + background: var(--surface2); +} +.exercise-filter-skill-block > .form-label { + margin-bottom: 6px; +} +.exercise-filter-skill-hint { + margin: 10px 0 8px; + font-size: 12px; + color: var(--text3); + line-height: 1.35; +} +.exercise-filter-skill-levels-row { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 10px; +} +.exercise-filter-skill-level-field { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} +.exercise-filter-skill-level-caption { + font-size: 11px; + color: var(--text3); + font-weight: 600; +} +.exercise-filter-level-select { + width: 72px; + padding: 6px 8px; + font-size: 14px; +} +.exercise-filter-skill-dash { + padding-bottom: 8px; + color: var(--text3); + font-weight: 600; } /* Reifegradmodell-Admin: klare Schritte, responsives Raster */ diff --git a/frontend/src/pages/ExercisesListPage.jsx b/frontend/src/pages/ExercisesListPage.jsx index 72aab3b..49eb660 100644 --- a/frontend/src/pages/ExercisesListPage.jsx +++ b/frontend/src/pages/ExercisesListPage.jsx @@ -19,17 +19,9 @@ const INITIAL_FILTERS = { 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 levelOptionShort(levelStr) { + const o = LEVEL_FILTER_OPTS.find((x) => String(x.level) === String(levelStr)) + return o ? String(o.level) : String(levelStr) } function ExercisesListPage() { @@ -53,8 +45,6 @@ function ExercisesListPage() { const [filters, setFilters] = useState(() => ({ ...INITIAL_FILTERS })) const [filterModalOpen, setFilterModalOpen] = useState(false) - const activeFilterGroups = useMemo(() => countActiveFilterGroups(filters), [filters]) - useEffect(() => { const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400) return () => clearTimeout(t) @@ -116,6 +106,122 @@ function ExercisesListPage() { [] ) + const filterChips = useMemo(() => { + const chips = [] + + ;(filters.focus_area_ids || []).forEach((id) => { + const opt = focusOptions.find((o) => String(o.id) === String(id)) + chips.push({ + key: `fa-${id}`, + label: `Fokus: ${opt?.label ?? id}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + focus_area_ids: prev.focus_area_ids.filter((x) => String(x) !== String(id)), + })), + }) + }) + ;(filters.style_direction_ids || []).forEach((id) => { + const opt = styleOptions.find((o) => String(o.id) === String(id)) + chips.push({ + key: `sd-${id}`, + label: `Stil: ${opt?.label ?? id}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + style_direction_ids: prev.style_direction_ids.filter((x) => String(x) !== String(id)), + })), + }) + }) + ;(filters.training_type_ids || []).forEach((id) => { + const opt = trainingTypeOptions.find((o) => String(o.id) === String(id)) + chips.push({ + key: `tt-${id}`, + label: `Trainingsstil: ${opt?.label ?? id}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + training_type_ids: prev.training_type_ids.filter((x) => String(x) !== String(id)), + })), + }) + }) + ;(filters.target_group_ids || []).forEach((id) => { + const opt = targetGroupOptions.find((o) => String(o.id) === String(id)) + chips.push({ + key: `tg-${id}`, + label: `Zielgruppe: ${opt?.label ?? id}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + target_group_ids: prev.target_group_ids.filter((x) => String(x) !== String(id)), + })), + }) + }) + ;(filters.skill_ids || []).forEach((id) => { + const opt = skillOptions.find((o) => String(o.id) === String(id)) + chips.push({ + key: `sk-${id}`, + label: `Fähigkeit: ${opt?.label ?? id}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + skill_ids: prev.skill_ids.filter((x) => String(x) !== String(id)), + })), + }) + }) + + if (filters.skill_min_level || filters.skill_max_level) { + const a = filters.skill_min_level ? levelOptionShort(filters.skill_min_level) : '…' + const b = filters.skill_max_level ? levelOptionShort(filters.skill_max_level) : '…' + chips.push({ + key: 'skill-levels', + label: `Stufe ${a}–${b}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + skill_min_level: '', + skill_max_level: '', + })), + }) + } + + ;(filters.visibility_any || []).forEach((id) => { + const opt = visibilityOptions.find((o) => String(o.id) === String(id)) + chips.push({ + key: `vis-${id}`, + label: `Sichtbarkeit: ${opt?.label ?? id}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + visibility_any: prev.visibility_any.filter((x) => String(x) !== String(id)), + })), + }) + }) + ;(filters.status_any || []).forEach((id) => { + const opt = statusOptions.find((o) => String(o.id) === String(id)) + chips.push({ + key: `st-${id}`, + label: `Status: ${opt?.label ?? id}`, + onRemove: () => + setFilters((prev) => ({ + ...prev, + status_any: prev.status_any.filter((x) => String(x) !== String(id)), + })), + }) + }) + + return chips + }, [ + filters, + focusOptions, + styleOptions, + trainingTypeOptions, + targetGroupOptions, + skillOptions, + visibilityOptions, + statusOptions, + ]) + const queryBase = useMemo(() => { const q = {} const n = (v) => (v === '' || v == null ? undefined : Number(v)) @@ -278,18 +384,37 @@ function ExercisesListPage() {
Vereins-/Trainerfilter folgen später. Fachliche Filter über „Filter“ – zwischen Feldern UND, Auswahl mehrerer Werte je Feld mit ODER.
@@ -324,47 +449,55 @@ function ExercisesListPage() {- Zwischen den Bereichen gilt UND. Innerhalb eines Bereichs werden mehrere Einträge mit{' '} - ODER verknüpft (die Übung muss mindestens eine gewählte Zuordnung erfüllen). + Zwischen den Bereichen gilt UND. Innerhalb eines Feldes werden mehrere Einträge mit{' '} + ODER verknüpft.
-+ Die Stufen filtern nach dem Niveau der Zuordnung Übung ↔ Fähigkeit (von–bis). +
+