All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added a new meta panel for exercise classification and target groups, improving the organization of exercise attributes. - Introduced ExerciseCatalogAssocEditor component to manage focus areas, training styles, and target groups, enhancing user interaction. - Refactored CSS styles for the new meta panel and associated components, ensuring a cohesive design and improved responsiveness. - Removed the MultiAssocBlock component to streamline the code and improve maintainability.
98 lines
3.1 KiB
JavaScript
98 lines
3.1 KiB
JavaScript
import React from 'react'
|
|
|
|
/**
|
|
* Kompakte Katalog-Zuordnung (Fokus, Stile, Zielgruppen …) als Chip-Zeilen.
|
|
*/
|
|
export default function ExerciseCatalogAssocEditor({
|
|
title,
|
|
rows,
|
|
setRows,
|
|
options,
|
|
idKey,
|
|
emptyLabel,
|
|
showPrimary = true,
|
|
}) {
|
|
const setPrimary = (idx) => {
|
|
setRows(rows.map((r, i) => ({ ...r, is_primary: i === idx })))
|
|
}
|
|
const updateRow = (idx, patch) => {
|
|
const next = rows.map((r, i) => (i === idx ? { ...r, ...patch } : r))
|
|
if (patch.is_primary === true) {
|
|
next.forEach((r, i) => {
|
|
if (i !== idx) r.is_primary = false
|
|
})
|
|
}
|
|
setRows(next)
|
|
}
|
|
const addRow = () => setRows([...rows, { [idKey]: '', is_primary: rows.length === 0 }])
|
|
const removeRow = (idx) => {
|
|
const next = rows.filter((_, i) => i !== idx)
|
|
if (next.length && showPrimary && !next.some((r) => r.is_primary)) next[0].is_primary = true
|
|
setRows(next)
|
|
}
|
|
|
|
const optionLabel = (o) => {
|
|
const parts = []
|
|
if (o.icon) parts.push(o.icon)
|
|
parts.push(o.name)
|
|
if (o.abbreviation) parts.push(`(${o.abbreviation})`)
|
|
return parts.join(' ')
|
|
}
|
|
|
|
return (
|
|
<div className="exercise-meta-block">
|
|
<div className="exercise-meta-block__head">
|
|
<h4 className="exercise-meta-block__title">{title}</h4>
|
|
<button type="button" className="btn btn-secondary exercise-meta-block__add" onClick={addRow}>
|
|
+ Eintrag
|
|
</button>
|
|
</div>
|
|
{rows.length === 0 ? (
|
|
<p className="exercise-meta-block__empty">{emptyLabel}</p>
|
|
) : (
|
|
<div className="exercise-meta-block__chips" role="list">
|
|
{rows.map((row, idx) => (
|
|
<div key={idx} className="exercise-catalog-chip" role="listitem">
|
|
<select
|
|
className="exercise-catalog-chip__select"
|
|
value={row[idKey] || ''}
|
|
aria-label={`${title} wählen`}
|
|
onChange={(e) =>
|
|
updateRow(idx, { [idKey]: e.target.value ? parseInt(e.target.value, 10) : '' })
|
|
}
|
|
>
|
|
<option value="">— wählen —</option>
|
|
{options.map((o) => (
|
|
<option key={o.id} value={o.id}>
|
|
{optionLabel(o)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{showPrimary ? (
|
|
<button
|
|
type="button"
|
|
className={`exercise-catalog-chip__primary${row.is_primary ? ' exercise-catalog-chip__primary--on' : ''}`}
|
|
title={row.is_primary ? 'Primär' : 'Als primär markieren'}
|
|
aria-pressed={!!row.is_primary}
|
|
onClick={() => setPrimary(idx)}
|
|
>
|
|
★
|
|
</button>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
className="exercise-catalog-chip__remove"
|
|
aria-label="Entfernen"
|
|
title="Entfernen"
|
|
onClick={() => removeRow(idx)}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|