154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate } from "react-router";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
|
|
import { API_URL } from "../config/api.ts";
|
|
import {
|
|
deserializeAuthResponse,
|
|
type LoginRequest,
|
|
type RawAuthResponse,
|
|
} from "../model.ts";
|
|
import { useAuth } from "../hooks/useAuth.ts";
|
|
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
|
|
import { useHeightTransition } from "../hooks/useHeightTransition.ts";
|
|
import { PageShell } from "../components/layout/PageShell.tsx";
|
|
import {
|
|
expectOk,
|
|
FormError,
|
|
FormProvider,
|
|
SubmitButton,
|
|
TextField,
|
|
useApiForm,
|
|
} from "../components/form/index.ts";
|
|
|
|
interface LoginValues {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
interface ResetValues {
|
|
email: string;
|
|
}
|
|
|
|
export function UserLogin() {
|
|
useDocumentTitle(t`Log in`);
|
|
const navigate = useNavigate();
|
|
const { login } = useAuth();
|
|
const [showReset, setShowReset] = useState(false);
|
|
const cardRef = useHeightTransition<HTMLDivElement>(showReset);
|
|
|
|
const form = useApiForm<LoginValues>({
|
|
defaultValues: { username: "", password: "" },
|
|
});
|
|
|
|
const onSubmit = form.submit(async ({ username, password }) => {
|
|
const data = await expectOk<RawAuthResponse>(
|
|
await fetch(`${API_URL}/api/users/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username, password } satisfies LoginRequest),
|
|
}),
|
|
);
|
|
login(deserializeAuthResponse(data));
|
|
navigate("/");
|
|
});
|
|
|
|
return (
|
|
<PageShell centered>
|
|
<div className="auth-card" ref={cardRef}>
|
|
<h1 className="auth-card-title">
|
|
<Trans>Log in</Trans>
|
|
</h1>
|
|
|
|
<FormProvider {...form}>
|
|
<form onSubmit={onSubmit} className="form">
|
|
<FormError title={t`Login failed`} />
|
|
<TextField<LoginValues>
|
|
name="username"
|
|
placeholder={t`Username`}
|
|
autoFocus
|
|
rules={{ required: true }}
|
|
/>
|
|
<TextField<LoginValues>
|
|
name="password"
|
|
type="password"
|
|
placeholder={t`Password`}
|
|
rules={{ required: true }}
|
|
/>
|
|
<SubmitButton pendingLabel={<Trans>Logging in…</Trans>}>
|
|
<Trans>Log in</Trans>
|
|
</SubmitButton>
|
|
</form>
|
|
</FormProvider>
|
|
|
|
<p className="auth-card-footer">
|
|
<button
|
|
type="button"
|
|
className="auth-link-btn"
|
|
onClick={() => setShowReset((v) => !v)}
|
|
>
|
|
<Trans>Forgot password?</Trans>
|
|
</button>
|
|
</p>
|
|
|
|
{showReset && <ResetRequestPanel />}
|
|
|
|
<p className="auth-card-footer">
|
|
<Trans>This is a mirage.</Trans>
|
|
</p>
|
|
</div>
|
|
</PageShell>
|
|
);
|
|
}
|
|
|
|
function ResetRequestPanel() {
|
|
const [sent, setSent] = useState(false);
|
|
const form = useApiForm<ResetValues>({ defaultValues: { email: "" } });
|
|
|
|
const onSubmit = form.submit(async ({ email }) => {
|
|
// The answer is the same whether or not the address is known; only a
|
|
// server that failed to take the request is worth reporting.
|
|
await expectOk(
|
|
await fetch(`${API_URL}/api/users/request-password-reset`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: email.trim() }),
|
|
}),
|
|
);
|
|
setSent(true);
|
|
});
|
|
|
|
if (sent) {
|
|
return (
|
|
<div className="auth-reset-panel animate-in">
|
|
<p className="auth-reset-sent">
|
|
<Trans>
|
|
If that address is registered you'll receive a reset link shortly.
|
|
</Trans>
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="auth-reset-panel animate-in">
|
|
<FormProvider {...form}>
|
|
<form onSubmit={onSubmit} className="form auth-reset-form">
|
|
<FormError title={t`Request failed`} />
|
|
<TextField<ResetValues>
|
|
name="email"
|
|
type="email"
|
|
placeholder={t`Your email address`}
|
|
autoFocus
|
|
rules={{ required: true }}
|
|
/>
|
|
<SubmitButton pendingLabel={<Trans>Sending…</Trans>}>
|
|
<Trans>Send reset link</Trans>
|
|
</SubmitButton>
|
|
</form>
|
|
</FormProvider>
|
|
</div>
|
|
);
|
|
}
|