Skip to content

Commit 57fc449

Browse files
refactor: standardize form error display and remove entrance animation from forms.
1 parent fbc995e commit 57fc449

9 files changed

Lines changed: 342 additions & 658 deletions

File tree

src/public/components/2fa.js

Lines changed: 59 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2,77 +2,69 @@ import { ConfigurationContext } from "../context/configuration.js";
22
import { afterLogin, errorTextTimeout, getPlaceholder, useTitle } from "../utils/utils.js";
33

44
export default function TwoFactorAuthentication() {
5-
const submitButtonText = i18next.t("button.submit");
5+
const submitButtonText = i18next.t("button.submit");
66

7-
const configuration = React.useContext(ConfigurationContext);
7+
const configuration = React.useContext(ConfigurationContext);
88

9-
const [buttonText, setButtonText] = React.useState(submitButtonText);
10-
const [hasError, setHasError] = React.useState(false);
11-
const [submitting, setSubmitting] = React.useState(false);
9+
const [errorMessage, setErrorMessage] = React.useState("");
10+
const [submitting, setSubmitting] = React.useState(false);
1211

13-
React.useEffect(() => useTitle(configuration["content.app-name"], i18next.t("title.2fa")), []);
12+
React.useEffect(() => useTitle(configuration["content.app-name"], i18next.t("title.2fa")), []);
1413

15-
const onSubmitError = (props) => {
16-
if (hasError) {
17-
return;
18-
}
19-
setHasError(true);
20-
setButtonText(props.errorText);
21-
setTimeout(() => {
22-
setButtonText(submitButtonText);
23-
setHasError(false);
24-
}, errorTextTimeout);
25-
};
14+
const onSubmitError = (props) => {
15+
setErrorMessage(props.errorText);
16+
};
2617

27-
function submit2fa(event) {
28-
event.preventDefault();
29-
const code = document.getElementById("code").value;
30-
const urlParams = new URLSearchParams(window.location.search);
31-
const target = urlParams.get("target");
32-
const sessionHash = urlParams.get("session_hash");
33-
setSubmitting(true);
34-
$.post("/user/do-2fa", {
35-
target,
36-
code,
37-
sessionHash
38-
})
39-
.done(function () {
40-
afterLogin(configuration)
41-
})
42-
.fail(function () {
43-
onSubmitError({ errorText: i18next.t("error.invalid-code") });
44-
})
45-
.always(function () {
46-
setSubmitting(false);
47-
});
48-
}
18+
function submit2fa(event) {
19+
event.preventDefault();
20+
setErrorMessage("");
21+
const code = document.getElementById("code").value;
22+
const urlParams = new URLSearchParams(window.location.search);
23+
const target = urlParams.get("target");
24+
const sessionHash = urlParams.get("session_hash");
25+
setSubmitting(true);
26+
$.post("/user/do-2fa", {
27+
target,
28+
code,
29+
sessionHash,
30+
})
31+
.done(function () {
32+
afterLogin(configuration);
33+
})
34+
.fail(function () {
35+
onSubmitError({ errorText: i18next.t("error.invalid-code") });
36+
})
37+
.always(function () {
38+
setSubmitting(false);
39+
});
40+
}
4941

50-
return (
51-
<form className={`form ${configuration["form.animate-entrance"] && "animate-jelly"}`} onSubmit={submit2fa}>
52-
<div className="noselect">
53-
<h3>
54-
{i18next.t("heading.2fa")}
55-
</h3>
56-
<p className="app-tagline">{i18next.t("message.enter-login-code")}</p>
57-
</div>
58-
<div className="form-group first">
59-
<label htmlFor="code">{i18next.t("field.label.verification-code")}</label>
60-
<input
61-
type="text"
62-
className="form-control"
63-
aria-label="Verification Code"
64-
aria-required="true"
65-
placeholder={getPlaceholder(i18next.t("field.placeholder.verification-code"), configuration)}
66-
pattern="[0-9]*"
67-
inputMode="numeric"
68-
minLength="6"
69-
autoComplete="off"
70-
id="code"
71-
required
72-
/>
73-
</div>
74-
<div className="page-links"></div>
75-
<input type="submit" disabled={submitting} className={"button" + (hasError ? " shake" : "")} value={buttonText} />
76-
</form >
77-
);
42+
return (
43+
<form className="form" onSubmit={submit2fa}>
44+
<div className="noselect">
45+
<h3>{i18next.t("heading.2fa")}</h3>
46+
<p className="app-tagline">{i18next.t("message.enter-login-code")}</p>
47+
</div>
48+
<div className="form-group first">
49+
<label htmlFor="code">{i18next.t("field.label.verification-code")}</label>
50+
<input
51+
type="text"
52+
className="form-control"
53+
aria-label="Verification Code"
54+
aria-required="true"
55+
placeholder={getPlaceholder(i18next.t("field.placeholder.verification-code"), configuration)}
56+
pattern="[0-9]*"
57+
inputMode="numeric"
58+
minLength="6"
59+
autoComplete="off"
60+
id="code"
61+
required
62+
/>
63+
</div>
64+
<div className="page-links"></div>
65+
<input type="submit" disabled={submitting} className="button" value={submitButtonText} />
66+
<div className="form-error-message">{errorMessage}</div>
67+
</form>
68+
);
7869
}
70+

