浏览代码

Add login behavior up to loading state.

The remaining functionality depends on implementing a new AuthManager and overlay system.
Daniel Scalzi 5 年之前
父节点
当前提交
5944f70a2a
共有 2 个文件被更改,包括 145 次插入25 次删除
  1. 114 9
      src/renderer/components/login/Login.tsx
  2. 31 16
      src/renderer/components/login/login-field/LoginField.tsx

+ 114 - 9
src/renderer/components/login/Login.tsx

@@ -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 */}

+ 31 - 16
src/renderer/components/login/login-field/LoginField.tsx

@@ -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} />