Initial Claude Run
This commit is contained in:
103
frontend/src/pages/Auth/ForgotPassword.js
Normal file
103
frontend/src/pages/Auth/ForgotPassword.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const { forgotPassword, loading } = useAuth();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError,
|
||||
reset,
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const result = await forgotPassword(data.email);
|
||||
if (result.success) {
|
||||
reset();
|
||||
} else {
|
||||
setError('root', {
|
||||
type: 'manual',
|
||||
message: result.error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Forgot your password?</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
{errors.root && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
{errors.root.message}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="label">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
className={errors.email ? 'input-error' : 'input'}
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: {
|
||||
value: /^\S+@\S+$/i,
|
||||
message: 'Invalid email address',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-primary w-full justify-center"
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner size="sm" color="white" />
|
||||
) : (
|
||||
'Send reset link'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/login"
|
||||
className="font-medium text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
Back to sign in
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
203
frontend/src/pages/Auth/Login.js
Normal file
203
frontend/src/pages/Auth/Login.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||
|
||||
const Login = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { login, loading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const from = location.state?.from?.pathname || '/dashboard';
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setError,
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const result = await login(data);
|
||||
if (result.success) {
|
||||
navigate(from, { replace: true });
|
||||
} else {
|
||||
setError('root', {
|
||||
type: 'manual',
|
||||
message: result.error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
// Redirect to Google OAuth endpoint
|
||||
window.location.href = `${process.env.REACT_APP_API_URL || 'http://localhost:5000'}/api/auth/google`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Sign in to your account</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Or{' '}
|
||||
<Link
|
||||
to="/register"
|
||||
className="font-medium text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
create a new account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
{errors.root && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
{errors.root.message}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="label">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
className={errors.email ? 'input-error' : 'input'}
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: {
|
||||
value: /^\S+@\S+$/i,
|
||||
message: 'Invalid email address',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="label">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
className={errors.password ? 'input-error pr-10' : 'input pr-10'}
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
{errors.password && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="font-medium text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-primary w-full justify-center"
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner size="sm" color="white" />
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleLogin}
|
||||
className="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-lg shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors duration-200"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-2">Sign in with Google</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
248
frontend/src/pages/Auth/Register.js
Normal file
248
frontend/src/pages/Auth/Register.js
Normal file
@@ -0,0 +1,248 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||
|
||||
const Register = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { register: registerUser, loading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch,
|
||||
setError,
|
||||
} = useForm();
|
||||
|
||||
const password = watch('password');
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const result = await registerUser(data);
|
||||
if (result.success) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setError('root', {
|
||||
type: 'manual',
|
||||
message: result.error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Create your account</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
to="/login"
|
||||
className="font-medium text-primary-600 hover:text-primary-500"
|
||||
>
|
||||
Sign in here
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
{errors.root && (
|
||||
<div className="rounded-md bg-red-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
{errors.root.message}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="label">
|
||||
First name
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
className={errors.firstName ? 'input-error' : 'input'}
|
||||
{...register('firstName', {
|
||||
required: 'First name is required',
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: 'First name must be at least 2 characters',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.firstName.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="lastName" className="label">
|
||||
Last name
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
autoComplete="family-name"
|
||||
className={errors.lastName ? 'input-error' : 'input'}
|
||||
{...register('lastName', {
|
||||
required: 'Last name is required',
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: 'Last name must be at least 2 characters',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.lastName.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="label">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
className={errors.email ? 'input-error' : 'input'}
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: {
|
||||
value: /^\S+@\S+$/i,
|
||||
message: 'Invalid email address',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.email.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="label">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
className={errors.password ? 'input-error pr-10' : 'input pr-10'}
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: 'Password must be at least 8 characters',
|
||||
},
|
||||
pattern: {
|
||||
value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
|
||||
message: 'Password must contain uppercase, lowercase, number, and special character',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
{errors.password && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="label">
|
||||
Confirm password
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
className={errors.confirmPassword ? 'input-error' : 'input'}
|
||||
{...register('confirmPassword', {
|
||||
required: 'Please confirm your password',
|
||||
validate: (value) =>
|
||||
value === password || 'Passwords do not match',
|
||||
})}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="mt-2 text-sm text-red-600">{errors.confirmPassword.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="agree-terms"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
{...register('agreeTerms', {
|
||||
required: 'You must agree to the terms and conditions',
|
||||
})}
|
||||
/>
|
||||
<label htmlFor="agree-terms" className="ml-2 block text-sm text-gray-900">
|
||||
I agree to the{' '}
|
||||
<Link
|
||||
to="/terms"
|
||||
className="text-primary-600 hover:text-primary-500"
|
||||
target="_blank"
|
||||
>
|
||||
Terms and Conditions
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link
|
||||
to="/privacy"
|
||||
className="text-primary-600 hover:text-primary-500"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</label>
|
||||
</div>
|
||||
{errors.agreeTerms && (
|
||||
<p className="text-sm text-red-600">{errors.agreeTerms.message}</p>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-primary w-full justify-center"
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner size="sm" color="white" />
|
||||
) : (
|
||||
'Create account'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
Reference in New Issue
Block a user