mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { HomeSimpleDoor, LogIn, UserPlus } from "iconoir-react";
|
|
import { useEffect } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
|
import FormField from "../components/forms/FormField";
|
|
import SubmitButton from "../components/forms/SubmitButton";
|
|
import { showErrorToast } from "../components/toast/toast";
|
|
import { createAuthSession, getOptionalAuth, login } from "../lib/auth.server";
|
|
import { loginSchema, type LoginSchema } from "../lib/schemas";
|
|
import type { Route } from "./+types/login";
|
|
|
|
export function meta({}: Route.MetaArgs) {
|
|
return [{ title: "Login | The Biergarten App" }];
|
|
}
|
|
|
|
export async function loader({ request }: Route.LoaderArgs) {
|
|
const auth = await getOptionalAuth(request);
|
|
if (auth) throw redirect("/dashboard");
|
|
return null;
|
|
}
|
|
|
|
export async function action({ request }: Route.ActionArgs) {
|
|
const formData = await request.formData();
|
|
const result = loginSchema.safeParse({
|
|
username: formData.get("username"),
|
|
password: formData.get("password"),
|
|
});
|
|
|
|
if (!result.success) {
|
|
return { error: result.error.issues[0].message };
|
|
}
|
|
|
|
try {
|
|
const payload = await login(result.data.username, result.data.password);
|
|
return createAuthSession(payload, "/dashboard");
|
|
} catch (err) {
|
|
return { error: err instanceof Error ? err.message : "Login failed." };
|
|
}
|
|
}
|
|
|
|
export default function Login({ actionData }: Route.ComponentProps) {
|
|
const navigation = useNavigation();
|
|
const submit = useSubmit();
|
|
const isSubmitting = navigation.state === "submitting";
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
} = useForm<LoginSchema>({ resolver: zodResolver(loginSchema) });
|
|
|
|
const onSubmit = handleSubmit((data) => {
|
|
submit(data, { method: "post" });
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (actionData?.error) {
|
|
showErrorToast(actionData.error);
|
|
}
|
|
}, [actionData?.error]);
|
|
|
|
return (
|
|
<div className="hero min-h-screen bg-base-200">
|
|
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
|
<div className="card-body gap-4">
|
|
<div className="text-center">
|
|
<h1 className="card-title text-3xl justify-center gap-2">
|
|
<LogIn className="size-7" aria-hidden="true" />
|
|
Login
|
|
</h1>
|
|
<p className="text-base-content/70">Sign in to your Biergarten account</p>
|
|
</div>
|
|
|
|
{actionData?.error && (
|
|
<div role="alert" className="alert alert-error alert-soft">
|
|
<span>{actionData.error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={onSubmit} className="space-y-3">
|
|
<FormField
|
|
id="username"
|
|
type="text"
|
|
autoComplete="username"
|
|
placeholder="your_username"
|
|
label="Username"
|
|
error={errors.username?.message}
|
|
{...register("username")}
|
|
/>
|
|
|
|
<FormField
|
|
id="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
placeholder="••••••••"
|
|
label="Password"
|
|
error={errors.password?.message}
|
|
{...register("password")}
|
|
/>
|
|
|
|
<SubmitButton
|
|
isSubmitting={isSubmitting}
|
|
idleText="Sign In"
|
|
submittingText="Signing in..."
|
|
/>
|
|
</form>
|
|
|
|
<div className="divider text-xs">New here?</div>
|
|
|
|
<div className="text-center space-y-2">
|
|
<Link to="/register" className="btn btn-outline btn-sm w-full gap-2">
|
|
<UserPlus className="size-4" aria-hidden="true" />
|
|
Create an account
|
|
</Link>
|
|
<Link
|
|
to="/"
|
|
className="link link-hover text-sm text-base-content/60 inline-flex items-center gap-1"
|
|
>
|
|
<HomeSimpleDoor className="size-4" aria-hidden="true" />
|
|
Back to home
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|