Skip to content

Commit

Permalink
Merge pull request #135 from brewbean/feat/reset-password
Browse files Browse the repository at this point in the history
Implement Reset form flow
  • Loading branch information
pattruong authored Feb 4, 2021
2 parents c56e823 + 9667f64 commit 3b7ca2c
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 1 deletion.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
src/tailwind.generated.css
src/constants/index.js
# Local Netlify folder
.netlify
13 changes: 13 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import CreateAccount from 'pages/CreateAccount'
import Activate from 'pages/Activate'
import Profile from 'pages/Profile'
import ModalFlowDemo from 'pages/ModalFlowDemo'
import Reset from 'pages/Reset'

const Test = () => {
return <div className='bg-gray-200'>Test page</div>
Expand Down Expand Up @@ -53,6 +54,18 @@ function App() {
>
<Activate />
</ContainerRoute>
<ContainerRoute
path='/reset'
defaultLayout={false}
config={{
flexCol: true,
layout: true,
paddedContent: true,
layoutClass: 'flex',
}}
>
<Reset />
</ContainerRoute>
<AuthRoute path='/profile'>
<Profile />
</AuthRoute>
Expand Down
152 changes: 152 additions & 0 deletions src/pages/Reset/Form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import axios from 'axios'
import { useState } from 'react'
import { validatePassword, passwordRequirements } from 'helper/form'
import FormAlert from 'components/FormAlert'
import { ButtonLoading } from 'components/Utility'
import { AUTH_API } from 'config'

const showAlerts = ({ type, text }) => (
<FormAlert key={text} type={type} text={text} />
)

function Form({ code, email, onSuccess, onFail }) {
const [isLoading, setIsLoading] = useState(false)
const [passwordAlerts, setPasswordAlerts] = useState(passwordRequirements)
const [errorMsg, setErrorMsg] = useState(null)
const [state, setState] = useState({
password: '',
confirmPassword: '',
})

const submitPassword = async (e) => {
e.preventDefault()
try {
if (state.password === state.confirmPassword) {
// trigger loading animation
setIsLoading(true)

await axios.put(AUTH_API + '/password/reset', {
code,
email,
password: state.password,
})

setIsLoading(false)
setState({ password: '', confirmPassword: '' })
onSuccess()
} else {
setErrorMsg('Passwords do not match')
}
} catch ({ response }) {
setIsLoading(false)
onFail(response)
}
}

const onChange = ({ target }) => {
if (errorMsg) {
setErrorMsg(null)
}

if (target.name === 'password') {
setPasswordAlerts(validatePassword(target.value))
}

setState({
...state,
[target.name]: target.value,
})
}

const alerts = Object.values(passwordAlerts).filter(
({ isActive }) => isActive
)

const inputStyle = {
default: 'border-gray-300 focus:ring-blue-700 focus:border-blue-700',
error: 'border-red-300 focus:ring-red-700 focus:border-red-700',
}

const isSubmitDisabled =
state.password === '' ||
state.confirmPassword === '' ||
isLoading ||
errorMsg ||
alerts.length > 0

return (
<form onSubmit={submitPassword} className='space-y-6 sm:space-y-5 mb-0'>
<h3 className='text-lg text-center font-medium text-gray-900'>
Enter a new password
</h3>

<div className='space-y-3'>
<div>
<label
htmlFor='password'
className={`block text-sm font-medium ${
errorMsg ? 'text-red-700' : 'text-gray-900'
}`}
>
New password
</label>
<input
type='password'
id='password'
name='password'
value={state.password}
disabled={isLoading}
onChange={onChange}
className={`mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none sm:text-sm ${
errorMsg ? inputStyle.error : inputStyle.default
}`}
/>
</div>
<div>
<label
htmlFor='confirmPassword'
className={`block text-sm font-medium ${
errorMsg ? 'text-red-700' : 'text-gray-900'
}`}
>
Confirm password
</label>
<input
type='password'
id='confirmPassword'
name='confirmPassword'
value={state.confirmPassword}
disabled={isLoading}
onChange={onChange}
className={`mt-1 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none sm:text-sm ${
errorMsg ? inputStyle.error : inputStyle.default
}`}
/>
</div>
{errorMsg && <p className='text-sm italic text-red-600'>{errorMsg}</p>}
{alerts.length > 0 && (
<div className='space-y-1'>{alerts.map(showAlerts)}</div>
)}
</div>
<div className='flex justify-center'>
<button
type='submit'
disabled={isSubmitDisabled}
className={`inline-flex items-center justify-center w-full py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white focus:outline-none focus:ring-2 focus:ring-offset-2 bg-blue-600 focus:ring-blue-500 disabled:opacity-50 ${
isSubmitDisabled ? 'cursor-not-allowed' : 'hover:bg-blue-700'
}`.trimEnd()}
>
{isLoading ? (
<>
<ButtonLoading />
Processing
</>
) : (
'Submit'
)}
</button>
</div>
</form>
)
}
export default Form
116 changes: 116 additions & 0 deletions src/pages/Reset/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import qs from 'qs'
import { useState } from 'react'
import { useLocation, Link } from 'react-router-dom'
import Form from './Form'

function Reset() {
const { search } = useLocation()
const { code, email } = qs.parse(search, { ignoreQueryPrefix: true })

const [showForm, setShowForm] = useState(code && email ? true : false)
const [isSuccess, setIsSuccess] = useState(false)
const [title, setTitle] = useState(code && email ? null : 'Bad link!')
const [subtext, setSubtext] = useState(
code && email ? null : 'Please check your email again.'
)

const onSuccess = () => {
setShowForm(false)
setIsSuccess(true)
setTitle('Password successfully changed!')
setSubtext('You may now log in with your new credentials.')
}
const onFail = (response) => {
setShowForm(false)
setIsSuccess(false)
setTitle('Error resetting password!')
setSubtext(response.data?.message)
}

return (
<div className='flex-1 flex items-center justify-center'>
<div className='absolute top-0 mt-10'>
<Link
to='/'
className={`text-2xl font-extrabold tracking-widest text-blue-500 ${
showForm ? 'pointer-events-none' : ''
}`.trimEnd()}
>
brew<span className='text-pink-400'>(</span>bean
<span className='text-pink-400'>)</span>
</Link>
</div>
<div className='bg-white shadow rounded sm:rounded-lg w-full sm:w-1/3'>
<div className='px-4 py-5 sm:p-8'>
{showForm ? (
<Form
onSuccess={onSuccess}
onFail={onFail}
code={code}
email={email}
/>
) : (
<>
<div className='pb-5'>
{isSuccess ? (
<div className='mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100'>
<svg
className='w-6 h-6 text-green-600'
fill='currentColor'
viewBox='0 0 20 20'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
d='M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z'
clipRule='evenodd'
/>
</svg>
</div>
) : (
<div className='mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100'>
<svg
className='w-6 h-6 text-red-600'
fill='currentColor'
viewBox='0 0 20 20'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
d='M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z'
clipRule='evenodd'
/>
</svg>
</div>
)}
</div>

{title && (
<h3 className='text-lg text-center font-medium text-gray-900'>
{title}
</h3>
)}
{subtext && (
<div className='mt-2 max-w-xl text-sm text-gray-500 text-center'>
<p>{subtext}</p>
</div>
)}

{isSuccess && (
<div className='mt-5 flex justify-center'>
<Link
to='/'
className='inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm'
>
Go to home page
</Link>
</div>
)}
</>
)}
</div>
</div>
</div>
)
}
export default Reset

0 comments on commit 3b7ca2c

Please sign in to comment.