From b5ddab211911685a8bb6deab7b04425a254db473 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Fri, 25 Sep 2020 03:40:26 -0700 Subject: [PATCH] Refactor signup modal --- src/components/Button/Button.css | 33 ++- src/components/Button/Button.tsx | 29 ++- src/components/Fieldset/Fieldset.css | 32 +++ src/components/Fieldset/Fieldset.tsx | 36 ++++ src/components/Modal/Modal.css | 27 +-- src/components/SignupModal/SignupModal.css | 2 +- src/components/SignupModal/SignupModal.tsx | 231 ++++++++++++++++++--- 7 files changed, 326 insertions(+), 64 deletions(-) create mode 100644 src/components/Fieldset/Fieldset.css create mode 100644 src/components/Fieldset/Fieldset.tsx diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css index 89827e17..4b3c37e1 100644 --- a/src/components/Button/Button.css +++ b/src/components/Button/Button.css @@ -4,7 +4,6 @@ box-shadow: none; align-items: center; - background: white; border-radius: 6px; display: inline-flex; font-family: -apple-system, "Helvetica Neue", "Lucida Grande"; @@ -14,6 +13,31 @@ padding: 8px 12px; } +.Button.btn-grey { + background: white; + color: #777; +} + +.Button.btn-blue { + background: #61B3FF; + color: #315E87; +} + +.Button.btn-blue:hover { + background: #4B9BE5; + color: #233E56; +} + +.Button.btn-disabled { + background: #e0e0e0; + color: #bababa; +} + +.Button.btn-disabled:hover { + background: #e0e0e0; + color: #bababa; +} + .Button .icon { color: #c9c9c9; height: 12px; @@ -21,13 +45,10 @@ } .Button .text { - color: #777; + color: inherit; } .Button:hover { - cursor: pointer; -} - -.Button:hover > * { color: #555; + cursor: pointer; } \ No newline at end of file diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 9cc2d0fc..97f0edd0 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -4,18 +4,34 @@ import './Button.css' import New from '../../../assets/new' import Menu from '../../../assets/menu' import Link from '../../../assets/link' +import classNames from 'classnames' interface Props { + color: string + disabled: boolean type: string | null - click: () => void + click: any } -class Button extends React.Component { +interface State { + isPressed: boolean +} + +class Button extends React.Component { static defaultProps: Props = { + color: 'grey', + disabled: false, type: null, click: () => {} } + constructor(props: Props) { + super(props) + this.state = { + isPressed: false, + } + } + render() { let icon if (this.props.type === 'new') { @@ -26,7 +42,14 @@ class Button extends React.Component { icon = } - return diff --git a/src/components/Fieldset/Fieldset.css b/src/components/Fieldset/Fieldset.css new file mode 100644 index 00000000..7ef09a00 --- /dev/null +++ b/src/components/Fieldset/Fieldset.css @@ -0,0 +1,32 @@ +.Fieldset { + border: none; + display: inline-flex; + flex-direction: column; + padding: 0; + margin: 0 0 8px 0; +} + +.Input { + -webkit-font-smoothing: antialiased; + border: none; + + background-color: white; + border-radius: 6px; + color: #555; + display: block; + font-size: 18px; + padding: 12px 16px; + width: 100%; +} + +.InputError { + color: #D13A3A; + font-size: 14px; + margin: 0 0 4px 0; + padding: 4px 16px; +} + +::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: #a9a9a9 !important; + opacity: 1; /* Firefox */ + } \ No newline at end of file diff --git a/src/components/Fieldset/Fieldset.tsx b/src/components/Fieldset/Fieldset.tsx new file mode 100644 index 00000000..c1849c28 --- /dev/null +++ b/src/components/Fieldset/Fieldset.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import './Fieldset.css' + +interface Props { + fieldName: string + placeholder: string + error: string + onBlur?: (event: React.ChangeEvent) => void + onChange?: (event: React.ChangeEvent) => void +} + +const Fieldset = React.forwardRef((props, ref) => { + const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text' + + return ( +
+ + { + props.error.length > 0 && +

{props.error}

+ } +
+ ) +}) + +export default Fieldset \ No newline at end of file diff --git a/src/components/Modal/Modal.css b/src/components/Modal/Modal.css index 1338650f..4de30e57 100644 --- a/src/components/Modal/Modal.css +++ b/src/components/Modal/Modal.css @@ -24,33 +24,8 @@ z-index: 10; } -.Modal .Input { - -webkit-font-smoothing: antialiased; - border: none; - - background-color: white; - border-radius: 6px; - color: #555; - display: block; - font-size: 18px; - margin-bottom: 8px; - padding: 12px 16px; -} - .Modal .Button { - background: #61B3FF; - color: #315E87; display: block; min-height: 45px; text-align: center; -} - -.Modal .Button:hover { - background: #4B9BE5; - color: #233E56; -} - -::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ - color: #a9a9a9 !important; - opacity: 1; /* Firefox */ - } \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/SignupModal/SignupModal.css b/src/components/SignupModal/SignupModal.css index 5f0157f6..ca212d3a 100644 --- a/src/components/SignupModal/SignupModal.css +++ b/src/components/SignupModal/SignupModal.css @@ -6,7 +6,7 @@ margin: 0; } -.SignupForm .fieldset { +.SignupForm #fields { display: flex; flex-direction: column; gap: 4px; diff --git a/src/components/SignupModal/SignupModal.tsx b/src/components/SignupModal/SignupModal.tsx index 18bce6e7..b1d43396 100644 --- a/src/components/SignupModal/SignupModal.tsx +++ b/src/components/SignupModal/SignupModal.tsx @@ -1,6 +1,10 @@ -import React from 'react' -import Portal from '~utils/Portal' +import React, { useEffect, useState } from 'react' +import { withCookies, Cookies } from 'react-cookie' +import { createPortal } from 'react-dom' +import api from '~utils/api' +import Button from '~components/Button/Button' +import Fieldset from '~components/Fieldset/Fieldset' import Modal from '~components/Modal/Modal' import Overlay from '~components/Overlay/Overlay' @@ -9,34 +13,205 @@ import './SignupModal.css' import New from '../../../assets/new' interface Props { + cookies: Cookies close: () => void } -const SignupModal = (props: Props) => { - return ( - -
- -
-

Sign up

- -
-
-
- - - - -
-
- -
-
-
- -
-
- ) +interface State { + formValid: boolean + errors: ErrorMap } -export default SignupModal \ No newline at end of file +interface ErrorMap { + [index: string]: string + username: string + email: string + password: string + passwordConfirmation: string +} + +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,}))$/ + +class SignupModal extends React.Component { + usernameInput: React.RefObject + emailInput: React.RefObject + passwordInput: React.RefObject + passwordConfirmationInput: React.RefObject + form: React.RefObject[] + + constructor(props: Props) { + super(props) + this.state = { + formValid: false, + errors: { + username: '', + email: '', + password: '', + passwordConfirmation: '' + } + } + + this.usernameInput = React.createRef() + this.emailInput = React.createRef() + this.passwordInput = React.createRef() + this.passwordConfirmationInput = React.createRef() + this.form = [this.usernameInput, this.emailInput, this.passwordInput, this.passwordConfirmationInput] + } + + check = (event: React.ChangeEvent) => { + const name = event.target.name + const value = event.target.value + + if (value.length > 0 && this.state.errors[name].length == 0) { + const errors = this.state.errors + + api.check(name, value) + .then((response) => { + if (!response.data.available) { + errors[name] = `This ${name} is already in use` + } + + this.setState({ errors: errors }) + }, (error) => { + console.log(error) + }) + } + } + + process = (event: React.FormEvent) => { + event.preventDefault() + + const body = { + user: { + username: this.usernameInput.current?.value, + email: this.emailInput.current?.value, + password: this.passwordInput.current?.value, + password_confirmation: this.passwordConfirmationInput.current?.value + } + } + + if (this.state.formValid) { + api.endpoints.users.create(body) + .then((response) => { + const cookies = this.props.cookies + cookies.set('user', response.data.user, { path: '/'}) + }, (error) => { + console.log(error) + }) + } + } + + validateForm = () => { + let valid = true + + Object.values(this.form).forEach( + (input) => input.current?.value.length == 0 && (valid = false) + ) + + Object.values(this.state.errors).forEach( + (error) => error.length > 0 && (valid = false) + ) + + return valid + } + + handleChange = (event: React.ChangeEvent) => { + event.preventDefault() + + const { name, value } = event.target + const errors = this.state.errors + + switch(name) { + case 'username': + errors.username = value.length < 3 + ? 'Username must be at least 3 characters' + : '' + break + + case 'email': + errors.email = emailRegex.test(value) + ? '' + : 'That email address is not valid' + break + + case 'password': + errors.password = value.length < 8 + ? 'Password must be at least 8 characters' + : '' + break + + case 'confirm_password': + errors.passwordConfirmation = this.passwordInput.current?.value === this.passwordConfirmationInput.current?.value + ? '' + : 'Your passwords don\'t match' + break + + default: + break + } + + this.setState({ errors: errors }) + this.setState({ formValid: this.validateForm() }) + } + + render() { + const errors = this.state.errors + return ( + createPortal( +
+ +
+

Sign up

+ +
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+ +
, + document.body + ) + ) + } +} + + +export default withCookies(SignupModal) \ No newline at end of file