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 80bcfd0..eb06540 100644
Binary files a/server/prisma/dev.db and b/server/prisma/dev.db differ
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' });
}
});