refactor: update loading state management and enhance search functionality in ExercisesListPage
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 1m55s

- Replaced loading state with listFetching for clearer intent in managing exercise list fetching.
- Introduced search title suggestions for improved user experience during exercise searches.
- Updated UI elements to reflect changes in loading states and added datalist for search inputs, enhancing usability.
- Adjusted text for loading indicators to provide a more localized experience.
This commit is contained in:
Lars 2026-04-28 15:24:05 +02:00
parent 756263bad4
commit d5fbc2cd5c

View File

@ -34,7 +34,7 @@ function ExercisesListPage() {
skills: [], skills: [],
}) })
const [catalogsReady, setCatalogsReady] = useState(false) const [catalogsReady, setCatalogsReady] = useState(false)
const [loading, setLoading] = useState(true) const [listFetching, setListFetching] = useState(false)
const [loadingMore, setLoadingMore] = useState(false) const [loadingMore, setLoadingMore] = useState(false)
const [offset, setOffset] = useState(0) const [offset, setOffset] = useState(0)
const [hasMore, setHasMore] = useState(false) const [hasMore, setHasMore] = useState(false)
@ -222,6 +222,12 @@ function ExercisesListPage() {
statusOptions, statusOptions,
]) ])
/** Für Browser-/datalist-Vorschläge (aktuelle Treffer-Titel, begrenzt) */
const searchTitleSuggestions = useMemo(() => {
const titles = exercises.map((e) => (e.title || '').trim()).filter(Boolean)
return [...new Set(titles)].slice(0, 80)
}, [exercises])
const queryBase = useMemo(() => { const queryBase = useMemo(() => {
const q = {} const q = {}
const n = (v) => (v === '' || v == null ? undefined : Number(v)) const n = (v) => (v === '' || v == null ? undefined : Number(v))
@ -284,7 +290,7 @@ function ExercisesListPage() {
if (!catalogsReady) return if (!catalogsReady) return
let cancelled = false let cancelled = false
const run = async () => { const run = async () => {
setLoading(true) setListFetching(true)
setOffset(0) setOffset(0)
try { try {
const batch = await api.listExercises({ ...queryBase, limit: PAGE_SIZE, offset: 0 }) const batch = await api.listExercises({ ...queryBase, limit: PAGE_SIZE, offset: 0 })
@ -298,7 +304,7 @@ function ExercisesListPage() {
alert('Fehler beim Laden: ' + err.message) alert('Fehler beim Laden: ' + err.message)
} }
} finally { } finally {
if (!cancelled) setLoading(false) if (!cancelled) setListFetching(false)
} }
} }
run() run()
@ -334,11 +340,11 @@ function ExercisesListPage() {
const resetAllFilters = useCallback(() => setFilters({ ...INITIAL_FILTERS }), []) const resetAllFilters = useCallback(() => setFilters({ ...INITIAL_FILTERS }), [])
if (!catalogsReady || loading) { if (!catalogsReady) {
return ( return (
<div style={{ padding: '2rem', textAlign: 'center' }}> <div style={{ padding: '2rem', textAlign: 'center' }}>
<div className="spinner"></div> <div className="spinner"></div>
<p>Laden...</p> <p>Lade Kataloge…</p>
</div> </div>
) )
} }
@ -363,13 +369,21 @@ function ExercisesListPage() {
<div className="card exercise-search-bar" style={{ marginBottom: '12px' }}> <div className="card exercise-search-bar" style={{ marginBottom: '12px' }}>
<label className="form-label">Volltextsuche (Titel, Ziel, …)</label> <label className="form-label">Volltextsuche (Titel, Ziel, …)</label>
<datalist id="exercise-search-titles">
{searchTitleSuggestions.map((t) => (
<option key={t} value={t} />
))}
</datalist>
<input <input
type="search" type="search"
className="form-input" className="form-input"
placeholder="Suchbegriffe…" placeholder="Suchbegriffe…"
value={searchInput} value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
autoComplete="off" autoComplete="on"
name="exercise-fulltext-search"
list="exercise-search-titles"
enterKeyHint="search"
style={{ marginBottom: '10px' }} style={{ marginBottom: '10px' }}
/> />
<label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label> <label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label>
@ -379,7 +393,10 @@ function ExercisesListPage() {
placeholder="zweiter Begriff — zusätzliche Volltextsuche (ODER)" placeholder="zweiter Begriff — zusätzliche Volltextsuche (ODER)"
value={aiSearchInput} value={aiSearchInput}
onChange={(e) => setAiSearchInput(e.target.value)} onChange={(e) => setAiSearchInput(e.target.value)}
autoComplete="off" autoComplete="on"
name="exercise-ai-search"
list="exercise-search-titles"
enterKeyHint="search"
/> />
<div className="exercise-search-bar__actions"> <div className="exercise-search-bar__actions">
<button type="button" className="btn btn-secondary exercise-filter-trigger" onClick={() => setFilterModalOpen(true)}> <button type="button" className="btn btn-secondary exercise-filter-trigger" onClick={() => setFilterModalOpen(true)}>
@ -416,7 +433,8 @@ function ExercisesListPage() {
</div> </div>
) : null} ) : null}
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: '10px', marginBottom: 0 }}> <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. Trefferliste aktualisiert sich kurz nach Eingabe. Titel der aktuellen Liste erscheinen als Vorschläge (Pfeil im
Feld). Fachliche Filter über „Filter“ – zwischen Feldern UND, Auswahl mehrerer Werte je Feld mit ODER.
</p> </p>
</div> </div>
@ -584,7 +602,12 @@ function ExercisesListPage() {
</div> </div>
)} )}
{exercises.length === 0 ? ( {listFetching && exercises.length === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '2rem' }}>
<div className="spinner"></div>
<p style={{ color: 'var(--text2)', marginTop: '12px' }}>Lade Übungen…</p>
</div>
) : exercises.length === 0 ? (
<div className="card"> <div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}> <p style={{ color: 'var(--text2)', textAlign: 'center' }}>
Keine Übungen gefunden. Keine Übungen gefunden.
@ -592,6 +615,9 @@ function ExercisesListPage() {
</div> </div>
) : ( ) : (
<> <>
{listFetching ? (
<p style={{ fontSize: '13px', color: 'var(--text3)', marginBottom: '8px' }}>Aktualisiere Treffer…</p>
) : null}
<p style={{ fontSize: '13px', color: 'var(--text2)', marginBottom: '10px' }}> <p style={{ fontSize: '13px', color: 'var(--text2)', marginBottom: '10px' }}>
{exercises.length} angezeigt {exercises.length} angezeigt
{hasMore ? ' · es gibt weitere Einträge' : ''} {hasMore ? ' · es gibt weitere Einträge' : ''}