Files
gerbeur/src/pages/UserRegister.tsx

166 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import type { SubmitEvent } from "react";
import { Link, useNavigate, useSearchParams } from "react-router";
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro";
import { API_URL, VALIDATION } from "../config/api.ts";
import {
deserializeAuthResponse,
parseAPIResponse,
type RawAuthResponse,
type RegisterRequest,
} from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { PageShell } from "../components/PageShell.tsx";
import { ErrorCard } from "../components/ErrorCard.tsx";
import { friendlyFetchError } from "../utils/apiError.ts";
type TokenState =
| { status: "checking" }
| { status: "invalid" }
| { status: "valid" };
type FormState =
| { status: "idle" }
| { status: "submitting" }
| { status: "error"; error: string };
export function UserRegister() {
const navigate = useNavigate();
const { login } = useAuth();
const [searchParams] = useSearchParams();
const token = searchParams.get("token") ?? "";
const [tokenState, setTokenState] = useState<TokenState>(() =>
token ? { status: "checking" } : { status: "invalid" }
);
const [formState, setFormState] = useState<FormState>({ status: "idle" });
const [prevToken, setPrevToken] = useState(token);
if (prevToken !== token) {
setPrevToken(token);
setTokenState(token ? { status: "checking" } : { status: "invalid" });
}
useEffect(() => {
if (!token) return;
fetch(`${API_URL}/api/invites/${encodeURIComponent(token)}`)
.then((r) => {
setTokenState(r.ok ? { status: "valid" } : { status: "invalid" });
})
.catch(() => setTokenState({ status: "invalid" }));
}, [token]);
const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
setFormState({ status: "submitting" });
const formData = new FormData(e.currentTarget);
const username = formData.get("username") as string;
const password = formData.get("password") as string;
const email = formData.get("email") as string;
try {
const res = await fetch(`${API_URL}/api/users/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(
{
username,
password,
inviteToken: token,
email,
} satisfies RegisterRequest,
),
});
const apiResponse = parseAPIResponse<RawAuthResponse>(await res.json());
if (apiResponse.success) {
login(deserializeAuthResponse(apiResponse.data));
navigate("/");
} else {
setFormState({ status: "error", error: apiResponse.error.message });
}
} catch (err) {
setFormState({ status: "error", error: friendlyFetchError(err) });
}
};
if (tokenState.status === "checking") {
return (
<PageShell centered>
<p className="page-loading"><Trans>Checking invite</Trans></p>
</PageShell>
);
}
if (tokenState.status === "invalid") {
return (
<PageShell centered>
<div className="page-error-wrap">
<ErrorCard
title={t`Invalid invite`}
message={t`This invite link is missing, expired, or already used.`}
/>
</div>
</PageShell>
);
}
return (
<PageShell centered>
<div className="auth-card">
<h1 className="auth-card-title"><Trans>Register</Trans></h1>
{formState.status === "error" && (
<ErrorCard title={t`Registration failed`} message={formState.error} />
)}
<form onSubmit={handleSubmit} className="auth-form">
<input
name="username"
type="text"
placeholder={t`Username`}
required
pattern={`[a-zA-Z0-9_]{${VALIDATION.USERNAME_MIN},${VALIDATION.USERNAME_MAX}}`}
title={t`${VALIDATION.USERNAME_MIN}${VALIDATION.USERNAME_MAX} characters: letters, numbers, or underscores`}
disabled={formState.status === "submitting"}
autoFocus
/>
<input
name="email"
type="email"
placeholder={t`Email address`}
required
disabled={formState.status === "submitting"}
/>
<input
name="password"
type="password"
placeholder={t`Password (min. ${VALIDATION.PASSWORD_MIN} characters)`}
required
minLength={VALIDATION.PASSWORD_MIN}
maxLength={VALIDATION.PASSWORD_MAX}
disabled={formState.status === "submitting"}
/>
<button
type="submit"
className="btn-primary"
disabled={formState.status === "submitting"}
>
{formState.status === "submitting"
? <Trans>Registering</Trans>
: <Trans>Register</Trans>}
</button>
</form>
<p className="auth-card-footer">
<Trans>Already have an account? <Link to="/login">Log in</Link></Trans>
</p>
</div>
</PageShell>
);
}