559 lines
33 KiB
TypeScript
559 lines
33 KiB
TypeScript
|
||
import React, { useState, useEffect } from 'react';
|
||
import { User, Language, ExerciseDef, ExerciseType } from '../types';
|
||
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword } from '../services/auth';
|
||
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;
|
||
onLogout: () => void;
|
||
lang: Language;
|
||
onLanguageChange: (lang: Language) => void;
|
||
}
|
||
|
||
const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChange }) => {
|
||
// Profile Data
|
||
const [weight, setWeight] = useState<string>('');
|
||
const [height, setHeight] = useState<string>('');
|
||
const [birthDate, setBirthDate] = useState<string>('');
|
||
const [gender, setGender] = useState<string>('MALE');
|
||
|
||
// 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);
|
||
// New exercise form
|
||
const [newExName, setNewExName] = useState('');
|
||
const [newExType, setNewExType] = useState<ExerciseType>(ExerciseType.STRENGTH);
|
||
const [newExBw, setNewExBw] = useState('100');
|
||
|
||
|
||
useEffect(() => {
|
||
const p = getCurrentUserProfile(user.id);
|
||
if (p) {
|
||
if (p.weight) setWeight(p.weight.toString());
|
||
if (p.height) setHeight(p.height.toString());
|
||
if (p.gender) setGender(p.gender);
|
||
if (p.birthDate) setBirthDate(new Date(p.birthDate).toISOString().split('T')[0]);
|
||
}
|
||
|
||
if (user.role === 'ADMIN') {
|
||
refreshUserList();
|
||
}
|
||
refreshExercises();
|
||
}, [user.id, user.role]);
|
||
|
||
const refreshUserList = () => {
|
||
setAllUsers(getUsers());
|
||
};
|
||
|
||
const refreshExercises = () => {
|
||
setExercises(getExercises(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
|
||
});
|
||
|
||
if (res.success) {
|
||
showSnackbar(t('profile_saved', lang) || 'Profile saved successfully', 'success');
|
||
} 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) => {
|
||
if (confirm(t('delete_confirm', lang))) {
|
||
deleteUser(uid);
|
||
refreshUserList();
|
||
}
|
||
};
|
||
|
||
const handleAdminBlockUser = (uid: string, isBlocked: boolean) => {
|
||
toggleBlockUser(uid, isBlocked);
|
||
refreshUserList();
|
||
};
|
||
|
||
const handleAdminResetPass = (uid: string) => {
|
||
const pass = adminPassResetInput[uid];
|
||
if (pass && pass.length >= 4) {
|
||
adminResetPassword(uid, pass);
|
||
alert(t('pass_reset', lang));
|
||
setAdminPassResetInput({ ...adminPassResetInput, [uid]: '' });
|
||
}
|
||
};
|
||
|
||
const handleDeleteMyAccount = () => {
|
||
deleteUser(user.id);
|
||
onLogout();
|
||
};
|
||
|
||
// Exercise Management Handlers
|
||
const handleArchiveExercise = (ex: ExerciseDef, archive: boolean) => {
|
||
const updated = { ...ex, isArchived: archive };
|
||
saveExercise(user.id, updated);
|
||
refreshExercises();
|
||
};
|
||
|
||
const handleSaveExerciseEdit = () => {
|
||
if (editingExercise && editingExercise.name) {
|
||
saveExercise(user.id, editingExercise);
|
||
setEditingExercise(null);
|
||
refreshExercises();
|
||
}
|
||
};
|
||
|
||
const handleCreateExercise = () => {
|
||
if (newExName.trim()) {
|
||
const newEx: ExerciseDef = {
|
||
id: crypto.randomUUID(),
|
||
name: newExName.trim(),
|
||
type: newExType,
|
||
bodyWeightPercentage: newExType === ExerciseType.BODYWEIGHT ? parseFloat(newExBw) : undefined
|
||
};
|
||
saveExercise(user.id, newEx);
|
||
setNewExName('');
|
||
setIsCreatingEx(false);
|
||
refreshExercises();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="h-full flex flex-col bg-surface">
|
||
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center justify-between z-10">
|
||
<h2 className="text-xl font-normal text-on-surface flex items-center gap-2">
|
||
<UserIcon size={20} />
|
||
{t('profile_title', lang)}
|
||
</h2>
|
||
<button onClick={onLogout} className="text-error flex items-center gap-1 text-sm font-medium hover:bg-error-container/10 px-3 py-1 rounded-full">
|
||
<LogOut size={16} /> {t('logout', lang)}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-4 space-y-6 pb-24">
|
||
|
||
{/* User Info Card */}
|
||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||
<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
|
||
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 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>
|
||
<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>
|
||
<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>
|
||
<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} className="w-full py-2 rounded-full border border-outline text-primary text-sm font-medium hover:bg-primary-container/10 flex justify-center gap-2 items-center">
|
||
<Save size={16} /> {t('save_profile', lang)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* EXERCISE MANAGER */}
|
||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||
<button
|
||
onClick={() => 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={() => 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>
|
||
|
||
<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)
|
||
.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">{ex.type}</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">
|
||
<Pencil size={16} />
|
||
</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)}
|
||
>
|
||
{ex.isArchived ? <ArchiveRestore size={16} /> : <Archive size={16} />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Change Password */}
|
||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||
<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} className="bg-secondary-container text-on-secondary-container px-4 rounded-lg font-medium text-sm">OK</button>
|
||
</div>
|
||
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
|
||
</div>
|
||
|
||
{/* User Self Deletion (Not for Admin) */}
|
||
{user.role !== 'ADMIN' && (
|
||
<div className="bg-surface-container rounded-xl p-4 border 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)} className="text-xs px-3 py-1 bg-surface-container-high rounded-full">{t('cancel', lang)}</button>
|
||
<button onClick={handleDeleteMyAccount} className="text-xs px-3 py-1 bg-error text-on-error rounded-full">{t('delete', lang)}</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* ADMIN AREA */}
|
||
{user.role === 'ADMIN' && (
|
||
<div className="bg-surface-container rounded-xl p-4 border 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>
|
||
<input
|
||
type="email"
|
||
placeholder="Email"
|
||
value={newUserEmail}
|
||
onChange={(e) => setNewUserEmail(e.target.value)}
|
||
className="w-full bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
|
||
/>
|
||
<input
|
||
type="text"
|
||
placeholder={t('login_password', lang)}
|
||
value={newUserPass}
|
||
onChange={(e) => setNewUserPass(e.target.value)}
|
||
className="w-full bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
|
||
/>
|
||
<button onClick={handleCreateUser} className="w-full py-2 bg-primary text-on-primary rounded-full text-sm font-medium">
|
||
{t('create_btn', lang)}
|
||
</button>
|
||
{createMsg && <p className="text-xs text-on-surface-variant text-center">{createMsg}</p>}
|
||
</div>
|
||
|
||
{/* User List */}
|
||
<div className="border-t border-outline-variant pt-4">
|
||
<button
|
||
onClick={() => setShowUserList(!showUserList)}
|
||
className="w-full flex justify-between items-center text-sm font-medium text-on-surface"
|
||
>
|
||
<span>{t('admin_users_list', lang)} ({allUsers.length})</span>
|
||
{showUserList ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||
</button>
|
||
|
||
{showUserList && (
|
||
<div className="mt-4 space-y-4">
|
||
{allUsers.map(u => (
|
||
<div key={u.id} className="bg-surface-container-high p-3 rounded-lg space-y-3">
|
||
<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)}
|
||
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)}
|
||
>
|
||
<Ban size={16} />
|
||
</button>
|
||
<button
|
||
onClick={() => handleAdminDeleteUser(u.id)}
|
||
className="p-2 text-on-surface-variant hover:text-error hover:bg-error/10 rounded-full"
|
||
title={t('delete', lang)}
|
||
>
|
||
<Trash2 size={16} />
|
||
</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)}
|
||
className="text-xs bg-secondary-container text-on-secondary-container px-3 py-2 rounded font-medium"
|
||
>
|
||
{t('reset_pass', lang)}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Exercise Modal */}
|
||
{editingExercise && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3">
|
||
<h3 className="text-xl font-normal text-on-surface mb-4">{t('edit', lang)}</h3>
|
||
<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="flex justify-end gap-2 pt-2">
|
||
<button onClick={() => setEditingExercise(null)} className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button>
|
||
<button onClick={handleSaveExerciseEdit} className="px-4 py-2 rounded-full bg-primary text-on-primary font-medium">{t('save', lang)}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Create Exercise Modal */}
|
||
{isCreatingEx && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3">
|
||
<h3 className="text-xl font-normal text-on-surface mb-4">{t('create_exercise', lang)}</h3>
|
||
<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={newExName}
|
||
onChange={(e) => setNewExName(e.target.value)}
|
||
className="w-full bg-transparent text-on-surface focus:outline-none"
|
||
autoFocus
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="text-[10px] text-on-surface-variant font-medium mb-2 block">{t('ex_type', lang)}</label>
|
||
<div className="flex flex-wrap gap-2">
|
||
<button
|
||
onClick={() => setNewExType(ExerciseType.STRENGTH)}
|
||
className={`px-3 py-1 rounded-full text-xs border ${newExType === ExerciseType.STRENGTH ? 'bg-primary text-on-primary border-transparent' : 'border-outline text-on-surface-variant'}`}
|
||
>
|
||
{t('type_strength', lang)}
|
||
</button>
|
||
<button
|
||
onClick={() => setNewExType(ExerciseType.BODYWEIGHT)}
|
||
className={`px-3 py-1 rounded-full text-xs border ${newExType === ExerciseType.BODYWEIGHT ? 'bg-primary text-on-primary border-transparent' : 'border-outline text-on-surface-variant'}`}
|
||
>
|
||
{t('type_bodyweight', lang)}
|
||
</button>
|
||
<button
|
||
onClick={() => setNewExType(ExerciseType.CARDIO)}
|
||
className={`px-3 py-1 rounded-full text-xs border ${newExType === ExerciseType.CARDIO ? 'bg-primary text-on-primary border-transparent' : 'border-outline text-on-surface-variant'}`}
|
||
>
|
||
{t('type_cardio', lang)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{newExType === ExerciseType.BODYWEIGHT && (
|
||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
||
<label className="text-[10px] text-on-surface-variant font-medium">{t('body_weight_percent', lang)}</label>
|
||
<input
|
||
type="number"
|
||
value={newExBw}
|
||
onChange={(e) => setNewExBw(e.target.value)}
|
||
className="w-full bg-transparent text-on-surface focus:outline-none"
|
||
/>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-end gap-2 pt-2">
|
||
<button onClick={() => setIsCreatingEx(false)} className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button>
|
||
<button onClick={handleCreateExercise} className="px-4 py-2 rounded-full bg-primary text-on-primary font-medium">{t('create_btn', lang)}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
<Snackbar
|
||
isOpen={snackbar.isOpen}
|
||
message={snackbar.message}
|
||
type={snackbar.type}
|
||
onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Profile; |