diff options
| author | blackhao <13851610112@163.com> | 2025-12-10 20:12:21 -0600 |
|---|---|---|
| committer | blackhao <13851610112@163.com> | 2025-12-10 20:12:21 -0600 |
| commit | 9646da833bc3d94564c10649b62a378d0190471e (patch) | |
| tree | d0c9c0584b8c4f167c281f5970f713b239a1d7c5 /frontend/src/pages/AuthPage.tsx | |
| parent | 9ba956c7aa601f0e6cd0fe2ede907cbc558fa1b8 (diff) | |
user data
Diffstat (limited to 'frontend/src/pages/AuthPage.tsx')
| -rw-r--r-- | frontend/src/pages/AuthPage.tsx | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/frontend/src/pages/AuthPage.tsx b/frontend/src/pages/AuthPage.tsx new file mode 100644 index 0000000..d213190 --- /dev/null +++ b/frontend/src/pages/AuthPage.tsx @@ -0,0 +1,303 @@ +import React, { useState, useCallback } from 'react'; +import { useAuthStore } from '../store/authStore'; +import { Loader2, User, Mail, Lock, AlertCircle, CheckCircle, XCircle } from 'lucide-react'; + +const API_BASE = 'http://localhost:8000'; + +interface AuthPageProps { + onSuccess?: () => void; +} + +const AuthPage: React.FC<AuthPageProps> = ({ onSuccess }) => { + const [isLogin, setIsLogin] = useState(true); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [localError, setLocalError] = useState(''); + + // Real-time availability checks + const [usernameStatus, setUsernameStatus] = useState<'idle' | 'checking' | 'available' | 'taken'>('idle'); + const [emailStatus, setEmailStatus] = useState<'idle' | 'checking' | 'available' | 'taken'>('idle'); + + const { login, register, isLoading, error, clearError } = useAuthStore(); + + // Check username availability + const checkUsername = useCallback(async (value: string) => { + if (!value.trim() || value.length < 2) { + setUsernameStatus('idle'); + return; + } + setUsernameStatus('checking'); + try { + const res = await fetch(`${API_BASE}/api/auth/check-username/${encodeURIComponent(value)}`); + const data = await res.json(); + setUsernameStatus(data.available ? 'available' : 'taken'); + } catch { + setUsernameStatus('idle'); + } + }, []); + + // Check email availability + const checkEmail = useCallback(async (value: string) => { + if (!value.trim() || !value.includes('@')) { + setEmailStatus('idle'); + return; + } + setEmailStatus('checking'); + try { + const res = await fetch(`${API_BASE}/api/auth/check-email/${encodeURIComponent(value)}`); + const data = await res.json(); + setEmailStatus(data.available ? 'available' : 'taken'); + } catch { + setEmailStatus('idle'); + } + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLocalError(''); + clearError(); + + // Validation + if (!username.trim()) { + setLocalError('Username is required'); + return; + } + if (!password) { + setLocalError('Password is required'); + return; + } + + if (!isLogin) { + if (!email.trim()) { + setLocalError('Email is required'); + return; + } + if (password !== confirmPassword) { + setLocalError('Passwords do not match'); + return; + } + if (password.length < 6) { + setLocalError('Password must be at least 6 characters'); + return; + } + if (usernameStatus === 'taken') { + setLocalError('Username is already taken'); + return; + } + if (emailStatus === 'taken') { + setLocalError('Email is already registered'); + return; + } + } + + try { + if (isLogin) { + await login(username, password); + onSuccess?.(); + } else { + await register(username, email, password); + // Auto-login after successful registration + await login(username, password); + onSuccess?.(); + } + } catch (err) { + // Error is handled by the store + } + }; + + const switchMode = () => { + setIsLogin(!isLogin); + setLocalError(''); + clearError(); + setPassword(''); + setConfirmPassword(''); + setUsernameStatus('idle'); + setEmailStatus('idle'); + }; + + const displayError = localError || error; + + return ( + <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900"> + {/* Background pattern */} + <div className="absolute inset-0 overflow-hidden pointer-events-none"> + <div className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-to-br from-blue-500/10 to-transparent rounded-full blur-3xl" /> + <div className="absolute -bottom-1/2 -right-1/2 w-full h-full bg-gradient-to-tl from-purple-500/10 to-transparent rounded-full blur-3xl" /> + </div> + + <div className="relative z-10 w-full max-w-md px-6"> + {/* Logo/Brand */} + <div className="text-center mb-8"> + <h1 className="text-4xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent"> + ContextFlow + </h1> + <p className="text-gray-400 mt-2"> + {isLogin ? 'Welcome back!' : 'Create your account'} + </p> + </div> + + {/* Card */} + <div className="bg-gray-800/50 backdrop-blur-xl border border-gray-700/50 rounded-2xl shadow-2xl p-8"> + {/* Error Message */} + {displayError && ( + <div className="mb-6 p-4 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-3"> + <AlertCircle className="text-red-400 flex-shrink-0" size={20} /> + <p className="text-red-400 text-sm">{displayError}</p> + </div> + )} + + <form onSubmit={handleSubmit} className="space-y-5"> + {/* Username */} + <div> + <label className="block text-sm font-medium text-gray-300 mb-2"> + Username + </label> + <div className="relative"> + <User className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={18} /> + <input + type="text" + value={username} + onChange={(e) => { + setUsername(e.target.value); + if (!isLogin) setUsernameStatus('idle'); + }} + onBlur={() => !isLogin && checkUsername(username)} + placeholder="Enter your username" + className={`w-full pl-10 pr-10 py-3 bg-gray-900/50 border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all ${ + !isLogin && usernameStatus === 'taken' ? 'border-red-500' : + !isLogin && usernameStatus === 'available' ? 'border-green-500' : 'border-gray-700' + }`} + autoComplete="username" + /> + {!isLogin && usernameStatus !== 'idle' && ( + <div className="absolute right-3 top-1/2 -translate-y-1/2"> + {usernameStatus === 'checking' && <Loader2 className="animate-spin text-gray-400" size={18} />} + {usernameStatus === 'available' && <CheckCircle className="text-green-500" size={18} />} + {usernameStatus === 'taken' && <XCircle className="text-red-500" size={18} />} + </div> + )} + </div> + {!isLogin && usernameStatus === 'taken' && ( + <p className="text-red-400 text-xs mt-1">This username is already taken</p> + )} + </div> + + {/* Email (only for register) */} + {!isLogin && ( + <div> + <label className="block text-sm font-medium text-gray-300 mb-2"> + Email + </label> + <div className="relative"> + <Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={18} /> + <input + type="email" + value={email} + onChange={(e) => { + setEmail(e.target.value); + setEmailStatus('idle'); + }} + onBlur={() => checkEmail(email)} + placeholder="Enter your email" + className={`w-full pl-10 pr-10 py-3 bg-gray-900/50 border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all ${ + emailStatus === 'taken' ? 'border-red-500' : + emailStatus === 'available' ? 'border-green-500' : 'border-gray-700' + }`} + autoComplete="email" + /> + {emailStatus !== 'idle' && ( + <div className="absolute right-3 top-1/2 -translate-y-1/2"> + {emailStatus === 'checking' && <Loader2 className="animate-spin text-gray-400" size={18} />} + {emailStatus === 'available' && <CheckCircle className="text-green-500" size={18} />} + {emailStatus === 'taken' && <XCircle className="text-red-500" size={18} />} + </div> + )} + </div> + {emailStatus === 'taken' && ( + <p className="text-red-400 text-xs mt-1">This email is already registered</p> + )} + </div> + )} + + {/* Password */} + <div> + <label className="block text-sm font-medium text-gray-300 mb-2"> + Password + </label> + <div className="relative"> + <Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={18} /> + <input + type="password" + value={password} + onChange={(e) => setPassword(e.target.value)} + placeholder="Enter your password" + className="w-full pl-10 pr-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" + autoComplete={isLogin ? "current-password" : "new-password"} + /> + </div> + </div> + + {/* Confirm Password (only for register) */} + {!isLogin && ( + <div> + <label className="block text-sm font-medium text-gray-300 mb-2"> + Confirm Password + </label> + <div className="relative"> + <Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={18} /> + <input + type="password" + value={confirmPassword} + onChange={(e) => setConfirmPassword(e.target.value)} + placeholder="Confirm your password" + className="w-full pl-10 pr-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" + autoComplete="new-password" + /> + </div> + </div> + )} + + {/* Submit Button */} + <button + type="submit" + disabled={isLoading} + className="w-full py-3 px-4 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-medium rounded-lg transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-500/25" + > + {isLoading ? ( + <> + <Loader2 className="animate-spin" size={20} /> + <span>{isLogin ? 'Signing in...' : 'Creating account...'}</span> + </> + ) : ( + <span>{isLogin ? 'Sign In' : 'Create Account'}</span> + )} + </button> + </form> + + {/* Switch Mode */} + <div className="mt-6 text-center"> + <p className="text-gray-400"> + {isLogin ? "Don't have an account? " : "Already have an account? "} + <button + onClick={switchMode} + className="text-blue-400 hover:text-blue-300 font-medium transition-colors" + > + {isLogin ? 'Sign up' : 'Sign in'} + </button> + </p> + </div> + </div> + + {/* Footer */} + <p className="text-center text-gray-500 text-sm mt-6"> + Visual LLM Conversation Graph Editor + </p> + </div> + </div> + ); +}; + +export default AuthPage; + |
