From 93dfe2be64e8658839bcfe5356adf35f8cde7075 Mon Sep 17 00:00:00 2001 From: Nicolas James Date: Thu, 13 Feb 2025 18:04:18 +1100 Subject: initial commit --- src/web/components/login/Login.css | 118 ++++++++++++++++++++ src/web/components/login/Login.jsx | 218 +++++++++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 src/web/components/login/Login.css create mode 100644 src/web/components/login/Login.jsx (limited to 'src/web/components/login') diff --git a/src/web/components/login/Login.css b/src/web/components/login/Login.css new file mode 100644 index 0000000..1be8f84 --- /dev/null +++ b/src/web/components/login/Login.css @@ -0,0 +1,118 @@ +.screenCenter { + display: flex; + justify-content: center; + align-items: center; + z-index: var(--login-z-index); + + position: fixed; + inset: 0; + visibility: hidden; +} + +.darken { + z-index: calc(var(--login-z-index) - 1); +} +.login { + display: flex; + flex-direction: column; + justify-content: center; + flex: 0 1 100%; + + position: relative; + padding: 4em 2em 2em; + margin: 0em 3em; + max-width: 25em; + + color: var(--black-98); + visibility: visible; + border-radius: 2em; + + translate: 100vw; + transition: translate var(--transition-time) ease-in-out; +} +.loginActive { + translate: initial; + visibility: initial; +} + +.formContainer { + display: flex; + justify-content: center; + flex-direction: column; +} + +.login .formContainer h1 { + font-size: 2em; + font-weight: bolder; + text-align: center; + color: white; + margin-bottom: 2rem; +} +.login .formContainer h2 { + font-size: 1.5em; + font-weight: 500; + margin-bottom: 0.5rem; +} +.login .formContainer p { + text-align: center; + line-height: 1.25em; +} + +.promptContainer { + min-height: 5em; + display: flex; + justify-content: space-between; +} + +.login .formContainer button { + height: 2.25rem; + font-size: 1.25em; + margin-bottom: 1.25rem; + width: 100%; + + display: flex; + justify-content: center; + align-items: center; + + border-radius: 2.5rem; + font-weight: 600; + cursor: pointer; +} + +.loading { + font-size: 1.65rem; + filter: invert(95%); +} + +.formContainer a { + display: block; + text-align: center; + text-decoration: underline; + cursor: pointer; + min-height: 3em; + color: inherit; +} + +.promptContainer input { + margin: 0 0.25em 0 0; +} + +.cardError { + display: none; + position: absolute; + inset: 0 0 auto 0; + min-height: 2em; + margin: 1em; + + justify-content: center; + align-items: center; + + color: white; + border-radius: 2em; + font-weight: normal; + font-style: italic; + text-transform: uppercase; +} +.cardErrorActive { + display: flex; +} \ No newline at end of file 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 ( + + ); +} + +function CardError({active, text}) { + return ( +
+

+ {text} +

+
+ ); +} + +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 ( +
+ + +

{title}

+ + + + + +
+ {prompt} +
+ + + + {promise == null ? title : } + + + { + 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} + +
+ ); +}; + +function RegisterPrompt() { + return ( +

+ By registering, you agree to both our privacy policy and to + waive all rights of basic privacy and security. +

+ ); +} + +function LoginPrompt(props) { + const {setLoginActive} = props; + + return ( + +
+ +
+ setLoginActive(false)}> + Can't log in? + +
+ ); +} + +function Prompt(props) { + const {setLoginActive} = props; + const [isRegister, setIsRegister] = useState(true); + return ( + : } + 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 ( + + setActive(false)} className={styles.darken} /> +
+ + + +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3