refactor: update loading state management and enhance search functionality in ExercisesListPage
- 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:
parent
756263bad4
commit
d5fbc2cd5c
|
|
@ -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' : ''}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user