diff --git a/app/components/App.tsx b/app/components/App.tsx index 80d3ac49..dbc41793 100644 --- a/app/components/App.tsx +++ b/app/components/App.tsx @@ -21,10 +21,10 @@ export interface AppProps { hasDatasets: boolean loading: boolean sessionID: string - peername: string apiConnection?: number hasAcceptedTOS: boolean hasSignedUp: boolean + hasSignedIn: boolean toast: IToast fetchSession: () => Promise fetchMyDatasets: (page?: number, pageSize?: number) => Promise @@ -33,7 +33,9 @@ export interface AppProps { initDataset: (path: string, name: string, format: string) => Promise acceptTOS: () => Action setHasSignedUp: () => Action + setHasSignedIn: () => Action signup: (username: string, email: string, password: string) => Promise + signin: (username: string, password: string) => Promise closeToast: () => Action setApiConnection: (status: number) => Action pingApi: () => Promise @@ -174,13 +176,15 @@ export default class App extends React.Component { render () { const { hasSignedUp, + hasSignedIn, hasAcceptedTOS, - peername, acceptTOS, signup, + signin, toast, closeToast, - setHasSignedUp + setHasSignedUp, + setHasSignedIn } = this.props return (
{ {this.renderAppError()} {this.renderModal()} {this.renderNoDatasets()} diff --git a/app/components/Onboard.tsx b/app/components/Onboard.tsx index 8b733031..1090878f 100644 --- a/app/components/Onboard.tsx +++ b/app/components/Onboard.tsx @@ -5,26 +5,30 @@ import { CSSTransition } from 'react-transition-group' import Welcome from './Welcome' import Signup from './Signup' +import Signin from './Signin' export interface OnboardProps { - peername: string hasAcceptedTOS: boolean hasSignedUp: boolean - + hasSignedIn: boolean acceptTOS: () => Action setHasSignedUp: () => Action + setHasSignedIn: () => Action signup: (username: string, email: string, password: string) => Promise + signin: (username: string, password: string) => Promise } // Onboard is a series of flows for onboarding a new user const Onboard: React.FunctionComponent = ( { - peername, hasAcceptedTOS, hasSignedUp, + hasSignedIn, acceptTOS, signup, - setHasSignedUp + setHasSignedUp, + signin, + setHasSignedIn }) => { const renderWelcome = () => { return ( @@ -40,7 +44,7 @@ const Onboard: React.FunctionComponent = ( ) } - const renderChoosePeerName = () => { + const renderSignup = () => { return ( = ( ) } + const renderSignin = () => { + return ( + + + + ) + } + return (
{renderWelcome()} - {renderChoosePeerName()} + {renderSignup()} + {renderSignin()}
) } diff --git a/app/components/Signin.tsx b/app/components/Signin.tsx new file mode 100644 index 00000000..aefad88f --- /dev/null +++ b/app/components/Signin.tsx @@ -0,0 +1,105 @@ +import * as React from 'react' +import WelcomeTemplate from './WelcomeTemplate' +import DebouncedTextInput from './form/DebouncedTextInput' +import getActionType from '../utils/actionType' +import { ApiAction } from '../store/api' +import { Action } from 'redux' + +import { validateUsername, validatePassword } from '../utils/formValidation' + +export interface SigninProps { + signin: (username: string, password: string) => Promise + setHasSignedIn: () => Action +} + +const Signin: React.FunctionComponent = (props: SigninProps) => { + const { signin, setHasSignedIn } = props + + // track two form values + const [username, setUsername] = React.useState('') + const [password, setPassword] = React.useState('') + + // track error messages to be displayed for the two form values + const [usernameError, setUsernameError] = React.useState() + const [passwordError, setPasswordError] = React.useState() + + // loading state, ready to proceed state + const [loading, setLoading] = React.useState(false) + const [acceptEnabled, setAcceptEnabled] = React.useState(false) + + // when the debounced form values are updated, validate everything + React.useEffect(() => { + const usernameError = validateUsername(username) + setUsernameError(usernameError) + const passwordError = validatePassword(password) + setPasswordError(passwordError) + + // if there are no errors and the values are not empty, enable the proceed button + const acceptEnabled = (usernameError === null && passwordError === null) && + (username !== '' && password !== '') + setAcceptEnabled(acceptEnabled) + }, [username, password]) + + const handleChange = (name: string, value: any) => { + if (name === 'username') setUsername(value) + if (name === 'password') setPassword(value) + } + + const handleSave = async () => { + new Promise(resolve => { + setLoading(true) + resolve() + }) + .then(async () => { + signin(username, password) + .then((action) => { + setLoading(false) + // TODO (ramfox): possibly these should move to the reducer + if (getActionType(action) === 'failure') { + const { errors } = action.payload.data + const { username, password } = errors + + // set server-side errors for each field + username && setUsernameError(username) + password && setPasswordError(password) + } else { + // SUCCESS! + setHasSignedIn() + } + }) + }) + } + + return ( + Sign Up'} + loading={loading} + id='signin-page' + > +
+ + +
+
+ ) +} + +export default Signin diff --git a/app/components/Signup.tsx b/app/components/Signup.tsx index 058fa6cd..4ee69920 100644 --- a/app/components/Signup.tsx +++ b/app/components/Signup.tsx @@ -1,40 +1,11 @@ import * as React from 'react' -import { useDebounce } from 'use-debounce' import WelcomeTemplate from './WelcomeTemplate' -import TextInput from './form/TextInput' +import DebouncedTextInput from './form/DebouncedTextInput' import getActionType from '../utils/actionType' import { ApiAction } from '../store/api' import { Action } from 'redux' -type ValidationError = string | null - -// validators return an error message or null -const validateUsername = (username: string): ValidationError => { - if (username) { - const invalidCharacters = !(/^[a-z0-9_-]+$/.test(username)) - if (invalidCharacters) return 'Usernames may only include a-z, 0-9, _ , and -' - - const tooLong = username.length > 50 - if (tooLong) return 'Username must be 50 characters or less' - } - return null -} - -const validateEmail = (email: string): ValidationError => { - if (email) { - const invalidEmail = !(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) - if (invalidEmail) return 'Enter a valid email address' - } - return null -} - -const validatePassword = (password: string) => { - if (password) { - const tooShort = password && password.length < 8 - if (tooShort) return 'Password must be at least 8 characters' - } - return null -} +import { validateUsername, validateEmail, validatePassword } from '../utils/formValidation' export interface SignupProps { signup: (username: string, email: string, password: string) => Promise @@ -54,11 +25,6 @@ const Signup: React.FunctionComponent = (props: SignupProps) => { const [emailError, setEmailError] = React.useState() const [passwordError, setPasswordError] = React.useState() - // debounce changes to the form values (only validate after debounce) - const [debouncedUsername] = useDebounce(username, 500) - const [debouncedEmail] = useDebounce(email, 500) - const [debouncedPassword] = useDebounce(password, 500) - // loading state, ready to proceed state const [loading, setLoading] = React.useState(false) const [acceptEnabled, setAcceptEnabled] = React.useState(false) @@ -76,7 +42,7 @@ const Signup: React.FunctionComponent = (props: SignupProps) => { const acceptEnabled = (usernameError === null && emailError === null && passwordError === null) && (username !== '' && email !== '' && password !== '') setAcceptEnabled(acceptEnabled) - }, [debouncedUsername, debouncedEmail, debouncedPassword]) + }, [username, email, password]) const handleChange = (name: string, value: any) => { if (name === 'username') setUsername(value) @@ -121,7 +87,7 @@ const Signup: React.FunctionComponent = (props: SignupProps) => { id='signup-page' >
- = (props: SignupProps) => { value={username} errorText={usernameError} onChange={handleChange} /> - = (props: SignupProps) => { value={email} errorText={emailError} onChange={handleChange} /> - = ({ onAcce

{title}

-
{subtitle}
+
{children} diff --git a/app/components/form/DebouncedTextInput.tsx b/app/components/form/DebouncedTextInput.tsx new file mode 100644 index 00000000..b112f3fc --- /dev/null +++ b/app/components/form/DebouncedTextInput.tsx @@ -0,0 +1,30 @@ +import * as React from 'react' +import { useDebounce } from 'use-debounce' +import TextInput, { TextInputProps } from './TextInput' + +interface DebouncedTextInputProps extends TextInputProps { + debounceTimer?: number +} + +const DebouncedTextInput: React.FunctionComponent = ({ debounceTimer = 500, ...props }) => { + const { value, name, onChange } = props + + const [internalValue, setInternalValue] = React.useState(value) + const [debouncedValue] = useDebounce(internalValue, debounceTimer) + React.useEffect(() => { + onChange(name, internalValue) + }, [debouncedValue]) + + // on change, update internalValue + const handleChange = (name: string, value: string) => { setInternalValue(value) } + + return ( + + ) +} + +export default DebouncedTextInput diff --git a/app/components/form/TextInput.tsx b/app/components/form/TextInput.tsx index bd9f5951..6940a26b 100644 --- a/app/components/form/TextInput.tsx +++ b/app/components/form/TextInput.tsx @@ -9,7 +9,7 @@ export interface TextInputProps { errorText?: string helpText?: string showHelpText?: boolean - onChange: (name: string, value: any, e: any) => void + onChange: (name: string, value: any) => void placeHolder?: string white?: boolean } @@ -32,7 +32,7 @@ const TextInput: React.FunctionComponent = ({ label, name, type, className='input' value={value || ''} placeholder={placeHolder} - onChange={(e) => { onChange(name, e.target.value, e) }} + onChange={(e) => { onChange(name, e.target.value) }} />
diff --git a/app/components/modals/AddDataset.tsx b/app/components/modals/AddDataset.tsx index c6d68bf0..28811d5b 100644 --- a/app/components/modals/AddDataset.tsx +++ b/app/components/modals/AddDataset.tsx @@ -10,7 +10,7 @@ import { fetchMyDatasets } from '../../actions/api' interface AddByNameProps { datasetName: string - onChange: (name: string, value: any, e: any) => void + onChange: (name: string, value: any) => void } const AddByName: React.FunctionComponent = ({ datasetName, onChange }) => { @@ -33,7 +33,7 @@ const AddByName: React.FunctionComponent = ({ datasetName, onCha interface AddByUrl { url: string - onChange: (name: string, value: any, e: any) => void + onChange: (name: string, value: any) => void } const AddByUrl: React.FunctionComponent = ({ url, onChange }) => { diff --git a/app/scss/_welcome.scss b/app/scss/_welcome.scss index cba6059c..d6228416 100644 --- a/app/scss/_welcome.scss +++ b/app/scss/_welcome.scss @@ -19,7 +19,8 @@ #app-error { z-index: 200 } #welcome-page { z-index: 199 } #signup-page { z-index: 198 } -#no-datasets-page { z-index: 197 } +#signin-page { z-index: 197 } +#no-datasets-page { z-index: 196 } #spinner-with-icon-wrap { width: 100%; height: 100%; position: absolute; } diff --git a/app/utils/formValidation.ts b/app/utils/formValidation.ts new file mode 100644 index 00000000..ce32ef18 --- /dev/null +++ b/app/utils/formValidation.ts @@ -0,0 +1,29 @@ +type ValidationError = string | null + +// validators return an error message or null +export const validateUsername = (username: string): ValidationError => { + if (username) { + const invalidCharacters = !(/^[a-z0-9_-]+$/.test(username)) + if (invalidCharacters) return 'Usernames may only include a-z, 0-9, _ , and -' + + const tooLong = username.length > 50 + if (tooLong) return 'Username must be 50 characters or less' + } + return null +} + +export const validateEmail = (email: string): ValidationError => { + if (email) { + const invalidEmail = !(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) + if (invalidEmail) return 'Enter a valid email address' + } + return null +} + +export const validatePassword = (password: string) => { + if (password) { + const tooShort = password && password.length < 8 + if (tooShort) return 'Password must be at least 8 characters' + } + return null +}