From d19a1061d83d98c36ea5b17ebf929413636816e0 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 20 May 2026 06:37:40 +0200 Subject: [PATCH] Enhance exercise listing with variant and media counts - Updated the `list_exercises` function to include counts for exercise variants and media, improving data retrieval for exercise details. - Added new CSS styles for the exercise card footer to display variant and media statistics in a visually appealing manner. - Implemented `ExerciseCardContentStats` component to conditionally render variant and media counts, enhancing the user interface of exercise cards. --- backend/routers/exercises.py | 14 ++++- frontend/src/app.css | 28 ++++++++++ .../components/exercises/ExerciseListCard.jsx | 53 ++++++++++++++++++- 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py index 02ffcc6..8fd392e 100644 --- a/backend/routers/exercises.py +++ b/backend/routers/exercises.py @@ -2095,7 +2095,17 @@ def list_exercises( FROM exercise_training_types ett JOIN training_types tt ON tt.id = ett.training_type_id WHERE ett.exercise_id = e.id - ) AS training_type_names + ) AS training_type_names, + ( + SELECT COUNT(*)::int + FROM exercise_variants ev + WHERE ev.exercise_id = e.id + ) AS variant_count, + ( + SELECT COUNT(*)::int + FROM exercise_media em + WHERE em.exercise_id = e.id + ) AS media_count {variants_sql} FROM exercises e LEFT JOIN profiles p ON e.created_by = p.id @@ -2118,6 +2128,8 @@ def list_exercises( d["focus_area_names"] = _coerce_json_str_list(d.get("focus_area_names")) d["style_direction_names"] = _coerce_json_str_list(d.get("style_direction_names")) d["training_type_names"] = _coerce_json_str_list(d.get("training_type_names")) + d["variant_count"] = int(d.get("variant_count") or 0) + d["media_count"] = int(d.get("media_count") or 0) if include_variants: v = d.get("variants") if isinstance(v, str): diff --git a/frontend/src/app.css b/frontend/src/app.css index 0c500ce..f5bcf83 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -4819,6 +4819,34 @@ html.modal-scroll-locked .app-main { min-height: 44px; box-sizing: border-box; } +.exercise-card__footer-meta { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + min-width: 0; + flex-shrink: 1; +} +.exercise-card__content-stats { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--text3); +} +.exercise-card__stat { + display: inline-flex; + align-items: center; + gap: 3px; + color: var(--text3); + font-size: 12px; + line-height: 1; +} +.exercise-card__stat-num { + font-variant-numeric: tabular-nums; + font-weight: 600; + font-size: 12px; + line-height: 1; +} .exercise-card__meta-compact { display: inline-flex; align-items: center; diff --git a/frontend/src/components/exercises/ExerciseListCard.jsx b/frontend/src/components/exercises/ExerciseListCard.jsx index 1ef58e8..234c69d 100644 --- a/frontend/src/components/exercises/ExerciseListCard.jsx +++ b/frontend/src/components/exercises/ExerciseListCard.jsx @@ -11,6 +11,8 @@ import { Archive, CircleDot, FilePenLine, + Image, + Layers, } from 'lucide-react' import ExerciseRichTextBlock from '../ExerciseRichTextBlock' import { coerceApiNameList } from '../../utils/sanitizeHtml' @@ -48,6 +50,52 @@ function exerciseCardClassName(exercise, userId) { .join(' ') } +function exerciseVariantCount(exercise) { + if (typeof exercise?.variant_count === 'number' && Number.isFinite(exercise.variant_count)) { + return Math.max(0, exercise.variant_count) + } + return Array.isArray(exercise?.variants) ? exercise.variants.length : 0 +} + +function exerciseMediaCount(exercise) { + if (typeof exercise?.media_count === 'number' && Number.isFinite(exercise.media_count)) { + return Math.max(0, exercise.media_count) + } + return Array.isArray(exercise?.media) ? exercise.media.length : 0 +} + +function ExerciseCardContentStats({ exercise }) { + const mediaCount = exerciseMediaCount(exercise) + const variantCount = exerciseVariantCount(exercise) + if (mediaCount <= 0 && variantCount <= 0) return null + + const mediaLabel = + mediaCount === 1 ? '1 Medium hinterlegt' : `${mediaCount} Medien hinterlegt` + const variantLabel = + variantCount === 1 ? '1 Variante' : `${variantCount} Varianten` + + return ( +
+ {mediaCount > 0 ? ( + + + + {mediaCount} + + + ) : null} + {variantCount > 0 ? ( + + + + {variantCount} + + + ) : null} +
+ ) +} + function ExerciseCardScopeStatus({ exercise }) { const v = exercise.visibility || 'private' const s = exercise.status || 'draft' @@ -138,7 +186,10 @@ export default function ExerciseListCard({ exercise, user, selectedIds, toggleSe
- +
+ + +