feat(web): login page with inline error handling

Add shadcn input/label/card primitives and implement the login page:
email/password form using useLogin, navigates to /objects on success,
shows inline i18n error on 401 (auth.invalid) or network failure.
2 new tests, 9 total green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 22:56:17 +02:00
parent 01f43e1f67
commit 057a00c413
5 changed files with 243 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
import { useState, type FormEvent } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useLogin } from "../api/queries";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function LoginPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const login = useLogin();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const onSubmit = (event: FormEvent) => {
event.preventDefault();
login.mutate(
{ email, password },
{ onSuccess: () => navigate("/objects", { replace: true }) },
);
};
const errorKey = login.error
? login.error.message === "invalid"
? "auth.invalid"
: "auth.networkError"
: null;
return (
<div className="flex min-h-screen items-center justify-center p-4">
<form onSubmit={onSubmit} className="w-full max-w-sm space-y-4">
<h1 className="text-2xl font-semibold">{t("app.name")}</h1>
<div className="space-y-2">
<Label htmlFor="email">{t("auth.email")}</Label>
<Input
id="email"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
autoComplete="username"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">{t("auth.password")}</Label>
<Input
id="password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
autoComplete="current-password"
/>
</div>
{errorKey && (
<p role="alert" className="text-sm text-red-600">
{t(errorKey)}
</p>
)}
<Button type="submit" className="w-full" disabled={login.isPending}>
{t("auth.signIn")}
</Button>
</form>
</div>
);
}