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 updatedUser = { ...tempUser, isFirstLogin: false };
|
||||
onLogin(updatedUser);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -14,19 +14,29 @@ interface PlansProps {
|
||||
const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
|
||||
const [editId, setEditId] = useState<string | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [steps, setSteps] = useState<PlannedSet[]>([]);
|
||||
|
||||
|
||||
const [availableExercises, setAvailableExercises] = useState<ExerciseDef[]>([]);
|
||||
const [showExerciseSelector, setShowExerciseSelector] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPlans(getPlans(userId));
|
||||
// Filter out archived exercises
|
||||
setAvailableExercises(getExercises(userId).filter(e => !e.isArchived));
|
||||
const loadData = async () => {
|
||||
const fetchedPlans = await getPlans(userId);
|
||||
setPlans(fetchedPlans);
|
||||
|
||||
const fetchedExercises = await getExercises(userId);
|
||||
// Filter out archived exercises
|
||||
if (Array.isArray(fetchedExercises)) {
|
||||
setAvailableExercises(fetchedExercises.filter(e => !e.isArchived));
|
||||
} else {
|
||||
setAvailableExercises([]);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, [userId]);
|
||||
|
||||
const handleCreateNew = () => {
|
||||
@@ -66,20 +76,20 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||
};
|
||||
|
||||
const toggleWeighted = (stepId: string) => {
|
||||
setSteps(steps.map(s => s.id === stepId ? { ...s, isWeighted: !s.isWeighted } : s));
|
||||
setSteps(steps.map(s => s.id === stepId ? { ...s, isWeighted: !s.isWeighted } : s));
|
||||
};
|
||||
|
||||
const removeStep = (stepId: string) => {
|
||||
setSteps(steps.filter(s => s.id !== stepId));
|
||||
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 newSteps = [...steps];
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
[newSteps[index], newSteps[targetIndex]] = [newSteps[targetIndex], newSteps[index]];
|
||||
setSteps(newSteps);
|
||||
if (direction === 'up' && index === 0) return;
|
||||
if (direction === 'down' && index === steps.length - 1) return;
|
||||
const newSteps = [...steps];
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
[newSteps[index], newSteps[targetIndex]] = [newSteps[targetIndex], newSteps[index]];
|
||||
setSteps(newSteps);
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
@@ -96,7 +106,7 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label>
|
||||
<input
|
||||
<input
|
||||
className="w-full bg-transparent text-xl text-on-surface focus:outline-none pt-1 pb-2"
|
||||
placeholder={t('plan_name_ph', lang)}
|
||||
value={name}
|
||||
@@ -106,7 +116,7 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('prep_title', lang)}</label>
|
||||
<textarea
|
||||
<textarea
|
||||
className="w-full bg-transparent text-base text-on-surface focus:outline-none pt-1 pb-2 min-h-[80px]"
|
||||
placeholder={t('plan_desc_ph', lang)}
|
||||
value={description}
|
||||
@@ -115,81 +125,81 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center px-2">
|
||||
<label className="text-sm text-primary font-medium">{t('exercises_list', lang)}</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{steps.map((step, idx) => (
|
||||
<div key={step.id} className="bg-surface-container rounded-xl p-3 flex items-center gap-3 shadow-elevation-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
{idx > 0 && (
|
||||
<button onClick={() => moveStep(idx, 'up')} className="p-1 text-on-surface-variant hover:bg-white/5 rounded">
|
||||
<ArrowUp size={16} />
|
||||
</button>
|
||||
)}
|
||||
{idx < steps.length - 1 && (
|
||||
<button onClick={() => moveStep(idx, 'down')} className="p-1 text-on-surface-variant hover:bg-white/5 rounded">
|
||||
<ArrowDown size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-8 h-8 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div className="flex justify-between items-center px-2">
|
||||
<label className="text-sm text-primary font-medium">{t('exercises_list', lang)}</label>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-base font-medium text-on-surface">{step.exerciseName}</div>
|
||||
<label className="flex items-center gap-2 mt-1 cursor-pointer w-fit">
|
||||
<div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}>
|
||||
{step.isWeighted && <Scale size={10} className="text-on-primary" />}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={step.isWeighted}
|
||||
onChange={() => toggleWeighted(step.id)}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button onClick={() => removeStep(step.id)} className="text-on-surface-variant hover:text-error p-2">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{steps.map((step, idx) => (
|
||||
<div key={step.id} className="bg-surface-container rounded-xl p-3 flex items-center gap-3 shadow-elevation-1">
|
||||
<div className="flex flex-col gap-1">
|
||||
{idx > 0 && (
|
||||
<button onClick={() => moveStep(idx, 'up')} className="p-1 text-on-surface-variant hover:bg-white/5 rounded">
|
||||
<ArrowUp size={16} />
|
||||
</button>
|
||||
)}
|
||||
{idx < steps.length - 1 && (
|
||||
<button onClick={() => moveStep(idx, 'down')} className="p-1 text-on-surface-variant hover:bg-white/5 rounded">
|
||||
<ArrowDown size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowExerciseSelector(true)}
|
||||
className="w-full py-4 rounded-full border border-outline text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary-container/10 transition-all"
|
||||
>
|
||||
<Plus size={20} />
|
||||
{t('add_exercise', lang)}
|
||||
</button>
|
||||
<div className="w-8 h-8 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xs font-bold shrink-0">
|
||||
{idx + 1}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-base font-medium text-on-surface">{step.exerciseName}</div>
|
||||
<label className="flex items-center gap-2 mt-1 cursor-pointer w-fit">
|
||||
<div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}>
|
||||
{step.isWeighted && <Scale size={10} className="text-on-primary" />}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={step.isWeighted}
|
||||
onChange={() => toggleWeighted(step.id)}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button onClick={() => removeStep(step.id)} className="text-on-surface-variant hover:text-error p-2">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowExerciseSelector(true)}
|
||||
className="w-full py-4 rounded-full border border-outline text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary-container/10 transition-all"
|
||||
>
|
||||
<Plus size={20} />
|
||||
{t('add_exercise', lang)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{showExerciseSelector && (
|
||||
<div className="absolute inset-0 bg-surface z-50 flex flex-col animate-in slide-in-from-bottom-full duration-200">
|
||||
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container">
|
||||
<span className="font-medium text-on-surface">{t('select_exercise', lang)}</span>
|
||||
<button onClick={() => setShowExerciseSelector(false)}><X /></button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{availableExercises.map(ex => (
|
||||
<button
|
||||
key={ex.id}
|
||||
onClick={() => addStep(ex)}
|
||||
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between"
|
||||
>
|
||||
<span>{ex.name}</span>
|
||||
<span className="text-xs bg-secondary-container text-on-secondary-container px-2 py-1 rounded-full">{ex.type}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-surface z-50 flex flex-col animate-in slide-in-from-bottom-full duration-200">
|
||||
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container">
|
||||
<span className="font-medium text-on-surface">{t('select_exercise', lang)}</span>
|
||||
<button onClick={() => setShowExerciseSelector(false)}><X /></button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
{availableExercises.map(ex => (
|
||||
<button
|
||||
key={ex.id}
|
||||
onClick={() => addStep(ex)}
|
||||
className="w-full text-left p-4 border-b border-outline-variant hover:bg-surface-container-high text-on-surface flex justify-between"
|
||||
>
|
||||
<span>{ex.name}</span>
|
||||
<span className="text-xs bg-secondary-container text-on-secondary-container px-2 py-1 rounded-full">{ex.type}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -203,50 +213,50 @@ const Plans: React.FC<PlansProps> = ({ userId, onStartPlan, lang }) => {
|
||||
|
||||
<div className="flex-1 p-4 overflow-y-auto space-y-4 pb-24">
|
||||
{plans.length === 0 ? (
|
||||
<div className="text-center text-on-surface-variant mt-20 flex flex-col items-center">
|
||||
<div className="w-16 h-16 bg-surface-container-high rounded-full flex items-center justify-center mb-4">
|
||||
<List size={32} />
|
||||
</div>
|
||||
<p className="text-lg">{t('plans_empty', lang)}</p>
|
||||
<div className="text-center text-on-surface-variant mt-20 flex flex-col items-center">
|
||||
<div className="w-16 h-16 bg-surface-container-high rounded-full flex items-center justify-center mb-4">
|
||||
<List size={32} />
|
||||
</div>
|
||||
<p className="text-lg">{t('plans_empty', lang)}</p>
|
||||
</div>
|
||||
) : (
|
||||
plans.map(plan => (
|
||||
<div key={plan.id} className="bg-surface-container rounded-xl p-4 shadow-elevation-1 border border-outline-variant/20 relative overflow-hidden">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-xl font-normal text-on-surface">{plan.name}</h3>
|
||||
<button
|
||||
onClick={(e) => handleDelete(plan.id, e)}
|
||||
className="text-on-surface-variant hover:text-error p-2 rounded-full hover:bg-white/5"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-on-surface-variant text-sm line-clamp-2 mb-4 min-h-[1.25rem]">
|
||||
{plan.description || t('prep_no_instructions', lang)}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-primary bg-primary-container/20 px-3 py-1 rounded-full">
|
||||
{plan.steps.length} {t('exercises_count', lang)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onStartPlan(plan)}
|
||||
className="flex items-center gap-2 bg-primary text-on-primary px-5 py-2 rounded-full text-sm font-medium hover:shadow-elevation-2 transition-all"
|
||||
>
|
||||
<PlayCircle size={18} />
|
||||
{t('start', lang)}
|
||||
</button>
|
||||
</div>
|
||||
plans.map(plan => (
|
||||
<div key={plan.id} className="bg-surface-container rounded-xl p-4 shadow-elevation-1 border border-outline-variant/20 relative overflow-hidden">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-xl font-normal text-on-surface">{plan.name}</h3>
|
||||
<button
|
||||
onClick={(e) => handleDelete(plan.id, e)}
|
||||
className="text-on-surface-variant hover:text-error p-2 rounded-full hover:bg-white/5"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-on-surface-variant text-sm line-clamp-2 mb-4 min-h-[1.25rem]">
|
||||
{plan.description || t('prep_no_instructions', lang)}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-primary bg-primary-container/20 px-3 py-1 rounded-full">
|
||||
{plan.steps.length} {t('exercises_count', lang)}
|
||||
</div>
|
||||
))
|
||||
<button
|
||||
onClick={() => onStartPlan(plan)}
|
||||
className="flex items-center gap-2 bg-primary text-on-primary px-5 py-2 rounded-full text-sm font-medium hover:shadow-elevation-2 transition-all"
|
||||
>
|
||||
<PlayCircle size={18} />
|
||||
{t('start', lang)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FAB */}
|
||||
<button
|
||||
onClick={handleCreateNew}
|
||||
className="absolute bottom-6 right-6 w-14 h-14 bg-primary-container text-on-primary-container rounded-[16px] shadow-elevation-3 flex items-center justify-center hover:bg-primary hover:text-on-primary transition-colors z-20"
|
||||
<button
|
||||
onClick={handleCreateNew}
|
||||
className="absolute bottom-6 right-6 w-14 h-14 bg-primary-container text-on-primary-container rounded-[16px] shadow-elevation-3 flex items-center justify-center hover:bg-primary hover:text-on-primary transition-colors z-20"
|
||||
>
|
||||
<Plus size={28} />
|
||||
<Plus size={28} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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.
@@ -30,7 +30,11 @@ model UserProfile {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
weight Float?
|
||||
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