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 ( +