rewrite "auth.forgot" page with React

This commit is contained in:
Pig Fang 2020-03-26 18:15:13 +08:00
parent 1b8c335cac
commit 2229fcf66b
7 changed files with 162 additions and 134 deletions

View File

@ -0,0 +1,28 @@
import React from 'react'
type AlertType = 'success' | 'info' | 'warning' | 'danger'
const icons = new Map<AlertType, string>([
['success', 'check'],
['info', 'info'],
['warning', 'exclamation-triangle'],
['danger', 'times-circle'],
])
interface Props {
type: AlertType
}
const Alert: React.FC<Props> = (props) => {
const { type } = props
const icon = icons.get(type)
return props.children ? (
<div className={`alert alert-${type}`}>
<i className={`icon fas fa-${icon}`}></i>
{props.children}
</div>
) : null
}
export default Alert

View File

@ -101,8 +101,8 @@ export default [
},
{
path: 'auth/forgot',
component: () => import('../views/auth/Forgot.vue'),
el: 'form',
react: () => import('../views/auth/Forgot'),
el: 'main',
},
{
path: 'auth/reset/(\\d+)',

View File

@ -0,0 +1,80 @@
import React, { useState, useRef } from 'react'
import { hot } from 'react-hot-loader/root'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import Alert from '@/components/Alert'
import Captcha from '@/components/Captcha'
const Forgot: React.FC = () => {
const [email, setEmail] = useState('')
const [isSending, setIsSending] = useState(false)
const [successMessage, setSuccessMessage] = useState('')
const [warningMessage, setWarningMessage] = useState('')
const ref = useRef<Captcha | null>(null)
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value)
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setWarningMessage('')
setIsSending(true)
const captcha = await ref.current!.execute()
const { code, message } = await fetch.post<fetch.ResponseBody>(
'/auth/forgot',
{ email, captcha },
)
if (code === 0) {
setSuccessMessage(message)
} else {
setWarningMessage(message)
ref.current!.reset()
}
setIsSending(false)
}
return (
<form onSubmit={handleSubmit}>
<div className="input-group mb-3">
<input
type="email"
className="form-control"
placeholder={t('auth.email')}
required
value={email}
onChange={handleEmailChange}
/>
<div className="input-group-append">
<div className="input-group-text">
<i className="fas fa-envelope"></i>
</div>
</div>
</div>
<Captcha ref={ref} />
<Alert type="success">{successMessage}</Alert>
<Alert type="warning">{warningMessage}</Alert>
<div className="d-flex justify-content-between align-items-center">
<a href={`${blessing.base_url}/auth/login`}>
{t('auth.forgot.login-link')}
</a>
<button className="btn btn-primary" type="submit" disabled={isSending}>
{isSending ? (
<>
<i className="fas fa-spinner fa-spin mr-1" />
{t('auth.sending')}
</>
) : (
t('auth.forgot.button')
)}
</button>
</div>
</form>
)
}
export default hot(Forgot)

View File

@ -1,90 +0,0 @@
<template>
<form @submit.prevent="submit">
<div class="input-group mb-3">
<input
v-model="email"
type="email"
class="form-control"
:placeholder="$t('auth.email')"
required
>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-envelope" />
</div>
</div>
</div>
<captcha ref="captcha" />
<div class="alert alert-success" :class="{ 'd-none': !successMsg }">
<i class="icon fas fa-check" />
{{ successMsg }}
</div>
<div class="alert alert-warning" :class="{ 'd-none': !warningMsg }">
<i class="icon fas fa-exclamation-triangle" />
{{ warningMsg }}
</div>
<div class="d-flex justify-content-between">
<a v-t="'auth.forgot.login-link'" :href="`${baseUrl}/auth/login`" class="text-center" />
<button
class="btn btn-primary"
type="submit"
:disabled="pending"
>
<template v-if="pending">
<i class="fa fa-spinner fa-spin" /> {{ $t('auth.sending') }}
</template>
<span v-else>{{ $t('auth.forgot.button') }}</span>
</button>
</div>
</form>
</template>
<script>
import Captcha from '../../components/Captcha.vue'
import emitMounted from '../../components/mixins/emitMounted'
export default {
name: 'Forgot',
components: {
Captcha,
},
mixins: [
emitMounted,
],
props: {
baseUrl: {
type: String,
default: blessing.base_url,
},
},
data: () => ({
email: '',
successMsg: '',
warningMsg: '',
pending: false,
}),
methods: {
async submit() {
const { email } = this
this.pending = true
const { code, message } = await this.$http.post(
'/auth/forgot',
{ email, captcha: await this.$refs.captcha.execute() },
)
if (code === 0) {
this.warningMsg = ''
this.successMsg = message
this.pending = false
} else {
this.warningMsg = message
this.pending = false
this.$refs.captcha.refresh()
}
},
},
}
</script>

View File

@ -1,38 +0,0 @@
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import Forgot from '@/views/auth/Forgot.vue'
import { flushPromises } from '../../utils'
window.blessing.extra = {}
const Captcha = Vue.extend({
methods: {
execute() {
return Promise.resolve('captcha')
},
refresh() { /* */ },
},
})
test('submit forgot form', async () => {
jest.spyOn(Date, 'now')
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: 'fail' })
.mockResolvedValueOnce({ code: 0, message: 'ok' })
const wrapper = mount(Forgot, { stubs: { Captcha } })
const form = wrapper.find('form')
const warning = wrapper.find('.alert-warning')
const success = wrapper.find('.alert-success')
wrapper.find('[type="email"]').setValue('a@b.c')
form.trigger('submit')
await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith(
'/auth/forgot',
{ email: 'a@b.c', captcha: 'captcha' },
)
expect(warning.text()).toBe('fail')
form.trigger('submit')
await flushPromises()
expect(success.text()).toBe('ok')
})

View File

@ -0,0 +1,51 @@
import React from 'react'
import { render, wait, fireEvent } from '@testing-library/react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import Forgot from '@/views/auth/Forgot'
jest.mock('@/scripts/net')
describe('submit', () => {
it('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByPlaceholderText, getByText, queryByText } = render(<Forgot />)
fireEvent.input(getByPlaceholderText(t('auth.email')), {
target: { value: 'a@b.c' },
})
fireEvent.input(getByPlaceholderText(t('auth.captcha')), {
target: { value: 'abc' },
})
fireEvent.click(getByText(t('auth.forgot.button')))
await wait()
expect(fetch.post).toBeCalledWith('/auth/forgot', {
email: 'a@b.c',
captcha: 'abc',
})
expect(queryByText('ok')).toBeInTheDocument()
})
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByPlaceholderText, getByText, queryByText } = render(<Forgot />)
fireEvent.input(getByPlaceholderText(t('auth.email')), {
target: { value: 'a@b.c' },
})
fireEvent.input(getByPlaceholderText(t('auth.captcha')), {
target: { value: 'abc' },
})
fireEvent.click(getByText(t('auth.forgot.button')))
await wait()
expect(fetch.post).toBeCalledWith('/auth/forgot', {
email: 'a@b.c',
captcha: 'abc',
})
expect(queryByText('failed')).toBeInTheDocument()
})
})

View File

@ -9,13 +9,10 @@
{{ session_pull('msg') }}
</div>
{% endif %}
<form></form>
<main></main>
{% endblock %}
{% block before_foot %}
{% if enable_recaptcha %}
<script src="{{ recaptcha_url }}" async defer></script>
{% endif %}
<script>
Object.defineProperty(blessing, 'extra', {
configurable: false,