diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 839f2ea..b7b63b9 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,47 +1,86 @@ 'use client'; import { useState } from 'react'; -import { useRouter } from 'next/navigation'; +// import { useRouter } from 'next/navigation'; +import TextInput from '@/components/TextInput'; +import { StyledButton, StyledForm } from '@/components/TextInput/styles'; +import COLORS from '@/styles/colors'; +import { H2, P3 } from '@/styles/text'; import { useAuth } from '../../../utils/AuthProvider'; export default function Login() { const { signIn } = useAuth(); // Use `signIn` function from AuthProvider const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const router = useRouter(); + const [showPassword, setShowPassword] = useState(false); + const [invalidEmailError, setInvalidEmailError] = useState(''); + const [invalidPasswordError, setInvalidPasswordError] = useState(''); + const isFormValid = email && password; + + // const { push } = useRouter(); const handleLogin = async () => { - // Define handleLogin try { - await signIn(email, password); - router.push('/'); // Redirect to the home page on success - } catch (error) { - if (error instanceof Error) { - console.error('Login Error:', error.message); + const { error } = await signIn(email, password); + // push('/'); + + if (error) { + // Match error messages from Supabase + if (error.message.includes('Invalid login credentials')) { + setInvalidEmailError('Invalid email address'); + setInvalidPasswordError('Invalid password'); + } + return; } + + // Clear errors on success + setInvalidEmailError(''); + setInvalidPasswordError(''); + } catch (err) { + console.error('Login Error:', err); + setInvalidEmailError('An unexpected error occurred. Please try again.'); } }; return ( - <> - setEmail(e.target.value)} - value={email} - placeholder="Email" - /> - {/* Email input*/} - setPassword(e.target.value)} - value={password} - placeholder="Password" - /> - {' '} - {/* Sign in button */} - + +
+

Log In

+
+ + {/* Email input*/} + {invalidEmailError} +
+
+ setShowPassword(!showPassword)} + error={!!invalidPasswordError} + /> + {invalidPasswordError} + {/* Password input*/} +
+ + Log in + {' '} + {/* Sign in button */} +
+
); } diff --git a/app/(auth)/signup/page.tsx b/app/(auth)/signup/page.tsx index ad3a436..14ed135 100644 --- a/app/(auth)/signup/page.tsx +++ b/app/(auth)/signup/page.tsx @@ -2,7 +2,10 @@ import { useState } from 'react'; import PasswordComplexity from '@/components/PasswordComplexity'; -import PasswordInput from '@/components/PasswordInput'; +import TextInput from '@/components/TextInput'; +import { StyledButton, StyledForm } from '@/components/TextInput/styles'; +import COLORS from '@/styles/colors'; +import { H2, P3 } from '@/styles/text'; import { useAuth } from '@/utils/AuthProvider'; export default function SignUp() { @@ -10,114 +13,186 @@ export default function SignUp() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); - const [passwordError, setPasswordError] = useState(''); - const [passwordComplexityError, setPasswordComplexityError] = useState< - string | null - >(null); - const [passwordComplexity, setPasswordComplexity] = useState(false); + const [samePasswordCheck, setSamePasswordCheck] = useState(''); + const [mismatchError, setMismatchError] = useState(''); + const [isPasswordComplexityMet, setIsPasswordComplexityMet] = + useState(false); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [checkEmailExistsError] = useState(''); + const [checkValidEmailError, setCheckValidEmailError] = useState(''); + const [isSubmitted, setIsSubmitted] = useState(false); + const [isEmailValid, setIsEmailValid] = useState(true); + + const isFormValid = email && password && confirmPassword; + + const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const handleEmailChange = async (newEmail: string) => { + setEmail(newEmail); + + // Validate email format + if (!isValidEmail(newEmail)) { + setIsEmailValid(false); + setCheckValidEmailError('Please enter a valid email address'); + } else { + setIsEmailValid(true); + setCheckValidEmailError(''); + } + // Check if the email already exists + }; // Handles input to password - const handlePasswordChange = (e: React.ChangeEvent) => { - const newPassword = e.target.value; + const handlePasswordChange = (newPassword: string) => { setPassword(newPassword); - validatePasswords(newPassword, confirmPassword); - validatePasswordComplexity(newPassword); + + if (!newPassword || !confirmPassword) { + setMismatchError(''); + setSamePasswordCheck(''); + return; + } + + // Set mismatch error if passwords do not match + if (newPassword !== confirmPassword) { + setMismatchError('✗ Passwords do not match'); + setSamePasswordCheck(''); + } else { + setMismatchError(''); + setSamePasswordCheck('✓ Passwords match'); + } + + validateIsPasswordComplexityMet(newPassword); }; // Handles input to confirm password - const handleConfirmPasswordChange = ( - e: React.ChangeEvent, - ) => { - const newConfirmPassword = e.target.value; + const handleConfirmPasswordChange = (newConfirmPassword: string) => { setConfirmPassword(newConfirmPassword); - validatePasswords(password, newConfirmPassword); - }; - // Checks if passwords match and sets error - const validatePasswords = ( - password: string | null, - confirmPassword: string | null, - ) => { - if (password !== confirmPassword) { - setPasswordError('Passwords do not match.'); + // Clear mismatch error if either field is empty + if (!password || !newConfirmPassword) { + setMismatchError(''); + setSamePasswordCheck(''); + return; + } + + // Set mismatch error if passwords do not match + if (password !== newConfirmPassword) { + setMismatchError('✗ Passwords do not match'); + setSamePasswordCheck(''); } else { - setPasswordError(''); // Clear error when passwords match + setMismatchError(''); + setSamePasswordCheck('✓ Passwords match'); } }; // Set password complexity error if requirements are not met - const validatePasswordComplexity = (password: string | null) => { + const validateIsPasswordComplexityMet = (password: string | null) => { const hasLowerCase = /[a-z]/.test(password || ''); const hasNumber = /\d/.test(password || ''); const longEnough = (password || '').length >= 8; if (password && hasLowerCase && hasNumber && longEnough) { - setPasswordComplexity(true); - setPasswordComplexityError(null); // Clear error if all conditions are met - } else if (password) { - setPasswordComplexity(false); - setPasswordComplexityError('Password must meet complexity requirements'); + setIsPasswordComplexityMet(true); } else { - setPasswordComplexity(false); - setPasswordComplexityError(null); // Clear error if password is empty + setIsPasswordComplexityMet(false); } }; const handleSignUp = async () => { - if (password) { + setIsSubmitted(true); + + if (!isValidEmail(email)) { + setCheckValidEmailError('Please enter a valid email address'); + } else { + setCheckValidEmailError(''); // Clear email format error if valid + } + + try { await signUp(email, password); + } catch (error) { + console.error('Sign up failed:', error); + alert('There was an error during sign up. Please try again.'); } }; return ( - <> - setEmail(e.target.value)} - value={email} - placeholder="Email" - /> - {/* Email input*/} - setShowPassword(!showPassword)} - name="password" - /> - {/* Password input with toggle visibility */} - setShowConfirmPassword(!showConfirmPassword)} - name="confirmPassword" - /> - {/* Confirm password input with toggle visibility */} - {' '} - {/* Sign up button */} - {confirmPassword && passwordError && ( -

{passwordError}

- )} - {/* Conditional password validation error message */} - - {/* Password complexity requirements */} - {password && !passwordComplexity && passwordComplexityError && ( -

{passwordComplexityError}

- )} - {/* Password complexity error message */} - + +
+

