UI refactoring: Profile, History, and Plans Components
This commit is contained in:
@@ -9,6 +9,9 @@ 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 { Modal } from './ui/Modal';
|
||||
|
||||
interface ProfileProps {
|
||||
user: User;
|
||||
@@ -238,338 +241,343 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
|
||||
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">
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center justify-between z-10 shrink-0">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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 */}
|
||||
<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}
|
||||
{/* 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>
|
||||
|
||||
<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>
|
||||
|
||||
{/* WEIGHT TRACKER */}
|
||||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
onClick={handleLogWeight}
|
||||
className="bg-primary text-on-primary px-4 py-3 rounded-lg font-medium text-sm 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>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<FilledInput
|
||||
label={t('filter_by_name', lang) || 'Filter by name'}
|
||||
value={exerciseNameFilter}
|
||||
onChange={(e: any) => setExerciseNameFilter(e.target.value)}
|
||||
icon={<i className="hidden" />} // No icon needed or maybe use Search icon? Profile doesn't import Search. I'll omit icon if optional.
|
||||
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>
|
||||
<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="checkbox"
|
||||
checked={showArchived}
|
||||
onChange={(e) => setShowArchived(e.target.checked)}
|
||||
className="accent-primary"
|
||||
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="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">
|
||||
<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 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>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 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>
|
||||
{/* EXERCISE MANAGER */}
|
||||
<Card>
|
||||
<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>
|
||||
|
||||
{/* 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} 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-error text-center font-medium">{createMsg}</p>}
|
||||
</div>
|
||||
{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>
|
||||
|
||||
{/* 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>
|
||||
<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}
|
||||
/>
|
||||
|
||||
{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 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 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>
|
||||
<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={() => handleAdminResetPass(u.id)}
|
||||
className="text-xs bg-secondary-container text-on-secondary-container px-3 py-2 rounded font-medium"
|
||||
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)}
|
||||
>
|
||||
{t('reset_pass', lang)}
|
||||
{ex.isArchived ? <ArchiveRestore size={16} /> : <Archive size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
{/* 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">
|
||||
<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)}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
{t('reset_pass', lang)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Edit Exercise Modal */}
|
||||
{editingExercise && (
|
||||
<Modal
|
||||
isOpen={!!editingExercise}
|
||||
onClose={() => setEditingExercise(null)}
|
||||
title={t('edit', lang)}
|
||||
maxWidth="sm"
|
||||
>
|
||||
<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>
|
||||
@@ -591,32 +599,32 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</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>
|
||||
<Button onClick={() => setEditingExercise(null)} variant="ghost">{t('cancel', lang)}</Button>
|
||||
<Button onClick={handleSaveExerciseEdit}>{t('save', lang)}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Create Exercise Modal */}
|
||||
{isCreatingEx && (
|
||||
<ExerciseModal
|
||||
isOpen={isCreatingEx}
|
||||
onClose={() => setIsCreatingEx(false)}
|
||||
onSave={handleCreateExercise}
|
||||
lang={lang}
|
||||
existingExercises={exercises}
|
||||
/>
|
||||
)}
|
||||
{/* Create Exercise Modal */}
|
||||
{isCreatingEx && (
|
||||
<ExerciseModal
|
||||
isOpen={isCreatingEx}
|
||||
onClose={() => setIsCreatingEx(false)}
|
||||
onSave={handleCreateExercise}
|
||||
lang={lang}
|
||||
existingExercises={exercises}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<Snackbar
|
||||
isOpen={snackbar.isOpen}
|
||||
message={snackbar.message}
|
||||
type={snackbar.type}
|
||||
onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))}
|
||||
/>
|
||||
</div>
|
||||
<Snackbar
|
||||
isOpen={snackbar.isOpen}
|
||||
message={snackbar.message}
|
||||
type={snackbar.type}
|
||||
onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user