UX - Filter #12

Merged
Lars merged 22 commits from develop into main 2026-05-06 21:25:56 +02:00
5 changed files with 706 additions and 312 deletions
Showing only changes of commit 68923b0364 - Show all commits

View File

@ -1425,6 +1425,29 @@ button.capture-shell__nav-item {
.admin-catalog-section { .admin-catalog-section {
padding: 14px; 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) */ /* Trainingsplanung: kompakte Segmente (Gruppe / Verein) */
@ -2302,6 +2325,427 @@ button.capture-shell__nav-item {
overscroll-behavior: contain; 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 { .exercise-filter-modal.admin-modal-sheet {
max-width: min(920px, calc(100dvw - 16px)); max-width: min(920px, calc(100dvw - 16px));
} }

View File

@ -20,7 +20,7 @@ function DetailPanel({ item, onUpdate, focusAreas }) {
return <TrainingTypeDetail item={item} onUpdate={onUpdate} focusAreas={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 }) { function FocusAreaDetail({ item, onUpdate }) {
@ -57,7 +57,7 @@ function FocusAreaDetail({ item, onUpdate }) {
return ( return (
<div> <div>
<h2 style={{ marginTop: 0 }}>Fokusbereich bearbeiten</h2> <h2 className="detail-panel__title">Fokusbereich bearbeiten</h2>
<div className="form-row"> <div className="form-row">
<label className="form-label">Name *</label> <label className="form-label">Name *</label>
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /> <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> <option value="inactive">Inaktiv</option>
</select> </select>
</div> </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}> <button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name}>
{saving ? 'Speichert...' : 'Speichern'} {saving ? 'Speichert...' : 'Speichern'}
</button> </button>
<button className="btn" onClick={handleDelete}>Löschen</button> <button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
</div> </div>
</div> </div>
) )
@ -130,7 +130,7 @@ function StyleDirectionDetail({ item, onUpdate, focusAreas }) {
return ( return (
<div> <div>
<h2 style={{ marginTop: 0 }}>Stilrichtung bearbeiten</h2> <h2 className="detail-panel__title">Stilrichtung bearbeiten</h2>
<div className="form-row"> <div className="form-row">
<label className="form-label">Name *</label> <label className="form-label">Name *</label>
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /> <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> <option value="inactive">Inaktiv</option>
</select> </select>
</div> </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}> <button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name || !form.focus_area_id}>
{saving ? 'Speichert...' : 'Speichern'} {saving ? 'Speichert...' : 'Speichern'}
</button> </button>
<button className="btn" onClick={handleDelete}>Löschen</button> <button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
</div> </div>
</div> </div>
) )
@ -212,7 +212,7 @@ function TrainingTypeDetail({ item, onUpdate, focusAreas }) {
return ( return (
<div> <div>
<h2 style={{ marginTop: 0 }}>Trainingstyp bearbeiten</h2> <h2 className="detail-panel__title">Trainingstyp bearbeiten</h2>
<div className="form-row"> <div className="form-row">
<label className="form-label">Name *</label> <label className="form-label">Name *</label>
<input className="form-input" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })} /> <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> <option value="inactive">Inaktiv</option>
</select> </select>
</div> </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}> <button className="btn btn-primary" onClick={handleSave} disabled={saving || !form.name || !form.focus_area_id}>
{saving ? 'Speichert...' : 'Speichern'} {saving ? 'Speichert...' : 'Speichern'}
</button> </button>
<button className="btn" onClick={handleDelete}>Löschen</button> <button type="button" className="btn btn-danger" onClick={handleDelete}>Löschen</button>
</div> </div>
</div> </div>
) )
@ -284,8 +284,8 @@ function CreateStyleDirectionForm({ item, onUpdate }) {
return ( return (
<div> <div>
<h2 style={{ marginTop: 0 }}>Neue Stilrichtung erstellen</h2> <h2 className="detail-panel__title">Neue Stilrichtung erstellen</h2>
<div style={{ padding: '12px', background: 'var(--surface2)', borderRadius: '8px', marginBottom: '20px', color: 'var(--text2)' }}> <div className="detail-panel__context">
Fokusbereich: <strong>{item.focus_area_name}</strong> Fokusbereich: <strong>{item.focus_area_name}</strong>
</div> </div>
<div className="form-row"> <div className="form-row">
@ -304,7 +304,7 @@ function CreateStyleDirectionForm({ item, onUpdate }) {
<label className="form-label">Sortierung</label> <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 })} /> <input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
</div> </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}> <button className="btn btn-primary" onClick={handleCreate} disabled={saving || !form.name}>
{saving ? 'Erstellt...' : 'Erstellen'} {saving ? 'Erstellt...' : 'Erstellen'}
</button> </button>
@ -342,8 +342,8 @@ function CreateTrainingTypeForm({ item, onUpdate }) {
return ( return (
<div> <div>
<h2 style={{ marginTop: 0 }}>Neuen Trainingstyp erstellen</h2> <h2 className="detail-panel__title">Neuen Trainingstyp erstellen</h2>
<div style={{ padding: '12px', background: 'var(--surface2)', borderRadius: '8px', marginBottom: '20px', color: 'var(--text2)' }}> <div className="detail-panel__context">
Fokusbereich: <strong>{item.focus_area_name}</strong> Fokusbereich: <strong>{item.focus_area_name}</strong>
</div> </div>
<div className="form-row"> <div className="form-row">
@ -362,7 +362,7 @@ function CreateTrainingTypeForm({ item, onUpdate }) {
<label className="form-label">Sortierung</label> <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 })} /> <input className="form-input" type="number" value={form.sort_order} onChange={e => setForm({ ...form, sort_order: parseInt(e.target.value) || 0 })} />
</div> </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}> <button className="btn btn-primary" onClick={handleCreate} disabled={saving || !form.name}>
{saving ? 'Erstellt...' : 'Erstellen'} {saving ? 'Erstellt...' : 'Erstellen'}
</button> </button>

