613 lines
17 KiB
JavaScript
613 lines
17 KiB
JavaScript
import * as React from 'react'
|
|
import {
|
|
Box,
|
|
Card,
|
|
Typography,
|
|
Button,
|
|
Container,
|
|
Dialog,
|
|
DialogContent,
|
|
Zoom,
|
|
Fade,
|
|
LinearProgress,
|
|
CircularProgress,
|
|
IconButton,
|
|
} from '@mui/material'
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
|
import LockIcon from '@mui/icons-material/Lock'
|
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
|
|
import CheckIcon from '@mui/icons-material/Check'
|
|
import CloseIcon from '@mui/icons-material/Close'
|
|
import { keyframes } from '@mui/system'
|
|
import MuiDynamicInput from '../../utills/MuiDynamicInput'
|
|
|
|
// Keyframe animations
|
|
const scaleIn = keyframes`
|
|
0% { transform: scale(0); opacity: 0; }
|
|
50% { transform: scale(1.2); }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
`
|
|
|
|
const pulse = keyframes`
|
|
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
|
|
70% { box-shadow: 0 0 0 20px rgba(76, 175, 80, 0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
|
|
`
|
|
|
|
const fadeInUp = keyframes`
|
|
0% { opacity: 0; transform: translateY(20px); }
|
|
100% { opacity: 1; transform: translateY(0); }
|
|
`
|
|
|
|
const float = keyframes`
|
|
0%, 100% { transform: translateY(0px); }
|
|
50% { transform: translateY(-20px); }
|
|
`
|
|
|
|
// Success Modal Component
|
|
function SuccessModal({ open, onClose, title, message }) {
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onClose={onClose}
|
|
TransitionComponent={Zoom}
|
|
TransitionProps={{ timeout: 400 }}
|
|
PaperProps={{
|
|
sx: {
|
|
borderRadius: 4,
|
|
minWidth: { xs: 280, sm: 400 },
|
|
overflow: 'visible',
|
|
},
|
|
}}
|
|
>
|
|
<DialogContent
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
py: 5,
|
|
px: 4,
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
width: 100,
|
|
height: 100,
|
|
borderRadius: '50%',
|
|
backgroundColor: 'success.light',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
animation: `${scaleIn} 0.5s ease-out, ${pulse} 1.5s ease-out 0.5s`,
|
|
mb: 3,
|
|
}}
|
|
>
|
|
<CheckCircleIcon
|
|
sx={{
|
|
fontSize: 60,
|
|
color: 'success.main',
|
|
animation: `${scaleIn} 0.6s ease-out 0.2s both`,
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
<Fade in={open} timeout={800}>
|
|
<Typography
|
|
variant="h5"
|
|
sx={{
|
|
fontWeight: 700,
|
|
color: 'success.main',
|
|
mb: 1,
|
|
animation: `${fadeInUp} 0.5s ease-out 0.3s both`,
|
|
}}
|
|
>
|
|
{title}
|
|
</Typography>
|
|
</Fade>
|
|
|
|
<Fade in={open} timeout={1000}>
|
|
<Typography
|
|
variant="body1"
|
|
sx={{
|
|
color: 'text.secondary',
|
|
mb: 3,
|
|
animation: `${fadeInUp} 0.5s ease-out 0.5s both`,
|
|
}}
|
|
>
|
|
{message}
|
|
</Typography>
|
|
</Fade>
|
|
|
|
<Fade in={open} timeout={1200}>
|
|
<Button
|
|
variant="contained"
|
|
onClick={onClose}
|
|
sx={{
|
|
mt: 1,
|
|
px: 5,
|
|
py: 1.5,
|
|
borderRadius: 50,
|
|
backgroundColor: 'success.main',
|
|
fontWeight: 600,
|
|
textTransform: 'none',
|
|
fontSize: '1rem',
|
|
animation: `${fadeInUp} 0.5s ease-out 0.7s both`,
|
|
'&:hover': { backgroundColor: 'success.dark' },
|
|
}}
|
|
>
|
|
Continue
|
|
</Button>
|
|
</Fade>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
// Password Strength Indicator
|
|
function PasswordStrength({ password }) {
|
|
const getStrength = () => {
|
|
let strength = 0
|
|
if (password.length >= 8) strength += 25
|
|
if (/[a-z]/.test(password)) strength += 25
|
|
if (/[A-Z]/.test(password)) strength += 25
|
|
if (/\d/.test(password)) strength += 15
|
|
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength += 10
|
|
return Math.min(strength, 100)
|
|
}
|
|
|
|
const strength = getStrength()
|
|
|
|
const getColor = () => {
|
|
if (strength < 30) return 'error'
|
|
if (strength < 60) return 'warning'
|
|
if (strength < 80) return 'info'
|
|
return 'success'
|
|
}
|
|
|
|
const getLabel = () => {
|
|
if (strength < 30) return 'Weak'
|
|
if (strength < 60) return 'Fair'
|
|
if (strength < 80) return 'Good'
|
|
return 'Strong'
|
|
}
|
|
|
|
if (!password) return null
|
|
|
|
return (
|
|
<Box sx={{ mt: 1, mb: 2 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Password Strength
|
|
</Typography>
|
|
<Typography variant="caption" color={getColor() + '.main'} fontWeight={600}>
|
|
{getLabel()}
|
|
</Typography>
|
|
</Box>
|
|
<LinearProgress
|
|
variant="determinate"
|
|
value={strength}
|
|
color={getColor()}
|
|
sx={{ height: 6, borderRadius: 3 }}
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
// Password Requirements Component
|
|
function PasswordRequirements({ password }) {
|
|
const requirements = [
|
|
{ label: 'At least 8 characters', met: password.length >= 8 },
|
|
{ label: 'One lowercase letter', met: /[a-z]/.test(password) },
|
|
{ label: 'One uppercase letter', met: /[A-Z]/.test(password) },
|
|
{ label: 'One number', met: /\d/.test(password) },
|
|
{ label: 'One special character', met: /[!@#$%^&*(),.?":{}|<>]/.test(password) },
|
|
]
|
|
|
|
return (
|
|
<Box sx={{ mb: 2 }}>
|
|
{requirements.map((req, index) => (
|
|
<Box
|
|
key={index}
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
py: 0.3,
|
|
}}
|
|
>
|
|
{req.met ? (
|
|
<CheckIcon sx={{ fontSize: 16, color: 'success.main' }} />
|
|
) : (
|
|
<CloseIcon sx={{ fontSize: 16, color: 'text.disabled' }} />
|
|
)}
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
color: req.met ? 'success.main' : 'text.disabled',
|
|
transition: 'color 0.3s',
|
|
}}
|
|
>
|
|
{req.label}
|
|
</Typography>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const ChangePasswordPage = () => {
|
|
const [loading, setLoading] = React.useState(false)
|
|
const [successModal, setSuccessModal] = React.useState(false)
|
|
|
|
const [formData, setFormData] = React.useState({
|
|
currentPassword: '',
|
|
newPassword: '',
|
|
confirmPassword: '',
|
|
})
|
|
|
|
const [errors, setErrors] = React.useState({})
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value } = e.target
|
|
setFormData((prev) => ({ ...prev, [name]: value }))
|
|
if (errors[name]) {
|
|
setErrors((prev) => ({ ...prev, [name]: '' }))
|
|
}
|
|
}
|
|
|
|
const validateForm = () => {
|
|
const newErrors = {}
|
|
|
|
if (!formData.currentPassword) {
|
|
newErrors.currentPassword = 'Current password is required'
|
|
}
|
|
|
|
if (!formData.newPassword) {
|
|
newErrors.newPassword = 'New password is required'
|
|
} else if (formData.newPassword.length < 8) {
|
|
newErrors.newPassword = 'Password must be at least 8 characters'
|
|
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.newPassword)) {
|
|
newErrors.newPassword = 'Must include uppercase, lowercase, and number'
|
|
}
|
|
|
|
if (!formData.confirmPassword) {
|
|
newErrors.confirmPassword = 'Please confirm your password'
|
|
} else if (formData.newPassword !== formData.confirmPassword) {
|
|
newErrors.confirmPassword = 'Passwords do not match'
|
|
}
|
|
|
|
if (formData.currentPassword === formData.newPassword) {
|
|
newErrors.newPassword = 'New password must be different from current'
|
|
}
|
|
|
|
setErrors(newErrors)
|
|
return Object.keys(newErrors).length === 0
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
if (!validateForm()) return
|
|
|
|
setLoading(true)
|
|
setTimeout(() => {
|
|
setLoading(false)
|
|
setSuccessModal(true)
|
|
console.log('Change Password Data:', {
|
|
currentPassword: formData.currentPassword,
|
|
newPassword: formData.newPassword,
|
|
})
|
|
}, 1500)
|
|
}
|
|
|
|
const handleSuccessClose = () => {
|
|
setSuccessModal(false)
|
|
setFormData({ currentPassword: '', newPassword: '', confirmPassword: '' })
|
|
console.log('Navigate to profile or dashboard...')
|
|
}
|
|
|
|
const handleBack = () => {
|
|
console.log('Navigate back...')
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
backgroundColor: '#f5f5f5',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
py: 4,
|
|
}}
|
|
>
|
|
<Container maxWidth="lg">
|
|
<Card
|
|
elevation={12}
|
|
sx={{
|
|
borderRadius: 4,
|
|
overflow: 'hidden',
|
|
display: 'flex',
|
|
flexDirection: { xs: 'column', md: 'row' },
|
|
minHeight: { md: 600 },
|
|
}}
|
|
>
|
|
{/* Left Side - Image */}
|
|
<Box
|
|
sx={{
|
|
flex: { md: 1 },
|
|
background: 'linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%)',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
p: { xs: 4, md: 6 },
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
minHeight: { xs: 250, md: 'auto' },
|
|
}}
|
|
>
|
|
{/* Background Decorations */}
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: -50,
|
|
left: -50,
|
|
width: 200,
|
|
height: 200,
|
|
borderRadius: '50%',
|
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
bottom: -80,
|
|
right: -80,
|
|
width: 300,
|
|
height: 300,
|
|
borderRadius: '50%',
|
|
backgroundColor: 'rgba(255,255,255,0.05)',
|
|
}}
|
|
/>
|
|
|
|
{/* Lock Icon with Animation */}
|
|
<Box
|
|
sx={{
|
|
width: 120,
|
|
height: 120,
|
|
borderRadius: '50%',
|
|
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
mb: 4,
|
|
animation: `${float} 3s ease-in-out infinite`,
|
|
}}
|
|
>
|
|
<LockIcon sx={{ fontSize: 60, color: 'white' }} />
|
|
</Box>
|
|
|
|
<Typography
|
|
variant="h4"
|
|
sx={{
|
|
color: 'white',
|
|
fontWeight: 700,
|
|
textAlign: 'center',
|
|
mb: 2,
|
|
}}
|
|
>
|
|
Secure Your Account
|
|
</Typography>
|
|
|
|
<Typography
|
|
variant="body1"
|
|
sx={{
|
|
color: 'rgba(255,255,255,0.9)',
|
|
textAlign: 'center',
|
|
maxWidth: 350,
|
|
lineHeight: 1.8,
|
|
}}
|
|
>
|
|
Keep your account safe by regularly updating your password. Choose
|
|
a strong password that you don't use elsewhere.
|
|
</Typography>
|
|
|
|
{/* Security Tips */}
|
|
<Box
|
|
sx={{
|
|
mt: 4,
|
|
p: 2,
|
|
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
borderRadius: 2,
|
|
maxWidth: 350,
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="subtitle2"
|
|
sx={{ color: 'white', fontWeight: 600, mb: 1 }}
|
|
>
|
|
Security Tips:
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: 'rgba(255,255,255,0.8)', display: 'block' }}
|
|
>
|
|
• Use a mix of letters, numbers & symbols
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: 'rgba(255,255,255,0.8)', display: 'block' }}
|
|
>
|
|
• Avoid personal information
|
|
</Typography>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: 'rgba(255,255,255,0.8)', display: 'block' }}
|
|
>
|
|
• Don't reuse passwords from other sites
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Right Side - Form */}
|
|
<Box
|
|
sx={{
|
|
flex: { md: 1 },
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
p: { xs: 3, sm: 4, md: 6 },
|
|
backgroundColor: 'white',
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
|
|
<IconButton onClick={handleBack} sx={{ mr: 1 }}>
|
|
<ArrowBackIcon />
|
|
</IconButton>
|
|
<Typography variant="h5" sx={{ fontWeight: 700, color: '#333' }}>
|
|
Change Password
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: 'text.secondary', mb: 4 }}
|
|
>
|
|
Your password must be different from previously used passwords.
|
|
</Typography>
|
|
|
|
{/* Form Fields */}
|
|
<Box sx={{ flex: 1 }}>
|
|
<MuiDynamicInput
|
|
type="password"
|
|
name="currentPassword"
|
|
label="Current Password"
|
|
value={formData.currentPassword}
|
|
onChange={handleChange}
|
|
error={errors.currentPassword}
|
|
showPasswordToggle
|
|
required
|
|
/>
|
|
|
|
<MuiDynamicInput
|
|
type="password"
|
|
name="newPassword"
|
|
label="New Password"
|
|
value={formData.newPassword}
|
|
onChange={handleChange}
|
|
error={errors.newPassword}
|
|
showPasswordToggle
|
|
required
|
|
/>
|
|
|
|
{/* Password Strength Indicator */}
|
|
<PasswordStrength password={formData.newPassword} />
|
|
|
|
{/* Password Requirements */}
|
|
<PasswordRequirements password={formData.newPassword} />
|
|
|
|
<MuiDynamicInput
|
|
type="password"
|
|
name="confirmPassword"
|
|
label="Confirm New Password"
|
|
value={formData.confirmPassword}
|
|
onChange={handleChange}
|
|
error={errors.confirmPassword}
|
|
showPasswordToggle
|
|
required
|
|
/>
|
|
|
|
{/* Match Indicator */}
|
|
{formData.confirmPassword && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1,
|
|
mt: -1,
|
|
mb: 2,
|
|
}}
|
|
>
|
|
{formData.newPassword === formData.confirmPassword ? (
|
|
<>
|
|
<CheckIcon sx={{ fontSize: 16, color: 'success.main' }} />
|
|
<Typography variant="caption" color="success.main">
|
|
Passwords match
|
|
</Typography>
|
|
</>
|
|
) : (
|
|
<>
|
|
<CloseIcon sx={{ fontSize: 16, color: 'error.main' }} />
|
|
<Typography variant="caption" color="error.main">
|
|
Passwords do not match
|
|
</Typography>
|
|
</>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Submit Button */}
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
onClick={handleSubmit}
|
|
disabled={loading}
|
|
sx={{
|
|
mt: 3,
|
|
py: 1.5,
|
|
borderRadius: 50,
|
|
backgroundColor: '#FF9800',
|
|
fontSize: '1rem',
|
|
fontWeight: 600,
|
|
textTransform: 'uppercase',
|
|
boxShadow: '0 4px 12px rgba(255, 152, 0, 0.4)',
|
|
'&:hover': {
|
|
backgroundColor: '#F57C00',
|
|
boxShadow: '0 6px 16px rgba(255, 152, 0, 0.5)',
|
|
},
|
|
'&:disabled': {
|
|
backgroundColor: '#FFB74D',
|
|
color: 'white',
|
|
},
|
|
}}
|
|
>
|
|
{loading ? (
|
|
<CircularProgress size={24} sx={{ color: 'white' }} />
|
|
) : (
|
|
'Update Password'
|
|
)}
|
|
</Button>
|
|
|
|
{/* Cancel Link */}
|
|
<Button
|
|
fullWidth
|
|
variant="text"
|
|
onClick={handleBack}
|
|
sx={{
|
|
mt: 2,
|
|
color: 'text.secondary',
|
|
textTransform: 'none',
|
|
'&:hover': {
|
|
backgroundColor: 'transparent',
|
|
color: '#FF9800',
|
|
},
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</Box>
|
|
</Card>
|
|
</Container>
|
|
|
|
{/* Success Modal */}
|
|
<SuccessModal
|
|
open={successModal}
|
|
onClose={handleSuccessClose}
|
|
title="Password Changed!"
|
|
message="Your password has been updated successfully. Please use your new password for future logins."
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
export default ChangePasswordPage |