Merge pull request #4168 from Ocelot-Social-Community/4092-redesign-registration-process

feat: 🍰 Redesign Registration Process Frontend
This commit is contained in:
Wolfgang Huß 2021-03-01 16:15:00 +01:00 committed by GitHub
commit 76d3788cf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1447 additions and 5 deletions

View File

@ -16,6 +16,7 @@ describe('rewards', () => {
}
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {

View File

@ -0,0 +1,87 @@
<template>
<div class="Sliders">
<slot :name="'header'" />
<ds-heading v-if="sliderData.sliders[sliderIndex].title" size="h3">
{{ sliderData.sliders[sliderIndex].title }}
</ds-heading>
<slot :name="sliderData.sliders[sliderIndex].name" />
<ds-flex>
<ds-flex-item :centered="true">
<div
v-for="(slider, index) in sliderData.sliders"
:key="slider.name"
:class="['Sliders__slider-selection', index < sliderIndex && '--confirmed']"
>
<base-button
:class="['selection-dot']"
style="float: left"
:circle="true"
size="small"
type="submit"
filled
:loading="false"
:disabled="index > sliderIndex"
@click="sliderData.sliderSelectorCallback(index)"
/>
</div>
</ds-flex-item>
<ds-flex-item>
<base-button
style="float: right"
:icon="sliderData.sliders[sliderIndex].button.icon"
type="submit"
filled
:loading="false"
:disabled="!sliderData.sliders[sliderIndex].validated"
@click="onNextClick"
>
{{ sliderData.sliders[sliderIndex].button.title }}
</base-button>
</ds-flex-item>
</ds-flex>
<slot :name="'footer'" />
</div>
</template>
<script>
export default {
name: 'ComponentSlider',
props: {
sliderData: { type: Object, required: true },
},
computed: {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
},
methods: {
async onNextClick() {
let success = true
if (this.sliderData.sliders[this.sliderIndex].button.sliderCallback) {
success = await this.sliderData.sliders[this.sliderIndex].button.sliderCallback()
}
success = success && this.sliderData.sliders[this.sliderIndex].button.callback(success)
if (success && this.sliderIndex < this.sliderData.sliders.length - 1) {
this.sliderData.sliderSelectorCallback(this.sliderIndex + 1)
}
},
},
}
</script>
<style lang="scss">
.Sliders {
&__slider-selection {
.selection-dot {
margin-right: 2px;
}
&.--confirmed {
opacity: $opacity-disabled;
}
}
}
</style>

View File

@ -0,0 +1,84 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import { action } from '@storybook/addon-actions'
import Vuex from 'vuex'
import helpers from '~/storybook/helpers'
import links from '~/constants/links.js'
import metadata from '~/constants/metadata.js'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import CreateUserAccount from './CreateUserAccount.vue'
helpers.init()
const createStore = ({ loginSuccess }) => {
return new Vuex.Store({
modules: {
auth: {
namespaced: true,
state: () => ({
pending: false,
}),
mutations: {
SET_PENDING(state, pending) {
state.pending = pending
},
},
getters: {
pending(state) {
return !!state.pending
},
},
actions: {
async login({ commit, dispatch }, args) {
action('Vuex action `auth/login`')(args)
return new Promise((resolve, reject) => {
commit('SET_PENDING', true)
setTimeout(() => {
commit('SET_PENDING', false)
if (loginSuccess) {
resolve(loginSuccess)
} else {
reject(new Error('Login unsuccessful'))
}
}, 1000)
})
},
},
},
},
})
}
storiesOf('CreateUserAccount', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('standard', () => ({
components: { LocaleSwitch, CreateUserAccount },
store: createStore({ loginSuccess: true }),
data: () => ({
links,
metadata,
nonce: 'A34RB56',
email: 'user@example.org',
}),
methods: {
handleSuccess() {
action('You are logged in!')()
},
},
template: `
<ds-container width="small">
<base-card>
<template #imageColumn>
<a :href="links.ORGANIZATION" :title="$t('login.moreInfo', metadata)" target="_blank">
<img class="image" alt="Sign up" src="/img/custom/sign-up.svg" />
</a>
</template>
<create-user-account @userCreated="handleSuccess" :email="email" :nonce="nonce" />
<template #topMenu>
<locale-switch offset="5" />
</template>
</base-card>
</ds-container>
`,
}))

View File

@ -0,0 +1,321 @@
<template>
<div v-if="response === 'success'">
<transition name="ds-transition-fade">
<sweetalert-icon icon="success" />
</transition>
<ds-text align="center" bold color="success">
{{ $t('components.registration.create-user-account.success') }}
</ds-text>
</div>
<div v-else-if="response === 'error'">
<transition name="ds-transition-fade">
<sweetalert-icon icon="error" />
</transition>
<ds-text align="center" bold color="danger">
{{ $t('components.registration.create-user-account.error') }}
</ds-text>
<ds-text align="center">
{{ $t('components.registration.create-user-account.help') }}
<a :href="'mailto:' + supportEmail">{{ supportEmail }}</a>
</ds-text>
<ds-space centered>
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</div>
<div v-else class="create-account-card">
<!-- Wolle <ds-space margin-top="large">
<ds-heading size="h3">
{{ $t('components.registration.create-user-account.title') }}
</ds-heading>
</ds-space> -->
<ds-form
class="create-user-account"
v-model="formData"
:schema="formSchema"
@submit="submit"
@input="handleInput"
@input-valid="handleInputValid"
>
<!-- leave this here in case the scoped variable is needed in the future nobody would remember this -->
<!-- <template v-slot="{ errors }"> -->
<template>
<ds-input
id="name"
model="name"
icon="user"
:label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.namePlaceholder')"
/>
<ds-input
id="about"
model="about"
type="textarea"
rows="3"
:label="$t('settings.data.labelBio')"
:placeholder="$t('settings.data.labelBio')"
/>
<ds-input
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength class="password-strength" :password="formData.password" />
<ds-text>
<!-- Wolle {{ $t('components.enter-nonce.form.description') }} -->
Your e-mail address:
<b>{{ this.sliderData.collectedInputData.email }}</b>
</ds-text>
<ds-text>
<input
id="checkbox0"
type="checkbox"
v-model="termsAndConditionsConfirmed"
:checked="termsAndConditionsConfirmed"
/>
<label for="checkbox0">
{{ $t('termsAndConditions.termsAndConditionsConfirmed') }}
<br />
<nuxt-link to="/terms-and-conditions">{{ $t('site.termsAndConditions') }}</nuxt-link>
</label>
</ds-text>
<ds-text>
<input id="checkbox1" type="checkbox" v-model="dataPrivacy" :checked="dataPrivacy" />
<label for="checkbox1">
{{ $t('components.registration.signup.form.data-privacy') }}
<br />
<nuxt-link to="/data-privacy">
{{ $t('site.data-privacy') }}
</nuxt-link>
</label>
</ds-text>
<ds-text>
<input id="checkbox2" type="checkbox" v-model="minimumAge" :checked="minimumAge" />
<label
for="checkbox2"
v-html="$t('components.registration.signup.form.minimum-age')"
></label>
</ds-text>
<ds-text>
<input id="checkbox3" type="checkbox" v-model="noCommercial" :checked="noCommercial" />
<label
for="checkbox3"
v-html="$t('components.registration.signup.form.no-commercial')"
></label>
</ds-text>
<ds-text>
<input id="checkbox4" type="checkbox" v-model="noPolitical" :checked="noPolitical" />
<label
for="checkbox4"
v-html="$t('components.registration.signup.form.no-political')"
></label>
</ds-text>
</template>
</ds-form>
</div>
</template>
<script>
import { VERSION } from '~/constants/terms-and-conditions-version.js'
import links from '~/constants/links'
import emails from '~/constants/emails'
import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
import { SignupVerificationMutation } from '~/graphql/Registration.js'
export default {
name: 'RegistrationItemCreateUserAccount',
components: {
PasswordStrength,
SweetalertIcon,
},
props: {
sliderData: { type: Object, required: true },
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
links,
supportEmail: emails.SUPPORT,
formData: {
name: '',
about: '',
...passwordForm.formData,
},
formSchema: {
name: {
type: 'string',
required: true,
min: 3,
},
about: {
type: 'string',
required: false,
},
...passwordForm.formSchema,
},
response: null, // Wolle
// TODO: Our styleguide does not support checkmarks.
// Integrate termsAndConditionsConfirmed into `this.formData` once we
// have checkmarks available.
termsAndConditionsConfirmed: false,
dataPrivacy: false,
minimumAge: false,
noCommercial: false,
noPolitical: false,
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.name = this.sliderData.collectedInputData.name
? this.sliderData.collectedInputData.name
: ''
this.formData.about = this.sliderData.collectedInputData.about
? this.sliderData.collectedInputData.about
: ''
this.formData.password = this.sliderData.collectedInputData.password
? this.sliderData.collectedInputData.password
: ''
this.formData.passwordConfirmation = this.sliderData.collectedInputData.passwordConfirmation
? this.sliderData.collectedInputData.passwordConfirmation
: ''
this.termsAndConditionsConfirmed = this.sliderData.collectedInputData
.termsAndConditionsConfirmed
? this.sliderData.collectedInputData.termsAndConditionsConfirmed
: false
this.dataPrivacy = this.sliderData.collectedInputData.dataPrivacy
? this.sliderData.collectedInputData.dataPrivacy
: false
this.minimumAge = this.sliderData.collectedInputData.minimumAge
? this.sliderData.collectedInputData.minimumAge
: false
this.noCommercial = this.sliderData.collectedInputData.noCommercial
? this.sliderData.collectedInputData.noCommercial
: false
this.noPolitical = this.sliderData.collectedInputData.noPolitical
? this.sliderData.collectedInputData.noPolitical
: false
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: { buttonSliderCallback: this.onNextClick },
})
})
},
computed: {
validInput() {
return (
this.formData.name.length >= 3 &&
this.formData.password.length >= 1 &&
this.formData.password === this.formData.passwordConfirmation &&
this.termsAndConditionsConfirmed &&
this.dataPrivacy &&
this.minimumAge &&
this.noCommercial &&
this.noPolitical
)
},
},
watch: {
termsAndConditionsConfirmed() {
this.sendValidation()
},
dataPrivacy() {
this.sendValidation()
},
minimumAge() {
this.sendValidation()
},
noCommercial() {
this.sendValidation()
},
noPolitical() {
this.sendValidation()
},
},
methods: {
sendValidation() {
const { name, about, password, passwordConfirmation } = this.formData
const {
termsAndConditionsConfirmed,
dataPrivacy,
minimumAge,
noCommercial,
noPolitical,
} = this
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: {
name,
about,
password,
passwordConfirmation,
termsAndConditionsConfirmed,
dataPrivacy,
minimumAge,
noCommercial,
noPolitical,
},
})
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
async submit() {
const { name, password, about } = this.formData
const { email, nonce } = this.sliderData.collectedInputData
const termsAndConditionsAgreedVersion = VERSION
const locale = this.$i18n.locale()
try {
await this.$apollo.mutate({
mutation: SignupVerificationMutation,
variables: {
name,
password,
about,
email,
nonce,
termsAndConditionsAgreedVersion,
locale,
},
})
this.response = 'success'
// Wolle setTimeout(() => {
// this.$emit('userCreated', {
// email,
// password,
// })
// }, 3000)
} catch (err) {
this.response = 'error'
}
},
onNextClick() {
this.submit()
return true
},
},
}
</script>
<style lang="scss" scoped>
.password-strength {
margin-bottom: 14px;
}
</style>

