feat: enhance UI and functionality for Skills and Exercises pages
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 29s
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 29s
- 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.
This commit is contained in:
parent
657f73d2c5
commit
68923b0364
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function DetailPanel({ item, onUpdate, focusAreas }) {
|
|||
return <TrainingTypeDetail item={item} onUpdate={onUpdate} focusAreas={focusAreas} />
|
||||
}
|
||||
|
||||
return <div style={{ padding: '20px', color: 'var(--text3)' }}>Unbekannter Typ: {type}</div>
|
||||
return <div className="detail-panel__unknown">Unbekannter Typ: {type}</div>
|
||||
}
|
||||
|
||||
function FocusAreaDetail({ item, onUpdate }) {
|
||||
|
|
@ -57,7 +57,7 @@ function FocusAreaDetail({ item, onUpdate }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>Fokusbereich bearbeiten</h2>
|
||||
<h2 className="detail-panel__title">Fokusbereich bearbeiten</h2>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Name *</label>
|
||||
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
|
||||
|
|
@ -81,11 +81,11 @@ function FocusAreaDetail({ item, onUpdate }) {
|
|||
<option value="inactive">Inaktiv</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '20px' }}>
|
||||
<div className="detail-panel__actions">
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name}>
|
||||
{saving ? 'Speichert...' : 'Speichern'}
|
||||
</button>
|
||||
<button className="btn" onClick={handleDelete}>Löschen</button>
|
||||
<button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -130,7 +130,7 @@ function StyleDirectionDetail({ item, onUpdate, focusAreas }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>Stilrichtung bearbeiten</h2>
|
||||
<h2 className="detail-panel__title">Stilrichtung bearbeiten</h2>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Name *</label>
|
||||
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
|
||||
|
|
@ -163,11 +163,11 @@ function StyleDirectionDetail({ item, onUpdate, focusAreas }) {
|
|||
<option value="inactive">Inaktiv</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '20px' }}>
|
||||
<div className="detail-panel__actions">
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name || !form.focus_area_id}>
|
||||
{saving ? 'Speichert...' : 'Speichern'}
|
||||
</button>
|
||||
<button className="btn" onClick={handleDelete}>Löschen</button>
|
||||
<button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -212,7 +212,7 @@ function TrainingTypeDetail({ item, onUpdate, focusAreas }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>Trainingstyp bearbeiten</h2>
|
||||
<h2 className="detail-panel__title">Trainingstyp bearbeiten</h2>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Name *</label>
|
||||
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} />
|
||||
|
|
@ -245,11 +245,11 @@ function TrainingTypeDetail({ item, onUpdate, focusAreas }) {
|
|||
<option value="inactive">Inaktiv</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '20px' }}>
|
||||
<div className="detail-panel__actions">
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name || !form.focus_area_id}>
|
||||
{saving ? 'Speichert...' : 'Speichern'}
|
||||
</button>
|
||||
<button className="btn" onClick={handleDelete}>Löschen</button>
|
||||
<button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -284,8 +284,8 @@ function CreateStyleDirectionForm({ item, onUpdate }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>Neue Stilrichtung erstellen</h2>
|
||||
<div style={{ padding: '12px', background: 'var(--surface2)', borderRadius: '8px', marginBottom: '20px', color: 'var(--text2)' }}>
|
||||
<h2 className="detail-panel__title">Neue Stilrichtung erstellen</h2>
|
||||
<div className="detail-panel__context">
|
||||
Fokusbereich: <strong>{item.focus_area_name}</strong>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
|
|
@ -304,7 +304,7 @@ function CreateStyleDirectionForm({ item, onUpdate }) {
|
|||
<label className="form-label">Sortierung</label>
|
||||
<input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '20px' }}>
|
||||
<div className="detail-panel__actions">
|
||||
<button className="btn btn-primary" onClick={handleCreate} disabled={saving || !form.name}>
|
||||
{saving ? 'Erstellt...' : 'Erstellen'}
|
||||
</button>
|
||||
|
|
@ -342,8 +342,8 @@ function CreateTrainingTypeForm({ item, onUpdate }) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginTop: 0 }}>Neuen Trainingstyp erstellen</h2>
|
||||
<div style={{ padding: '12px', background: 'var(--surface2)', borderRadius: '8px', marginBottom: '20px', color: 'var(--text2)' }}>
|
||||
<h2 className="detail-panel__title">Neuen Trainingstyp erstellen</h2>
|
||||
<div className="detail-panel__context">
|
||||
Fokusbereich: <strong>{item.focus_area_name}</strong>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
|
|
@ -362,7 +362,7 @@ function CreateTrainingTypeForm({ item, onUpdate }) {
|
|||
<label className="form-label">Sortierung</label>
|
||||
<input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '20px' }}>
|
||||
<div className="detail-panel__actions">
|
||||
<button className="btn btn-primary" onClick={handleCreate} disabled={saving || !form.name}>
|
||||
{saving ? 'Erstellt...' : 'Erstellen'}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
{/* Focus Area Header */}
|
||||
<div
|
||||
onClick={() => 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
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onClick={(e) => { e.stopPropagation(); onToggle(nodeId) }}
|
||||
style={{ marginRight: '8px', cursor: 'pointer', fontSize: '18px' }}
|
||||
<div className="focus-tree-root">
|
||||
<div className={'focus-tree-header' + (isSelected ? ' focus-tree-header--selected' : '')}>
|
||||
<button
|
||||
type="button"
|
||||
className="focus-tree-toggle"
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={isExpanded ? 'Bereich einklappen' : 'Bereich aufklappen'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggle(nodeId)
|
||||
}}
|
||||
>
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
<span style={{ marginRight: '8px' }}>{focusArea.icon}</span>
|
||||
<span>{focusArea.name}</span>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={18} strokeWidth={2} aria-hidden />
|
||||
) : (
|
||||
<ChevronRight size={18} strokeWidth={2} aria-hidden />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="focus-tree-header__label"
|
||||
onClick={() => onSelect(focusArea, 'focus_area')}
|
||||
>
|
||||
{focusArea.icon ? (
|
||||
<span className="focus-tree-emoji" aria-hidden>
|
||||
{focusArea.icon}
|
||||
</span>
|
||||
) : null}
|
||||
<span>{focusArea.name}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Children: Style Directions + Training Types */}
|
||||
{isExpanded && (
|
||||
<div style={{ marginLeft: '28px', marginTop: '8px' }}>
|
||||
{/* Style Directions Section */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '6px', textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="focus-tree-children">
|
||||
<div className="focus-tree-group">
|
||||
<div className="focus-tree-group__head">
|
||||
<span>Stilrichtungen</span>
|
||||
<button
|
||||
className="btn"
|
||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||
type="button"
|
||||
className="btn btn-secondary btn-tiny focus-tree-add-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onSelect({ _createType: 'style_direction', focus_area_id: focusArea.id, focus_area_name: focusArea.name }, 'create_style_direction')
|
||||
onSelect(
|
||||
{ _createType: 'style_direction', focus_area_id: focusArea.id, focus_area_name: focusArea.name },
|
||||
'create_style_direction'
|
||||
)
|
||||
}}
|
||||
>
|
||||
+ Neu
|
||||
</button>
|
||||
</div>
|
||||
{focusArea.style_directions && focusArea.style_directions.map(sd => (
|
||||
<StyleDirectionNode
|
||||
key={sd.id}
|
||||
styleDirection={sd}
|
||||
onSelect={onSelect}
|
||||
isSelected={selectedType === 'style_direction' && selectedId === sd.id}
|
||||
/>
|
||||
))}
|
||||
{focusArea.style_directions &&
|
||||
focusArea.style_directions.map((sd) => (
|
||||
<StyleDirectionNode
|
||||
key={sd.id}
|
||||
styleDirection={sd}
|
||||
onSelect={onSelect}
|
||||
isSelected={selectedType === 'style_direction' && selectedId === sd.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Training Types Section */}
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '6px', textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div className="focus-tree-group">
|
||||
<div className="focus-tree-group__head">
|
||||
<span>Trainingstypen</span>
|
||||
<button
|
||||
className="btn"
|
||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||
type="button"
|
||||
className="btn btn-secondary btn-tiny focus-tree-add-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onSelect({ _createType: 'training_type', focus_area_id: focusArea.id, focus_area_name: focusArea.name }, 'create_training_type')
|
||||
onSelect(
|
||||
{ _createType: 'training_type', focus_area_id: focusArea.id, focus_area_name: focusArea.name },
|
||||
'create_training_type'
|
||||
)
|
||||
}}
|
||||
>
|
||||
+ Neu
|
||||
</button>
|
||||
</div>
|
||||
{focusArea.training_types && focusArea.training_types.map(tt => (
|
||||
<TrainingTypeNode
|
||||
key={tt.id}
|
||||
trainingType={tt}
|
||||
onSelect={onSelect}
|
||||
isSelected={selectedType === 'training_type' && selectedId === tt.id}
|
||||
/>
|
||||
))}
|
||||
{focusArea.training_types &&
|
||||
focusArea.training_types.map((tt) => (
|
||||
<TrainingTypeNode
|
||||
key={tt.id}
|
||||
trainingType={tt}
|
||||
onSelect={onSelect}
|
||||
isSelected={selectedType === 'training_type' && selectedId === tt.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -92,28 +105,26 @@ function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, se
|
|||
function StyleDirectionNode({ styleDirection, onSelect, isSelected }) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={'focus-tree-item' + (isSelected ? ' focus-tree-item--selected' : '')}
|
||||
onClick={() => 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 && (
|
||||
<span style={{ marginLeft: '8px', opacity: 0.7, fontSize: '12px' }}>
|
||||
({styleDirection.abbreviation})
|
||||
</span>
|
||||
)}
|
||||
{styleDirection.target_groups && styleDirection.target_groups.length > 0 && (
|
||||
<div style={{ fontSize: '11px', opacity: 0.8, marginTop: '4px' }}>
|
||||
Zielgruppen: {styleDirection.target_groups.map(tg => tg.name).join(', ')}
|
||||
{styleDirection.abbreviation ? (
|
||||
<span className="focus-tree-item__abbr">({styleDirection.abbreviation})</span>
|
||||
) : null}
|
||||
{styleDirection.target_groups && styleDirection.target_groups.length > 0 ? (
|
||||
<div className="focus-tree-item__meta">
|
||||
Zielgruppen: {styleDirection.target_groups.map((tg) => tg.name).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -121,25 +132,23 @@ function StyleDirectionNode({ styleDirection, onSelect, isSelected }) {
|
|||
function TrainingTypeNode({ trainingType, onSelect, isSelected }) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={'focus-tree-item' + (isSelected ? ' focus-tree-item--selected' : '')}
|
||||
onClick={() => 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 && (
|
||||
<span style={{ marginLeft: '8px', opacity: 0.7, fontSize: '12px' }}>
|
||||
({trainingType.abbreviation})
|
||||
</span>
|
||||
)}
|
||||
{trainingType.abbreviation ? (
|
||||
<span className="focus-tree-item__abbr">({trainingType.abbreviation})</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FocusAreaNode
|
||||
export default FocusAreaNode
|
||||
|
|
@ -545,65 +545,64 @@ function ExercisesListPage() {
|
|||
|
||||
if (!catalogsReady && pageTab === 'list') {
|
||||
return (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<div className="spinner"></div>
|
||||
<p>Lade Kataloge…</p>
|
||||
<div className="app-page">
|
||||
<div className="empty-state" style={{ padding: '2.5rem 1rem' }}>
|
||||
<div className="spinner" />
|
||||
<p className="muted" style={{ marginTop: '12px' }}>
|
||||
Lade Kataloge…
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: '1.35rem' }}>Übungen</h1>
|
||||
<div className="exercises-page__header">
|
||||
<h1 className="page-title exercises-page__title">Übungen</h1>
|
||||
{pageTab === 'list' ? (
|
||||
<Link to="/exercises/new" className="btn btn-primary">
|
||||
+ Neu
|
||||
</Link>
|
||||
) : (
|
||||
<span />
|
||||
<span aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Übungen Bereiche"
|
||||
style={{ display: 'flex', gap: '8px', marginBottom: '14px', flexWrap: 'wrap' }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={pageTab === 'list'}
|
||||
className={pageTab === 'list' ? 'btn btn-primary' : 'btn btn-secondary'}
|
||||
onClick={() => setPageTab('list')}
|
||||
>
|
||||
Liste
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={pageTab === 'progression'}
|
||||
className={pageTab === 'progression' ? 'btn btn-primary' : 'btn btn-secondary'}
|
||||
onClick={() => setPageTab('progression')}
|
||||
>
|
||||
Progressionsgraphen
|
||||
</button>
|
||||
<div className="exercises-page-toolbar-tabs" role="tablist" aria-label="Übungen Bereiche">
|
||||
<div className="planning-segment-group planning-segment-group--equal exercises-page-mode-switch">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={pageTab === 'list'}
|
||||
className={
|
||||
'planning-segment-group__btn' +
|
||||
(pageTab === 'list' ? ' planning-segment-group__btn--active' : '')
|
||||
}
|
||||
onClick={() => setPageTab('list')}
|
||||
>
|
||||
Liste
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={pageTab === 'progression'}
|
||||
className={
|
||||
'planning-segment-group__btn' +
|
||||
(pageTab === 'progression' ? ' planning-segment-group__btn--active' : '')
|
||||
}
|
||||
onClick={() => setPageTab('progression')}
|
||||
>
|
||||
Progressionsgraphen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pageTab === 'progression' ? (
|
||||
<ExerciseProgressionGraphPanel />
|
||||
) : (
|
||||
<>
|
||||
<div className="card exercise-search-bar" style={{ marginBottom: '12px' }}>
|
||||
<div className="card exercise-search-bar">
|
||||
<label className="form-label">Volltextsuche (Titel, Ziel, …)</label>
|
||||
<datalist id="exercise-search-titles">
|
||||
{searchTitleSuggestions.map((t) => (
|
||||
|
|
@ -612,7 +611,7 @@ function ExercisesListPage() {
|
|||
</datalist>
|
||||
<input
|
||||
type="search"
|
||||
className="form-input"
|
||||
className="form-input exercise-search-bar__primary"
|
||||
placeholder="Suchbegriffe…"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
|
|
@ -620,7 +619,6 @@ function ExercisesListPage() {
|
|||
name="exercise-fulltext-search"
|
||||
list="exercise-search-titles"
|
||||
enterKeyHint="search"
|
||||
style={{ marginBottom: '10px' }}
|
||||
/>
|
||||
<label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label>
|
||||
<input
|
||||
|
|
@ -668,13 +666,13 @@ function ExercisesListPage() {
|
|||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: '10px', marginBottom: 0 }}>
|
||||
<p className="exercise-search-hint">
|
||||
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 ? (
|
||||
<>
|
||||
{' '}
|
||||
<button type="button" className="btn btn-secondary" style={{ marginLeft: '6px' }} onClick={toggleSelectAllPage}>
|
||||
<button type="button" className="btn btn-secondary btn-small" onClick={toggleSelectAllPage}>
|
||||
{allOnPageSelected ? 'Auswahl auf dieser Seite aufheben' : 'Alle auf dieser Seite auswählen'}
|
||||
</button>
|
||||
</>
|
||||
|
|
@ -683,24 +681,15 @@ function ExercisesListPage() {
|
|||
</div>
|
||||
|
||||
{selectedIds.size > 0 ? (
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
marginBottom: '12px',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<div className="card exercise-bulk-toolbar">
|
||||
<strong>{selectedIds.size} ausgewählt</strong>
|
||||
<button type="button" className="btn btn-secondary" onClick={clearSelection}>
|
||||
<button type="button" className="btn btn-secondary btn-small" onClick={clearSelection}>
|
||||
Auswahl aufheben
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={openBulkModal}>
|
||||
<button type="button" className="btn btn-primary btn-small" onClick={openBulkModal}>
|
||||
Massenänderung…
|
||||
</button>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text3)' }}>
|
||||
<span className="exercise-bulk-toolbar__meta">
|
||||
Bis zu {BULK_MAX_IDS} pro Anfrage. Für „Verein“ ohne Auswahl: aktiver Vereinskontext (
|
||||
<code>X-Active-Club-Id</code>
|
||||
).
|
||||
|
|
@ -736,7 +725,7 @@ function ExercisesListPage() {
|
|||
</button>
|
||||
</div>
|
||||
<div className="admin-modal-sheet__body exercise-filter-modal__scroll">
|
||||
<p style={{ fontSize: '13px', color: 'var(--text2)', marginTop: 0, marginBottom: '14px' }}>
|
||||
<p className="muted" style={{ marginTop: 0, marginBottom: '14px' }}>
|
||||
Zwischen den Bereichen gilt <strong>UND</strong>. Innerhalb eines Feldes werden mehrere Einträge mit{' '}
|
||||
<strong>ODER</strong> verknüpft.
|
||||
</p>
|
||||
|
|
@ -900,12 +889,12 @@ function ExercisesListPage() {
|
|||
</button>
|
||||
</div>
|
||||
<div className="admin-modal-sheet__body exercise-filter-modal__scroll">
|
||||
<p style={{ fontSize: '13px', color: 'var(--text2)', marginTop: 0 }}>
|
||||
<p className="muted" style={{ marginTop: 0 }}>
|
||||
Es werden <strong>{selectedIds.size}</strong> Ü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).
|
||||
</p>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, marginBottom: '14px' }}>
|
||||
<p className="form-sub" style={{ marginTop: 0, marginBottom: '14px' }}>
|
||||
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() {
|
|||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div className="exercise-filter-modal__footer" style={{ justifyContent: 'flex-end' }}>
|
||||
<div className="exercise-filter-modal__footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
|
|
@ -1084,52 +1073,43 @@ function ExercisesListPage() {
|
|||
) : null}
|
||||
|
||||
{listFetching && exercises.length === 0 ? (
|
||||
<div className="card" style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<div className="spinner"></div>
|
||||
<p style={{ color: 'var(--text2)', marginTop: '12px' }}>Lade Übungen…</p>
|
||||
<div className="card empty-state" style={{ padding: '2rem 1rem' }}>
|
||||
<div className="spinner" />
|
||||
<p className="muted" style={{ marginTop: '12px' }}>
|
||||
Lade Übungen…
|
||||
</p>
|
||||
</div>
|
||||
) : exercises.length === 0 ? (
|
||||
<div className="card">
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||||
Keine Übungen gefunden.
|
||||
</p>
|
||||
<p className="exercises-empty-text">Keine Übungen gefunden.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{listFetching ? (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text3)', marginBottom: '8px' }}>Aktualisiere Treffer…</p>
|
||||
<p className="exercises-meta-line exercises-meta-line--muted">Aktualisiere Treffer…</p>
|
||||
) : null}
|
||||
<p style={{ fontSize: '13px', color: 'var(--text2)', marginBottom: '10px' }}>
|
||||
<p className="exercises-meta-line">
|
||||
{exercises.length} angezeigt
|
||||
{hasMore ? ' · es gibt weitere Einträge' : ''}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
<div className="exercises-list-grid">
|
||||
{exercises.map((exercise) => (
|
||||
<div key={exercise.id} className="card exercise-card">
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
|
||||
<div className="exercise-card-layout">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(Number(exercise.id))}
|
||||
onChange={() => toggleSelect(exercise.id)}
|
||||
aria-label={`„${(exercise.title || 'Übung').replace(/"/g, '')}“ auswählen`}
|
||||
style={{ marginTop: '4px', flexShrink: 0 }}
|
||||
className="exercise-card-layout__check"
|
||||
/>
|
||||
<div className="exercise-card__body" style={{ flex: 1, minWidth: 0 }}>
|
||||
<h3 style={{ marginBottom: '8px', fontSize: '1.05rem', lineHeight: 1.3 }}>
|
||||
<Link
|
||||
to={`/exercises/${exercise.id}`}
|
||||
style={{ color: 'inherit', textDecoration: 'none' }}
|
||||
>
|
||||
<div className="exercise-card__body exercise-card-body-flex">
|
||||
<h3 className="exercise-card-title">
|
||||
<Link to={`/exercises/${exercise.id}`}>
|
||||
{exercise.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '8px' }}>
|
||||
<div className="exercise-card-tags">
|
||||
{exercise.focus_area && (
|
||||
<span className="exercise-tag exercise-tag--accent">{exercise.focus_area}</span>
|
||||
)}
|
||||
|
|
@ -1137,7 +1117,7 @@ function ExercisesListPage() {
|
|||
<span className="exercise-tag">{exercise.status}</span>
|
||||
</div>
|
||||
{exercise.summary && (
|
||||
<p style={{ color: 'var(--text2)', fontSize: '13px', lineHeight: 1.4 }}>
|
||||
<p className="exercise-card-summary">
|
||||
{exercise.summary.length > 160
|
||||
? `${exercise.summary.slice(0, 160)}…`
|
||||
: exercise.summary}
|
||||
|
|
@ -1154,12 +1134,7 @@ function ExercisesListPage() {
|
|||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
style={{
|
||||
background: 'var(--danger)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
}}
|
||||
className="btn btn-danger btn-small"
|
||||
onClick={() => handleDelete(exercise)}
|
||||
>
|
||||
Löschen
|
||||
|
|
@ -1169,7 +1144,7 @@ function ExercisesListPage() {
|
|||
))}
|
||||
</div>
|
||||
{hasMore && (
|
||||
<div style={{ textAlign: 'center', marginTop: '16px' }}>
|
||||
<div className="exercises-load-more">
|
||||
<button type="button" className="btn btn-secondary" disabled={loadingMore} onClick={loadMore}>
|
||||
{loadingMore ? 'Laden…' : 'Mehr laden'}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ function SkillsPage() {
|
|||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<div className="skills-page__loading">
|
||||
<div className="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
|
|
@ -143,40 +143,38 @@ function SkillsPage() {
|
|||
const methodsByCategory = groupByCategory(methods)
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<h1 style={{ marginBottom: '1.5rem' }}>Fähigkeiten & Methoden</h1>
|
||||
<div className="app-page skills-page">
|
||||
<h1 className="page-title">Fähigkeiten & Methoden</h1>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '1.5rem',
|
||||
borderBottom: '2px solid var(--border)'
|
||||
}}>
|
||||
<div className="skills-page__tabs-scroll">
|
||||
<div
|
||||
className="planning-segment-group planning-segment-group--equal skills-page-mode-switch"
|
||||
role="tablist"
|
||||
aria-label="Bereich wählen"
|
||||
>
|
||||
{['skills', 'methods'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab}
|
||||
className={
|
||||
'planning-segment-group__btn' +
|
||||
(activeTab === tab ? ' planning-segment-group__btn--active' : '')
|
||||
}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
background: activeTab === tab ? 'var(--accent)' : 'transparent',
|
||||
color: activeTab === tab ? 'white' : 'var(--text1)',
|
||||
border: 'none',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
cursor: 'pointer',
|
||||
fontWeight: activeTab === tab ? 'bold' : 'normal'
|
||||
}}
|
||||
>
|
||||
{tab === 'skills' ? 'Fähigkeiten' : 'Trainingsmethoden'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills Tab */}
|
||||
{activeTab === 'skills' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||
<p style={{ color: 'var(--text2)' }}>
|
||||
<div className="skills-page__intro-row">
|
||||
<p>
|
||||
Fähigkeiten sind Kompetenzen, die in Übungen trainiert werden.
|
||||
</p>
|
||||
{isAdmin && (
|
||||
|
|
@ -188,60 +186,46 @@ function SkillsPage() {
|
|||
|
||||
{Object.keys(skillsByCategory).length === 0 ? (
|
||||
<div className="card">
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||||
<p className="skills-page__empty">
|
||||
Keine Fähigkeiten gefunden
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
Object.keys(skillsByCategory).sort().map(category => (
|
||||
<div key={category} style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ marginBottom: '1rem', textTransform: 'capitalize' }}>
|
||||
<div key={category} className="skills-page__category">
|
||||
<h2 className="skills-page__category-title">
|
||||
{category}
|
||||
</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<div className="skills-page__card-grid">
|
||||
{skillsByCategory[category].map(skill => (
|
||||
<div key={skill.id} className="card">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '0.5rem' }}>
|
||||
<h3 style={{ fontSize: '1rem' }}>{skill.name}</h3>
|
||||
<div key={skill.id} className="card skills-page-card">
|
||||
<div className="skills-page-card__head">
|
||||
<h3 className="skills-page-card__title">{skill.name}</h3>
|
||||
{skill.importance && (
|
||||
<span style={{
|
||||
fontSize: '0.875rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--accent)',
|
||||
color: 'white'
|
||||
}}>
|
||||
<span className="skills-page-card__badge">
|
||||
⭐ {skill.importance}/5
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{skill.description && (
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem' }}>
|
||||
<p className="skills-page-card__desc">
|
||||
{skill.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto' }}>
|
||||
<div className="skills-page-card__actions">
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ flex: 1 }}
|
||||
type="button"
|
||||
className="btn btn-secondary skills-page-card__grow"
|
||||
onClick={() => handleEdit(skill, 'skill')}
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
style={{
|
||||
background: 'var(--danger)',
|
||||
color: 'white',
|
||||
border: 'none'
|
||||
}}
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
onClick={() => handleDelete(skill, 'skill')}
|
||||
>
|
||||
Löschen
|
||||
|
|
@ -260,8 +244,8 @@ function SkillsPage() {
|
|||
{/* Methods Tab */}
|
||||
{activeTab === 'methods' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||
<p style={{ color: 'var(--text2)' }}>
|
||||
<div className="skills-page__intro-row">
|
||||
<p>
|
||||
Trainingsmethoden sind didaktische Ansätze für die Trainingsgestaltung.
|
||||
</p>
|
||||
{isAdmin && (
|
||||
|
|
@ -273,52 +257,36 @@ function SkillsPage() {
|
|||
|
||||
{Object.keys(methodsByCategory).length === 0 ? (
|
||||
<div className="card">
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center' }}>
|
||||
<p className="skills-page__empty">
|
||||
Keine Trainingsmethoden gefunden
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
Object.keys(methodsByCategory).sort().map(category => (
|
||||
<div key={category} style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ marginBottom: '1rem', textTransform: 'capitalize' }}>
|
||||
<div key={category} className="skills-page__category">
|
||||
<h2 className="skills-page__category-title">
|
||||
{category}
|
||||
</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<div className="skills-page__card-grid skills-page__card-grid--methods">
|
||||
{methodsByCategory[category].map(method => (
|
||||
<div key={method.id} className="card">
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<h3 style={{ fontSize: '1rem', marginBottom: '0.25rem' }}>
|
||||
<div key={method.id} className="card skills-page-card">
|
||||
<div className="skills-page-card__meta-block">
|
||||
<h3 className="skills-page-card__title skills-page-card__title--method">
|
||||
{method.name}
|
||||
{method.abbreviation && (
|
||||
<span style={{ color: 'var(--text2)', fontSize: '0.875rem', marginLeft: '0.5rem' }}>
|
||||
<span className="skills-page-card__abbr">
|
||||
({method.abbreviation})
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<div className="skills-page-card__meta-row">
|
||||
{method.typical_duration && (
|
||||
<span style={{
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--surface2)',
|
||||
color: 'var(--text2)'
|
||||
}}>
|
||||
<span className="skills-page-card__chip">
|
||||
⏱️ {method.typical_duration} min
|
||||
</span>
|
||||
)}
|
||||
{method.typical_group_size && (
|
||||
<span style={{
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--surface2)',
|
||||
color: 'var(--text2)'
|
||||
}}>
|
||||
<span className="skills-page-card__chip">
|
||||
👥 {method.typical_group_size}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -326,27 +294,23 @@ function SkillsPage() {
|
|||
</div>
|
||||
|
||||
{method.description && (
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem' }}>
|
||||
<p className="skills-page-card__desc">
|
||||
{method.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto' }}>
|
||||
<div className="skills-page-card__actions">
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{ flex: 1 }}
|
||||
type="button"
|
||||
className="btn btn-secondary skills-page-card__grow"
|
||||
onClick={() => handleEdit(method, 'method')}
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
className="btn"
|
||||
style={{
|
||||
background: 'var(--danger)',
|
||||
color: 'white',
|
||||
border: 'none'
|
||||
}}
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
onClick={() => handleDelete(method, 'method')}
|
||||
>
|
||||
Löschen
|
||||
|
|
@ -364,36 +328,37 @@ function SkillsPage() {
|
|||
|
||||
{/* Modal */}
|
||||
{showModal && isAdmin && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
padding: '1rem'
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'var(--surface)',
|
||||
borderRadius: '12px',
|
||||
padding: '2rem',
|
||||
maxWidth: '600px',
|
||||
width: '100%',
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<h2 style={{ marginBottom: '1.5rem' }}>
|
||||
{editing
|
||||
? (modalType === 'skill' ? 'Fähigkeit bearbeiten' : 'Methode bearbeiten')
|
||||
: (modalType === 'skill' ? 'Neue Fähigkeit' : 'Neue Methode')
|
||||
}
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="admin-modal-backdrop"
|
||||
role="presentation"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) setShowModal(false)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="admin-modal-sheet skills-page-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="skills-page-modal-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="admin-modal-sheet__header">
|
||||
<h2 id="skills-page-modal-title" className="admin-modal-sheet__title">
|
||||
{editing
|
||||
? (modalType === 'skill' ? 'Fähigkeit bearbeiten' : 'Methode bearbeiten')
|
||||
: (modalType === 'skill' ? 'Neue Fähigkeit' : 'Neue Methode')
|
||||
}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary admin-modal-sheet__close"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-modal-sheet__body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Name *</label>
|
||||
<input
|
||||
|
|
@ -455,7 +420,7 @@ function SkillsPage() {
|
|||
|
||||
{modalType === 'method' && (
|
||||
<>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||
<div className="exercise-filters-modal-grid exercise-filters-modal-grid--two">
|
||||
<div className="form-row">
|
||||
<label className="form-label">Typische Dauer (min)</label>
|
||||
<input
|
||||
|
|
@ -492,8 +457,8 @@ function SkillsPage() {
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1.5rem' }}>
|
||||
<button type="submit" className="btn btn-primary" style={{ flex: 1 }}>
|
||||
<div className="skills-page-modal__footer">
|
||||
<button type="submit" className="btn btn-primary skills-page-modal__submit">
|
||||
{editing ? 'Speichern' : 'Erstellen'}
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -505,6 +470,7 @@ function SkillsPage() {
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user