This commit is contained in:
Meenadeveloper 2025-11-22 17:07:12 +05:30
parent c873c7cd56
commit 1573a267e6
15 changed files with 2711 additions and 1 deletions

View File

@ -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
View File

@ -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",

View File

@ -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",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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 />} />
</>
)
}

View 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>
)
}
*/