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

This commit is contained in:
khannurien
2026-06-26 22:20:07 +00:00
parent 26f5abfa4e
commit 73e0114bf7
30 changed files with 2369 additions and 2120 deletions

View File

@@ -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">