v3: code quality and visual consistency pass (refactored all forms across the app), fixed filename-related upload bug
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 47s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 47s
This commit is contained in:
@@ -4,27 +4,42 @@ import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
import { API_URL, VALIDATION } from "../config/api.ts";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
import {
|
||||
expectOk,
|
||||
FormError,
|
||||
FormProvider,
|
||||
SubmitButton,
|
||||
TextField,
|
||||
useApiForm,
|
||||
} from "../components/form/index.ts";
|
||||
|
||||
type State =
|
||||
| { status: "idle" }
|
||||
| { status: "submitting" }
|
||||
| { status: "done" }
|
||||
| { status: "error"; error: string };
|
||||
interface Values {
|
||||
newPassword: string;
|
||||
confirm: string;
|
||||
}
|
||||
|
||||
export function ResetPassword() {
|
||||
const [params] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const token = params.get("token") ?? "";
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirm, setConfirm] = useState("");
|
||||
const [state, setState] = useState<State>({ status: "idle" });
|
||||
const form = useApiForm<Values>({
|
||||
mode: "onChange",
|
||||
defaultValues: { newPassword: "", confirm: "" },
|
||||
});
|
||||
|
||||
const mismatch = confirm.length > 0 && newPassword !== confirm;
|
||||
const tooShort = newPassword.length > 0 &&
|
||||
newPassword.length < VALIDATION.PASSWORD_MIN;
|
||||
const onSubmit = form.submit(async ({ newPassword }) => {
|
||||
await expectOk(
|
||||
await fetch(`${API_URL}/api/users/reset-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token, newPassword }),
|
||||
}),
|
||||
);
|
||||
setDone(true);
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return (
|
||||
@@ -48,7 +63,7 @@ export function ResetPassword() {
|
||||
);
|
||||
}
|
||||
|
||||
if (state.status === "done") {
|
||||
if (done) {
|
||||
return (
|
||||
<PageShell centered>
|
||||
<div className="auth-card">
|
||||
@@ -70,31 +85,6 @@ export function ResetPassword() {
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
if (mismatch || tooShort || !newPassword) return;
|
||||
|
||||
setState({ status: "submitting" });
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/users/reset-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token, newPassword }),
|
||||
});
|
||||
const body = await res.json();
|
||||
if (body.success) {
|
||||
setState({ status: "done" });
|
||||
} else {
|
||||
setState({
|
||||
status: "error",
|
||||
error: body.error?.message ?? t`Unknown error`,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
setState({ status: "error", error: t`Could not connect to server` });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell centered>
|
||||
<div className="auth-card">
|
||||
@@ -102,57 +92,40 @@ export function ResetPassword() {
|
||||
<Trans>Set new password</Trans>
|
||||
</h1>
|
||||
|
||||
{state.status === "error" && (
|
||||
<ErrorCard title={t`Reset failed`} message={state.error} />
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
<div className="form-group">
|
||||
<input
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={onSubmit} className="form">
|
||||
<FormError title={t`Reset failed`} />
|
||||
<TextField<Values>
|
||||
name="newPassword"
|
||||
type="password"
|
||||
placeholder={t`New password`}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
minLength={VALIDATION.PASSWORD_MIN}
|
||||
maxLength={VALIDATION.PASSWORD_MAX}
|
||||
required
|
||||
autoFocus
|
||||
disabled={state.status === "submitting"}
|
||||
maxLength={VALIDATION.PASSWORD_MAX}
|
||||
rules={{
|
||||
required: true,
|
||||
minLength: {
|
||||
value: VALIDATION.PASSWORD_MIN,
|
||||
message: t`At least ${VALIDATION.PASSWORD_MIN} characters`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{tooShort && (
|
||||
<span className="auth-field-hint auth-field-hint--error">
|
||||
<Trans>At least {VALIDATION.PASSWORD_MIN} characters</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
<TextField<Values>
|
||||
name="confirm"
|
||||
type="password"
|
||||
placeholder={t`Confirm new password`}
|
||||
value={confirm}
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
disabled={state.status === "submitting"}
|
||||
rules={{
|
||||
required: true,
|
||||
validate: (value, values) =>
|
||||
value === values.newPassword || t`Passwords do not match`,
|
||||
}}
|
||||
/>
|
||||
{mismatch && (
|
||||
<span className="auth-field-hint auth-field-hint--error">
|
||||
<Trans>Passwords do not match</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={state.status === "submitting" || mismatch || tooShort ||
|
||||
!newPassword || !confirm}
|
||||
>
|
||||
{state.status === "submitting"
|
||||
? <Trans>Saving…</Trans>
|
||||
: <Trans>Set new password</Trans>}
|
||||
</button>
|
||||
</form>
|
||||
<SubmitButton pendingLabel={<Trans>Saving…</Trans>}>
|
||||
<Trans>Set new password</Trans>
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
<p className="auth-card-footer">
|
||||
<Link to="/login">
|
||||
|
||||
Reference in New Issue
Block a user