feat: add reusable PasswordInput with show/hide toggle
Replaces all type="password" inputs site-wide with a PasswordInput component that includes an eye icon toggle for visibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
32
frontend/src/components/common/PasswordInput.tsx
Normal file
32
frontend/src/components/common/PasswordInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface PasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PasswordInput({ className, ...props }: PasswordInputProps) {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
type={visible ? 'text' : 'password'}
|
||||||
|
className={cn(className, 'pr-10')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setVisible((v) => !v)}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-white/40 hover:bg-white/10 hover:text-white"
|
||||||
|
tabIndex={-1}
|
||||||
|
title={visible ? 'Hide password' : 'Show password'}
|
||||||
|
>
|
||||||
|
{visible ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordInput
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { IntakeFormField } from '@/types'
|
import type { IntakeFormField } from '@/types'
|
||||||
|
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface IntakeFormModalProps {
|
interface IntakeFormModalProps {
|
||||||
@@ -119,8 +120,7 @@ export function IntakeFormModal({ isOpen, fields, treeName, onSubmit, onCancel }
|
|||||||
|
|
||||||
case 'password':
|
case 'password':
|
||||||
input = (
|
input = (
|
||||||
<input
|
<PasswordInput
|
||||||
type="password"
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(field.variable_name, e.target.value)}
|
onChange={(e) => setValue(field.variable_name, e.target.value)}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from '@/store/authStore'
|
||||||
import { authApi } from '@/api/auth'
|
import { authApi } from '@/api/auth'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
|
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -87,9 +88,8 @@ export function ChangePasswordPage() {
|
|||||||
<label htmlFor="current-password" className="mb-1 block text-sm font-medium text-white">
|
<label htmlFor="current-password" className="mb-1 block text-sm font-medium text-white">
|
||||||
Current Password
|
Current Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="current-password"
|
id="current-password"
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
value={currentPassword}
|
value={currentPassword}
|
||||||
@@ -107,9 +107,8 @@ export function ChangePasswordPage() {
|
|||||||
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-white">
|
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-white">
|
||||||
New Password
|
New Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="new-password"
|
id="new-password"
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
@@ -131,9 +130,8 @@ export function ChangePasswordPage() {
|
|||||||
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-white">
|
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-white">
|
||||||
Confirm New Password
|
Confirm New Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|||||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from '@/store/authStore'
|
||||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||||
|
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
@@ -95,10 +96,9 @@ export function LoginPage() {
|
|||||||
<label htmlFor="password" className="mb-1 block text-sm font-medium text-white">
|
<label htmlFor="password" className="mb-1 block text-sm font-medium text-white">
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'
|
|||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from '@/store/authStore'
|
||||||
import { inviteApi } from '@/api/invite'
|
import { inviteApi } from '@/api/invite'
|
||||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||||
|
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function RegisterPage() {
|
export function RegisterPage() {
|
||||||
@@ -187,10 +188,9 @@ export function RegisterPage() {
|
|||||||
<label htmlFor="password" className="block text-sm font-medium text-white">
|
<label htmlFor="password" className="block text-sm font-medium text-white">
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
@@ -211,10 +211,9 @@ export function RegisterPage() {
|
|||||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-white">
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-white">
|
||||||
Confirm password
|
Confirm password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Link, useSearchParams, useNavigate } from 'react-router-dom'
|
|||||||
import { authApi } from '@/api/auth'
|
import { authApi } from '@/api/auth'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||||
|
import { PasswordInput } from '@/components/common/PasswordInput'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function ResetPasswordPage() {
|
export function ResetPasswordPage() {
|
||||||
@@ -123,9 +124,8 @@ export function ResetPasswordPage() {
|
|||||||
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-white">
|
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-white">
|
||||||
New Password
|
New Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="new-password"
|
id="new-password"
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
@@ -147,9 +147,8 @@ export function ResetPasswordPage() {
|
|||||||
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-white">
|
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-white">
|
||||||
Confirm New Password
|
Confirm New Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<PasswordInput
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
|
|||||||
Reference in New Issue
Block a user