From 1e1fd80fb7e3366e19fd6c88258629dfbb394611 Mon Sep 17 00:00:00 2001
From: Lars
Date: Wed, 6 May 2026 12:41:04 +0200
Subject: [PATCH] feat: enhance card layouts and UI components across multiple
pages
- 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.
---
frontend/src/app.css | 80 +++++++++++++++----
frontend/src/pages/ClubsPage.jsx | 12 +--
frontend/src/pages/ExercisesListPage.jsx | 52 +++++++++++-
.../TrainingFrameworkProgramsListPage.jsx | 4 +-
4 files changed, 122 insertions(+), 26 deletions(-)
diff --git a/frontend/src/app.css b/frontend/src/app.css
index a9ef5e3..9946aa7 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -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 {
diff --git a/frontend/src/pages/ClubsPage.jsx b/frontend/src/pages/ClubsPage.jsx
index 1cfc127..153dce7 100644
--- a/frontend/src/pages/ClubsPage.jsx
+++ b/frontend/src/pages/ClubsPage.jsx
@@ -504,11 +504,13 @@ function ClubsPage() {
) : (
-
+
{groups.map(group => (
{group.name}
diff --git a/frontend/src/pages/ExercisesListPage.jsx b/frontend/src/pages/ExercisesListPage.jsx
index 2bfaa37..807b04d 100644
--- a/frontend/src/pages/ExercisesListPage.jsx
+++ b/frontend/src/pages/ExercisesListPage.jsx
@@ -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 (
+
+
+
+
+
+ ·
+
+
+
+
+
+ )
+}
+
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) => (
{name}
))}
-
{visibilityLabel(exercise.visibility)}
-
{statusLabel(exercise.status)}
{summaryHtml ? (
-
)
diff --git a/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx b/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx
index d4ba42e..0194980 100644
--- a/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx
+++ b/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx
@@ -159,9 +159,9 @@ export default function TrainingFrameworkProgramsListPage() {
) : (
-