Files
gymflow/src/components/Profile.tsx

735 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { User, Language, ExerciseDef, ExerciseType, BodyWeightRecord } from '../types';
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth';
import { getExercises, saveExercise } from '../services/storage';
import { getWeightHistory, logWeight } from '../services/weight';
import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, Plus, RefreshCcw } from 'lucide-react';
import ExerciseModal from './ExerciseModal';
import FilledInput from './FilledInput';
import { t } from '../services/i18n';
import Snackbar from './Snackbar';
import { Button } from './ui/Button';
import { Card } from './ui/Card';
import { TopBar } from './ui/TopBar';
import { Modal } from './ui/Modal';
import { SideSheet } from './ui/SideSheet';
import { Checkbox } from './ui/Checkbox';
import { DatePicker } from './ui/DatePicker';
interface ProfileProps {
user: User;
onLogout: () => void;
lang: Language;
onLanguageChange: (lang: Language) => void;
onUserUpdate?: (user: User) => void;
}
const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChange, onUserUpdate }) => {
// Profile Data
const [weight, setWeight] = useState<string>('');
const [height, setHeight] = useState<string>('');
const [birthDate, setBirthDate] = useState<string>('');
const [gender, setGender] = useState<string>('MALE');
// Weight Tracker
const [weightHistory, setWeightHistory] = useState<BodyWeightRecord[]>([]);
const [todayWeight, setTodayWeight] = useState<string>('');
const [showWeightTracker, setShowWeightTracker] = useState(false);
// Admin: Create User
const [newUserEmail, setNewUserEmail] = useState('');
const [newUserPass, setNewUserPass] = useState('');
const [createMsg, setCreateMsg] = useState('');
// Admin: User List
const [showUserList, setShowUserList] = useState(false);
const [allUsers, setAllUsers] = useState<any[]>([]);
const [adminPassResetInput, setAdminPassResetInput] = useState<{ [key: string]: string }>({});
// Change Password
const [newPassword, setNewPassword] = useState('');
const [passMsg, setPassMsg] = useState('');
// Account Deletion
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// Exercise Management
const [showExercises, setShowExercises] = useState(false);
const [exercises, setExercises] = useState<ExerciseDef[]>([]);
const [showArchived, setShowArchived] = useState(false);
const [editingExercise, setEditingExercise] = useState<ExerciseDef | null>(null);
const [isCreatingEx, setIsCreatingEx] = useState(false);
const [exerciseNameFilter, setExerciseNameFilter] = useState('');
// Admin Confirmation Modal State
type AdminActionType = 'DELETE_USER' | 'BLOCK_USER' | 'UNBLOCK_USER';
const [confirmAction, setConfirmAction] = useState<{ type: AdminActionType, userId: string, email?: string, currentBlockState?: boolean } | null>(null);
const exerciseTypeLabels: Record<ExerciseType, string> = {
[ExerciseType.STRENGTH]: t('type_strength', lang),
[ExerciseType.BODYWEIGHT]: t('type_bodyweight', lang),
[ExerciseType.CARDIO]: t('type_cardio', lang),
[ExerciseType.STATIC]: t('type_static', lang),
[ExerciseType.HIGH_JUMP]: t('type_height', lang),
[ExerciseType.LONG_JUMP]: t('type_dist', lang),
[ExerciseType.PLYOMETRIC]: t('type_jump', lang),
};
useEffect(() => {
// Load profile data from user object (comes from /auth/me endpoint)
if (user.profile) {
if (user.profile.weight) setWeight(user.profile.weight.toString());
if (user.profile.height) setHeight(user.profile.height.toString());
if (user.profile.gender) setGender(user.profile.gender);
if (user.profile.birthDate) setBirthDate(new Date(user.profile.birthDate).toISOString().split('T')[0]);
}
if (user.role === 'ADMIN') {
refreshUserList();
}
refreshExercises();
refreshWeightHistory();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user.id, user.role, JSON.stringify(user.profile)]);
const refreshWeightHistory = async () => {
const history = await getWeightHistory();
setWeightHistory(history);
// Check if we have a weight for today
const today = new Date().toISOString().split('T')[0];
const todayRecord = history.find(r => r.dateStr === today);
if (todayRecord) {
setTodayWeight(todayRecord.weight.toString());
}
};
const refreshUserList = async () => {
const res = await getUsers();
if (res.success && res.users) {
setAllUsers(res.users);
}
};
const refreshExercises = async () => {
const exercises = await getExercises(user.id);
setExercises(exercises);
};
// 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 handleLogWeight = async () => {
if (!todayWeight) return;
const weightVal = parseFloat(todayWeight);
if (isNaN(weightVal)) return;
const res = await logWeight(weightVal);
if (res) {
showSnackbar('Weight logged successfully', 'success');
refreshWeightHistory();
// Also update the profile weight display if it's today
setWeight(todayWeight);
// And trigger user update to sync across app
const userRes = await getMe();
if (userRes.success && userRes.user && onUserUpdate) {
onUserUpdate(userRes.user);
}
} else {
showSnackbar('Failed to log weight', 'error');
}
};
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).toISOString() : undefined,
language: lang
});
if (res.success) {
showSnackbar(t('profile_saved', lang) || 'Profile saved successfully', 'success');
// Refetch user data to update the profile in the app state
const userRes = await getMe();
if (userRes.success && userRes.user && onUserUpdate) {
onUserUpdate(userRes.user);
}
} else {
showSnackbar(res.error || 'Failed to save profile', 'error');
}
};
const handleChangePassword = async () => {
if (newPassword.length < 4) {
setPassMsg('Password too short');
return;
}
const res = await changePassword(user.id, newPassword);
if (res.success) {
setPassMsg('Password changed');
setNewPassword('');
} else {
setPassMsg(res.error || 'Error changing password');
}
};
const handleCreateUser = async () => {
const res = await createUser(newUserEmail, newUserPass);
if (res.success) {
setCreateMsg(`${t('user_created', lang)}: ${newUserEmail}`);
setNewUserEmail('');
setNewUserPass('');
refreshUserList();
} else {
setCreateMsg(res.error || 'Error');
}
};
const handleAdminDeleteUser = (uid: string, email: string) => {
setConfirmAction({ type: 'DELETE_USER', userId: uid, email });
};
const handleAdminBlockUser = (uid: string, isBlocked: boolean, email: string) => {
setConfirmAction({
type: isBlocked ? 'BLOCK_USER' : 'UNBLOCK_USER',
userId: uid,
email,
currentBlockState: isBlocked
});
};
const handleConfirmAction = async () => {
if (!confirmAction) return;
if (confirmAction.type === 'DELETE_USER') {
await deleteUser(confirmAction.userId);
await refreshUserList();
} else if (confirmAction.type === 'BLOCK_USER' || confirmAction.type === 'UNBLOCK_USER') {
// If type is BLOCK_USER, we are passing true to block. If UNBLOCK, false.
// But simpler: we stored currentBlockState which is the target state (e.g. isBlocked=true passed to handler)
await toggleBlockUser(confirmAction.userId, confirmAction.currentBlockState!);
await refreshUserList();
}
setConfirmAction(null);
};
const handleAdminResetPass = async (uid: string) => {
const pass = adminPassResetInput[uid];
if (pass && pass.length >= 4) {
const res = await adminResetPassword(uid, pass);
if (res.success) {
alert(t('pass_reset', lang) || 'Password reset successfully');
setAdminPassResetInput({ ...adminPassResetInput, [uid]: '' });
} else {
alert(res.error || 'Failed to reset password');
}
}
};
const handleDeleteMyAccount = () => {
deleteUser(user.id);
onLogout();
};
// Exercise Management Handlers
const handleArchiveExercise = async (ex: ExerciseDef, archive: boolean) => {
const updated = { ...ex, isArchived: archive };
await saveExercise(user.id, updated);
await refreshExercises();
};
const handleSaveExerciseEdit = async () => {
if (editingExercise && editingExercise.name) {
await saveExercise(user.id, editingExercise);
setEditingExercise(null);
await refreshExercises();
}
};
const handleCreateExercise = async (newEx: ExerciseDef) => {
await saveExercise(user.id, newEx);
setIsCreatingEx(false);
await refreshExercises();
};
return (
<div className="h-full flex flex-col bg-surface">
<TopBar
title={t('profile_title', lang)}
icon={UserIcon}
actions={
<Button onClick={onLogout} variant="ghost" size="sm" className="text-error hover:bg-error-container/10">
<LogOut size={16} className="mr-1" /> {t('logout', lang)}
</Button>
}
/>
<div className="flex-1 overflow-y-auto p-4 space-y-6 pb-24">
<div className="max-w-2xl mx-auto space-y-6">
{/* User Info Card */}
<Card>
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xl font-bold">
{user.email[0].toUpperCase()}
</div>
<div>
<div className="text-lg font-medium text-on-surface">{user.email}</div>
<div className="text-xs text-on-surface-variant bg-surface-container-high px-2 py-1 rounded w-fit mt-1 flex items-center gap-1">
{user.role === 'ADMIN' && <Shield size={10} />}
{user.role}
</div>
</div>
</div>
<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>
<input
data-testid="profile-weight-input"
type="number"
step="0.1"
value={weight}
onChange={(e) => setWeight(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"><Ruler size={10} /> {t('height', lang)}</label>
<input data-testid="profile-height-input" type="number" value={height} onChange={(e) => setHeight(e.target.value)} className="w-full bg-transparent text-on-surface focus:outline-none" />
</div>
<div>
<DatePicker
label={t('birth_date', lang)}
value={birthDate}
onChange={(val) => setBirthDate(val)}
testId="profile-birth-date"
/>
</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>
<select data-testid="profile-gender" 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>
<option value="OTHER">{t('other', lang)}</option>
</select>
</div>
</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>
<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>
</select>
</div>
<Button onClick={handleSaveProfile} variant="outline" fullWidth>
<Save size={16} className="mr-2" /> {t('save_profile', lang)}
</Button>
</Card>
{/* WEIGHT TRACKER */}
<Card>
<button
onClick={() => setShowWeightTracker(!showWeightTracker)}
className="w-full flex justify-between items-center text-sm font-bold text-primary"
>
<span className="flex items-center gap-2"><Scale size={14} /> Weight Tracker</span>
{showWeightTracker ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
{showWeightTracker && (
<div className="mt-4 space-y-4">
<div className="flex gap-2 items-end">
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2 flex-1">
<label className="text-[10px] text-on-surface-variant font-medium">Today's Weight (kg)</label>
<input
type="number"
step="0.1"
value={todayWeight}
onChange={(e) => setTodayWeight(e.target.value)}
className="w-full bg-transparent text-on-surface focus:outline-none"
placeholder="Enter weight..."
/>
</div>
<Button
onClick={handleLogWeight}
className="mb-[1px]"
>
Log
</Button>
</div>
<div className="space-y-2 max-h-60 overflow-y-auto">
<h4 className="text-xs font-medium text-on-surface-variant">History</h4>
{weightHistory.length === 0 ? (
<p className="text-xs text-on-surface-variant italic">No weight records yet.</p>
) : (
weightHistory.map(record => (
<div key={record.id} className="flex justify-between items-center p-3 bg-surface-container-high rounded-lg">
<span className="text-sm text-on-surface">{new Date(record.date).toLocaleDateString()}</span>
<span className="text-sm font-bold text-primary">{record.weight} kg</span>
</div>
))
)}
</div>
</div>
)}
</Card>
{/* EXERCISE MANAGER */}
<Card>
<button
onClick={() => {
console.log('Toggling showExercises', !showExercises);
setShowExercises(!showExercises);
}}
className="w-full flex justify-between items-center text-sm font-bold text-primary"
>
<span className="flex items-center gap-2"><Dumbbell size={14} /> {t('manage_exercises', lang)}</span>
{showExercises ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
{showExercises && (
<div className="mt-4 space-y-4">
<button
onClick={() => {
console.log('Clicking New Exercise');
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)}
</button>
<FilledInput
label={t('filter_by_name', lang) || 'Filter by name'}
value={exerciseNameFilter}
onChange={(e: any) => setExerciseNameFilter(e.target.value)}
icon={<i className="hidden" />}
type="text"
autoFocus={false}
/>
<div className="flex items-center justify-end gap-2">
<label className="text-xs text-on-surface-variant">{t('show_archived', lang)}</label>
<input
type="checkbox"
checked={showArchived}
onChange={(e) => setShowArchived(e.target.checked)}
className="accent-primary"
/>
</div>
<div className="space-y-2">
{exercises
.filter(e => showArchived || !e.isArchived)
.filter(e => e.name.toLowerCase().includes(exerciseNameFilter.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name))
.map(ex => (
<div key={ex.id} className={`p-3 rounded-lg flex justify-between items-center border border-outline-variant/20 ${ex.isArchived ? 'bg-surface-container-low opacity-60' : 'bg-surface-container-high'}`}>
<div className="overflow-hidden mr-2">
<div className="font-medium text-sm text-on-surface truncate">{ex.name}</div>
<div className="text-xs text-on-surface-variant">
{exerciseTypeLabels[ex.type]}
{ex.isUnilateral && `, ${t('unilateral', lang)}`}
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<button onClick={() => setEditingExercise(ex)} className="p-2 text-on-surface-variant hover:text-primary hover:bg-white/5 rounded-full" role="button" aria-label="Edit Exercise">
<Pencil size={18} />
</button>
<button
onClick={() => handleArchiveExercise(ex, !ex.isArchived)}
className={`p-2 rounded-full hover:bg-white/5 ${ex.isArchived ? 'text-primary' : 'text-on-surface-variant'}`}
title={ex.isArchived ? t('unarchive', lang) : t('archive', lang)}
role="button"
aria-label={ex.isArchived ? 'Unarchive Exercise' : 'Archive Exercise'}
>
{ex.isArchived ? <ArchiveRestore size={18} /> : <Archive size={18} />}
</button>
</div>
</div>
))}
</div>
</div>
)}
</Card>
{/* Change Password */}
<Card>
<h3 className="text-sm font-bold text-primary mb-4 flex items-center gap-2"><Lock size={14} /> {t('change_pass_btn', lang)}</h3>
<div className="flex gap-2">
<input
type="password"
placeholder={t('change_pass_new', lang)}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="flex-1 bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
/>
<Button onClick={handleChangePassword} size="sm" variant="secondary">OK</Button>
</div>
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
</Card>
{/* User Self Deletion (Not for Admin) */}
{user.role !== 'ADMIN' && (
<Card className="border-error/30">
<h3 className="text-sm font-bold text-error mb-2 flex items-center gap-2"><Trash2 size={14} /> {t('delete_account', lang)}</h3>
{!showDeleteConfirm ? (
<button onClick={() => setShowDeleteConfirm(true)} className="text-error text-sm hover:underline">
{t('delete', lang)}
</button>
) : (
<div className="space-y-2">
<p className="text-xs text-error">{t('delete_account_confirm', lang)}</p>
<div className="flex gap-2">
<Button onClick={() => setShowDeleteConfirm(false)} size="sm" variant="ghost">{t('cancel', lang)}</Button>
<Button onClick={handleDeleteMyAccount} size="sm" variant="destructive">{t('delete', lang)}</Button>
</div>
</div>
)}
</Card>
)}
{/* ADMIN AREA */}
{user.role === 'ADMIN' && (
<Card className="border-primary/30 relative overflow-hidden">
<div className="absolute top-0 right-0 p-2 bg-primary/10 rounded-bl-xl">
<Shield size={16} className="text-primary" />
</div>
<h3 className="text-sm font-bold text-primary mb-4 flex items-center gap-2"><UserPlus size={14} /> {t('admin_area', lang)}</h3>
{/* Create User */}
<div className="space-y-3 mb-6">
<h4 className="text-xs font-medium text-on-surface-variant">{t('create_user', lang)}</h4>
<FilledInput
label="Email"
value={newUserEmail}
onChange={(e) => setNewUserEmail(e.target.value)}
type="email"
/>
<FilledInput
label={t('login_password', lang)}
value={newUserPass}
onChange={(e) => setNewUserPass(e.target.value)}
type="text"
/>
<Button onClick={handleCreateUser} fullWidth>
{t('create_btn', lang)}
</Button>
{createMsg && <p className="text-xs text-error text-center font-medium">{createMsg}</p>}
</div>
{/* User List */}
<div className="border-t border-outline-variant pt-4">
<div
role="button"
aria-expanded={showUserList}
onClick={() => setShowUserList(!showUserList)}
className="w-full flex justify-between items-center text-sm font-medium text-on-surface cursor-pointer select-none"
>
<span>{t('admin_users_list', lang)} ({allUsers.length})</span>
<div className="flex items-center gap-2">
<button
onClick={(e) => {
e.stopPropagation();
refreshUserList();
}}
className="p-1 hover:bg-surface-container-high rounded-full transition-colors text-on-surface-variant hover:text-primary"
title="Refresh List"
>
<RefreshCcw size={14} />
</button>
{showUserList ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</div>
</div>
{showUserList && (
<div className="mt-4 space-y-4" data-testid="user-list">
{allUsers.map(u => (
<div key={u.id} className="bg-surface-container-high p-3 rounded-lg space-y-3" data-testid="user-row">
<div className="flex justify-between items-center">
<div className="overflow-hidden">
<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>}
</div>
</div>
<div className="flex gap-1">
{u.role !== 'ADMIN' && (
<>
<button
onClick={() => handleAdminBlockUser(u.id, !u.isBlocked, u.email)}
className={`p-2 rounded-full ${u.isBlocked ? 'bg-primary/20 text-primary' : 'text-on-surface-variant hover:bg-white/10'}`}
title={u.isBlocked ? t('unblock', lang) : t('block', lang)}
aria-label={u.isBlocked ? t('unblock', lang) : t('block', lang)}
>
<Ban size={18} />
</button>
<button
onClick={() => handleAdminDeleteUser(u.id, u.email)}
className="p-2 text-on-surface-variant hover:text-error hover:bg-error/10 rounded-full"
title={t('delete', lang)}
aria-label={t('delete', lang)}
>
<Trash2 size={18} />
</button>
</>
)}
</div>
</div>
{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" />
<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 })}
/>
</div>
<Button
onClick={() => handleAdminResetPass(u.id)}
size="sm"
variant="secondary"
>
{t('reset_pass', lang)}
</Button>
</div>
)}
</div>
))}
</div>
)}
</div>
</Card>
)}
{/* Edit Exercise Modal */}
{editingExercise && (
<SideSheet
isOpen={!!editingExercise}
onClose={() => setEditingExercise(null)}
title={t('edit', lang)}
width="md"
footer={
<div className="flex justify-end gap-2">
<Button onClick={() => setEditingExercise(null)} variant="ghost">{t('cancel', lang)}</Button>
<Button onClick={handleSaveExerciseEdit}>{t('save', lang)}</Button>
</div>
}
>
<div className="space-y-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 font-medium">{t('ex_name', lang)}</label>
<input
value={editingExercise.name}
onChange={(e) => setEditingExercise({ ...editingExercise, name: e.target.value })}
className="w-full bg-transparent text-on-surface focus:outline-none"
/>
</div>
{editingExercise.type === ExerciseType.BODYWEIGHT && (
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
<label className="text-[10px] text-on-surface-variant font-medium">{t('body_weight_percent', lang)}</label>
<input
type="number"
value={editingExercise.bodyWeightPercentage || 100}
onChange={(e) => setEditingExercise({ ...editingExercise, bodyWeightPercentage: parseFloat(e.target.value) })}
className="w-full bg-transparent text-on-surface focus:outline-none"
/>
</div>
)}
<div className="px-1 py-2">
<Checkbox
checked={editingExercise.isUnilateral || false}
onChange={(e) => setEditingExercise({ ...editingExercise, isUnilateral: e.target.checked })}
label={t('unilateral_exercise', lang) || 'Unilateral exercise'}
/>
</div>
</div>
</SideSheet>
)}
{/* Create Exercise Modal */}
{isCreatingEx && (
<ExerciseModal
isOpen={isCreatingEx}
onClose={() => setIsCreatingEx(false)}
onSave={handleCreateExercise}
lang={lang}
existingExercises={exercises}
/>
)}
{/* Admin Confirmation Modal */}
<Modal
isOpen={!!confirmAction}
onClose={() => setConfirmAction(null)}
title={
confirmAction?.type === 'DELETE_USER' ? t('delete_user_confirm_title', lang) :
confirmAction?.type === 'BLOCK_USER' ? t('block_confirm_title', lang) :
t('unblock_confirm_title', lang)
}
>
<div className="space-y-4">
<p className="text-on-surface-variant">
{
confirmAction?.type === 'DELETE_USER' ? t('delete_user_confirm_msg', lang) :
confirmAction?.type === 'BLOCK_USER' ? t('block_confirm_msg', lang) :
t('unblock_confirm_msg', lang)
}
</p>
{confirmAction?.email && (
<p className="font-medium text-on-surface">{confirmAction.email}</p>
)}
<div className="flex justify-end gap-2 mt-4">
<Button onClick={() => setConfirmAction(null)} variant="ghost">
{t('cancel', lang)}
</Button>
<Button
onClick={handleConfirmAction}
variant={confirmAction?.type === 'UNBLOCK_USER' ? 'primary' : 'destructive'}
>
{t('confirm', lang)}
</Button>
</div>
</div>
</Modal>
</div>
<Snackbar
isOpen={snackbar.isOpen}
message={snackbar.message}
type={snackbar.type}
onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))}
/>
</div>
</div>
);
};
export default Profile;