thirukalyanamweb/src/components/auth/ForgotPassworForm.jsx

789 lines
23 KiB
JavaScript

import {
Box,
Card,
Typography,
Button,
// Link,
Container,
Dialog,
DialogContent,
Zoom,
Fade,
Stepper,
Step,
StepLabel,
IconButton,
CircularProgress,
} from '@mui/material'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import LockResetIcon from '@mui/icons-material/LockReset'
import PhoneAndroidIcon from '@mui/icons-material/PhoneAndroid'
import VpnKeyIcon from '@mui/icons-material/VpnKey'
import { keyframes } from '@mui/system'
import { useEffect, useRef, useState } from 'react'
import MuiDynamicInput from '../../utills/MuiDynamicInput'
import axiosInstance from '../../api/axiosInstance'
import toast from 'react-hot-toast'
import { Link } from 'react-router-dom'
// 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 shake = keyframes`
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
`
// 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' },
}}
>
Back to Login
</Button>
</Fade>
</DialogContent>
</Dialog>
)
}
// OTP Input Component
function OTPInput({ value, onChange, error, disabled }) {
const inputRefs = useRef([])
const otpLength = 4
const handleChange = (index, e) => {
const val = e.target.value
if (!/^\d*$/.test(val)) return
const newOtp = value.split('')
newOtp[index] = val.slice(-1)
const otpString = newOtp.join('')
onChange({ target: { name: 'otp', value: otpString } })
// Move to next input
if (val && index < otpLength - 1) {
inputRefs.current[index + 1]?.focus()
}
}
const handleKeyDown = (index, e) => {
if (e.key === 'Backspace' && !value[index] && index > 0) {
inputRefs.current[index - 1]?.focus()
}
}
const handlePaste = (e) => {
e.preventDefault()
const pastedData = e.clipboardData.getData('text').slice(0, otpLength)
if (/^\d+$/.test(pastedData)) {
onChange({ target: { name: 'otp', value: pastedData } })
const focusIndex = Math.min(pastedData.length, otpLength - 1)
inputRefs.current[focusIndex]?.focus()
}
}
return (
<Box sx={{ mb: 2 }}>
<Box
sx={{
display: 'flex',
gap: { xs: 1, sm: 1.5 },
justifyContent: 'center',
animation: error ? `${shake} 0.5s ease-out` : 'none',
}}
>
{Array.from({ length: otpLength }).map((_, index) => (
<input
key={index}
ref={(el) => (inputRefs.current[index] = el)}
type="text"
inputMode="numeric"
maxLength={1}
value={value[index] || ''}
onChange={(e) => handleChange(index, e)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={handlePaste}
disabled={disabled}
style={{
width: '45px',
height: '55px',
textAlign: 'center',
fontSize: '24px',
fontWeight: 600,
border: `2px solid ${error ? '#f44336' : value[index] ? '#4CAF50' : '#ddd'}`,
borderRadius: '12px',
outline: 'none',
transition: 'all 0.2s',
backgroundColor: disabled ? '#f5f5f5' : 'white',
}}
onFocus={(e) => {
e.target.style.borderColor = '#4CAF50'
e.target.style.boxShadow = '0 0 0 3px rgba(76, 175, 80, 0.2)'
}}
onBlur={(e) => {
e.target.style.borderColor = value[index] ? '#4CAF50' : '#ddd'
e.target.style.boxShadow = 'none'
}}
/>
))}
</Box>
{error && (
<Typography
color="error"
variant="caption"
sx={{ display: 'block', textAlign: 'center', mt: 1 }}
>
{error}
</Typography>
)}
</Box>
)
}
const ForgotPassworForm = () => {
const steps = ['Enter Mobile', 'Verify OTP', 'Reset Password']
const [activeStep, setActiveStep] = useState(0)
const [loading, setLoading] = useState(false)
const [successModal, setSuccessModal] =useState(false)
const [resendTimer, setResendTimer] = useState(0)
const [formData, setFormData] = useState({
mobile: '',
otp: '',
newPassword: '',
confirmPassword: '',
})
const [errors, setErrors] = useState({})
// Resend OTP Timer
useEffect(() => {
let interval
if (resendTimer > 0) {
interval = setInterval(() => {
setResendTimer((prev) => prev - 1)
}, 1000)
}
return () => clearInterval(interval)
}, [resendTimer])
const handleChange = (e) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: '' }))
}
}
// Step 1: Validate Mobile
const validateMobile = () => {
const newErrors = {}
if (!formData.mobile) {
newErrors.mobile = 'Mobile number is required'
} else if (!/^\d{10}$/.test(formData.mobile)) {
newErrors.mobile = 'Enter a valid 10-digit mobile number'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
// Step 2: Validate OTP
const validateOTP = () => {
const newErrors = {}
if (!formData.otp || formData.otp.length !== 4) {
newErrors.otp = 'Please enter the 6-digit OTP'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
// Step 3: Validate Password
const validatePassword = () => {
const newErrors = {}
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'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
// Submit Mobile
const handleMobileSubmit = async () => {
if (!validateMobile()) return
setLoading(true)
try {
const response = await axiosInstance.post(`/forgot_password_send_otp?mobile=${formData.mobile}`)
toast.success(response.data?.message || "OTP sent successfully")
setLoading(false)
setActiveStep(1)
setResendTimer(120) // 2 minutes timer
} catch (error) {
setLoading(false)
console.error("Forgot password error:", error)
const errorMessage = error.response?.data?.message || error.response?.data?.error || "Failed to send OTP"
toast.error(errorMessage)
}
}
// Verify OTP
const handleOTPSubmit = async () => {
if (!validateOTP()) return
setLoading(true)
try {
const response = await axiosInstance.post(`/forgot_password_verify_otp?mobile=${formData.mobile}&otp=${formData.otp}`)
toast.success(response.data?.message || "OTP Verified Successfully")
setLoading(false)
setActiveStep(2)
} catch (error) {
setLoading(false)
console.error("OTP verification error:", error)
const errorMessage = error.response?.data?.message || error.response?.data?.error || "Invalid OTP"
toast.error(errorMessage)
}
}
// Resend OTP
const handleResendOTP = () => {
handleMobileSubmit()
}
// Submit New Password
const handlePasswordSubmit = async () => {
if (!validatePassword()) return
setLoading(true)
try {
const encodedPassword = encodeURIComponent(formData.newPassword)
const response = await axiosInstance.post(`/forgot_password_update?mobile=${formData.mobile}&otp=${formData.otp}&password=${encodedPassword}`)
toast.success(response.data?.message || "Password updated successfully")
setLoading(false)
setSuccessModal(true)
} catch (error) {
setLoading(false)
console.error("Password update error:", error)
const errorMessage = error.response?.data?.message || error.response?.data?.error || "Failed to update password"
toast.error(errorMessage)
}
}
const handleBack = () => {
if (activeStep > 0) {
setActiveStep((prev) => prev - 1)
setErrors({})
}
}
const handleSuccessClose = () => {
setSuccessModal(false)
setActiveStep(0)
setFormData({ mobile: '', otp: '', newPassword: '', confirmPassword: '' })
// Navigate to login page
console.log('Navigate to login...')
}
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// Render step icon
const getStepIcon = (step) => {
switch (step) {
case 0:
return <PhoneAndroidIcon />
case 1:
return <VpnKeyIcon />
case 2:
return <LockResetIcon />
default:
return null
}
}
return (
<>
<Container maxWidth="sm" sx={{ py: 4, minHeight: '100vh' }}>
<Card
elevation={8}
sx={{
borderRadius: 4,
overflow: 'hidden',
}}
>
{/* Header */}
<Box
sx={{
background: 'linear-gradient(135deg, #034E08 0%, #034E08 100%)',
py: 3,
px: 4,
position: 'relative',
}}
>
{activeStep > 0 && (
<IconButton
onClick={handleBack}
sx={{
position: 'absolute',
left: 16,
top: '50%',
transform: 'translateY(-50%)',
color: 'white',
}}
>
<ArrowBackIcon />
</IconButton>
)}
<Typography
variant="h5"
component="h1"
sx={{
color: 'white',
fontWeight: 600,
textAlign: 'center',
}}
>
Forgot Password
</Typography>
</Box>
{/* Stepper */}
<Box sx={{ px: { xs: 2, sm: 4 }, pt: 3 }}>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label, index) => (
<Step key={label}>
<StepLabel
StepIconProps={{
sx: {
'&.Mui-active': { color: '#034E08' },
'&.Mui-completed': { color: '#034E08' },
},
}}
>
<Typography
variant="caption"
sx={{
display: { xs: 'none', sm: 'block' },
fontWeight: activeStep === index ? 600 : 400,
}}
>
{label}
</Typography>
</StepLabel>
</Step>
))}
</Stepper>
</Box>
{/* Form Content */}
<Box sx={{ p: 4 }}>
{/* Step 1: Mobile */}
{activeStep === 0 && (
<Fade in={activeStep === 0}>
<Box>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mb: 3,
}}
>
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<PhoneAndroidIcon sx={{ fontSize: 40, color: '#A70710' }} />
</Box>
</Box>
<Typography
variant="body1"
sx={{ textAlign: 'center', mb: 3, color: 'text.secondary' }}
>
Enter your mobile number and we'll send you an OTP to reset
your password.
</Typography>
<MuiDynamicInput
type="tel"
name="mobile"
label="Mobile Number"
value={formData.mobile}
onChange={handleChange}
error={errors.mobile}
inputProps={{ maxLength: 10 }}
autoFocus
required
/>
<Button
fullWidth
variant="contained"
onClick={handleMobileSubmit}
disabled={loading}
sx={{
mt: 2,
py: 1.5,
borderRadius: 50,
backgroundColor: '#A70710',
fontSize: '1rem',
fontWeight: 600,
'&:hover': { backgroundColor: '#A70710' },
'&:disabled': { backgroundColor: '#A70710', color: 'white' },
}}
>
{loading ? (
<CircularProgress size={24} sx={{ color: 'white' }} />
) : (
'Send OTP'
)}
</Button>
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Link
to="/login"
sx={{
color: '#A70710',
fontWeight: 500,
textDecoration: 'none',
'&:hover': { textDecoration: 'underline' },
}}
>
Back to Login
</Link>
</Box>
</Box>
</Fade>
)}
{/* Step 2: OTP Verification */}
{activeStep === 1 && (
<Fade in={activeStep === 1}>
<Box>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mb: 3,
}}
>
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<VpnKeyIcon sx={{ fontSize: 40, color: '#4CAF50' }} />
</Box>
</Box>
<Typography
variant="body1"
sx={{ textAlign: 'center', mb: 1, color: 'text.secondary' }}
>
We've sent a 4-digit OTP to
</Typography>
<Typography
variant="body1"
sx={{
textAlign: 'center',
mb: 3,
fontWeight: 600,
color: '#4CAF50',
}}
>
{formData.mobile}
</Typography>
<OTPInput
value={formData.otp}
onChange={handleChange}
error={errors.otp}
disabled={loading}
/>
{/* Timer and Resend */}
<Box sx={{ textAlign: 'center', mb: 3 }}>
{resendTimer > 0 ? (
<Typography variant="body2" color="text.secondary">
Resend OTP in{' '}
<Typography
component="span"
sx={{ fontWeight: 600, color: '#FF9800' }}
>
{formatTime(resendTimer)}
</Typography>
</Typography>
) : (
<Link
component="button"
onClick={handleResendOTP}
sx={{
color: '#FF9800',
fontWeight: 600,
textDecoration: 'none',
'&:hover': { textDecoration: 'underline' },
}}
>
Resend OTP
</Link>
)}
</Box>
<Button
fullWidth
variant="contained"
onClick={handleOTPSubmit}
disabled={loading || formData.otp.length !== 4}
sx={{
py: 1.5,
borderRadius: 50,
backgroundColor: '#FF9800',
fontSize: '1rem',
fontWeight: 600,
'&:hover': { backgroundColor: '#F57C00' },
'&:disabled': { backgroundColor: '#FFB74D', color: 'white' },
}}
>
{loading ? (
<CircularProgress size={24} sx={{ color: 'white' }} />
) : (
'Verify OTP'
)}
</Button>
</Box>
</Fade>
)}
{/* Step 3: Reset Password */}
{activeStep === 2 && (
<Fade in={activeStep === 2}>
<Box>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mb: 3,
}}
>
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<LockResetIcon sx={{ fontSize: 40, color: '#4CAF50' }} />
</Box>
</Box>
<Typography
variant="body1"
sx={{ textAlign: 'center', mb: 3, color: 'text.secondary' }}
>
Create a strong password with at least 8 characters including
uppercase, lowercase, and numbers.
</Typography>
<MuiDynamicInput
type="password"
name="newPassword"
label="New Password"
value={formData.newPassword}
onChange={handleChange}
error={errors.newPassword}
showPasswordToggle
required
/>
<MuiDynamicInput
type="password"
name="confirmPassword"
label="Confirm Password"
value={formData.confirmPassword}
onChange={handleChange}
error={errors.confirmPassword}
showPasswordToggle
required
/>
<Button
fullWidth
variant="contained"
onClick={handlePasswordSubmit}
disabled={loading}
sx={{
mt: 2,
py: 1.5,
borderRadius: 50,
backgroundColor: '#FF9800',
fontSize: '1rem',
fontWeight: 600,
'&:hover': { backgroundColor: '#F57C00' },
'&:disabled': { backgroundColor: '#FFB74D', color: 'white' },
}}
>
{loading ? (
<CircularProgress size={24} sx={{ color: 'white' }} />
) : (
'Reset Password'
)}
</Button>
</Box>
</Fade>
)}
</Box>
</Card>
{/* Success Modal */}
<SuccessModal
open={successModal}
onClose={handleSuccessClose}
title="Password Reset Successful!"
message="Your password has been reset successfully. You can now login with your new password."
/>
</Container>
</>
)
}
export default ForgotPassworForm