From d656ba7eba0c8b8439185191a1cb18f5c46c81bb Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 28 Feb 2022 13:30:26 -0800 Subject: [PATCH 1/6] Fix logout state bug Logging out didn't reset state, so the HeaderMenu still displayed the logged in menu --- components/TopHeader/index.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/components/TopHeader/index.tsx b/components/TopHeader/index.tsx index b9b5897c..eed02d45 100644 --- a/components/TopHeader/index.tsx +++ b/components/TopHeader/index.tsx @@ -5,7 +5,7 @@ import clonedeep from 'lodash.clonedeep' import { useSnapshot } from 'valtio' import api from '~utils/api' -import { accountState } from '~utils/accountState' +import { accountState, initialAccountState } from '~utils/accountState' import { appState, initialAppState } from '~utils/appState' import Header from '~components/Header' @@ -51,10 +51,15 @@ const TopHeader = () => { function logout() { removeCookie('user') - accountState.authorized = false + // Clean state + const resetState = clonedeep(initialAccountState) + Object.keys(resetState).forEach((key) => { + if (key !== 'language') + accountState[key] = resetState[key] + }) + appState.party.editable = false - // TODO: How can we log out without navigating to root router.push('/') return false } From 829146f1bd127b22799e18b53828dd636ed77dbd Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 28 Feb 2022 14:16:04 -0800 Subject: [PATCH 2/6] Redesign Signup Modal --- components/Button/index.scss | 2 + components/HeaderMenu/index.tsx | 9 +-- components/SignupModal/index.scss | 91 +++++++++-------------- components/SignupModal/index.tsx | 116 +++++++++++++++++------------- styles/globals.scss | 29 ++++++-- 5 files changed, 126 insertions(+), 121 deletions(-) diff --git a/components/Button/index.scss b/components/Button/index.scss index e2d7ce8d..616c9cf0 100644 --- a/components/Button/index.scss +++ b/components/Button/index.scss @@ -132,5 +132,7 @@ .text { color: inherit; + display: block; + width: 100%; } } diff --git a/components/HeaderMenu/index.tsx b/components/HeaderMenu/index.tsx index 00b87ae1..bb4c030b 100644 --- a/components/HeaderMenu/index.tsx +++ b/components/HeaderMenu/index.tsx @@ -83,14 +83,7 @@ const HeaderMenu = (props: Props) => { /> ) : null} -
  • - Sign up -
  • - {signupOpen ? ( - - ) : null} + diff --git a/components/SignupModal/index.scss b/components/SignupModal/index.scss index 5affc575..d515648a 100644 --- a/components/SignupModal/index.scss +++ b/components/SignupModal/index.scss @@ -1,67 +1,46 @@ -.SignupForm { - padding: 16px; -} - -.SignupForm form { - margin: 0; -} - -.SignupForm #fields { +.Signup.Dialog form { display: flex; flex-direction: column; - gap: 4px; - margin-bottom: 8px; -} + gap: $unit / 2; + margin-bottom: $unit; -#ModalTop { - display: flex; - flex-direction: row; - align-items: center; - margin-bottom: 16px; - margin-right: -8px; -} + .Button { + font-size: $font-regular; + padding: ($unit * 1.5) ($unit * 2); + width: 100%; -#ModalTop h1 { - margin: 0; - font-size: $font-xlarge; - text-align: left; - flex-grow: 1; -} + &.btn-disabled { + background: $grey-90; + color: $grey-70; + } -#ModalTop i { - padding: 8px; -} + &:not(.btn-disabled) { + background: $grey-90; + color: $grey-40; -#ModalTop i:hover { - cursor: pointer; -} + &:hover { + background: $grey-80; + } + } + } -#ModalTop i:hover svg { - color: #444; -} + .terms { + color: $grey-40; + font-size: $font-small; + line-height: 1.2; + margin-top: $unit; + text-align: center; -#ModalTop svg { - color: #888; - height: 18px; - width: 18px; - transform: rotate(45deg); -} + a { + color: $blue; -#ModalBottom { - display: flex; - flex-direction: row; - justify-content: flex-end; -} + &:hover { + color: darken($blue, 30); + } + } + } -#ModalBottom a { - color: #666; - font-size: $font-regular; - font-weight: 500; - flex-grow: 1; - display: flex; - align-items: center; -} - -#ModalBottom .Button { - min-width: 88px; + input { + background: $grey-90; + } } diff --git a/components/SignupModal/index.tsx b/components/SignupModal/index.tsx index ee6e15b4..79934dd3 100644 --- a/components/SignupModal/index.tsx +++ b/components/SignupModal/index.tsx @@ -2,6 +2,8 @@ import React, { useState } from 'react' import { withCookies, Cookies } from 'react-cookie' import { createPortal } from 'react-dom' +import * as Dialog from '@radix-ui/react-dialog' + import api from '~utils/api' import { accountState } from '~utils/accountState' @@ -10,12 +12,10 @@ import Fieldset from '~components/Fieldset' import Modal from '~components/Modal' import Overlay from '~components/Overlay' +import CrossIcon from '~public/icons/Cross.svg' import './index.scss' -interface Props { - cookies: Cookies - close: () => void -} +interface Props {} interface ErrorMap { [index: string]: string @@ -28,6 +28,7 @@ interface ErrorMap { const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const SignupModal = (props: Props) => { + // Set up error handling const [formValid, setFormValid] = useState(false) const [errors, setErrors] = useState({ username: '', @@ -36,6 +37,7 @@ const SignupModal = (props: Props) => { passwordConfirmation: '' }) + // Set up form refs const usernameInput = React.createRef() const emailInput = React.createRef() const passwordInput = React.createRef() @@ -153,58 +155,70 @@ const SignupModal = (props: Props) => { } return ( - createPortal( -
    - {} } - > + + +
  • + Sign up +
  • +
    + + event.preventDefault() }> +
    + Sign up + + + + + +
    + + +
    -
    -
    +
    -
    +
    -
    +
    -
    -
    -
    - -
    +
    + + + + + By signing up, I agree to the
    Terms and Conditions and Usage Guidelines. +
    - - -
    , - document.body - ) + + + + ) } diff --git a/styles/globals.scss b/styles/globals.scss index 2a49d5a9..d7f43e04 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -90,16 +90,18 @@ select { } .Dialog { + $multiplier: 4; + animation: 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0s 1 normal none running openModal; background: white; border-radius: $unit; display: flex; flex-direction: column; - gap: $unit * 3; + gap: $unit * $multiplier; height: auto; min-width: $unit * 48; min-height: $unit * 12; - padding: $unit * 3; + padding: $unit * $multiplier; position: absolute; top: 50%; left: 50%; @@ -108,28 +110,43 @@ select { .DialogHeader { display: flex; + gap: $unit; + + .left { + display: flex; + flex-direction: column; + flex-grow: 1; + gap: $unit; + + p { + color: $grey-40; + font-size: $font-small; + line-height: 1.25; + } + } } .DialogClose { background: transparent; - height: 21px; - width: 21px; &:hover { cursor: pointer; svg { - fill: $grey-00; + fill: $error; } } svg { fill: $grey-40; + float: right; + height: 24px; + width: 24px; } } .DialogTitle { - font-size: $font-large; + font-size: $font-xlarge; flex-grow: 1; } From 0e3aacfbb08ca5acb10f8ee51d3f824f1b54035a Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 28 Feb 2022 15:00:32 -0800 Subject: [PATCH 3/6] Refactor validation in SignupModal --- components/SignupModal/index.tsx | 117 ++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 32 deletions(-) diff --git a/components/SignupModal/index.tsx b/components/SignupModal/index.tsx index 79934dd3..8670eab3 100644 --- a/components/SignupModal/index.tsx +++ b/components/SignupModal/index.tsx @@ -9,8 +9,6 @@ import { accountState } from '~utils/accountState' import Button from '~components/Button' import Fieldset from '~components/Fieldset' -import Modal from '~components/Modal' -import Overlay from '~components/Overlay' import CrossIcon from '~public/icons/Cross.svg' import './index.scss' @@ -28,6 +26,8 @@ interface ErrorMap { const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const SignupModal = (props: Props) => { + // TODO: Clear error state on close + // Set up error handling const [formValid, setFormValid] = useState(false) const [errors, setErrors] = useState({ @@ -55,16 +55,19 @@ const SignupModal = (props: Props) => { .then((response) => { if (!response.data.available) { newErrors[name] = `This ${name} is already in use` + setErrors(newErrors) + setFormValid(false) + } else { + setFormValid(true) } - - setErrors(newErrors) }, (error) => { console.error(error) }) } } - function process(event: React.FormEvent) { + // TODO: Refactor this + function register(event: React.FormEvent) { event.preventDefault() const body = { @@ -95,31 +98,53 @@ const SignupModal = (props: Props) => { } } - function validateForm() { - let valid = true - - Object.values(form).forEach( - (input) => input.current?.value.length == 0 && (valid = false) - ) - - Object.values(errors).forEach( - (error) => error.length > 0 && (valid = false) - ) - - return valid - } - - function handleChange(event: React.ChangeEvent) { + function handleNameChange(event: React.ChangeEvent) { event.preventDefault() - const { name, value } = event.target + const fieldName = event.target.name + const value = event.target.value + + if (value.length >= 3) { + api.check(fieldName, value) + .then((response) => { + processNameCheck(fieldName, value, response.data.available) + }, (error) => { + console.error(error) + }) + } else { + validateName(fieldName, value) + } + } + + function processNameCheck(fieldName: string, value: string, available: boolean) { + const newErrors = {...errors} + + if (available) { + // Continue checking for errors + newErrors[fieldName] = '' + setErrors(newErrors) + setFormValid(true) + + validateName(fieldName, value) + } else { + newErrors[fieldName] = `This ${fieldName} is already in use` + setErrors(newErrors) + setFormValid(false) + } + } + + function validateName(fieldName: string, value: string) { let newErrors = {...errors} - switch(name) { + switch(fieldName) { case 'username': - newErrors.username = value.length < 3 - ? 'Username must be at least 3 characters' - : '' + if (value.length < 3) + newErrors.username = 'Username must be at least 3 characters' + else if (value.length > 20) + newErrors.username = 'Username must be less than 20 characters' + else + newErrors.username = '' + break case 'email': @@ -128,6 +153,21 @@ const SignupModal = (props: Props) => { : 'That email address is not valid' break + default: + break + } + + setErrors(newErrors) + setFormValid(validateForm(newErrors)) + } + + function handlePasswordChange(event: React.ChangeEvent) { + event.preventDefault() + + const { name, value } = event.target + let newErrors = {...errors} + + switch(name) { case 'password': newErrors.password = passwordInput.current?.value.includes(usernameInput.current?.value!) ? 'Your password should not contain your username' @@ -151,7 +191,21 @@ const SignupModal = (props: Props) => { } setErrors(newErrors) - setFormValid(validateForm()) + setFormValid(validateForm(newErrors)) + } + + function validateForm(errors: ErrorMap) { + let valid = true + + Object.values(form).forEach( + (input) => input.current?.value.length == 0 && (valid = false) + ) + + Object.values(errors).forEach( + (error) => error.length > 0 && (valid = false) + ) + + return valid } return ( @@ -174,12 +228,11 @@ const SignupModal = (props: Props) => { -
    +
    @@ -188,7 +241,7 @@ const SignupModal = (props: Props) => { fieldName="email" placeholder="Email address" onBlur={check} - onChange={handleChange} + onChange={handleNameChange} error={errors.email} ref={emailInput} /> @@ -196,7 +249,7 @@ const SignupModal = (props: Props) => {
    @@ -204,7 +257,7 @@ const SignupModal = (props: Props) => {
    From 35cf0ee369e1fe7961650e4f96a8bc43ba6a3eaf Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 28 Feb 2022 16:03:34 -0800 Subject: [PATCH 4/6] Rework signup method and open/close behavior --- components/SignupModal/index.tsx | 65 ++++++++++++++------------------ 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/components/SignupModal/index.tsx b/components/SignupModal/index.tsx index 8670eab3..a069e83d 100644 --- a/components/SignupModal/index.tsx +++ b/components/SignupModal/index.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react' -import { withCookies, Cookies } from 'react-cookie' -import { createPortal } from 'react-dom' +import { useCookies } from 'react-cookie' import * as Dialog from '@radix-ui/react-dialog' @@ -26,9 +25,7 @@ interface ErrorMap { const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const SignupModal = (props: Props) => { - // TODO: Clear error state on close - - // Set up error handling + // Set up form states and error handling const [formValid, setFormValid] = useState(false) const [errors, setErrors] = useState({ username: '', @@ -36,6 +33,12 @@ const SignupModal = (props: Props) => { password: '', passwordConfirmation: '' }) + + // Cookies + const [cookies, setCookies] = useCookies() + + // States + const [open, setOpen] = useState(false) // Set up form refs const usernameInput = React.createRef() @@ -44,29 +47,6 @@ const SignupModal = (props: Props) => { const passwordConfirmationInput = React.createRef() const form = [usernameInput, emailInput, passwordInput, passwordConfirmationInput] - function check(event: React.ChangeEvent) { - const name = event.target.name - const value = event.target.value - - if (value.length > 0 && errors[name].length == 0) { - const newErrors = {...errors} - - api.check(name, value) - .then((response) => { - if (!response.data.available) { - newErrors[name] = `This ${name} is already in use` - setErrors(newErrors) - setFormValid(false) - } else { - setFormValid(true) - } - }, (error) => { - console.error(error) - }) - } - } - - // TODO: Refactor this function register(event: React.FormEvent) { event.preventDefault() @@ -79,23 +59,27 @@ const SignupModal = (props: Props) => { } } - if (formValid) { + if (formValid) api.endpoints.users.create(body) .then((response) => { - const cookies = props.cookies - cookies.set('user', response.data.user, { path: '/'}) + // Set cookies + setCookies('user', response.data.user, { path: '/'}) + // Set states accountState.account.authorized = true accountState.account.user = { id: response.data.user.id, username: response.data.user.username } - props.close() + // Close the modal + setOpen(false) }, (error) => { console.error(error) }) - } + .catch(error => { + console.error(error) + }) } function handleNameChange(event: React.ChangeEvent) { @@ -208,8 +192,18 @@ const SignupModal = (props: Props) => { return valid } + function openChange(open: boolean) { + setOpen(open) + setErrors({ + username: '', + email: '', + password: '', + passwordConfirmation: '' + }) + } + return ( - +
  • Sign up @@ -240,7 +234,6 @@ const SignupModal = (props: Props) => {
    { } -export default withCookies(SignupModal) \ No newline at end of file +export default SignupModal \ No newline at end of file From 1c7e602464d14a59c7e0ef0e33aebcc37abd8ca8 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 28 Feb 2022 16:40:16 -0800 Subject: [PATCH 5/6] Refactor and redesign LoginModal --- components/LoginModal/index.scss | 43 +++++++--- components/LoginModal/index.tsx | 141 +++++++++++++++++-------------- 2 files changed, 109 insertions(+), 75 deletions(-) diff --git a/components/LoginModal/index.scss b/components/LoginModal/index.scss index ea98dd58..e9628d99 100644 --- a/components/LoginModal/index.scss +++ b/components/LoginModal/index.scss @@ -1,14 +1,31 @@ -.LoginForm { - #fields { - display: flex; - flex-direction: column; - gap: $unit; - } - .fieldset { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 8px; - } -} +.Login.Dialog form { + display: flex; + flex-direction: column; + gap: $unit / 2; + margin-bottom: $unit; + .Button { + font-size: $font-regular; + padding: ($unit * 1.5) ($unit * 2); + width: 100%; + + &.btn-disabled { + background: $grey-90; + color: $grey-70; + cursor: not-allowed; + } + + &:not(.btn-disabled) { + background: $grey-90; + color: $grey-40; + + &:hover { + background: $grey-80; + } + } + } + + input { + background: $grey-90; + } +} \ No newline at end of file diff --git a/components/LoginModal/index.tsx b/components/LoginModal/index.tsx index a3c4763b..e74f44f4 100644 --- a/components/LoginModal/index.tsx +++ b/components/LoginModal/index.tsx @@ -1,21 +1,18 @@ import React, { useState } from 'react' -import { withCookies, Cookies } from 'react-cookie' -import { createPortal } from 'react-dom' +import { useCookies } from 'react-cookie' + +import * as Dialog from '@radix-ui/react-dialog' import api from '~utils/api' import { accountState } from '~utils/accountState' import Button from '~components/Button' import Fieldset from '~components/Fieldset' -import Modal from '~components/Modal' -import Overlay from '~components/Overlay' +import CrossIcon from '~public/icons/Cross.svg' import './index.scss' -interface Props { - cookies: Cookies - close: () => void -} +interface Props {} interface ErrorMap { [index: string]: string @@ -26,31 +23,40 @@ interface ErrorMap { const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const LoginModal = (props: Props) => { - const emailInput: React.RefObject = React.createRef() - const passwordInput: React.RefObject = React.createRef() - const form: React.RefObject[] = [emailInput, passwordInput] - + // Set up form states and error handling const [formValid, setFormValid] = useState(false) const [errors, setErrors] = useState({ email: '', password: '' }) - function handleChange(event: React.ChangeEvent) { - event.preventDefault() + // Cookies + const [cookies, setCookies] = useCookies() + // States + const [open, setOpen] = useState(false) + + // Set up form refs + const emailInput: React.RefObject = React.createRef() + const passwordInput: React.RefObject = React.createRef() + const form: React.RefObject[] = [emailInput, passwordInput] + + function handleChange(event: React.ChangeEvent) { const { name, value } = event.target - let newErrors = errors + let newErrors = {...errors} switch(name) { case 'email': - errors.email = emailRegex.test(value) - ? '' - : 'That email address is not valid' + if (value.length == 0) + newErrors.email = 'Please enter your email' + else if (!emailRegex.test(value)) + newErrors.email = 'That email address is not valid' + else + newErrors.email = '' break case 'password': - errors.password = value.length == 0 + newErrors.password = value.length == 0 ? 'Please enter your password' : '' break @@ -60,10 +66,10 @@ const LoginModal = (props: Props) => { } setErrors(newErrors) - setFormValid(validateForm()) + setFormValid(validateForm(newErrors)) } - function validateForm() { + function validateForm(errors: ErrorMap) { let valid = true Object.values(form).forEach( @@ -73,11 +79,11 @@ const LoginModal = (props: Props) => { Object.values(errors).forEach( (error) => error.length > 0 && (valid = false) ) - + return valid } - function submit(event: React.FormEvent) { + function login(event: React.FormEvent) { event.preventDefault() const body = { @@ -89,65 +95,76 @@ const LoginModal = (props: Props) => { if (formValid) { api.login(body) .then((response) => { - const cookies = props.cookies - const cookieObj = { user_id: response.data.user.id, username: response.data.user.username, access_token: response.data.access_token } - cookies.set('user', cookieObj, { path: '/'}) + setCookies('user', cookieObj, { path: '/'}) accountState.account.authorized = true accountState.account.user = { id: cookieObj.user_id, username: cookieObj.username } - props.close() + setOpen(false) }, (error) => { console.error(error) }) } } - return ( - createPortal( -
    - {} } - > - -
    -
    + function openChange(open: boolean) { + setOpen(open) + setErrors({ + email: '', + password: '' + }) + } -
    -
    -
    - Forgot your password? - -
    + return ( + + +
  • + Log in +
  • +
    + + event.preventDefault() }> +
    + Log in + + + + + +
    + + +
    + +
    + + - - -
    , - document.body - ) + + + + ) } -export default withCookies(LoginModal) \ No newline at end of file +export default LoginModal \ No newline at end of file From f3d2ff9662392c2030b3f563e7e656d97e4cfa33 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 28 Feb 2022 16:40:48 -0800 Subject: [PATCH 6/6] Implement new LoginModal in HeaderMenu --- components/Fieldset/index.tsx | 2 +- components/HeaderMenu/index.tsx | 15 +-------------- components/SignupModal/index.scss | 1 + components/SignupModal/index.tsx | 4 +--- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/components/Fieldset/index.tsx b/components/Fieldset/index.tsx index b8a98e39..1eb78e55 100644 --- a/components/Fieldset/index.tsx +++ b/components/Fieldset/index.tsx @@ -12,7 +12,7 @@ interface Props { const Fieldset = React.forwardRef(function fieldSet(props, ref) { const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text' - + return (
    { - const { open: signupOpen, openModal: openSignupModal, closeModal: closeSignupModal } = useSignupModal() - const { open: loginOpen, openModal: openLoginModal, closeModal: closeLoginModal } = useLoginModal() - const { open: aboutOpen, openModal: openAboutModal, closeModal: closeAboutModal } = useAboutModal() - function authItems() { return (
  • -
  • - Log in -
  • - {loginOpen ? ( - - ) : null} - +
    - ) } diff --git a/components/SignupModal/index.scss b/components/SignupModal/index.scss index d515648a..fad95e51 100644 --- a/components/SignupModal/index.scss +++ b/components/SignupModal/index.scss @@ -12,6 +12,7 @@ &.btn-disabled { background: $grey-90; color: $grey-70; + cursor: not-allowed; } &:not(.btn-disabled) { diff --git a/components/SignupModal/index.tsx b/components/SignupModal/index.tsx index a069e83d..431a0cc6 100644 --- a/components/SignupModal/index.tsx +++ b/components/SignupModal/index.tsx @@ -88,7 +88,7 @@ const SignupModal = (props: Props) => { const fieldName = event.target.name const value = event.target.value - if (value.length >= 3) { + if (value.length >= 3) { api.check(fieldName, value) .then((response) => { processNameCheck(fieldName, value, response.data.available) @@ -220,8 +220,6 @@ const SignupModal = (props: Props) => { - -