Merge pull request 'feat: improve login and register ux' (#1104) from feature/login-improvements into main
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1104 Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
commit
8058790f9a
13 changed files with 255 additions and 119 deletions
|
@ -25,7 +25,6 @@ context('Registration', () => {
|
||||||
cy.get('#username').type(fixture.username)
|
cy.get('#username').type(fixture.username)
|
||||||
cy.get('#email').type(fixture.email)
|
cy.get('#email').type(fixture.email)
|
||||||
cy.get('#password').type(fixture.password)
|
cy.get('#password').type(fixture.password)
|
||||||
cy.get('#passwordValidation').type(fixture.password)
|
|
||||||
cy.get('#register-submit').click()
|
cy.get('#register-submit').click()
|
||||||
cy.url().should('include', '/')
|
cy.url().should('include', '/')
|
||||||
cy.clock(1625656161057) // 13:00
|
cy.clock(1625656161057) // 13:00
|
||||||
|
@ -43,7 +42,6 @@ context('Registration', () => {
|
||||||
cy.get('#username').type(fixture.username)
|
cy.get('#username').type(fixture.username)
|
||||||
cy.get('#email').type(fixture.email)
|
cy.get('#email').type(fixture.email)
|
||||||
cy.get('#password').type(fixture.password)
|
cy.get('#password').type(fixture.password)
|
||||||
cy.get('#passwordValidation').type(fixture.password)
|
|
||||||
cy.get('#register-submit').click()
|
cy.get('#register-submit').click()
|
||||||
cy.get('div.message.danger').contains('A user with this username already exists.')
|
cy.get('div.message.danger').contains('A user with this username already exists.')
|
||||||
})
|
})
|
||||||
|
|
85
src/components/input/password.vue
Normal file
85
src/components/input/password.vue
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<template>
|
||||||
|
<div class="password-field">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
:placeholder="$t('user.auth.passwordPlaceholder')"
|
||||||
|
required
|
||||||
|
:type="passwordFieldType"
|
||||||
|
autocomplete="current-password"
|
||||||
|
@keyup.enter="e => $emit('submit', e)"
|
||||||
|
:tabindex="props.tabindex"
|
||||||
|
@focusout="validate"
|
||||||
|
@input="handleInput"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
@click="togglePasswordFieldType"
|
||||||
|
class="password-field-type-toggle"
|
||||||
|
aria-label="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
|
||||||
|
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')">
|
||||||
|
<icon :icon="passwordFieldType === 'password' ? 'eye' : 'eye-slash'"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" v-if="!isValid">
|
||||||
|
{{ $t('user.auth.passwordRequired') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref, watch} from 'vue'
|
||||||
|
import {useDebounceFn} from '@vueuse/core'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tabindex: String,
|
||||||
|
modelValue: String,
|
||||||
|
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
|
||||||
|
validateInitially: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['submit', 'update:modelValue'])
|
||||||
|
|
||||||
|
const passwordFieldType = ref<String>('password')
|
||||||
|
const password = ref<String>('')
|
||||||
|
const isValid = ref<Boolean>(!props.validateInitially)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.validateInitially,
|
||||||
|
(doValidate: Boolean) => {
|
||||||
|
if (doValidate) {
|
||||||
|
validate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
useDebounceFn(() => {
|
||||||
|
isValid.value = password.value !== ''
|
||||||
|
}, 100)()
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePasswordFieldType() {
|
||||||
|
passwordFieldType.value = passwordFieldType.value === 'password'
|
||||||
|
? 'text'
|
||||||
|
: 'password'
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(e) {
|
||||||
|
password.value = e.target.value
|
||||||
|
emit('update:modelValue', e.target.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.password-field {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-field-type-toggle {
|
||||||
|
position: absolute;
|
||||||
|
color: var(--grey-400);
|
||||||
|
top: 50%;
|
||||||
|
right: 1rem;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,18 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="message-wrapper">
|
<div class="message-wrapper">
|
||||||
<div class="message" :class="variant">
|
<div class="message" :class="[variant, textAlignClass]">
|
||||||
<slot/>
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
defineProps({
|
import {computed, PropType} from 'vue'
|
||||||
|
|
||||||
|
const TEXT_ALIGN_MAP = Object.freeze({
|
||||||
|
left: '',
|
||||||
|
center: 'has-text-centered',
|
||||||
|
right: 'has-text-right',
|
||||||
|
})
|
||||||
|
|
||||||
|
type textAlignVariants = keyof typeof TEXT_ALIGN_MAP
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'info',
|
default: 'info',
|
||||||
},
|
},
|
||||||
|
textAlign: {
|
||||||
|
type: String as PropType<textAlignVariants>,
|
||||||
|
default: 'left',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const textAlignClass = computed(() => TEXT_ALIGN_MAP[props.textAlign])
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
<div>
|
<div>
|
||||||
<h2 class="title" v-if="title">{{ title }}</h2>
|
<h2 class="title" v-if="title">{{ title }}</h2>
|
||||||
<api-config/>
|
<api-config/>
|
||||||
|
<Message v-if="motd !== ''" class="is-hidden-tablet mb-4">
|
||||||
|
{{ motd }}
|
||||||
|
</Message>
|
||||||
<slot/>
|
<slot/>
|
||||||
</div>
|
</div>
|
||||||
<legal/>
|
<legal/>
|
||||||
|
@ -38,8 +41,8 @@ const store = useStore()
|
||||||
const {t} = useI18n()
|
const {t} = useI18n()
|
||||||
|
|
||||||
const motd = computed(() => store.state.config.motd)
|
const motd = computed(() => store.state.config.motd)
|
||||||
// @ts-ignore
|
|
||||||
const title = computed(() => t(route.meta.title ?? ''))
|
const title = computed(() => t(route.meta?.title as string || ''))
|
||||||
useTitle(() => title.value)
|
useTitle(() => title.value)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
6
src/helpers/isEmail.ts
Normal file
6
src/helpers/isEmail.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export function isEmail(email: string): Boolean {
|
||||||
|
const format = /^.+@.+$/
|
||||||
|
const match = email.match(format)
|
||||||
|
|
||||||
|
return match === null ? false : match.length > 0
|
||||||
|
}
|
|
@ -31,10 +31,9 @@
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"usernameEmail": "Username Or Email Address",
|
"usernameEmail": "Username Or Email Address",
|
||||||
"usernamePlaceholder": "e.g. frederick",
|
"usernamePlaceholder": "e.g. frederick",
|
||||||
"email": "E-mail address",
|
"email": "Email address",
|
||||||
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
|
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"passwordRepeat": "Retype your password",
|
|
||||||
"passwordPlaceholder": "e.g. •••••••••••",
|
"passwordPlaceholder": "e.g. •••••••••••",
|
||||||
"forgotPassword": "Forgot your password?",
|
"forgotPassword": "Forgot your password?",
|
||||||
"resetPassword": "Reset your password",
|
"resetPassword": "Reset your password",
|
||||||
|
@ -45,12 +44,19 @@
|
||||||
"totpTitle": "Two Factor Authentication Code",
|
"totpTitle": "Two Factor Authentication Code",
|
||||||
"totpPlaceholder": "e.g. 123456",
|
"totpPlaceholder": "e.g. 123456",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"register": "Register",
|
"createAccount": "Create account",
|
||||||
"loginWith": "Log in with {provider}",
|
"loginWith": "Log in with {provider}",
|
||||||
"authenticating": "Authenticating…",
|
"authenticating": "Authenticating…",
|
||||||
"openIdStateError": "State does not match, refusing to continue!",
|
"openIdStateError": "State does not match, refusing to continue!",
|
||||||
"openIdGeneralError": "An error occured while authenticating against the third party.",
|
"openIdGeneralError": "An error occured while authenticating against the third party.",
|
||||||
"logout": "Logout"
|
"logout": "Logout",
|
||||||
|
"emailInvalid": "Please enter a valid email address.",
|
||||||
|
"usernameRequired": "Please provide a username.",
|
||||||
|
"passwordRequired": "Please provide a password.",
|
||||||
|
"showPassword": "Show the password",
|
||||||
|
"hidePassword": "Hide the password",
|
||||||
|
"noAccountYet": "Don't have an account yet?",
|
||||||
|
"alreadyHaveAnAccount": "Already have an account?"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
|
@ -61,7 +67,7 @@
|
||||||
"currentPasswordPlaceholder": "Your current password",
|
"currentPasswordPlaceholder": "Your current password",
|
||||||
"passwordsDontMatch": "The new password and its confirmation don't match.",
|
"passwordsDontMatch": "The new password and its confirmation don't match.",
|
||||||
"passwordUpdateSuccess": "The password was successfully updated.",
|
"passwordUpdateSuccess": "The password was successfully updated.",
|
||||||
"updateEmailTitle": "Update Your E-Mail Address",
|
"updateEmailTitle": "Update Your Email Address",
|
||||||
"updateEmailNew": "New Email Address",
|
"updateEmailNew": "New Email Address",
|
||||||
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
|
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
|
||||||
"general": {
|
"general": {
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {
|
||||||
faCocktail,
|
faCocktail,
|
||||||
faCoffee,
|
faCoffee,
|
||||||
faCog,
|
faCog,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
faExclamation,
|
faExclamation,
|
||||||
|
@ -87,6 +89,8 @@ library.add(faCocktail)
|
||||||
library.add(faCoffee)
|
library.add(faCoffee)
|
||||||
library.add(faCog)
|
library.add(faCog)
|
||||||
library.add(faComments)
|
library.add(faComments)
|
||||||
|
library.add(faEye)
|
||||||
|
library.add(faEyeSlash)
|
||||||
library.add(faEllipsisH)
|
library.add(faEllipsisH)
|
||||||
library.add(faEllipsisV)
|
library.add(faEllipsisV)
|
||||||
library.add(faExclamation)
|
library.add(faExclamation)
|
||||||
|
|
|
@ -133,7 +133,7 @@ const router = createRouter({
|
||||||
name: 'user.register',
|
name: 'user.register',
|
||||||
component: RegisterComponent,
|
component: RegisterComponent,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'user.auth.register',
|
title: 'user.auth.createAccount',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess">
|
<message variant="success" text-align="center" class="mb-4" v-if="confirmedEmailSuccess">
|
||||||
{{ $t('user.auth.confirmEmailSuccess') }}
|
{{ $t('user.auth.confirmEmailSuccess') }}
|
||||||
</message>
|
</message>
|
||||||
<message variant="danger" v-if="errorMessage">
|
<message variant="danger" v-if="errorMessage" class="mb-4">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</message>
|
</message>
|
||||||
<form @submit.prevent="submit" id="loginform" v-if="localAuthEnabled">
|
<form @submit.prevent="submit" id="loginform" v-if="localAuthEnabled">
|
||||||
|
@ -20,24 +20,26 @@
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
v-focus
|
v-focus
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
|
tabindex="1"
|
||||||
|
@focusout="validateField('username')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help is-danger" v-if="!usernameValid">
|
||||||
|
{{ $t('user.auth.usernameRequired') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
<div class="label-with-link">
|
||||||
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
||||||
<div class="control">
|
<router-link
|
||||||
<input
|
:to="{ name: 'user.password-reset.request' }"
|
||||||
class="input"
|
class="reset-password-link"
|
||||||
id="password"
|
tabindex="6"
|
||||||
name="password"
|
>
|
||||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
{{ $t('user.auth.forgotPassword') }}
|
||||||
ref="password"
|
</router-link>
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
@keyup.enter="submit"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<password tabindex="2" @submit="submit" v-model="password" :validate-initially="validatePasswordInitially"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" v-if="needsTotpPasscode">
|
<div class="field" v-if="needsTotpPasscode">
|
||||||
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
|
<label class="label" for="totpPasscode">{{ $t('user.auth.totpTitle') }}</label>
|
||||||
|
@ -52,32 +54,28 @@
|
||||||
type="text"
|
type="text"
|
||||||
v-focus
|
v-focus
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
|
tabindex="3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field is-grouped login-buttons">
|
|
||||||
<div class="control is-expanded">
|
|
||||||
<x-button
|
<x-button
|
||||||
@click="submit"
|
@click="submit"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
tabindex="4"
|
||||||
>
|
>
|
||||||
{{ $t('user.auth.login') }}
|
{{ $t('user.auth.login') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button
|
<p class="mt-2" v-if="registrationEnabled">
|
||||||
|
{{ $t('user.auth.noAccountYet') }}
|
||||||
|
<router-link
|
||||||
:to="{ name: 'user.register' }"
|
:to="{ name: 'user.register' }"
|
||||||
v-if="registrationEnabled"
|
type="secondary"
|
||||||
variant="secondary"
|
tabindex="5"
|
||||||
>
|
>
|
||||||
{{ $t('user.auth.register') }}
|
{{ $t('user.auth.createAccount') }}
|
||||||
</x-button>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<router-link :to="{ name: 'user.password-reset.request' }" class="reset-password-link">
|
|
||||||
{{ $t('user.auth.forgotPassword') }}
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -97,6 +95,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import {useDebounceFn} from '@vueuse/core'
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
import {HTTPFactory} from '@/http-common'
|
import {HTTPFactory} from '@/http-common'
|
||||||
|
@ -105,15 +104,20 @@ import {getErrorText} from '@/message'
|
||||||
import Message from '@/components/misc/message'
|
import Message from '@/components/misc/message'
|
||||||
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
import {redirectToProvider} from '../../helpers/redirectToProvider'
|
||||||
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
|
import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
|
||||||
|
import Password from '@/components/input/password'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
Password,
|
||||||
Message,
|
Message,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
confirmedEmailSuccess: false,
|
confirmedEmailSuccess: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
|
usernameValid: true,
|
||||||
|
password: '',
|
||||||
|
validatePasswordInitially: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
|
@ -166,6 +170,13 @@ export default {
|
||||||
localAuthEnabled: state => state.config.auth.local.enabled,
|
localAuthEnabled: state => state.config.auth.local.enabled,
|
||||||
openidConnect: state => state.config.auth.openidConnect,
|
openidConnect: state => state.config.auth.openidConnect,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
validateField() {
|
||||||
|
// using computed so that debounced function definition stays
|
||||||
|
return useDebounceFn((field) => {
|
||||||
|
this[`${field}Valid`] = this.$refs[field].value !== ''
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setLoading() {
|
setLoading() {
|
||||||
|
@ -185,7 +196,14 @@ export default {
|
||||||
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
// For more info, see https://kolaente.dev/vikunja/frontend/issues/78
|
||||||
const credentials = {
|
const credentials = {
|
||||||
username: this.$refs.username.value,
|
username: this.$refs.username.value,
|
||||||
password: this.$refs.password.value,
|
password: this.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentials.username === '' || credentials.password === '') {
|
||||||
|
// Trigger the validation error messages
|
||||||
|
this.validateField('username')
|
||||||
|
this.validatePasswordInitially = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.needsTotpPasscode) {
|
if (this.needsTotpPasscode) {
|
||||||
|
@ -196,7 +214,7 @@ export default {
|
||||||
await this.$store.dispatch('auth/login', credentials)
|
await this.$store.dispatch('auth/login', credentials)
|
||||||
this.$store.commit('auth/needsTotpPasscode', false)
|
this.$store.commit('auth/needsTotpPasscode', false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response && e.response.data.code === 1017 && !credentials.totpPasscode) {
|
if (e.response?.data.code === 1017 && !this.credentials.totpPasscode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,22 +229,21 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.login-buttons {
|
|
||||||
@media screen and (max-width: 450px) {
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.control:first-child {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin: 0 0.4rem 0 0;
|
margin: 0 0.4rem 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-password-link {
|
.reset-password-link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-top: 5px;
|
}
|
||||||
|
|
||||||
|
.label-with-link {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<message v-if="errorMsg">
|
<message v-if="errorMsg" class="mb-4">
|
||||||
{{ errorMsg }}
|
{{ errorMsg }}
|
||||||
</message>
|
</message>
|
||||||
<div class="has-text-centered" v-if="successMessage">
|
<div class="has-text-centered mb-4" v-if="successMessage">
|
||||||
<message variant="success">
|
<message variant="success">
|
||||||
{{ successMessage }}
|
{{ successMessage }}
|
||||||
</message>
|
</message>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<message variant="danger" v-if="errorMessage !== ''">
|
<message variant="danger" v-if="errorMessage !== ''" class="mb-4">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</message>
|
</message>
|
||||||
<form @submit.prevent="submit" id="registerform">
|
<form @submit.prevent="submit" id="registerform">
|
||||||
|
@ -18,8 +18,12 @@
|
||||||
v-focus
|
v-focus
|
||||||
v-model="credentials.username"
|
v-model="credentials.username"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
|
@focusout="validateUsername"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help is-danger" v-if="!usernameValid">
|
||||||
|
{{ $t('user.auth.usernameRequired') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="email">{{ $t('user.auth.email') }}</label>
|
<label class="label" for="email">{{ $t('user.auth.email') }}</label>
|
||||||
|
@ -33,68 +37,46 @@
|
||||||
type="email"
|
type="email"
|
||||||
v-model="credentials.email"
|
v-model="credentials.email"
|
||||||
@keyup.enter="submit"
|
@keyup.enter="submit"
|
||||||
|
@focusout="validateEmail"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="help is-danger" v-if="!emailValid">
|
||||||
|
{{ $t('user.auth.emailInvalid') }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
<label class="label" for="password">{{ $t('user.auth.password') }}</label>
|
||||||
<div class="control">
|
<password @submit="submit" @update:modelValue="v => credentials.password = v" :validate-initially="validatePasswordInitially"/>
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
v-model="credentials.password"
|
|
||||||
@keyup.enter="submit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label class="label" for="passwordValidation">{{ $t('user.auth.passwordRepeat') }}</label>
|
|
||||||
<div class="control">
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
id="passwordValidation"
|
|
||||||
name="passwordValidation"
|
|
||||||
:placeholder="$t('user.auth.passwordPlaceholder')"
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
v-model="passwordValidation"
|
|
||||||
@keyup.enter="submit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field is-grouped">
|
|
||||||
<div class="control">
|
|
||||||
<x-button
|
<x-button
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
id="register-submit"
|
id="register-submit"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
|
:disabled="!everythingValid"
|
||||||
>
|
>
|
||||||
{{ $t('user.auth.register') }}
|
{{ $t('user.auth.createAccount') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button :to="{ name: 'user.login' }" variant="secondary">
|
<p class="mt-2">
|
||||||
|
{{ $t('user.auth.alreadyHaveAnAccount') }}
|
||||||
|
<router-link :to="{ name: 'user.login' }">
|
||||||
{{ $t('user.auth.login') }}
|
{{ $t('user.auth.login') }}
|
||||||
</x-button>
|
</router-link>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {useDebounceFn} from '@vueuse/core'
|
||||||
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
import {ref, reactive, toRaw, computed, onBeforeMount} from 'vue'
|
||||||
import {useI18n} from 'vue-i18n'
|
|
||||||
|
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import {store} from '@/store'
|
import {store} from '@/store'
|
||||||
import Message from '@/components/misc/message'
|
import Message from '@/components/misc/message'
|
||||||
|
import {isEmail} from '@/helpers/isEmail'
|
||||||
|
import Password from '@/components/input/password'
|
||||||
|
|
||||||
// FIXME: use the `beforeEnter` hook of vue-router
|
// FIXME: use the `beforeEnter` hook of vue-router
|
||||||
// Check if the user is already logged in, if so, redirect them to the homepage
|
// Check if the user is already logged in, if so, redirect them to the homepage
|
||||||
|
@ -104,27 +86,45 @@ onBeforeMount(() => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const {t} = useI18n()
|
|
||||||
|
|
||||||
const credentials = reactive({
|
const credentials = reactive({
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
})
|
})
|
||||||
const passwordValidation = ref('')
|
|
||||||
|
|
||||||
const loading = computed(() => store.state.loading)
|
const loading = computed(() => store.state.loading)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
const validatePasswordInitially = ref(false)
|
||||||
|
|
||||||
|
const DEBOUNCE_TIME = 100
|
||||||
|
|
||||||
|
// debouncing to prevent error messages when clicking on the log in button
|
||||||
|
const emailValid = ref(true)
|
||||||
|
const validateEmail = useDebounceFn(() => {
|
||||||
|
emailValid.value = isEmail(credentials.email)
|
||||||
|
}, DEBOUNCE_TIME)
|
||||||
|
|
||||||
|
const usernameValid = ref(true)
|
||||||
|
const validateUsername = useDebounceFn(() => {
|
||||||
|
usernameValid.value = credentials.username !== ''
|
||||||
|
}, DEBOUNCE_TIME)
|
||||||
|
|
||||||
|
const everythingValid = computed(() => {
|
||||||
|
return credentials.username !== '' &&
|
||||||
|
credentials.email !== '' &&
|
||||||
|
credentials.password !== '' &&
|
||||||
|
emailValid.value &&
|
||||||
|
usernameValid.value
|
||||||
|
})
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
validatePasswordInitially.value = true
|
||||||
|
|
||||||
if (credentials.password !== passwordValidation.value) {
|
if (!everythingValid.value) {
|
||||||
errorMessage.value = t('user.auth.passwordsDontMatch')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await store.dispatch('auth/register', toRaw(credentials))
|
await store.dispatch('auth/register', toRaw(credentials))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<message variant="danger" v-if="errorMsg">
|
<message variant="danger" v-if="errorMsg" class="mb-4">
|
||||||
{{ errorMsg }}
|
{{ errorMsg }}
|
||||||
</message>
|
</message>
|
||||||
<div class="has-text-centered" v-if="isSuccess">
|
<div class="has-text-centered mb-4" v-if="isSuccess">
|
||||||
<message variant="success">
|
<message variant="success">
|
||||||
{{ $t('user.auth.resetPasswordSuccess') }}
|
{{ $t('user.auth.resetPasswordSuccess') }}
|
||||||
</message>
|
</message>
|
||||||
|
|
Loading…
Reference in a new issue