View File

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { ChevronDown, ChevronRight } from 'lucide-react'
function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, selectedType }) { function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, selectedType }) {
const nodeId = `fa-${focusArea.id}` 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 const isSelected = selectedType === 'focus_area' && selectedId === focusArea.id
return ( return (
<div style={{ marginBottom: '12px' }}> <div className="focus-tree-root">
{/* Focus Area Header */} <div className={'focus-tree-header' + (isSelected ? ' focus-tree-header--selected' : '')}>
<div <button
onClick={() => onSelect(focusArea, 'focus_area')} type="button"
style={{ className="focus-tree-toggle"
display: 'flex', aria-expanded={isExpanded}
alignItems: 'center', aria-label={isExpanded ? 'Bereich einklappen' : 'Bereich aufklappen'}
padding: '8px 12px', onClick={(e) => {
borderRadius: '8px', e.stopPropagation()
cursor: 'pointer', onToggle(nodeId)
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' }}
> >
{isExpanded ? '▼' : '▶'} {isExpanded ? (
</span> <ChevronDown size={18} strokeWidth={2} aria-hidden />
<span style={{ marginRight: '8px' }}>{focusArea.icon}</span> ) : (
<span>{focusArea.name}</span> <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> </div>
{/* Children: Style Directions + Training Types */}
{isExpanded && ( {isExpanded && (
<div style={{ marginLeft: '28px', marginTop: '8px' }}> <div className="focus-tree-children">
{/* Style Directions Section */} <div className="focus-tree-group">
<div style={{ marginBottom: '12px' }}> <div className="focus-tree-group__head">
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '6px', textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Stilrichtungen</span> <span>Stilrichtungen</span>
<button <button
className="btn" type="button"
style={{ fontSize: '11px', padding: '4px 8px' }} className="btn btn-secondary btn-tiny focus-tree-add-btn"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() 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 + Neu
</button> </button>
</div> </div>
{focusArea.style_directions && focusArea.style_directions.map(sd => ( {focusArea.style_directions &&
<StyleDirectionNode focusArea.style_directions.map((sd) => (
key={sd.id} <StyleDirectionNode
styleDirection={sd} key={sd.id}
onSelect={onSelect} styleDirection={sd}
isSelected={selectedType === 'style_direction' && selectedId === sd.id} onSelect={onSelect}
/> isSelected={selectedType === 'style_direction' && selectedId === sd.id}
))} />
))}
</div> </div>
{/* Training Types Section */} <div className="focus-tree-group">
<div> <div className="focus-tree-group__head">
<div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '6px', textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Trainingstypen</span> <span>Trainingstypen</span>
<button <button
className="btn" type="button"
style={{ fontSize: '11px', padding: '4px 8px' }} className="btn btn-secondary btn-tiny focus-tree-add-btn"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() 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 + Neu
</button> </button>
</div> </div>
{focusArea.training_types && focusArea.training_types.map(tt => ( {focusArea.training_types &&
<TrainingTypeNode focusArea.training_types.map((tt) => (
key={tt.id} <TrainingTypeNode
trainingType={tt} key={tt.id}
onSelect={onSelect} trainingType={tt}
isSelected={selectedType === 'training_type' && selectedId === tt.id} onSelect={onSelect}
/> isSelected={selectedType === 'training_type' && selectedId === tt.id}
))} />
))}
</div> </div>
</div> </div>
)} )}
@ -92,28 +105,26 @@ function FocusAreaNode({ focusArea, expanded, onToggle, onSelect, selectedId, se
function StyleDirectionNode({ styleDirection, onSelect, isSelected }) { function StyleDirectionNode({ styleDirection, onSelect, isSelected }) {
return ( return (
<div <div
role="button"
tabIndex={0}
className={'focus-tree-item' + (isSelected ? ' focus-tree-item--selected' : '')}
onClick={() => onSelect(styleDirection, 'style_direction')} onClick={() => onSelect(styleDirection, 'style_direction')}
style={{ onKeyDown={(e) => {
padding: '6px 12px', if (e.key === 'Enter' || e.key === ' ') {
marginBottom: '4px', e.preventDefault()
borderRadius: '6px', onSelect(styleDirection, 'style_direction')
cursor: 'pointer', }
background: isSelected ? 'var(--accent)' : 'var(--surface2)',
color: isSelected ? 'white' : 'var(--text1)',
fontSize: '14px'
}} }}
> >
{styleDirection.name} {styleDirection.name}
{styleDirection.abbreviation && ( {styleDirection.abbreviation ? (
<span style={{ marginLeft: '8px', opacity: 0.7, fontSize: '12px' }}> <span className="focus-tree-item__abbr">({styleDirection.abbreviation})</span>
({styleDirection.abbreviation}) ) : null}
</span> {styleDirection.target_groups && styleDirection.target_groups.length > 0 ? (
)} <div className="focus-tree-item__meta">
{styleDirection.target_groups && styleDirection.target_groups.length > 0 && ( Zielgruppen: {styleDirection.target_groups.map((tg) => tg.name).join(', ')}
<div style={{ fontSize: '11px', opacity: 0.8, marginTop: '4px' }}>
Zielgruppen: {styleDirection.target_groups.map(tg => tg.name).join(', ')}
</div> </div>
)} ) : null}
</div> </div>
) )
} }
@ -121,25 +132,23 @@ function StyleDirectionNode({ styleDirection, onSelect, isSelected }) {
function TrainingTypeNode({ trainingType, onSelect, isSelected }) { function TrainingTypeNode({ trainingType, onSelect, isSelected }) {
return ( return (
<div <div
role="button"
tabIndex={0}
className={'focus-tree-item' + (isSelected ? ' focus-tree-item--selected' : '')}
onClick={() => onSelect(trainingType, 'training_type')} onClick={() => onSelect(trainingType, 'training_type')}
style={{ onKeyDown={(e) => {
padding: '6px 12px', if (e.key === 'Enter' || e.key === ' ') {
marginBottom: '4px', e.preventDefault()
borderRadius: '6px', onSelect(trainingType, 'training_type')
cursor: 'pointer', }
background: isSelected ? 'var(--accent)' : 'var(--surface2)',
color: isSelected ? 'white' : 'var(--text1)',
fontSize: '14px'
}} }}
> >
{trainingType.name} {trainingType.name}
{trainingType.abbreviation && ( {trainingType.abbreviation ? (
<span style={{ marginLeft: '8px', opacity: 0.7, fontSize: '12px' }}> <span className="focus-tree-item__abbr">({trainingType.abbreviation})</span>
({trainingType.abbreviation}) ) : null}
</span>
)}
</div> </div>
) )
} }
export default FocusAreaNode export default FocusAreaNode

View File

@ -545,65 +545,64 @@ function ExercisesListPage() {
if (!catalogsReady && pageTab === 'list') { if (!catalogsReady && pageTab === 'list') {
return ( return (
<div style={{ padding: '2rem', textAlign: 'center' }}> <div className="app-page">
<div className="spinner"></div> <div className="empty-state" style={{ padding: '2.5rem 1rem' }}>
<p>Lade Kataloge</p> <div className="spinner" />
<p className="muted" style={{ marginTop: '12px' }}>
Lade Kataloge
</p>
</div>
</div> </div>
) )
} }
return ( return (
<div className="app-page"> <div className="app-page">
<div <div className="exercises-page__header">
style={{ <h1 className="page-title exercises-page__title">Übungen</h1>
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '12px',
flexWrap: 'wrap',
gap: '8px',
}}
>
<h1 style={{ fontSize: '1.35rem' }}>Übungen</h1>
{pageTab === 'list' ? ( {pageTab === 'list' ? (
<Link to="/exercises/new" className="btn btn-primary"> <Link to="/exercises/new" className="btn btn-primary">
+ Neu + Neu
</Link> </Link>
) : ( ) : (
<span /> <span aria-hidden="true" />
)} )}
</div> </div>
<div <div className="exercises-page-toolbar-tabs" role="tablist" aria-label="Übungen Bereiche">
role="tablist" <div className="planning-segment-group planning-segment-group--equal exercises-page-mode-switch">
aria-label="Übungen Bereiche" <button
style={{ display: 'flex', gap: '8px', marginBottom: '14px', flexWrap: 'wrap' }} type="button"
> role="tab"
<button aria-selected={pageTab === 'list'}
type="button" className={
role="tab" 'planning-segment-group__btn' +
aria-selected={pageTab === 'list'} (pageTab === 'list' ? ' planning-segment-group__btn--active' : '')
className={pageTab === 'list' ? 'btn btn-primary' : 'btn btn-secondary'} }
onClick={() => setPageTab('list')} onClick={() => setPageTab('list')}
> >
Liste Liste
</button> </button>
<button <button
type="button" type="button"
role="tab" role="tab"
aria-selected={pageTab === 'progression'} aria-selected={pageTab === 'progression'}
className={pageTab === 'progression' ? 'btn btn-primary' : 'btn btn-secondary'} className={
onClick={() => setPageTab('progression')} 'planning-segment-group__btn' +
> (pageTab === 'progression' ? ' planning-segment-group__btn--active' : '')
Progressionsgraphen }
</button> onClick={() => setPageTab('progression')}
>
Progressionsgraphen
</button>
</div>
</div> </div>
{pageTab === 'progression' ? ( {pageTab === 'progression' ? (
<ExerciseProgressionGraphPanel /> <ExerciseProgressionGraphPanel />
) : ( ) : (
<> <>
<div className="card exercise-search-bar" style={{ marginBottom: '12px' }}> <div className="card exercise-search-bar">
<label className="form-label">Volltextsuche (Titel, Ziel, )</label> <label className="form-label">Volltextsuche (Titel, Ziel, )</label>
<datalist id="exercise-search-titles"> <datalist id="exercise-search-titles">
{searchTitleSuggestions.map((t) => ( {searchTitleSuggestions.map((t) => (
@ -612,7 +611,7 @@ function ExercisesListPage() {
</datalist> </datalist>
<input <input
type="search" type="search"
className="form-input" className="form-input exercise-search-bar__primary"
placeholder="Suchbegriffe…" placeholder="Suchbegriffe…"
value={searchInput} value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} onChange={(e) => setSearchInput(e.target.value)}
@ -620,7 +619,6 @@ function ExercisesListPage() {
name="exercise-fulltext-search" name="exercise-fulltext-search"
list="exercise-search-titles" list="exercise-search-titles"
enterKeyHint="search" enterKeyHint="search"
style={{ marginBottom: '10px' }}
/> />
<label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label> <label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label>
<input <input
@ -668,13 +666,13 @@ function ExercisesListPage() {
))} ))}
</div> </div>
) : null} ) : 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 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. Feld). Fachliche Filter über Filter zwischen Feldern UND, Auswahl mehrerer Werte je Feld mit ODER.
{exercises.length > 0 ? ( {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'} {allOnPageSelected ? 'Auswahl auf dieser Seite aufheben' : 'Alle auf dieser Seite auswählen'}
</button> </button>
</> </>
@ -683,24 +681,15 @@ function ExercisesListPage() {
</div> </div>
{selectedIds.size > 0 ? ( {selectedIds.size > 0 ? (
<div <div className="card exercise-bulk-toolbar">
className="card"
style={{
marginBottom: '12px',
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: '10px',
}}
>
<strong>{selectedIds.size} ausgewählt</strong> <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 Auswahl aufheben
</button> </button>
<button type="button" className="btn btn-primary" onClick={openBulkModal}> <button type="button" className="btn btn-primary btn-small" onClick={openBulkModal}>
Massenänderung Massenänderung
</button> </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 ( Bis zu {BULK_MAX_IDS} pro Anfrage. Für Verein ohne Auswahl: aktiver Vereinskontext (
<code>X-Active-Club-Id</code> <code>X-Active-Club-Id</code>
). ).
@ -736,7 +725,7 @@ function ExercisesListPage() {
</button> </button>
</div> </div>
<div className="admin-modal-sheet__body exercise-filter-modal__scroll"> <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{' '} Zwischen den Bereichen gilt <strong>UND</strong>. Innerhalb eines Feldes werden mehrere Einträge mit{' '}
<strong>ODER</strong> verknüpft. <strong>ODER</strong> verknüpft.
</p> </p>
@ -900,12 +889,12 @@ function ExercisesListPage() {
</button> </button>
</div> </div>
<div className="admin-modal-sheet__body exercise-filter-modal__scroll"> <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 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 höchstens {BULK_MAX_IDS}. Ohne Berechtigung bleiben Einzelübungen unverändert (siehe Hinweis nach dem
Speichern). Speichern).
</p> </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 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 Übungen vollständig (leere Auswahl = alle Zuordnungen dieser Kategorie entfernen). Die erste Auswahl gilt
als Primärzuordnung. als Primärzuordnung.
@ -1066,7 +1055,7 @@ function ExercisesListPage() {
</div> </div>
</section> </section>
</div> </div>
<div className="exercise-filter-modal__footer" style={{ justifyContent: 'flex-end' }}> <div className="exercise-filter-modal__footer">
<button <button
type="button" type="button"
className="btn" className="btn"
@ -1084,52 +1073,43 @@ function ExercisesListPage() {
) : null} ) : null}
{listFetching && exercises.length === 0 ? ( {listFetching && exercises.length === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '2rem' }}> <div className="card empty-state" style={{ padding: '2rem 1rem' }}>
<div className="spinner"></div> <div className="spinner" />
<p style={{ color: 'var(--text2)', marginTop: '12px' }}>Lade Übungen</p> <p className="muted" style={{ marginTop: '12px' }}>
Lade Übungen
</p>
</div> </div>
) : exercises.length === 0 ? ( ) : exercises.length === 0 ? (
<div className="card"> <div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}> <p className="exercises-empty-text">Keine Übungen gefunden.</p>
Keine Übungen gefunden.
</p>
</div> </div>
) : ( ) : (
<> <>
{listFetching ? ( {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} ) : null}
<p style={{ fontSize: '13px', color: 'var(--text2)', marginBottom: '10px' }}> <p className="exercises-meta-line">
{exercises.length} angezeigt {exercises.length} angezeigt
{hasMore ? ' · es gibt weitere Einträge' : ''} {hasMore ? ' · es gibt weitere Einträge' : ''}
</p> </p>
<div <div className="exercises-list-grid">
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '12px',
}}
>
{exercises.map((exercise) => ( {exercises.map((exercise) => (
<div key={exercise.id} className="card exercise-card"> <div key={exercise.id} className="card exercise-card">
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}> <div className="exercise-card-layout">
<input <input
type="checkbox" type="checkbox"
checked={selectedIds.has(Number(exercise.id))} checked={selectedIds.has(Number(exercise.id))}
onChange={() => toggleSelect(exercise.id)} onChange={() => toggleSelect(exercise.id)}
aria-label={`${(exercise.title || 'Übung').replace(/"/g, '')}“ auswählen`} 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 }}> <div className="exercise-card__body exercise-card-body-flex">
<h3 style={{ marginBottom: '8px', fontSize: '1.05rem', lineHeight: 1.3 }}> <h3 className="exercise-card-title">
<Link <Link to={`/exercises/${exercise.id}`}>
to={`/exercises/${exercise.id}`}
style={{ color: 'inherit', textDecoration: 'none' }}
>
{exercise.title} {exercise.title}
</Link> </Link>
</h3> </h3>
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '8px' }}> <div className="exercise-card-tags">
{exercise.focus_area && ( {exercise.focus_area && (
<span className="exercise-tag exercise-tag--accent">{exercise.focus_area}</span> <span className="exercise-tag exercise-tag--accent">{exercise.focus_area}</span>
)} )}
@ -1137,7 +1117,7 @@ function ExercisesListPage() {
<span className="exercise-tag">{exercise.status}</span> <span className="exercise-tag">{exercise.status}</span>
</div> </div>
{exercise.summary && ( {exercise.summary && (
<p style={{ color: 'var(--text2)', fontSize: '13px', lineHeight: 1.4 }}> <p className="exercise-card-summary">
{exercise.summary.length > 160 {exercise.summary.length > 160
? `${exercise.summary.slice(0, 160)}` ? `${exercise.summary.slice(0, 160)}`
: exercise.summary} : exercise.summary}
@ -1154,12 +1134,7 @@ function ExercisesListPage() {
</Link> </Link>
<button <button
type="button" type="button"
className="btn" className="btn btn-danger btn-small"
style={{
background: 'var(--danger)',
color: 'white',
border: 'none',
}}
onClick={() => handleDelete(exercise)} onClick={() => handleDelete(exercise)}
> >
Löschen Löschen
@ -1169,7 +1144,7 @@ function ExercisesListPage() {
))} ))}
</div> </div>
{hasMore && ( {hasMore && (
<div style={{ textAlign: 'center', marginTop: '16px' }}> <div className="exercises-load-more">
<button type="button" className="btn btn-secondary" disabled={loadingMore} onClick={loadMore}> <button type="button" className="btn btn-secondary" disabled={loadingMore} onClick={loadMore}>
{loadingMore ? 'Laden…' : 'Mehr laden'} {loadingMore ? 'Laden…' : 'Mehr laden'}
</button> </button>

View File

@ -132,7 +132,7 @@ function SkillsPage() {
if (loading) { if (loading) {
return ( return (
<div style={{ padding: '2rem', textAlign: 'center' }}> <div className="skills-page__loading">
<div className="spinner"></div> <div className="spinner"></div>
<p>Laden...</p> <p>Laden...</p>
</div> </div>
@ -143,40 +143,38 @@ function SkillsPage() {
const methodsByCategory = groupByCategory(methods) const methodsByCategory = groupByCategory(methods)
return ( return (
<div className="app-page"> <div className="app-page skills-page">
<h1 style={{ marginBottom: '1.5rem' }}>Fähigkeiten & Methoden</h1> <h1 className="page-title">Fähigkeiten & Methoden</h1>
{/* Tabs */} <div className="skills-page__tabs-scroll">
<div style={{ <div
display: 'flex', className="planning-segment-group planning-segment-group--equal skills-page-mode-switch"
gap: '0.5rem', role="tablist"
marginBottom: '1.5rem', aria-label="Bereich wählen"
borderBottom: '2px solid var(--border)' >
}}>
{['skills', 'methods'].map(tab => ( {['skills', 'methods'].map(tab => (
<button <button
key={tab} 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)} 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'} {tab === 'skills' ? 'Fähigkeiten' : 'Trainingsmethoden'}
</button> </button>
))} ))}
</div> </div>
</div>
{/* Skills Tab */} {/* Skills Tab */}
{activeTab === 'skills' && ( {activeTab === 'skills' && (
<> <>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}> <div className="skills-page__intro-row">
<p style={{ color: 'var(--text2)' }}> <p>
Fähigkeiten sind Kompetenzen, die in Übungen trainiert werden. Fähigkeiten sind Kompetenzen, die in Übungen trainiert werden.
</p> </p>
{isAdmin && ( {isAdmin && (
@ -188,60 +186,46 @@ function SkillsPage() {
{Object.keys(skillsByCategory).length === 0 ? ( {Object.keys(skillsByCategory).length === 0 ? (
<div className="card"> <div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}> <p className="skills-page__empty">
Keine Fähigkeiten gefunden Keine Fähigkeiten gefunden
</p> </p>
</div> </div>
) : ( ) : (
Object.keys(skillsByCategory).sort().map(category => ( Object.keys(skillsByCategory).sort().map(category => (
<div key={category} style={{ marginBottom: '2rem' }}> <div key={category} className="skills-page__category">
<h2 style={{ marginBottom: '1rem', textTransform: 'capitalize' }}> <h2 className="skills-page__category-title">
{category} {category}
</h2> </h2>
<div style={{ <div className="skills-page__card-grid">
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '1rem'
}}>
{skillsByCategory[category].map(skill => ( {skillsByCategory[category].map(skill => (
<div key={skill.id} className="card"> <div key={skill.id} className="card skills-page-card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '0.5rem' }}> <div className="skills-page-card__head">
<h3 style={{ fontSize: '1rem' }}>{skill.name}</h3> <h3 className="skills-page-card__title">{skill.name}</h3>
{skill.importance && ( {skill.importance && (
<span style={{ <span className="skills-page-card__badge">
fontSize: '0.875rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background: 'var(--accent)',
color: 'white'
}}>
{skill.importance}/5 {skill.importance}/5
</span> </span>
)} )}
</div> </div>
{skill.description && ( {skill.description && (
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem' }}> <p className="skills-page-card__desc">
{skill.description} {skill.description}
</p> </p>
)} )}
{isAdmin && ( {isAdmin && (
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto' }}> <div className="skills-page-card__actions">
<button <button
className="btn btn-secondary" type="button"
style={{ flex: 1 }} className="btn btn-secondary skills-page-card__grow"
onClick={() => handleEdit(skill, 'skill')} onClick={() => handleEdit(skill, 'skill')}
> >
Bearbeiten Bearbeiten
</button> </button>
<button <button
className="btn" type="button"
style={{ className="btn btn-danger"
background: 'var(--danger)',
color: 'white',
border: 'none'
}}
onClick={() => handleDelete(skill, 'skill')} onClick={() => handleDelete(skill, 'skill')}
> >
Löschen Löschen
@ -260,8 +244,8 @@ function SkillsPage() {
{/* Methods Tab */} {/* Methods Tab */}
{activeTab === 'methods' && ( {activeTab === 'methods' && (
<> <>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '1rem' }}> <div className="skills-page__intro-row">
<p style={{ color: 'var(--text2)' }}> <p>
Trainingsmethoden sind didaktische Ansätze für die Trainingsgestaltung. Trainingsmethoden sind didaktische Ansätze für die Trainingsgestaltung.
</p> </p>
{isAdmin && ( {isAdmin && (
@ -273,52 +257,36 @@ function SkillsPage() {
{Object.keys(methodsByCategory).length === 0 ? ( {Object.keys(methodsByCategory).length === 0 ? (
<div className="card"> <div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}> <p className="skills-page__empty">
Keine Trainingsmethoden gefunden Keine Trainingsmethoden gefunden
</p> </p>
</div> </div>
) : ( ) : (
Object.keys(methodsByCategory).sort().map(category => ( Object.keys(methodsByCategory).sort().map(category => (
<div key={category} style={{ marginBottom: '2rem' }}> <div key={category} className="skills-page__category">
<h2 style={{ marginBottom: '1rem', textTransform: 'capitalize' }}> <h2 className="skills-page__category-title">
{category} {category}
</h2> </h2>
<div style={{ <div className="skills-page__card-grid skills-page__card-grid--methods">
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '1rem'
}}>
{methodsByCategory[category].map(method => ( {methodsByCategory[category].map(method => (
<div key={method.id} className="card"> <div key={method.id} className="card skills-page-card">
<div style={{ marginBottom: '0.5rem' }}> <div className="skills-page-card__meta-block">
<h3 style={{ fontSize: '1rem', marginBottom: '0.25rem' }}> <h3 className="skills-page-card__title skills-page-card__title--method">
{method.name} {method.name}
{method.abbreviation && ( {method.abbreviation && (
<span style={{ color: 'var(--text2)', fontSize: '0.875rem', marginLeft: '0.5rem' }}> <span className="skills-page-card__abbr">
({method.abbreviation}) ({method.abbreviation})
</span> </span>
)} )}
</h3> </h3>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}> <div className="skills-page-card__meta-row">
{method.typical_duration && ( {method.typical_duration && (
<span style={{ <span className="skills-page-card__chip">
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background: 'var(--surface2)',
color: 'var(--text2)'
}}>
{method.typical_duration} min {method.typical_duration} min
</span> </span>
)} )}
{method.typical_group_size && ( {method.typical_group_size && (
<span style={{ <span className="skills-page-card__chip">
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
background: 'var(--surface2)',
color: 'var(--text2)'
}}>
👥 {method.typical_group_size} 👥 {method.typical_group_size}
</span> </span>
)} )}
@ -326,27 +294,23 @@ function SkillsPage() {
</div> </div>
{method.description && ( {method.description && (
<p style={{ color: 'var(--text2)', fontSize: '0.875rem', marginBottom: '1rem' }}> <p className="skills-page-card__desc">
{method.description} {method.description}
</p> </p>
)} )}
{isAdmin && ( {isAdmin && (
<div style={{ display: 'flex', gap: '0.5rem', marginTop: 'auto' }}> <div className="skills-page-card__actions">
<button <button
className="btn btn-secondary" type="button"
style={{ flex: 1 }} className="btn btn-secondary skills-page-card__grow"
onClick={() => handleEdit(method, 'method')} onClick={() => handleEdit(method, 'method')}
> >
Bearbeiten Bearbeiten
</button> </button>
<button <button
className="btn" type="button"
style={{ className="btn btn-danger"
background: 'var(--danger)',
color: 'white',
border: 'none'
}}
onClick={() => handleDelete(method, 'method')} onClick={() => handleDelete(method, 'method')}
> >
Löschen Löschen
@ -364,36 +328,37 @@ function SkillsPage() {
{/* Modal */} {/* Modal */}
{showModal && isAdmin && ( {showModal && isAdmin && (
<div style={{ <div
position: 'fixed', className="admin-modal-backdrop"
top: 0, role="presentation"
left: 0, onClick={(e) => {
right: 0, if (e.target === e.currentTarget) setShowModal(false)
bottom: 0, }}
background: 'rgba(0,0,0,0.5)', >
display: 'flex', <div
alignItems: 'center', className="admin-modal-sheet skills-page-modal"
justifyContent: 'center', role="dialog"
zIndex: 1000, aria-modal="true"
padding: '1rem' aria-labelledby="skills-page-modal-title"
}}> onClick={(e) => e.stopPropagation()}
<div style={{ >
background: 'var(--surface)', <div className="admin-modal-sheet__header">
borderRadius: '12px', <h2 id="skills-page-modal-title" className="admin-modal-sheet__title">
padding: '2rem', {editing
maxWidth: '600px', ? (modalType === 'skill' ? 'Fähigkeit bearbeiten' : 'Methode bearbeiten')
width: '100%', : (modalType === 'skill' ? 'Neue Fähigkeit' : 'Neue Methode')
maxHeight: '90vh', }
overflowY: 'auto' </h2>
}}> <button
<h2 style={{ marginBottom: '1.5rem' }}> type="button"
{editing className="btn btn-secondary admin-modal-sheet__close"
? (modalType === 'skill' ? 'Fähigkeit bearbeiten' : 'Methode bearbeiten') onClick={() => setShowModal(false)}
: (modalType === 'skill' ? 'Neue Fähigkeit' : 'Neue Methode') >
} Schließen
</h2> </button>
</div>
<form onSubmit={handleSubmit}> <div className="admin-modal-sheet__body">
<form onSubmit={handleSubmit}>
<div className="form-row"> <div className="form-row">
<label className="form-label">Name *</label> <label className="form-label">Name *</label>
<input <input
@ -455,7 +420,7 @@ function SkillsPage() {
{modalType === 'method' && ( {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"> <div className="form-row">
<label className="form-label">Typische Dauer (min)</label> <label className="form-label">Typische Dauer (min)</label>
<input <input
@ -492,8 +457,8 @@ function SkillsPage() {
</select> </select>
</div> </div>
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1.5rem' }}> <div className="skills-page-modal__footer">
<button type="submit" className="btn btn-primary" style={{ flex: 1 }}> <button type="submit" className="btn btn-primary skills-page-modal__submit">
{editing ? 'Speichern' : 'Erstellen'} {editing ? 'Speichern' : 'Erstellen'}
</button> </button>
<button <button
@ -505,6 +470,7 @@ function SkillsPage() {
</button> </button>
</div> </div>
</form> </form>
</div>
</div> </div>
</div> </div>
)} )}