diff options
Diffstat (limited to 'src/web/components/login/Login.jsx')
| -rw-r--r-- | src/web/components/login/Login.jsx | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/src/web/components/login/Login.jsx b/src/web/components/login/Login.jsx new file mode 100644 index 0000000..7944aa3 --- /dev/null +++ b/src/web/components/login/Login.jsx @@ -0,0 +1,218 @@ +import React, {Fragment, useState, useContext} from "react"; +import {Link} from "react-router-dom"; + +import Card from "components/card/Card.jsx"; +import Button from "components/button/Button.jsx"; +import Darken from "components/darken/Darken.jsx"; +import Loading from "components/loading/Loading.jsx"; +import Input from "components/input/Input.jsx"; +import {UserContext} from "contexts/UserContext.jsx"; +import {maybeAuthFromCookie} from "helpers/Auth.jsx"; +import {getFieldError, correctFieldAnimate} from "helpers/Input.jsx"; + +import styles from "./Login.css"; + +const validateEmail = (email) => getFieldError(email?.target?.value, [3, 254]); +const validatePassword = (password) => getFieldError(password?.target?.value, [8, 64]); + +function RequestButton({email, password, apiPath, promise, setPromise, setEmail, + setPassword, setLoginActive, setError, children}) { + + const userState = useContext(UserContext); + + const onClick = () => { + if (promise != null) { + return; + } + setError(null); + + // We don't want to send a worthless request, we should animate the bad values. + const emailErrors = validateEmail(email); + const passwordErrors = validatePassword(password); + if (emailErrors != null || passwordErrors != null) { + correctFieldAnimate(email, setEmail, emailErrors); + correctFieldAnimate(password, setPassword, passwordErrors); + return; + } + + const init = { + method: "POST", + headers: { + 'Content-Type': "application/json" + }, + body: JSON.stringify({ + "email": email.target.value, + "password": password.target.value + }), + referrer: "same-origin", + }; + setPromise(fetch(apiPath, init) + .then((response) => { + if (response.ok) { + if (!maybeAuthFromCookie(userState)) { + throw new Error("rejected authentication token"); + } + + setLoginActive(false); + return; + } + + return response.json() + .then((json) => { + if ("error" in json) { + throw new Error(json.error); + } + throw new Error("unexpected data received"); + }) + }) + .catch((error) => { + console.log(error); + setError(error.message); + }) + .finally(() => { + setPromise(null); + }) + ); + }; + + // I'm not worried about this object becoming unmounted mid promise because + // our website design removes this possibility. + return ( + <Button className={(promise != null ? styles.buttonLoading : null)} + onClick={onClick}> + + {children} + </Button> + ); +} + +function CardError({active, text}) { + return ( + <div className={styles.cardError + (active ? " " + styles.cardErrorActive : "")}> + <p> + {text} + </p> + </div> + ); +} + +function FormContents(props) { + const {setLoginActive, + isRegister, + setIsRegister, + title, + prompt, + linkContents, + apiPath} = props; + + const [error, setError] = useState(null); + // Email and password .value's are null here to represent the state where the user + // has not typed in anything yet. This is to only validate after at least some + // input has been received. + const [email, setEmail] = useState(null); + const [password, setPassword] = useState(null); + const [promise, setPromise] = useState(null); + + return ( + <div className={styles.formContainer}> + <CardError active={error != null} text={error} /> + + <h1>{title}</h1> + + <Input type="text" + title="Email" + field={email} + setField={setEmail} + validateField={validateEmail} /> + + <Input type="text" + title="Password" + field={password} + setField={setPassword} + validateField={validatePassword} /> + + <div className={styles.promptContainer}> + {prompt} + </div> + + <RequestButton email={email} + password={password} + promise={promise} + setEmail={setEmail} + setPassword={setPassword} + setLoginActive={setLoginActive} + setPromise={setPromise} + setError={setError} + apiPath={apiPath}> + + {promise == null ? title : <Loading className={styles.loading} />} + </RequestButton> + + <a onClick={() => { + if (promise != null) { // disallow mid transaction change + return; + } + setIsRegister(!isRegister); + setError(null); + setEmail({...email, value: null, animating: false}); + setPassword({...password, value: null, animating: false});}}> + + {linkContents} + </a> + </div> + ); +}; + +function RegisterPrompt() { + return ( + <p> + By registering, you agree to both our privacy policy and to + waive all rights of basic privacy and security. + </p> + ); +} + +function LoginPrompt(props) { + const {setLoginActive} = props; + + return ( + <Fragment> + <div> + <input type="checkbox" id="save_login" /><label htmlFor="save_login">Save Login</label> + </div> + <Link to="settings/recover" onClick={() => setLoginActive(false)}> + Can't log in? + </Link> + </Fragment> + ); +} + +function Prompt(props) { + const {setLoginActive} = props; + const [isRegister, setIsRegister] = useState(true); + return ( + <FormContents setLoginActive={setLoginActive} + isRegister={isRegister} + setIsRegister={setIsRegister} + title={isRegister ? "Sign Up" : "Login"} + prompt={isRegister ? <RegisterPrompt /> : <LoginPrompt setLoginActive={setLoginActive} />} + linkContents={isRegister ? "Already have an account?" : "Don't have an account?"} + apiPath={isRegister ? "/api/signup" : "/api/login"} /> + ); +} + +// A popup which asks for login/register info. +export default function Login(props) { + const {active, setActive} = props; + + return ( + <Fragment> + <Darken active={active} onClick={() => setActive(false)} className={styles.darken} /> + <div className={styles.screenCenter}> + <Card className={styles.login + (active ? " " + styles.loginActive : "")}> + <Prompt setLoginActive={setActive} /> + </Card> + </div> + </Fragment> + ); +}
\ No newline at end of file |
