feat: enhance card layouts and UI components across multiple pages
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 10s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Failing after 29s

- Updated CSS styles to improve card spacing and layout consistency in grid formats.
- Introduced a new card-grid class for better handling of card arrangements in ClubsPage and TrainingFrameworkProgramsListPage.
- Added ExerciseCardScopeStatus component to display visibility and status icons in ExercisesListPage, enhancing user feedback.
- Refactored exercise card actions and footer for improved layout and accessibility.
- Enhanced overall responsiveness and visual clarity across various components.
This commit is contained in:
Lars 2026-05-06 12:41:04 +02:00
parent b9ef0395c1
commit 1e1fd80fb7
4 changed files with 122 additions and 26 deletions

View File

@ -214,12 +214,26 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
/* Cards */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
/* Vertikaler Rhythmus nur im normalen Blockfluss — in Grids/Flex mit gap stört margin-top zwischen Geschwistern */
.card + .card { margin-top: 12px; }
/* In CSS-Grids: Abstände nur über gap, nicht über Adjacent-Sibling-Margin */
ul > li.card + li.card,
.exercises-list-grid > .card + .card,
.ref-value-tiles-grid > .card + .card {
.ref-value-tiles-grid > .card + .card,
.skills-page__card-grid > .card + .card,
.dashboard-training-grid > .card + .card,
.framework-slots-board > .card + .card,
[class*="slots-board"] > .card + .card,
.card-grid > .card + .card,
.clubs-groups-card-grid > .card + .card {
margin-top: 0;
}
/* Optional: Raster für Karten (Abstände nur über gap); Spalten per Modifier oder inline grid-template-columns */
.card-grid {
display: grid;
gap: 14px;
align-items: stretch;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
}
.card-title { font-size: 13px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 10px; }
/* Stats grid */
@ -1509,6 +1523,7 @@ button.capture-shell__nav-item {
* viele flache Tabs (z. B. Stammdaten) .admin-page-subtabs (gleiche Chip-Idee, Edge-Scroll mobil)
* Wechsel zwischen Admin-Seiten .admin-top-nav
* Sub-Sub (dritte Ebene, z. B. Editor-Spalten): bewusst in jeweiligen Feature-Layouts (Seitenleiste / Panel).
* Karten-Raster: .card-grid oder Klassen mit *list-grid* / *slots-board* / .dashboard-training-grid dort kein .card+.card-Abstand (nur gap).
* ---------- */
/* Admin-Kataloge: Seite „Stammdaten“ — viele Unter-Tabs, Chip-Scroll */
@ -3779,19 +3794,53 @@ button.capture-shell__nav-item {
line-height: 1.45;
}
.exercise-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: auto;
padding-top: 10px;
border-top: 1px solid var(--border);
flex-wrap: nowrap;
min-height: 44px;
box-sizing: border-box;
}
.exercise-card__meta-compact {
display: inline-flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
color: var(--text3);
font-size: 0;
line-height: 0;
}
.exercise-card__meta-glyph {
display: inline-flex;
color: var(--text3);
opacity: 0.9;
}
.exercise-card__meta-sep {
font-size: 11px;
line-height: 1;
opacity: 0.45;
user-select: none;
padding: 0 1px;
}
.exercise-card__actions {
flex-shrink: 0;
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: auto;
padding-top: 10px;
border-top: 1px solid var(--border);
margin-top: 0;
padding-top: 0;
border-top: none;
}
.exercise-card__actions--icons {
justify-content: flex-end;
gap: 8px;
flex-wrap: nowrap;
margin-left: auto;
}
.exercise-card__icon-btn {
display: inline-flex;
@ -3868,17 +3917,18 @@ button.capture-shell__nav-item {
color: var(--text1);
border-color: var(--border2);
}
.exercise-tag--scope {
font-weight: 700;
background: var(--surface);
color: var(--text2);
/* Liste Rahmenprogramme: Abstand nur über gap (kein .card+.card zwischen li) */
.framework-programs-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.exercise-tag--meta {
font-weight: 600;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text3);
.framework-programs-list > li.card {
margin-bottom: 0;
}
.exercise-detail-shell {

View File

@ -504,11 +504,13 @@ function ClubsPage() {
</p>
</div>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: '1rem'
}}>
<div
className="card-grid clubs-groups-card-grid"
style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: '1rem'
}}
>
{groups.map(group => (
<div key={group.id} className="card">
<h3 style={{ marginBottom: '0.5rem' }}>{group.name}</h3>

View File

@ -1,6 +1,17 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { Eye, Pencil, Trash2 } from 'lucide-react'
import {
Eye,
Pencil,
Trash2,
Globe,
Users,
Lock,
CheckCircle2,
Archive,
CircleDot,
FilePenLine,
} from 'lucide-react'
import api from '../utils/api'
import { useAuth } from '../context/AuthContext'
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
@ -56,6 +67,38 @@ function exerciseCardClassName(exercise, userId) {
.join(' ')
}
function ExerciseCardScopeStatus({ exercise }) {
const v = exercise.visibility || 'private'
const s = exercise.status || 'draft'
const visLabel = visibilityLabel(v)
const stLabel = statusLabel(s)
const tip = `${visLabel} · ${stLabel}`
let VisIcon = Lock
if (v === 'official') VisIcon = Globe
else if (v === 'club') VisIcon = Users
let StatIcon = FilePenLine
if (s === 'approved') StatIcon = CheckCircle2
else if (s === 'archived') StatIcon = Archive
else if (s === 'in_review') StatIcon = CircleDot
return (
<div
className="exercise-card__meta-compact"
title={tip}
aria-label={`Sichtbarkeit: ${visLabel}. Status: ${stLabel}.`}
>
<span className="exercise-card__meta-glyph">
<VisIcon size={15} strokeWidth={2} aria-hidden />
</span>
<span className="exercise-card__meta-sep" aria-hidden>
·
</span>
<span className="exercise-card__meta-glyph">
<StatIcon size={15} strokeWidth={2} aria-hidden />
</span>
</div>
)
}
function levelOptionShort(levelStr) {
const o = LEVEL_FILTER_OPTS.find((x) => String(x.level) === String(levelStr))
return o ? String(o.level) : String(levelStr)
@ -1157,8 +1200,6 @@ function ExercisesListPage() {
{typeNames.map((name) => (
<span key={`tt:${name}`} className="exercise-tag exercise-tag--training">{name}</span>
))}
<span className="exercise-tag exercise-tag--scope">{visibilityLabel(exercise.visibility)}</span>
<span className="exercise-tag exercise-tag--meta">{statusLabel(exercise.status)}</span>
</div>
{summaryHtml ? (
<div
@ -1168,7 +1209,9 @@ function ExercisesListPage() {
) : null}
</div>
</div>
<div className="exercise-card__actions exercise-card__actions--icons">
<div className="exercise-card__footer">
<ExerciseCardScopeStatus exercise={exercise} />
<div className="exercise-card__actions exercise-card__actions--icons">
<Link
to={`/exercises/${exercise.id}`}
className="exercise-card__icon-btn"
@ -1194,6 +1237,7 @@ function ExercisesListPage() {
>
<Trash2 size={18} strokeWidth={2} aria-hidden />
</button>
</div>
</div>
</div>
)

View File

@ -159,9 +159,9 @@ export default function TrainingFrameworkProgramsListPage() {
</Link>
</div>
) : (
<ul style={{ listStyle: 'none' }}>
<ul className="framework-programs-list">
{rows.map((r) => (
<li key={r.id} className="card" style={{ marginBottom: '12px' }}>
<li key={r.id} className="card">
<div
style={{
display: 'flex',