diff --git a/.gitignore b/.gitignore index 2fb4d1f..cb5b281 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ node_modules dist dist-ssr *.local -*.db server/prisma/dev.db # Editor directories and files diff --git a/components/ExerciseModal.tsx b/components/ExerciseModal.tsx new file mode 100644 index 0000000..6fd7a62 --- /dev/null +++ b/components/ExerciseModal.tsx @@ -0,0 +1,112 @@ +import React, { useState } from 'react'; +import { X, Dumbbell, User, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, Percent } from 'lucide-react'; +import { ExerciseDef, ExerciseType, Language } from '../types'; +import { t } from '../services/i18n'; +import FilledInput from './FilledInput'; + +interface ExerciseModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (exercise: ExerciseDef) => Promise | void; + lang: Language; +} + +const ExerciseModal: React.FC = ({ isOpen, onClose, onSave, lang }) => { + const [newName, setNewName] = useState(''); + const [newType, setNewType] = useState(ExerciseType.STRENGTH); + const [newBwPercentage, setNewBwPercentage] = useState('100'); + + 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 handleCreateExercise = async () => { + if (!newName.trim()) return; + const newEx: ExerciseDef = { + id: crypto.randomUUID(), + name: newName.trim(), + type: newType, + ...(newType === ExerciseType.BODYWEIGHT && { bodyWeightPercentage: parseFloat(newBwPercentage) || 100 }) + }; + await onSave(newEx); + setNewName(''); + setNewType(ExerciseType.STRENGTH); + setNewBwPercentage('100'); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

{t('create_exercise', lang)}