src/public/components/consent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default function ConsentScreen() {
4242
React.useEffect(() => useTitle(configuration["content.app-name"], i18next.t("title.consent")), []);
4343

4444
return clientInfo && permissionsInfo ? (
45-
<div className={`form ${configuration["form.animate-entrance"] && "animate-jelly"}`}>
45+
<div className="form">
4646
<div className="noselect">
4747
<h3 className="long-header">
4848
{i18next.t("heading.consent")}<span className="header-separator">&nbsp;&#x2022;&nbsp;</span>

src/public/components/get-code.js

Lines changed: 79 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2,88 +2,90 @@ import { ConfigurationContext } from "../context/configuration.js";
22
import { errorTextTimeout, getPlaceholder, useTitle } from "../utils/utils.js";
33

44
export default function GetCode() {
5-
const submitButtonText = i18next.t("button.get-code");
5+
const submitButtonText = i18next.t("button.get-code");
66

7-
const configuration = React.useContext(ConfigurationContext);
7+
const configuration = React.useContext(ConfigurationContext);
88

9-
const [buttonText, setButtonText] = React.useState(submitButtonText);
10-
const [hasError, setHasError] = React.useState(false);
11-
const [submitting, setSubmitting] = React.useState(false);
9+
const [errorMessage, setErrorMessage] = React.useState("");
10+
// hasError removed
11+
const [submitting, setSubmitting] = React.useState(false);
1212

13-
React.useEffect(() => useTitle(configuration["content.app-name"], i18next.t("title.verify-your-identity")), []);
13+
React.useEffect(() => useTitle(configuration["content.app-name"], i18next.t("title.verify-your-identity")), []);
1414

15-
const onSubmitError = (props) => {
16-
if (hasError) {
17-
return;
18-
}
19-
setHasError(true);
20-
setButtonText(props.errorText);
21-
setTimeout(() => {
22-
setButtonText(submitButtonText);
23-
setHasError(false);
24-
}, errorTextTimeout);
25-
};
15+
const onSubmitError = (props) => {
16+
17+
setErrorMessage(props.errorText);
18+
};
2619

27-
function getCode(event) {
28-
event.preventDefault();
29-
const email = document.getElementById("email").value;
30-
setSubmitting(true);
31-
$.get("/user/code", {
32-
email,
33-
})
34-
.done(function (response) {
35-
const urlParams = new URLSearchParams(window.location.search);
36-
urlParams.set('target', response.data.target);
37-
window.location = `/reset-password?${urlParams.toString()}`;
38-
})
39-
.fail(function (response) {
40-
if (response.responseJSON.additionalInfo && response.status === 400) {
41-
return onFieldError({ response });
42-
}
43-
onSubmitError({ errorText: i18next.t("error.invalid-login") });
44-
})
45-
.always(function () {
46-
setSubmitting(false);
47-
});
48-
}
20+
function getCode(event) {
21+
event.preventDefault();
22+
setErrorMessage("");
23+
24+
const email = document.getElementById("email").value;
25+
setSubmitting(true);
26+
$.get("/user/code", {
27+
email,
28+
})
29+
.done(function (response) {
30+
const urlParams = new URLSearchParams(window.location.search);
31+
urlParams.set("target", response.data.target);
32+
window.location = `/reset-password?${urlParams.toString()}`;
33+
})
34+
.fail(function (response) {
35+
if (response.responseJSON.additionalInfo && response.status === 400) {
36+
return onFieldError({ response });
37+
}
38+
onSubmitError({ errorText: i18next.t("error.invalid-login") });
39+
})
40+
.always(function () {
41+
setSubmitting(false);
42+
});
43+
}
4944

50-
if (!configuration["privilege.can-reset-password"]) {
51-
return null;
52-
}
45+
if (!configuration["privilege.can-reset-password"]) {
46+
return null;
47+
}
5348

54-
return (
55-
<form className={`form ${configuration["form.animate-entrance"] && "animate-jelly"}`} onSubmit={getCode}>
56-
<div className="noselect">
57-
<h3>
58-
{i18next.t("heading.verify-your-identity")}
59-
</h3>
60-
<p className="app-tagline">{i18next.t("message.recover-instructions")}</p>
61-
</div>
62-
<div className="form-group first last">
63-
<label className="noselect" htmlFor="email">{i18next.t("field.label.email")}</label>
64-
<input
65-
type="email"
66-
className="form-control"
67-
aria-label="Email"
68-
aria-required="true"
69-
placeholder={getPlaceholder(i18next.t("field.placeholder.email"), configuration)}
70-
minLength="8"
71-
autoComplete="off"
72-
autoCorrect="off"
73-
autoCapitalize="off"
74-
spellCheck="false"
75-
id="email"
76-
required
77-
/>
78-
</div>
79-
<div className="page-links">
80-
<span className="page-link">
81-
<a href={"/login" + window.location.search} className="page-link" aria-label="Login">
82-
{i18next.t("link.login-minimal")}
83-
</a>
84-
</span>
85-
</div>
86-
<input type="submit" disabled={submitting} className={"button" + (hasError ? " shake" : "")} value={buttonText} />
87-
</form>
88-
);
49+
return (
50+
<form className="form" onSubmit={getCode}>
51+
<div className="noselect">
52+
<h3>{i18next.t("heading.verify-your-identity")}</h3>
53+
<p className="app-tagline">{i18next.t("message.recover-instructions")}</p>
54+
</div>
55+
<div className="form-group first last">
56+
<label className="noselect" htmlFor="email">
57+
{i18next.t("field.label.email")}
58+
</label>
59+
<input
60+
type="email"
61+
className="form-control"
62+
aria-label="Email"
63+
aria-required="true"
64+
placeholder={getPlaceholder(i18next.t("field.placeholder.email"), configuration)}
65+
minLength="8"
66+
autoComplete="off"
67+
autoCorrect="off"
68+
autoCapitalize="off"
69+
spellCheck="false"
70+
id="email"
71+
required
72+
/>
73+
</div>
74+
<div className="page-links">
75+
<span className="page-link">
76+
<a href={"/login" + window.location.search} className="page-link" aria-label="Login">
77+
{i18next.t("link.login-minimal")}
78+
</a>
79+
</span>
80+
</div>
81+
<input
82+
type="submit"
83+
disabled={submitting}
84+
className="button"
85+
value={submitButtonText}
86+
/>
87+
<div className="form-error-message">{errorMessage}</div>
88+
</form>
89+
);
8990
}
91+

src/public/components/login.js

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ export default function Login() {
1717
const configuration = React.useContext(ConfigurationContext);
1818
const theme = React.useContext(ThemeContext);
1919

20-
const [buttonText, setButtonText] = React.useState(submitButtonText);
21-
const [hasError, setHasError] = React.useState(false);
20+
const [errorMessage, setErrorMessage] = React.useState("");
2221
const [submitting, setSubmitting] = React.useState(false);
2322
const [miniIconLoaded, setMiniIconLoaded] = React.useState(false);
2423
const [existingSession, setExistingSession] = React.useState(null);
@@ -48,15 +47,7 @@ export default function Login() {
4847
}, []);
4948

5049
const onSubmitError = (props) => {
51-
if (hasError) {
52-
return;
53-
}
54-
setHasError(true);
55-
setButtonText(props.errorText);
56-
setTimeout(() => {
57-
setButtonText(submitButtonText);
58-
setHasError(false);
59-
}, errorTextTimeout);
50+
setErrorMessage(props.errorText);
6051
};
6152

6253
function onFieldError({ response, buttonText }) {
@@ -80,6 +71,7 @@ export default function Login() {
8071

8172
async function login(event) {
8273
event.preventDefault();
74+
setErrorMessage("");
8375
const username = document.getElementById("username").value;
8476
const password = document.getElementById("password").value;
8577
setSubmitting(true);
@@ -166,7 +158,7 @@ export default function Login() {
166158
}
167159

168160
return (
169-
<form className={`form ${configuration["form.animate-entrance"] && "animate-jelly"}`} onSubmit={login}>
161+
<form className="form" onSubmit={login}>
170162
<div className="noselect">
171163
<h3>
172164
{i18next.t("heading.login")}&nbsp;
@@ -240,12 +232,7 @@ export default function Login() {
240232
)}
241233
</div>
242234
<div className="flex flex-col gap">
243-
<input
244-
type="submit"
245-
disabled={submitting}
246-
className={"button" + (hasError ? " shake" : "")}
247-
value={buttonText}
248-
/>
235+
<input type="submit" disabled={submitting} className="button" value={submitButtonText} />
249236
{configuration["user.account-creation.sso.google.enabled"] && (
250237
<a onClick={handleSSOClick} className="ghost-link">
251238
<button type="button" disabled={submitting} className={"button outline"}>
@@ -255,6 +242,7 @@ export default function Login() {
255242
</a>
256243
)}
257244
</div>
245+
<div className="form-error-message">{errorMessage}</div>
258246
</form>
259247
);
260248
}

0 commit comments

Comments
 (0)