175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
// app/auth/sign-up/sign-up.tsx
|
|
"use client";
|
|
|
|
import { useState, useCallback } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
type Props = { nextPath?: string };
|
|
|
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
export default function SignUp({ nextPath = "/portal" }: Props) {
|
|
const router = useRouter();
|
|
const [email, setEmail] = useState("");
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
const onSubmit = useCallback(
|
|
async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
setErr(null);
|
|
|
|
const em = email.trim().toLowerCase();
|
|
const un = username.trim();
|
|
|
|
if (!em || !un || !password || !confirmPassword) {
|
|
setErr("All fields are required.");
|
|
return;
|
|
}
|
|
if (!EMAIL_RE.test(em)) {
|
|
setErr("Please enter a valid email address.");
|
|
return;
|
|
}
|
|
if (password.length < 8) {
|
|
setErr("Password must be at least 8 characters.");
|
|
return;
|
|
}
|
|
if (password !== confirmPassword) {
|
|
setErr("Passwords do not match.");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/auth/register", {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
body: JSON.stringify({ email: em, username: un, password, confirmPassword }),
|
|
});
|
|
|
|
const txt = await res.text();
|
|
let j: any = null;
|
|
try {
|
|
j = txt ? JSON.parse(txt) : null;
|
|
} catch {
|
|
j = null;
|
|
}
|
|
|
|
if (!res.ok) {
|
|
const message =
|
|
j?.error ||
|
|
j?.message ||
|
|
(res.status === 409 ? "Email or username already in use." : `Sign-up failed (${res.status})`);
|
|
throw new Error(message);
|
|
}
|
|
|
|
router.replace(nextPath);
|
|
router.refresh();
|
|
} catch (e: any) {
|
|
setErr(e?.message || "Unable to sign up.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[email, username, password, confirmPassword, nextPath, router]
|
|
);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-md rounded-lg border p-6">
|
|
<h1 className="mb-1 text-2xl font-semibold">Create Account</h1>
|
|
<p className="mb-6 text-sm opacity-70">Join MakeArmy to manage rigs, settings, and projects.</p>
|
|
|
|
<form className="space-y-4" onSubmit={onSubmit}>
|
|
<div className="space-y-1">
|
|
<label className="text-sm font-medium">Email</label>
|
|
<input
|
|
type="email"
|
|
autoComplete="email"
|
|
className="w-full rounded-md border px-3 py-2"
|
|
placeholder="you@example.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.currentTarget.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-sm font-medium">Username</label>
|
|
<input
|
|
type="text"
|
|
autoComplete="username"
|
|
className="w-full rounded-md border px-3 py-2"
|
|
placeholder="your-handle"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.currentTarget.value)}
|
|
required
|
|
minLength={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm font-medium">Password</label>
|
|
<button
|
|
type="button"
|
|
className="text-xs opacity-70 hover:opacity-100"
|
|
onClick={() => setShowPassword((s) => !s)}
|
|
>
|
|
{showPassword ? "Hide" : "Show"}
|
|
</button>
|
|
</div>
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
autoComplete="new-password"
|
|
className="w-full rounded-md border px-3 py-2"
|
|
placeholder="At least 8 characters"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.currentTarget.value)}
|
|
required
|
|
minLength={8}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-sm font-medium">Confirm Password</label>
|
|
<input
|
|
type={showPassword ? "text" : "password"}
|
|
autoComplete="new-password"
|
|
className="w-full rounded-md border px-3 py-2"
|
|
placeholder="Re-enter your password"
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
|
|
required
|
|
minLength={8}
|
|
/>
|
|
</div>
|
|
|
|
{err && (
|
|
<div className="rounded-md border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
|
|
{err}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full rounded-md bg-black px-3 py-2 text-white disabled:opacity-60"
|
|
>
|
|
{loading ? "Creating account…" : "Sign Up"}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="mt-4 text-center text-sm">
|
|
<span className="opacity-70">Already have an account?</span>{" "}
|
|
<a className="underline" href={`/auth/sign-in?next=${encodeURIComponent(nextPath)}`}>
|
|
Sign in
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|