diff --git a/index.html b/index.html index 03b7350..0584822 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + thirukalyanam diff --git a/package-lock.json b/package-lock.json index f832c6b..d19a387 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6646263..a5c8b94 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/assets/images/appstore-badge.svg b/src/assets/images/appstore-badge.svg new file mode 100644 index 0000000..9a07d5b --- /dev/null +++ b/src/assets/images/appstore-badge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/googleplay-badge.svg b/src/assets/images/googleplay-badge.svg new file mode 100644 index 0000000..f2cce61 --- /dev/null +++ b/src/assets/images/googleplay-badge.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/phone-left.avif b/src/assets/images/phone-left.avif new file mode 100644 index 0000000..7a86279 Binary files /dev/null and b/src/assets/images/phone-left.avif differ diff --git a/src/assets/images/success.gif b/src/assets/images/success.gif new file mode 100644 index 0000000..c450135 Binary files /dev/null and b/src/assets/images/success.gif differ diff --git a/src/components/auth/ChangePasswordForm.jsx b/src/components/auth/ChangePasswordForm.jsx new file mode 100644 index 0000000..2c0f0c1 --- /dev/null +++ b/src/components/auth/ChangePasswordForm.jsx @@ -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 ( + + + + + + + + + {title} + + + + + + {message} + + + + + + + + + ) +} + +// 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 ( + + + + Password Strength + + + {getLabel()} + + + + + ) +} + +// 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 ( + + {requirements.map((req, index) => ( + + {req.met ? ( + + ) : ( + + )} + + {req.label} + + + ))} + + ) +} + +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 ( + + + + {/* Left Side - Image */} + + {/* Background Decorations */} + + + + {/* Lock Icon with Animation */} + + + + + + Secure Your Account + + + + Keep your account safe by regularly updating your password. Choose + a strong password that you don't use elsewhere. + + + {/* Security Tips */} + + + Security Tips: + + + • Use a mix of letters, numbers & symbols + + + • Avoid personal information + + + • Don't reuse passwords from other sites + + + + + {/* Right Side - Form */} + + {/* Header */} + + + + + + Change Password + + + + + Your password must be different from previously used passwords. + + + {/* Form Fields */} + + + + + + {/* Password Strength Indicator */} + + + {/* Password Requirements */} + + + + + {/* Match Indicator */} + {formData.confirmPassword && ( + + {formData.newPassword === formData.confirmPassword ? ( + <> + + + Passwords match + + + ) : ( + <> + + + Passwords do not match + + + )} + + )} + + + {/* Submit Button */} + + + {/* Cancel Link */} + + + + + + {/* Success Modal */} + + + ) +} + +export default ChangePasswordPage \ No newline at end of file diff --git a/src/components/auth/ForgotPassworForm.jsx b/src/components/auth/ForgotPassworForm.jsx new file mode 100644 index 0000000..2a55b53 --- /dev/null +++ b/src/components/auth/ForgotPassworForm.jsx @@ -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 ( + + + + + + + + + {title} + + + + + + {message} + + + + + + + + + ) +} + +// 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 ( + + + {Array.from({ length: otpLength }).map((_, index) => ( + (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' + }} + /> + ))} + + {error && ( + + {error} + + )} + + ) +} + + +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 + case 1: + return + case 2: + return + default: + return null + } + } + + + + return ( + <> + + + {/* Header */} + + {activeStep > 0 && ( + + + + )} + + Forgot Password + + + + {/* Stepper */} + + + {steps.map((label, index) => ( + + + + {label} + + + + ))} + + + + {/* Form Content */} + + {/* Step 1: Email */} + {activeStep === 0 && ( + + + + + + + + + + Enter your email address and we'll send you an OTP to reset + your password. + + + + + + + + + Back to Login + + + + + )} + + {/* Step 2: OTP Verification */} + {activeStep === 1 && ( + + + + + + + + + + We've sent a 6-digit OTP to + + + {formData.email} + + + + + {/* Timer and Resend */} + + {resendTimer > 0 ? ( + + Resend OTP in{' '} + + {formatTime(resendTimer)} + + + ) : ( + + Resend OTP + + )} + + + + + + )} + + {/* Step 3: Reset Password */} + {activeStep === 2 && ( + + + + + + + + + + Create a strong password with at least 8 characters including + uppercase, lowercase, and numbers. + + + + + + + + + + )} + + + + {/* Success Modal */} + + + + ) +} + +export default ForgotPassworForm diff --git a/src/components/auth/LoginPanel.jsx b/src/components/auth/LoginPanel.jsx new file mode 100644 index 0000000..a51e1da --- /dev/null +++ b/src/components/auth/LoginPanel.jsx @@ -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 ( + + + {/* Animated Success Icon */} + + + + + {/* Success Title */} + + + {title || 'Success!'} + + + + {/* Success Message */} + + + {message || 'Your action was completed successfully.'} + + + + {/* Continue Button */} + + + + + + ) +} +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 ( + <> + + + + {/* Header */} + + + Member Login + + + + {/* Form Content */} + + {/* Email or Mobile Input */} + + + {/* Password Input */} + + + {/* Keep me logged in Checkbox */} + + + {/* Login Button */} + + + {/* Trouble logging in section */} + + + Trouble logging in? + + + + + Login with OTP + + + + + + Forgot Password? + + + + + + {/* Animated Success Modal */} + + + + + + ) +} + +export default LoginPanel diff --git a/src/components/auth/PromoPanel.jsx b/src/components/auth/PromoPanel.jsx new file mode 100644 index 0000000..9724bdc --- /dev/null +++ b/src/components/auth/PromoPanel.jsx @@ -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 ( + <> + + + + + +

