form
This commit is contained in:
parent
c873c7cd56
commit
1573a267e6
@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<title>thirukalyanam</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.8",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/styled-engine-sc": "^7.3.5",
|
||||
@ -1181,6 +1182,24 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lottiefiles/dotlottie-react": {
|
||||
"version": "0.17.8",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.17.8.tgz",
|
||||
"integrity": "sha512-Hk0bISNURSqL7t+H7S5lW2NQVa1hiibnqRRg6kOWZpswBxfQk+/6WBPc9EfuetdoZmiMoDsmcI0HR4I20oTBRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lottiefiles/dotlottie-web": "0.57.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@lottiefiles/dotlottie-web": {
|
||||
"version": "0.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.57.0.tgz",
|
||||
"integrity": "sha512-gcgvu9T21YzeY3JjHCZrxftucsxzMH6e9h+8NMv8mbfo1y1M9/jdcsdu40S+pnSLz9/OyiSBQ/EjDsbSOHZy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.17",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.8",
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/styled-engine-sc": "^7.3.5",
|
||||
|
||||
5
src/assets/images/appstore-badge.svg
Normal file
5
src/assets/images/appstore-badge.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
40
src/assets/images/googleplay-badge.svg
Normal file
40
src/assets/images/googleplay-badge.svg
Normal file
@ -0,0 +1,40 @@
|
||||
<svg width="193" height="57" viewBox="0 0 193 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.466 56.366H7.849a7.126 7.126 0 0 1-5.013-2.063A7.066 7.066 0 0 1 .74 49.322V7.046A7.058 7.058 0 0 1 2.833 2.06 7.118 7.118 0 0 1 7.849 0h177.617a7.117 7.117 0 0 1 5.015 2.06 7.054 7.054 0 0 1 2.092 4.986v42.275a7.066 7.066 0 0 1-2.095 4.982 7.128 7.128 0 0 1-5.012 2.063z" fill="#000"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.466 1.127c1.578 0 3.093.623 4.212 1.73a5.935 5.935 0 0 1 1.763 4.185v42.28a5.926 5.926 0 0 1-1.759 4.187 5.976 5.976 0 0 1-4.216 1.727H7.849a5.988 5.988 0 0 1-4.213-1.73 5.937 5.937 0 0 1-1.763-4.185V7.046a5.925 5.925 0 0 1 1.758-4.19A5.976 5.976 0 0 1 7.85 1.127h177.617zm0-1.127H7.849c-1.88 0-3.682.743-5.014 2.063A7.066 7.066 0 0 0 .741 7.046v42.275a7.057 7.057 0 0 0 2.092 4.986 7.118 7.118 0 0 0 5.016 2.06h177.617a7.117 7.117 0 0 0 5.015-2.06 7.054 7.054 0 0 0 2.092-4.986V7.046a7.071 7.071 0 0 0-2.094-4.982A7.13 7.13 0 0 0 185.466 0z" fill="#A6A6A6"/>
|
||||
<path d="M101.794 18.647a4.323 4.323 0 0 1-3.215-1.328 4.509 4.509 0 0 1-1.298-3.233 4.396 4.396 0 0 1 1.298-3.229 4.295 4.295 0 0 1 3.212-1.328 4.363 4.363 0 0 1 3.215 1.328 4.667 4.667 0 0 1 0 6.46 4.279 4.279 0 0 1-3.212 1.33zm-38.117 0a4.483 4.483 0 0 1-3.23-1.308 4.536 4.536 0 0 1-1.021-4.983 4.558 4.558 0 0 1 1.694-2.055 4.588 4.588 0 0 1 2.557-.763c.62-.001 1.234.126 1.803.373.528.214.998.548 1.372.976l.085.102-.956.934-.1-.12a2.72 2.72 0 0 0-2.22-.953 3.077 3.077 0 0 0-2.212.9 3.407 3.407 0 0 0 0 4.68 3.241 3.241 0 0 0 4.485 0c.377-.401.606-.918.65-1.465h-3.067V13.65h4.384l.018.121c.036.218.057.439.063.66a3.942 3.942 0 0 1-1.091 2.917 4.27 4.27 0 0 1-3.209 1.299h-.005zm50.681-.186h-1.351l-4.14-6.596.035 1.188v5.404h-1.352V9.725h1.542l.043.066 3.892 6.213-.035-1.184V9.725h1.366v8.736zm-22.72 0h-1.36v-7.422h-2.375V9.725h6.116v1.314h-2.375v7.422h-.006zm-4.86 0h-1.366V9.725h1.366v8.736zm-7.676 0h-1.366v-7.422h-2.369V9.725h6.116v1.314h-2.375l-.006 7.422zm-4.607-.015h-5.237V9.725h5.237v1.314H70.64v2.396h3.49v1.3h-3.49v2.397h3.869l-.013 1.314zm25.085-2.028a3.027 3.027 0 0 0 2.212.914 2.958 2.958 0 0 0 2.212-.914 3.45 3.45 0 0 0 0-4.652 3.031 3.031 0 0 0-2.212-.914 2.941 2.941 0 0 0-2.209.914 3.46 3.46 0 0 0-.007 4.652h.004z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M97.16 30.658a6.038 6.038 0 0 0-3.339 1.019 6 6 0 0 0-2.21 2.694 5.971 5.971 0 0 0 1.317 6.526 6.044 6.044 0 0 0 6.558 1.291 6.016 6.016 0 0 0 2.7-2.207 5.982 5.982 0 0 0 1.013-3.329 5.877 5.877 0 0 0-1.742-4.273 5.923 5.923 0 0 0-4.296-1.721zm0 9.614a3.672 3.672 0 0 1-2.1-.482 3.65 3.65 0 0 1-1.463-1.577c-.318-.66-.428-1.4-.318-2.123.111-.723.438-1.397.939-1.933a3.663 3.663 0 0 1 4.017-.909 3.656 3.656 0 0 1 1.685 1.34 3.63 3.63 0 0 1 .635 2.051 3.455 3.455 0 0 1-.93 2.527 3.484 3.484 0 0 1-2.464 1.106zm-13.173-9.614a6.038 6.038 0 0 0-3.34 1.019 6 6 0 0 0-2.209 2.694 5.972 5.972 0 0 0 1.317 6.526 6.044 6.044 0 0 0 6.558 1.291 6.015 6.015 0 0 0 2.699-2.207 5.976 5.976 0 0 0 1.013-3.329 5.88 5.88 0 0 0-1.742-4.275 5.929 5.929 0 0 0-4.3-1.72h.004zm0 9.614a3.671 3.671 0 0 1-2.1-.482 3.65 3.65 0 0 1-1.464-1.577 3.63 3.63 0 0 1 .62-4.056 3.663 3.663 0 0 1 4.019-.909 3.657 3.657 0 0 1 1.684 1.34c.414.604.635 1.32.635 2.051a3.456 3.456 0 0 1-.93 2.529 3.486 3.486 0 0 1-2.468 1.103l.004.001zm-15.678-7.784v2.535h6.116a5.337 5.337 0 0 1-1.383 3.194 6.241 6.241 0 0 1-4.719 1.862 6.808 6.808 0 0 1-4.803-1.981 6.75 6.75 0 0 1-1.99-4.783 6.75 6.75 0 0 1 1.99-4.784 6.809 6.809 0 0 1 4.803-1.981 6.543 6.543 0 0 1 4.607 1.816l1.807-1.8a8.892 8.892 0 0 0-6.398-2.568 9.347 9.347 0 0 0-6.704 2.67 9.27 9.27 0 0 0 0 13.262 9.377 9.377 0 0 0 6.704 2.67 8.583 8.583 0 0 0 6.525-2.613 8.396 8.396 0 0 0 2.215-5.95c.007-.53-.04-1.06-.14-1.58h-8.63v.03zm64.12 1.974a5.574 5.574 0 0 0-1.972-2.72 5.612 5.612 0 0 0-3.184-1.099 5.738 5.738 0 0 0-4.125 1.789 5.692 5.692 0 0 0-1.549 4.206 5.83 5.83 0 0 0 1.706 4.263 5.89 5.89 0 0 0 4.269 1.731 6.001 6.001 0 0 0 2.845-.696 5.97 5.97 0 0 0 2.17-1.96l-2.044-1.363a3.414 3.414 0 0 1-1.257 1.22 3.436 3.436 0 0 1-1.7.438 3.082 3.082 0 0 1-2.925-1.816l8.05-3.321-.286-.672h.002zm-8.21 2.003a3.283 3.283 0 0 1 .838-2.403 3.306 3.306 0 0 1 2.307-1.09 2.315 2.315 0 0 1 2.234 1.269l-5.379 2.224zm-6.538 5.809h2.642V24.662h-2.642v17.612zm-4.342-10.286h-.095a4.178 4.178 0 0 0-3.176-1.346 6.03 6.03 0 0 0-4.1 1.837 5.982 5.982 0 0 0-1.679 4.153c0 1.548.602 3.036 1.679 4.153a6.03 6.03 0 0 0 4.1 1.837 4.173 4.173 0 0 0 3.176-1.365h.095v.861c0 2.294-1.228 3.521-3.207 3.521a3.348 3.348 0 0 1-1.84-.607 3.329 3.329 0 0 1-1.191-1.522l-2.303.956a5.723 5.723 0 0 0 2.125 2.579c.95.63 2.067.965 3.209.96 3.098 0 5.724-1.81 5.724-6.248V31.003h-2.498v.987l-.019-.002zm-3.03 8.283a3.666 3.666 0 0 1-2.38-1.169 3.635 3.635 0 0 1 0-4.925 3.666 3.666 0 0 1 2.38-1.17 3.4 3.4 0 0 1 2.381 1.147 3.356 3.356 0 0 1 .826 2.502 3.307 3.307 0 0 1-.826 2.495 3.35 3.35 0 0 1-2.387 1.12h.006zm34.5-15.61h-6.321v17.614h2.643v-6.67h3.68a5.522 5.522 0 0 0 4.08-1.494 5.48 5.48 0 0 0 1.717-3.977 5.48 5.48 0 0 0-3.603-5.142 5.52 5.52 0 0 0-2.194-.329l-.002-.001zm.077 8.484h-3.758v-6.044h3.758a3.04 3.04 0 0 1 2.146.886 3.016 3.016 0 0 1 0 4.273 3.041 3.041 0 0 1-2.146.886zm16.322-2.519a4.995 4.995 0 0 0-2.761.648 4.967 4.967 0 0 0-1.957 2.044l2.343.966a2.523 2.523 0 0 1 2.407-1.3 2.56 2.56 0 0 1 1.864.562c.523.425.855 1.04.924 1.708v.186a5.94 5.94 0 0 0-2.753-.674c-2.53 0-5.092 1.377-5.092 3.96a4.061 4.061 0 0 0 1.368 2.866 4.092 4.092 0 0 0 3.018 1.017 3.718 3.718 0 0 0 3.363-1.721h.095v1.363h2.545v-6.747c-.009-3.14-2.353-4.879-5.37-4.879l.006.001zm-.332 9.646c-.864 0-2.076-.42-2.076-1.504 0-1.364 1.511-1.88 2.8-1.88a4.603 4.603 0 0 1 2.406.595 3.166 3.166 0 0 1-1.039 1.983c-.577.513-1.322.8-2.096.805l.005.001zm14.987-9.27-3.031 7.64h-.095l-3.145-7.64h-2.848l4.719 10.68-2.689 5.935h2.753l7.259-16.615h-2.923zM152.07 42.275h2.643V24.662h-2.643v17.613z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.507 10.616a2.847 2.847 0 0 0-.646 1.989v31.16a2.71 2.71 0 0 0 .66 1.97l.109.096 17.536-17.459v-.39l-17.55-17.46-.109.094z" fill="url(#3w6b1qcb1a)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m38.996 34.209-5.848-5.823v-.408l5.848-5.822.122.077 6.917 3.915c1.98 1.11 1.98 2.944 0 4.069l-6.917 3.915-.122.077z" fill="url(#mrtz4zu0db)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m39.123 34.132-5.974-5.949L15.507 45.75a2.322 2.322 0 0 0 2.94.096l20.676-11.71" fill="url(#fdv0z8t5qc)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.123 22.233 18.447 10.538a2.3 2.3 0 0 0-2.94.096l17.642 17.549 5.974-5.95z" fill="url(#trpcf7zoxd)"/>
|
||||
<path opacity=".2" fill-rule="evenodd" clip-rule="evenodd" d="M38.997 34.006 18.461 45.624a2.336 2.336 0 0 1-2.83.014l-.11.109.11.095a2.342 2.342 0 0 0 2.83-.014l20.677-11.695-.141-.127z" fill="#000"/>
|
||||
<path opacity=".12" fill-rule="evenodd" clip-rule="evenodd" d="m46.04 30.014-7.062 3.992.122.12 6.917-3.914a2.477 2.477 0 0 0 1.479-2.035 2.53 2.53 0 0 1-1.456 1.837z" fill="#000"/>
|
||||
<path opacity=".25" fill-rule="evenodd" clip-rule="evenodd" d="M18.447 10.742 46.041 26.35a2.599 2.599 0 0 1 1.479 1.832 2.462 2.462 0 0 0-1.48-2.035l-27.593-15.61c-1.98-1.127-3.586-.186-3.586 2.067v.204c0-2.255 1.605-3.18 3.586-2.066z" fill="#fff"/>
|
||||
<defs>
|
||||
<linearGradient id="3w6b1qcb1a" x1="60.122" y1="47.067" x2="57.983" y2="46.767" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00A0FF"/>
|
||||
<stop offset=".007" stop-color="#00A1FF"/>
|
||||
<stop offset=".26" stop-color="#00BEFF"/>
|
||||
<stop offset=".512" stop-color="#00D2FF"/>
|
||||
<stop offset=".76" stop-color="#00DFFF"/>
|
||||
<stop offset="1" stop-color="#00E3FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="mrtz4zu0db" x1="61.172" y1="44.804" x2="57.435" y2="44.804" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFE000"/>
|
||||
<stop offset=".409" stop-color="#FFBD00"/>
|
||||
<stop offset=".775" stop-color="orange"/>
|
||||
<stop offset="1" stop-color="#FF9C00"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fdv0z8t5qc" x1="60.991" y1="45.344" x2="59.571" y2="42.236" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF3A44"/>
|
||||
<stop offset="1" stop-color="#C31162"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="trpcf7zoxd" x1="59.338" y1="47.999" x2="59.979" y2="46.614" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#32A071"/>
|
||||
<stop offset=".068" stop-color="#2DA771"/>
|
||||
<stop offset=".476" stop-color="#15CF74"/>
|
||||
<stop offset=".801" stop-color="#06E775"/>
|
||||
<stop offset="1" stop-color="#00F076"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src/assets/images/phone-left.avif
Normal file
BIN
src/assets/images/phone-left.avif
Normal file
Binary file not shown.
BIN
src/assets/images/success.gif
Normal file
BIN
src/assets/images/success.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 MiB |
613
src/components/auth/ChangePasswordForm.jsx
Normal file
613
src/components/auth/ChangePasswordForm.jsx
Normal file
@ -0,0 +1,613 @@
|
||||
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
|
||||
772
src/components/auth/ForgotPassworForm.jsx
Normal file
772
src/components/auth/ForgotPassworForm.jsx
Normal file
@ -0,0 +1,772 @@
|
||||
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 EmailIcon from '@mui/icons-material/Email'
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey'
|
||||
import { keyframes } from '@mui/system'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
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 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 = 6
|
||||
|
||||
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 Email', '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({
|
||||
email: '',
|
||||
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 Email
|
||||
const validateEmail = () => {
|
||||
const newErrors = {}
|
||||
if (!formData.email) {
|
||||
newErrors.email = 'Email is required'
|
||||
} else if (!/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/.test(formData.email)) {
|
||||
newErrors.email = 'Enter a valid email address'
|
||||
}
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
// Step 2: Validate OTP
|
||||
const validateOTP = () => {
|
||||
const newErrors = {}
|
||||
if (!formData.otp || formData.otp.length !== 6) {
|
||||
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 Email
|
||||
const handleEmailSubmit = async () => {
|
||||
if (!validateEmail()) return
|
||||
|
||||
setLoading(true)
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
setActiveStep(1)
|
||||
setResendTimer(120) // 2 minutes timer
|
||||
console.log('OTP sent to:', formData.email)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// Verify OTP
|
||||
const handleOTPSubmit = async () => {
|
||||
if (!validateOTP()) return
|
||||
|
||||
setLoading(true)
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
setActiveStep(2)
|
||||
console.log('OTP Verified:', formData.otp)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// Resend OTP
|
||||
const handleResendOTP = () => {
|
||||
setResendTimer(120)
|
||||
setFormData((prev) => ({ ...prev, otp: '' }))
|
||||
console.log('OTP Resent to:', formData.email)
|
||||
}
|
||||
|
||||
// Submit New Password
|
||||
const handlePasswordSubmit = async () => {
|
||||
if (!validatePassword()) return
|
||||
|
||||
setLoading(true)
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
setSuccessModal(true)
|
||||
console.log('Password Reset Data:', {
|
||||
email: formData.email,
|
||||
otp: formData.otp,
|
||||
newPassword: formData.newPassword,
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
if (activeStep > 0) {
|
||||
setActiveStep((prev) => prev - 1)
|
||||
setErrors({})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSuccessClose = () => {
|
||||
setSuccessModal(false)
|
||||
setActiveStep(0)
|
||||
setFormData({ email: '', 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 <EmailIcon />
|
||||
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, #4CAF50 0%, #45a049 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: '#4CAF50' },
|
||||
'&.Mui-completed': { color: '#4CAF50' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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: Email */}
|
||||
{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',
|
||||
}}
|
||||
>
|
||||
<EmailIcon sx={{ fontSize: 40, color: '#4CAF50' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ textAlign: 'center', mb: 3, color: 'text.secondary' }}
|
||||
>
|
||||
Enter your email address and we'll send you an OTP to reset
|
||||
your password.
|
||||
</Typography>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="email"
|
||||
name="email"
|
||||
label="Email Address"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={handleEmailSubmit}
|
||||
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' }} />
|
||||
) : (
|
||||
'Send OTP'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ mt: 3, textAlign: 'center' }}>
|
||||
<Link
|
||||
href="/login"
|
||||
sx={{
|
||||
color: '#FF9800',
|
||||
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 6-digit OTP to
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
mb: 3,
|
||||
fontWeight: 600,
|
||||
color: '#4CAF50',
|
||||
}}
|
||||
>
|
||||
{formData.email}
|
||||
</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 !== 6}
|
||||
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
|
||||
412
src/components/auth/LoginPanel.jsx
Normal file
412
src/components/auth/LoginPanel.jsx
Normal file
@ -0,0 +1,412 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Typography,
|
||||
Button,
|
||||
Link,
|
||||
Divider,
|
||||
Container,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
Zoom,
|
||||
Fade,
|
||||
} from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
import MuiDynamicInput from '../../utills/MuiDynamicInput.jsx'
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||
import { keyframes } from '@mui/system'
|
||||
|
||||
|
||||
// Define keyframe animations
|
||||
const scaleIn = keyframes`
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const checkmarkDraw = keyframes`
|
||||
0% {
|
||||
stroke-dashoffset: 100;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
`
|
||||
|
||||
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);
|
||||
}
|
||||
`
|
||||
|
||||
// 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',
|
||||
}}
|
||||
>
|
||||
{/* Animated Success Icon */}
|
||||
<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>
|
||||
|
||||
{/* Success Title */}
|
||||
<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 || 'Success!'}
|
||||
</Typography>
|
||||
</Fade>
|
||||
|
||||
{/* Success Message */}
|
||||
<Fade in={open} timeout={1000}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
mb: 3,
|
||||
animation: `${fadeInUp} 0.5s ease-out 0.5s both`,
|
||||
}}
|
||||
>
|
||||
{message || 'Your action was completed successfully.'}
|
||||
</Typography>
|
||||
</Fade>
|
||||
|
||||
{/* Continue Button */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
const LoginPanel = () => {
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
emailOrMobile: '',
|
||||
password: '',
|
||||
keepLoggedIn: false,
|
||||
})
|
||||
|
||||
const [errors, setErrors] =useState({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [successModal, setSuccessModal] = useState(false)
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||
// Clear error when user types
|
||||
if (errors[name]) {
|
||||
setErrors((prev) => ({ ...prev, [name]: '' }))
|
||||
}
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {}
|
||||
|
||||
if (!formData.emailOrMobile) {
|
||||
newErrors.emailOrMobile = 'Email or Mobile Number is required'
|
||||
} else if (
|
||||
!/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(formData.emailOrMobile) &&
|
||||
!/^\d{10}$/.test(formData.emailOrMobile)
|
||||
) {
|
||||
newErrors.emailOrMobile = 'Enter a valid email or 10-digit mobile number'
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required'
|
||||
} else if (formData.password.length < 6) {
|
||||
newErrors.password = 'Password must be at least 6 characters'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
setLoading(true)
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
setSuccessModal(true) // Show success modal
|
||||
console.log('Form Data:', formData)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleCloseSuccessModal = () => {
|
||||
setSuccessModal(false)
|
||||
// Reset form after successful login
|
||||
setFormData({
|
||||
emailOrMobile: '',
|
||||
password: '',
|
||||
keepLoggedIn: false,
|
||||
})
|
||||
// Navigate to dashboard or next page
|
||||
console.log('Navigate to dashboard...')
|
||||
}
|
||||
|
||||
const handleOTPLogin = () => {
|
||||
console.log('Navigate to OTP Login')
|
||||
}
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
console.log('Navigate to Forgot Password')
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Container maxWidth="sm" sx={{ py: 4 }}>
|
||||
<Card
|
||||
elevation={8}
|
||||
sx={{
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, #034E08 0%, #034E08 100%)',
|
||||
py: 3,
|
||||
px: 4,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h1"
|
||||
sx={{
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Member Login
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Form Content */}
|
||||
<Box sx={{ p: 4 }}>
|
||||
{/* Email or Mobile Input */}
|
||||
<MuiDynamicInput
|
||||
type="text"
|
||||
name="emailOrMobile"
|
||||
label="E-mail or Mobile Number"
|
||||
value={formData.emailOrMobile}
|
||||
onChange={handleChange}
|
||||
error={errors.emailOrMobile}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Password Input */}
|
||||
<MuiDynamicInput
|
||||
type="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
error={errors.password}
|
||||
showPasswordToggle
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Keep me logged in Checkbox */}
|
||||
<MuiDynamicInput
|
||||
type="checkbox"
|
||||
name="keepLoggedIn"
|
||||
label="Keep me logged in"
|
||||
value={formData.keepLoggedIn}
|
||||
onChange={handleChange}
|
||||
color="success"
|
||||
/>
|
||||
|
||||
{/* Login Button */}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
mt: 2,
|
||||
py: 1.5,
|
||||
borderRadius: 50,
|
||||
backgroundColor: '#A70710',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
boxShadow: '0 4px 12px rgba(255, 0, 0, 0.4)',
|
||||
'&:hover': {
|
||||
backgroundColor: '#A70710',
|
||||
boxShadow: '0 6px 16px rgba(255, 0, 0, 0.5)',
|
||||
},
|
||||
'&:disabled': {
|
||||
backgroundColor: '#923237ff',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'LOGIN'}
|
||||
</Button>
|
||||
|
||||
{/* Trouble logging in section */}
|
||||
<Box sx={{ mt: 4, textAlign: 'center' }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ fontWeight: 600, color: 'text.primary', mb: 1 }}
|
||||
>
|
||||
Trouble logging in?
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
onClick={handleOTPLogin}
|
||||
sx={{
|
||||
color: '#A70710',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
color: '#A70710',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Login with OTP
|
||||
</Link>
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
||||
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
onClick={handleForgotPassword}
|
||||
sx={{
|
||||
color: '#A70710',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
color: '#A70710',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
{/* Animated Success Modal */}
|
||||
<SuccessModal
|
||||
open={successModal}
|
||||
onClose={handleCloseSuccessModal}
|
||||
title="Login Successful!"
|
||||
message="Welcome back! You have been logged in successfully."
|
||||
/>
|
||||
|
||||
</Container>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPanel
|
||||
85
src/components/auth/PromoPanel.jsx
Normal file
85
src/components/auth/PromoPanel.jsx
Normal file
@ -0,0 +1,85 @@
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import Rating from '@mui/material/Rating'
|
||||
import phoneImage from '../../assets/images/phone-left.avif'
|
||||
import appstoreBadge from '../../assets/images/appstore-badge.svg'
|
||||
import googleplayBadge from '../../assets/images/googleplay-badge.svg'
|
||||
|
||||
const PromoPanel = () => {
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="relative p-4"
|
||||
>
|
||||
|
||||
|
||||
<h2 className="text-2xl lg:text-3xl font-semibold text-gray-900 leading-tight">
|
||||
To speed up your partner search, download <span className='text-[#A70710]'>Thirukalyanam App</span>
|
||||
</h2>
|
||||
|
||||
<div style={{boxShadow:"0px 6px 20px 2px #f1f1f1"}} className='mt-6 grid grid-cols-1 md:grid-cols-2 gap-2 pb-4 rounded-[15px] bg-[ffff] border border-1 border-[#f1f1f1] '>
|
||||
|
||||
|
||||
<div className='w-[260px] h-[100%]] md:h-[290px] overflow-show'>
|
||||
<div className="relative mt-8 lg:mt-10">
|
||||
<img
|
||||
src={phoneImage}
|
||||
alt="TamilMatrimony App preview on mobile"
|
||||
className="w-full max-w-md sm:max-w-lg lg:max-w-xl drop-shadow-xl mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className='p-4'>
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<a href="#" aria-label="Download on the App Store">
|
||||
<img
|
||||
src={appstoreBadge}
|
||||
alt="Download on the App Store"
|
||||
className="h-10 w-auto hover:scale-[1.02] transition-transform"
|
||||
/>
|
||||
</a>
|
||||
<a href="#" aria-label="Get it on Google Play">
|
||||
<img
|
||||
src={googleplayBadge}
|
||||
alt="Get it on Google Play"
|
||||
className="h-10 w-auto hover:scale-[1.02] transition-transform"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col gap-2">
|
||||
|
||||
<span className="text-[18px] font-semibold text-gray-800">ThirukalyanamMatrimony®</span>
|
||||
<span className="text-[16px] text-gray-600">Trusted Matrimony App</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Rating value={4.3} precision={0.1} readOnly />
|
||||
<span className="text-sm text-gray-700">4.3</span>
|
||||
|
||||
</div>
|
||||
|
||||
<span className=" text-[14px] text-gray-500">10M+ Downloads | Based on Customer Reviews</span>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</motion.div>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PromoPanel
|
||||
26
src/pages/auth/ForgotPasswordPage.jsx
Normal file
26
src/pages/auth/ForgotPasswordPage.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import PromoPanel from '../../components/auth/PromoPanel'
|
||||
import ForgotPassworForm from '../../components/auth/ForgotPassworForm'
|
||||
|
||||
const ForgotPasswordPage = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="h-full max-h-dvh w-full max-w-[1100px] mx-auto">
|
||||
|
||||
<div class="my-6 grid grid-cols-1 md:grid-cols-[60%_40%] gap-2 ">
|
||||
<div class="">
|
||||
{/* Left: Promo */}
|
||||
<PromoPanel />
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
{/* Right: Login */}
|
||||
<ForgotPassworForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPasswordPage
|
||||
25
src/pages/auth/LoginPage.jsx
Normal file
25
src/pages/auth/LoginPage.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import LoginPanel from "../../components/auth/LoginPanel"
|
||||
import PromoPanel from "../../components/auth/PromoPanel"
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="h-full max-h-dvh w-full max-w-[1100px] mx-auto">
|
||||
|
||||
<div class="my-6 grid grid-cols-1 md:grid-cols-[60%_40%] gap-2 ">
|
||||
<div class="">
|
||||
{/* Left: Promo */}
|
||||
<PromoPanel />
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
{/* Right: Login */}
|
||||
<LoginPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
@ -1,13 +1,21 @@
|
||||
import { Route } from "react-router-dom";
|
||||
import HomePage from "../pages/HomePage";
|
||||
import LandingLayout from "../layout/LandingLayout";
|
||||
import LoginPage from "../pages/auth/LoginPage";
|
||||
import ForgotPasswordPage from "../pages/auth/ForgotPasswordPage";
|
||||
import ChangePasswordPage from "../components/auth/ChangePasswordForm";
|
||||
const PublicRoutes = () => {
|
||||
return (
|
||||
<>
|
||||
<Route element={<LandingLayout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
|
||||
</Route>
|
||||
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
||||
<Route path="/change-password" element={<ChangePasswordPage />} />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
703
src/utills/MuiDynamicInput.jsx
Normal file
703
src/utills/MuiDynamicInput.jsx
Normal file
@ -0,0 +1,703 @@
|
||||
import {
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
Select,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
FormGroup,
|
||||
Chip,
|
||||
Box,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Switch,
|
||||
Slider,
|
||||
Autocomplete,
|
||||
Rating,
|
||||
} from '@mui/material'
|
||||
import Visibility from '@mui/icons-material/Visibility'
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* MuiDynamicInput - A comprehensive reusable input component
|
||||
*
|
||||
* Supported types:
|
||||
* - text, email, number, tel, url (standard text inputs)
|
||||
* - password (with optional show/hide toggle)
|
||||
* - select (single selection dropdown)
|
||||
* - multiselect (multiple selection dropdown)
|
||||
* - tags (chip-style multi-select)
|
||||
* - radio (radio button group)
|
||||
* - checkbox (checkbox group or single)
|
||||
* - switch (toggle switch)
|
||||
* - date, time, datetime-local (date/time pickers)
|
||||
* - textarea (multi-line text)
|
||||
* - file (file upload)
|
||||
* - slider (range slider)
|
||||
* - autocomplete (searchable dropdown)
|
||||
* - rating (star rating)
|
||||
*/
|
||||
|
||||
export default function MuiDynamicInput({
|
||||
type = 'text',
|
||||
name = '',
|
||||
label = '',
|
||||
value,
|
||||
onChange,
|
||||
options = [],
|
||||
error = '',
|
||||
helperText = '',
|
||||
fullWidth = true,
|
||||
autoFocus = false,
|
||||
showPasswordToggle = true,
|
||||
placeholder = '',
|
||||
disabled = false,
|
||||
required = false,
|
||||
size = 'medium', // 'small' | 'medium'
|
||||
variant = 'outlined', // 'outlined' | 'filled' | 'standard'
|
||||
margin = 'normal', // 'none' | 'dense' | 'normal'
|
||||
multiline = false,
|
||||
rows = 4,
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
accept = '*', // for file input
|
||||
multiple = false, // for file input
|
||||
color = 'primary', // 'primary' | 'secondary' | 'success' | 'error' | 'warning'
|
||||
sx = {},
|
||||
}) {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [fileName, setFileName] = useState('')
|
||||
|
||||
const handleTogglePassword = () => setShowPassword((prev) => !prev)
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const files = event.target.files
|
||||
if (files && files.length > 0) {
|
||||
const names = Array.from(files).map(f => f.name).join(', ')
|
||||
setFileName(names)
|
||||
}
|
||||
onChange && onChange(event)
|
||||
}
|
||||
|
||||
// Common props for TextField-based inputs
|
||||
const commonTextFieldProps = {
|
||||
name,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
error: !!error,
|
||||
helperText: error || helperText,
|
||||
fullWidth,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
disabled,
|
||||
required,
|
||||
size,
|
||||
variant,
|
||||
margin,
|
||||
color,
|
||||
sx,
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
// ==================== PASSWORD ====================
|
||||
case 'password':
|
||||
return (
|
||||
<FormControl
|
||||
variant={variant}
|
||||
fullWidth={fullWidth}
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
size={size}
|
||||
sx={sx}
|
||||
>
|
||||
<InputLabel color={color}>{label}</InputLabel>
|
||||
<OutlinedInput
|
||||
name={name}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
autoFocus={autoFocus}
|
||||
placeholder={placeholder}
|
||||
color={color}
|
||||
endAdornment={
|
||||
showPasswordToggle && (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleTogglePassword}
|
||||
edge="end"
|
||||
disabled={disabled}
|
||||
sx={{ color: '#034E08' }}
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== SELECT ====================
|
||||
case 'select':
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth={fullWidth}
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
size={size}
|
||||
variant={variant}
|
||||
sx={sx}
|
||||
>
|
||||
<InputLabel color={color}>{label}</InputLabel>
|
||||
<Select
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={label}
|
||||
color={color}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== MULTISELECT ====================
|
||||
case 'multiselect':
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth={fullWidth}
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
size={size}
|
||||
variant={variant}
|
||||
sx={sx}
|
||||
>
|
||||
<InputLabel color={color}>{label}</InputLabel>
|
||||
<Select
|
||||
name={name}
|
||||
multiple
|
||||
value={value || []}
|
||||
onChange={onChange}
|
||||
label={label}
|
||||
color={color}
|
||||
renderValue={(selected) => selected.map(v =>
|
||||
options.find(o => o.value === v)?.label || v
|
||||
).join(', ')}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
<Checkbox checked={(value || []).includes(opt.value)} color={color} />
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== TAGS (Chips) ====================
|
||||
case 'tags':
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth={fullWidth}
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
size={size}
|
||||
variant={variant}
|
||||
sx={sx}
|
||||
>
|
||||
<InputLabel color={color}>{label}</InputLabel>
|
||||
<Select
|
||||
name={name}
|
||||
multiple
|
||||
value={value || []}
|
||||
onChange={onChange}
|
||||
label={label}
|
||||
color={color}
|
||||
renderValue={(selected) => (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{selected.map((val) => (
|
||||
<Chip
|
||||
key={val}
|
||||
label={options.find(o => o.value === val)?.label || val}
|
||||
size="small"
|
||||
color={color}
|
||||
onDelete={disabled ? undefined : () => {
|
||||
const newValue = (value || []).filter(v => v !== val)
|
||||
onChange({ target: { name, value: newValue } })
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
<Checkbox checked={(value || []).includes(opt.value)} color={color} />
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== RADIO ====================
|
||||
case 'radio':
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
fullWidth={fullWidth}
|
||||
sx={sx}
|
||||
>
|
||||
<FormLabel component="legend" color={color}>{label}</FormLabel>
|
||||
<RadioGroup
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
row={options.length <= 4}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<FormControlLabel
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
control={<Radio color={color} size={size} />}
|
||||
label={opt.label}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== CHECKBOX ====================
|
||||
case 'checkbox':
|
||||
// Single checkbox (boolean value)
|
||||
if (options.length === 0) {
|
||||
return (
|
||||
<FormControl
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
sx={sx}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name={name}
|
||||
checked={!!value}
|
||||
onChange={(e) => onChange({
|
||||
target: { name, value: e.target.checked }
|
||||
})}
|
||||
color={color}
|
||||
size={size}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
// Multiple checkboxes (array value)
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
fullWidth={fullWidth}
|
||||
sx={sx}
|
||||
>
|
||||
<FormLabel component="legend" color={color}>{label}</FormLabel>
|
||||
<FormGroup row={options.length <= 4}>
|
||||
{options.map((opt) => (
|
||||
<FormControlLabel
|
||||
key={opt.value}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={(value || []).includes(opt.value)}
|
||||
onChange={(e) => {
|
||||
const currentValue = value || []
|
||||
const newValue = e.target.checked
|
||||
? [...currentValue, opt.value]
|
||||
: currentValue.filter((v) => v !== opt.value)
|
||||
onChange({ target: { name, value: newValue } })
|
||||
}}
|
||||
color={color}
|
||||
size={size}
|
||||
/>
|
||||
}
|
||||
label={opt.label}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== SWITCH ====================
|
||||
case 'switch':
|
||||
return (
|
||||
<FormControl
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
sx={sx}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name={name}
|
||||
checked={!!value}
|
||||
onChange={(e) => onChange({
|
||||
target: { name, value: e.target.checked }
|
||||
})}
|
||||
color={color}
|
||||
size={size}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== DATE / TIME / DATETIME ====================
|
||||
case 'date':
|
||||
case 'time':
|
||||
case 'datetime-local':
|
||||
return (
|
||||
<TextField
|
||||
{...commonTextFieldProps}
|
||||
type={type}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
)
|
||||
|
||||
// ==================== TEXTAREA ====================
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextField
|
||||
{...commonTextFieldProps}
|
||||
multiline
|
||||
rows={rows}
|
||||
/>
|
||||
)
|
||||
|
||||
// ==================== FILE ====================
|
||||
case 'file':
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth={fullWidth}
|
||||
margin={margin}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
sx={sx}
|
||||
>
|
||||
<OutlinedInput
|
||||
name={name}
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
disabled={disabled}
|
||||
inputProps={{ accept, multiple }}
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<CloudUploadIcon color={error ? 'error' : color} />
|
||||
</InputAdornment>
|
||||
}
|
||||
sx={{
|
||||
'& input::file-selector-button': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FormHelperText>
|
||||
{error || helperText || (fileName ? `Selected: ${fileName}` : 'Choose file(s)')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== SLIDER ====================
|
||||
case 'slider':
|
||||
return (
|
||||
<FormControl
|
||||
fullWidth={fullWidth}
|
||||
margin={margin}
|
||||
disabled={disabled}
|
||||
sx={sx}
|
||||
>
|
||||
<FormLabel color={color}>{label}</FormLabel>
|
||||
<Slider
|
||||
name={name}
|
||||
value={value || min}
|
||||
onChange={(_, newValue) => onChange({
|
||||
target: { name, value: newValue }
|
||||
})}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
valueLabelDisplay="auto"
|
||||
disabled={disabled}
|
||||
color={color}
|
||||
size={size}
|
||||
/>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText error={!!error}>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== AUTOCOMPLETE ====================
|
||||
case 'autocomplete':
|
||||
return (
|
||||
<Autocomplete
|
||||
options={options}
|
||||
getOptionLabel={(option) => option.label || ''}
|
||||
value={options.find(o => o.value === value) || null}
|
||||
onChange={(_, newValue) => onChange({
|
||||
target: { name, value: newValue?.value || '' }
|
||||
})}
|
||||
disabled={disabled}
|
||||
fullWidth={fullWidth}
|
||||
size={size}
|
||||
sx={sx}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
name={name}
|
||||
label={label}
|
||||
error={!!error}
|
||||
helperText={error || helperText}
|
||||
required={required}
|
||||
variant={variant}
|
||||
margin={margin}
|
||||
color={color}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
// ==================== RATING ====================
|
||||
case 'rating':
|
||||
return (
|
||||
<FormControl
|
||||
margin={margin}
|
||||
disabled={disabled}
|
||||
sx={sx}
|
||||
>
|
||||
<FormLabel color={color}>{label}</FormLabel>
|
||||
<Rating
|
||||
name={name}
|
||||
value={value || 0}
|
||||
onChange={(_, newValue) => onChange({
|
||||
target: { name, value: newValue }
|
||||
})}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
max={max || 5}
|
||||
/>
|
||||
{(error || helperText) && (
|
||||
<FormHelperText error={!!error}>{error || helperText}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)
|
||||
|
||||
// ==================== DEFAULT (text, email, number, tel, url) ====================
|
||||
default:
|
||||
return (
|
||||
<TextField
|
||||
{...commonTextFieldProps}
|
||||
type={type}
|
||||
multiline={multiline}
|
||||
rows={multiline ? rows : undefined}
|
||||
inputProps={type === 'number' ? { min, max, step } : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== USAGE EXAMPLE ====================
|
||||
/*
|
||||
import MuiDynamicInput from './MuiDynamicInput'
|
||||
|
||||
function MyForm() {
|
||||
const [formData, setFormData] = React.useState({
|
||||
email: '',
|
||||
password: '',
|
||||
country: '',
|
||||
skills: [],
|
||||
gender: '',
|
||||
newsletter: false,
|
||||
bio: '',
|
||||
birthDate: '',
|
||||
rating: 0,
|
||||
})
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 600, mx: 'auto', p: 3 }}>
|
||||
<MuiDynamicInput
|
||||
type="email"
|
||||
name="email"
|
||||
label="Email Address"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
showPasswordToggle
|
||||
required
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="select"
|
||||
name="country"
|
||||
label="Country"
|
||||
value={formData.country}
|
||||
onChange={handleChange}
|
||||
options={[
|
||||
{ value: 'us', label: 'United States' },
|
||||
{ value: 'uk', label: 'United Kingdom' },
|
||||
{ value: 'in', label: 'India' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="tags"
|
||||
name="skills"
|
||||
label="Skills"
|
||||
value={formData.skills}
|
||||
onChange={handleChange}
|
||||
options={[
|
||||
{ value: 'react', label: 'React' },
|
||||
{ value: 'vue', label: 'Vue' },
|
||||
{ value: 'angular', label: 'Angular' },
|
||||
]}
|
||||
color="success"
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="radio"
|
||||
name="gender"
|
||||
label="Gender"
|
||||
value={formData.gender}
|
||||
onChange={handleChange}
|
||||
options={[
|
||||
{ value: 'male', label: 'Male' },
|
||||
{ value: 'female', label: 'Female' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="checkbox"
|
||||
name="newsletter"
|
||||
label="Subscribe to newsletter"
|
||||
value={formData.newsletter}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="textarea"
|
||||
name="bio"
|
||||
label="Bio"
|
||||
value={formData.bio}
|
||||
onChange={handleChange}
|
||||
rows={4}
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="date"
|
||||
name="birthDate"
|
||||
label="Birth Date"
|
||||
value={formData.birthDate}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="rating"
|
||||
name="rating"
|
||||
label="Rate your experience"
|
||||
value={formData.rating}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="slider"
|
||||
name="volume"
|
||||
label="Volume"
|
||||
value={formData.volume}
|
||||
onChange={handleChange}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
|
||||
<MuiDynamicInput
|
||||
type="file"
|
||||
name="avatar"
|
||||
label="Upload Avatar"
|
||||
onChange={handleChange}
|
||||
accept="image/*"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
*/
|
||||
Loading…
Reference in New Issue
Block a user