trptk/components/auth/AuthForm.tsx
2026-02-24 17:14:07 +01:00

291 lines
9.4 KiB
TypeScript

"use client";
import { useState } from "react";
import { useAuth } from "./AuthContext";
import { ArrowButton } from "@/components/ArrowLink";
const inputClass =
"no-ring w-full rounded-xl border border-lightline px-6 py-3 shadow-lg text-lighttext transition-all duration-200 ease-in-out placeholder:text-lightsec hover:border-lightline-hover focus:border-lightline-focus dark:border-darkline dark:text-darktext dark:placeholder:text-darksec dark:hover:border-darkline-hover dark:focus:border-darkline-focus";
const PASSWORD_RULES = [
{ label: "At least 8 characters", test: (pw: string) => pw.length >= 8 },
{ label: "One uppercase letter", test: (pw: string) => /[A-Z]/.test(pw) },
{ label: "One lowercase letter", test: (pw: string) => /[a-z]/.test(pw) },
{ label: "One number", test: (pw: string) => /\d/.test(pw) },
{ label: "One special character (!@#$%^&*\u2026)", test: (pw: string) => /[^A-Za-z0-9]/.test(pw) },
];
type AuthFormProps = {
onSuccess?: () => void;
/** Hide title and left-align the mode toggle link */
compact?: boolean;
};
export function AuthForm({ onSuccess, compact }: AuthFormProps) {
const { login, register } = useAuth();
const [mode, setMode] = useState<"login" | "register">("login");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const showRules = mode === "register" && password.length > 0;
async function doSubmit() {
setError(null);
if (mode === "register") {
const failing = PASSWORD_RULES.find((r) => !r.test(password));
if (failing) {
setError(failing.label.replace(/^One/, "Must contain at least one").replace(/^At least/, "Must be at least"));
return;
}
if (password !== confirmPassword) {
setError("Passwords do not match");
return;
}
}
setSubmitting(true);
try {
if (mode === "login") {
await login(email, password);
} else {
await register({ email, password, first_name: firstName, last_name: lastName });
}
onSuccess?.();
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong");
} finally {
setSubmitting(false);
}
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
doSubmit();
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "Enter") {
e.preventDefault();
doSubmit();
}
}
return (
<div>
{!compact && (
<h2 className="text-center font-argesta text-3xl">
{mode === "login" ? "Sign In" : "Create Account"}
</h2>
)}
{compact ? (
<div className="flex flex-col gap-4">
{mode === "register" && (
<div className="grid gap-3 sm:grid-cols-2">
<input
type="text"
placeholder="First name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
onKeyDown={handleKeyDown}
className={inputClass}
/>
<input
type="text"
placeholder="Last name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
onKeyDown={handleKeyDown}
className={inputClass}
/>
</div>
)}
<input
type="email"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={handleKeyDown}
className={inputClass}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={handleKeyDown}
className={inputClass}
/>
{showRules && (
<ul className="flex flex-col gap-1 text-xs">
{PASSWORD_RULES.map((rule) => {
const passed = rule.test(password);
return (
<li
key={rule.label}
className={
passed
? "text-green-600 dark:text-green-400"
: "text-lightsec dark:text-darksec"
}
>
{passed ? "\u2713" : "\u2022"} {rule.label}
</li>
);
})}
</ul>
)}
{mode === "register" && (
<input
type="password"
placeholder="Confirm password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
onKeyDown={handleKeyDown}
className={inputClass}
/>
)}
{error && (
<div className="rounded-xl border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
{error}
</div>
)}
<button
type="button"
disabled={submitting}
onClick={() => doSubmit()}
className="w-full rounded-xl bg-trptkblue px-4 py-4 font-silkasb text-sm text-white shadow-lg transition-all duration-200 ease-in-out hover:opacity-90 disabled:pointer-events-none disabled:opacity-50 dark:bg-white dark:text-lighttext"
>
{submitting
? mode === "login"
? "Signing in\u2026"
: "Creating account\u2026"
: mode === "login"
? "Sign In"
: "Create Account"}
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="mt-10 flex flex-col gap-4">
{mode === "register" && (
<div className="grid gap-3 sm:grid-cols-2">
<input
type="text"
required
placeholder="First name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className={inputClass}
/>
<input
type="text"
required
placeholder="Last name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
className={inputClass}
/>
</div>
)}
<input
type="email"
required
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={inputClass}
/>
<input
type="password"
required
minLength={8}
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className={inputClass}
/>
{showRules && (
<ul className="flex flex-col gap-1 text-xs">
{PASSWORD_RULES.map((rule) => {
const passed = rule.test(password);
return (
<li
key={rule.label}
className={
passed
? "text-green-600 dark:text-green-400"
: "text-lightsec dark:text-darksec"
}
>
{passed ? "\u2713" : "\u2022"} {rule.label}
</li>
);
})}
</ul>
)}
{mode === "register" && (
<input
type="password"
required
minLength={8}
placeholder="Confirm password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={inputClass}
/>
)}
{error && (
<div className="rounded-xl border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
{error}
</div>
)}
<button
type="submit"
disabled={submitting}
className="w-full rounded-xl bg-trptkblue px-4 py-4 font-silkasb text-sm text-white shadow-lg transition-all duration-200 ease-in-out hover:opacity-90 disabled:pointer-events-none disabled:opacity-50 dark:bg-white dark:text-lighttext"
>
{submitting
? mode === "login"
? "Signing in\u2026"
: "Creating account\u2026"
: mode === "login"
? "Sign In"
: "Create Account"}
</button>
</form>
)}
<p className={`mt-6 text-sm text-lightsec dark:text-darksec${compact ? "" : " text-center"}`}>
{mode === "login" ? "Don\u2019t have an account?" : "Already have an account?"}{" "}
<ArrowButton
onClick={() => {
setMode(mode === "login" ? "register" : "login");
setError(null);
}}
className="text-trptkblue dark:text-white"
>
{mode === "login" ? "Create one" : "Sign in"}
</ArrowButton>
</p>
</div>
);
}