-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #135 from brewbean/feat/reset-password
Implement Reset form flow
- Loading branch information
Showing
4 changed files
with
281 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |