rewrite "auth.forgot" page with React
This commit is contained in:
parent
1b8c335cac
commit
2229fcf66b
28
resources/assets/src/components/Alert.tsx
Normal file
28
resources/assets/src/components/Alert.tsx
Normal 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
|
||||
|
|
@ -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+)',
|
||||
|
|
|
|||
80
resources/assets/src/views/auth/Forgot.tsx
Normal file
80
resources/assets/src/views/auth/Forgot.tsx
Normal 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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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')
|
||||
})
|
||||
51
resources/assets/tests/views/auth/Forgot.test.tsx
Normal file
51
resources/assets/tests/views/auth/Forgot.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user