1. Change Password fixed. 2. Personal Data implemented. 3. New alerts style. 4. Better dropdowns.
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user