755 lines
26 KiB
JavaScript
755 lines
26 KiB
JavaScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { X, User, Mail, Shield, Calendar, Clock, Loader, AlertCircle, RefreshCw, Lock, Eye, EyeOff, CheckCircle } from 'lucide-react';
|
|
|
|
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|
|
|
// ============================================
|
|
// INLINE STYLES — Dark theme matching DESIGN_SYSTEM.md
|
|
// ============================================
|
|
const STYLES = {
|
|
overlay: {
|
|
position: 'fixed',
|
|
inset: 0,
|
|
background: 'rgba(10, 14, 39, 0.97)',
|
|
backdropFilter: 'blur(12px)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 50,
|
|
padding: '1rem',
|
|
},
|
|
panel: {
|
|
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
|
border: '1.5px solid rgba(14, 165, 233, 0.3)',
|
|
borderRadius: '0.5rem',
|
|
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.6), 0 10px 30px rgba(14, 165, 233, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
|
width: '100%',
|
|
maxWidth: '480px',
|
|
maxHeight: '90vh',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
position: 'relative',
|
|
},
|
|
header: {
|
|
padding: '1.25rem 1.5rem',
|
|
borderBottom: '1px solid rgba(14, 165, 233, 0.2)',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
flexShrink: 0,
|
|
},
|
|
headerTitle: {
|
|
color: '#F8FAFC',
|
|
fontSize: '1.25rem',
|
|
fontWeight: '600',
|
|
margin: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
},
|
|
headerIcon: {
|
|
color: '#0EA5E9',
|
|
},
|
|
closeButton: {
|
|
background: 'transparent',
|
|
border: 'none',
|
|
color: '#94A3B8',
|
|
cursor: 'pointer',
|
|
padding: '0.25rem',
|
|
borderRadius: '0.25rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
transition: 'color 0.2s',
|
|
},
|
|
body: {
|
|
padding: '1.5rem',
|
|
overflowY: 'auto',
|
|
flex: 1,
|
|
},
|
|
// Profile info section
|
|
profileSection: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '0.75rem',
|
|
},
|
|
fieldRow: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.75rem',
|
|
padding: '0.625rem 0.75rem',
|
|
background: 'rgba(15, 23, 42, 0.6)',
|
|
border: '1px solid rgba(14, 165, 233, 0.15)',
|
|
borderRadius: '0.375rem',
|
|
},
|
|
fieldIcon: {
|
|
color: '#0EA5E9',
|
|
flexShrink: 0,
|
|
},
|
|
fieldContent: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
minWidth: 0,
|
|
},
|
|
fieldLabel: {
|
|
color: '#94A3B8',
|
|
fontSize: '0.7rem',
|
|
fontWeight: '500',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.05em',
|
|
},
|
|
fieldValue: {
|
|
color: '#F8FAFC',
|
|
fontSize: '0.875rem',
|
|
fontWeight: '400',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
},
|
|
// Loading state
|
|
loadingContainer: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '3rem 1rem',
|
|
gap: '0.75rem',
|
|
},
|
|
loadingText: {
|
|
color: '#94A3B8',
|
|
fontSize: '0.875rem',
|
|
},
|
|
// Error state
|
|
errorContainer: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: '2rem 1rem',
|
|
gap: '0.75rem',
|
|
},
|
|
errorText: {
|
|
color: '#FCA5A5',
|
|
fontSize: '0.875rem',
|
|
textAlign: 'center',
|
|
},
|
|
retryButton: {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '0.375rem',
|
|
padding: '0.5rem 1rem',
|
|
background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.15) 0%, rgba(14, 165, 233, 0.1) 100%)',
|
|
border: '1px solid #0EA5E9',
|
|
borderRadius: '0.375rem',
|
|
color: '#38BDF8',
|
|
fontSize: '0.8rem',
|
|
fontWeight: '600',
|
|
cursor: 'pointer',
|
|
transition: 'all 0.2s',
|
|
},
|
|
// Separator between profile info and password form
|
|
separator: {
|
|
height: '1px',
|
|
background: 'linear-gradient(90deg, transparent, rgba(14, 165, 233, 0.3), transparent)',
|
|
margin: '1.5rem 0',
|
|
border: 'none',
|
|
},
|
|
// Password change section
|
|
passwordSection: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '0.75rem',
|
|
},
|
|
passwordHeading: {
|
|
color: '#F8FAFC',
|
|
fontSize: '1rem',
|
|
fontWeight: '600',
|
|
margin: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
},
|
|
passwordHeadingIcon: {
|
|
color: '#0EA5E9',
|
|
},
|
|
formGroup: {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '0.25rem',
|
|
},
|
|
inputLabel: {
|
|
color: '#94A3B8',
|
|
fontSize: '0.75rem',
|
|
fontWeight: '500',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.05em',
|
|
},
|
|
inputWrapper: {
|
|
position: 'relative',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
},
|
|
input: {
|
|
width: '100%',
|
|
padding: '0.625rem 0.75rem',
|
|
paddingRight: '2.5rem',
|
|
background: 'rgba(15, 23, 42, 0.6)',
|
|
border: '1px solid rgba(14, 165, 233, 0.25)',
|
|
borderRadius: '0.375rem',
|
|
color: '#F8FAFC',
|
|
fontSize: '0.875rem',
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
outline: 'none',
|
|
transition: 'border-color 0.2s, box-shadow 0.2s',
|
|
boxSizing: 'border-box',
|
|
},
|
|
inputError: {
|
|
borderColor: 'rgba(239, 68, 68, 0.5)',
|
|
},
|
|
visibilityToggle: {
|
|
position: 'absolute',
|
|
right: '0.5rem',
|
|
background: 'transparent',
|
|
border: 'none',
|
|
color: '#94A3B8',
|
|
cursor: 'pointer',
|
|
padding: '0.25rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
transition: 'color 0.2s',
|
|
},
|
|
validationError: {
|
|
color: '#FCA5A5',
|
|
fontSize: '0.75rem',
|
|
marginTop: '0.125rem',
|
|
},
|
|
submitButton: {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: '0.5rem',
|
|
padding: '0.625rem 1.25rem',
|
|
background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.15) 0%, rgba(14, 165, 233, 0.1) 100%)',
|
|
border: '1px solid #0EA5E9',
|
|
borderRadius: '0.375rem',
|
|
color: '#38BDF8',
|
|
fontSize: '0.875rem',
|
|
fontWeight: '600',
|
|
cursor: 'pointer',
|
|
transition: 'all 0.2s',
|
|
marginTop: '0.5rem',
|
|
width: '100%',
|
|
},
|
|
submitButtonDisabled: {
|
|
opacity: 0.5,
|
|
cursor: 'not-allowed',
|
|
},
|
|
changeError: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
padding: '0.625rem 0.75rem',
|
|
background: 'rgba(239, 68, 68, 0.1)',
|
|
border: '1px solid rgba(239, 68, 68, 0.3)',
|
|
borderRadius: '0.375rem',
|
|
color: '#FCA5A5',
|
|
fontSize: '0.8rem',
|
|
},
|
|
changeSuccess: {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
padding: '0.625rem 0.75rem',
|
|
background: 'rgba(16, 185, 129, 0.1)',
|
|
border: '1px solid rgba(16, 185, 129, 0.3)',
|
|
borderRadius: '0.375rem',
|
|
color: '#6EE7B7',
|
|
fontSize: '0.8rem',
|
|
},
|
|
// Group badge
|
|
groupBadge: (group) => {
|
|
const colors = {
|
|
Admin: { bg: 'rgba(239, 68, 68, 0.2)', border: 'rgba(239, 68, 68, 0.5)', text: '#FCA5A5' },
|
|
Standard_User: { bg: 'rgba(14, 165, 233, 0.2)', border: 'rgba(14, 165, 233, 0.5)', text: '#7DD3FC' },
|
|
Leadership: { bg: 'rgba(168, 85, 247, 0.2)', border: 'rgba(168, 85, 247, 0.5)', text: '#D8B4FE' },
|
|
Read_Only: { bg: 'rgba(148, 163, 184, 0.2)', border: 'rgba(148, 163, 184, 0.5)', text: '#CBD5E1' },
|
|
};
|
|
const c = colors[group] || colors.Read_Only;
|
|
return {
|
|
display: 'inline-block',
|
|
padding: '0.125rem 0.5rem',
|
|
background: c.bg,
|
|
border: `1px solid ${c.border}`,
|
|
borderRadius: '0.25rem',
|
|
color: c.text,
|
|
fontSize: '0.8rem',
|
|
fontWeight: '500',
|
|
};
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Format a date string into a user-friendly format.
|
|
* e.g. "Jan 15, 2026 at 10:30 AM"
|
|
*/
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return 'Never';
|
|
try {
|
|
const date = new Date(dateStr);
|
|
if (isNaN(date.getTime())) return 'Unknown';
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
}) + ' at ' + date.toLocaleTimeString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true,
|
|
});
|
|
} catch {
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
function formatGroupName(group) {
|
|
if (!group) return '';
|
|
return group.replace(/_/g, ' ');
|
|
}
|
|
|
|
export default function UserProfilePanel({ isOpen, onClose }) {
|
|
const [profile, setProfile] = useState(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
// Password change form state
|
|
const [currentPassword, setCurrentPassword] = useState('');
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [changeLoading, setChangeLoading] = useState(false);
|
|
const [changeError, setChangeError] = useState(null);
|
|
const [changeSuccess, setChangeSuccess] = useState(null);
|
|
// Password visibility toggles
|
|
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
|
|
const [showNewPassword, setShowNewPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const panelRef = useRef(null);
|
|
|
|
const fetchProfile = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/profile`, {
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.error || `Failed to fetch profile (${response.status})`);
|
|
}
|
|
const data = await response.json();
|
|
setProfile(data);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Client-side validation for the password change form.
|
|
* Returns an object with field-specific error messages, or null if valid.
|
|
*/
|
|
function validatePasswordForm() {
|
|
const errors = {};
|
|
if (newPassword.length > 0 && newPassword.length < 8) {
|
|
errors.newPassword = 'Password must be at least 8 characters';
|
|
}
|
|
if (confirmPassword.length > 0 && newPassword !== confirmPassword) {
|
|
errors.confirmPassword = 'Passwords do not match';
|
|
}
|
|
return Object.keys(errors).length > 0 ? errors : null;
|
|
}
|
|
|
|
const validationErrors = validatePasswordForm();
|
|
|
|
/**
|
|
* Returns true if the form can be submitted:
|
|
* all fields filled, no validation errors, not currently loading.
|
|
*/
|
|
function canSubmitPasswordForm() {
|
|
return (
|
|
currentPassword.length > 0 &&
|
|
newPassword.length >= 8 &&
|
|
confirmPassword.length > 0 &&
|
|
newPassword === confirmPassword &&
|
|
!changeLoading
|
|
);
|
|
}
|
|
|
|
async function handlePasswordChange(e) {
|
|
e.preventDefault();
|
|
// Final client-side validation guard
|
|
if (!canSubmitPasswordForm()) return;
|
|
|
|
setChangeLoading(true);
|
|
setChangeError(null);
|
|
setChangeSuccess(null);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/change-password`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ currentPassword, newPassword }),
|
|
});
|
|
|
|
const data = await response.json().catch(() => ({}));
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
throw new Error(data.error || 'Current password is incorrect');
|
|
} else if (response.status === 429) {
|
|
throw new Error(data.error || 'Too many attempts. Please try again later.');
|
|
} else if (response.status === 400) {
|
|
throw new Error(data.error || 'Validation error');
|
|
} else {
|
|
throw new Error(data.error || 'An error occurred. Please try again.');
|
|
}
|
|
}
|
|
|
|
// Success — clear form and show message
|
|
setCurrentPassword('');
|
|
setNewPassword('');
|
|
setConfirmPassword('');
|
|
setShowCurrentPassword(false);
|
|
setShowNewPassword(false);
|
|
setShowConfirmPassword(false);
|
|
setChangeSuccess(data.message || 'Password changed successfully');
|
|
} catch (err) {
|
|
setChangeError(err.message);
|
|
} finally {
|
|
setChangeLoading(false);
|
|
}
|
|
}
|
|
|
|
// Fetch profile when modal opens
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
fetchProfile();
|
|
} else {
|
|
// Reset state when closed
|
|
setProfile(null);
|
|
setError(null);
|
|
setCurrentPassword('');
|
|
setNewPassword('');
|
|
setConfirmPassword('');
|
|
setChangeLoading(false);
|
|
setChangeError(null);
|
|
setChangeSuccess(null);
|
|
setShowCurrentPassword(false);
|
|
setShowNewPassword(false);
|
|
setShowConfirmPassword(false);
|
|
}
|
|
}, [isOpen, fetchProfile]);
|
|
|
|
// Click-outside-to-close
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
function handleClickOutside(event) {
|
|
if (panelRef.current && !panelRef.current.contains(event.target)) {
|
|
onClose();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, [isOpen, onClose]);
|
|
|
|
// Escape key to close
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
function handleKeyDown(event) {
|
|
if (event.key === 'Escape') {
|
|
onClose();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, onClose]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div style={STYLES.overlay}>
|
|
<div ref={panelRef} style={STYLES.panel}>
|
|
{/* Header */}
|
|
<div style={STYLES.header}>
|
|
<h2 style={STYLES.headerTitle}>
|
|
<User style={STYLES.headerIcon} size={20} />
|
|
My Profile
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
style={STYLES.closeButton}
|
|
onMouseEnter={(e) => { e.currentTarget.style.color = '#F8FAFC'; }}
|
|
onMouseLeave={(e) => { e.currentTarget.style.color = '#94A3B8'; }}
|
|
aria-label="Close profile panel"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div style={STYLES.body}>
|
|
{/* Loading state */}
|
|
{loading && (
|
|
<div style={STYLES.loadingContainer}>
|
|
<Loader size={28} color="#0EA5E9" style={{ animation: 'spin 1s linear infinite' }} />
|
|
<span style={STYLES.loadingText}>Loading profile...</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error state */}
|
|
{!loading && error && (
|
|
<div style={STYLES.errorContainer}>
|
|
<AlertCircle size={32} color="#EF4444" />
|
|
<span style={STYLES.errorText}>{error}</span>
|
|
<button
|
|
onClick={fetchProfile}
|
|
style={STYLES.retryButton}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.25) 0%, rgba(14, 165, 233, 0.2) 100%)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.15) 0%, rgba(14, 165, 233, 0.1) 100%)';
|
|
}}
|
|
>
|
|
<RefreshCw size={14} />
|
|
Retry
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Profile info section */}
|
|
{!loading && !error && profile && (
|
|
<div style={STYLES.profileSection}>
|
|
{/* Username */}
|
|
<div style={STYLES.fieldRow}>
|
|
<User size={18} style={STYLES.fieldIcon} />
|
|
<div style={STYLES.fieldContent}>
|
|
<span style={STYLES.fieldLabel}>Username</span>
|
|
<span style={STYLES.fieldValue}>{profile.username}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Email */}
|
|
<div style={STYLES.fieldRow}>
|
|
<Mail size={18} style={STYLES.fieldIcon} />
|
|
<div style={STYLES.fieldContent}>
|
|
<span style={STYLES.fieldLabel}>Email</span>
|
|
<span style={STYLES.fieldValue}>{profile.email}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Group */}
|
|
<div style={STYLES.fieldRow}>
|
|
<Shield size={18} style={STYLES.fieldIcon} />
|
|
<div style={STYLES.fieldContent}>
|
|
<span style={STYLES.fieldLabel}>Group</span>
|
|
<span style={{ ...STYLES.fieldValue, display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
<span style={STYLES.groupBadge(profile.group)}>
|
|
{formatGroupName(profile.group)}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Created At */}
|
|
<div style={STYLES.fieldRow}>
|
|
<Calendar size={18} style={STYLES.fieldIcon} />
|
|
<div style={STYLES.fieldContent}>
|
|
<span style={STYLES.fieldLabel}>Account Created</span>
|
|
<span style={STYLES.fieldValue}>{formatDate(profile.created_at)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Last Login */}
|
|
<div style={STYLES.fieldRow}>
|
|
<Clock size={18} style={STYLES.fieldIcon} />
|
|
<div style={STYLES.fieldContent}>
|
|
<span style={STYLES.fieldLabel}>Last Login</span>
|
|
<span style={STYLES.fieldValue}>{formatDate(profile.last_login)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Password change section — shown when profile is loaded */}
|
|
{!loading && !error && profile && (
|
|
<>
|
|
<hr style={STYLES.separator} />
|
|
<div style={STYLES.passwordSection}>
|
|
<h3 style={STYLES.passwordHeading}>
|
|
<Lock size={18} style={STYLES.passwordHeadingIcon} />
|
|
Change Password
|
|
</h3>
|
|
|
|
{/* Success message */}
|
|
{changeSuccess && (
|
|
<div style={STYLES.changeSuccess}>
|
|
<CheckCircle size={16} />
|
|
{changeSuccess}
|
|
</div>
|
|
)}
|
|
|
|
{/* API error message */}
|
|
{changeError && (
|
|
<div style={STYLES.changeError}>
|
|
<AlertCircle size={16} />
|
|
{changeError}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handlePasswordChange} autoComplete="off">
|
|
{/* Current Password */}
|
|
<div style={{ ...STYLES.formGroup, marginBottom: '0.75rem' }}>
|
|
<label style={STYLES.inputLabel}>Current Password</label>
|
|
<div style={STYLES.inputWrapper}>
|
|
<input
|
|
type={showCurrentPassword ? 'text' : 'password'}
|
|
value={currentPassword}
|
|
onChange={(e) => { setCurrentPassword(e.target.value); setChangeError(null); }}
|
|
style={STYLES.input}
|
|
onFocus={(e) => { e.currentTarget.style.borderColor = '#0EA5E9'; e.currentTarget.style.boxShadow = '0 0 0 2px rgba(14, 165, 233, 0.15)'; }}
|
|
onBlur={(e) => { e.currentTarget.style.borderColor = 'rgba(14, 165, 233, 0.25)'; e.currentTarget.style.boxShadow = 'none'; }}
|
|
placeholder="Enter current password"
|
|
autoComplete="current-password"
|
|
/>
|
|
<button
|
|
type="button"
|
|
style={STYLES.visibilityToggle}
|
|
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
|
onMouseEnter={(e) => { e.currentTarget.style.color = '#F8FAFC'; }}
|
|
onMouseLeave={(e) => { e.currentTarget.style.color = '#94A3B8'; }}
|
|
aria-label={showCurrentPassword ? 'Hide current password' : 'Show current password'}
|
|
tabIndex={-1}
|
|
>
|
|
{showCurrentPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* New Password */}
|
|
<div style={{ ...STYLES.formGroup, marginBottom: '0.75rem' }}>
|
|
<label style={STYLES.inputLabel}>New Password</label>
|
|
<div style={STYLES.inputWrapper}>
|
|
<input
|
|
type={showNewPassword ? 'text' : 'password'}
|
|
value={newPassword}
|
|
onChange={(e) => { setNewPassword(e.target.value); setChangeError(null); }}
|
|
style={{
|
|
...STYLES.input,
|
|
...(validationErrors?.newPassword ? STYLES.inputError : {}),
|
|
}}
|
|
onFocus={(e) => { e.currentTarget.style.borderColor = validationErrors?.newPassword ? 'rgba(239, 68, 68, 0.5)' : '#0EA5E9'; e.currentTarget.style.boxShadow = '0 0 0 2px rgba(14, 165, 233, 0.15)'; }}
|
|
onBlur={(e) => { e.currentTarget.style.borderColor = validationErrors?.newPassword ? 'rgba(239, 68, 68, 0.5)' : 'rgba(14, 165, 233, 0.25)'; e.currentTarget.style.boxShadow = 'none'; }}
|
|
placeholder="Minimum 8 characters"
|
|
autoComplete="new-password"
|
|
/>
|
|
<button
|
|
type="button"
|
|
style={STYLES.visibilityToggle}
|
|
onClick={() => setShowNewPassword(!showNewPassword)}
|
|
onMouseEnter={(e) => { e.currentTarget.style.color = '#F8FAFC'; }}
|
|
onMouseLeave={(e) => { e.currentTarget.style.color = '#94A3B8'; }}
|
|
aria-label={showNewPassword ? 'Hide new password' : 'Show new password'}
|
|
tabIndex={-1}
|
|
>
|
|
{showNewPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
</div>
|
|
{validationErrors?.newPassword && (
|
|
<span style={STYLES.validationError}>{validationErrors.newPassword}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Confirm New Password */}
|
|
<div style={{ ...STYLES.formGroup, marginBottom: '0.75rem' }}>
|
|
<label style={STYLES.inputLabel}>Confirm New Password</label>
|
|
<div style={STYLES.inputWrapper}>
|
|
<input
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
value={confirmPassword}
|
|
onChange={(e) => { setConfirmPassword(e.target.value); setChangeError(null); }}
|
|
style={{
|
|
...STYLES.input,
|
|
...(validationErrors?.confirmPassword ? STYLES.inputError : {}),
|
|
}}
|
|
onFocus={(e) => { e.currentTarget.style.borderColor = validationErrors?.confirmPassword ? 'rgba(239, 68, 68, 0.5)' : '#0EA5E9'; e.currentTarget.style.boxShadow = '0 0 0 2px rgba(14, 165, 233, 0.15)'; }}
|
|
onBlur={(e) => { e.currentTarget.style.borderColor = validationErrors?.confirmPassword ? 'rgba(239, 68, 68, 0.5)' : 'rgba(14, 165, 233, 0.25)'; e.currentTarget.style.boxShadow = 'none'; }}
|
|
placeholder="Re-enter new password"
|
|
autoComplete="new-password"
|
|
/>
|
|
<button
|
|
type="button"
|
|
style={STYLES.visibilityToggle}
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
onMouseEnter={(e) => { e.currentTarget.style.color = '#F8FAFC'; }}
|
|
onMouseLeave={(e) => { e.currentTarget.style.color = '#94A3B8'; }}
|
|
aria-label={showConfirmPassword ? 'Hide confirm password' : 'Show confirm password'}
|
|
tabIndex={-1}
|
|
>
|
|
{showConfirmPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
</button>
|
|
</div>
|
|
{validationErrors?.confirmPassword && (
|
|
<span style={STYLES.validationError}>{validationErrors.confirmPassword}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Submit button */}
|
|
<button
|
|
type="submit"
|
|
disabled={!canSubmitPasswordForm()}
|
|
style={{
|
|
...STYLES.submitButton,
|
|
...(!canSubmitPasswordForm() ? STYLES.submitButtonDisabled : {}),
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (canSubmitPasswordForm()) {
|
|
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.25) 0%, rgba(14, 165, 233, 0.2) 100%)';
|
|
e.currentTarget.style.boxShadow = '0 0 20px rgba(14, 165, 233, 0.25)';
|
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(14, 165, 233, 0.15) 0%, rgba(14, 165, 233, 0.1) 100%)';
|
|
e.currentTarget.style.boxShadow = 'none';
|
|
e.currentTarget.style.transform = 'none';
|
|
}}
|
|
>
|
|
{changeLoading ? (
|
|
<>
|
|
<Loader size={16} style={{ animation: 'spin 1s linear infinite' }} />
|
|
Changing Password...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Lock size={16} />
|
|
Change Password
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|