From 222d08195d7788f554fbb56802c97891504d1bac Mon Sep 17 00:00:00 2001 From: Rukayat Zakariyau Date: Sat, 30 May 2026 13:15:57 +0100 Subject: [PATCH] feat: add RegisterPage component --- .../module/auth/register/RegisterPage.tsx | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 frontend/module/auth/register/RegisterPage.tsx diff --git a/frontend/module/auth/register/RegisterPage.tsx b/frontend/module/auth/register/RegisterPage.tsx new file mode 100644 index 0000000..abc4b01 --- /dev/null +++ b/frontend/module/auth/register/RegisterPage.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +interface RegisterForm { + fullName: string; + email: string; + password: string; + confirmPassword: string; +} + +interface FieldErrors { + fullName?: string; + email?: string; + password?: string; + confirmPassword?: string; +} + +function getStrength(password: string): { label: string; color: string; width: string } { + if (password.length === 0) return { label: "", color: "", width: "w-0" }; + let score = 0; + if (password.length >= 8) score++; + if (/[A-Z]/.test(password)) score++; + if (/[0-9]/.test(password)) score++; + if (/[^A-Za-z0-9]/.test(password)) score++; + if (score <= 1) return { label: "Weak", color: "bg-red-500", width: "w-1/3" }; + if (score <= 2) return { label: "Medium", color: "bg-yellow-500", width: "w-2/3" }; + return { label: "Strong", color: "bg-green-500", width: "w-full" }; +} + +export default function RegisterPage() { + const router = useRouter(); + const [form, setForm] = useState({ + fullName: "", + email: "", + password: "", + confirmPassword: "", + }); + const [errors, setErrors] = useState({}); + const [serverError, setServerError] = useState(null); + const [loading, setLoading] = useState(false); + + const strength = getStrength(form.password); + + function validate(): boolean { + const next: FieldErrors = {}; + if (!form.fullName.trim()) next.fullName = "Full name is required"; + if (!form.email.trim()) next.email = "Email is required"; + else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) + next.email = "Enter a valid email address"; + if (!form.password) next.password = "Password is required"; + if (!form.confirmPassword) next.confirmPassword = "Please confirm your password"; + else if (form.password !== form.confirmPassword) + next.confirmPassword = "Passwords do not match"; + setErrors(next); + return Object.keys(next).length === 0; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setServerError(null); + if (!validate()) return; + setLoading(true); + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/auth/register`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fullName: form.fullName, + email: form.email, + password: form.password, + }), + } + ); + if (res.status === 409) { + setServerError("An account with this email already exists."); + return; + } + if (!res.ok) { + setServerError("Something went wrong. Please try again."); + return; + } + router.push("/login?registered=true"); + } catch { + setServerError("Network error. Please try again."); + } finally { + setLoading(false); + } + } + + function field( + id: keyof RegisterForm, + label: string, + type: string, + autoComplete: string + ) { + return ( +
+ + setForm({ ...form, [id]: e.target.value })} + aria-describedby={errors[id] ? `${id}-error` : undefined} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + {errors[id] && ( +

+ {errors[id]} +

+ )} +
+ ); + } + + return ( +
+
+

Create account

+ + {serverError && ( +

+ {serverError} +

+ )} + +
+ {field("fullName", "Full name", "text", "name")} + {field("email", "Email", "email", "email")} + +
+ + setForm({ ...form, password: e.target.value })} + aria-describedby={errors.password ? "password-error" : undefined} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + {form.password && ( +
+
+
+
+

{strength.label}

+
+ )} + {errors.password && ( +

+ {errors.password} +

+ )} +
+ + {field("confirmPassword", "Confirm password", "password", "new-password")} + + + +
+
+ ); +}