All Tests Work! Password reset implemented. Users list sorted.

This commit is contained in:
AG
2025-12-10 12:08:11 +02:00
parent bc9b685dec
commit 5597d45e48
16 changed files with 1033 additions and 39 deletions

View File

@@ -26,7 +26,7 @@ const Navbar: React.FC<NavbarProps> = ({ lang }) => {
return (
<>
{/* MOBILE: Bottom Navigation Bar (MD3) */}
<div className="md:hidden fixed bottom-0 left-0 w-full bg-surface-container shadow-elevation-2 border-t border-white/5 pb-safe z-50 h-20">
<div role="navigation" aria-label="Bottom Navigation" className="md:hidden fixed bottom-0 left-0 w-full bg-surface-container shadow-elevation-2 border-t border-white/5 pb-safe z-50 h-20">
<div className="flex justify-evenly items-center h-full px-1">
{navItems.map((item) => {
const isActive = currentPath === item.path || (item.path !== '/' && currentPath.startsWith(item.path));
@@ -51,7 +51,7 @@ const Navbar: React.FC<NavbarProps> = ({ lang }) => {
</div>
{/* DESKTOP: Navigation Rail (MD3) */}
<div className="hidden md:flex flex-col w-20 h-full bg-surface-container border-r border-outline-variant items-center py-8 gap-8 z-50">
<div role="navigation" aria-label="Desktop Navigation" className="hidden md:flex flex-col w-20 h-full bg-surface-container border-r border-outline-variant items-center py-8 gap-8 z-50">
<div className="flex flex-col gap-6 w-full px-2">
{navItems.map((item) => {
const isActive = currentPath === item.path || (item.path !== '/' && currentPath.startsWith(item.path));

View File

@@ -4,7 +4,7 @@ import { User, Language, ExerciseDef, ExerciseType, BodyWeightRecord } from '../
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 } from 'lucide-react';
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';
@@ -202,12 +202,16 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
await refreshUserList();
};
const handleAdminResetPass = (uid: string) => {
const handleAdminResetPass = async (uid: string) => {
const pass = adminPassResetInput[uid];
if (pass && pass.length >= 4) {
adminResetPassword(uid, pass);
alert(t('pass_reset', lang));
setAdminPassResetInput({ ...adminPassResetInput, [uid]: '' });
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');
}
}
};
@@ -274,6 +278,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
<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}
@@ -283,15 +288,15 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
</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" />
<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 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" />
<input data-testid="profile-birth-date" 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">
<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>
@@ -507,18 +512,32 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
{/* User List */}
<div className="border-t border-outline-variant pt-4">
<button
<div
role="button"
aria-expanded={showUserList}
onClick={() => setShowUserList(!showUserList)}
className="w-full flex justify-between items-center text-sm font-medium text-on-surface"
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>
{showUserList ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</button>
<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">
<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">
<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>
@@ -534,6 +553,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
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)}
aria-label={u.isBlocked ? t('unblock', lang) : t('block', lang)}
>
<Ban size={16} />
</button>
@@ -541,6 +561,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
onClick={() => handleAdminDeleteUser(u.id)}
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={16} />
</button>