pre-Prod Alpha #18

Merged
Lars merged 8 commits from develop into main 2026-05-07 10:37:21 +02:00
3 changed files with 163 additions and 67 deletions
Showing only changes of commit 40a3b4b8e6 - Show all commits

View File

@ -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));

View File

@ -191,49 +191,48 @@ 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 ? (
{!phase0Err && phase0Stats ? ( <div className="dashboard-phase0-kpis__loading muted">Zahlen werden geladen</div>
<> ) : null}
<Link className="dashboard-kpi-card" to={draftsHref}> {!phase0Err && phase0Stats ? (
<span className="dashboard-kpi-card__icon" aria-hidden> <div className="dashboard-phase0-kpis">
<FilePenLine size={22} strokeWidth={2} /> <Link className="dashboard-kpi-card" to={draftsHref}>
</span> <span className="dashboard-kpi-card__icon" aria-hidden>
<span className="dashboard-kpi-card__value"> <FilePenLine size={22} strokeWidth={2} />
{formatCappedCount(phase0Stats.draftCount, phase0Stats.draftCapped)} </span>
</span> <span className="dashboard-kpi-card__value">
<span className="dashboard-kpi-card__label">Übungs-Entwürfe</span> {formatCappedCount(phase0Stats.draftCount, phase0Stats.draftCapped)}
<span className="dashboard-kpi-card__hint">finalisieren</span> </span>
</Link> <span className="dashboard-kpi-card__label">Übungs-Entwürfe</span>
<Link className="dashboard-kpi-card" to={mineHref}> <span className="dashboard-kpi-card__hint">finalisieren</span>
<span className="dashboard-kpi-card__icon" aria-hidden> </Link>
<Library size={22} strokeWidth={2} /> <Link className="dashboard-kpi-card" to={mineHref}>
</span> <span className="dashboard-kpi-card__icon" aria-hidden>
<span className="dashboard-kpi-card__value"> <Library size={22} strokeWidth={2} />
{formatCappedCount(phase0Stats.mineCount, phase0Stats.mineCapped)} </span>
</span> <span className="dashboard-kpi-card__value">
<span className="dashboard-kpi-card__label">Meine Übungen</span> {formatCappedCount(phase0Stats.mineCount, phase0Stats.mineCapped)}
<span className="dashboard-kpi-card__hint">alle Status</span> </span>
</Link> <span className="dashboard-kpi-card__label">Meine Übungen</span>
<div className="dashboard-kpi-card dashboard-kpi-card--static"> <span className="dashboard-kpi-card__hint">alle Status</span>
<span className="dashboard-kpi-card__icon" aria-hidden> </Link>
<ClipboardList size={22} strokeWidth={2} /> <div className="dashboard-kpi-card dashboard-kpi-card--static">
</span> <span className="dashboard-kpi-card__icon" aria-hidden>
<span className="dashboard-kpi-card__value"> <ClipboardList size={22} strokeWidth={2} />
{formatCappedCount(phase0Stats.ytdCompletedCount, phase0Stats.ytdCapped)} </span>
</span> <span className="dashboard-kpi-card__value">
<span className="dashboard-kpi-card__label">Gehalten {phase0Stats.year}</span> {formatCappedCount(phase0Stats.ytdCompletedCount, phase0Stats.ytdCapped)}
<span className="dashboard-kpi-card__hint">abrechnungsnah</span> </span>
</div> <span className="dashboard-kpi-card__label">Gehalten {phase0Stats.year}</span>
</> <span className="dashboard-kpi-card__hint">abrechnungsnah</span>
) : !phase0Err ? ( </div>
<div className="dashboard-phase0-kpis__loading muted">Zahlen werden geladen</div> </div>
) : null} ) : null}
</div>
</section> </section>
<section className="dashboard-section" aria-labelledby="dash-trainings-title"> <section className="dashboard-section" aria-labelledby="dash-trainings-title">

View File

@ -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'
return { const statusDraft = sp.get('status') === 'draft'
...mergedFilters,
status_rules: [{ key: 'url-dashboard-draft', id: 'draft', mode: 'require' }], 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 {
...mergedFromPrefs,
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,20 +858,30 @@ 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">
<button type="button" className="btn btn-secondary exercise-filter-trigger" onClick={() => setFilterModalOpen(true)}> <div className="exercise-search-bar__actions-main">
Filter <button
{filterChips.length > 0 ? ( type="button"
<span className="exercise-filter-badge" aria-hidden> className={'btn btn-secondary exercise-mine-toggle' + (mineOnly ? ' exercise-mine-toggle--active' : '')}
{filterChips.length} onClick={() => setMineOnly((v) => !v)}
</span> title="Nur Übungen, die mit deinem Profil als Ersteller gespeichert sind"
) : null} >
</button> Meine Übungen
{filterChips.length > 0 ? (
<button type="button" className="btn" onClick={resetAllFilters}>
Alle entfernen
</button> </button>
) : null} <button type="button" className="btn btn-secondary exercise-filter-trigger" onClick={() => setFilterModalOpen(true)}>
Filter
{filterChips.length > 0 ? (
<span className="exercise-filter-badge" aria-hidden>
{filterChips.length}
</span>
) : null}
</button>
{filterChips.length > 0 ? (
<button type="button" className="btn" onClick={resetAllFilters}>
Alle entfernen
</button>
) : 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">