Compare commits
No commits in common. "7450c269a54cf51714108febec643e8cafd0c5af" and "99a5fccaa5d38c8ab046f32c87cbf497cf80a7dc" have entirely different histories.
7450c269a5
...
99a5fccaa5
|
|
@ -2095,17 +2095,7 @@ def list_exercises(
|
||||||
FROM exercise_training_types ett
|
FROM exercise_training_types ett
|
||||||
JOIN training_types tt ON tt.id = ett.training_type_id
|
JOIN training_types tt ON tt.id = ett.training_type_id
|
||||||
WHERE ett.exercise_id = e.id
|
WHERE ett.exercise_id = e.id
|
||||||
) AS training_type_names,
|
) AS training_type_names
|
||||||
(
|
|
||||||
SELECT COUNT(*)::int
|
|
||||||
FROM exercise_variants ev
|
|
||||||
WHERE ev.exercise_id = e.id
|
|
||||||
) AS variant_count,
|
|
||||||
(
|
|
||||||
SELECT COUNT(*)::int
|
|
||||||
FROM exercise_media em
|
|
||||||
WHERE em.exercise_id = e.id
|
|
||||||
) AS media_count
|
|
||||||
{variants_sql}
|
{variants_sql}
|
||||||
FROM exercises e
|
FROM exercises e
|
||||||
LEFT JOIN profiles p ON e.created_by = p.id
|
LEFT JOIN profiles p ON e.created_by = p.id
|
||||||
|
|
@ -2128,8 +2118,6 @@ def list_exercises(
|
||||||
d["focus_area_names"] = _coerce_json_str_list(d.get("focus_area_names"))
|
d["focus_area_names"] = _coerce_json_str_list(d.get("focus_area_names"))
|
||||||
d["style_direction_names"] = _coerce_json_str_list(d.get("style_direction_names"))
|
d["style_direction_names"] = _coerce_json_str_list(d.get("style_direction_names"))
|
||||||
d["training_type_names"] = _coerce_json_str_list(d.get("training_type_names"))
|
d["training_type_names"] = _coerce_json_str_list(d.get("training_type_names"))
|
||||||
d["variant_count"] = int(d.get("variant_count") or 0)
|
|
||||||
d["media_count"] = int(d.get("media_count") or 0)
|
|
||||||
if include_variants:
|
if include_variants:
|
||||||
v = d.get("variants")
|
v = d.get("variants")
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
|
|
|
||||||
|
|
@ -4819,34 +4819,6 @@ html.modal-scroll-locked .app-main {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.exercise-card__footer-meta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
min-width: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
.exercise-card__content-stats {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: var(--text3);
|
|
||||||
}
|
|
||||||
.exercise-card__stat {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
color: var(--text3);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.exercise-card__stat-num {
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.exercise-card__meta-compact {
|
.exercise-card__meta-compact {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import { COMBINATION_ARCHETYPE_OPTIONS, ARCHETYPE_DEFAULT_REP_SERIES_COUNT, defa
|
||||||
import { readSlotProfilesV1, normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../../utils/combinationMethodProfileUi'
|
import { readSlotProfilesV1, normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../../utils/combinationMethodProfileUi'
|
||||||
import { GripVertical } from 'lucide-react'
|
import { GripVertical } from 'lucide-react'
|
||||||
import UnsavedChangesPrompt from '../UnsavedChangesPrompt'
|
import UnsavedChangesPrompt from '../UnsavedChangesPrompt'
|
||||||
import PageFormEditorChrome from '../PageFormEditorChrome'
|
|
||||||
import { useBeforeUnloadWhen, useUnsavedChangesBlocker } from '../../hooks/useUnsavedChangesBlocker'
|
import { useBeforeUnloadWhen, useUnsavedChangesBlocker } from '../../hooks/useUnsavedChangesBlocker'
|
||||||
|
|
||||||
const INTENSITY_OPTIONS = [
|
const INTENSITY_OPTIONS = [
|
||||||
|
|
@ -837,7 +836,7 @@ function ExerciseFormPageRoot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const performSaveAttempt = useCallback(
|
const performSaveAttempt = useCallback(
|
||||||
async ({ fromUnsavedDialog = false, closeAfter = false } = {}) => {
|
async ({ fromUnsavedDialog = false } = {}) => {
|
||||||
if (!formData.title || formData.title.trim().length < 3) {
|
if (!formData.title || formData.title.trim().length < 3) {
|
||||||
toast.error('Titel mindestens 3 Zeichen')
|
toast.error('Titel mindestens 3 Zeichen')
|
||||||
return false
|
return false
|
||||||
|
|
@ -941,15 +940,12 @@ function ExerciseFormPageRoot() {
|
||||||
setVariants((ex.variants || []).map(apiVariantToRow))
|
setVariants((ex.variants || []).map(apiVariantToRow))
|
||||||
setFormDirty(false)
|
setFormDirty(false)
|
||||||
toast.success('Gespeichert.')
|
toast.success('Gespeichert.')
|
||||||
if (closeAfter) navigate('/exercises')
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const created = await api.createExercise(payload)
|
const created = await api.createExercise(payload)
|
||||||
setFormDirty(false)
|
setFormDirty(false)
|
||||||
toast.success('Übung angelegt.')
|
toast.success('Übung angelegt.')
|
||||||
if (closeAfter) {
|
if (!fromUnsavedDialog) {
|
||||||
navigate('/exercises')
|
|
||||||
} else if (!fromUnsavedDialog) {
|
|
||||||
navigate(`/exercises/${created.id}/edit`, { replace: true })
|
navigate(`/exercises/${created.id}/edit`, { replace: true })
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
@ -963,39 +959,10 @@ function ExerciseFormPageRoot() {
|
||||||
[exerciseId, formData, isEdit, navigate, toast],
|
[exerciseId, formData, isEdit, navigate, toast],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = async (e) => {
|
||||||
async (e) => {
|
e.preventDefault()
|
||||||
e?.preventDefault?.()
|
await performSaveAttempt({ fromUnsavedDialog: false })
|
||||||
await performSaveAttempt({ fromUnsavedDialog: false, closeAfter: false })
|
}
|
||||||
},
|
|
||||||
[performSaveAttempt],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSaveAndClose = useCallback(
|
|
||||||
async (e) => {
|
|
||||||
e?.preventDefault?.()
|
|
||||||
await performSaveAttempt({ fromUnsavedDialog: false, closeAfter: true })
|
|
||||||
},
|
|
||||||
[performSaveAttempt],
|
|
||||||
)
|
|
||||||
|
|
||||||
const goBackToList = useCallback(() => {
|
|
||||||
navigate('/exercises')
|
|
||||||
}, [navigate])
|
|
||||||
|
|
||||||
const actionConfig = useMemo(
|
|
||||||
() => ({
|
|
||||||
formId: 'exercise-form',
|
|
||||||
saving,
|
|
||||||
isNew: !isEdit,
|
|
||||||
onSave: handleSubmit,
|
|
||||||
onSaveAndClose: handleSaveAndClose,
|
|
||||||
onCancel: goBackToList,
|
|
||||||
showSave: true,
|
|
||||||
showSaveAndClose: true,
|
|
||||||
}),
|
|
||||||
[saving, isEdit, handleSubmit, handleSaveAndClose, goBackToList],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleUnsavedDialogSave = async () => {
|
const handleUnsavedDialogSave = async () => {
|
||||||
const ok = await performSaveAttempt({ fromUnsavedDialog: true })
|
const ok = await performSaveAttempt({ fromUnsavedDialog: true })
|
||||||
|
|
@ -1195,28 +1162,27 @@ function ExerciseFormPageRoot() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageFormEditorChrome
|
<div style={{ padding: '12px' }} className="app-page">
|
||||||
testId="exercise-form-page"
|
<div style={{ marginBottom: '12px' }}>
|
||||||
title={isEdit ? 'Übung bearbeiten' : 'Neue Übung'}
|
<button type="button" className="btn btn-secondary" onClick={() => navigate('/exercises')}>
|
||||||
backTo="/exercises"
|
← Übersicht
|
||||||
backLabel="Übersicht"
|
</button>
|
||||||
actionConfig={actionConfig}
|
{isEdit && (
|
||||||
>
|
<button
|
||||||
{isEdit ? (
|
type="button"
|
||||||
<p style={{ margin: '0 0 12px' }}>
|
|
||||||
<Link
|
|
||||||
to={`/exercises/${exerciseId}`}
|
|
||||||
state={{ fromExerciseEdit: true }}
|
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
style={{ fontSize: '0.875rem' }}
|
style={{ marginLeft: '8px' }}
|
||||||
|
onClick={() => navigate(`/exercises/${exerciseId}`, { state: { fromExerciseEdit: true } })}
|
||||||
>
|
>
|
||||||
Ansehen
|
Ansehen
|
||||||
</Link>
|
</button>
|
||||||
</p>
|
)}
|
||||||
) : null}
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<form id="exercise-form" onSubmit={handleSubmit}>
|
<h1 style={{ marginTop: 0, fontSize: '1.25rem' }}>{isEdit ? 'Übung bearbeiten' : 'Neue Übung'}</h1>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Titel *</label>
|
<label className="form-label">Titel *</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -1977,6 +1943,12 @@ function ExerciseFormPageRoot() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
<button type="submit" className="btn btn-primary" disabled={saving}>
|
||||||
|
{saving ? 'Speichern…' : isEdit ? 'Speichern' : 'Anlegen & weiter'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2467,9 +2439,8 @@ function ExerciseFormPageRoot() {
|
||||||
isBusy={saving}
|
isBusy={saving}
|
||||||
onSave={handleUnsavedDialogSave}
|
onSave={handleUnsavedDialogSave}
|
||||||
onDiscardWithoutSave={() => setFormDirty(false)}
|
onDiscardWithoutSave={() => setFormDirty(false)}
|
||||||
detail="Du hast ungespeicherte Änderungen vorgenommen. Möchtest du die Seite wirklich verlassen?"
|
|
||||||
/>
|
/>
|
||||||
</PageFormEditorChrome>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ import {
|
||||||
Archive,
|
Archive,
|
||||||
CircleDot,
|
CircleDot,
|
||||||
FilePenLine,
|
FilePenLine,
|
||||||
Image,
|
|
||||||
Layers,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import ExerciseRichTextBlock from '../ExerciseRichTextBlock'
|
import ExerciseRichTextBlock from '../ExerciseRichTextBlock'
|
||||||
import { coerceApiNameList } from '../../utils/sanitizeHtml'
|
import { coerceApiNameList } from '../../utils/sanitizeHtml'
|
||||||
|
|
@ -50,52 +48,6 @@ function exerciseCardClassName(exercise, userId) {
|
||||||
.join(' ')
|
.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function exerciseVariantCount(exercise) {
|
|
||||||
if (typeof exercise?.variant_count === 'number' && Number.isFinite(exercise.variant_count)) {
|
|
||||||
return Math.max(0, exercise.variant_count)
|
|
||||||
}
|
|
||||||
return Array.isArray(exercise?.variants) ? exercise.variants.length : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function exerciseMediaCount(exercise) {
|
|
||||||
if (typeof exercise?.media_count === 'number' && Number.isFinite(exercise.media_count)) {
|
|
||||||
return Math.max(0, exercise.media_count)
|
|
||||||
}
|
|
||||||
return Array.isArray(exercise?.media) ? exercise.media.length : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExerciseCardContentStats({ exercise }) {
|
|
||||||
const mediaCount = exerciseMediaCount(exercise)
|
|
||||||
const variantCount = exerciseVariantCount(exercise)
|
|
||||||
if (mediaCount <= 0 && variantCount <= 0) return null
|
|
||||||
|
|
||||||
const mediaLabel =
|
|
||||||
mediaCount === 1 ? '1 Medium hinterlegt' : `${mediaCount} Medien hinterlegt`
|
|
||||||
const variantLabel =
|
|
||||||
variantCount === 1 ? '1 Variante' : `${variantCount} Varianten`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="exercise-card__content-stats">
|
|
||||||
{mediaCount > 0 ? (
|
|
||||||
<span className="exercise-card__stat" title={mediaLabel} aria-label={mediaLabel}>
|
|
||||||
<Image size={15} strokeWidth={2} aria-hidden />
|
|
||||||
<span className="exercise-card__stat-num" aria-hidden>
|
|
||||||
{mediaCount}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{variantCount > 0 ? (
|
|
||||||
<span className="exercise-card__stat" title={variantLabel} aria-label={variantLabel}>
|
|
||||||
<Layers size={15} strokeWidth={2} aria-hidden />
|
|
||||||
<span className="exercise-card__stat-num" aria-hidden>
|
|
||||||
{variantCount}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExerciseCardScopeStatus({ exercise }) {
|
function ExerciseCardScopeStatus({ exercise }) {
|
||||||
const v = exercise.visibility || 'private'
|
const v = exercise.visibility || 'private'
|
||||||
const s = exercise.status || 'draft'
|
const s = exercise.status || 'draft'
|
||||||
|
|
@ -186,10 +138,7 @@ export default function ExerciseListCard({ exercise, user, selectedIds, toggleSe
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="exercise-card__footer">
|
<div className="exercise-card__footer">
|
||||||
<div className="exercise-card__footer-meta">
|
|
||||||
<ExerciseCardScopeStatus exercise={exercise} />
|
<ExerciseCardScopeStatus exercise={exercise} />
|
||||||
<ExerciseCardContentStats exercise={exercise} />
|
|
||||||
</div>
|
|
||||||
<div className="exercise-card__actions exercise-card__actions--icons">
|
<div className="exercise-card__actions exercise-card__actions--icons">
|
||||||
<Link
|
<Link
|
||||||
to={`/exercises/${exercise.id}`}
|
to={`/exercises/${exercise.id}`}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user