summaryrefslogtreecommitdiff
path: root/frontend/src/pages/AuthPage.tsx
diff options
context:
space:
mode:
authorblackhao <13851610112@163.com>2025-12-10 20:12:21 -0600
committerblackhao <13851610112@163.com>2025-12-10 20:12:21 -0600
commit9646da833bc3d94564c10649b62a378d0190471e (patch)
treed0c9c0584b8c4f167c281f5970f713b239a1d7c5 /frontend/src/pages/AuthPage.tsx
parent9ba956c7aa601f0e6cd0fe2ede907cbc558fa1b8 (diff)
user data
Diffstat (limited to 'frontend/src/pages/AuthPage.tsx')
-rw-r--r--frontend/src/pages/AuthPage.tsx303
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;
+