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() {
) : ( -