1. Change Password fixed. 2. Personal Data implemented. 3. New alerts style. 4. Better dropdowns.
This commit is contained in:
16
App.tsx
16
App.tsx
@@ -10,7 +10,7 @@ import Login from './components/Login';
|
||||
import Profile from './components/Profile';
|
||||
import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from './types';
|
||||
import { getSessions, saveSession, deleteSession } from './services/storage';
|
||||
import { getCurrentUserProfile } from './services/auth';
|
||||
import { getCurrentUserProfile, getMe } from './services/auth';
|
||||
import { getSystemLanguage } from './services/i18n';
|
||||
|
||||
function App() {
|
||||
@@ -25,6 +25,20 @@ function App() {
|
||||
useEffect(() => {
|
||||
// Set initial language
|
||||
setLanguage(getSystemLanguage());
|
||||
|
||||
// Restore session
|
||||
const restoreSession = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const res = await getMe();
|
||||
if (res.success && res.user) {
|
||||
setCurrentUser(res.user);
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
};
|
||||
restoreSession();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -36,11 +36,15 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = () => {
|
||||
const handleChangePassword = async () => {
|
||||
if (tempUser && newPassword.length >= 4) {
|
||||
changePassword(tempUser.id, newPassword);
|
||||
const res = await changePassword(tempUser.id, newPassword);
|
||||
if (res.success) {
|
||||
const updatedUser = { ...tempUser, isFirstLogin: false };
|
||||
onLogin(updatedUser);
|
||||
} else {
|
||||
setError(res.error || t('change_pass_error', language));
|
||||
}
|
||||
} else {
|
||||
setError(t('login_password_short', language));
|
||||
}
|
||||
|
||||
@@ -24,9 +24,19 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||
const [showExerciseSelector, setShowExerciseSelector] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPlans(getPlans(userId));
|
||||
const loadData = async () => {
|
||||
const fetchedPlans = await getPlans(userId);
|
||||
setPlans(fetchedPlans);
|
||||
|
||||
const fetchedExercises = await getExercises(userId);
|
||||
// Filter out archived exercises
|
||||
setAvailableExercises(getExercises(userId).filter(e => !e.isArchived));
|
||||
if (Array.isArray(fetchedExercises)) {
|
||||
setAvailableExercises(fetchedExercises.filter(e => !e.isArchived));
|
||||
} else {
|
||||
setAvailableExercises([]);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, [userId]);
|
||||
|
||||
const handleCreateNew = () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, g
|
||||
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 { t } from '../services/i18n';
|
||||
import Snackbar from './Snackbar';
|
||||
|
||||
interface ProfileProps {
|
||||
user: User;
|
||||
@@ -28,7 +29,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
// Admin: User List
|
||||
const [showUserList, setShowUserList] = useState(false);
|
||||
const [allUsers, setAllUsers] = useState<any[]>([]);
|
||||
const [adminPassResetInput, setAdminPassResetInput] = useState<{[key:string]: string}>({});
|
||||
const [adminPassResetInput, setAdminPassResetInput] = useState<{ [key: string]: string }>({});
|
||||
|
||||
// Change Password
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
@@ -72,29 +73,49 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
setExercises(getExercises(user.id));
|
||||
};
|
||||
|
||||
const handleSaveProfile = () => {
|
||||
updateUserProfile(user.id, {
|
||||
// Snackbar State
|
||||
const [snackbar, setSnackbar] = useState<{ isOpen: boolean; message: string; type: 'success' | 'error' | 'info' }>({
|
||||
isOpen: false,
|
||||
message: '',
|
||||
type: 'info'
|
||||
});
|
||||
|
||||
const showSnackbar = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
setSnackbar({ isOpen: true, message, type });
|
||||
};
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
const res = await updateUserProfile(user.id, {
|
||||
weight: parseFloat(weight) || undefined,
|
||||
height: parseFloat(height) || undefined,
|
||||
gender: gender as any,
|
||||
birthDate: birthDate ? new Date(birthDate).getTime() : undefined,
|
||||
language: lang
|
||||
});
|
||||
alert('Saved');
|
||||
|
||||
if (res.success) {
|
||||
showSnackbar(t('profile_saved', lang) || 'Profile saved successfully', 'success');
|
||||
} else {
|
||||
showSnackbar(res.error || 'Failed to save profile', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = () => {
|
||||
const handleChangePassword = async () => {
|
||||
if (newPassword.length < 4) {
|
||||
setPassMsg('Password too short');
|
||||
return;
|
||||
}
|
||||
changePassword(user.id, newPassword);
|
||||
const res = await changePassword(user.id, newPassword);
|
||||
if (res.success) {
|
||||
setPassMsg('Password changed');
|
||||
setNewPassword('');
|
||||
} else {
|
||||
setPassMsg(res.error || 'Error changing password');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateUser = () => {
|
||||
const res = createUser(newUserEmail, newUserPass);
|
||||
const handleCreateUser = async () => {
|
||||
const res = await createUser(newUserEmail, newUserPass);
|
||||
if (res.success) {
|
||||
setCreateMsg(`${t('user_created', lang)}: ${newUserEmail}`);
|
||||
setNewUserEmail('');
|
||||
@@ -122,7 +143,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
if (pass && pass.length >= 4) {
|
||||
adminResetPassword(uid, pass);
|
||||
alert(t('pass_reset', lang));
|
||||
setAdminPassResetInput({...adminPassResetInput, [uid]: ''});
|
||||
setAdminPassResetInput({ ...adminPassResetInput, [uid]: '' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,7 +214,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
<h3 className="text-sm font-bold text-primary mb-4">{t('personal_data', lang)}</h3>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<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 flex gap-1 items-center"><Scale size={10}/> {t('weight_kg', lang)}</label>
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Scale size={10} /> {t('weight_kg', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
@@ -203,15 +224,15 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
/>
|
||||
</div>
|
||||
<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 flex gap-1 items-center"><Ruler size={10}/> {t('height', lang)}</label>
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Ruler size={10} /> {t('height', lang)}</label>
|
||||
<input type="number" value={height} onChange={(e) => setHeight(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none" />
|
||||
</div>
|
||||
<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 flex gap-1 items-center"><Calendar size={10}/> {t('birth_date', lang)}</label>
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Calendar size={10} /> {t('birth_date', lang)}</label>
|
||||
<input type="date" value={birthDate} onChange={(e) => setBirthDate(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none text-sm py-1" />
|
||||
</div>
|
||||
<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 flex gap-1 items-center"><PersonStanding size={10}/> {t('gender', lang)}</label>
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><PersonStanding size={10} /> {t('gender', lang)}</label>
|
||||
<select value={gender} onChange={(e) => setGender(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none text-sm py-1 bg-surface-container-high">
|
||||
<option value="MALE">{t('male', lang)}</option>
|
||||
<option value="FEMALE">{t('female', lang)}</option>
|
||||
@@ -221,7 +242,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2 mb-4">
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Globe size={10}/> {t('language', lang)}</label>
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Globe size={10} /> {t('language', lang)}</label>
|
||||
<select value={lang} onChange={(e) => onLanguageChange(e.target.value as Language)} className="w-full bg-transparent text-on-surface focus:outline-none text-sm py-1 bg-surface-container-high">
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Русский</option>
|
||||
@@ -249,7 +270,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
onClick={() => setIsCreatingEx(true)}
|
||||
className="w-full py-2 border border-outline border-dashed rounded-lg text-sm text-on-surface-variant hover:bg-surface-container-high flex items-center justify-center gap-2"
|
||||
>
|
||||
<Plus size={16}/> {t('create_exercise', lang)}
|
||||
<Plus size={16} /> {t('create_exercise', lang)}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@@ -377,7 +398,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
<div className="font-medium text-sm text-on-surface truncate">{u.email}</div>
|
||||
<div className="text-xs text-on-surface-variant flex gap-2">
|
||||
<span>{u.role}</span>
|
||||
{u.isBlocked && <span className="text-error font-bold flex items-center gap-1"><Ban size={10}/> {t('block', lang)}</span>}
|
||||
{u.isBlocked && <span className="text-error font-bold flex items-center gap-1"><Ban size={10} /> {t('block', lang)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
@@ -405,13 +426,13 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
{u.role !== 'ADMIN' && (
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex-1 flex items-center bg-surface-container rounded px-2 border border-outline-variant/20">
|
||||
<KeyRound size={12} className="text-on-surface-variant mr-2"/>
|
||||
<KeyRound size={12} className="text-on-surface-variant mr-2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('change_pass_new', lang)}
|
||||
className="bg-transparent text-xs py-2 w-full focus:outline-none text-on-surface"
|
||||
value={adminPassResetInput[u.id] || ''}
|
||||
onChange={(e) => setAdminPassResetInput({...adminPassResetInput, [u.id]: e.target.value})}
|
||||
onChange={(e) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -440,7 +461,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label>
|
||||
<input
|
||||
value={editingExercise.name}
|
||||
onChange={(e) => setEditingExercise({...editingExercise, name: e.target.value})}
|
||||
onChange={(e) => setEditingExercise({ ...editingExercise, name: e.target.value })}
|
||||
className="w-full bg-transparent text-on-surface focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
@@ -450,7 +471,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
<input
|
||||
type="number"
|
||||
value={editingExercise.bodyWeightPercentage || 100}
|
||||
onChange={(e) => setEditingExercise({...editingExercise, bodyWeightPercentage: parseFloat(e.target.value)})}
|
||||
onChange={(e) => setEditingExercise({ ...editingExercise, bodyWeightPercentage: parseFloat(e.target.value) })}
|
||||
className="w-full bg-transparent text-on-surface focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
@@ -525,6 +546,12 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
)}
|
||||
|
||||
</div>
|
||||
<Snackbar
|
||||
isOpen={snackbar.isOpen}
|
||||
message={snackbar.message}
|
||||
type={snackbar.type}
|
||||
onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
49
components/Snackbar.tsx
Normal file
49
components/Snackbar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { X, CheckCircle, AlertCircle, Info } from 'lucide-react';
|
||||
|
||||
export type SnackbarType = 'success' | 'error' | 'info';
|
||||
|
||||
interface SnackbarProps {
|
||||
message: string;
|
||||
type?: SnackbarType;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
const Snackbar: React.FC<SnackbarProps> = ({ message, type = 'info', isOpen, onClose, duration = 3000 }) => {
|
||||
useEffect(() => {
|
||||
if (isOpen && duration > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
onClose();
|
||||
}, duration);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen, duration, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const bgColors = {
|
||||
success: 'bg-primary-container text-on-primary-container',
|
||||
error: 'bg-error-container text-on-error-container',
|
||||
info: 'bg-secondary-container text-on-secondary-container'
|
||||
};
|
||||
|
||||
const icons = {
|
||||
success: <CheckCircle size={20} />,
|
||||
error: <AlertCircle size={20} />,
|
||||
info: <Info size={20} />
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 flex items-center gap-3 px-4 py-3 rounded-lg shadow-elevation-3 ${bgColors[type]} min-w-[300px] animate-in fade-in slide-in-from-bottom-4 duration-300`}>
|
||||
<div className="shrink-0">{icons[type]}</div>
|
||||
<p className="flex-1 text-sm font-medium">{message}</p>
|
||||
<button onClick={onClose} className="p-1 hover:bg-black/10 rounded-full transition-colors">
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Snackbar;
|
||||
36
index.css
Normal file
36
index.css
Normal file
@@ -0,0 +1,36 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #49454F;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Hide number input arrows */
|
||||
input[type=number]::-webkit-inner-spin-button,
|
||||
input[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Dropdown Styles for Dark Mode */
|
||||
select option {
|
||||
background-color: #2B2930; /* surface-container-high */
|
||||
color: #E6E0E9; /* on-surface */
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Ensure select itself has correct background when closed */
|
||||
select {
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
|
||||
379
package-lock.json
generated
379
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
@@ -255,6 +256,16 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
@@ -1496,6 +1507,146 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -1523,6 +1674,34 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
|
||||
"integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.8.1",
|
||||
"shell-quote": "^1.8.1",
|
||||
"spawn-command": "0.0.2",
|
||||
"supports-color": "^8.1.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.13.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -1674,6 +1853,23 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -1922,6 +2118,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
@@ -1982,6 +2188,16 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
@@ -2107,6 +2323,13 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -2412,6 +2635,16 @@
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
@@ -2475,6 +2708,16 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -2532,6 +2775,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -2554,6 +2810,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spawn-command": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
|
||||
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -2650,6 +2912,22 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
@@ -2673,6 +2951,23 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
@@ -2967,12 +3262,96 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -6,18 +6,20 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"dev:full": "concurrently \"npm run dev\" \"npm run dev --prefix server\""
|
||||
},
|
||||
"dependencies": {
|
||||
"recharts": "^3.4.1",
|
||||
"react-dom": "^19.2.0",
|
||||
"@google/genai": "^1.30.0",
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0"
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -31,6 +31,10 @@ model UserProfile {
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
weight Float?
|
||||
height Float?
|
||||
gender String?
|
||||
birthDate DateTime?
|
||||
language String? @default("en")
|
||||
}
|
||||
|
||||
model Exercise {
|
||||
|
||||
@@ -7,19 +7,34 @@ const router = express.Router();
|
||||
const prisma = new PrismaClient();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||
|
||||
// Get Current User
|
||||
router.get('/me', async (req, res) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
include: { profile: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const { password: _, ...userSafe } = user;
|
||||
res.json({ success: true, user: userSafe });
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Admin check (hardcoded for now as per original logic, or we can seed it)
|
||||
// For now, let's stick to DB users, but maybe seed admin if needed.
|
||||
// The original code had hardcoded admin. Let's support that via a special check or just rely on DB.
|
||||
// Let's rely on DB for consistency, but if the user wants the specific admin account, they should register it.
|
||||
// However, to match original behavior, I'll add a check or just let them register 'admin@gymflow.ai'.
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { profile: true }
|
||||
@@ -47,4 +62,73 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Change Password
|
||||
router.post('/change-password', async (req, res) => {
|
||||
console.log('DEBUG: change-password route hit');
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const { userId, newPassword } = req.body;
|
||||
|
||||
// Verify token
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
if (decoded.userId !== userId) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
if (!newPassword || newPassword.length < 4) {
|
||||
return res.status(400).json({ error: 'Password too short' });
|
||||
}
|
||||
|
||||
const hashed = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
password: hashed,
|
||||
isFirstLogin: false
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update Profile
|
||||
router.patch('/profile', async (req, res) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const { userId, profile } = req.body;
|
||||
|
||||
// Verify token
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
if (decoded.userId !== userId) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
// Update or create profile
|
||||
await prisma.userProfile.upsert({
|
||||
where: { userId: userId },
|
||||
update: {
|
||||
...profile
|
||||
},
|
||||
create: {
|
||||
userId: userId,
|
||||
...profile
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -54,19 +54,39 @@ export const adminResetPassword = (userId: string, newPass: string) => {
|
||||
// Admin only
|
||||
};
|
||||
|
||||
export const updateUserProfile = async (userId: string, profile: Partial<UserProfile>) => {
|
||||
// Not implemented in backend yet as a separate endpoint,
|
||||
// but typically this would be a PATCH /users/me/profile
|
||||
// For now, let's skip or implement if needed.
|
||||
// The session save updates weight.
|
||||
export const updateUserProfile = async (userId: string, profile: Partial<UserProfile>): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const res = await api.patch('/auth/profile', { userId, profile });
|
||||
return res;
|
||||
} catch (e) {
|
||||
return { success: false, error: 'Failed to update profile' };
|
||||
}
|
||||
};
|
||||
|
||||
export const changePassword = (userId: string, newPassword: string) => {
|
||||
// Not implemented
|
||||
export const changePassword = async (userId: string, newPassword: string) => {
|
||||
try {
|
||||
const res = await api.post('/auth/change-password', { userId, newPassword });
|
||||
if (!res.success) {
|
||||
console.error('Failed to change password:', res.error);
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { success: false, error: 'Network error' };
|
||||
}
|
||||
};
|
||||
|
||||
export const getCurrentUserProfile = (userId: string): UserProfile | undefined => {
|
||||
// This was synchronous. Now it needs to be async or fetched on load.
|
||||
// For now, we return undefined and let the app fetch it.
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getMe = async (): Promise<{ success: boolean; user?: User; error?: string }> => {
|
||||
try {
|
||||
const res = await api.get('/auth/me');
|
||||
return res;
|
||||
} catch (e) {
|
||||
return { success: false, error: 'Failed to fetch user' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ const translations = {
|
||||
change_pass_desc: 'This is your first login. Please set a new password.',
|
||||
change_pass_new: 'New Password',
|
||||
change_pass_save: 'Save & Login',
|
||||
change_pass_error: 'Error changing password',
|
||||
passwords_mismatch: 'Passwords do not match',
|
||||
register_title: 'Create Account',
|
||||
confirm_password: 'Confirm Password',
|
||||
@@ -145,6 +146,7 @@ const translations = {
|
||||
archive: 'Archive',
|
||||
unarchive: 'Unarchive',
|
||||
show_archived: 'Show Archived',
|
||||
profile_saved: 'Profile saved successfully',
|
||||
},
|
||||
ru: {
|
||||
// Tabs
|
||||
@@ -167,6 +169,7 @@ const translations = {
|
||||
change_pass_desc: 'Это ваш первый вход. Пожалуйста, установите новый пароль.',
|
||||
change_pass_new: 'Новый пароль',
|
||||
change_pass_save: 'Сохранить и войти',
|
||||
change_pass_error: 'Ошибка смены пароля',
|
||||
passwords_mismatch: 'Пароли не совпадают',
|
||||
register_title: 'Регистрация',
|
||||
confirm_password: 'Подтвердите пароль',
|
||||
@@ -283,6 +286,7 @@ const translations = {
|
||||
archive: 'Архив',
|
||||
unarchive: 'Вернуть',
|
||||
show_archived: 'Показать архивные',
|
||||
profile_saved: 'Профиль успешно сохранен',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user