Exercise management enhanced
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,7 +11,6 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
*.db
|
|
||||||
server/prisma/dev.db
|
server/prisma/dev.db
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
|||||||
112
components/ExerciseModal.tsx
Normal file
112
components/ExerciseModal.tsx
Normal file
@@ -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> | void;
|
||||||
|
lang: Language;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave, lang }) => {
|
||||||
|
const [newName, setNewName] = useState('');
|
||||||
|
const [newType, setNewType] = useState<ExerciseType>(ExerciseType.STRENGTH);
|
||||||
|
const [newBwPercentage, setNewBwPercentage] = useState<string>('100');
|
||||||
|
|
||||||
|
const exerciseTypeLabels: Record<ExerciseType, string> = {
|
||||||
|
[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 (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-[60] flex items-end sm:items-center justify-center p-4 backdrop-blur-sm">
|
||||||
|
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3 animate-in slide-in-from-bottom-10 duration-200">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h3 className="text-2xl font-normal text-on-surface">{t('create_exercise', lang)}</h3>
|
||||||
|
<button onClick={onClose} className="p-2 bg-surface-container-high rounded-full hover:bg-outline-variant/20"><X size={20} /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<FilledInput
|
||||||
|
label={t('ex_name', lang)}
|
||||||
|
value={newName}
|
||||||
|
onChange={(e: any) => setNewName(e.target.value)}
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-on-surface-variant font-medium mb-3">{t('ex_type', lang)}</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{ 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) => (
|
||||||
|
<button
|
||||||
|
key={type.id}
|
||||||
|
onClick={() => setNewType(type.id)}
|
||||||
|
className={`px-4 py-2 rounded-lg flex items-center gap-2 text-xs font-medium border transition-all ${newType === type.id
|
||||||
|
? 'bg-secondary-container text-on-secondary-container border-transparent'
|
||||||
|
: 'bg-transparent text-on-surface-variant border-outline hover:border-on-surface-variant'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<type.icon size={14} /> {type.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{newType === ExerciseType.BODYWEIGHT && (
|
||||||
|
<FilledInput
|
||||||
|
label={t('body_weight_percent', lang)}
|
||||||
|
value={newBwPercentage}
|
||||||
|
onChange={(e: any) => setNewBwPercentage(e.target.value)}
|
||||||
|
icon={<Percent size={12} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
<button
|
||||||
|
onClick={handleCreateExercise}
|
||||||
|
className="px-8 py-3 bg-primary text-on-primary rounded-full font-medium shadow-elevation-1"
|
||||||
|
>
|
||||||
|
{t('create_btn', lang)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExerciseModal;
|
||||||
32
components/FilledInput.tsx
Normal file
32
components/FilledInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface FilledInputProps {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
type?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
step?: string;
|
||||||
|
inputMode?: "search" | "text" | "email" | "tel" | "url" | "none" | "numeric" | "decimal";
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilledInput: React.FC<FilledInputProps> = ({ label, value, onChange, type = "number", icon, autoFocus, step, inputMode }) => (
|
||||||
|
<div className="relative group bg-surface-container-high rounded-t-lg border-b border-outline-variant hover:bg-white/5 focus-within:border-primary transition-colors">
|
||||||
|
<label className="absolute top-2 left-4 text-[10px] font-medium text-on-surface-variant flex items-center gap-1">
|
||||||
|
{icon} {label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
step={step}
|
||||||
|
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
className="w-full pt-6 pb-2 px-4 bg-transparent text-2xl text-on-surface focus:outline-none placeholder-transparent"
|
||||||
|
placeholder="0"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FilledInput;
|
||||||
@@ -3,7 +3,8 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { User, Language, ExerciseDef, ExerciseType } from '../types';
|
import { User, Language, ExerciseDef, ExerciseType } from '../types';
|
||||||
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth';
|
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth';
|
||||||
import { getExercises, saveExercise } from '../services/storage';
|
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 { t } from '../services/i18n';
|
||||||
import Snackbar from './Snackbar';
|
import Snackbar from './Snackbar';
|
||||||
|
|
||||||
@@ -45,10 +46,16 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
const [editingExercise, setEditingExercise] = useState<ExerciseDef | null>(null);
|
const [editingExercise, setEditingExercise] = useState<ExerciseDef | null>(null);
|
||||||
const [isCreatingEx, setIsCreatingEx] = useState(false);
|
const [isCreatingEx, setIsCreatingEx] = useState(false);
|
||||||
// New exercise form
|
|
||||||
const [newExName, setNewExName] = useState('');
|
const exerciseTypeLabels: Record<ExerciseType, string> = {
|
||||||
const [newExType, setNewExType] = useState<ExerciseType>(ExerciseType.STRENGTH);
|
[ExerciseType.STRENGTH]: t('type_strength', lang),
|
||||||
const [newExBw, setNewExBw] = useState('100');
|
[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(() => {
|
useEffect(() => {
|
||||||
@@ -178,19 +185,10 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateExercise = () => {
|
const handleCreateExercise = async (newEx: ExerciseDef) => {
|
||||||
if (newExName.trim()) {
|
await saveExercise(user.id, newEx);
|
||||||
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);
|
setIsCreatingEx(false);
|
||||||
refreshExercises();
|
await refreshExercises();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -302,7 +300,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
<div key={ex.id} className={`p-3 rounded-lg flex justify-between items-center border border-outline-variant/20 ${ex.isArchived ? 'bg-surface-container-low opacity-60' : 'bg-surface-container-high'}`}>
|
<div key={ex.id} className={`p-3 rounded-lg flex justify-between items-center border border-outline-variant/20 ${ex.isArchived ? 'bg-surface-container-low opacity-60' : 'bg-surface-container-high'}`}>
|
||||||
<div className="overflow-hidden mr-2">
|
<div className="overflow-hidden mr-2">
|
||||||
<div className="font-medium text-sm text-on-surface truncate">{ex.name}</div>
|
<div className="font-medium text-sm text-on-surface truncate">{ex.name}</div>
|
||||||
<div className="text-xs text-on-surface-variant">{ex.type}</div>
|
<div className="text-xs text-on-surface-variant">{exerciseTypeLabels[ex.type]}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<button onClick={() => setEditingExercise(ex)} className="p-2 text-on-surface-variant hover:text-primary hover:bg-white/5 rounded-full">
|
<button onClick={() => setEditingExercise(ex)} className="p-2 text-on-surface-variant hover:text-primary hover:bg-white/5 rounded-full">
|
||||||
@@ -498,62 +496,12 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
|
|
||||||
{/* Create Exercise Modal */}
|
{/* Create Exercise Modal */}
|
||||||
{isCreatingEx && (
|
{isCreatingEx && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
<ExerciseModal
|
||||||
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3">
|
isOpen={isCreatingEx}
|
||||||
<h3 className="text-xl font-normal text-on-surface mb-4">{t('create_exercise', lang)}</h3>
|
onClose={() => setIsCreatingEx(false)}
|
||||||
<div className="space-y-4">
|
onSave={handleCreateExercise}
|
||||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
lang={lang}
|
||||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label>
|
|
||||||
<input
|
|
||||||
value={newExName}
|
|
||||||
onChange={(e) => setNewExName(e.target.value)}
|
|
||||||
className="w-full bg-transparent text-on-surface focus:outline-none"
|
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="text-[10px] text-on-surface-variant font-medium mb-2 block">{t('ex_type', lang)}</label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setNewExType(ExerciseType.STRENGTH)}
|
|
||||||
className={`px-3 py-1 rounded-full text-xs border ${newExType === ExerciseType.STRENGTH ? 'bg-primary text-on-primary border-transparent' : 'border-outline text-on-surface-variant'}`}
|
|
||||||
>
|
|
||||||
{t('type_strength', lang)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setNewExType(ExerciseType.BODYWEIGHT)}
|
|
||||||
className={`px-3 py-1 rounded-full text-xs border ${newExType === ExerciseType.BODYWEIGHT ? 'bg-primary text-on-primary border-transparent' : 'border-outline text-on-surface-variant'}`}
|
|
||||||
>
|
|
||||||
{t('type_bodyweight', lang)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setNewExType(ExerciseType.CARDIO)}
|
|
||||||
className={`px-3 py-1 rounded-full text-xs border ${newExType === ExerciseType.CARDIO ? 'bg-primary text-on-primary border-transparent' : 'border-outline text-on-surface-variant'}`}
|
|
||||||
>
|
|
||||||
{t('type_cardio', lang)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{newExType === ExerciseType.BODYWEIGHT && (
|
|
||||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
|
||||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('body_weight_percent', lang)}</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={newExBw}
|
|
||||||
onChange={(e) => setNewExBw(e.target.value)}
|
|
||||||
className="w-full bg-transparent text-on-surface focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
|
||||||
<button onClick={() => setIsCreatingEx(false)} className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button>
|
|
||||||
<button onClick={handleCreateExercise} className="px-4 py-2 rounded-full bg-primary text-on-primary font-medium">{t('create_btn', lang)}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,23 +18,8 @@ interface TrackerProps {
|
|||||||
lang: Language;
|
lang: Language;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilledInput = ({ label, value, onChange, type = "number", icon, autoFocus, step }: any) => (
|
import FilledInput from './FilledInput';
|
||||||
<div className="relative group bg-surface-container-high rounded-t-lg border-b border-outline-variant hover:bg-white/5 focus-within:border-primary transition-colors">
|
import ExerciseModal from './ExerciseModal';
|
||||||
<label className="absolute top-2 left-4 text-[10px] font-medium text-on-surface-variant flex items-center gap-1">
|
|
||||||
{icon} {label}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
step={step}
|
|
||||||
inputMode="decimal"
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
className="w-full pt-6 pb-2 px-4 bg-transparent text-2xl text-on-surface focus:outline-none placeholder-transparent"
|
|
||||||
placeholder="0"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSetAdded, onRemoveSet, lang }) => {
|
const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSetAdded, onRemoveSet, lang }) => {
|
||||||
const [exercises, setExercises] = useState<ExerciseDef[]>([]);
|
const [exercises, setExercises] = useState<ExerciseDef[]>([]);
|
||||||
@@ -58,9 +43,6 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
|
|||||||
|
|
||||||
// Create Exercise State
|
// Create Exercise State
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [newName, setNewName] = useState('');
|
|
||||||
const [newType, setNewType] = useState<ExerciseType>(ExerciseType.STRENGTH);
|
|
||||||
const [newBwPercentage, setNewBwPercentage] = useState<string>('100');
|
|
||||||
|
|
||||||
// Plan Execution State
|
// Plan Execution State
|
||||||
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
const [currentStepIndex, setCurrentStepIndex] = useState(0);
|
||||||
@@ -179,21 +161,11 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateExercise = async () => {
|
const handleCreateExercise = async (newEx: ExerciseDef) => {
|
||||||
if (!newName.trim()) return;
|
|
||||||
const newEx: ExerciseDef = {
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
name: newName.trim(),
|
|
||||||
type: newType,
|
|
||||||
...(newType === ExerciseType.BODYWEIGHT && { bodyWeightPercentage: parseFloat(newBwPercentage) || 100 })
|
|
||||||
};
|
|
||||||
await saveExercise(userId, newEx);
|
await saveExercise(userId, newEx);
|
||||||
const exList = await getExercises(userId);
|
const exList = await getExercises(userId);
|
||||||
setExercises(exList.filter(e => !e.isArchived));
|
setExercises(exList.filter(e => !e.isArchived));
|
||||||
setSelectedExercise(newEx);
|
setSelectedExercise(newEx);
|
||||||
setNewName('');
|
|
||||||
setNewType(ExerciseType.STRENGTH);
|
|
||||||
setNewBwPercentage('100');
|
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -518,68 +490,12 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCreating && (
|
{isCreating && (
|
||||||
<div className="fixed inset-0 bg-black/60 z-[60] flex items-end sm:items-center justify-center p-4 backdrop-blur-sm">
|
<ExerciseModal
|
||||||
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3 animate-in slide-in-from-bottom-10 duration-200">
|
isOpen={isCreating}
|
||||||
<div className="flex justify-between items-center mb-6">
|
onClose={() => setIsCreating(false)}
|
||||||
<h3 className="text-2xl font-normal text-on-surface">{t('create_exercise', lang)}</h3>
|
onSave={handleCreateExercise}
|
||||||
<button onClick={() => setIsCreating(false)} className="p-2 bg-surface-container-high rounded-full hover:bg-outline-variant/20"><X size={20} /></button>
|
lang={lang}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<FilledInput
|
|
||||||
label={t('ex_name', lang)}
|
|
||||||
value={newName}
|
|
||||||
onChange={(e: any) => setNewName(e.target.value)}
|
|
||||||
type="text"
|
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-on-surface-variant font-medium mb-3">{t('ex_type', lang)}</label>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{[
|
|
||||||
{ 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) => (
|
|
||||||
<button
|
|
||||||
key={type.id}
|
|
||||||
onClick={() => setNewType(type.id)}
|
|
||||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 text-xs font-medium border transition-all ${newType === type.id
|
|
||||||
? 'bg-secondary-container text-on-secondary-container border-transparent'
|
|
||||||
: 'bg-transparent text-on-surface-variant border-outline hover:border-on-surface-variant'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<type.icon size={14} /> {type.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{newType === ExerciseType.BODYWEIGHT && (
|
|
||||||
<FilledInput
|
|
||||||
label={t('body_weight_percent', lang)}
|
|
||||||
value={newBwPercentage}
|
|
||||||
onChange={(e: any) => setNewBwPercentage(e.target.value)}
|
|
||||||
icon={<Percent size={12} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end mt-4">
|
|
||||||
<button
|
|
||||||
onClick={handleCreateExercise}
|
|
||||||
className="px-8 py-3 bg-primary text-on-primary rounded-full font-medium shadow-elevation-1"
|
|
||||||
>
|
|
||||||
{t('create_btn', lang)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
34
create_admin.js
Normal file
34
create_admin.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
Binary file not shown.
@@ -72,6 +72,7 @@ router.post('/', async (req: any, res) => {
|
|||||||
// Create new
|
// Create new
|
||||||
const newExercise = await prisma.exercise.create({
|
const newExercise = await prisma.exercise.create({
|
||||||
data: {
|
data: {
|
||||||
|
id: id || undefined, // Use provided ID if available
|
||||||
userId,
|
userId,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
|
|||||||
Reference in New Issue
Block a user