Refactor signup modal
This commit is contained in:
parent
365de7ceab
commit
b5ddab2119
7 changed files with 326 additions and 64 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
32
src/components/Fieldset/Fieldset.css
Normal file
32
src/components/Fieldset/Fieldset.css
Normal 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 */
|
||||
}
|
||||
36
src/components/Fieldset/Fieldset.tsx
Normal file
36
src/components/Fieldset/Fieldset.tsx
Normal 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
|
||||
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.SignupForm .fieldset {
|
||||
.SignupForm #fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in a new issue