- Added support for style direction mappings in the backend, allowing for improved categorization of exercises. - Introduced a new function to normalize property synonyms, enhancing the mapping of exercise properties. - Updated the exercise catalog assignment logic to include style directions, ensuring proper database entries. - Enhanced the ExercisesListPage with new filtering options for style directions, improving user experience and search capabilities.
68 lines
2.0 KiB
JavaScript
68 lines
2.0 KiB
JavaScript
import React, { useMemo, useState } from 'react'
|
|
|
|
/**
|
|
* Kombination aus Filter-Eingabe und Auswahl: Liste wird per Tippen eingeschränkt.
|
|
* Die aktuelle Auswahl bleibt sichtbar, auch wenn sie durch den Filter ausgeblendet würde.
|
|
*/
|
|
export default function SearchableSelect({
|
|
value,
|
|
onChange,
|
|
options = [],
|
|
idKey = 'id',
|
|
labelKey = 'label',
|
|
allLabel = 'Alle',
|
|
filterPlaceholder = 'Tippen zum Filtern…',
|
|
selectClassName = 'form-input',
|
|
className = '',
|
|
}) {
|
|
const [q, setQ] = useState('')
|
|
|
|
const rows = useMemo(() => {
|
|
return options.map((o) => ({
|
|
id: o[idKey],
|
|
label: typeof o[labelKey] === 'function' ? o[labelKey](o) : String(o[labelKey] ?? ''),
|
|
}))
|
|
}, [options, idKey, labelKey])
|
|
|
|
const filtered = useMemo(() => {
|
|
const t = q.trim().toLowerCase()
|
|
if (!t) return rows
|
|
return rows.filter((r) => r.label.toLowerCase().includes(t) || String(r.id).includes(t))
|
|
}, [rows, q])
|
|
|
|
const withSelection = useMemo(() => {
|
|
if (value === '' || value == null) return filtered
|
|
const sel = String(value)
|
|
if (filtered.some((r) => String(r.id) === sel)) return filtered
|
|
const found = rows.find((r) => String(r.id) === sel)
|
|
if (found) return [found, ...filtered]
|
|
return filtered
|
|
}, [filtered, rows, value])
|
|
|
|
return (
|
|
<div className={className} style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
<input
|
|
type="search"
|
|
className={selectClassName}
|
|
placeholder={filterPlaceholder}
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
autoComplete="off"
|
|
aria-label="Filter"
|
|
/>
|
|
<select
|
|
className={selectClassName}
|
|
value={value === '' || value == null ? '' : String(value)}
|
|
onChange={(e) => onChange(e.target.value === '' ? '' : e.target.value)}
|
|
>
|
|
<option value="">{allLabel}</option>
|
|
{withSelection.map((r) => (
|
|
<option key={String(r.id)} value={r.id}>
|
|
{r.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)
|
|
}
|