translaite/app/routes/join.tsx

199 lines
5.7 KiB
TypeScript
Raw Permalink Normal View History

2023-08-07 10:52:44 +02:00
import type { ActionArgs, LoaderArgs, V2_MetaFunction } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import {
Form,
Link,
useActionData,
useLoaderData,
useSearchParams,
} from "@remix-run/react";
2023-08-07 10:52:44 +02:00
import { useEffect, useRef } from "react";
import { createUser, getUserByEmail, countUsers } from "~/models/user.server";
2023-08-07 10:52:44 +02:00
import { createUserSession, getUserId } from "~/session.server";
import { safeRedirect, validateEmail } from "~/utils";
import { isSignupAllowed } from "~/config.server";
2023-08-07 10:52:44 +02:00
export const loader = async ({ request }: LoaderArgs) => {
const userId = await getUserId(request);
if (userId) return redirect("/");
const isFirstUser = (await countUsers()) === 0;
if (!(await isSignupAllowed())) {
return redirect("/login");
}
return json({ isFirstUser });
2023-08-07 10:52:44 +02:00
};
export const action = async ({ request }: ActionArgs) => {
const formData = await request.formData();
const email = formData.get("email");
const password = formData.get("password");
const redirectTo = safeRedirect(formData.get("redirectTo"), "/");
const isFirstUser = (await countUsers()) === 0;
if (!isSignupAllowed() && !isFirstUser) {
return json(
{
errors: {
email: "User signup is disabled",
password: null,
confirmPassword: null,
},
},
{ status: 418 }
);
}
2023-08-07 10:52:44 +02:00
if (!validateEmail(email)) {
return json(
{ errors: { email: "Email is invalid", password: null } },
{ status: 400 }
);
}
if (typeof password !== "string" || password.length === 0) {
return json(
{ errors: { email: null, password: "Password is required" } },
{ status: 400 }
);
}
if (password.length < 8) {
return json(
{ errors: { email: null, password: "Password is too short" } },
{ status: 400 }
);
}
const existingUser = await getUserByEmail(email);
if (existingUser) {
return json(
{
errors: {
email: "A user already exists with this email",
password: null,
},
},
{ status: 400 }
);
}
const user = await createUser(email, password);
return createUserSession({
redirectTo,
remember: false,
request,
userId: user.id,
});
};
export const meta: V2_MetaFunction = () => [{ title: "Sign Up | TranslAIte" }];
2023-08-07 10:52:44 +02:00
export default function Join() {
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get("redirectTo") ?? undefined;
const actionData = useActionData<typeof action>();
const loaderData = useLoaderData<typeof loader>();
2023-08-07 10:52:44 +02:00
const emailRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (actionData?.errors?.email) {
emailRef.current?.focus();
} else if (actionData?.errors?.password) {
passwordRef.current?.focus();
}
}, [actionData]);
return (
<div className="flex min-h-full flex-col justify-center">
<div className="mx-auto w-full max-w-md px-8">
<Form method="post" className="space-y-6">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Email address
</label>
<div className="mt-1">
<input
ref={emailRef}
id="email"
required
autoFocus={true}
name="email"
type="email"
autoComplete="email"
aria-invalid={actionData?.errors?.email ? true : undefined}
aria-describedby="email-error"
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
/>
{actionData?.errors?.email ? (
<div className="pt-1 text-red-700" id="email-error">
{actionData.errors.email}
</div>
) : null}
</div>
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
>
Password
</label>
<div className="mt-1">
<input
id="password"
ref={passwordRef}
name="password"
type="password"
autoComplete="new-password"
aria-invalid={actionData?.errors?.password ? true : undefined}
aria-describedby="password-error"
className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
/>
{actionData?.errors?.password ? (
<div className="pt-1 text-red-700" id="password-error">
{actionData.errors.password}
</div>
) : null}
</div>
</div>
<input type="hidden" name="redirectTo" value={redirectTo} />
<button
type="submit"
className="w-full rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 focus:bg-blue-400"
>
Create Account
</button>
{!loaderData.isFirstUser && (
<div className="flex items-center justify-center">
<div className="text-center text-sm text-gray-500">
Already have an account?{" "}
<Link
className="text-blue-500 underline"
to={{
pathname: "/login",
search: searchParams.toString(),
}}
>
Log in
</Link>
</div>
2023-08-07 10:52:44 +02:00
</div>
)}
2023-08-07 10:52:44 +02:00
</Form>
</div>
</div>
);
}