| import { useState, FormEvent } from "react"; | |
| import { Link, useNavigate } from "react-router-dom"; | |
| import api from "@/lib/api"; | |
| import { toast } from "react-toastify"; | |
| import { motion } from "framer-motion"; | |
| import { User, Mail, Lock, Eye, EyeOff, Loader2, CheckCircle, AlertCircle } from "lucide-react"; | |
| interface RegisterFormProps { | |
| onSuccess?: () => void; | |
| } | |
| export function RegisterForm({ onSuccess }: RegisterFormProps) { | |
| const navigate = useNavigate(); | |
| const [form, setForm] = useState({ | |
| username: "", | |
| email: "", | |
| password: "", | |
| confirmPassword: "", | |
| }); | |
| const [showPassword, setShowPassword] = useState(false); | |
| const [showConfirm, setShowConfirm] = useState(false); | |
| const [loading, setLoading] = useState(false); | |
| const [errors, setErrors] = useState<Record<string, string>>({}); | |
| const [touched, setTouched] = useState<Record<string, boolean>>({}); | |
| // Password strength calculation | |
| const calculateStrength = (pwd: string) => { | |
| let score = 0; | |
| if (pwd.length >= 6) score++; | |
| if (pwd.length >= 10) score++; | |
| if (/[A-Z]/.test(pwd)) score++; | |
| if (/[0-9]/.test(pwd)) score++; | |
| if (/[^A-Za-z0-9]/.test(pwd)) score++; | |
| return Math.min(score, 5); | |
| }; | |
| const strengthScore = calculateStrength(form.password); | |
| const strengthColor = () => { | |
| if (strengthScore <= 1) return "bg-red-500"; | |
| if (strengthScore <= 3) return "bg-yellow-500"; | |
| return "bg-green-500"; | |
| }; | |
| const strengthText = () => { | |
| if (strengthScore <= 1) return "Weak"; | |
| if (strengthScore <= 3) return "Medium"; | |
| return "Strong"; | |
| }; | |
| const validateField = (name: string, value: string) => { | |
| switch (name) { | |
| case "username": | |
| if (value.length < 3) return "Username must be at least 3 characters"; | |
| if (!/^[a-zA-Z0-9_]+$/.test(value)) return "Username can only contain letters, numbers, and underscores"; | |
| return ""; | |
| case "email": | |
| if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "Invalid email address"; | |
| return ""; | |
| case "password": | |
| if (value.length < 6) return "Password must be at least 6 characters"; | |
| return ""; | |
| case "confirmPassword": | |
| if (value !== form.password) return "Passwords do not match"; | |
| return ""; | |
| default: | |
| return ""; | |
| } | |
| }; | |
| const validateForm = () => { | |
| const newErrors: Record<string, string> = {}; | |
| newErrors.username = validateField("username", form.username); | |
| newErrors.email = validateField("email", form.email); | |
| newErrors.password = validateField("password", form.password); | |
| newErrors.confirmPassword = validateField("confirmPassword", form.confirmPassword); | |
| setErrors(newErrors); | |
| return !Object.values(newErrors).some(e => e); | |
| }; | |
| const handleBlur = (name: string) => { | |
| setTouched(prev => ({ ...prev, [name]: true })); | |
| setErrors(prev => ({ ...prev, [name]: validateField(name, form[name as keyof typeof form]) })); | |
| }; | |
| const handleSubmit = async (e: FormEvent) => { | |
| e.preventDefault(); | |
| if (!validateForm()) return; | |
| setLoading(true); | |
| try { | |
| await api.post("/auth/register", { | |
| username: form.username.trim(), | |
| email: form.email.trim() || undefined, | |
| password: form.password, | |
| }); | |
| toast.success("Account created! You can now log in."); | |
| onSuccess?.(); | |
| navigate("/login?registered=true"); | |
| } catch (err: any) { | |
| const msg = err.response?.data?.error || err.response?.data?.message || "Registration failed"; | |
| toast.error(msg); | |
| setErrors(prev => ({ ...prev, general: msg })); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="w-full max-w-md bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-8" | |
| > | |
| <div className="text-center mb-6"> | |
| <h2 className="text-3xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"> | |
| Join the Platform | |
| </h2> | |
| <p className="text-gray-500 dark:text-gray-400 mt-1">Create your free research account</p> | |
| </div> | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| {/* Username */} | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | |
| Username * | |
| </label> | |
| <div className="relative"> | |
| <User className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> | |
| <input | |
| type="text" | |
| className={`w-full pl-10 pr-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary transition | |
| ${touched.username && errors.username ? "border-red-500" : "border-gray-300 dark:border-gray-600"}`} | |
| placeholder="myusername" | |
| value={form.username} | |
| onChange={(e) => setForm({ ...form, username: e.target.value })} | |
| onBlur={() => handleBlur("username")} | |
| disabled={loading} | |
| /> | |
| </div> | |
| {touched.username && errors.username && ( | |
| <p className="mt-1 text-xs text-red-500 flex items-center gap-1"> | |
| <AlertCircle className="w-3 h-3" /> {errors.username} | |
| </p> | |
| )} | |
| </div> | |
| {/* Email */} | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | |
| Email (optional) | |
| </label> | |
| <div className="relative"> | |
| <Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> | |
| <input | |
| type="email" | |
| className={`w-full pl-10 pr-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary transition | |
| ${touched.email && errors.email ? "border-red-500" : "border-gray-300 dark:border-gray-600"}`} | |
| placeholder="you@example.com" | |
| value={form.email} | |
| onChange={(e) => setForm({ ...form, email: e.target.value })} | |
| onBlur={() => handleBlur("email")} | |
| disabled={loading} | |
| /> | |
| </div> | |
| {touched.email && errors.email && ( | |
| <p className="mt-1 text-xs text-red-500 flex items-center gap-1"> | |
| <AlertCircle className="w-3 h-3" /> {errors.email} | |
| </p> | |
| )} | |
| </div> | |
| {/* Password */} | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | |
| Password * | |
| </label> | |
| <div className="relative"> | |
| <Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> | |
| <input | |
| type={showPassword ? "text" : "password"} | |
| className={`w-full pl-10 pr-10 py-2 border rounded-lg focus:ring-2 focus:ring-primary transition | |
| ${touched.password && errors.password ? "border-red-500" : "border-gray-300 dark:border-gray-600"}`} | |
| placeholder="••••••••" | |
| value={form.password} | |
| onChange={(e) => setForm({ ...form, password: e.target.value })} | |
| onBlur={() => handleBlur("password")} | |
| disabled={loading} | |
| /> | |
| <button | |
| type="button" | |
| onClick={() => setShowPassword(!showPassword)} | |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600" | |
| > | |
| {showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} | |
| </button> | |
| </div> | |
| <div className="mt-1"> | |
| <div className="h-1 w-full bg-gray-200 rounded-full overflow-hidden"> | |
| <div className={`h-full ${strengthColor()}`} style={{ width: `${(strengthScore / 5) * 100}%` }} /> | |
| </div> | |
| <p className="text-xs text-gray-500 mt-1"> | |
| Password strength: <span className="font-medium">{strengthText()}</span> (min 6 chars) | |
| </p> | |
| </div> | |
| {touched.password && errors.password && ( | |
| <p className="mt-1 text-xs text-red-500 flex items-center gap-1"> | |
| <AlertCircle className="w-3 h-3" /> {errors.password} | |
| </p> | |
| )} | |
| </div> | |
| {/* Confirm Password */} | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> | |
| Confirm Password * | |
| </label> | |
| <div className="relative"> | |
| <Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5" /> | |
| <input | |
| type={showConfirm ? "text" : "password"} | |
| className={`w-full pl-10 pr-10 py-2 border rounded-lg focus:ring-2 focus:ring-primary transition | |
| ${touched.confirmPassword && errors.confirmPassword ? "border-red-500" : "border-gray-300 dark:border-gray-600"}`} | |
| placeholder="••••••••" | |
| value={form.confirmPassword} | |
| onChange={(e) => setForm({ ...form, confirmPassword: e.target.value })} | |
| onBlur={() => handleBlur("confirmPassword")} | |
| disabled={loading} | |
| /> | |
| <button | |
| type="button" | |
| onClick={() => setShowConfirm(!showConfirm)} | |
| className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600" | |
| > | |
| {showConfirm ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} | |
| </button> | |
| </div> | |
| {touched.confirmPassword && errors.confirmPassword && ( | |
| <p className="mt-1 text-xs text-red-500 flex items-center gap-1"> | |
| <AlertCircle className="w-3 h-3" /> {errors.confirmPassword} | |
| </p> | |
| )} | |
| </div> | |
| {errors.general && ( | |
| <div className="p-2 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm"> | |
| {errors.general} | |
| </div> | |
| )} | |
| <button | |
| type="submit" | |
| disabled={loading} | |
| className="w-full bg-primary hover:bg-primary/90 text-white font-semibold py-2 rounded-lg transition disabled:opacity-50 flex items-center justify-center gap-2" | |
| > | |
| {loading ? <Loader2 className="animate-spin w-5 h-5" /> : <CheckCircle className="w-5 h-5" />} | |
| {loading ? "Creating account..." : "Create Account"} | |
| </button> | |
| </form> | |
| <p className="text-center text-sm mt-6 text-gray-600 dark:text-gray-400"> | |
| Already have an account?{" "} | |
| <Link to="/login" className="text-primary font-semibold hover:underline"> | |
| Sign in | |
| </Link> | |
| </p> | |
| </motion.div> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 10.8 kB
- Xet hash:
- 0ec19768fec85bd569761695c44a2a63808e8f2422168295bc8f64c65ce5ff0c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.