From 68923b03646199b8e61a9d76835f5a72432f6ac1 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 6 May 2026 11:24:44 +0200 Subject: [PATCH] feat: enhance UI and functionality for Skills and Exercises pages - Added new CSS styles for Skills and Exercises pages, improving layout and responsiveness. - Refactored components to utilize new styles, enhancing visual consistency and user experience. - Implemented horizontal scrollable navigation for exercises and skills tabs, improving usability on smaller screens. - Updated button styles and introduced new class names for better maintainability and accessibility. - Enhanced loading states and empty messages for improved user feedback during data fetching. --- frontend/src/app.css | 444 ++++++++++++++++++ frontend/src/components/admin/DetailPanel.jsx | 32 +- .../src/components/admin/FocusAreaNode.jsx | 175 +++---- frontend/src/pages/ExercisesListPage.jsx | 159 +++---- frontend/src/pages/SkillsPage.jsx | 208 ++++---- 5 files changed, 706 insertions(+), 312 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index ab83190..bea00ec 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1425,6 +1425,29 @@ button.capture-shell__nav-item { .admin-catalog-section { padding: 14px; } + + .exercises-page-toolbar-tabs, + .skills-page__tabs-scroll { + margin-left: calc(-1 * max(12px, env(safe-area-inset-left, 0px))); + margin-right: calc(-1 * max(12px, env(safe-area-inset-right, 0px))); + padding-left: max(12px, env(safe-area-inset-left, 0px)); + padding-right: max(12px, env(safe-area-inset-right, 0px)); + padding-bottom: 4px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + + .exercises-page-toolbar-tabs::-webkit-scrollbar, + .skills-page__tabs-scroll::-webkit-scrollbar { + display: none; + } + + .exercises-page-mode-switch, + .skills-page-mode-switch { + width: max(100%, min(20rem, 100vw - 24px)); + max-width: none; + } } /* Trainingsplanung: kompakte Segmente (Gruppe / Verein) */ @@ -2302,6 +2325,427 @@ button.capture-shell__nav-item { overscroll-behavior: contain; } +.skills-page-modal.admin-modal-sheet { + max-width: min(600px, 100vw - 32px); +} + +/* Admin Hierarchie: Detail-Panel */ +.detail-panel__title { + margin-top: 0; +} +.detail-panel__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 20px; +} +.detail-panel__context { + padding: 12px; + margin-bottom: 20px; + border-radius: 8px; + background: var(--surface2); + color: var(--text2); +} +.detail-panel__unknown { + padding: 20px; + color: var(--text3); +} + +/* Seite Fähigkeiten & Methoden */ +.skills-page__loading { + padding: 2rem; + text-align: center; +} +.skills-page__tabs-scroll { + margin-bottom: 1.5rem; +} +.skills-page__intro-row { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + margin-bottom: 1rem; +} +.skills-page__intro-row p { + margin: 0; + flex: 1 1 12rem; + color: var(--text2); +} +.skills-page__empty { + margin: 0; + text-align: center; + color: var(--text2); +} +.skills-page__category { + margin-bottom: 2rem; +} +.skills-page__category-title { + margin: 0 0 1rem; + text-transform: capitalize; +} +.skills-page__card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} +.skills-page__card-grid--methods { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); +} +.skills-page-card { + display: flex; + flex-direction: column; + height: 100%; +} +.skills-page-card__head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + margin-bottom: 0.5rem; +} +.skills-page-card__meta-block { + margin-bottom: 0.5rem; +} +.skills-page-card__title { + margin: 0; + font-size: 1rem; +} +.skills-page-card__title--method { + margin: 0 0 0.25rem; +} +.skills-page-card__abbr { + color: var(--text2); + font-size: 0.875rem; + margin-left: 0.5rem; + font-weight: 400; +} +.skills-page-card__badge { + flex-shrink: 0; + font-size: 0.875rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + background: var(--accent); + color: #fff; +} +.skills-page-card__meta-row { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} +.skills-page-card__chip { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + background: var(--surface2); + color: var(--text2); +} +.skills-page-card__desc { + margin: 0 0 1rem; + color: var(--text2); + font-size: 0.875rem; +} +.skills-page-card__actions { + display: flex; + gap: 0.5rem; + margin-top: auto; +} +.skills-page-card__grow { + flex: 1; +} +.skills-page-modal__footer { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1.5rem; +} +.skills-page-modal__submit { + flex: 1; +} + +/* Übungsliste: Kopf, Modus-Segmente, Hinweise */ +.exercises-page__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + flex-wrap: wrap; + gap: 8px; +} +.exercises-page__title { + margin: 0; +} +.exercises-page-toolbar-tabs { + margin-bottom: 14px; +} +.exercises-page-mode-switch, +.skills-page-mode-switch { + width: 100%; + max-width: min(100%, 28rem); +} +.exercise-search-hint { + font-size: 12px; + color: var(--text3); + margin-top: 10px; + margin-bottom: 0; + line-height: 1.45; +} +.exercise-search-hint .btn { + margin-left: 6px; + vertical-align: middle; +} +.exercise-search-bar { + margin-bottom: 12px; +} +.exercise-search-bar__primary { + margin-bottom: 10px; +} +.exercise-bulk-toolbar { + margin-bottom: 12px; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; +} +.exercise-bulk-toolbar__meta { + font-size: 12px; + color: var(--text3); + line-height: 1.4; + flex: 1 1 200px; +} +.exercises-list-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); + gap: 12px; +} +.exercise-card-layout { + display: flex; + gap: 10px; + align-items: flex-start; +} +.exercise-card-layout__check { + margin-top: 4px; + flex-shrink: 0; + accent-color: var(--accent); +} +.exercise-card-body-flex { + flex: 1; + min-width: 0; +} +.exercise-card-title { + margin: 0 0 8px; + font-size: 1.05rem; + line-height: 1.3; + font-weight: 700; +} +.exercise-card-title a { + color: inherit; + text-decoration: none; +} +.exercise-card-title a:hover { + color: var(--accent-dark); +} +@media (prefers-color-scheme: dark) { + .exercise-card-title a:hover { + color: var(--accent); + } +} +.exercise-card-tags { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-bottom: 8px; +} +.exercise-card-summary { + color: var(--text2); + font-size: 13px; + line-height: 1.4; + margin: 0; +} +.exercises-meta-line { + font-size: 13px; + color: var(--text2); + margin: 0 0 10px; +} +.exercises-meta-line--muted { + color: var(--text3); + margin-bottom: 8px; +} +.exercises-load-more { + text-align: center; + margin-top: 16px; +} +.exercises-empty-text { + margin: 0; + color: var(--text2); + text-align: center; +} + +/* Admin Hierarchie-Baum (Fokusbereich) */ +.focus-tree-root { + margin-bottom: 12px; +} +.focus-tree-header { + display: flex; + align-items: stretch; + gap: 2px; + padding: 4px 4px 4px 6px; + border-radius: 10px; + border: 1px solid transparent; + background: transparent; + transition: background 0.12s, border-color 0.12s; +} +.focus-tree-header:hover { + background: var(--surface2); + border-color: var(--border); +} +.focus-tree-header--selected { + background: var(--accent); + border-color: var(--accent); +} +.focus-tree-header--selected:hover { + background: color-mix(in srgb, var(--accent) 94%, #000); + border-color: var(--accent); +} +.focus-tree-toggle { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + min-height: 36px; + margin: 0; + padding: 0; + border: none; + border-radius: 8px; + background: transparent; + color: var(--text2); + cursor: pointer; + align-self: center; + -webkit-tap-highlight-color: transparent; +} +.focus-tree-toggle:hover { + background: rgba(0, 0, 0, 0.05); +} +.focus-tree-header--selected .focus-tree-toggle { + color: #fff; +} +.focus-tree-header--selected .focus-tree-toggle:hover { + background: rgba(255, 255, 255, 0.12); +} +.focus-tree-header__label { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px 6px 2px; + margin: 0; + border: none; + background: transparent; + color: inherit; + font: inherit; + font-weight: 600; + text-align: left; + cursor: pointer; + border-radius: 8px; + -webkit-tap-highlight-color: transparent; +} +.focus-tree-emoji { + flex-shrink: 0; + line-height: 1; +} +.focus-tree-children { + margin-top: 8px; + margin-left: 8px; + padding-left: 12px; + border-left: 2px solid var(--border); +} +.focus-tree-group { + margin-bottom: 12px; +} +.focus-tree-group:last-child { + margin-bottom: 0; +} +.focus-tree-group__head { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + color: var(--text3); + margin-bottom: 6px; + text-transform: uppercase; + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} +.focus-tree-add-btn { + flex-shrink: 0; +} +.focus-tree-item { + padding: 8px 12px; + margin-bottom: 4px; + border-radius: 8px; + cursor: pointer; + background: var(--surface2); + color: var(--text1); + font-size: 14px; + border: 1px solid var(--border); + line-height: 1.35; + transition: background 0.12s, border-color 0.12s, color 0.12s; +} +.focus-tree-item:last-child { + margin-bottom: 0; +} +.focus-tree-item:hover { + border-color: var(--accent); +} +.focus-tree-item--selected { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} +.focus-tree-item--selected:hover { + border-color: var(--accent); +} +.focus-tree-item__abbr { + margin-left: 8px; + font-size: 12px; + opacity: 0.85; +} +.focus-tree-item__meta { + font-size: 11px; + margin-top: 4px; + line-height: 1.35; + opacity: 0.88; +} +.focus-tree-item--selected .focus-tree-item__abbr, +.focus-tree-item--selected .focus-tree-item__meta { + opacity: 0.95; + color: #fff; +} + +@media (max-width: 1023px) { + .exercise-filter-chips-row { + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 4px; + margin-left: -2px; + margin-right: -2px; + padding-left: 2px; + padding-right: 2px; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + .exercise-filter-chips-row::-webkit-scrollbar { + display: none; + } + .focus-tree-children { + margin-left: 4px; + padding-left: 8px; + } +} + .exercise-filter-modal.admin-modal-sheet { max-width: min(920px, calc(100dvw - 16px)); } diff --git a/frontend/src/components/admin/DetailPanel.jsx b/frontend/src/components/admin/DetailPanel.jsx index 4de3552..d4726be 100644 --- a/frontend/src/components/admin/DetailPanel.jsx +++ b/frontend/src/components/admin/DetailPanel.jsx @@ -20,7 +20,7 @@ function DetailPanel({ item, onUpdate, focusAreas }) { return } - return
Unbekannter Typ: {type}
+ return
Unbekannter Typ: {type}
} function FocusAreaDetail({ item, onUpdate }) { @@ -57,7 +57,7 @@ function FocusAreaDetail({ item, onUpdate }) { return (
-

Fokusbereich bearbeiten

+

Fokusbereich bearbeiten

setForm({ ...form, name: e.target.value })} /> @@ -81,11 +81,11 @@ function FocusAreaDetail({ item, onUpdate }) {
-
+
- +
) @@ -130,7 +130,7 @@ function StyleDirectionDetail({ item, onUpdate, focusAreas }) { return (
-

Stilrichtung bearbeiten

+

Stilrichtung bearbeiten

setForm({ ...form, name: e.target.value })} /> @@ -163,11 +163,11 @@ function StyleDirectionDetail({ item, onUpdate, focusAreas }) {
-
+
- +
) @@ -212,7 +212,7 @@ function TrainingTypeDetail({ item, onUpdate, focusAreas }) { return (
-

Trainingstyp bearbeiten

+

Trainingstyp bearbeiten

setForm({ ...form, name: e.target.value })} /> @@ -245,11 +245,11 @@ function TrainingTypeDetail({ item, onUpdate, focusAreas }) {
-
+
- +
) @@ -284,8 +284,8 @@ function CreateStyleDirectionForm({ item, onUpdate }) { return (
-

Neue Stilrichtung erstellen

-
+

Neue Stilrichtung erstellen

+
Fokusbereich: {item.focus_area_name}
@@ -304,7 +304,7 @@ function CreateStyleDirectionForm({ item, onUpdate }) { setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
-
+
@@ -342,8 +342,8 @@ function CreateTrainingTypeForm({ item, onUpdate }) { return (
-

Neuen Trainingstyp erstellen

-
+

Neuen Trainingstyp erstellen

+
Fokusbereich: {item.focus_area_name}
@@ -362,7 +362,7 @@ function CreateTrainingTypeForm({ item, onUpdate }) { setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
-
+
diff --git a/frontend/src/components/admin/FocusAreaNode.jsx b/frontend/src/components/admin/FocusAreaNode.jsx index 104cbef..3f5636f 100644 --- a/frontend/src/components/admin/FocusAreaNode.jsx +++ b/frontend/src/components/admin/FocusAreaNode.jsx @@ -1,4 +1,5 @@ import React from 'react' +import { ChevronDown, ChevronRight } from 'lucide-react' function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, selectedType }) { const nodeId = `fa-${focusArea.id}` @@ -6,82 +7,94 @@ function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, se const isSelected = selectedType === 'focus_area' && selectedId === focusArea.id return ( -
- {/* Focus Area Header */} -
onSelect(focusArea, 'focus_area')} - style={{ - display: 'flex', - alignItems: 'center', - padding: '8px 12px', - borderRadius: '8px', - cursor: 'pointer', - background: isSelected ? 'var(--accent)' : 'transparent', - color: isSelected ? 'white' : 'var(--text1)', - fontWeight: 600 - }} - > - { e.stopPropagation(); onToggle(nodeId) }} - style={{ marginRight: '8px', cursor: 'pointer', fontSize: '18px' }} +
+
+ +
- {/* Children: Style Directions + Training Types */} {isExpanded && ( -
- {/* Style Directions Section */} -
-
+
+
+
Stilrichtungen
- {focusArea.style_directions && focusArea.style_directions.map(sd => ( - - ))} + {focusArea.style_directions && + focusArea.style_directions.map((sd) => ( + + ))}
- {/* Training Types Section */} -
-
+
+
Trainingstypen
- {focusArea.training_types && focusArea.training_types.map(tt => ( - - ))} + {focusArea.training_types && + focusArea.training_types.map((tt) => ( + + ))}
)} @@ -92,28 +105,26 @@ function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, se function StyleDirectionNode({ styleDirection, onSelect, isSelected }) { return (
onSelect(styleDirection, 'style_direction')} - style={{ - padding: '6px 12px', - marginBottom: '4px', - borderRadius: '6px', - cursor: 'pointer', - background: isSelected ? 'var(--accent)' : 'var(--surface2)', - color: isSelected ? 'white' : 'var(--text1)', - fontSize: '14px' + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onSelect(styleDirection, 'style_direction') + } }} > {styleDirection.name} - {styleDirection.abbreviation && ( - - ({styleDirection.abbreviation}) - - )} - {styleDirection.target_groups && styleDirection.target_groups.length > 0 && ( -
- Zielgruppen: {styleDirection.target_groups.map(tg => tg.name).join(', ')} + {styleDirection.abbreviation ? ( + ({styleDirection.abbreviation}) + ) : null} + {styleDirection.target_groups && styleDirection.target_groups.length > 0 ? ( +
+ Zielgruppen: {styleDirection.target_groups.map((tg) => tg.name).join(', ')}
- )} + ) : null}
) } @@ -121,25 +132,23 @@ function StyleDirectionNode({ styleDirection, onSelect, isSelected }) { function TrainingTypeNode({ trainingType, onSelect, isSelected }) { return (
onSelect(trainingType, 'training_type')} - style={{ - padding: '6px 12px', - marginBottom: '4px', - borderRadius: '6px', - cursor: 'pointer', - background: isSelected ? 'var(--accent)' : 'var(--surface2)', - color: isSelected ? 'white' : 'var(--text1)', - fontSize: '14px' + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onSelect(trainingType, 'training_type') + } }} > {trainingType.name} - {trainingType.abbreviation && ( - - ({trainingType.abbreviation}) - - )} + {trainingType.abbreviation ? ( + ({trainingType.abbreviation}) + ) : null}
) } -export default FocusAreaNode +export default FocusAreaNode \ No newline at end of file diff --git a/frontend/src/pages/ExercisesListPage.jsx b/frontend/src/pages/ExercisesListPage.jsx index 6cacfd9..e90a586 100644 --- a/frontend/src/pages/ExercisesListPage.jsx +++ b/frontend/src/pages/ExercisesListPage.jsx @@ -545,65 +545,64 @@ function ExercisesListPage() { if (!catalogsReady && pageTab === 'list') { return ( -
-
-

Lade Kataloge…

+
+
+
+

+ Lade Kataloge… +

+
) } return (
-
-

Übungen

+
+

Übungen

{pageTab === 'list' ? ( + Neu ) : ( - +
-
- - +
+
+ + +
{pageTab === 'progression' ? ( ) : ( <> -
+
{searchTitleSuggestions.map((t) => ( @@ -612,7 +611,7 @@ function ExercisesListPage() { setSearchInput(e.target.value)} @@ -620,7 +619,6 @@ function ExercisesListPage() { name="exercise-fulltext-search" list="exercise-search-titles" enterKeyHint="search" - style={{ marginBottom: '10px' }} /> ) : null} -

+

Trefferliste aktualisiert sich kurz nach Eingabe. Titel der aktuellen Liste erscheinen als Vorschläge (Pfeil im Feld). Fachliche Filter über „Filter“ – zwischen Feldern UND, Auswahl mehrerer Werte je Feld mit ODER. {exercises.length > 0 ? ( <> {' '} - @@ -683,24 +681,15 @@ function ExercisesListPage() {

{selectedIds.size > 0 ? ( -
+
{selectedIds.size} ausgewählt - - - + Bis zu {BULK_MAX_IDS} pro Anfrage. Für „Verein“ ohne Auswahl: aktiver Vereinskontext ( X-Active-Club-Id ). @@ -736,7 +725,7 @@ function ExercisesListPage() {
-

+

Zwischen den Bereichen gilt UND. Innerhalb eines Feldes werden mehrere Einträge mit{' '} ODER verknüpft.

@@ -900,12 +889,12 @@ function ExercisesListPage() {
-

+

Es werden {selectedIds.size} Übung(en) aus der aktuellen Auswahl bearbeitet. Pro Durchlauf höchstens {BULK_MAX_IDS}. Ohne Berechtigung bleiben Einzelübungen unverändert (siehe Hinweis nach dem Speichern).

-

+

Unter „Zuordnung ersetzen“: die gewählte Liste ersetzt die bisherige Zuordnung bei allen betroffenen Übungen vollständig (leere Auswahl = alle Zuordnungen dieser Kategorie entfernen). Die erste Auswahl gilt als Primärzuordnung. @@ -1066,7 +1055,7 @@ function ExercisesListPage() {

-
+
{hasMore && ( -
+
diff --git a/frontend/src/pages/SkillsPage.jsx b/frontend/src/pages/SkillsPage.jsx index 09e66ba..ab977a0 100644 --- a/frontend/src/pages/SkillsPage.jsx +++ b/frontend/src/pages/SkillsPage.jsx @@ -132,7 +132,7 @@ function SkillsPage() { if (loading) { return ( -
+

Laden...

@@ -143,40 +143,38 @@ function SkillsPage() { const methodsByCategory = groupByCategory(methods) return ( -
-

Fähigkeiten & Methoden

+
+

Fähigkeiten & Methoden

- {/* Tabs */} -
+
+
{['skills', 'methods'].map(tab => ( ))}
+
{/* Skills Tab */} {activeTab === 'skills' && ( <> -
-

+

+

Fähigkeiten sind Kompetenzen, die in Übungen trainiert werden.

{isAdmin && ( @@ -188,60 +186,46 @@ function SkillsPage() { {Object.keys(skillsByCategory).length === 0 ? (
-

+

Keine Fähigkeiten gefunden

) : ( Object.keys(skillsByCategory).sort().map(category => ( -
-

+
+

{category}

-
+
{skillsByCategory[category].map(skill => ( -
-
-

{skill.name}

+
+
+

{skill.name}

{skill.importance && ( - + ⭐ {skill.importance}/5 )}
{skill.description && ( -

+

{skill.description}

)} {isAdmin && ( -
+
+
+
+
-
+
-
-
+
)}