| import { useState, useEffect } from 'react'; |
| import { useAuth } from '../context/AuthContext'; |
| import { Link } from 'react-router-dom'; |
| import { User, Mail, Lock, ArrowRight, Loader, CheckCircle, Eye, EyeOff, XCircle, Check, Wallet } from 'lucide-react'; |
|
|
| const Register = () => { |
| const [formData, setFormData] = useState({ username: '', email: '', password: '', confirmPassword: '' }); |
| const [showPassword, setShowPassword] = useState(false); |
| const [showConfirmPassword, setShowConfirmPassword] = useState(false); |
| const [isPasswordFocused, setIsPasswordFocused] = useState(false); |
| const { register } = useAuth(); |
| const [isSubmitting, setIsSubmitting] = useState(false); |
| const [error, setError] = useState(''); |
|
|
| const [passwordCriteria, setPasswordCriteria] = useState({ length: false, upper: false, lower: false, number: false, special: false }); |
| const [passwordsMatch, setPasswordsMatch] = useState(true); |
| const [allValid, setAllValid] = useState(false); |
|
|
| useEffect(() => { |
| const { password, confirmPassword } = formData; |
| const criteria = { |
| length: password.length >= 8, |
| upper: /[A-Z]/.test(password), |
| lower: /[a-z]/.test(password), |
| number: /[0-9]/.test(password), |
| special: /[^A-Za-z0-9]/.test(password), |
| }; |
| setPasswordCriteria(criteria); |
| setPasswordsMatch(password === confirmPassword || confirmPassword === ''); |
| setAllValid(Object.values(criteria).every(Boolean) && password === confirmPassword && password !== ''); |
| }, [formData.password, formData.confirmPassword]); |
|
|
| const handleSubmit = async (e) => { |
| e.preventDefault(); |
| if (isSubmitting) return; |
| if (!allValid) { setError('Please ensure all requirements are met and passwords match.'); return; } |
| setIsSubmitting(true); |
| setError(''); |
| try { |
| await register(formData.username, formData.email, formData.password); |
| } catch (err) { |
| setError(err.response?.data?.username?.[0] || 'Registration failed. Please try again.'); |
| setIsSubmitting(false); |
| } |
| }; |
|
|
| const RequirementItem = ({ met, text }) => ( |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.78rem', color: met ? '#34d399' : 'var(--text-muted)', transition: 'color 0.2s', marginBottom: '0.2rem' }}> |
| {met ? <Check size={12} color="#34d399" /> : <div style={{ width: 12, height: 12, borderRadius: '50%', border: '1px solid currentColor', flexShrink: 0 }} />} |
| {text} |
| </div> |
| ); |
|
|
| return ( |
| <div className="home-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', padding: '2rem 1rem' }}> |
| <div className="bg-blobs"> |
| <div className="blob blob-2 animate-blob" /> |
| <div className="blob blob-3 animate-blob animation-delay-2000" /> |
| <div className="blob blob-1 animate-blob animation-delay-4000" /> |
| </div> |
| <div className="grid-overlay" /> |
| |
| <div className="glass-pro animate-slide-up" style={{ |
| padding: '2.25rem', |
| width: '100%', |
| maxWidth: '400px', |
| borderRadius: '20px', |
| position: 'relative', |
| overflow: 'hidden', |
| border: '1px solid rgba(139,92,246,0.2)', |
| boxShadow: '0 30px 80px rgba(0,0,0,0.6), 0 0 60px rgba(139,92,246,0.08)', |
| margin: '1rem', |
| }}> |
| {/* Decorative orb */} |
| <div style={{ |
| position: 'absolute', top: '-60px', left: '-60px', |
| width: '200px', height: '200px', |
| background: 'radial-gradient(circle, rgba(139,92,246,0.12) 0%, transparent 70%)', |
| pointerEvents: 'none', |
| }} /> |
| |
| {/* Header */} |
| <div style={{ textAlign: 'center', marginBottom: '1.75rem' }}> |
| <div className="brand-icon" style={{ width: '48px', height: '48px', margin: '0 auto 1.25rem', borderRadius: '14px', background: 'linear-gradient(135deg, #8b5cf6, #ec4899)', justifyContent: 'center', alignItems: 'center', display: 'flex', boxShadow: '0 4px 14px rgba(139,92,246,0.4)' }}> |
| <Wallet size={24} /> |
| </div> |
| <h2 style={{ fontSize: '1.75rem', marginBottom: '0.4rem', letterSpacing: '-0.02em', background: 'linear-gradient(135deg, #c084fc, #f472b6)', WebkitBackgroundClip: 'text', backgroundClip: 'text', color: 'transparent' }}> |
| Create Account |
| </h2> |
| <p style={{ color: 'var(--text-muted)', fontSize: '0.88rem' }}>Join FinMK and take control of your wealth</p> |
| </div> |
| |
| {/* Error */} |
| {error && ( |
| <div className="animate-slide-up" style={{ |
| backgroundColor: 'rgba(244,63,94,0.08)', |
| border: '1px solid rgba(244,63,94,0.2)', |
| color: '#fb7185', |
| padding: '0.65rem 0.875rem', |
| borderRadius: '10px', |
| marginBottom: '1.25rem', |
| display: 'flex', |
| alignItems: 'center', |
| gap: '0.5rem', |
| fontSize: '0.875rem' |
| }}> |
| <span>⚠</span> {error} |
| </div> |
| )} |
| |
| <form onSubmit={handleSubmit}> |
| <div className="auth-input-group delay-100 animate-slide-up"> |
| <input type="text" className="input-field-pro" placeholder="Username" |
| value={formData.username} onChange={(e) => setFormData({ ...formData, username: e.target.value })} required /> |
| <User className="auth-input-icon" size={18} /> |
| </div> |
| |
| <div className="auth-input-group delay-200 animate-slide-up"> |
| <input type="email" className="input-field-pro" placeholder="Email Address" |
| value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} required /> |
| <Mail className="auth-input-icon" size={18} /> |
| </div> |
| |
| <div className="auth-input-group delay-300 animate-slide-up" style={{ marginBottom: '0.75rem' }}> |
| <input type={showPassword ? 'text' : 'password'} className="input-field-pro" placeholder="Password" |
| value={formData.password} |
| onChange={(e) => setFormData({ ...formData, password: e.target.value })} |
| onFocus={() => setIsPasswordFocused(true)} |
| onBlur={() => setIsPasswordFocused(false)} |
| required style={{ paddingRight: '2.8rem' }} /> |
| <Lock className="auth-input-icon" size={18} /> |
| <button type="button" onClick={() => setShowPassword(!showPassword)} style={{ position: 'absolute', right: '0.875rem', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', padding: 0, display: 'flex' }}> |
| {showPassword ? <EyeOff size={18} /> : <Eye size={18} />} |
| </button> |
| </div> |
| |
| <div className="auth-input-group delay-300 animate-slide-up" style={{ marginBottom: '0.75rem' }}> |
| <input type={showConfirmPassword ? 'text' : 'password'} className="input-field-pro" placeholder="Confirm Password" |
| value={formData.confirmPassword} |
| onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })} |
| required style={{ paddingRight: '2.8rem', borderColor: (!passwordsMatch && formData.confirmPassword) ? 'rgba(244,63,94,0.5)' : 'var(--glass-border)' }} /> |
| <Lock className="auth-input-icon" size={18} /> |
| <button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} style={{ position: 'absolute', right: '0.875rem', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', padding: 0, display: 'flex' }}> |
| {showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />} |
| </button> |
| </div> |
| |
| {/* Password strength */} |
| {(isPasswordFocused || formData.password.length > 0) && ( |
| <div className="animate-slide-up" style={{ background: 'rgba(0,0,0,0.2)', padding: '0.75rem', borderRadius: '10px', marginBottom: '1rem', border: '1px solid var(--glass-border)' }}> |
| <p style={{ fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>Requirements</p> |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.15rem' }}> |
| <RequirementItem met={passwordCriteria.length} text="8+ Characters" /> |
| <RequirementItem met={passwordCriteria.upper} text="Uppercase" /> |
| <RequirementItem met={passwordCriteria.lower} text="Lowercase" /> |
| <RequirementItem met={passwordCriteria.number} text="Number" /> |
| <RequirementItem met={passwordCriteria.special} text="Symbol" /> |
| <RequirementItem met={passwordsMatch && formData.confirmPassword !== ''} text="Passwords match" /> |
| </div> |
| </div> |
| )} |
| |
| <button type="submit" className="btn-primary delay-300 animate-slide-up" disabled={isSubmitting || !allValid} style={{ |
| width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', |
| gap: '0.5rem', marginTop: '0.25rem', padding: '0.8rem', borderRadius: '10px', |
| background: 'linear-gradient(135deg, #8b5cf6, #ec4899)', |
| boxShadow: allValid ? '0 8px 24px rgba(139,92,246,0.4)' : 'none', |
| opacity: (!allValid && !isSubmitting) ? 0.55 : 1, |
| cursor: (!allValid && !isSubmitting) ? 'not-allowed' : 'pointer', |
| fontSize: '0.95rem', |
| }}> |
| {isSubmitting ? ( |
| <><Loader className="spinning" size={18} /> Creating Account...</> |
| ) : ( |
| <><CheckCircle size={18} /> Create Account</> |
| )} |
| </button> |
| </form> |
|
|
| <div className="delay-300 animate-slide-up" style={{ marginTop: '1.5rem', textAlign: 'center', borderTop: '1px solid var(--glass-border)', paddingTop: '1.25rem' }}> |
| <p style={{ color: 'var(--text-muted)', fontSize: '0.875rem' }}> |
| Already have an account?{' '} |
| <Link to="/login" style={{ color: '#c084fc', marginLeft: '0.25rem', fontWeight: 600, textDecoration: 'none' }}> |
| Sign in → |
| </Link> |
| </p> |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default Register; |
|
|