shinkan-jinkendo/frontend/src/components/SearchableSelect.jsx
Lars 025b161d2f
Some checks failed
Deploy Development / deploy (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 1m56s
feat: enhance exercise mapping and filtering capabilities
- 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.
2026-04-28 07:25:33 +02:00

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>
)
}