Archived exercises hidden from selects. Password fields Show Password toggle
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useId } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { X, Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
interface FilledInputProps {
|
||||
label: string;
|
||||
@@ -18,15 +18,19 @@ interface FilledInputProps {
|
||||
rightElement?: React.ReactNode;
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
showPasswordToggle?: boolean;
|
||||
}
|
||||
|
||||
const FilledInput: React.FC<FilledInputProps> = ({
|
||||
label, value, onChange, onClear, onFocus, onBlur, type = "number", icon,
|
||||
autoFocus, step, inputMode, autocapitalize, autoComplete, rightElement,
|
||||
multiline = false, rows = 3
|
||||
multiline = false, rows = 3, showPasswordToggle = false
|
||||
}) => {
|
||||
const id = useId();
|
||||
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
const actualType = type === 'password' && showPassword ? 'text' : type;
|
||||
|
||||
const handleClear = () => {
|
||||
const syntheticEvent = {
|
||||
@@ -47,11 +51,11 @@ const FilledInput: React.FC<FilledInputProps> = ({
|
||||
<input
|
||||
ref={inputRef as React.RefObject<HTMLInputElement>}
|
||||
id={id}
|
||||
type={type}
|
||||
type={actualType}
|
||||
step={step}
|
||||
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
|
||||
autoFocus={autoFocus}
|
||||
className={`w-full h-[56px] pt-5 pb-1 pl-4 bg-transparent text-body-lg text-on-surface focus:outline-none placeholder-transparent ${rightElement ? 'pr-20' : 'pr-10'}`}
|
||||
className={`w-full h-[56px] pt-5 pb-1 pl-4 bg-transparent text-body-lg text-on-surface focus:outline-none placeholder-transparent ${rightElement ? 'pr-20' : (showPasswordToggle && type === 'password' ? 'pr-20' : 'pr-10')}`}
|
||||
placeholder=" "
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@@ -80,12 +84,24 @@ const FilledInput: React.FC<FilledInputProps> = ({
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
aria-label="Clear input"
|
||||
className={`absolute top-1/2 -translate-y-1/2 p-2 text-on-surface-variant hover:text-on-surface rounded-full transition-opacity ${rightElement ? 'right-12' : 'right-2'}`}
|
||||
className={`absolute top-1/2 -translate-y-1/2 p-2 text-on-surface-variant hover:text-on-surface rounded-full transition-opacity ${(rightElement || (showPasswordToggle && type === 'password')) ? 'right-12' : 'right-2'}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showPasswordToggle && type === 'password' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
aria-label="Toggle visibility"
|
||||
className="absolute top-1/2 -translate-y-1/2 right-2 p-2 text-on-surface-variant hover:text-on-surface rounded-full transition-opacity"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
)}
|
||||
{
|
||||
rightElement && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
|
||||
|
||||
@@ -66,6 +66,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
type="password"
|
||||
showPasswordToggle
|
||||
/>
|
||||
<button
|
||||
onClick={handleChangePassword}
|
||||
@@ -119,6 +120,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
icon={<Lock size={16} />}
|
||||
showPasswordToggle
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ 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, RefreshCcw } from 'lucide-react';
|
||||
import { generatePassword } from '../utils/password';
|
||||
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, Sparkles } from 'lucide-react';
|
||||
import ExerciseModal from './ExerciseModal';
|
||||
import FilledInput from './FilledInput';
|
||||
import { t } from '../services/i18n';
|
||||
@@ -115,7 +116,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
};
|
||||
|
||||
const refreshExercises = async () => {
|
||||
const exercises = await getExercises(user.id);
|
||||
const exercises = await getExercises(user.id, true);
|
||||
|
||||
setExercises(exercises);
|
||||
};
|
||||
@@ -478,15 +479,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
{/* 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 className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<FilledInput
|
||||
label={t('change_pass_new', lang)}
|
||||
value={newPassword}
|
||||
onChange={(e: any) => setNewPassword(e.target.value)}
|
||||
type="password"
|
||||
showPasswordToggle
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleChangePassword} className="mb-0.5">OK</Button>
|
||||
</div>
|
||||
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
|
||||
</Card>
|
||||
@@ -533,6 +536,16 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
value={newUserPass}
|
||||
onChange={(e) => setNewUserPass(e.target.value)}
|
||||
type="text"
|
||||
rightElement={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNewUserPass(generatePassword(8))}
|
||||
className="p-2 text-primary hover:bg-primary/10 rounded-full transition-colors"
|
||||
title="Generate"
|
||||
>
|
||||
<Sparkles size={20} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<Button onClick={handleCreateUser} fullWidth>
|
||||
{t('create_btn', lang)}
|
||||
@@ -601,20 +614,19 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</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"
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<FilledInput
|
||||
label={t('change_pass_new', lang)}
|
||||
value={adminPassResetInput[u.id] || ''}
|
||||
onChange={(e) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })}
|
||||
onChange={(e: any) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })}
|
||||
type="password"
|
||||
showPasswordToggle
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handleAdminResetPass(u.id)}
|
||||
size="sm"
|
||||
className="mb-0.5"
|
||||
variant="secondary"
|
||||
>
|
||||
{t('reset_pass', lang)}
|
||||
|
||||
@@ -7,9 +7,9 @@ interface ApiResponse<T> {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const getExercises = async (userId: string): Promise<ExerciseDef[]> => {
|
||||
export const getExercises = async (userId: string, includeArchived: boolean = false): Promise<ExerciseDef[]> => {
|
||||
try {
|
||||
const res = await api.get<ApiResponse<ExerciseDef[]>>('/exercises');
|
||||
const res = await api.get<ApiResponse<ExerciseDef[]>>(`/exercises${includeArchived ? '?includeArchived=true' : ''}`);
|
||||
return res.data || [];
|
||||
} catch {
|
||||
return [];
|
||||
|
||||
9
src/utils/password.ts
Normal file
9
src/utils/password.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
export function generatePassword(length = 8): string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
|
||||
let password = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
return password;
|
||||
}
|
||||
Reference in New Issue
Block a user