feat: enhance RichTextEditor and exercise variant management
- Added support for saving and restoring text selection in the RichTextEditor, improving user experience during formatting. - Updated CSS styles for the RichTextEditor to enhance list formatting and overall appearance. - Introduced new ExerciseVariantFields component to streamline the editing of exercise variants, including fields for variant name, description, execution changes, and more. - Enhanced ExerciseFormPage to manage exercise variants more effectively, including improved state handling and UI updates for variant selection.
This commit is contained in:
parent
1ee1a2f2d9
commit
91b3fec5cc
|
|
@ -2406,6 +2406,26 @@ a.analysis-split__nav-item {
|
|||
overflow-y: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
/* Listen im Editor (nicht nur in .rich-text-content) – sonst „unsichtbare“ Bullets */
|
||||
.rich-text-editor ul,
|
||||
.rich-text-editor ol {
|
||||
margin: 0.35rem 0;
|
||||
padding-left: 1.35rem;
|
||||
list-style-position: outside;
|
||||
}
|
||||
.rich-text-editor ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.rich-text-editor ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
.rich-text-editor li {
|
||||
margin: 0.15rem 0;
|
||||
display: list-item;
|
||||
}
|
||||
.rich-text-editor p {
|
||||
margin: 0.35rem 0;
|
||||
}
|
||||
.rich-text-editor:empty:before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--text3);
|
||||
|
|
@ -2441,6 +2461,45 @@ a.analysis-split__nav-item {
|
|||
.exercise-card__body {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.exercise-variants-details summary {
|
||||
list-style: none;
|
||||
}
|
||||
.exercise-variants-details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.exercise-variants-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.exercise-variants-summary__title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.exercise-variants-summary__badge {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text2);
|
||||
background: var(--surface2);
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.exercise-variants-details__body {
|
||||
padding: 0 16px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.exercise-variants-hint {
|
||||
font-size: 13px;
|
||||
color: var(--text2);
|
||||
margin: 12px 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.exercise-card__actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,30 @@ function exec(cmd, value = null) {
|
|||
}
|
||||
}
|
||||
|
||||
/** Selection speichern, bevor Toolbar-Klicks sie zerstört (mousedown + preventDefault allein reicht nicht überall). */
|
||||
function saveSelectionInside(editorEl) {
|
||||
const sel = window.getSelection()
|
||||
if (!sel || sel.rangeCount === 0 || !editorEl) return null
|
||||
try {
|
||||
const range = sel.getRangeAt(0)
|
||||
if (!editorEl.contains(range.commonAncestorContainer)) return null
|
||||
return range.cloneRange()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function restoreSelection(range) {
|
||||
if (!range) return
|
||||
try {
|
||||
const sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
/** Browser: formatBlock erwartet oft Tag in Großschreibung. */
|
||||
function formatBlock(tag) {
|
||||
const t = String(tag).toUpperCase()
|
||||
|
|
@ -44,14 +68,29 @@ export default function RichTextEditor({ value, onChange, placeholder, minHeight
|
|||
|
||||
const run = (fn) => (e) => {
|
||||
e.preventDefault()
|
||||
ref.current?.focus()
|
||||
e.stopPropagation()
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const saved = saveSelectionInside(el)
|
||||
el.focus()
|
||||
restoreSelection(saved)
|
||||
try {
|
||||
document.execCommand('styleWithCSS', false, false)
|
||||
} catch {
|
||||
/* optional */
|
||||
}
|
||||
fn()
|
||||
sync()
|
||||
}
|
||||
|
||||
const onLink = (e) => {
|
||||
e.preventDefault()
|
||||
ref.current?.focus()
|
||||
e.stopPropagation()
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const saved = saveSelectionInside(el)
|
||||
el.focus()
|
||||
restoreSelection(saved)
|
||||
const url = window.prompt('Link-URL (https://…)')
|
||||
if (url) {
|
||||
exec('createLink', url)
|
||||
|
|
@ -79,7 +118,12 @@ export default function RichTextEditor({ value, onChange, placeholder, minHeight
|
|||
Ü3
|
||||
</button>
|
||||
<span className="rte-sep" />
|
||||
<button type="button" className="rte-btn" title="Aufzählung" onMouseDown={run(() => exec('insertUnorderedList'))}>
|
||||
<button
|
||||
type="button"
|
||||
className="rte-btn"
|
||||
title="Aufzählung (Mehrzeilen-Markierung möglich)"
|
||||
onMouseDown={run(() => exec('insertUnorderedList'))}
|
||||
>
|
||||
• Liste
|
||||
</button>
|
||||
<button type="button" className="rte-btn" title="Nummerierte Liste" onMouseDown={run(() => exec('insertOrderedList'))}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import api, { buildExerciseApiPayload } from '../utils/api'
|
||||
import RichTextEditor from '../components/RichTextEditor'
|
||||
|
|
@ -87,6 +87,126 @@ function buildVariantPayloadFromRow(row) {
|
|||
}
|
||||
}
|
||||
|
||||
/** Gemeinsame Felder für „Variante bearbeiten“ und „Neue Variante“. */
|
||||
function ExerciseVariantFields({ row, onPatch, prerequisiteOthers, rteMinHeight = '110px' }) {
|
||||
return (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Variantenname *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={row.variant_name || ''}
|
||||
onChange={(e) => onPatch({ variant_name: e.target.value })}
|
||||
minLength={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Kurzbeschreibung</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={row.description || ''}
|
||||
onChange={(e) => onPatch({ description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Abweichungen zur Durchführung</label>
|
||||
<RichTextEditor
|
||||
value={row.execution_changes || ''}
|
||||
onChange={(html) => onPatch({ execution_changes: html })}
|
||||
placeholder="Was unterscheidet diese Variante? (Listen über Symbolleiste)"
|
||||
minHeight={rteMinHeight}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Dauer Min</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={row.duration_min}
|
||||
onChange={(e) => onPatch({ duration_min: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Dauer Max</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={row.duration_max}
|
||||
onChange={(e) => onPatch({ duration_max: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Materialänderungen (eine Zeile pro Eintrag)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={row.equipment_lines || ''}
|
||||
onChange={(e) => onPatch({ equipment_lines: e.target.value })}
|
||||
placeholder="+ Pratzen"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '10px' }}>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Schwere relativ</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={row.difficulty_adjustment || ''}
|
||||
onChange={(e) => onPatch({ difficulty_adjustment: e.target.value })}
|
||||
>
|
||||
{VARIANT_DIFFICULTY.map((o) => (
|
||||
<option key={o.label} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Progressions-Stufe (1–10)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
className="form-input"
|
||||
value={row.progression_level}
|
||||
onChange={(e) =>
|
||||
onPatch({
|
||||
progression_level: e.target.value === '' ? '' : parseInt(e.target.value, 10),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Voraussetzungs-Variante</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={
|
||||
row.prerequisite_variant_id === '' || row.prerequisite_variant_id == null
|
||||
? ''
|
||||
: String(row.prerequisite_variant_id)
|
||||
}
|
||||
onChange={(e) =>
|
||||
onPatch({
|
||||
prerequisite_variant_id: e.target.value === '' ? '' : parseInt(e.target.value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">— keine —</option>
|
||||
{prerequisiteOthers.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.variant_name || `Variante #${o.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
title: '',
|
||||
|
|
@ -237,6 +357,8 @@ function ExerciseFormPage() {
|
|||
const [variantDraft, setVariantDraft] = useState(() => emptyVariantDraft())
|
||||
const [variantSavingId, setVariantSavingId] = useState(null)
|
||||
const [variantBusy, setVariantBusy] = useState(false)
|
||||
const [variantEditSelection, setVariantEditSelection] = useState(null)
|
||||
const variantsDetailsRef = useRef(null)
|
||||
|
||||
const [mediaFile, setMediaFile] = useState(null)
|
||||
const [mediaType, setMediaType] = useState('image')
|
||||
|
|
@ -284,6 +406,7 @@ function ExerciseFormPage() {
|
|||
setMediaList([])
|
||||
setVariants([])
|
||||
setVariantDraft(emptyVariantDraft())
|
||||
setVariantEditSelection(null)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
|
@ -297,6 +420,7 @@ function ExerciseFormPage() {
|
|||
setMediaList(exercise.media || [])
|
||||
setVariants((exercise.variants || []).map(apiVariantToRow))
|
||||
setVariantDraft(emptyVariantDraft())
|
||||
setVariantEditSelection(null)
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
alert(err.message || 'Übung nicht ladbar')
|
||||
|
|
@ -312,6 +436,19 @@ function ExerciseFormPage() {
|
|||
}
|
||||
}, [isEdit, exerciseId, navigate])
|
||||
|
||||
useEffect(() => {
|
||||
if (variantEditSelection == null || variantEditSelection === 'new') return
|
||||
if (!variants.some((v) => v.id === variantEditSelection)) {
|
||||
setVariantEditSelection(null)
|
||||
}
|
||||
}, [variants, variantEditSelection])
|
||||
|
||||
useEffect(() => {
|
||||
if (variantEditSelection != null && variantsDetailsRef.current) {
|
||||
variantsDetailsRef.current.open = true
|
||||
}
|
||||
}, [variantEditSelection])
|
||||
|
||||
const updateFormField = (field, value) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
|
@ -493,6 +630,7 @@ function ExerciseFormPage() {
|
|||
setVariantBusy(true)
|
||||
try {
|
||||
await api.deleteExerciseVariant(exerciseId, id)
|
||||
if (variantEditSelection === id) setVariantEditSelection(null)
|
||||
await refreshVariants()
|
||||
} catch (e) {
|
||||
alert(e.message || String(e))
|
||||
|
|
@ -530,9 +668,11 @@ function ExerciseFormPage() {
|
|||
}
|
||||
setVariantBusy(true)
|
||||
try {
|
||||
await api.createExerciseVariant(exerciseId, payload)
|
||||
const created = await api.createExerciseVariant(exerciseId, payload)
|
||||
setVariantDraft(emptyVariantDraft())
|
||||
await refreshVariants()
|
||||
if (created?.id != null) setVariantEditSelection(created.id)
|
||||
else setVariantEditSelection(null)
|
||||
} catch (err) {
|
||||
alert(err.message || String(err))
|
||||
} finally {
|
||||
|
|
@ -542,6 +682,12 @@ function ExerciseFormPage() {
|
|||
|
||||
const availableSkills = skillsCatalog.filter((s) => !formData.skills.some((x) => x.skill_id === s.id))
|
||||
|
||||
const selectedVariantForEdit =
|
||||
typeof variantEditSelection === 'number' ? variants.find((v) => v.id === variantEditSelection) : null
|
||||
const selectedVariantIdx = selectedVariantForEdit
|
||||
? variants.findIndex((v) => v.id === selectedVariantForEdit.id)
|
||||
: -1
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
|
|
@ -854,304 +1000,155 @@ function ExerciseFormPage() {
|
|||
</div>
|
||||
|
||||
{isEdit && (
|
||||
<div className="card" style={{ marginTop: '16px' }}>
|
||||
<h2 style={{ marginTop: 0, fontSize: '1.1rem' }}>Übungsvarianten</h2>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '13px', marginBottom: '12px' }}>
|
||||
Alternative Ausführungen zur Stammübung. Reihenfolge (Nach oben/unten) steuert Darstellung und Auswahl in der
|
||||
Trainingsplanung. „Voraussetzung“ verknüpft Varianten für spätere Progressions-Serien als Blöcke.
|
||||
</p>
|
||||
{variants.length === 0 && (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text3)', marginBottom: '12px' }}>Noch keine Varianten angelegt.</p>
|
||||
)}
|
||||
{variants.map((v, idx) => (
|
||||
<div
|
||||
key={v.id}
|
||||
style={{
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '10px',
|
||||
padding: '12px',
|
||||
marginBottom: '12px',
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '10px' }}>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text3)' }}>#{idx + 1}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||
disabled={variantBusy || idx === 0}
|
||||
onClick={() => moveVariantRow(idx, -1)}
|
||||
>
|
||||
Nach oben
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||
disabled={variantBusy || idx === variants.length - 1}
|
||||
onClick={() => moveVariantRow(idx, 1)}
|
||||
>
|
||||
Nach unten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
style={{ fontSize: '12px', marginLeft: 'auto' }}
|
||||
disabled={variantSavingId === v.id || variantBusy}
|
||||
onClick={() => saveVariantRow(v)}
|
||||
>
|
||||
{variantSavingId === v.id ? 'Speichern…' : 'Speichern'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
style={{ fontSize: '12px', background: 'var(--danger)', color: '#fff', border: 'none' }}
|
||||
disabled={variantBusy}
|
||||
onClick={() => deleteVariantRow(v.id)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
<details ref={variantsDetailsRef} className="card exercise-variants-details" style={{ marginTop: '16px' }}>
|
||||
<summary className="exercise-variants-summary">
|
||||
<span className="exercise-variants-summary__title">Übungsvarianten</span>
|
||||
<span className="exercise-variants-summary__badge">
|
||||
{variants.length === 0
|
||||
? 'keine'
|
||||
: `${variants.length} ${variants.length === 1 ? 'Variante' : 'Varianten'}`}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="exercise-variants-details__body">
|
||||
<p className="exercise-variants-hint">
|
||||
Pro Durchgang nur eine Variante bearbeiten – weniger Scrollen. Reihenfolge entspricht Planung und Auswahl im
|
||||
Training; „Voraussetzung“ nutzt ihr später für Progressions-Serien.
|
||||
</p>
|
||||
|
||||
{variants.length > 0 && (
|
||||
<div className="form-row">
|
||||
<label className="form-label">Variantenname *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={v.variant_name || ''}
|
||||
onChange={(e) => updateVariantField(v.id, { variant_name: e.target.value })}
|
||||
minLength={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Kurzbeschreibung</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={v.description || ''}
|
||||
onChange={(e) => updateVariantField(v.id, { description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Abweichungen zur Durchführung</label>
|
||||
<RichTextEditor
|
||||
value={v.execution_changes || ''}
|
||||
onChange={(html) => updateVariantField(v.id, { execution_changes: html })}
|
||||
placeholder="Was unterscheidet diese Variante?"
|
||||
minHeight="100px"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Dauer Min</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={v.duration_min}
|
||||
onChange={(e) => updateVariantField(v.id, { duration_min: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Dauer Max</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={v.duration_max}
|
||||
onChange={(e) => updateVariantField(v.id, { duration_max: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Materialänderungen (eine Zeile pro Eintrag)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={v.equipment_lines || ''}
|
||||
onChange={(e) => updateVariantField(v.id, { equipment_lines: e.target.value })}
|
||||
placeholder="+ Pratzen"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '10px' }}>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Schwere relativ</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={v.difficulty_adjustment || ''}
|
||||
onChange={(e) => updateVariantField(v.id, { difficulty_adjustment: e.target.value })}
|
||||
>
|
||||
{VARIANT_DIFFICULTY.map((o) => (
|
||||
<option key={o.label} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Progressions-Stufe (1–10)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
className="form-input"
|
||||
value={v.progression_level}
|
||||
onChange={(e) =>
|
||||
updateVariantField(v.id, {
|
||||
progression_level: e.target.value === '' ? '' : parseInt(e.target.value, 10),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Voraussetzungs-Variante</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={
|
||||
v.prerequisite_variant_id === '' || v.prerequisite_variant_id == null
|
||||
? ''
|
||||
: String(v.prerequisite_variant_id)
|
||||
}
|
||||
onChange={(e) =>
|
||||
updateVariantField(v.id, {
|
||||
prerequisite_variant_id: e.target.value === '' ? '' : parseInt(e.target.value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">— keine —</option>
|
||||
{variants
|
||||
.filter((o) => o.id !== v.id)
|
||||
.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.variant_name || `Variante #${o.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<form
|
||||
onSubmit={createVariantSubmit}
|
||||
style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border)' }}
|
||||
>
|
||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>Neue Variante anlegen</h3>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Variantenname *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={variantDraft.variant_name}
|
||||
onChange={(e) => setVariantDraft((d) => ({ ...d, variant_name: e.target.value }))}
|
||||
minLength={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Kurzbeschreibung</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={variantDraft.description}
|
||||
onChange={(e) => setVariantDraft((d) => ({ ...d, description: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Abweichungen zur Durchführung</label>
|
||||
<RichTextEditor
|
||||
value={variantDraft.execution_changes}
|
||||
onChange={(html) => setVariantDraft((d) => ({ ...d, execution_changes: html }))}
|
||||
placeholder="Optional: abweichende Schritte"
|
||||
minHeight="100px"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Dauer Min</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={variantDraft.duration_min}
|
||||
onChange={(e) => setVariantDraft((d) => ({ ...d, duration_min: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Dauer Max</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
value={variantDraft.duration_max}
|
||||
onChange={(e) => setVariantDraft((d) => ({ ...d, duration_max: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Materialänderungen</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={variantDraft.equipment_lines}
|
||||
onChange={(e) => setVariantDraft((d) => ({ ...d, equipment_lines: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '10px' }}>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Schwere relativ</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={variantDraft.difficulty_adjustment}
|
||||
onChange={(e) => setVariantDraft((d) => ({ ...d, difficulty_adjustment: e.target.value }))}
|
||||
>
|
||||
{VARIANT_DIFFICULTY.map((o) => (
|
||||
<option key={o.label} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Progressions-Stufe (1–10)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={10}
|
||||
className="form-input"
|
||||
value={variantDraft.progression_level}
|
||||
onChange={(e) =>
|
||||
setVariantDraft((d) => ({
|
||||
...d,
|
||||
progression_level: e.target.value === '' ? 1 : parseInt(e.target.value, 10),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Voraussetzungs-Variante</label>
|
||||
<label className="form-label" htmlFor="variant-edit-select">
|
||||
Variante auswählen
|
||||
</label>
|
||||
<select
|
||||
id="variant-edit-select"
|
||||
className="form-input"
|
||||
value={
|
||||
variantDraft.prerequisite_variant_id === '' || variantDraft.prerequisite_variant_id == null
|
||||
? ''
|
||||
: String(variantDraft.prerequisite_variant_id)
|
||||
}
|
||||
onChange={(e) =>
|
||||
setVariantDraft((d) => ({
|
||||
...d,
|
||||
prerequisite_variant_id: e.target.value === '' ? '' : parseInt(e.target.value, 10),
|
||||
}))
|
||||
variantEditSelection === 'new'
|
||||
? 'new'
|
||||
: variantEditSelection == null
|
||||
? ''
|
||||
: String(variantEditSelection)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
if (val === '') setVariantEditSelection(null)
|
||||
else if (val === 'new') setVariantEditSelection('new')
|
||||
else setVariantEditSelection(parseInt(val, 10))
|
||||
}}
|
||||
>
|
||||
<option value="">— keine —</option>
|
||||
{variants.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.variant_name || `Variante #${o.id}`}
|
||||
<option value="">— nicht bearbeiten —</option>
|
||||
{variants.map((v) => (
|
||||
<option key={v.id} value={v.id}>
|
||||
{(v.variant_name && String(v.variant_name).trim()) || `Variante #${v.id}`}
|
||||
</option>
|
||||
))}
|
||||
<option value="new">+ Neue Variante anlegen…</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary" disabled={variantBusy}>
|
||||
{variantBusy ? 'Anlegen…' : 'Variante anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variants.length === 0 && (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text3)', marginBottom: '10px' }}>
|
||||
Noch keine Varianten – optional für andere Ausführung, Dauer oder Material in Planung und Training.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{variants.length === 0 && variantEditSelection !== 'new' && (
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setVariantEditSelection('new')}>
|
||||
Erste Variante anlegen
|
||||
</button>
|
||||
)}
|
||||
|
||||
{variantEditSelection === 'new' && (
|
||||
<form
|
||||
className="exercise-variant-single-form"
|
||||
onSubmit={createVariantSubmit}
|
||||
style={{ marginTop: '14px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}
|
||||
>
|
||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>Neue Variante</h3>
|
||||
<ExerciseVariantFields
|
||||
row={variantDraft}
|
||||
onPatch={(patch) => setVariantDraft((d) => ({ ...d, ...patch }))}
|
||||
prerequisiteOthers={variants}
|
||||
rteMinHeight="110px"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary" style={{ marginTop: '10px' }} disabled={variantBusy}>
|
||||
{variantBusy ? 'Anlegen…' : 'Variante anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{selectedVariantForEdit && (
|
||||
<div
|
||||
className="exercise-variant-single-form"
|
||||
style={{ marginTop: '14px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text3)' }}>
|
||||
Pos. {selectedVariantIdx + 1} von {variants.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||
disabled={variantBusy || selectedVariantIdx <= 0}
|
||||
onClick={() => moveVariantRow(selectedVariantIdx, -1)}
|
||||
>
|
||||
Nach oben
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: '11px', padding: '4px 8px' }}
|
||||
disabled={variantBusy || selectedVariantIdx >= variants.length - 1}
|
||||
onClick={() => moveVariantRow(selectedVariantIdx, 1)}
|
||||
>
|
||||
Nach unten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
style={{ marginLeft: 'auto', fontSize: '12px' }}
|
||||
disabled={variantSavingId === selectedVariantForEdit.id || variantBusy}
|
||||
onClick={() => saveVariantRow(selectedVariantForEdit)}
|
||||
>
|
||||
{variantSavingId === selectedVariantForEdit.id ? 'Speichern…' : 'Speichern'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
style={{ fontSize: '12px', background: 'var(--danger)', color: '#fff', border: 'none' }}
|
||||
disabled={variantBusy}
|
||||
onClick={() => deleteVariantRow(selectedVariantForEdit.id)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
<ExerciseVariantFields
|
||||
row={selectedVariantForEdit}
|
||||
onPatch={(patch) => updateVariantField(selectedVariantForEdit.id, patch)}
|
||||
prerequisiteOthers={variants.filter((o) => o.id !== selectedVariantForEdit.id)}
|
||||
rteMinHeight="110px"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variants.length > 0 && variantEditSelection == null && (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text3)', marginTop: '12px', marginBottom: 0 }}>
|
||||
Wähle eine Variante zum Bearbeiten oder „Neue Variante anlegen…“.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{isEdit && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user