From 465186041e10a7360e462da512b780adf44f1423 Mon Sep 17 00:00:00 2001 From: Hailemeskel Getaneh Date: Fri, 3 Jul 2026 17:30:15 +0300 Subject: [PATCH] feat: add password visibility toggle on login and signup pages - Add showPassword state to LoginPage for password field toggle - Add showPassword and showConfirm states to RegisterPage for both password fields - Wrap password inputs in .password-input-wrapper div with absolute-positioned toggle button - Use inline SVG eye/eye-off icons (no external dependencies) - Add .password-input-wrapper and .password-toggle-btn CSS classes to App.css - Toggle button is accessible with aria-label and focus-visible outline - Applied to both frontend/ and todo-frontend/ directories Closes #11 --- frontend/src/App.css | 41 ++++++++++ frontend/src/pages/LoginPage.jsx | 44 ++++++++--- frontend/src/pages/RegisterPage.jsx | 96 ++++++++++++++++++------ todo-frontend/src/App.css | 41 ++++++++++ todo-frontend/src/pages/LoginPage.jsx | 44 ++++++++--- todo-frontend/src/pages/RegisterPage.jsx | 96 ++++++++++++++++++------ 6 files changed, 294 insertions(+), 68 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 7043abd..ace719b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -567,6 +567,47 @@ .rule--pass .rule-icon { color: var(--green); } .rule--fail .rule-icon { color: #d1d5db; } +/* ─── Password Toggle Button ────────────────────────────── */ +.password-input-wrapper { + position: relative; + width: 100%; +} + +.password-input-wrapper .field-input { + padding-right: 42px; /* Ensure input text doesn't overlap the eye button */ +} + +.password-toggle-btn { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: center; + transition: color 0.15s, opacity 0.15s; + border-radius: 4px; +} + +.password-toggle-btn:hover:not(:disabled) { + color: var(--text); +} + +.password-toggle-btn:focus-visible { + outline: 2px solid var(--green); + outline-offset: -2px; +} + +.password-toggle-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* ─── Responsive ─────────────────────────────────────────── */ @media (max-width: 600px) { .header { padding: 20px 16px; } diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index e373db1..e62683c 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -7,6 +7,7 @@ const isValidEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); const [errors, setErrors] = useState({}); const [apiError, setApiError] = useState(''); const [loading, setLoading] = useState(false); @@ -82,16 +83,39 @@ export default function LoginPage() {
- { setPassword(e.target.value); clear('password'); }} - disabled={loading} - autoComplete="current-password" - /> +
+ { setPassword(e.target.value); clear('password'); }} + disabled={loading} + autoComplete="current-password" + /> + +
{errors.password &&

✗ {errors.password}

}
diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx index aa4dd18..f2eea12 100644 --- a/frontend/src/pages/RegisterPage.jsx +++ b/frontend/src/pages/RegisterPage.jsx @@ -24,6 +24,8 @@ export default function RegisterPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); const [errors, setErrors] = useState({}); const [apiError, setApiError] = useState(''); const [loading, setLoading] = useState(false); @@ -129,20 +131,43 @@ export default function RegisterPage() { {/* Password */}
- { - setPassword(e.target.value); - setTouched(p => ({ ...p, password: true })); - clear('password'); - }} - disabled={loading} - autoComplete="new-password" - /> +
+ { + setPassword(e.target.value); + setTouched(p => ({ ...p, password: true })); + clear('password'); + }} + disabled={loading} + autoComplete="new-password" + /> + +
{errors.password &&

✗ {errors.password}

} {/* Strength bar — shown as soon as user starts typing */} @@ -179,16 +204,39 @@ export default function RegisterPage() { {/* Confirm password */}
- { setConfirm(e.target.value); clear('confirm'); }} - disabled={loading} - autoComplete="new-password" - /> +
+ { setConfirm(e.target.value); clear('confirm'); }} + disabled={loading} + autoComplete="new-password" + /> + +
{errors.confirm &&

✗ {errors.confirm}

}
diff --git a/todo-frontend/src/App.css b/todo-frontend/src/App.css index 7043abd..ace719b 100644 --- a/todo-frontend/src/App.css +++ b/todo-frontend/src/App.css @@ -567,6 +567,47 @@ .rule--pass .rule-icon { color: var(--green); } .rule--fail .rule-icon { color: #d1d5db; } +/* ─── Password Toggle Button ────────────────────────────── */ +.password-input-wrapper { + position: relative; + width: 100%; +} + +.password-input-wrapper .field-input { + padding-right: 42px; /* Ensure input text doesn't overlap the eye button */ +} + +.password-toggle-btn { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: center; + transition: color 0.15s, opacity 0.15s; + border-radius: 4px; +} + +.password-toggle-btn:hover:not(:disabled) { + color: var(--text); +} + +.password-toggle-btn:focus-visible { + outline: 2px solid var(--green); + outline-offset: -2px; +} + +.password-toggle-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* ─── Responsive ─────────────────────────────────────────── */ @media (max-width: 600px) { .header { padding: 20px 16px; } diff --git a/todo-frontend/src/pages/LoginPage.jsx b/todo-frontend/src/pages/LoginPage.jsx index ccbaf7b..921331b 100644 --- a/todo-frontend/src/pages/LoginPage.jsx +++ b/todo-frontend/src/pages/LoginPage.jsx @@ -7,6 +7,7 @@ const isValidEmail = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); const [errors, setErrors] = useState({}); const [apiError, setApiError] = useState(''); const [loading, setLoading] = useState(false); @@ -82,16 +83,39 @@ export default function LoginPage() {
- { setPassword(e.target.value); clear('password'); }} - disabled={loading} - autoComplete="current-password" - /> +
+ { setPassword(e.target.value); clear('password'); }} + disabled={loading} + autoComplete="current-password" + /> + +
{errors.password &&

✗ {errors.password}

}
diff --git a/todo-frontend/src/pages/RegisterPage.jsx b/todo-frontend/src/pages/RegisterPage.jsx index ddb36df..1a59027 100644 --- a/todo-frontend/src/pages/RegisterPage.jsx +++ b/todo-frontend/src/pages/RegisterPage.jsx @@ -24,6 +24,8 @@ export default function RegisterPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); const [errors, setErrors] = useState({}); const [apiError, setApiError] = useState(''); const [loading, setLoading] = useState(false); @@ -129,20 +131,43 @@ export default function RegisterPage() { {/* Password */}
- { - setPassword(e.target.value); - setTouched(p => ({ ...p, password: true })); - clear('password'); - }} - disabled={loading} - autoComplete="new-password" - /> +
+ { + setPassword(e.target.value); + setTouched(p => ({ ...p, password: true })); + clear('password'); + }} + disabled={loading} + autoComplete="new-password" + /> + +
{errors.password &&

✗ {errors.password}

} {/* Strength bar — shown as soon as user starts typing */} @@ -179,16 +204,39 @@ export default function RegisterPage() { {/* Confirm password */}
- { setConfirm(e.target.value); clear('confirm'); }} - disabled={loading} - autoComplete="new-password" - /> +
+ { setConfirm(e.target.value); clear('confirm'); }} + disabled={loading} + autoComplete="new-password" + /> + +
{errors.confirm &&

✗ {errors.confirm}

}