From cce1e58c7bc969c741733efe97a31b969ba35e65 Mon Sep 17 00:00:00 2001 From: AG Date: Mon, 24 Nov 2025 22:11:53 +0200 Subject: [PATCH] 1. Plan creation, editing and deletion fixed. 2. Planned sets drag&drop implemented. --- components/Plans.tsx | 213 ++++++++++++++++++++++++++++++++----- server/prisma/dev.db | Bin 61440 -> 61440 bytes server/src/routes/plans.ts | 22 ++-- 3 files changed, 202 insertions(+), 33 deletions(-) diff --git a/components/Plans.tsx b/components/Plans.tsx index 1bd5843..652b3a6 100644 --- a/components/Plans.tsx +++ b/components/Plans.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Scale } from 'lucide-react'; -import { WorkoutPlan, ExerciseDef, PlannedSet, Language } from '../types'; -import { getPlans, savePlan, deletePlan, getExercises } from '../services/storage'; +import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Scale, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical } from 'lucide-react'; +import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types'; +import { getPlans, savePlan, deletePlan, getExercises, saveExercise } from '../services/storage'; import { t } from '../services/i18n'; interface PlansProps { @@ -11,6 +11,24 @@ interface PlansProps { lang: Language; } +const FilledInput = ({ label, value, onChange, type = "number", icon, autoFocus, step }: any) => ( +
+ + +
+); + const Plans: React.FC = ({ userId, onStartPlan, lang }) => { const [plans, setPlans] = useState([]); const [isEditing, setIsEditing] = useState(false); @@ -23,6 +41,16 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { const [availableExercises, setAvailableExercises] = useState([]); const [showExerciseSelector, setShowExerciseSelector] = useState(false); + // Drag and Drop Refs + const dragItem = React.useRef(null); + const [draggingIndex, setDraggingIndex] = useState(null); + + // Create Exercise State + const [isCreatingExercise, setIsCreatingExercise] = useState(false); + const [newExName, setNewExName] = useState(''); + const [newExType, setNewExType] = useState(ExerciseType.STRENGTH); + const [newExBwPercentage, setNewExBwPercentage] = useState('100'); + useEffect(() => { const loadData = async () => { const fetchedPlans = await getPlans(userId); @@ -47,19 +75,29 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { setIsEditing(true); }; - const handleSave = () => { + const handleEdit = (plan: WorkoutPlan) => { + setEditId(plan.id); + setName(plan.name); + setDescription(plan.description || ''); + setSteps(plan.steps); + setIsEditing(true); + }; + + const handleSave = async () => { if (!name.trim() || !editId) return; const newPlan: WorkoutPlan = { id: editId, name, description, steps }; - savePlan(userId, newPlan); - setPlans(getPlans(userId)); + await savePlan(userId, newPlan); + const updated = await getPlans(userId); + setPlans(updated); setIsEditing(false); }; - const handleDelete = (id: string, e: React.MouseEvent) => { + const handleDelete = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); if (confirm(t('delete_confirm', lang))) { - deletePlan(userId, id); - setPlans(getPlans(userId)); + await deletePlan(userId, id); + const updated = await getPlans(userId); + setPlans(updated); } }; @@ -75,6 +113,37 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { setShowExerciseSelector(false); }; + const handleCreateExercise = async () => { + if (!newExName.trim()) return; + const newEx: ExerciseDef = { + id: crypto.randomUUID(), + name: newExName.trim(), + type: newExType, + ...(newExType === ExerciseType.BODYWEIGHT && { bodyWeightPercentage: parseFloat(newExBwPercentage) || 100 }) + }; + await saveExercise(userId, newEx); + const exList = await getExercises(userId); + setAvailableExercises(exList.filter(e => !e.isArchived)); + + // Automatically add the new exercise to the plan + addStep(newEx); + + setNewExName(''); + setNewExType(ExerciseType.STRENGTH); + setNewExBwPercentage('100'); + setIsCreatingExercise(false); + }; + + const exerciseTypeLabels: Record = { + [ExerciseType.STRENGTH]: t('type_strength', lang), + [ExerciseType.BODYWEIGHT]: t('type_bodyweight', lang), + [ExerciseType.CARDIO]: t('type_cardio', lang), + [ExerciseType.STATIC]: t('type_static', lang), + [ExerciseType.HIGH_JUMP]: t('type_height', lang), + [ExerciseType.LONG_JUMP]: t('type_dist', lang), + [ExerciseType.PLYOMETRIC]: t('type_jump', lang), + }; + const toggleWeighted = (stepId: string) => { setSteps(steps.map(s => s.id === stepId ? { ...s, isWeighted: !s.isWeighted } : s)); }; @@ -83,13 +152,27 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { setSteps(steps.filter(s => s.id !== stepId)); }; - const moveStep = (index: number, direction: 'up' | 'down') => { - if (direction === 'up' && index === 0) return; - if (direction === 'down' && index === steps.length - 1) return; + const onDragStart = (index: number) => { + dragItem.current = index; + setDraggingIndex(index); + }; + + const onDragEnter = (index: number) => { + if (dragItem.current === null) return; + if (dragItem.current === index) return; + const newSteps = [...steps]; - const targetIndex = direction === 'up' ? index - 1 : index + 1; - [newSteps[index], newSteps[targetIndex]] = [newSteps[targetIndex], newSteps[index]]; + const draggedItemContent = newSteps.splice(dragItem.current, 1)[0]; + newSteps.splice(index, 0, draggedItemContent); + setSteps(newSteps); + dragItem.current = index; + setDraggingIndex(index); + }; + + const onDragEnd = () => { + dragItem.current = null; + setDraggingIndex(null); }; if (isEditing) { @@ -131,18 +214,17 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => {
{steps.map((step, idx) => ( -
-
- {idx > 0 && ( - - )} - {idx < steps.length - 1 && ( - - )} +
onDragStart(idx)} + onDragEnter={() => onDragEnter(idx)} + onDragOver={(e) => e.preventDefault()} + onDragEnd={onDragEnd} + > +
+
@@ -185,7 +267,12 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => {
{t('select_exercise', lang)} - +
+ + +
{availableExercises.map(ex => ( @@ -199,6 +286,70 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { ))}
+ + {isCreatingExercise && ( +
+
+

{t('create_exercise', lang)}

+ +
+ +
+ setNewExName(e.target.value)} + type="text" + autoFocus + /> + +
+ +
+ {[ + { id: ExerciseType.STRENGTH, label: exerciseTypeLabels[ExerciseType.STRENGTH], icon: Dumbbell }, + { id: ExerciseType.BODYWEIGHT, label: exerciseTypeLabels[ExerciseType.BODYWEIGHT], icon: User }, + { id: ExerciseType.CARDIO, label: exerciseTypeLabels[ExerciseType.CARDIO], icon: Flame }, + { id: ExerciseType.STATIC, label: exerciseTypeLabels[ExerciseType.STATIC], icon: TimerIcon }, + { id: ExerciseType.HIGH_JUMP, label: exerciseTypeLabels[ExerciseType.HIGH_JUMP], icon: ArrowUp }, + { id: ExerciseType.LONG_JUMP, label: exerciseTypeLabels[ExerciseType.LONG_JUMP], icon: Ruler }, + { id: ExerciseType.PLYOMETRIC, label: exerciseTypeLabels[ExerciseType.PLYOMETRIC], icon: Footprints }, + ].map((type) => ( + + ))} +
+
+ + {newExType === ExerciseType.BODYWEIGHT && ( + setNewExBwPercentage(e.target.value)} + icon={} + /> + )} + +
+ +
+
+
+ )}
)}
@@ -231,6 +382,14 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => {
+
+ +

{plan.description || t('prep_no_instructions', lang)}

diff --git a/server/prisma/dev.db b/server/prisma/dev.db index 80bcfd0990857b90cc83963a56e29ad54bcaf943..eb065402bddc6534bc3be9f61660f26fc012d5e3 100644 GIT binary patch delta 2134 zcma)7O=w+36u$2#F)w}Z-c&&dweWmKTcHzX=FZHW5rn2~Y%AD?B-f%gwDa3Mn?Ldr zNtO7zv51IPo|_iIqQ#Z8#4Nf{S8f&Tu8S;+Yj=WzE9X8F@-$j57w+bq^PTT}=Vz`y zEM0wAx_+QA_Q_Mn3u7A>Z=W6NuiXlw8xalfhx1{jc58G0(1$f}ssFJwJ@j;8z!E7W z6&x^>a6qvC2`dP&R>uZWOJW8)8}E*cPc$#v`owB$7Nkq9V`opic5eLi$y3u8RG&vW z=#h8=og&-<2@?ZI!WrO56M&7-F`~vINi(FQdqKujc^ia6Ev=FiXe>O4WD4+vS%4&0 zF?2{d-lgL~=H~v*q~t-KIXEzYwMvx04k&73Kqy2&BBDU7IoHTY#4zbHIc691Y<*(c zwpvruljCPjPM=c6Z4ly&R{%>N%AqfZF)xgfzzIR15l%qJGLR?!1Eh1~!sVgtGL{|MWMOKBmG=KYyt9!dK$$2_u?GsiP|v?*ntdFn5A9xOgTTzfMp zRqq7Rty;Bur}y)t_GuNIuXsiK`U-(%OvWOnKw(4yaohoJwFJyMXJA4UhuSOp9KUDM zuM0Qt*{QYZ^8?$#&=>}tW`JSlgQ=9hyTSpeu@ypLs9@I9L*dblYlWL%j-|g%2SJf_ z7S>-c2SM=N=8x;NI2`^Sl)~FV_)~a0`X%~d_w&v9Abd5eI}giB=9q`kfKUBv=g*Ix z%{WYU?yrw@PJaA-wH!8r=z8>I_*vNSB0V0q*UE!wnhq2SK~UjZB!(vh2xuK3A}bYT zGn_cVI5)}}nulecp{Q2ZNbL>--#AuX2jTp;Qcb?&y zD6bWFihen$kT?-gaSXJS*l#{ZKoLJ3G)9mTMs$pIp46SK#gQT&>|FnRrCN?=g6M%? z!E4cs@9-;8cm49{+g+ACYW%$`Ik*=OmC{OQ>+a$HLTx^%&HKLV8hmf4P)Sqz;`;~s z`V7U25Uv1DV0Ll53?hjWAY6V1k_=M6XLCoG>$avnqAh- zmOY)t{LcWj|8b3bg3ic!>%(?VbB|^E%2H<8eKx$U**a&Nmu6S2PvzuX%dQ{U=8C9h zR3#KJCA|1Ry~ZCQut8=}ZrLmn_CyQpaOJ{fYo$5eGarzFtJ3B#a@6WKYZv z*WOpH$NVV5?Y)d)Y-7d}24d@9Jfa-SUO@|}(U2PmxfOc|!&pJf3ZgP)wkbV+=@o>4BQt#*Z%@TTTJx; delta 193 zcmZp8z})bFd4e>f;Y1l{M#GH>OZ?dw`DZfl&)h6{pr3#8%=qlhEC+7zPrj3{D$2mX zz|Nb+z`uasf$tGt3!fVA3Er&Df&wADEHUitp_B7!qgZ0t*h4p;sWoK+tLFR0z+cEO z&+ozajei#ZBmTpi1r;9fv1l-dGEV;0rwU_$N=8Ujnsv ay#^D508sEa|HK8xY+OJ$Ah?SzC;$NEb3R4@ diff --git a/server/src/routes/plans.ts b/server/src/routes/plans.ts index 546874a..04f2e06 100644 --- a/server/src/routes/plans.ts +++ b/server/src/routes/plans.ts @@ -28,8 +28,15 @@ router.get('/', async (req: any, res) => { const plans = await prisma.workoutPlan.findMany({ where: { userId } }); - res.json(plans); + + const mappedPlans = plans.map((p: any) => ({ + ...p, + steps: p.exercises ? JSON.parse(p.exercises) : [] + })); + + res.json(mappedPlans); } catch (error) { + console.error('Error fetching plans:', error); res.status(500).json({ error: 'Server error' }); } }); @@ -38,16 +45,18 @@ router.get('/', async (req: any, res) => { router.post('/', async (req: any, res) => { try { const userId = req.user.userId; - const { id, name, description, exercises } = req.body; + const { id, name, description, steps } = req.body; + + const exercisesJson = JSON.stringify(steps || []); const existing = await prisma.workoutPlan.findUnique({ where: { id } }); if (existing) { const updated = await prisma.workoutPlan.update({ where: { id }, - data: { name, description, exercises } + data: { name, description, exercises: exercisesJson } }); - return res.json(updated); + res.json({ ...updated, steps: steps || [] }); } else { const created = await prisma.workoutPlan.create({ data: { @@ -55,12 +64,13 @@ router.post('/', async (req: any, res) => { userId, name, description, - exercises + exercises: exercisesJson } }); - return res.json(created); + res.json({ ...created, steps: steps || [] }); } } catch (error) { + console.error('Error saving plan:', error); res.status(500).json({ error: 'Server error' }); } });