diff --git a/frontend/src/components/PasswordInput.jsx b/frontend/src/components/PasswordInput.jsx index bfeaeeb..99e5cb3 100644 --- a/frontend/src/components/PasswordInput.jsx +++ b/frontend/src/components/PasswordInput.jsx @@ -15,19 +15,24 @@ const EyeSlashedIcon = () => ( ); -const PasswordInput = ({ value, onChange, placeholder = "Password", id = "password", error }) => { +const PasswordInput = ({ value, onChange, placeholder = "Password", id = "password", error, className }) => { const [isPasswordVisible, setIsPasswordVisible] = useState(false); const togglePasswordVisibility = () => { setIsPasswordVisible(prevState => !prevState); }; + // Default classes if no className is provided + const defaultClasses = "w-full px-4 py-2 mt-2 border rounded-md focus:outline-none focus:ring-1"; + const errorClasses = error ? 'border-red-500 focus:ring-red-500' : 'focus:ring-blue-600 dark:border-gray-600'; + const inputClasses = `${defaultClasses} ${errorClasses} ${className ?? ''}`; + return (
{isPasswordVisible ? : } diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index e211bee..d3acd3b 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import useAuth from '../hooks/useAuth'; import PasswordInput from '../components/PasswordInput'; +import { HiMail, HiArrowRight } from 'react-icons/hi'; /* // Demo user credentials const DEMO_EMAIL = 'test@example.com'; @@ -11,17 +12,21 @@ export default function LoginPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [serverError, setServerError] = useState(''); + const [isLoading, setIsLoading] = useState(false); const { login } = useAuth(); const handleSubmit = async (e) => { e.preventDefault(); setServerError(''); // Clear previous server errors + setIsLoading(true); try { await login(email, password); // Toast handled globally in AuthContext } catch (error) { setServerError(error.message); // Toast handled globally in AuthContext + } finally { + setIsLoading(false); } }; @@ -31,41 +36,117 @@ export default function LoginPage() { }; */ return ( -
- - +
+ {/* Logo/Brand */} + Paisable -
-

Login to your account

- {serverError &&

{serverError}

} -
-
-
- - setEmail(e.target.value)} required /> -
-
- - setPassword(e.target.value)} /> + {/* Login Card */} +
+ {/* Header */} +
+

Welcome Back

+

Sign in to continue to your account

+
+ + {/* Error Message */} + {serverError && ( +
+

{serverError}

+
+ )} + + {/* Form */} + + {/* Email Field */} +
+ +
+
+
+ setEmail(e.target.value)} + required + />
-
- - {/**/} +
+ + {/* Password Field */} +
+ + setPassword(e.target.value)} + className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + /> +
+ + {/* Submit Button */} + + + {/* Divider */} +
+
+
-
- Don't have an account? Register +
+ + New to Paisable? +
+ + {/* Register Link */} +
+ + Create an account + + +
+ + {/* Footer */} +

+ Protected by industry-standard security +

); } \ No newline at end of file diff --git a/frontend/src/pages/RegisterPage.jsx b/frontend/src/pages/RegisterPage.jsx index fec46f5..781a520 100644 --- a/frontend/src/pages/RegisterPage.jsx +++ b/frontend/src/pages/RegisterPage.jsx @@ -2,15 +2,38 @@ import React, { useState } from 'react'; import { Link } from 'react-router-dom'; import useAuth from '../hooks/useAuth'; import PasswordInput from '../components/PasswordInput'; +import { HiMail, HiArrowRight, HiCheckCircle, HiXCircle } from 'react-icons/hi'; export default function RegisterPage() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [errors, setErrors] = useState({}); - // State for server-side errors const [serverError, setServerError] = useState(''); + const [isLoading, setIsLoading] = useState(false); const { signup } = useAuth(); + // Password strength indicator + const getPasswordStrength = () => { + if (!password) return null; + const hasLength = password.length >= 8 && password.length <= 16; + const hasLetter = /[a-zA-Z]/.test(password); + const hasDigit = /\d/.test(password); + const hasSymbol = /[\W_]/.test(password); + + const criteria = [hasLength, hasLetter, hasDigit, hasSymbol]; + const metCriteria = criteria.filter(Boolean).length; + + return { + hasLength, + hasLetter, + hasDigit, + hasSymbol, + strength: metCriteria + }; + }; + + const passwordStrength = getPasswordStrength(); + const validate = () => { const newErrors = {}; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -39,68 +62,204 @@ export default function RegisterPage() { const handleSubmit = async (e) => { e.preventDefault(); - setServerError(''); // Clear previous server errors + setServerError(''); const validationErrors = validate(); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } setErrors({}); + setIsLoading(true); - // The signup function in AuthContext needs to be updated to handle errors try { await signup(email, password); } catch (error) { setServerError(error.message); + } finally { + setIsLoading(false); } }; + const PasswordCriterion = ({ met, text }) => ( +
+ {met ? ( + + ) : ( + + )} + + {text} + +
+ ); + return ( -
- +
+ {/* Logo/Brand */} + Paisable -
-

Create an account

- {serverError &&

{serverError}

} -
-
-
- + + {/* Register Card */} +
+ {/* Header */} +
+

Create Account

+

Start managing your finances today

+
+ + {/* Error Message */} + {serverError && ( +
+

{serverError}

+
+ )} + + {/* Form */} + + {/* Email Field */} +
+ +
+
+
setEmail(e.target.value)} required /> - {errors.email &&

{errors.email}

}
-
- - setPassword(e.target.value)} - error={errors.password} - /> - {errors.password &&

{errors.password}

} -
-
-
+ + {/* Password Field */} +
+ + setPassword(e.target.value)} + error={errors.password} + className={`w-full px-4 py-3 border rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 transition-all duration-200 ${ + errors.password + ? 'border-red-500 focus:ring-red-500' + : 'border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-transparent' + }`} + /> + {errors.password && ( +

+ + {errors.password} +

+ )} + + {/* Password Strength Indicator */} + {password && passwordStrength && ( +
+

+ Password Requirements: +

+
+ + + + +
+ {/* Strength Bar */} +
+
+ {[1, 2, 3, 4].map((level) => ( +
= level + ? passwordStrength.strength === 4 + ? 'bg-green-500' + : passwordStrength.strength === 3 + ? 'bg-yellow-500' + : 'bg-red-500' + : 'bg-gray-200 dark:bg-gray-600' + }`} + /> + ))} +
+
+
+ )} +
+ + {/* Submit Button */} + + + + )} + + + {/* Divider */} +
+
+
-
- Already have an account? - - Log in - +
+ + Already have an account? +
+ + {/* Login Link */} +
+ + Sign in instead + + +
+ + {/* Footer */} +

+ By signing up, you agree to our Terms & Privacy Policy +

); } \ No newline at end of file