Sign Up

+
+ + {/* Email input*/} + {checkEmailExistsError && ( + {checkEmailExistsError} + )} + {!isEmailValid && isSubmitted && ( + {checkValidEmailError} + )} +
+
+ setShowPassword(!showPassword)} + label="Password" + error={isSubmitted && !isPasswordComplexityMet} + /> + {/* Password input*/} + + + + {/* Password complexity requirements */} +
+
+ {password && ( + + setShowConfirmPassword(!showConfirmPassword) + } + label="Confirm Password" + error={isSubmitted && !samePasswordCheck} + /> + )} + {/* Confirm password input with toggle visibility */} + + {samePasswordCheck && ( + {samePasswordCheck} + )} + + {isSubmitted && !samePasswordCheck && ( + {mismatchError} + )} + {/* Conditional password validation error message */} +
+ + Sign up + {' '} + {/* Sign up button */} +
+
); } diff --git a/components/PasswordComplexity.tsx b/components/PasswordComplexity.tsx index e7b2ba1..ad4167d 100644 --- a/components/PasswordComplexity.tsx +++ b/components/PasswordComplexity.tsx @@ -1,14 +1,35 @@ +import COLORS from '@/styles/colors'; +import { P3 } from '@/styles/text'; + export default function PasswordComplexity({ password }: { password: string }) { - // Display requirements if there is input + // Define complexity rules with their check logic + const requirements = [ + { + met: /[a-z]/.test(password), + text: 'At least 1 lowercase character', + }, + { + met: /\d/.test(password), + text: 'At least 1 number', + }, + { + met: password.length >= 8, + text: 'At least 8 characters', + }, + ]; + + // Sort requirements: passed ones at the top + const sortedRequirements = requirements.sort((a, b) => { + return Number(b.met) - Number(a.met); + }); + + // Display sorted requirements only if there is input if (password.length > 0) { return (
- - - = 8} text="At least 8 characters" /> + {sortedRequirements.map((req, index) => ( + + ))}
); } @@ -19,8 +40,12 @@ export default function PasswordComplexity({ password }: { password: string }) { // Helper component to display each requirement with conditional styling function Requirement({ met, text }: { met: boolean; text: string }) { return ( -

+ {met ? '✓' : '✗'} {text} -

+ ); } diff --git a/components/PasswordInput.tsx b/components/PasswordInput.tsx index e10a3c9..ee96c30 100644 --- a/components/PasswordInput.tsx +++ b/components/PasswordInput.tsx @@ -1,7 +1,7 @@ import React from 'react'; interface PasswordInputProps { - value: string | null; + value: string; onChange: (e: React.ChangeEvent) => void; placeholder: string; isVisible: boolean; @@ -14,7 +14,6 @@ const PasswordInput: React.FC = ({ onChange, placeholder, isVisible, - toggleVisibility, name, }) => { return ( @@ -26,9 +25,6 @@ const PasswordInput: React.FC = ({ value={value || ''} placeholder={placeholder} /> - ); }; diff --git a/components/TextInput/index.tsx b/components/TextInput/index.tsx new file mode 100644 index 0000000..698bb23 --- /dev/null +++ b/components/TextInput/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import Icon from '../Icon'; +import { IconWrapper, InputWrapper, StyledInput, StyledLabel } from './styles'; + +interface TextInputProps { + label: string; + id: string; + type: string; + value: string; + onChange: (s: string) => void; + isVisible?: boolean; + toggleVisibility?: () => void; + error?: boolean; +} + +const TextInput: React.FC = ({ + label, + id, + type, + onChange, + isVisible, + value, + toggleVisibility, + error, +}) => { + const inputType = type === 'password' && isVisible ? 'text' : type; + + const handleChange = (event: React.ChangeEvent) => { + onChange(event.target.value); + }; + + return ( + + {label && {label}} + + {type === 'password' && toggleVisibility && ( + + + + )} + + ); +}; + +export default TextInput; diff --git a/components/TextInput/styles.ts b/components/TextInput/styles.ts new file mode 100644 index 0000000..7040b92 --- /dev/null +++ b/components/TextInput/styles.ts @@ -0,0 +1,69 @@ +import styled from 'styled-components'; +import COLORS from '@/styles/colors'; +import { P2 } from '@/styles/text'; + +export const InputWrapper = styled.div` + position: relative; + display: flex; + flex-direction: column; + // overflow: visible; +`; + +export const StyledLabel = styled(P2).attrs({ as: 'label' })` + margin-bottom: 0.25rem; +`; + +export const StyledInput = styled(P2).attrs({ as: 'input' })<{ + error?: boolean; +}>` + padding: 0.75rem; + border: 0.0625rem solid #ccc; + border: 1px solid ${({ error }) => (error ? COLORS.errorRed : '#ccc')}; + border-radius: 0.3125rem; + font-family: inherit; /* Inherit font-family from P2 */ + margin-bottom: 0.25rem; + transition: border-color 0.3s ease; + + &:focus { + border-color: ${({ error }) => (error ? COLORS.errorRed : COLORS.shrub)}; + outline: none; + } +`; + +export const IconWrapper = styled.span` + position: absolute; + right: 1rem; + cursor: pointer; + top: 50%; + display: flex; + align-items: center; + justify-content: center; + color: ${COLORS.darkgray}; +`; + +export const StyledButton = styled.button` + background-color: ${COLORS.shrub}; + color: white; + padding: 0.625rem 1.25rem; + border: none; + border-radius: 3.125rem; + justify-content: center; + width: 100%; + height: 2.625rem; + cursor: pointer; + + &:disabled { + background-color: ${COLORS.midgray}; // Change to a gray color to indicate disabled state + cursor: not-allowed; + } +`; + +export const TextSpacingWrapper = styled.div` + margin: 0; + marginbottom: 0.25rem; +`; + +export const StyledForm = styled.form` + minheight: 100; + padding: 1.25rem; +`; diff --git a/lib/icons.tsx b/lib/icons.tsx index ffb6ffd..f17e088 100644 --- a/lib/icons.tsx +++ b/lib/icons.tsx @@ -347,6 +347,64 @@ export const IconSvgs = { /> ), + + eye: ( + + + + + + + + + + ), + + hide: ( + + + + + + + + + + ), }; export type IconType = keyof typeof IconSvgs; diff --git a/utils/AuthProvider.tsx b/utils/AuthProvider.tsx index b4b5a96..c8b42e0 100644 --- a/utils/AuthProvider.tsx +++ b/utils/AuthProvider.tsx @@ -10,7 +10,8 @@ import { useState, } from 'react'; import { UUID } from 'crypto'; -import { AuthResponse, Session } from '@supabase/supabase-js'; +import { AuthError, AuthResponse, Session } from '@supabase/supabase-js'; +import { checkEmailExists } from '@/api/supabase/queries/users'; import supabase from '../api/supabase/createClient'; interface AuthContextType { @@ -79,6 +80,11 @@ export function AuthProvider({ children }: { children: ReactNode }) { const value = await supabase.auth.signUp({ email, password }); // will trigger onAuthStateChange to update the session // check if email already exists + const emailExists = await checkEmailExists(email); + if (emailExists) { + const authError = new AuthError('Account already exists for this email'); + value.error = authError; + } return value; }, []);