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
import React, { Component } from "react";
import { Motion, spring, presets } from "react-motion";
import PropTypes from "prop-types";
import document from "global/document";
import Observer from "react-intersection-observer";
import { css } from "@emotion/css";
import { createPortal } from "react-dom";
import React, { Component } from "react"
import { Motion, spring, presets } from "react-motion"
import PropTypes from "prop-types"
import document from "global/document"
import Observer from "react-intersection-observer"
import { css } from "@emotion/css"
import { createPortal } from "react-dom"
import {
isDirectionBottom,
@ -67,11 +67,16 @@ export default class Drawer extends Component {
listenersAttached: false
}
DESKTOP_MODE = false
DRAGGER_HEIGHT_SIZE = 100
MAX_NEGATIVE_SCROLL = 5
SCROLL_TO_CLOSE = 475
ALLOW_DRAWER_TRANSFORM = true
componentDidMount() {
this.DESKTOP_MODE = !window.isMobile
}
componentDidUpdate(prevProps, nextState) {
// in the process of closing the drawer
if (!this.props.open && prevProps.open) {
@ -333,6 +338,17 @@ export default class Drawer extends Component {
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()
stopPropagation = (event) => event.stopPropagation()
@ -392,7 +408,7 @@ export default class Drawer extends Component {
<div
id={id}
style={containerStyle}
onClick={this.props.onRequestClose}
onMouseDown={this.onClickOutside}
className={`${Container} ${containerElementClass} `}
ref={getContainerRef}
>

View File

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

View File

@ -60,23 +60,27 @@
margin-bottom: 20px;
.component {
margin-top: 5px;
}
transition: all 150ms ease-in-out;
.component-row {
display: inline-flex;
flex-direction: row;
justify-content: space-between;
&.hidden {
height: 0px;
opacity: 0;
margin: 0;
}
}
}
.field-error {
color: red;
font-size: 0.8rem;
margin: 20px 0;
}
.component-row {
display: inline-flex;
flex-direction: row;
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 SessionModel from "../session"
const emailRegex = new RegExp(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)
export default class AuthModel {
static login = async (payload) => {
const response = await request({
@ -50,4 +52,31 @@ export default class AuthModel {
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,
})
}
}
}