+ +
+ +
+ setNewName(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) => ( + + ))} +
+
+ + {newType === ExerciseType.BODYWEIGHT && ( + setNewBwPercentage(e.target.value)} + icon={} + /> + )} + +
+ +
+
+
+
+ ); +}; + +export default ExerciseModal; diff --git a/components/FilledInput.tsx b/components/FilledInput.tsx new file mode 100644 index 0000000..7445a26 --- /dev/null +++ b/components/FilledInput.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface FilledInputProps { + label: string; + value: string | number; + onChange: (e: React.ChangeEvent) => void; + type?: string; + icon?: React.ReactNode; + autoFocus?: boolean; + step?: string; + inputMode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal"; +} + +const FilledInput: React.FC = ({ label, value, onChange, type = "number", icon, autoFocus, step, inputMode }) => ( +
+ + +
+); + +export default FilledInput; diff --git a/components/Profile.tsx b/components/Profile.tsx index a034e56..7a789de 100644 --- a/components/Profile.tsx +++ b/components/Profile.tsx @@ -3,7 +3,8 @@ import React, { useState, useEffect } from 'react'; import { User, Language, ExerciseDef, ExerciseType } from '../types'; import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth'; import { getExercises, saveExercise } from '../services/storage'; -import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, X, Plus, Percent } from 'lucide-react'; +import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, Plus } from 'lucide-react'; +import ExerciseModal from './ExerciseModal'; import { t } from '../services/i18n'; import Snackbar from './Snackbar'; @@ -45,10 +46,16 @@ const Profile: React.FC = ({ user, onLogout, lang, onLanguageChang const [showArchived, setShowArchived] = useState(false); const [editingExercise, setEditingExercise] = useState(null); const [isCreatingEx, setIsCreatingEx] = useState(false); - // New exercise form - const [newExName, setNewExName] = useState(''); - const [newExType, setNewExType] = useState(ExerciseType.STRENGTH); - const [newExBw, setNewExBw] = useState('100'); + + 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), + }; useEffect(() => { @@ -178,19 +185,10 @@ const Profile: React.FC = ({ user, onLogout, lang, onLanguageChang } }; - const handleCreateExercise = () => { - if (newExName.trim()) { - const newEx: ExerciseDef = { - id: crypto.randomUUID(), - name: newExName.trim(), - type: newExType, - bodyWeightPercentage: newExType === ExerciseType.BODYWEIGHT ? parseFloat(newExBw) : undefined - }; - saveExercise(user.id, newEx); - setNewExName(''); - setIsCreatingEx(false); - refreshExercises(); - } + const handleCreateExercise = async (newEx: ExerciseDef) => { + await saveExercise(user.id, newEx); + setIsCreatingEx(false); + await refreshExercises(); }; return ( @@ -302,7 +300,7 @@ const Profile: React.FC = ({ user, onLogout, lang, onLanguageChang
{ex.name}
-
{ex.type}
+
{exerciseTypeLabels[ex.type]}
- - -
-
- - {newExType === ExerciseType.BODYWEIGHT && ( -
- - setNewExBw(e.target.value)} - className="w-full bg-transparent text-on-surface focus:outline-none" - /> -
- )} -
- - -
- - - + setIsCreatingEx(false)} + onSave={handleCreateExercise} + lang={lang} + /> )} diff --git a/components/Tracker.tsx b/components/Tracker.tsx index d151a29..f44c3cb 100644 --- a/components/Tracker.tsx +++ b/components/Tracker.tsx @@ -18,23 +18,8 @@ interface TrackerProps { lang: Language; } -const FilledInput = ({ label, value, onChange, type = "number", icon, autoFocus, step }: any) => ( -
- - -
-); +import FilledInput from './FilledInput'; +import ExerciseModal from './ExerciseModal'; const Tracker: React.FC = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSetAdded, onRemoveSet, lang }) => { const [exercises, setExercises] = useState([]); @@ -58,9 +43,6 @@ const Tracker: React.FC = ({ userId, userWeight, activeSession, ac // Create Exercise State const [isCreating, setIsCreating] = useState(false); - const [newName, setNewName] = useState(''); - const [newType, setNewType] = useState(ExerciseType.STRENGTH); - const [newBwPercentage, setNewBwPercentage] = useState('100'); // Plan Execution State const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -179,21 +161,11 @@ const Tracker: React.FC = ({ userId, userWeight, activeSession, ac } }; - const handleCreateExercise = async () => { - if (!newName.trim()) return; - const newEx: ExerciseDef = { - id: crypto.randomUUID(), - name: newName.trim(), - type: newType, - ...(newType === ExerciseType.BODYWEIGHT && { bodyWeightPercentage: parseFloat(newBwPercentage) || 100 }) - }; + const handleCreateExercise = async (newEx: ExerciseDef) => { await saveExercise(userId, newEx); const exList = await getExercises(userId); setExercises(exList.filter(e => !e.isArchived)); setSelectedExercise(newEx); - setNewName(''); - setNewType(ExerciseType.STRENGTH); - setNewBwPercentage('100'); setIsCreating(false); }; @@ -518,68 +490,12 @@ const Tracker: React.FC = ({ userId, userWeight, activeSession, ac {isCreating && ( -
-
-
-

{t('create_exercise', lang)}

- -
- -
- setNewName(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) => ( - - ))} -
-
- - {newType === ExerciseType.BODYWEIGHT && ( - setNewBwPercentage(e.target.value)} - icon={} - /> - )} - -
- -
-
-
-
+ setIsCreating(false)} + onSave={handleCreateExercise} + lang={lang} + /> )} ); diff --git a/create_admin.js b/create_admin.js new file mode 100644 index 0000000..9cc4b13 --- /dev/null +++ b/create_admin.js @@ -0,0 +1,34 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcryptjs'); + +(async () => { + const prisma = new PrismaClient(); + try { + const email = 'admin@gymflow.com'; + const password = 'admin123'; + const hashedPassword = await bcrypt.hash(password, 10); + + const user = await prisma.user.upsert({ + where: { email }, + update: {}, + create: { + email, + password: hashedPassword, + role: 'ADMIN', + isFirstLogin: false, + profile: { + create: { + weight: 80, + height: 180, + gender: 'MALE' + } + } + } + }); + console.log('Admin user created/verified:', user); + } catch (e) { + console.error('Error:', e); + } finally { + await prisma.$disconnect(); + } +})(); diff --git a/server/prisma/dev.db b/server/prisma/dev.db index 12485f6..5ee55ae 100644 Binary files a/server/prisma/dev.db and b/server/prisma/dev.db differ diff --git a/server/src/routes/exercises.ts b/server/src/routes/exercises.ts index ba28df9..92ef11d 100644 --- a/server/src/routes/exercises.ts +++ b/server/src/routes/exercises.ts @@ -72,6 +72,7 @@ router.post('/', async (req: any, res) => { // Create new const newExercise = await prisma.exercise.create({ data: { + id: id || undefined, // Use provided ID if available userId, name, type,