From 7da7fb75cb66fee9a00cfdcefb55ae9febe109b0 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 26 May 2021 00:51:16 -0400 Subject: [PATCH] more nav-based routing --- vocodes/src/App.tsx | 4 +- .../src/migration/MigrationTopNav_Session.tsx | 6 +- vocodes/src/session/SessionWrapper.ts | 7 + vocodes/src/v1/OldVocodesContainer.tsx | 4 +- vocodes/src/v1/modes/login/LoginComponent.tsx | 4 +- .../src/v1/modes/signup/SignupComponent.tsx | 4 +- vocodes/src/v1/navigation/NewTopNav.tsx | 37 +- vocodes/src/v2/NewVocodesContainer.tsx | 80 +--- vocodes/src/v2/login/LoginComponent.tsx | 160 ++++++++ vocodes/src/v2/signup/SignupComponent.tsx | 382 ++++++++++++++++++ 10 files changed, 601 insertions(+), 87 deletions(-) create mode 100644 vocodes/src/v2/login/LoginComponent.tsx create mode 100644 vocodes/src/v2/signup/SignupComponent.tsx diff --git a/vocodes/src/App.tsx b/vocodes/src/App.tsx index d417db271f..7c66db66fd 100644 --- a/vocodes/src/App.tsx +++ b/vocodes/src/App.tsx @@ -133,7 +133,9 @@ class App extends React.Component { /> -

New Vocodes

+
diff --git a/vocodes/src/migration/MigrationTopNav_Session.tsx b/vocodes/src/migration/MigrationTopNav_Session.tsx index ea94c9e628..d24984bc9a 100644 --- a/vocodes/src/migration/MigrationTopNav_Session.tsx +++ b/vocodes/src/migration/MigrationTopNav_Session.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { MigrationMode } from '../App' import { SessionWrapper } from '../session/SessionWrapper'; +import { Link } from 'react-router-dom'; interface Props { sessionWrapper: SessionWrapper, @@ -29,10 +30,11 @@ function MigrationTopNav_Session(props: Props) { onClick={() => props.setMigrationModeCallback(MigrationMode.OLD_VOCODES)} >{displayName} } else { - sessionLink = + >Sign Up / Login } return sessionLink; diff --git a/vocodes/src/session/SessionWrapper.ts b/vocodes/src/session/SessionWrapper.ts index eb4fbe0012..dc6e492b17 100644 --- a/vocodes/src/session/SessionWrapper.ts +++ b/vocodes/src/session/SessionWrapper.ts @@ -1,5 +1,12 @@ import { SessionStateResponse } from "./SessionState"; +/** + * SessionWrapper can wrap each of the following states: + * + * 1) non-existing session + * 2) logged-out session + * 3) logged-in session + */ export class SessionWrapper { sessionStateResponse?: SessionStateResponse; diff --git a/vocodes/src/v1/OldVocodesContainer.tsx b/vocodes/src/v1/OldVocodesContainer.tsx index b5e4b7af77..32e6227de5 100644 --- a/vocodes/src/v1/OldVocodesContainer.tsx +++ b/vocodes/src/v1/OldVocodesContainer.tsx @@ -15,10 +15,10 @@ import { VideoJob, VideoJobStatus } from './modes/video/VideoJob'; import { VideoJobPoller } from './modes/video/VideoJobPoller'; import { VideoQueuePoller } from './modes/video/VideoQueuePoller'; import { VideoQueueStats } from './modes/video/VideoQueueStats'; -import { SignupComponent } from './modes/signup/SignupComponent'; +import { SignupComponent } from '../v2/signup/SignupComponent'; import { ApiConfig } from './api/ApiConfig'; import { SessionStateResponse } from './api/SessionState'; -import { LoginComponent } from './modes/login/LoginComponent'; +import { LoginComponent } from '../v2/login/LoginComponent'; import { UploadComponent } from './modes/upload/UploadComponent'; interface Props { diff --git a/vocodes/src/v1/modes/login/LoginComponent.tsx b/vocodes/src/v1/modes/login/LoginComponent.tsx index e3d425a853..e8bef3da6e 100644 --- a/vocodes/src/v1/modes/login/LoginComponent.tsx +++ b/vocodes/src/v1/modes/login/LoginComponent.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ApiConfig } from '../../api/ApiConfig'; -import { Mode } from '../../../AppMode'; +import { ApiConfig } from '../../v1/api/ApiConfig'; +import { Mode } from '../../AppMode'; enum FieldTriState { EMPTY_FALSE, diff --git a/vocodes/src/v1/modes/signup/SignupComponent.tsx b/vocodes/src/v1/modes/signup/SignupComponent.tsx index fc7ec56744..f2e4875708 100644 --- a/vocodes/src/v1/modes/signup/SignupComponent.tsx +++ b/vocodes/src/v1/modes/signup/SignupComponent.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { ApiConfig } from '../../api/ApiConfig'; -import { Mode } from '../../../AppMode'; +import { ApiConfig } from '../../v1/api/ApiConfig'; +import { Mode } from '../../AppMode'; enum FieldTriState { EMPTY_FALSE, diff --git a/vocodes/src/v1/navigation/NewTopNav.tsx b/vocodes/src/v1/navigation/NewTopNav.tsx index 8239a62a73..7c7dc8f120 100644 --- a/vocodes/src/v1/navigation/NewTopNav.tsx +++ b/vocodes/src/v1/navigation/NewTopNav.tsx @@ -1,13 +1,12 @@ import React from 'react'; -import { NewMode } from '../../v2/NewVocodesContainer' import { SessionStateResponse } from '../api/SessionState'; +import { SessionWrapper } from '../../session/SessionWrapper'; interface Props { sessionState?: SessionStateResponse, - mode: NewMode, + sessionWrapper: SessionWrapper, ///historyBadgeCount: number, //isHistoryCountBadgeVisible: boolean, - switchModeCallback: (mode: NewMode ) => void, logoutHandler: () => void, } @@ -46,30 +45,30 @@ function NewTopNav(props: Props) { if (loggedIn) { loginManagement = ( - props.switchModeCallback(NewMode.MY_PROFILE_MODE)}>{displayName} - props.logoutHandler()}>Log Out + {}}>{displayName} + {}}>Log Out ); column1 = (
); } else { loginManagement = ( - props.switchModeCallback(NewMode.LOGIN_MODE)}>Login - props.switchModeCallback(NewMode.SIGNUP_MODE)} className="signup">Sign Up + {}}>Login + {}} className="signup">Sign Up ); column1 = (
); @@ -77,7 +76,7 @@ function NewTopNav(props: Props) {
); @@ -88,10 +87,10 @@ function NewTopNav(props: Props) { NEW Community Features let you use Your Own Voices and Video:

- props.switchModeCallback(NewMode.COMMUNITY_TTS_MODE)}>TTS - props.switchModeCallback(NewMode.COMMUNITY_W2L_MODE)}>Video - props.switchModeCallback(NewMode.UPLOAD_MODE)}>Upload - props.switchModeCallback(NewMode.MY_DATA_MODE)}>My Data + {}}>TTS + {}}>Video + {}}>Upload + {}}>My Data ); } @@ -105,13 +104,13 @@ function NewTopNav(props: Props) {
@@ -119,19 +118,19 @@ function NewTopNav(props: Props) {
diff --git a/vocodes/src/v2/NewVocodesContainer.tsx b/vocodes/src/v2/NewVocodesContainer.tsx index 579c67fb05..f773c719d5 100644 --- a/vocodes/src/v2/NewVocodesContainer.tsx +++ b/vocodes/src/v2/NewVocodesContainer.tsx @@ -1,27 +1,16 @@ import React from 'react'; import { NewTopNav } from '../v1/navigation/NewTopNav'; -import { SessionStateResponse } from '../v1/api/SessionState'; -import { LoginComponent } from '../v1/modes/login/LoginComponent'; -import { SignupComponent } from '../v1/modes/signup/SignupComponent'; +import { LoginComponent } from './login/LoginComponent'; +import { SignupComponent } from './signup/SignupComponent'; +import { Switch, Route } from 'react-router-dom'; +import { SessionWrapper } from '../session/SessionWrapper'; -enum NewMode { - LOGIN_MODE, - SIGNUP_MODE, - COMMUNITY_TTS_MODE, - COMMUNITY_W2L_MODE, - UPLOAD_MODE, - MY_DATA_MODE, - MY_PROFILE_MODE, - EDIT_MY_PROFILE_MODE, -} - interface Props { - sessionState?: SessionStateResponse, + sessionWrapper: SessionWrapper, } interface State { - mode: NewMode, } class NewVocodesContainer extends React.Component { @@ -30,64 +19,37 @@ class NewVocodesContainer extends React.Component { super(props); this.state = { - mode: NewMode.SIGNUP_MODE, } } - switchMode = (mode: NewMode) => { - this.setState({mode: mode}); - } - logout = () => { } public render() { - let innerComponent =
; - let loggedIn = false; - let displayName = "My Account"; - - if (this.props.sessionState !== undefined) { - console.log('sessionstate', this.props.sessionState); - loggedIn = this.props.sessionState.logged_in; - if (this.props.sessionState.user !== undefined && - this.props.sessionState.user !== null) { - displayName = this.props.sessionState.user.display_name; - } - } - - - switch (this.state.mode) { - case NewMode.SIGNUP_MODE: - innerComponent = ( - {}} - loggedIn={loggedIn} - /> - ); - break; - case NewMode.LOGIN_MODE: - innerComponent = ( - {}} - loggedIn={loggedIn} - /> - ); - break; - } - return (
- {innerComponent} + + + {}} + sessionWrapper={this.props.sessionWrapper} + /> + + + {}} + sessionWrapper={this.props.sessionWrapper} + /> + +
@@ -95,4 +57,4 @@ class NewVocodesContainer extends React.Component { } } -export { NewVocodesContainer, NewMode } +export { NewVocodesContainer } diff --git a/vocodes/src/v2/login/LoginComponent.tsx b/vocodes/src/v2/login/LoginComponent.tsx new file mode 100644 index 0000000000..e1814d1f7b --- /dev/null +++ b/vocodes/src/v2/login/LoginComponent.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { ApiConfig } from '../../v1/api/ApiConfig'; +import { Mode } from '../../AppMode'; +import { SessionWrapper } from '../../session/SessionWrapper'; + +enum FieldTriState { + EMPTY_FALSE, + FALSE, + TRUE, +} + +interface Props { + sessionWrapper: SessionWrapper, + querySessionCallback : () => void, + switchModeCallback?: (mode: Mode) => void, +} + +interface State { + usernameOrEmail: string, + usernameOrEmailValid: FieldTriState, + usernameOrEmailInvalidReason: string, + + password: string, + passwordValid: FieldTriState, + passwordInvalidReason: string, +} + +class LoginComponent extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { + usernameOrEmail: "", + usernameOrEmailValid: FieldTriState.EMPTY_FALSE, + usernameOrEmailInvalidReason: "", + + password: "", + passwordValid: FieldTriState.EMPTY_FALSE, + passwordInvalidReason: "", + }; + } + + handleUsernameOrEmailChange = (ev: React.FormEvent) => { + ev.preventDefault(); + + const usernameOrEmail = (ev.target as HTMLInputElement).value; + + this.setState({ + usernameOrEmail: usernameOrEmail, + }) + + return false; + } + + handlePasswordChange = (ev: React.FormEvent) => { + ev.preventDefault(); + + const password = (ev.target as HTMLInputElement).value; + + this.setState({ + password: password, + }) + + return false; + } + + handleFormSubmit = (ev: React.FormEvent) : boolean => { + ev.preventDefault(); + + const api = new ApiConfig(); + const endpointUrl = api.login(); + + const request = { + username_or_email: this.state.usernameOrEmail, + password: this.state.password, + } + + fetch(endpointUrl, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(request), + }) + .then(res => res.json()) + .then(res => { + console.log('login response', res) + if (res.success) { + console.log('querying new session'); + this.props.querySessionCallback(); + + if (this.props.switchModeCallback !== undefined) { + this.props.switchModeCallback(Mode.SPEAK_MODE); + } + return; + } + }) + .catch(e => { + //this.props.onSpeakErrorCallback(); + }); + + return false; + } + + public render() { + if (this.props.sessionWrapper.isLoggedIn()) { + return
Invalid view for logged in users.
; + } + + return ( +
+

Login

+ +

+ +
+
+ +
+ + + + + + + +
+ {/*

*/} +
+ +
+ +
+ + + + + + + +
+ {/*

*/} +
+ +
+ +
+
+ +
+
+
+
+ ) + } +} + +export { LoginComponent }; \ No newline at end of file diff --git a/vocodes/src/v2/signup/SignupComponent.tsx b/vocodes/src/v2/signup/SignupComponent.tsx new file mode 100644 index 0000000000..973237d5fc --- /dev/null +++ b/vocodes/src/v2/signup/SignupComponent.tsx @@ -0,0 +1,382 @@ +import React from 'react'; +import { ApiConfig } from '../../v1/api/ApiConfig'; +import { Mode } from '../../AppMode'; +import { SessionWrapper } from '../../session/SessionWrapper'; + +enum FieldTriState { + EMPTY_FALSE, + FALSE, + TRUE, +} + +interface Props { + sessionWrapper: SessionWrapper, + querySessionCallback : () => void, + switchModeCallback?: (mode: Mode) => void, +} + +interface State { + username: string, + usernameValid: FieldTriState, + usernameInvalidReason: string, + + email: string, + emailValid: FieldTriState, + emailInvalidReason: string, + + password: string, + passwordValid: FieldTriState, + passwordInvalidReason: string, + + passwordConfirmation: string, + passwordConfirmationValid: FieldTriState, + passwordConfirmationInvalidReason: string, +} + +class SignupComponent extends React.Component { + + constructor(props: Props) { + super(props); + this.state = { + username: "", + usernameValid: FieldTriState.EMPTY_FALSE, + usernameInvalidReason: "", + + email: "", + emailValid: FieldTriState.EMPTY_FALSE, + emailInvalidReason: "", + + password: "", + passwordValid: FieldTriState.EMPTY_FALSE, + passwordInvalidReason: "", + + passwordConfirmation: "", + passwordConfirmationValid: FieldTriState.EMPTY_FALSE, + passwordConfirmationInvalidReason: "", + }; + } + + handleUsernameChange = (ev: React.FormEvent) => { + ev.preventDefault(); + + const username = (ev.target as HTMLInputElement).value; + + let usernameValid = FieldTriState.EMPTY_FALSE; + let usernameInvalidReason = ""; + + if (username.length > 1) { + if (username.length < 3) { + usernameValid = FieldTriState.FALSE; + usernameInvalidReason = "username is too short"; + } else if (username.length > 15) { + usernameValid = FieldTriState.FALSE; + usernameInvalidReason = "username is too long"; + } else { + usernameValid = FieldTriState.TRUE; + } + } + + this.setState({ + username: username, + usernameValid: usernameValid, + usernameInvalidReason: usernameInvalidReason, + }) + + return false; + } + + handleEmailChange = (ev: React.FormEvent) => { + ev.preventDefault(); + + const email = (ev.target as HTMLInputElement).value; + + let emailValid = FieldTriState.EMPTY_FALSE; + let emailInvalidReason = ""; + + if (email.length > 1) { + if (email.length < 3) { + emailValid = FieldTriState.FALSE; + emailInvalidReason = "email is too short"; + } else if (!email.includes("@")) { + emailValid = FieldTriState.FALSE; + emailInvalidReason = "email is invalid"; + } else { + emailValid = FieldTriState.TRUE; + } + } + + this.setState({ + email: email, + emailValid: emailValid, + emailInvalidReason: emailInvalidReason, + }) + + return false; + } + + handlePasswordChange = (ev: React.FormEvent) => { + ev.preventDefault(); + + const password = (ev.target as HTMLInputElement).value; + + let passwordValid = FieldTriState.EMPTY_FALSE; + let passwordInvalidReason = ""; + let passwordConfirmationValid = FieldTriState.EMPTY_FALSE; + let passwordConfirmationInvalidReason = ""; + + if (password.length > 1) { + if (password.length < 5) { + passwordValid = FieldTriState.FALSE; + passwordInvalidReason = "password is too short"; + } else { + passwordValid = FieldTriState.TRUE; + } + + if (password !== this.state.passwordConfirmation) { + passwordConfirmationValid = FieldTriState.FALSE; + passwordConfirmationInvalidReason = "passwords do not match"; + } else { + passwordConfirmationValid = FieldTriState.TRUE; + passwordConfirmationInvalidReason = ""; + + } + } + + this.setState({ + password: password, + passwordValid: passwordValid, + passwordInvalidReason: passwordInvalidReason, + passwordConfirmationValid: passwordConfirmationValid, + passwordConfirmationInvalidReason: passwordConfirmationInvalidReason, + }) + + return false; + } + + + handlePasswordConfirmationChange = (ev: React.FormEvent) => { + ev.preventDefault(); + + const passwordConfirmation = (ev.target as HTMLInputElement).value; + + let passwordConfirmationValid = FieldTriState.EMPTY_FALSE; + let passwordConfirmationInvalidReason = ""; + + if (passwordConfirmation.length > 1) { + + if (passwordConfirmation !== this.state.password) { + passwordConfirmationValid = FieldTriState.FALSE; + passwordConfirmationInvalidReason = "passwords do not match"; + } else { + passwordConfirmationValid = FieldTriState.TRUE; + passwordConfirmationInvalidReason = ""; + + } + } + + this.setState({ + passwordConfirmation: passwordConfirmation, + passwordConfirmationValid: passwordConfirmationValid, + passwordConfirmationInvalidReason: passwordConfirmationInvalidReason, + }) + + return false; + } + + handleFormSubmit = (ev: React.FormEvent) : boolean => { + ev.preventDefault(); + + if (!this.state.usernameValid || + !this.state.emailValid || + !this.state.passwordValid || + !this.state.passwordConfirmationValid) { + return false; + } + + const api = new ApiConfig(); + const endpointUrl = api.createAccount(); + + const request = { + username: this.state.username, + email_address: this.state.email, + password: this.state.password, + password_confirmation: this.state.passwordConfirmation, + } + + fetch(endpointUrl, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(request), + }) + .then(res => res.json()) + .then(res => { + console.log('create account response', res) + if (res.success) { + console.log('querying new session'); + this.props.querySessionCallback(); + + if (this.props.switchModeCallback !== undefined) { + this.props.switchModeCallback(Mode.SPEAK_MODE); + } + return; + } + }) + .catch(e => { + //this.props.onSpeakErrorCallback(); + }); + + return false; + } + + public render() { + if (this.props.sessionWrapper.isLoggedIn()) { + return
Invalid view for logged in users.
; + } + + let usernameInputClass = "input"; + let usernameHelpClass = "help"; + switch (this.state.usernameValid) { + case FieldTriState.EMPTY_FALSE: + break; + case FieldTriState.FALSE: + usernameInputClass += " is-danger"; + usernameHelpClass += " is-danger"; + break; + case FieldTriState.TRUE: + usernameInputClass += " is-success"; + usernameHelpClass += " is-success"; + break; + } + + let emailInputClass = "input"; + let emailHelpClass = "help"; + switch (this.state.emailValid) { + case FieldTriState.EMPTY_FALSE: + break; + case FieldTriState.FALSE: + emailInputClass += " is-danger"; + emailHelpClass += " is-danger"; + break; + case FieldTriState.TRUE: + emailInputClass += " is-success"; + emailHelpClass += " is-success"; + break; + } + + let passwordInputClass = "input"; + let passwordHelpClass = "help"; + switch (this.state.passwordValid) { + case FieldTriState.EMPTY_FALSE: + break; + case FieldTriState.FALSE: + passwordInputClass += " is-danger"; + passwordHelpClass += " is-danger"; + break; + case FieldTriState.TRUE: + passwordInputClass += " is-success"; + passwordHelpClass += " is-success"; + break; + } + + let passwordConfirmationInputClass = "input"; + let passwordConfirmationHelpClass = "help"; + switch (this.state.passwordConfirmationValid) { + case FieldTriState.EMPTY_FALSE: + break; + case FieldTriState.FALSE: + passwordConfirmationInputClass += " is-danger"; + passwordConfirmationHelpClass += " is-danger"; + break; + case FieldTriState.TRUE: + passwordConfirmationInputClass += " is-success"; + passwordConfirmationHelpClass += " is-success"; + break; + } + + return ( +
+

Sign Up

+ +
+
+ +
+ + + + + + + +
+

{this.state.usernameInvalidReason}

+
+ +
+ +
+ + + + + + + +
+

{this.state.emailInvalidReason}

+
+ +
+ +
+ + + + + + + +
+

{this.state.passwordInvalidReason}

+
+ +
+ +
+ + + + + + + +
+

{this.state.passwordConfirmationInvalidReason}

+
+ +
+ +
+ Remember your password! We don't have password reset currently, and it'll be a + few more weeks before it's added (there are more important features to work on). If you lose your + password, please let us know in Discord. +
+ +
+
+ +
+
+ +
+ +
+ ) + } +} + +export { SignupComponent }; \ No newline at end of file