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' import './Login.css'
enum LoginStatus {
IDLE,
LOADING,
SUCCESS,
ERROR
}
type LoginProperties = { type LoginProperties = {
cancelable: boolean 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 { getCancelButton(): JSX.Element {
if(this.props.cancelable) { if(this.props.cancelable) {
return ( return (
<> <>
<div id="loginCancelContainer"> <div id="loginCancelContainer">
<button id="loginCancelButton"> <button id="loginCancelButton" disabled={this.isFormDisabled()}>
<div id="loginCancelIcon">X</div> <div id="loginCancelIcon">X</div>
<span id="loginCancelText">Cancel</span> <span id="loginCancelText">Cancel</span>
</button> </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() { render() {
return ( return (
<> <>
<div id="loginContainer"> <div id="loginContainer">
{this.getCancelButton()} {this.getCancelButton()}
<div id="loginContent"> <div id="loginContent">
<form id="loginForm"> <form id="loginForm" onSubmit={this.handleFormSubmit}>
<img id="loginImageSeal" src="../images/SealCircle.png"/> <img id="loginImageSeal" src="../images/SealCircle.png"/>
<span id="loginSubheader">MINECRAFT LOGIN</span> <span id="loginSubheader">MINECRAFT LOGIN</span>
<LoginField password={false} /> <LoginField
<LoginField password={true} /> 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"> <div id="loginOptions">
<span className="loginSpanDim"> <span className="loginSpanDim">
<a href="https://my.minecraft.net/en-us/password/forgot/">forgot password?</a> <a href="https://my.minecraft.net/en-us/password/forgot/">forgot password?</a>
</span> </span>
<label id="checkmarkContainer"> <label id="checkmarkContainer" {...(this.isFormDisabled() ? {disabled: true} : {})} >
<input id="loginRememberOption" type="checkbox" checked></input> <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 id="loginRememberText" className="loginSpanDim">remember me?</span>
<span className="loginCheckmark"></span> <span className="loginCheckmark"></span>
</label> </label>
</div> </div>
<button id="loginButton" disabled> <button
id="loginButton"
disabled={!this.canSave()}
onClick={this.handleLoginButtonClick}
{...(this.isLoading() ? {loading: "true"} : {})}>
<div id="loginButtonContent"> <div id="loginButtonContent">
LOGIN {this.getButtonText()}
<svg id="loginSVG" viewBox="0 0 24.87 13.97"> <svg id="loginSVG" viewBox="0 0 24.87 13.97">
<defs> <defs>
<style>{'.arrowLine{transition: 0.25s ease;}'}</style> {/** TODO */} <style>{'.arrowLine{transition: 0.25s ease;}'}</style> {/** TODO */}

View File

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