291 lines
9.4 KiB
TypeScript
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>
|
|
);
|
|
}
|