feat: enhance dashboard and exercises list UI with new styles and filtering options
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
All checks were successful
Deploy Development / deploy (push) Successful in 37s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
- Added responsive styles for dashboard KPI cards and exercise search actions, improving layout on smaller screens. - Refactored the Dashboard component to streamline error handling and loading states for better user experience. - Updated ExercisesListPage to include new filtering options for user-created exercises, enhancing data relevance. - Improved overall CSS organization and introduced new classes for better styling consistency across components.
This commit is contained in:
parent
3bf0af001a
commit
40a3b4b8e6
|
|
@ -2706,6 +2706,21 @@ a.analysis-split__nav-item {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
.exercise-search-bar__actions--split {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.exercise-search-bar__actions-main {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.exercise-mine-toggle--active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-light);
|
||||||
|
color: var(--accent-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.exercise-filter-trigger {
|
.exercise-filter-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -3607,13 +3622,11 @@ a.analysis-split__nav-item {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-phase0-kpis__loading {
|
.dashboard-phase0-kpis__loading {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin: 0;
|
margin: 0 0 10px;
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-kpi-card {
|
.dashboard-kpi-card {
|
||||||
|
|
@ -3685,6 +3698,55 @@ a.analysis-split__nav-item {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.dashboard-phase0-kpis {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding: 2px 0 4px;
|
||||||
|
margin-left: calc(-1 * max(12px, env(safe-area-inset-left, 0px)));
|
||||||
|
margin-right: calc(-1 * max(12px, env(safe-area-inset-right, 0px)));
|
||||||
|
padding-left: max(12px, env(safe-area-inset-left, 0px));
|
||||||
|
padding-right: max(12px, env(safe-area-inset-right, 0px));
|
||||||
|
}
|
||||||
|
.dashboard-phase0-kpis::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.dashboard-kpi-card {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
width: min(132px, 38vw);
|
||||||
|
min-height: 0;
|
||||||
|
padding: 10px 10px 8px;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.dashboard-kpi-card__icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.dashboard-kpi-card__icon svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.dashboard-kpi-card__value {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
.dashboard-kpi-card__label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
.dashboard-kpi-card__hint {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-training-preview-grid {
|
.dashboard-training-preview-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
|
||||||
|
|
|
||||||
|
|
@ -191,14 +191,16 @@ function Dashboard() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dashboard-phase0-kpis">
|
|
||||||
{phase0Err ? (
|
{phase0Err ? (
|
||||||
<p className="dashboard-phase0-kpis__err" role="alert">
|
<p className="dashboard-phase0-kpis__err" role="alert">
|
||||||
{phase0Err}
|
{phase0Err}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!phase0Err && !phase0Stats ? (
|
||||||
|
<div className="dashboard-phase0-kpis__loading muted">Zahlen werden geladen…</div>
|
||||||
|
) : null}
|
||||||
{!phase0Err && phase0Stats ? (
|
{!phase0Err && phase0Stats ? (
|
||||||
<>
|
<div className="dashboard-phase0-kpis">
|
||||||
<Link className="dashboard-kpi-card" to={draftsHref}>
|
<Link className="dashboard-kpi-card" to={draftsHref}>
|
||||||
<span className="dashboard-kpi-card__icon" aria-hidden>
|
<span className="dashboard-kpi-card__icon" aria-hidden>
|
||||||
<FilePenLine size={22} strokeWidth={2} />
|
<FilePenLine size={22} strokeWidth={2} />
|
||||||
|
|
@ -229,11 +231,8 @@ function Dashboard() {
|
||||||
<span className="dashboard-kpi-card__label">Gehalten {phase0Stats.year}</span>
|
<span className="dashboard-kpi-card__label">Gehalten {phase0Stats.year}</span>
|
||||||
<span className="dashboard-kpi-card__hint">abrechnungsnah</span>
|
<span className="dashboard-kpi-card__hint">abrechnungsnah</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
) : !phase0Err ? (
|
|
||||||
<div className="dashboard-phase0-kpis__loading muted">Zahlen werden geladen…</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="dashboard-section" aria-labelledby="dash-trainings-title">
|
<section className="dashboard-section" aria-labelledby="dash-trainings-title">
|
||||||
|
|
|
||||||
|
|
@ -123,16 +123,29 @@ function levelOptionShort(levelStr) {
|
||||||
return o ? String(o.level) : String(levelStr)
|
return o ? String(o.level) : String(levelStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
function exerciseListFiltersWithDashboardUrl(mergedFilters) {
|
function applyDashboardExerciseListUrl(mergedFromPrefs) {
|
||||||
try {
|
try {
|
||||||
const sp = new URLSearchParams(window.location.search)
|
const sp = new URLSearchParams(window.location.search)
|
||||||
if (sp.get('status') !== 'draft') return mergedFilters
|
const mine = sp.get('mine') === '1' || sp.get('created_by_me') === '1'
|
||||||
|
const statusDraft = sp.get('status') === 'draft'
|
||||||
|
|
||||||
|
if (mine) {
|
||||||
|
const next = { ...INITIAL_EXERCISE_LIST_FILTERS }
|
||||||
|
if (statusDraft) {
|
||||||
|
next.status_rules = [{ key: 'url-dashboard-draft', id: 'draft', mode: 'require' }]
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusDraft) {
|
||||||
return {
|
return {
|
||||||
...mergedFilters,
|
...mergedFromPrefs,
|
||||||
status_rules: [{ key: 'url-dashboard-draft', id: 'draft', mode: 'require' }],
|
status_rules: [{ key: 'url-dashboard-draft', id: 'draft', mode: 'require' }],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return mergedFromPrefs
|
||||||
} catch {
|
} catch {
|
||||||
return mergedFilters
|
return mergedFromPrefs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +205,7 @@ function ExercisesListPage() {
|
||||||
if (!user?.id) return
|
if (!user?.id) return
|
||||||
if (prefsAppliedRef.current) return
|
if (prefsAppliedRef.current) return
|
||||||
const merged = mergeExerciseListPrefsFromApi(user.exercise_list_prefs)
|
const merged = mergeExerciseListPrefsFromApi(user.exercise_list_prefs)
|
||||||
setFilters(exerciseListFiltersWithDashboardUrl(merged))
|
setFilters(applyDashboardExerciseListUrl(merged))
|
||||||
try {
|
try {
|
||||||
const sp = new URLSearchParams(window.location.search)
|
const sp = new URLSearchParams(window.location.search)
|
||||||
if (sp.get('mine') === '1' || sp.get('created_by_me') === '1') setMineOnly(true)
|
if (sp.get('mine') === '1' || sp.get('created_by_me') === '1') setMineOnly(true)
|
||||||
|
|
@ -270,6 +283,14 @@ function ExercisesListPage() {
|
||||||
const filterChips = useMemo(() => {
|
const filterChips = useMemo(() => {
|
||||||
const chips = []
|
const chips = []
|
||||||
|
|
||||||
|
if (mineOnly) {
|
||||||
|
chips.push({
|
||||||
|
key: 'mine-only',
|
||||||
|
label: 'Nur von mir erstellt',
|
||||||
|
onRemove: () => setMineOnly(false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pushCatalogRuleFilterChips(chips, 'focus_rules', filters.focus_rules, focusOptions, 'Fokus', setFilters)
|
pushCatalogRuleFilterChips(chips, 'focus_rules', filters.focus_rules, focusOptions, 'Fokus', setFilters)
|
||||||
|
|
||||||
if (filters.focus_only_without) {
|
if (filters.focus_only_without) {
|
||||||
|
|
@ -410,6 +431,7 @@ function ExercisesListPage() {
|
||||||
|
|
||||||
return chips
|
return chips
|
||||||
}, [
|
}, [
|
||||||
|
mineOnly,
|
||||||
filters,
|
filters,
|
||||||
focusOptions,
|
focusOptions,
|
||||||
styleOptions,
|
styleOptions,
|
||||||
|
|
@ -625,7 +647,10 @@ function ExercisesListPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetAllFilters = useCallback(() => setFilters({ ...INITIAL_EXERCISE_LIST_FILTERS }), [])
|
const resetAllFilters = useCallback(() => {
|
||||||
|
setMineOnly(false)
|
||||||
|
setFilters({ ...INITIAL_EXERCISE_LIST_FILTERS })
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSaveExerciseFilterPrefs = useCallback(async () => {
|
const handleSaveExerciseFilterPrefs = useCallback(async () => {
|
||||||
const uid = user?.id
|
const uid = user?.id
|
||||||
|
|
@ -833,7 +858,16 @@ function ExercisesListPage() {
|
||||||
list="exercise-search-titles"
|
list="exercise-search-titles"
|
||||||
enterKeyHint="search"
|
enterKeyHint="search"
|
||||||
/>
|
/>
|
||||||
<div className="exercise-search-bar__actions">
|
<div className="exercise-search-bar__actions exercise-search-bar__actions--split">
|
||||||
|
<div className="exercise-search-bar__actions-main">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={'btn btn-secondary exercise-mine-toggle' + (mineOnly ? ' exercise-mine-toggle--active' : '')}
|
||||||
|
onClick={() => setMineOnly((v) => !v)}
|
||||||
|
title="Nur Übungen, die mit deinem Profil als Ersteller gespeichert sind"
|
||||||
|
>
|
||||||
|
Meine Übungen
|
||||||
|
</button>
|
||||||
<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)}>
|
||||||
Filter
|
Filter
|
||||||
{filterChips.length > 0 ? (
|
{filterChips.length > 0 ? (
|
||||||
|
|
@ -848,6 +882,7 @@ function ExercisesListPage() {
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{filterChips.length > 0 ? (
|
{filterChips.length > 0 ? (
|
||||||
<div className="exercise-filter-chips-row" role="list" aria-label="Aktive Filter">
|
<div className="exercise-filter-chips-row" role="list" aria-label="Aktive Filter">
|
||||||
{filterChips.map((c) => (
|
{filterChips.map((c) => (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user