View File

@ -0,0 +1,262 @@
<template>
<!-- Wolle <ds-space v-if="!data && !error" margin="large"> -->
<ds-form
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
>
<!-- Wolle <h1>
{{
invitation
? $t('profile.invites.title', metadata)
: $t('components.registration.signup.title', metadata)
}}
</h1> -->
<ds-text
v-if="token"
v-html="$t('registration.signup.form.invitation-code', { code: token })"
/>
<ds-text>
{{
invitation
? $t('profile.invites.description')
: $t('components.registration.signup.form.description')
}}
</ds-text>
<ds-input
:placeholder="invitation ? $t('profile.invites.emailPlaceholder') : $t('login.email')"
type="email"
id="email"
model="email"
name="email"
icon="envelope"
/>
<slot></slot>
<ds-text v-if="sliderData.collectedInputData.emailSend">
<input id="checkbox" type="checkbox" v-model="sendEmailAgain" :checked="sendEmailAgain" />
<label for="checkbox0">
<!-- Wolle {{ $t('termsAndConditions.termsAndConditionsConfirmed') }} -->
{{ 'Send e-mail again' }}
</label>
</ds-text>
</ds-form>
<!-- Wolle </ds-space>
<div v-else margin="large">
<template v-if="!error">
<transition name="ds-transition-fade">
<sweetalert-icon icon="info" />
</transition>
<ds-text align="center" v-html="submitMessage" />
</template>
<template v-else>
<transition name="ds-transition-fade">
<sweetalert-icon icon="error" />
</transition>
<ds-text align="center">{{ error.message }}</ds-text>
<ds-space centered class="space-top">
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</template>
</div> -->
</template>
<script>
import gql from 'graphql-tag'
import metadata from '~/constants/metadata'
import { isEmail } from 'validator'
import normalizeEmail from '~/components/utils/NormalizeEmail'
// Wolle import { SweetalertIcon } from 'vue-sweetalert-icons'
export const SignupMutation = gql`
mutation($email: String!) {
Signup(email: $email) {
email
}
}
`
export const SignupByInvitationMutation = gql`
mutation($email: String!, $token: String!) {
SignupByInvitation(email: $email, token: $token) {
email
}
}
`
export default {
name: 'RegistrationItemEnterEmail',
components: {
// Wolle SweetalertIcon,
},
props: {
sliderData: { type: Object, required: true },
token: { type: String, default: null }, // Wolle not used???
invitation: { type: Boolean, default: false },
},
data() {
return {
metadata,
formData: {
email: '',
},
formSchema: {
email: {
type: 'email',
required: true,
message: this.$t('common.validations.email'),
},
},
// TODO: Our styleguide does not support checkmarks.
// Integrate termsAndConditionsConfirmed into `this.formData` once we
// have checkmarks available.
sendEmailAgain: false,
error: null, // Wolle
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.email = this.sliderData.collectedInputData.email
? this.sliderData.collectedInputData.email
: ''
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: {
...this.buttonValues().sliderSettings,
buttonSliderCallback: this.onNextClick,
},
})
})
},
watch: {
sendEmailAgain() {
this.setButtonValues()
},
},
computed: {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
// Wolle submitMessage() {
// const { email } = this.data.Signup
// return this.$t('components.registration.signup.form.success', { email })
// },
validInput() {
return isEmail(this.formData.email)
},
},
methods: {
async sendValidation() {
if (this.formData.email && isEmail(this.formData.email)) {
this.formData.email = normalizeEmail(this.formData.email)
}
const { email } = this.formData
this.sliderData.setSliderValuesCallback(this.validInput, { collectedInputData: { email } })
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
buttonValues() {
return {
sliderSettings: {
buttonTitle: this.sliderData.collectedInputData.emailSend
? this.sendEmailAgain
? 'Resend e-mail'
: 'Skip resend'
: 'Send e-mail', // Wolle
buttonIcon: this.sliderData.collectedInputData.emailSend
? this.sendEmailAgain
? 'envelope'
: 'arrow-right'
: 'envelope', // Wolle
},
}
},
setButtonValues() {
this.sliderData.setSliderValuesCallback(this.validInput, this.buttonValues())
},
async onNextClick() {
const mutation = this.token ? SignupByInvitationMutation : SignupMutation
const { token } = this
const { email } = this.formData
const variables = { email, token }
if (!this.sendEmailAgain && this.sliderData.collectedInputData.emailSend) {
return true
}
if (
this.sendEmailAgain ||
!this.sliderData.sliders[this.sliderIndex].data.request ||
(this.sliderData.sliders[this.sliderIndex].data.request &&
(!this.sliderData.sliders[this.sliderIndex].data.request.variables ||
(this.sliderData.sliders[this.sliderIndex].data.request.variables &&
!this.sliderData.sliders[this.sliderIndex].data.request.variables === variables)))
) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { request: { variables }, response: null } },
)
try {
const response = await this.$apollo.mutate({ mutation, variables }) // e-mail is send in emailMiddleware of backend
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { response: response.data } },
)
if (this.sliderData.sliders[this.sliderIndex].data.response) {
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: { emailSend: true },
})
this.setButtonValues()
const { email: respnseEmail } =
this.sliderData.sliders[this.sliderIndex].data.response.Signup ||
this.sliderData.sliders[this.sliderIndex].data.response.SignupByInvitation
this.$toast.success(
this.$t('components.registration.email.form.success', { email: respnseEmail }),
)
}
return true
} catch (err) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { request: null, response: null } },
)
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: { emailSend: false },
})
this.setButtonValues()
const { message } = err
const mapping = {
'A user account with this email already exists': 'email-exists',
'Invitation code already used or does not exist': 'invalid-invitation-token',
}
for (const [pattern, key] of Object.entries(mapping)) {
if (message.includes(pattern))
this.error = {
key,
message: this.$t(`components.registration.signup.form.errors.${key}`),
}
}
if (!this.error) {
this.$toast.error(message)
}
return false
}
}
},
},
}
</script>
<style>
.space-top {
margin-top: 6ex;
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<ds-form
class="enter-invite"
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
>
<ds-input
:placeholder="$t('components.enter-invite.form.invite-code')"
model="inviteCode"
name="inviteCode"
id="inviteCode"
icon="question-circle"
/>
<ds-text>
{{ $t('components.enter-invite.form.description') }}
</ds-text>
<slot></slot>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
export const isValidInviteCodeQuery = gql`
query($code: ID!) {
isValidInviteCode(code: $code)
}
`
export default {
name: 'RegistrationItemEnterInvite',
props: {
sliderData: { type: Object, required: true },
},
data() {
return {
formData: {
inviteCode: '',
},
formSchema: {
inviteCode: {
type: 'string',
// Wolle min: 6,
// max: 6,
required: true,
message: this.$t('components.enter-invite.form.validations.length'),
},
},
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.inviteCode = this.sliderData.collectedInputData.inviteCode
? this.sliderData.collectedInputData.inviteCode
: ''
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: { buttonSliderCallback: this.onNextClick },
})
})
},
computed: {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
validInput() {
return this.formData.inviteCode.length === 6
},
},
methods: {
async sendValidation() {
const { inviteCode } = this.formData
let dbValidated = false
if (this.validInput) {
await this.handleSubmitVerify()
dbValidated = this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode
}
this.sliderData.setSliderValuesCallback(dbValidated, { collectedInputData: { inviteCode } })
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
async handleSubmitVerify() {
const { inviteCode } = this.formData
const variables = { code: inviteCode }
if (
!this.sliderData.sliders[this.sliderIndex].data.request ||
(this.sliderData.sliders[this.sliderIndex].data.request &&
(!this.sliderData.sliders[this.sliderIndex].data.request.variables ||
(this.sliderData.sliders[this.sliderIndex].data.request.variables &&
!this.sliderData.sliders[this.sliderIndex].data.request.variables === variables)))
) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { request: { variables }, response: null } },
)
try {
const response = await this.$apollo.query({ query: isValidInviteCodeQuery, variables })
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { response: response.data } },
)
if (
this.sliderData.sliders[this.sliderIndex].data.response &&
this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode
) {
this.$toast.success(
this.$t('components.registration.invite-code.form.success', { inviteCode }),
)
}
} catch (err) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { response: { isValidInviteCode: false } } },
)
const { message } = err
this.$toast.error(message)
}
}
},
onNextClick() {
return true
},
},
}
</script>
<style lang="scss">
.enter-invite {
display: flex;
flex-direction: column;
margin: $space-large 0 $space-xxx-small 0;
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<ds-form
class="enter-nonce"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitVerify"
@input="handleInput"
@input-valid="handleInputValid"
>
<ds-text>
<!-- Wolle {{ $t('components.enter-nonce.form.description') }} -->
Your e-mail address:
<b>{{ this.sliderData.collectedInputData.email }}</b>
</ds-text>
<ds-input
:placeholder="$t('components.enter-nonce.form.nonce')"
model="nonce"
name="nonce"
id="nonce"
icon="question-circle"
/>
<ds-text>
{{ $t('components.enter-nonce.form.description') }}
</ds-text>
<slot></slot>
</ds-form>
</template>
<script>
export default {
name: 'RegistrationItemEnterNonce',
props: {
sliderData: { type: Object, required: true },
},
data() {
return {
formData: {
nonce: '',
},
formSchema: {
nonce: {
type: 'string',
// Wolle min: 6,
// max: 6,
required: true,
message: this.$t('components.enter-nonce.form.validations.length'),
},
},
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
// console.log('mounted !!! ')
this.formData.nonce = this.sliderData.collectedInputData.nonce
? this.sliderData.collectedInputData.nonce
: ''
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: { buttonSliderCallback: this.onNextClick },
})
})
},
computed: {
validInput() {
return this.formData.nonce.length === 6
},
},
methods: {
sendValidation() {
const { nonce } = this.formData
// Wolle shall the nonce be validated in the database?
// let dbValidated = false
// if (this.validInput) {
// await this.handleSubmitVerify()
// dbValidated = this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode
// }
// this.sliderData.setSliderValuesCallback(dbValidated, {
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: {
nonce,
},
})
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
handleSubmitVerify() {},
onNextClick() {
return true
},
},
}
</script>
<style lang="scss">
.enter-nonce {
display: flex;
flex-direction: column;
margin: $space-large 0 $space-xxx-small 0;
}
</style>

