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;
|
box-shadow: none;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-family: -apple-system, "Helvetica Neue", "Lucida Grande";
|
font-family: -apple-system, "Helvetica Neue", "Lucida Grande";
|
||||||
|
|
@ -14,6 +13,31 @@
|
||||||
padding: 8px 12px;
|
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 {
|
.Button .icon {
|
||||||
color: #c9c9c9;
|
color: #c9c9c9;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
|
|
@ -21,13 +45,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button .text {
|
.Button .text {
|
||||||
color: #777;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button:hover {
|
.Button:hover {
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button:hover > * {
|
|
||||||
color: #555;
|
color: #555;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -4,18 +4,34 @@ import './Button.css'
|
||||||
import New from '../../../assets/new'
|
import New from '../../../assets/new'
|
||||||
import Menu from '../../../assets/menu'
|
import Menu from '../../../assets/menu'
|
||||||
import Link from '../../../assets/link'
|
import Link from '../../../assets/link'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
color: string
|
||||||
|
disabled: boolean
|
||||||
type: string | null
|
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 = {
|
static defaultProps: Props = {
|
||||||
|
color: 'grey',
|
||||||
|
disabled: false,
|
||||||
type: null,
|
type: null,
|
||||||
click: () => {}
|
click: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
isPressed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let icon
|
let icon
|
||||||
if (this.props.type === 'new') {
|
if (this.props.type === 'new') {
|
||||||
|
|
@ -26,7 +42,14 @@ class Button extends React.Component<Props> {
|
||||||
icon = <span className='icon'><Link /></span>
|
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}
|
{icon}
|
||||||
<span className='text'>{this.props.children}</span>
|
<span className='text'>{this.props.children}</span>
|
||||||
</button>
|
</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;
|
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 {
|
.Modal .Button {
|
||||||
background: #61B3FF;
|
|
||||||
color: #315E87;
|
|
||||||
display: block;
|
display: block;
|
||||||
min-height: 45px;
|
min-height: 45px;
|
||||||
text-align: center;
|
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;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SignupForm .fieldset {
|
.SignupForm #fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import Portal from '~utils/Portal'
|
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 Modal from '~components/Modal/Modal'
|
||||||
import Overlay from '~components/Overlay/Overlay'
|
import Overlay from '~components/Overlay/Overlay'
|
||||||
|
|
||||||
|
|
@ -9,34 +13,205 @@ import './SignupModal.css'
|
||||||
import New from '../../../assets/new'
|
import New from '../../../assets/new'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
cookies: Cookies
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignupModal = (props: Props) => {
|
interface State {
|
||||||
return (
|
formValid: boolean
|
||||||
<Portal>
|
errors: ErrorMap
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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