diff --git a/frontend/src/Pages/Authentication/forms.tsx b/frontend/src/Pages/Authentication/forms.tsx index 221257f9..759c8294 100644 --- a/frontend/src/Pages/Authentication/forms.tsx +++ b/frontend/src/Pages/Authentication/forms.tsx @@ -1,7 +1,6 @@ - import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { useContext, useState, useEffect } from 'react'; +import { useContext, useState, useEffect, useRef } from 'react'; import { AuthContext } from '../../context/authContext'; import { useCallback } from "react"; @@ -10,6 +9,110 @@ interface LoginFormProps { infoMessage?: string; } +// Email autocomplete component +interface EmailInputWithSuggestionsProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + className?: string; +} + +const EmailInputWithSuggestions: React.FC = ({ + value, + onChange, + placeholder = "name@example.com", + className = "" +}) => { + const [showSuggestions, setShowSuggestions] = useState(false); + const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(-1); + const wrapperRef = useRef(null); + + const emailDomains = ['@gmail.com', '@yahoo.com', '@outlook.com', '@hotmail.com']; + + const getEmailSuggestions = (email: string) => { + if (!email || email.includes('@')) return []; + return emailDomains.map(domain => email + domain); + }; + + const suggestions = getEmailSuggestions(value); + + const handleInputChange = (newValue: string) => { + onChange(newValue); + if (newValue && !newValue.includes('@')) { + setShowSuggestions(true); + setActiveSuggestionIndex(-1); + } else { + setShowSuggestions(false); + } + }; + + const handleSuggestionClick = (suggestion: string) => { + onChange(suggestion); + setShowSuggestions(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!showSuggestions || suggestions.length === 0) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + setActiveSuggestionIndex(prev => + prev < suggestions.length - 1 ? prev + 1 : prev + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setActiveSuggestionIndex(prev => prev > 0 ? prev - 1 : -1); + } else if (e.key === 'Enter' && activeSuggestionIndex >= 0) { + e.preventDefault(); + handleSuggestionClick(suggestions[activeSuggestionIndex]); + } else if (e.key === 'Escape') { + setShowSuggestions(false); + } + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setShowSuggestions(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ handleInputChange(e.target.value)} + onKeyDown={handleKeyDown} + onFocus={() => value && !value.includes('@') && setShowSuggestions(true)} + placeholder={placeholder} + className={className} + /> + {showSuggestions && suggestions.length > 0 && ( +
+ {suggestions.map((suggestion, index) => ( +
handleSuggestionClick(suggestion)} + className={`px-4 py-2 cursor-pointer ${ + index === activeSuggestionIndex + ? 'bg-gray-100 dark:bg-slate-700' + : 'hover:bg-gray-100 dark:hover:bg-slate-700' + }`} + > + {suggestion} +
+ ))} +
+ )} +
+ ); +}; + export const LoginForm: React.FC = ({ startForgotPassword, infoMessage }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -24,28 +127,50 @@ export const LoginForm: React.FC = ({ startForgotPassword, infoM const [localError, setLocalError] = useState(null); + const MIN_PASSWORD_LENGTH = 8; -const MIN_PASSWORD_LENGTH = 8; -const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (password.length < MIN_PASSWORD_LENGTH) { - setLocalError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`); - return; - } - setLocalError(null); - await login(email, password); -}; + const validateEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLocalError(null); + // Validate email + if (!email.trim()) { + setLocalError('Please enter your email address'); + return; + } + if (!validateEmail(email)) { + setLocalError('Please enter a valid email address'); + return; + } + + // Validate password + if (!password) { + setLocalError('Please enter your password'); + return; + } + + if (password.length < MIN_PASSWORD_LENGTH) { + setLocalError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`); + return; + } + + await login(email, password); + }; + + const handleGoogleLogin = useCallback( + (response: { credential: string; select_by: string }) => { + const idToken = response.credential; + googleLogin(idToken); + }, + [googleLogin] + ); -const handleGoogleLogin = useCallback( - (response: { credential: string; select_by: string }) => { - const idToken = response.credential; - googleLogin(idToken); - }, - [googleLogin] -); useEffect(() => { const google = window.google; if (!google?.accounts) { @@ -75,11 +200,10 @@ const handleGoogleLogin = useCallback( return (
{infoMessage &&

{infoMessage}

} - setEmail(e.target.value)} + onChange={setEmail} + placeholder="name@example.com" className="mb-2 dark:border-white" /> {localError && ( -

+

{localError}

-)} + )}
= ({ startOtpVerification }) const { signup, googleLogin, error, loading } = authContext; + const [localError, setLocalError] = useState(null); + + const MIN_PASSWORD_LENGTH = 8; + + const validateEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + setLocalError(null); + + // Validate email + if (!email.trim()) { + setLocalError('Please enter your email address'); + return; + } + + if (!validateEmail(email)) { + setLocalError('Please enter a valid email address'); + return; + } + + // Validate password + if (!password) { + setLocalError('Please enter a password'); + return; + } + + if (password.length < MIN_PASSWORD_LENGTH) { + setLocalError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`); + return; + } + + // Validate confirm password + if (!confirmPassword) { + setLocalError('Please confirm your password'); + return; + } if (password !== confirmPassword) { - authContext.handleError('Passwords do not match'); + setLocalError('Passwords do not match'); return; } @@ -149,14 +311,13 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) startOtpVerification(email); }; - const handleGoogleLogin = useCallback( - (response: { credential: string; select_by: string }) => { - const idToken = response.credential; - googleLogin(idToken); - }, - [googleLogin] -); - + const handleGoogleLogin = useCallback( + (response: { credential: string; select_by: string }) => { + const idToken = response.credential; + googleLogin(idToken); + }, + [googleLogin] + ); useEffect(() => { const google = window.google; @@ -186,11 +347,10 @@ export const SignUpForm: React.FC = ({ startOtpVerification }) return ( - setEmail(e.target.value)} + onChange={setEmail} + placeholder="name@example.com" className="mb-2 dark:border-white" /> = ({ startOtpVerification })
show password
+ {localError &&

{localError}

} {error &&

{error}

} - -
-

Notifications

+ + {/* Header */} +
+

Notifications

{unreadCount > 0 && ( - )}
+ + {/* Notification List */} {notifications.length === 0 ? ( -
+
No notifications yet
) : ( -
+
{notifications.map((notification) => ( -
handleNotificationClick(notification)} > + {/* Delete button */} +
+ {/* Unread dot */}
-

+

{notification.title}

-

+

{notification.message}

-

+

{new Date(notification.createdAt).toLocaleDateString()}

@@ -188,47 +196,48 @@ function Header() { - + + {/* Avatar / Profile */} - -
+ +
User avatar
-

{user?.displayName || "User"}

-

{user?.email || "No email"}

+

{user?.displayName || "User"}

+

{user?.email || "No email"}

- User ID - + User ID + {user?.id || "N/A"}
- Rating - {user?.rating ? Math.round(user.rating) : 1500} + Rating + {user?.rating ? Math.round(user.rating) : 1500}
+ {/* Mobile Drawer */} {isDrawerOpen && (
-
-
+ /> +
+
- - DebateAI by - + DebateAI by DebateAI Logo
@@ -319,10 +308,10 @@ function NavItem({ to, label, icon, onClick }: NavItemProps) { to={to} onClick={onClick} className={({ isActive }) => - `group flex items-center px-2 py-2 text-sm font-medium rounded-md ${ + `group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors ${ isActive - ? "bg-gray-200 text-gray-900" - : "text-gray-600 hover:bg-gray-100 hover:text-gray-900" + ? "bg-accent text-accent-foreground" + : "text-muted-foreground hover:bg-accent hover:text-foreground" }` } > @@ -332,4 +321,4 @@ function NavItem({ to, label, icon, onClick }: NavItemProps) { ); } -export default Header; +export default Header; \ No newline at end of file