View File

@ -0,0 +1,160 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import RegistrationSlider from './RegistrationSlider.vue'
import helpers from '~/storybook/helpers'
import Vue from 'vue'
const plugins = [
(app = {}) => {
app.$apollo = {
mutate: (data) => {
if (JSON.stringify(data).includes('UpdateUser')) {
return { data: { UpdateUser: { id: data.variables.id, locale: data.variables.locale } } }
}
if (JSON.stringify(data).includes('Signup')) {
return { data: { Signup: { email: data.variables.email } } }
}
if (JSON.stringify(data).includes('SignupByInvitation')) {
return { data: { SignupByInvitation: { email: data.variables.email } } }
}
if (JSON.stringify(data).includes('SignupVerification')) {
return { data: { SignupByInvitation: { ...data.variables } } }
}
throw new Error(`Mutation name not found!`)
},
query: (data) => {
if (JSON.stringify(data).includes('isValidInviteCode')) {
return { data: { isValidInviteCode: true } }
}
throw new Error(`Query name not found!`)
},
}
Vue.prototype.$apollo = app.$apollo
return app
},
]
helpers.init({ plugins })
storiesOf('RegistrationSlider', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('invite-code empty', () => ({
components: { RegistrationSlider },
store: helpers.store,
data: () => ({}),
template: `
<registration-slider registrationType="invite-code" />
`,
}))
.add('invite-code with data', () => ({
components: { RegistrationSlider },
store: helpers.store,
data: () => ({
overwriteSliderData: {
collectedInputData: {
inviteCode: 'IN1T6Y',
email: 'wolle.huss@pjannto.com',
emailSend: false,
nonce: 'NTRSCZ',
name: 'Wolle',
password: 'Hello',
passwordConfirmation: 'Hello',
about: `Hey`,
termsAndConditionsConfirmed: true,
dataPrivacy: true,
minimumAge: true,
noCommercial: true,
noPolitical: true,
},
},
}),
template: `
<registration-slider registrationType="invite-code" :overwriteSliderData="overwriteSliderData" />
`,
}))
.add('public-registration empty', () => ({
components: { RegistrationSlider },
store: helpers.store,
data: () => ({}),
template: `
<registration-slider registrationType="public-registration" />
`,
}))
.add('public-registration with data', () => ({
components: { RegistrationSlider },
store: helpers.store,
data: () => ({
overwriteSliderData: {
collectedInputData: {
inviteCode: null,
email: 'wolle.huss@pjannto.com',
emailSend: false,
nonce: 'NTRSCZ',
name: 'Wolle',
password: 'Hello',
passwordConfirmation: 'Hello',
about: `Hey`,
termsAndConditionsConfirmed: true,
dataPrivacy: true,
minimumAge: true,
noCommercial: true,
noPolitical: true,
},
},
}),
template: `
<registration-slider registrationType="public-registration" :overwriteSliderData="overwriteSliderData" />
`,
}))
.add('invite-mail empty', () => ({
components: { RegistrationSlider },
store: helpers.store,
data: () => ({
overwriteSliderData: {
collectedInputData: {
inviteCode: null,
email: 'wolle.huss@pjannto.com',
emailSend: true,
nonce: null,
name: null,
password: null,
passwordConfirmation: null,
about: null,
termsAndConditionsConfirmed: null,
dataPrivacy: null,
minimumAge: null,
noCommercial: null,
noPolitical: null,
},
},
}),
template: `
<registration-slider registrationType="invite-mail" :overwriteSliderData="overwriteSliderData" />
`,
}))
.add('invite-mail with data', () => ({
components: { RegistrationSlider },
store: helpers.store,
data: () => ({
overwriteSliderData: {
collectedInputData: {
inviteCode: null,
email: 'wolle.huss@pjannto.com',
emailSend: true,
nonce: 'NTRSCZ',
name: 'Wolle',
password: 'Hello',
passwordConfirmation: 'Hello',
about: `Hey`,
termsAndConditionsConfirmed: true,
dataPrivacy: true,
minimumAge: true,
noCommercial: true,
noPolitical: true,
},
},
}),
template: `
<registration-slider registrationType="invite-mail" :overwriteSliderData="overwriteSliderData" />
`,
}))

View File

@ -0,0 +1,235 @@
<template>
<section class="login-form">
<base-card>
<template #imageColumn>
<a :href="links.ORGANIZATION" :title="$t('login.moreInfo', metadata)" target="_blank">
<img class="image" alt="Welcome" src="/img/custom/welcome.svg" />
</a>
</template>
<component-slider :sliderData="sliderData">
<template #header>
<ds-heading size="h2">
{{ $t('components.registration.signup.title', metadata) }}
</ds-heading>
</template>
<template v-if="['invite-code'].includes(registrationType)" #enter-invite>
<registration-item-enter-invite :sliderData="sliderData" />
</template>
<template
v-if="['invite-code', 'public-registration'].includes(registrationType)"
#enter-email
>
<!-- Wolle !!! may create same source with 'webapp/pages/registration/signup.vue' -->
<!-- <signup v-if="publicRegistration" :invitation="false" @submit="handleSubmitted"> -->
<registration-item-enter-email :sliderData="sliderData" :invitation="false" />
</template>
<template
v-if="['invite-code', 'public-registration', 'invite-mail'].includes(registrationType)"
#enter-nonce
>
<registration-item-enter-nonce :sliderData="sliderData" />
</template>
<template #create-user-account>
<registration-item-create-user-account :sliderData="sliderData" />
</template>
<template #footer>
<ds-space margin-bottom="xxx-small" margin-top="small" centered>
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</template>
</component-slider>
<template #topMenu>
<locale-switch offset="5" />
</template>
</base-card>
</section>
</template>
<script>
import links from '~/constants/links.js'
import metadata from '~/constants/metadata.js'
import ComponentSlider from '~/components/ComponentSlider/ComponentSlider'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import RegistrationItemCreateUserAccount from './RegistrationItemCreateUserAccount'
import RegistrationItemEnterEmail from '~/components/Registration/RegistrationItemEnterEmail'
import RegistrationItemEnterInvite from './RegistrationItemEnterInvite'
import RegistrationItemEnterNonce from './RegistrationItemEnterNonce'
export default {
name: 'RegistrationSlider',
components: {
ComponentSlider,
LocaleSwitch,
RegistrationItemCreateUserAccount,
RegistrationItemEnterEmail,
RegistrationItemEnterInvite,
RegistrationItemEnterNonce,
},
props: {
registrationType: { type: String, required: true },
overwriteSliderData: { type: Object, default: () => {} },
},
data() {
const slidersPortfolio = [
{
name: 'enter-invite',
// title: this.$t('components.registration.create-user-account.title'),
title: 'Invitation', // Wolle
validated: false,
data: { request: null, response: { isValidInviteCode: false } },
button: {
title: 'Next', // Wolle
icon: 'arrow-right',
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
{
name: 'enter-email',
title: 'E-Mail', // Wolle
validated: false,
data: { request: null, response: null },
button: {
title: '', // set by slider component
icon: '', // set by slider component
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
{
name: 'enter-nonce',
title: 'E-Mail Confirmation', // Wolle
validated: false,
data: { request: null, response: null },
button: {
title: 'Confirm', // Wolle
icon: 'arrow-right',
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
{
name: 'create-user-account',
title: this.$t('components.registration.create-user-account.title'),
validated: false,
data: { request: null, response: null },
button: {
// title: this.$t('actions.save'), // Wolle
title: 'Create', // Wolle
icon: 'check',
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
]
let sliders = []
switch (this.registrationType) {
case 'invite-code':
sliders = [
slidersPortfolio[0],
slidersPortfolio[1],
slidersPortfolio[2],
slidersPortfolio[3],
]
break
case 'public-registration':
sliders = [slidersPortfolio[1], slidersPortfolio[2], slidersPortfolio[3]]
break
case 'invite-mail':
sliders = [slidersPortfolio[2], slidersPortfolio[3]]
break
}
return {
links,
metadata,
sliderData: {
collectedInputData: {
inviteCode: null,
email: null,
emailSend: null,
nonce: null,
name: null,
password: null,
passwordConfirmation: null,
about: null,
termsAndConditionsConfirmed: null,
dataPrivacy: null,
minimumAge: null,
noCommercial: null,
noPolitical: null,
},
sliderIndex: 0,
sliders: sliders,
sliderSelectorCallback: this.sliderSelectorCallback,
setSliderValuesCallback: this.setSliderValuesCallback,
...this.overwriteSliderData,
},
}
},
computed: {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
},
methods: {
setSliderValuesCallback(isValid, { collectedInputData, sliderData, sliderSettings }) {
// all changes of 'this.sliders' has to be filled in from the top to be spread to the component slider and all slider components in the slot
this.sliderData.sliders[this.sliderIndex].validated = isValid
if (collectedInputData) {
this.sliderData.collectedInputData = {
...this.sliderData.collectedInputData,
...collectedInputData,
}
}
if (sliderData) {
if (this.sliderData.sliders[this.sliderIndex].data) {
this.sliderData.sliders[this.sliderIndex].data = {
request: sliderData.request
? sliderData.request
: this.sliderData.sliders[this.sliderIndex].data.request,
response: sliderData.response
? sliderData.response
: this.sliderData.sliders[this.sliderIndex].data.response,
}
}
}
if (sliderSettings) {
const { buttonTitle, buttonIcon, buttonSliderCallback } = sliderSettings
if (buttonTitle) {
this.sliderData.sliders[this.sliderIndex].button.title = buttonTitle
}
if (buttonIcon) {
this.sliderData.sliders[this.sliderIndex].button.icon = buttonIcon
}
if (buttonSliderCallback) {
this.sliderData.sliders[this.sliderIndex].button.sliderCallback = buttonSliderCallback
}
}
},
sliderSelectorCallback(selectedIndex) {
// all changes of 'this.sliders' has to be filled in from the top to be spread to the component slider and all slider components in the slot
if (selectedIndex <= this.sliderIndex + 1 && selectedIndex < this.sliderData.sliders.length) {
this.sliderData.sliderIndex = selectedIndex
}
},
buttonCallback(success) {
// all changes of 'this.sliders' has to be filled in from the top to be spread to the component slider and all slider components in the slot
return success
},
},
}
</script>
<style lang="scss"></style>

View File

@ -15,7 +15,7 @@ export default function PasswordForm({ translate }) {
},
passwordConfirmation: [
{
validator(rule, value, callback, source, options) {
validator(_rule, value, callback, source, _options) {
var errors = []
if (source.password !== value) {
errors.push(new Error(passwordMismatchMessage))

View File

@ -120,6 +120,16 @@
"versus": "Versus"
},
"components": {
"enter-invite": {
"form": {
"description": "Gib den Einladungs-Code ein, den du bekommen hast.",
"invite-code": "Einladungs-Code eingeben",
"next": "Weiter",
"validations": {
"length": "muss genau 6 Buchstaben lang sein"
}
}
},
"enter-nonce": {
"form": {
"description": "Öffne Dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
@ -152,9 +162,19 @@
"success": "Dein Benutzerkonto wurde erstellt!",
"title": "Benutzerkonto anlegen"
},
"email": {
"form": {
"success": "Verifikations-E-Mail gesendet an <b>{email}</b>!"
}
},
"invite-code": {
"form": {
"success": "Gültiger Einladungs-Code <b>{inviteCode}</b>!"
}
},
"signup": {
"form": {
"data-privacy": "Ich habe die <a href=\"/data-privacy\" target=\"_blank\"><ds-text bold color=\"primary\">Datenschutzerklärung</ds-text></a> gelesen und verstanden",
"data-privacy": "Ich habe die Datenschutzerklärung gelesen und verstanden.",
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
"errors": {
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!",
@ -733,7 +753,7 @@
"bank": "Bankverbindung",
"code-of-conduct": "Verhaltenscodex",
"contact": "Kontakt",
"data-privacy": "Datenschutz",
"data-privacy": "Datenschutzerklärung",
"director": "Geschäftsführer",
"error-occurred": "Ein Fehler ist aufgetreten.",
"faq": "FAQ",

View File

@ -120,6 +120,16 @@
"versus": "Versus"
},
"components": {
"enter-invite": {
"form": {
"description": "Enter the invitation code you received.",
"invite-code": "Enter your invite code",
"next": "Continue",
"validations": {
"length": "must be 6 characters long"
}
}
},
"enter-nonce": {
"form": {
"description": "Open your inbox and enter the code that we've sent to you.",
@ -152,9 +162,19 @@
"success": "Your account has been created!",
"title": "Create user account"
},
"email": {
"form": {
"success": "Verification e-mail send to <b>{email}</b>!"
}
},
"invite-code": {
"form": {
"success": "Valid invite code <b>{inviteCode}</b>!"
}
},
"signup": {
"form": {
"data-privacy": "I have read and understood the <a href=\"/data-privacy\" target=\"_blank\"><ds-text bold color=\"primary\">Privacy Statement</ds-text></a>.",
"data-privacy": "I have read and understood the privacy statement.",
"description": "To get started, you can register here for free:",
"errors": {
"email-exists": "There is already a user account with this e-mail address!",
@ -762,7 +782,7 @@
"termsAndConditions": {
"agree": "I agree!",
"newTermsAndConditions": "New Terms and Conditions",
"termsAndConditionsConfirmed": "I have read and confirmed the Terms and Conditions.",
"termsAndConditionsConfirmed": "I have read and confirmed the terms and conditions.",
"termsAndConditionsNewConfirm": "I have read and agree to the new terms of conditions.",
"termsAndConditionsNewConfirmText": "Please read the new terms of use now!"
},