tahamajs/IE / Projects /front /src /components /auth /RegisterForm.tsx
tahamajs's picture
download
raw
10.8 kB
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.