Skip to content

Commit

Permalink
feat: add signin page and debounced text input
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswhong committed Aug 28, 2019
1 parent 2ae0a95 commit b022d1b
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 56 deletions.
14 changes: 10 additions & 4 deletions app/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiAction>
fetchMyDatasets: (page?: number, pageSize?: number) => Promise<ApiAction>
Expand All @@ -33,7 +33,9 @@ export interface AppProps {
initDataset: (path: string, name: string, format: string) => Promise<ApiAction>
acceptTOS: () => Action
setHasSignedUp: () => Action
setHasSignedIn: () => Action
signup: (username: string, email: string, password: string) => Promise<ApiAction>
signin: (username: string, password: string) => Promise<ApiAction>
closeToast: () => Action
setApiConnection: (status: number) => Action
pingApi: () => Promise<ApiAction>
Expand Down Expand Up @@ -174,13 +176,15 @@ export default class App extends React.Component<AppProps, AppState> {
render () {
const {
hasSignedUp,
hasSignedIn,
hasAcceptedTOS,
peername,
acceptTOS,
signup,
signin,
toast,
closeToast,
setHasSignedUp
setHasSignedUp,
setHasSignedIn
} = this.props
return (<div style={{
height: '100%',
Expand All @@ -191,11 +195,13 @@ export default class App extends React.Component<AppProps, AppState> {
{this.renderAppError()}
{this.renderModal()}
<Onboard
peername={peername}
hasAcceptedTOS={hasAcceptedTOS}
hasSignedUp={hasSignedUp}
hasSignedIn={hasSignedIn}
setHasSignedUp={setHasSignedUp}
setHasSignedIn={setHasSignedIn}
signup={signup}
signin={signin}
acceptTOS={acceptTOS}
/>
{this.renderNoDatasets()}
Expand Down
34 changes: 28 additions & 6 deletions app/components/Onboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApiAction>
signin: (username: string, password: string) => Promise<ApiAction>
}

// Onboard is a series of flows for onboarding a new user
const Onboard: React.FunctionComponent<OnboardProps> = (
{
peername,
hasAcceptedTOS,
hasSignedUp,
hasSignedIn,
acceptTOS,
signup,
setHasSignedUp
setHasSignedUp,
signin,
setHasSignedIn
}) => {
const renderWelcome = () => {
return (
Expand All @@ -40,7 +44,7 @@ const Onboard: React.FunctionComponent<OnboardProps> = (
)
}

const renderChoosePeerName = () => {
const renderSignup = () => {
return (
<CSSTransition
in={!hasSignedUp}
Expand All @@ -57,10 +61,28 @@ const Onboard: React.FunctionComponent<OnboardProps> = (
)
}

const renderSignin = () => {
return (
<CSSTransition
in={!hasSignedIn}
classNames="fade"
component="div"
timeout={1000}
unmountOnExit
>
<Signin
signin={signin}
setHasSignedIn={setHasSignedIn}
/>
</CSSTransition>
)
}

return (
<div>
{renderWelcome()}
{renderChoosePeerName()}
{renderSignup()}
{renderSignin()}
</div>
)
}
Expand Down
105 changes: 105 additions & 0 deletions app/components/Signin.tsx
Original file line number Diff line number Diff line change
@@ -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<ApiAction>
setHasSignedIn: () => Action
}

const Signin: React.FunctionComponent<SigninProps> = (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 (
<WelcomeTemplate
onAccept={handleSave}
acceptEnabled={acceptEnabled}
acceptText='Take me to Qri '
title='Sign in to Qri'
subtitle={'Don\'t have an account? <a href=\'#\'> Sign Up</a>'}
loading={loading}
id='signin-page'
>
<div className='welcome-form'>
<DebouncedTextInput
name= 'username'
label='Username'
type='text'
maxLength={100}
value={username}
errorText={usernameError}
onChange={handleChange} />
<DebouncedTextInput
name= 'password'
label='Password'
type='password'
maxLength={100}
value={password}
errorText={passwordError}
onChange={handleChange} />
</div>
</WelcomeTemplate>
)
}

export default Signin
46 changes: 6 additions & 40 deletions app/components/Signup.tsx
Original file line number Diff line number Diff line change
@@ -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<ApiAction>
Expand All @@ -54,11 +25,6 @@ const Signup: React.FunctionComponent<SignupProps> = (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)
Expand All @@ -76,7 +42,7 @@ const Signup: React.FunctionComponent<SignupProps> = (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)
Expand Down Expand Up @@ -121,23 +87,23 @@ const Signup: React.FunctionComponent<SignupProps> = (props: SignupProps) => {
id='signup-page'
>
<div className='welcome-form'>
<TextInput
<DebouncedTextInput
name= 'username'
label='Username'
type='text'
maxLength={100}
value={username}
errorText={usernameError}
onChange={handleChange} />
<TextInput
<DebouncedTextInput
name= 'email'
label='Email'
type='email'
maxLength={100}
value={email}
errorText={emailError}
onChange={handleChange} />
<TextInput
<DebouncedTextInput
name= 'password'
label='Password'
type='password'
Expand Down
2 changes: 1 addition & 1 deletion app/components/WelcomeTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const WelcomeTemplate: React.FunctionComponent<WelcomeTemplateProps> = ({ onAcce
<img className='welcome-graphic' src={logo} />
<div className='welcome-title'>
<h2>{title}</h2>
<h6>{subtitle}</h6>
<h6 dangerouslySetInnerHTML={{ __html: subtitle }}></h6>
</div>
<div className='welcome-content'>
{children}
Expand Down
30 changes: 30 additions & 0 deletions app/components/form/DebouncedTextInput.tsx
Original file line number Diff line number Diff line change
@@ -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<DebouncedTextInputProps> = ({ 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 (
<TextInput
{...props}
value={internalValue}
onChange={handleChange}
/>
)
}

export default DebouncedTextInput
4 changes: 2 additions & 2 deletions app/components/form/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -32,7 +32,7 @@ const TextInput: React.FunctionComponent<TextInputProps> = ({ label, name, type,
className='input'
value={value || ''}
placeholder={placeHolder}
onChange={(e) => { onChange(name, e.target.value, e) }}
onChange={(e) => { onChange(name, e.target.value) }}
/>
</div>
<div style={{ height: 20 }}>
Expand Down
Loading

0 comments on commit b022d1b

Please sign in to comment.