Refactor signup modal

This commit is contained in:
Justin Edmund 2020-09-25 03:40:26 -07:00
parent 365de7ceab
commit b5ddab2119
7 changed files with 326 additions and 64 deletions

View file

@ -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;
}

View file

@ -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<Props> {
interface State {
isPressed: boolean
}
class Button extends React.Component<Props, State> {
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<Props> {
icon = <span className='icon'><Link /></span>
}
return <button className='Button' onClick={this.props.click}>
const classes = classNames({
Button: true,
'btn-pressed': this.state.isPressed,
'btn-disabled': this.props.disabled,
[`btn-${this.props.color}`]: true
})
return <button className={classes} disabled={this.props.disabled} onClick={this.props.click}>
{icon}
<span className='text'>{this.props.children}</span>
</button>

View file

@ -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 */
}

View file

@ -0,0 +1,36 @@
import React from 'react'
import './Fieldset.css'
interface Props {
fieldName: string
placeholder: string
error: string
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
const Fieldset = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const fieldType = (['password', 'confirm_password'].includes(props.fieldName)) ? 'password' : 'text'
return (
<fieldset className="Fieldset">
<input
autoComplete="off"
className="Input"
name={props.fieldName}
type={fieldType}
placeholder={props.placeholder}
onBlur={props.onBlur}
onChange={props.onChange}
ref={ref}
formNoValidate
/>
{
props.error.length > 0 &&
<p className='InputError'>{props.error}</p>
}
</fieldset>
)
})
export default Fieldset

View file

@ -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 */
}
}

View file

@ -6,7 +6,7 @@
margin: 0;
}
.SignupForm .fieldset {
.SignupForm #fields {
display: flex;
flex-direction: column;
gap: 4px;

View file

@ -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 (
<Portal>
<div>
<Modal styleName="SignupForm">
<div id="ModalTop">
<h1>Sign up</h1>
<i className='close' onClick={props.close}><New /></i>
</div>
<form>
<div className="fieldset">
<input className="Input" name="username" type="text" placeholder="Username" />
<input className="Input" name="email" type="text" placeholder="Email address" />
<input className="Input" name="password" type="password" placeholder="Password" />
<input className="Input" name="confirm_password" type="password" placeholder="Password (again)" />
</div>
<div id="ModalBottom">
<button className="Button">Sign up</button>
</div>
</form>
</Modal>
<Overlay onClick={props.close} />
</div>
</Portal>
)
interface State {
formValid: boolean
errors: ErrorMap
}
export default SignupModal
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<Props, State> {
usernameInput: React.RefObject<HTMLInputElement>
emailInput: React.RefObject<HTMLInputElement>
passwordInput: React.RefObject<HTMLInputElement>
passwordConfirmationInput: React.RefObject<HTMLInputElement>
form: React.RefObject<HTMLInputElement>[]
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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(
<div>
<Modal styleName="SignupForm">
<div id="ModalTop">
<h1>Sign up</h1>
<i className='close' onClick={this.props.close}><New /></i>
</div>
<form className="form" onSubmit={this.process}>
<div id="fields">
<Fieldset
fieldName="username"
placeholder="Username"
onBlur={this.check}
onChange={this.handleChange}
error={errors.username}
ref={this.usernameInput}
/>
<Fieldset
fieldName="email"
placeholder="Email address"
onBlur={this.check}
onChange={this.handleChange}
error={errors.email}
ref={this.emailInput}
/>
<Fieldset
fieldName="password"
placeholder="Password"
onChange={this.handleChange}
error={errors.password}
ref={this.passwordInput}
/>
<Fieldset
fieldName="confirm_password"
placeholder="Password (again)"
onChange={this.handleChange}
error={errors.passwordConfirmation}
ref={this.passwordConfirmationInput}
/>
</div>
<div id="ModalBottom">
<Button color="blue" disabled={!this.state.formValid}>Sign up</Button>
</div>
</form>
</Modal>
<Overlay onClick={this.props.close} />
</div>,
document.body
)
)
}
}
export default withCookies(SignupModal)