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' : ''}