Merge pull request #103 from ragestudio/improve-login-form

Improve login form
This commit is contained in:
srgooglo 2023-06-05 12:51:09 +02:00 committed by GitHub
commit a564e62fbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 209 additions and 99 deletions

View File

@ -1,11 +1,11 @@
// © Jack Hanford https://github.com/hanford/react-drag-drawer // © Jack Hanford https://github.com/hanford/react-drag-drawer
import React, { Component } from "react"; import React, { Component } from "react"
import { Motion, spring, presets } from "react-motion"; import { Motion, spring, presets } from "react-motion"
import PropTypes from "prop-types"; import PropTypes from "prop-types"
import document from "global/document"; import document from "global/document"
import Observer from "react-intersection-observer"; import Observer from "react-intersection-observer"
import { css } from "@emotion/css"; import { css } from "@emotion/css"
import { createPortal } from "react-dom"; import { createPortal } from "react-dom"
import { import {
isDirectionBottom, isDirectionBottom,
@ -67,11 +67,16 @@ export default class Drawer extends Component {
listenersAttached: false listenersAttached: false
} }
DESKTOP_MODE = false
DRAGGER_HEIGHT_SIZE = 100 DRAGGER_HEIGHT_SIZE = 100
MAX_NEGATIVE_SCROLL = 5 MAX_NEGATIVE_SCROLL = 5
SCROLL_TO_CLOSE = 475 SCROLL_TO_CLOSE = 475
ALLOW_DRAWER_TRANSFORM = true ALLOW_DRAWER_TRANSFORM = true
componentDidMount() {
this.DESKTOP_MODE = !window.isMobile
}
componentDidUpdate(prevProps, nextState) { componentDidUpdate(prevProps, nextState) {
// in the process of closing the drawer // in the process of closing the drawer
if (!this.props.open && prevProps.open) { if (!this.props.open && prevProps.open) {
@ -333,6 +338,17 @@ export default class Drawer extends Component {
this.ALLOW_DRAWER_TRANSFORM = inView this.ALLOW_DRAWER_TRANSFORM = inView
} }
onClickOutside = (event) => {
if (!this.props.allowClose) {
return false
}
// check if is clicking outside main component
if (this.drawer && !this.drawer.contains(event.target)) {
this.props.onRequestClose(this)
}
}
preventDefault = (event) => event.preventDefault() preventDefault = (event) => event.preventDefault()
stopPropagation = (event) => event.stopPropagation() stopPropagation = (event) => event.stopPropagation()
@ -392,7 +408,7 @@ export default class Drawer extends Component {
<div <div
id={id} id={id}
style={containerStyle} style={containerStyle}
onClick={this.props.onRequestClose} onMouseDown={this.onClickOutside}
className={`${Container} ${containerElementClass} `} className={`${Container} ${containerElementClass} `}
ref={getContainerRef} ref={getContainerRef}
> >

View File

@ -1,58 +1,28 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import { Icons } from "components/Icons" import { Icons } from "components/Icons"
import classnames from "classnames"
import AuthModel from "models/auth" import AuthModel from "models/auth"
import config from "config" import config from "config"
import "./index.less" import "./index.less"
const LoginSteps = { const stepsOnError = {
"username": (props) => { username: "This username or email is not exist",
const handleUpdate = (e) => { password: "Password is incorrect",
props.onUpdate(e.target.value) }
}
return <div className="field"> const stepsValidations = {
<span><Icons.Mail /> Username or Email</span> username: async (state) => {
const check = await AuthModel.usernameValidation(state.username).catch((err) => {
return {
exists: false
}
})
<div className="component"> return check.exists
<antd.Input
name="username"
defaultValue={props.defaultValue}
placeholder="myusername / myemail@example.com"
onChange={handleUpdate}
onPressEnter={props.next}
autoCorrect="off"
autoCapitalize="none"
autoComplete="on"
autoFocus
/>
</div>
</div>
}, },
"password": (props) => {
const handleUpdate = (e) => {
props.onUpdate(e.target.value)
}
return <div className="field">
<span><Icons.Lock /> Password</span>
<div className="component">
<antd.Input.Password
name="password"
defaultValue={props.defaultValue}
onChange={handleUpdate}
onPressEnter={props.next}
autoCorrect="off"
autoCapitalize="none"
autoComplete="on"
autoFocus
/>
</div>
</div>
}
} }
const phasesToSteps = { const phasesToSteps = {
@ -72,6 +42,8 @@ export default class Login extends React.Component {
phase: 0, phase: 0,
} }
formRef = React.createRef()
handleFinish = async () => { handleFinish = async () => {
const payload = { const payload = {
username: this.state.loginInputs.username, username: this.state.loginInputs.username,
@ -136,6 +108,14 @@ export default class Login extends React.Component {
} }
onUpdateInput = (input, value) => { onUpdateInput = (input, value) => {
// remove error from ref
this.formRef.current.setFields([
{
name: input,
errors: []
}
])
this.setState({ this.setState({
loginInputs: { loginInputs: {
...this.state.loginInputs, ...this.state.loginInputs,
@ -144,19 +124,28 @@ export default class Login extends React.Component {
}) })
} }
renderCurrentInput = () => { nextStep = async () => {
const { phase } = this.state const phase = phasesToSteps[this.state.phase]
const step = phasesToSteps[phase] if (typeof stepsValidations[phase] === "function") {
this.toogleLoading(true)
return React.createElement(LoginSteps[step], { const result = await stepsValidations[phase](this.state.loginInputs)
onUpdate: (...props) => this.onUpdateInput(step, ...props),
next: this.nextStep, this.toogleLoading(false)
defaultValue: this.state.loginInputs[step],
}) if (!result) {
} this.formRef.current.setFields([
{
name: phase,
errors: [stepsOnError[phase]]
},
])
return false
}
}
nextStep = () => {
const to = this.state.phase + 1 const to = this.state.phase + 1
if (!phasesToSteps[to]) { if (!phasesToSteps[to]) {
@ -204,36 +193,71 @@ export default class Login extends React.Component {
To continue to {config.app.siteName} To continue to {config.app.siteName}
</h3> </h3>
<div className="fields"> <antd.Form
{this.renderCurrentInput()} name="login"
className="fields"
autoCorrect="off"
autoCapitalize="none"
autoComplete="on"
onFinish={this.handleFinish}
ref={this.formRef}
>
<antd.Form.Item
name="username"
className="field"
>
<span><Icons.Mail /> Username or Email</span>
<antd.Input
placeholder="myusername / myemail@example.com"
onChange={(e) => this.onUpdateInput("username", e.target.value)}
onPressEnter={this.nextStep}
disabled={this.state.phase !== 0}
autoFocus
/>
</antd.Form.Item>
<div className="field"> <antd.Form.Item
<div className="component-row"> name="password"
className={classnames(
"field",
{ {
this.state.phase > 0 && <antd.Button ["hidden"]: this.state.phase !== 1,
onClick={this.prevStep}
disabled={this.state.loading}
>
Back
</antd.Button>
} }
<antd.Button )}
onClick={this.nextStep} >
disabled={!this.canNext() || this.state.loading} <span><Icons.Lock /> Password</span>
loading={this.state.loading} <antd.Input.Password
> placeholder="********"
Continue onChange={(e) => this.onUpdateInput("password", e.target.value)}
</antd.Button> onPressEnter={this.nextStep}
</div> />
</div> </antd.Form.Item>
</antd.Form>
<div className="field-error"> <div className="component-row">
{this.state.error} {
</div> this.state.phase > 0 && <antd.Button
onClick={this.prevStep}
disabled={this.state.loading}
>
Back
</antd.Button>
}
<antd.Button
onClick={this.nextStep}
disabled={!this.canNext() || this.state.loading}
loading={this.state.loading}
>
Continue
</antd.Button>
</div>
<div className="field" onClick={this.onClickRegister}> <div className="field-error">
<a>You need a account?</a> {this.state.error}
</div> </div>
<div className="field" onClick={this.onClickRegister}>
<a>You need a account?</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -60,23 +60,27 @@
margin-bottom: 20px; margin-bottom: 20px;
.component { transition: all 150ms ease-in-out;
margin-top: 5px;
}
.component-row { &.hidden {
display: inline-flex; height: 0px;
flex-direction: row; opacity: 0;
margin: 0;
justify-content: space-between;
} }
} }
}
.field-error { .component-row {
color: red; display: inline-flex;
font-size: 0.8rem; flex-direction: row;
margin: 20px 0;
} justify-content: space-between;
}
.field-error {
color: red;
font-size: 0.8rem;
margin: 20px 0;
} }
} }

View File

@ -1,6 +1,8 @@
import request from "../../handlers/request" import request from "../../handlers/request"
import SessionModel from "../session" import SessionModel from "../session"
const emailRegex = new RegExp(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)
export default class AuthModel { export default class AuthModel {
static login = async (payload) => { static login = async (payload) => {
const response = await request({ const response = await request({
@ -50,4 +52,31 @@ export default class AuthModel {
return response return response
} }
static usernameValidation = async (username) => {
let payload = {}
// check if usename arguemnt is an email
if (emailRegex.test(username)) {
payload.email = username
} else {
payload.username = username
}
const response = await request({
method: "get",
url: "/auth/login/validation",
params: payload,
}).catch((error) => {
console.error(error)
return false
})
if (!response) {
throw new Error("Unable to validate user")
}
return response.data
}
} }

View File

@ -0,0 +1,37 @@
import { User } from "@models"
export default {
method: "GET",
route: "/login/validation",
fn: async function (req, res) {
// just check if the provided user or/and email exists, if is return false, otherwise return true
const { username, email } = req.query
if (!username && !email) {
return res.status(400).json({
message: "Missing username or email",
})
}
const user = await User.findOne({
$or: [
{ username: username },
{ email: email },
]
}).catch((error) => {
return false
})
if (user) {
return res.json({
message: "User already exists",
exists: true,
})
} else {
return res.json({
message: "User doesn't exists",
exists: false,
})
}
}
}