+ To speed up your partner search, download Thirukalyanam App +

+ +
+ + +
+
+ TamilMatrimony App preview on mobile +
+
+ + +
+ +
+ + ThirukalyanamMatrimony® + Trusted Matrimony App + +
+ +
+ + 4.3 + +
+ + 10M+ Downloads | Based on Customer Reviews + + +
+ +
+ + + + +
+ + + ) +} + +export default PromoPanel diff --git a/src/pages/auth/ForgotPasswordPage.jsx b/src/pages/auth/ForgotPasswordPage.jsx new file mode 100644 index 0000000..2e613bd --- /dev/null +++ b/src/pages/auth/ForgotPasswordPage.jsx @@ -0,0 +1,26 @@ +import React from 'react' +import PromoPanel from '../../components/auth/PromoPanel' +import ForgotPassworForm from '../../components/auth/ForgotPassworForm' + +const ForgotPasswordPage = () => { + return ( + <> +
+ +
+
+ {/* Left: Promo */} + +
+ +
+ {/* Right: Login */} + +
+
+
+ + ) +} + +export default ForgotPasswordPage diff --git a/src/pages/auth/LoginPage.jsx b/src/pages/auth/LoginPage.jsx new file mode 100644 index 0000000..07e00f7 --- /dev/null +++ b/src/pages/auth/LoginPage.jsx @@ -0,0 +1,25 @@ +import LoginPanel from "../../components/auth/LoginPanel" +import PromoPanel from "../../components/auth/PromoPanel" + +const LoginPage = () => { + return ( + <> +
+ +
+
+ {/* Left: Promo */} + +
+ +
+ {/* Right: Login */} + +
+
+
+ + ) +} + +export default LoginPage diff --git a/src/routes/PublicRoutes.jsx b/src/routes/PublicRoutes.jsx index b0b93da..68bcafb 100644 --- a/src/routes/PublicRoutes.jsx +++ b/src/routes/PublicRoutes.jsx @@ -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 ( <> }> } /> + - + + } /> + } /> + } /> + ) } diff --git a/src/utills/MuiDynamicInput.jsx b/src/utills/MuiDynamicInput.jsx new file mode 100644 index 0000000..078bc97 --- /dev/null +++ b/src/utills/MuiDynamicInput.jsx @@ -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 ( + + {label} + + + {showPassword ? : } + + + ) + } + label={label} + /> + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== SELECT ==================== + case 'select': + return ( + + {label} + + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== MULTISELECT ==================== + case 'multiselect': + return ( + + {label} + + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== TAGS (Chips) ==================== + case 'tags': + return ( + + {label} + + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== RADIO ==================== + case 'radio': + return ( + + {label} + + {options.map((opt) => ( + } + label={opt.label} + /> + ))} + + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== CHECKBOX ==================== + case 'checkbox': + // Single checkbox (boolean value) + if (options.length === 0) { + return ( + + onChange({ + target: { name, value: e.target.checked } + })} + color={color} + size={size} + /> + } + label={label} + /> + {(error || helperText) && ( + {error || helperText} + )} + + ) + } + // Multiple checkboxes (array value) + return ( + + {label} + + {options.map((opt) => ( + { + 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} + /> + ))} + + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== SWITCH ==================== + case 'switch': + return ( + + onChange({ + target: { name, value: e.target.checked } + })} + color={color} + size={size} + /> + } + label={label} + /> + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== DATE / TIME / DATETIME ==================== + case 'date': + case 'time': + case 'datetime-local': + return ( + + ) + + // ==================== TEXTAREA ==================== + case 'textarea': + return ( + + ) + + // ==================== FILE ==================== + case 'file': + return ( + + + + + } + sx={{ + '& input::file-selector-button': { + display: 'none', + }, + }} + /> + + {error || helperText || (fileName ? `Selected: ${fileName}` : 'Choose file(s)')} + + + ) + + // ==================== SLIDER ==================== + case 'slider': + return ( + + {label} + onChange({ + target: { name, value: newValue } + })} + min={min} + max={max} + step={step} + valueLabelDisplay="auto" + disabled={disabled} + color={color} + size={size} + /> + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== AUTOCOMPLETE ==================== + case 'autocomplete': + return ( + 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) => ( + + )} + /> + ) + + // ==================== RATING ==================== + case 'rating': + return ( + + {label} + onChange({ + target: { name, value: newValue } + })} + disabled={disabled} + size={size} + max={max || 5} + /> + {(error || helperText) && ( + {error || helperText} + )} + + ) + + // ==================== DEFAULT (text, email, number, tel, url) ==================== + default: + return ( + + ) + } +} + +// ==================== 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 ( + + + + + + + + + + + + + + + + + + + + + + + + ) +} +*/ \ No newline at end of file