From 25fd5fe1b8db47d785a4b5527ad00175ae102cb6 Mon Sep 17 00:00:00 2001 From: laryhills Date: Wed, 10 Sep 2025 21:36:54 +0100 Subject: [PATCH 1/2] refactor: :recycle: replace LoginModal with AuthModal and update header component for authentication handling --- frontend/src/components/LoginModal.tsx | 215 -------------------- frontend/src/components/auth/AuthModal.tsx | 75 +++++++ frontend/src/components/auth/LoginForm.tsx | 174 ++++++++++++++++ frontend/src/components/auth/SignupForm.tsx | 165 +++++++++++++++ frontend/src/components/header.tsx | 23 ++- 5 files changed, 428 insertions(+), 224 deletions(-) delete mode 100644 frontend/src/components/LoginModal.tsx create mode 100644 frontend/src/components/auth/AuthModal.tsx create mode 100644 frontend/src/components/auth/LoginForm.tsx create mode 100644 frontend/src/components/auth/SignupForm.tsx diff --git a/frontend/src/components/LoginModal.tsx b/frontend/src/components/LoginModal.tsx deleted file mode 100644 index 945d075..0000000 --- a/frontend/src/components/LoginModal.tsx +++ /dev/null @@ -1,215 +0,0 @@ -'use client'; - -import type React from 'react'; -import { useState } from 'react'; -import { useAuth } from '../../context/AuthContext'; - -type ModalProps = { - closeModal: () => void; -}; -export function LoginForm({ closeModal }: ModalProps) { - const { login, signup } = useAuth(); - const [isSignup, setIsSignup] = useState(false); - const [formData, setFormData] = useState({ - email: '', - password: '', - username: '', - }); - const [rememberMe, setRememberMe] = useState(false); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setIsLoading(true); - - try { - if (isSignup) { - await signup(formData.email, formData.password, formData.username); - } else { - await login(formData.email, formData.password); - closeModal(); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
- {/* Title */} -
-

- {isSignup ? 'Sign Up' : 'Login'} -

- -
- {/* Error Message */} - {error && ( -
- {error} -
- )} - -
- {/* Email input */} -
- - setFormData({ ...formData, email: e.target.value }) - } - required - className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" - /> -
- - {/* Username input (only for signup) */} - {isSignup && ( -
- - setFormData({ ...formData, username: e.target.value }) - } - required - className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" - /> -
- )} - - {/* Password input */} -
- - setFormData({ ...formData, password: e.target.value }) - } - required - className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" - /> -
- - {/* Remember Me & Forgot Password (only for login) */} - {!isSignup && ( -
-
- setRememberMe(e.target.checked)} - className="border-white/30 data-[state=checked]:bg-secondary data-[state=checked]:border-secondary" - /> - -
- -
- )} - - {/* Login/Signup button */} - - - {/* Switch between Login/Signup */} -
- - {isSignup - ? 'Already have an account? ' - : 'Already have an account? '} - - -
-
- - {/* Divider */} -
-
-
-
-
-
- - Continue with - -
-
-
- - {/* Google Login button */} - -
-
- ); -} diff --git a/frontend/src/components/auth/AuthModal.tsx b/frontend/src/components/auth/AuthModal.tsx new file mode 100644 index 0000000..b502feb --- /dev/null +++ b/frontend/src/components/auth/AuthModal.tsx @@ -0,0 +1,75 @@ +'use client'; + +import type React from 'react'; +import { useState, useEffect } from 'react'; +import { LoginForm } from './LoginForm'; +import { SignupForm } from './SignupForm'; + +type AuthMode = 'login' | 'signup'; + +type AuthModalProps = { + closeModal: () => void; + initialMode?: AuthMode; + onModeChange?: (mode: AuthMode) => void; +}; + +export function AuthModal({ closeModal, initialMode = 'login', onModeChange }: AuthModalProps) { + const [mode, setMode] = useState(initialMode); + + // Sync internal mode state when initialMode prop changes + useEffect(() => { + setMode(initialMode); + }, [initialMode]); + + const handleSwitchToSignup = () => { + setMode('signup'); + onModeChange?.('signup'); + }; + + const handleSwitchToLogin = () => { + setMode('login'); + onModeChange?.('login'); + }; + + const handleSuccess = () => { + closeModal(); + }; + + return ( +
+
+ {/* Title */} +
+

+ {mode === 'signup' ? 'Sign Up' : 'Login'} +

+ +
+ + {/* Conditional Form Rendering */} + {mode === 'login' ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..d0dcaac --- /dev/null +++ b/frontend/src/components/auth/LoginForm.tsx @@ -0,0 +1,174 @@ +'use client'; + +import type React from 'react'; +import { useState } from 'react'; +import { useAuth } from '../../../context/AuthContext'; + +type LoginFormProps = { + onSwitchToSignup: () => void; + onSuccess: () => void; +}; + +export function LoginForm({ onSwitchToSignup, onSuccess }: LoginFormProps) { + const { login } = useAuth(); + const [formData, setFormData] = useState({ + email: '', + password: '', + }); + const [rememberMe, setRememberMe] = useState(false); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + await login(formData.email, formData.password); + onSuccess(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleGoogleLogin = () => { + // Add Google OAuth logic here + console.log('Google login clicked'); + }; + + return ( +
+ {/* Error Message */} + {error && ( +
+ {error} +
+ )} + +
+ {/* Email input */} +
+ + setFormData({ ...formData, email: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Password input */} +
+ + setFormData({ ...formData, password: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Remember Me & Forgot Password */} +
+
+ setRememberMe(e.target.checked)} + className="border-white/30 data-[state=checked]:bg-secondary data-[state=checked]:border-secondary" + /> + +
+ +
+ + {/* Login button */} + + + {/* Switch to Signup */} +
+ + Don't have an account? + + +
+
+ + {/* Divider */} +
+
+
+
+
+
+ + Continue with + +
+
+
+ + {/* Google Login button */} + +
+ ); +} diff --git a/frontend/src/components/auth/SignupForm.tsx b/frontend/src/components/auth/SignupForm.tsx new file mode 100644 index 0000000..e6dc6ad --- /dev/null +++ b/frontend/src/components/auth/SignupForm.tsx @@ -0,0 +1,165 @@ +'use client'; + +import type React from 'react'; +import { useState } from 'react'; +import { useAuth } from '../../../context/AuthContext'; + +type SignupFormProps = { + onSwitchToLogin: () => void; + onSuccess: () => void; +}; + +export function SignupForm({ onSwitchToLogin, onSuccess }: SignupFormProps) { + const { signup } = useAuth(); + const [formData, setFormData] = useState({ + email: '', + password: '', + username: '', + }); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + await signup(formData.email, formData.password, formData.username); + onSuccess(); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleGoogleLogin = () => { + // Add Google OAuth logic here + console.log('Google signup clicked'); + }; + + return ( +
+ {/* Error Message */} + {error && ( +
+ {error} +
+ )} + +
+ {/* Email input */} +
+ + setFormData({ ...formData, email: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Username input */} +
+ + setFormData({ ...formData, username: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Password input */} +
+ + setFormData({ ...formData, password: e.target.value }) + } + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Signup button */} + + + {/* Switch to Login */} +
+ + Already have an account? + + +
+
+ + {/* Divider */} +
+
+
+
+
+
+ + Continue with + +
+
+
+ + {/* Google Login button */} + +
+ ); +} diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 95f3f58..7f01792 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -13,7 +13,7 @@ import { PopoverTrigger, } from '@/components/ui/popover'; import { useAuth } from '../../context/AuthContext'; -import { LoginForm } from './LoginModal'; +import { AuthModal } from './auth/AuthModal'; const navItems = [ { label: 'Home', href: '/' }, @@ -26,6 +26,7 @@ export default function Header() { const pathname = usePathname(); const { isAuthenticated, user } = useAuth(); const [modal, setModal] = useState(false); + const [lastAuthMode, setLastAuthMode] = useState<'login' | 'signup'>('login'); const isActive = (href: string) => pathname === href; const avatar = useMemo(() => { return createAvatar(adventurer, { @@ -52,9 +53,8 @@ export default function Header() {
{item.label} @@ -66,19 +66,24 @@ export default function Header() {
{!isAuthenticated && ( - - + +
setModal(true)} > Login / Sign up
- - setModal(false)} /> + { + e.preventDefault(); + }}> + { setModal(false) }} + initialMode={lastAuthMode} + onModeChange={setLastAuthMode} + />
)} From a773f01418d95e13dfe7c146ba953ffbfe8dcba2 Mon Sep 17 00:00:00 2001 From: laryhills Date: Wed, 10 Sep 2025 22:03:29 +0100 Subject: [PATCH 2/2] feat(auth): :sparkles: add forgot and reset password functionality with UI updates --- frontend/service/api/auth.ts | 38 ++++ frontend/src/app/reset-password/page.tsx | 190 ++++++++++++++++++ frontend/src/components/auth/AuthModal.tsx | 21 +- .../components/auth/ForgotPasswordForm.tsx | 94 +++++++++ frontend/src/components/auth/LoginForm.tsx | 14 +- frontend/src/components/header.tsx | 28 ++- frontend/types/auth.entity.ts | 17 ++ 7 files changed, 392 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/reset-password/page.tsx create mode 100644 frontend/src/components/auth/ForgotPasswordForm.tsx diff --git a/frontend/service/api/auth.ts b/frontend/service/api/auth.ts index 088600f..02ba395 100644 --- a/frontend/service/api/auth.ts +++ b/frontend/service/api/auth.ts @@ -5,6 +5,10 @@ import { LoginRequest, SignupRequest, UserResponse, + ForgotPasswordRequest, + ForgotPasswordResponse, + ResetPasswordRequest, + ResetPasswordResponse, } from '../../types/auth.entity'; const getApiUrl = (endpoint: string) => { @@ -68,4 +72,38 @@ export const authApi = { }, }); }, + + forgotPassword: async (data: ForgotPasswordRequest): Promise => { + const response = await fetch(getApiUrl('/auth/forgot-password'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to send reset email'); + } + + return await response.json(); + }, + + resetPassword: async (data: ResetPasswordRequest): Promise => { + const response = await fetch(getApiUrl('/auth/reset-password'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to reset password'); + } + + return await response.json(); + }, }; diff --git a/frontend/src/app/reset-password/page.tsx b/frontend/src/app/reset-password/page.tsx new file mode 100644 index 0000000..c8c8858 --- /dev/null +++ b/frontend/src/app/reset-password/page.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { useEffect, useState, Suspense } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { authApi } from '../../../service/api/auth'; + +function ResetPasswordContent() { + const searchParams = useSearchParams(); + const router = useRouter(); + const token = searchParams.get('token'); + + const [formData, setFormData] = useState({ + newPassword: '', + confirmPassword: '', + }); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (!token) { + setError('Invalid or missing reset token'); + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setSuccess(''); + + if (!token) { + setError('Invalid or missing reset token'); + return; + } + + if (formData.newPassword !== formData.confirmPassword) { + setError('Passwords do not match'); + return; + } + + if (formData.newPassword.length < 8) { + setError('Password must be at least 8 characters long'); + return; + } + + setIsLoading(true); + + try { + const response = await authApi.resetPassword({ + token, + newPassword: formData.newPassword, + }); + setSuccess(response.message); + setFormData({ newPassword: '', confirmPassword: '' }); + + // Redirect to home after 2 seconds with success message + setTimeout(() => { + router.push('/?reset=success'); + }, 2000); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + const handleBackToLogin = () => { + router.push('/'); + }; + + return ( +
+
+
+

+ Reset Password +

+ + {success && ( +
+ {success} +
Redirecting to login...
+
+ )} + + {error && ( +
+ {error} +
+ )} + + {!success && ( +
+
+ Enter your new password below. +
+ + {/* New Password input */} +
+ + setFormData({ ...formData, newPassword: e.target.value }) + } + required + minLength={8} + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Confirm Password input */} +
+ + setFormData({ ...formData, confirmPassword: e.target.value }) + } + required + minLength={8} + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Password requirements */} +
+ Password must be at least 8 characters long. +
+ + + + +
+ +
+
+ )} + + {success && ( +
+ +
+ )} +
+
+
+ ); +} + +export default function ResetPasswordPage() { + return ( + +
Loading...
+ + }> + +
+ ); +} diff --git a/frontend/src/components/auth/AuthModal.tsx b/frontend/src/components/auth/AuthModal.tsx index b502feb..bfcb38b 100644 --- a/frontend/src/components/auth/AuthModal.tsx +++ b/frontend/src/components/auth/AuthModal.tsx @@ -4,16 +4,18 @@ import type React from 'react'; import { useState, useEffect } from 'react'; import { LoginForm } from './LoginForm'; import { SignupForm } from './SignupForm'; +import { ForgotPasswordForm } from './ForgotPasswordForm'; -type AuthMode = 'login' | 'signup'; +type AuthMode = 'login' | 'signup' | 'forgot-password'; type AuthModalProps = { closeModal: () => void; initialMode?: AuthMode; onModeChange?: (mode: AuthMode) => void; + resetSuccess?: boolean; }; -export function AuthModal({ closeModal, initialMode = 'login', onModeChange }: AuthModalProps) { +export function AuthModal({ closeModal, initialMode = 'login', onModeChange, resetSuccess }: AuthModalProps) { const [mode, setMode] = useState(initialMode); // Sync internal mode state when initialMode prop changes @@ -31,6 +33,11 @@ export function AuthModal({ closeModal, initialMode = 'login', onModeChange }: A onModeChange?.('login'); }; + const handleSwitchToForgotPassword = () => { + setMode('forgot-password'); + onModeChange?.('forgot-password'); + }; + const handleSuccess = () => { closeModal(); }; @@ -47,7 +54,7 @@ export function AuthModal({ closeModal, initialMode = 'login', onModeChange }: A {/* Title */}

- {mode === 'signup' ? 'Sign Up' : 'Login'} + {mode === 'signup' ? 'Sign Up' : mode === 'forgot-password' ? 'Forgot Password' : 'Login'}

diff --git a/frontend/src/components/auth/ForgotPasswordForm.tsx b/frontend/src/components/auth/ForgotPasswordForm.tsx new file mode 100644 index 0000000..7ecefa4 --- /dev/null +++ b/frontend/src/components/auth/ForgotPasswordForm.tsx @@ -0,0 +1,94 @@ +'use client'; + +import type React from 'react'; +import { useState } from 'react'; +import { authApi } from '../../../service/api/auth'; + +type ForgotPasswordFormProps = { + onSwitchToLogin: () => void; +}; + +export function ForgotPasswordForm({ onSwitchToLogin }: ForgotPasswordFormProps) { + const [email, setEmail] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setSuccess(''); + setIsLoading(true); + + try { + const response = await authApi.forgotPassword({ email }); + setSuccess(response.message); + setEmail(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {success && ( +
+ {success} +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+
+ Enter your email address and we'll send you a link to reset your password. +
+ + {/* Email input */} +
+ setEmail(e.target.value)} + required + className="h-14 bg-white/5 border-white/10 text-white placeholder:text-gray-400 rounded-xl focus:border-secondary focus:ring-secondary/20 w-full px-2" + /> +
+ + {/* Send Reset Email button */} + + + {/* Back to Login */} +
+ + Remember your password? + + +
+
+
+ ); +} diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index d0dcaac..29d1445 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -6,10 +6,12 @@ import { useAuth } from '../../../context/AuthContext'; type LoginFormProps = { onSwitchToSignup: () => void; + onSwitchToForgotPassword: () => void; onSuccess: () => void; + resetSuccess?: boolean; }; -export function LoginForm({ onSwitchToSignup, onSuccess }: LoginFormProps) { +export function LoginForm({ onSwitchToSignup, onSwitchToForgotPassword, onSuccess, resetSuccess }: LoginFormProps) { const { login } = useAuth(); const [formData, setFormData] = useState({ email: '', @@ -41,6 +43,13 @@ export function LoginForm({ onSwitchToSignup, onSuccess }: LoginFormProps) { return (
+ {/* Success Message for Password Reset */} + {resetSuccess && ( +
+ Password has been reset successfully! You can now login with your new password. +
+ )} + {/* Error Message */} {error && (
@@ -93,6 +102,7 @@ export function LoginForm({ onSwitchToSignup, onSuccess }: LoginFormProps) {