Add login behavior up to loading state.

The remaining functionality depends on implementing a new AuthManager and overlay system.
This commit is contained in:
Daniel Scalzi 2020-05-23 03:31:26 -04:00
parent c718cc741a
commit 5944f70a2a
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
2 changed files with 145 additions and 25 deletions

View File

@ -3,18 +3,47 @@ import LoginField from './login-field/LoginField'
import './Login.css'
enum LoginStatus {
IDLE,
LOADING,
SUCCESS,
ERROR
}
type LoginProperties = {
cancelable: boolean
}
export default class Login extends React.Component<LoginProperties> {
type LoginState = {
rememberMe: boolean,
userValid: boolean,
passValid: boolean,
status: LoginStatus
}
export default class Login extends React.Component<LoginProperties, LoginState> {
private userRef: React.RefObject<LoginField>
private passRef: React.RefObject<LoginField>
constructor(props: LoginProperties) {
super(props)
this.state = {
rememberMe: true,
userValid: false,
passValid: false,
status: LoginStatus.IDLE
}
this.userRef = React.createRef()
this.passRef = React.createRef()
}
getCancelButton(): JSX.Element {
if(this.props.cancelable) {
return (
<>
<div id="loginCancelContainer">
<button id="loginCancelButton">
<button id="loginCancelButton" disabled={this.isFormDisabled()}>
<div id="loginCancelIcon">X</div>
<span id="loginCancelText">Cancel</span>
</button>
@ -26,32 +55,108 @@ export default class Login extends React.Component<LoginProperties> {
}
}
isFormDisabled = (): boolean => {
return this.state.status !== LoginStatus.IDLE
}
isLoading = (): boolean => {
return this.state.status === LoginStatus.LOADING
}
canSave = (): boolean => {
return this.state.passValid && this.state.userValid && !this.isFormDisabled()
}
getButtonText = (): string => {
switch(this.state.status) {
case LoginStatus.LOADING:
return 'LOGGING IN'
case LoginStatus.SUCCESS:
return 'SUCCESS'
case LoginStatus.ERROR:
case LoginStatus.IDLE:
return 'LOGIN'
}
}
handleUserValidityChange = (valid: boolean): void => {
this.setState({
...this.state,
userValid: valid
})
}
handlePassValidityChange = (valid: boolean): void => {
this.setState({
...this.state,
passValid: valid
})
}
handleCheckBoxChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({
...this.state,
rememberMe: event.target.checked
})
}
handleFormSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault()
}
handleLoginButtonClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
console.log(this.userRef.current!.getValue())
console.log(this.passRef.current!.getValue())
this.setState({
...this.state,
status: LoginStatus.LOADING
})
}
render() {
return (
<>
<div id="loginContainer">
{this.getCancelButton()}
<div id="loginContent">
<form id="loginForm">
<form id="loginForm" onSubmit={this.handleFormSubmit}>
<img id="loginImageSeal" src="../images/SealCircle.png"/>
<span id="loginSubheader">MINECRAFT LOGIN</span>
<LoginField password={false} />
<LoginField password={true} />
<LoginField
ref={this.userRef}
password={false}
disabled={this.isFormDisabled()}
onValidityChange={this.handleUserValidityChange} />
<LoginField
ref={this.passRef}
password={true}
disabled={this.isFormDisabled()}
onValidityChange={this.handlePassValidityChange} />
<div id="loginOptions">
<span className="loginSpanDim">
<a href="https://my.minecraft.net/en-us/password/forgot/">forgot password?</a>
</span>
<label id="checkmarkContainer">
<input id="loginRememberOption" type="checkbox" checked></input>
<label id="checkmarkContainer" {...(this.isFormDisabled() ? {disabled: true} : {})} >
<input
id="loginRememberOption"
type="checkbox"
checked={this.state.rememberMe}
onChange={this.handleCheckBoxChange}
disabled={this.isFormDisabled()}
></input>
<span id="loginRememberText" className="loginSpanDim">remember me?</span>
<span className="loginCheckmark"></span>
</label>
</div>
<button id="loginButton" disabled>
<button
id="loginButton"
disabled={!this.canSave()}
onClick={this.handleLoginButtonClick}
{...(this.isLoading() ? {loading: "true"} : {})}>
<div id="loginButtonContent">
LOGIN
{this.getButtonText()}
<svg id="loginSVG" viewBox="0 0 24.87 13.97">
<defs>
<style>{'.arrowLine{transition: 0.25s ease;}'}</style> {/** TODO */}

View File

@ -8,17 +8,19 @@ enum FieldError {
}
type LoginFieldProps = {
password: boolean
password: boolean,
disabled: boolean,
onValidityChange: (valid: boolean) => void
}
type LoginFieldSettings = {
type LoginFieldState = {
errorText: FieldError,
hasError: boolean,
shake: boolean,
value: string
}
export default class LoginField extends React.Component<LoginFieldProps, LoginFieldSettings> {
export default class LoginField extends React.Component<LoginFieldProps, LoginFieldState> {
private readonly USERNAME_REGEX = /^[a-zA-Z0-9_]{1,16}$/
private readonly BASIC_EMAIL_REGEX = /^\S+@\S+\.\S+$/
@ -27,6 +29,7 @@ export default class LoginField extends React.Component<LoginFieldProps, LoginFi
private readonly SHAKE_CLASS = 'shake'
private errorSpanRef: React.RefObject<HTMLSpanElement>
private internalTrigger = false // Indicates that the component updated from an internal trigger.
constructor(props: LoginFieldProps) {
super(props)
@ -40,18 +43,25 @@ export default class LoginField extends React.Component<LoginFieldProps, LoginFi
}
componentDidUpdate() {
if(this.state.hasError) {
// @ts-ignore Opacity is a number, not a string..
this.errorSpanRef.current!.style.opacity = 1
if(this.state.shake) {
this.errorSpanRef.current!.classList.remove(this.SHAKE_CLASS)
void this.errorSpanRef.current!.offsetWidth
this.errorSpanRef.current!.classList.add(this.SHAKE_CLASS)
if(this.internalTrigger) {
if(this.state.hasError) {
// @ts-ignore Opacity is a number, not a string..
this.errorSpanRef.current!.style.opacity = 1
if(this.state.shake) {
this.errorSpanRef.current!.classList.remove(this.SHAKE_CLASS)
void this.errorSpanRef.current!.offsetWidth
this.errorSpanRef.current!.classList.add(this.SHAKE_CLASS)
}
} else {
// @ts-ignore Opacity is a number, not a string..
this.errorSpanRef.current!.style.opacity = 0
}
} else {
// @ts-ignore Opacity is a number, not a string..
this.errorSpanRef.current!.style.opacity = 0
}
this.internalTrigger = false
}
public getValue(): string {
return this.state.value
}
private getFieldSvg(): JSX.Element {
@ -86,7 +96,7 @@ export default class LoginField extends React.Component<LoginFieldProps, LoginFi
return `* ${error}`
}
private getErrorState(shake: boolean, errorText: FieldError): Partial<LoginFieldSettings> {
private getErrorState(shake: boolean, errorText: FieldError): Partial<LoginFieldState> & Required<{hasError: boolean}> {
return {
shake,
errorText,
@ -94,7 +104,7 @@ export default class LoginField extends React.Component<LoginFieldProps, LoginFi
}
}
private getValidState(): Partial<LoginFieldSettings> {
private getValidState(): Partial<LoginFieldState> & Required<{hasError: boolean}> {
return {
hasError: false
}
@ -111,11 +121,13 @@ export default class LoginField extends React.Component<LoginFieldProps, LoginFi
} else {
newState = this.getErrorState(shakeOnError, FieldError.REQUIRED)
}
this.internalTrigger = true
this.setState({
...this.state,
...newState,
value
})
this.props.onValidityChange(!newState.hasError)
}
private validatePassword = (value: string, shakeOnError: boolean): void => {
@ -125,11 +137,13 @@ export default class LoginField extends React.Component<LoginFieldProps, LoginFi
} else {
newState = this.getErrorState(shakeOnError, FieldError.REQUIRED)
}
this.internalTrigger = true
this.setState({
...this.state,
...newState,
value
})
this.props.onValidityChange(!newState.hasError)
}
private getValidateFunction(): (value: string, shakeOnError: boolean) => void {
@ -156,8 +170,9 @@ export default class LoginField extends React.Component<LoginFieldProps, LoginFi
</span>
<input
className="loginField"
disabled={this.props.disabled}
type={this.props.password ? 'password' : 'text'}
value={this.state.value}
defaultValue={this.state.value}
placeholder={this.props.password ? 'PASSWORD' : 'EMAIL OR USERNAME'}
onBlur={this.handleBlur}
onInput={this.handleInput} />