feat: enhance TrainingUnitSectionsEditor with heading accessory support
- Added a new `headingAccessory` prop to TrainingUnitSectionsEditor for customizable header content. - Updated the component to conditionally render the heading and accessory in a flexible toolbar layout. - Refactored TrainingFrameworkProgramEditPage and TrainingPlanningPage to utilize the new heading accessory feature, improving user interaction and layout consistency.
This commit is contained in:
parent
2bfe67879f
commit
4080088d42
|
|
@ -46,6 +46,7 @@ export default function TrainingUnitSectionsEditor({
|
||||||
showExecutionExtras = false,
|
showExecutionExtras = false,
|
||||||
heading = 'Abschnitte & Übungen',
|
heading = 'Abschnitte & Übungen',
|
||||||
hideHeading = false,
|
hideHeading = false,
|
||||||
|
headingAccessory = null,
|
||||||
wideExerciseGrid = false,
|
wideExerciseGrid = false,
|
||||||
enableItemDragReorder = true,
|
enableItemDragReorder = true,
|
||||||
enableSectionDragReorder = true,
|
enableSectionDragReorder = true,
|
||||||
|
|
@ -352,9 +353,40 @@ export default function TrainingUnitSectionsEditor({
|
||||||
'training-unit-sections-editor' +
|
'training-unit-sections-editor' +
|
||||||
(wideExerciseGrid ? ' training-unit-sections-editor--wide' : '')
|
(wideExerciseGrid ? ' training-unit-sections-editor--wide' : '')
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
{(!hideHeading || headingAccessory) ? (
|
||||||
|
<div
|
||||||
|
className="tu-editor-heading-toolbar"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!hideHeading ? (
|
{!hideHeading ? (
|
||||||
<h3 style={{ margin: '0 0 0.75rem', fontSize: '1rem' }}>{heading}</h3>
|
<h3 style={{ margin: 0, fontSize: '1rem', flex: '1 1 200px', minWidth: 0 }}>
|
||||||
|
{heading}
|
||||||
|
</h3>
|
||||||
|
) : headingAccessory ? (
|
||||||
|
<span style={{ flex: '1 1 auto', minWidth: 0 }} />
|
||||||
|
) : null}
|
||||||
|
{headingAccessory ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '8px',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{headingAccessory}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{list.map((sec, sIdx) => {
|
{list.map((sec, sIdx) => {
|
||||||
const planMin = sectionPlannedMinutes(sec)
|
const planMin = sectionPlannedMinutes(sec)
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ import ExercisePeekModal from '../components/ExercisePeekModal'
|
||||||
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
|
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
|
||||||
import {
|
import {
|
||||||
defaultSection,
|
defaultSection,
|
||||||
exerciseRow,
|
|
||||||
normalizeUnitToForm,
|
normalizeUnitToForm,
|
||||||
enrichSectionsWithVariants,
|
enrichSectionsWithVariants,
|
||||||
buildSectionsPayload,
|
buildSectionsPayload,
|
||||||
|
hydrateExercisePlanningRow,
|
||||||
} from '../utils/trainingUnitSectionsForm'
|
} from '../utils/trainingUnitSectionsForm'
|
||||||
|
|
||||||
const DND_FW_SLOT = 'application/x-shinkan-framework-slot'
|
const DND_FW_SLOT = 'application/x-shinkan-framework-slot'
|
||||||
|
|
@ -52,27 +52,6 @@ async function enrichFrameworkSlotSections(slots) {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hydrateExerciseForSlotRow(exercise) {
|
|
||||||
let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
|
|
||||||
let title = exercise?.title || ''
|
|
||||||
const id = exercise?.id
|
|
||||||
if (!id) return null
|
|
||||||
if (!variants.length) {
|
|
||||||
try {
|
|
||||||
const full = await api.getExercise(id)
|
|
||||||
variants = Array.isArray(full?.variants) ? full.variants : []
|
|
||||||
title = full?.title || title
|
|
||||||
} catch {
|
|
||||||
variants = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const row = exerciseRow()
|
|
||||||
row.exercise_id = id
|
|
||||||
row.exercise_variant_id = ''
|
|
||||||
row.exercise_title = title
|
|
||||||
row.variants = variants
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
function goalHoverText(g) {
|
function goalHoverText(g) {
|
||||||
const t = (g.title || '').trim() || 'Ohne Titel'
|
const t = (g.title || '').trim() || 'Ohne Titel'
|
||||||
const n = (g.notes || '').trim()
|
const n = (g.notes || '').trim()
|
||||||
|
|
@ -1138,7 +1117,7 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
const { slotIdx, sectionIndex: sIdx } = sectionPickerCtx
|
const { slotIdx, sectionIndex: sIdx } = sectionPickerCtx
|
||||||
const rows = []
|
const rows = []
|
||||||
for (const ex of picked) {
|
for (const ex of picked) {
|
||||||
const row = await hydrateExerciseForSlotRow(ex)
|
const row = await hydrateExercisePlanningRow(ex)
|
||||||
if (row) rows.push(row)
|
if (row) rows.push(row)
|
||||||
}
|
}
|
||||||
if (!rows.length) return
|
if (!rows.length) return
|
||||||
|
|
@ -1166,7 +1145,7 @@ export default function TrainingFrameworkProgramEditPage() {
|
||||||
if (sectionPickerCtx.multi) return
|
if (sectionPickerCtx.multi) return
|
||||||
if (typeof sectionPickerCtx.itemIndex !== 'number') return
|
if (typeof sectionPickerCtx.itemIndex !== 'number') return
|
||||||
const { slotIdx, sectionIndex: sIdx, itemIndex: iIdx } = sectionPickerCtx
|
const { slotIdx, sectionIndex: sIdx, itemIndex: iIdx } = sectionPickerCtx
|
||||||
const row = await hydrateExerciseForSlotRow(exercise)
|
const row = await hydrateExercisePlanningRow(exercise)
|
||||||
if (!row) return
|
if (!row) return
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
|
||||||
|
|
@ -7,34 +7,12 @@ import ExercisePeekModal from '../components/ExercisePeekModal'
|
||||||
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
|
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
|
||||||
import {
|
import {
|
||||||
defaultSection,
|
defaultSection,
|
||||||
exerciseRow,
|
|
||||||
normalizeUnitToForm,
|
normalizeUnitToForm,
|
||||||
enrichSectionsWithVariants,
|
enrichSectionsWithVariants,
|
||||||
buildSectionsPayload,
|
buildSectionsPayload,
|
||||||
|
hydrateExercisePlanningRow,
|
||||||
} from '../utils/trainingUnitSectionsForm'
|
} from '../utils/trainingUnitSectionsForm'
|
||||||
|
|
||||||
async function hydrateExerciseForPickerRow(exercise) {
|
|
||||||
let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
|
|
||||||
let title = exercise?.title || ''
|
|
||||||
const id = exercise?.id
|
|
||||||
if (!id) return null
|
|
||||||
if (!variants.length) {
|
|
||||||
try {
|
|
||||||
const full = await api.getExercise(id)
|
|
||||||
variants = Array.isArray(full?.variants) ? full.variants : []
|
|
||||||
title = full?.title || title
|
|
||||||
} catch {
|
|
||||||
variants = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const row = exerciseRow()
|
|
||||||
row.exercise_id = id
|
|
||||||
row.exercise_variant_id = ''
|
|
||||||
row.exercise_title = title
|
|
||||||
row.variants = variants
|
|
||||||
return row
|
|
||||||
}
|
|
||||||
|
|
||||||
function TrainingPlanningPage() {
|
function TrainingPlanningPage() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const [groups, setGroups] = useState([])
|
const [groups, setGroups] = useState([])
|
||||||
|
|
@ -634,7 +612,7 @@ function TrainingPlanningPage() {
|
||||||
background: 'var(--surface)',
|
background: 'var(--surface)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: 'clamp(12px, 3vw, 2rem)',
|
padding: 'clamp(12px, 3vw, 2rem)',
|
||||||
maxWidth: 'min(960px, 100%)',
|
maxWidth: 'min(1100px, 100%)',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxHeight: '92vh',
|
maxHeight: '92vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
|
|
@ -715,26 +693,16 @@ function TrainingPlanningPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div style={{ marginTop: '2rem' }}>
|
||||||
style={{
|
<TrainingUnitSectionsEditor
|
||||||
display: 'flex',
|
heading="Abschnitte & Übungen"
|
||||||
justifyContent: 'space-between',
|
headingAccessory={
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: '2rem',
|
|
||||||
marginBottom: '0.75rem',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: '0.5rem'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h3 style={{ margin: 0 }}>Abschnitte & Übungen</h3>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={handleSaveAsTemplate}>
|
<button type="button" className="btn btn-secondary" onClick={handleSaveAsTemplate}>
|
||||||
Vorlage aus Aufbau speichern
|
Vorlage aus Aufbau speichern
|
||||||
</button>
|
</button>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<TrainingUnitSectionsEditor
|
|
||||||
hideHeading
|
|
||||||
sections={formData.sections}
|
sections={formData.sections}
|
||||||
|
wideExerciseGrid
|
||||||
onSectionsChange={(updater) =>
|
onSectionsChange={(updater) =>
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -754,6 +722,7 @@ function TrainingPlanningPage() {
|
||||||
}
|
}
|
||||||
showExecutionExtras={!!editingUnit}
|
showExecutionExtras={!!editingUnit}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '1.75rem' }} />
|
<div style={{ marginBottom: '1.75rem' }} />
|
||||||
|
|
||||||
|
|
@ -867,7 +836,7 @@ function TrainingPlanningPage() {
|
||||||
const { sIdx } = exercisePickerTarget
|
const { sIdx } = exercisePickerTarget
|
||||||
const rows = []
|
const rows = []
|
||||||
for (const ex of picked) {
|
for (const ex of picked) {
|
||||||
const row = await hydrateExerciseForPickerRow(ex)
|
const row = await hydrateExercisePlanningRow(ex)
|
||||||
if (row) rows.push(row)
|
if (row) rows.push(row)
|
||||||
}
|
}
|
||||||
if (!rows.length) return
|
if (!rows.length) return
|
||||||
|
|
@ -884,7 +853,7 @@ function TrainingPlanningPage() {
|
||||||
}
|
}
|
||||||
onSelectExercise={async (ex) => {
|
onSelectExercise={async (ex) => {
|
||||||
if (!exercisePickerTarget || exercisePickerTarget.multi) return
|
if (!exercisePickerTarget || exercisePickerTarget.multi) return
|
||||||
const row = await hydrateExerciseForPickerRow(ex)
|
const row = await hydrateExercisePlanningRow(ex)
|
||||||
if (!row) return
|
if (!row) return
|
||||||
const { sIdx, iIdx } = exercisePickerTarget
|
const { sIdx, iIdx } = exercisePickerTarget
|
||||||
if (typeof iIdx !== 'number') return
|
if (typeof iIdx !== 'number') return
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,28 @@ export function exerciseRow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function hydrateExercisePlanningRow(exercise) {
|
||||||
|
let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
|
||||||
|
let title = exercise?.title || ''
|
||||||
|
const id = exercise?.id
|
||||||
|
if (!id) return null
|
||||||
|
if (!variants.length) {
|
||||||
|
try {
|
||||||
|
const full = await api.getExercise(id)
|
||||||
|
variants = Array.isArray(full?.variants) ? full.variants : []
|
||||||
|
title = full?.title || title
|
||||||
|
} catch {
|
||||||
|
variants = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const row = exerciseRow()
|
||||||
|
row.exercise_id = id
|
||||||
|
row.exercise_variant_id = ''
|
||||||
|
row.exercise_title = title
|
||||||
|
row.variants = variants
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
export function noteRow() {
|
export function noteRow() {
|
||||||
return { item_type: 'note', note_body: '' }
|
return { item_type: 'note', note_body: '' }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user