fix(webapp): optimize registration layout (#8630)

* - fixed nowrap for button
- restyled bullets for slider
- relocated back links
- removed icons

* - removed icon from RegistrationSlideEmail too

* - added media query for padding

* - fixed missing constants

* - fixed padding in no-header layout

* - fixed sticky footer in registration flow

* - removed icons from inputs

* - set fixed height for back link

* - fixed invite code placeholder

* - added auto submit to invite and email code forms
- fixed layout password inputs
- added layout to checkboxes (create)
- removed unnecessary texts
- moved backLink for password-reset
- tidied up create layout

* fixed margin

* - fixed nonceLength

* lint fixes

* corrected path

---------

Co-authored-by: Sebastian Stein <sebastian@codepassion.de>
Co-authored-by: Ulf Gebhardt <ulf.gebhardt@webcraft-media.de>
This commit is contained in:
sebastian2357 2025-06-11 18:45:03 +02:00 committed by GitHub
parent a0e4b49833
commit b1c19d0c94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 271 additions and 158 deletions

View File

@ -276,6 +276,7 @@ $size-avatar-large: 114px;
$size-button-large: 50px;
$size-button-base: 36px;
$size-button-small: 26px;
$size-button-tiny: 18px;
/**
* @tokens Size Images

View File

@ -274,6 +274,7 @@ $size-avatar-large: 114px;
$size-button-large: 50px;
$size-button-base: 36px;
$size-button-small: 26px;
$size-button-tiny: 18px;
/**
* @tokens Size Images

View File

@ -38,8 +38,8 @@
<base-button
:class="['selection-dot']"
style="float: left"
:circle="true"
size="small"
:bullet="true"
size="tiny"
type="submit"
filled
:loading="false"
@ -54,6 +54,7 @@
:icon="sliderData.sliders[sliderIndex].button.icon"
type="submit"
filled
padding
:loading="
sliderData.sliders[sliderIndex].button.loading !== undefined
? sliderData.sliders[sliderIndex].button.loading

View File

@ -28,6 +28,8 @@
</template>
<script>
import registrationConstants from '~/constants/registration'
export default {
props: {
email: { type: String, required: true },
@ -43,7 +45,9 @@ export default {
min: 5,
max: 5,
required: true,
message: this.$t('components.registration.email-nonce.form.validations.length'),
message: this.$t('components.registration.email-nonce.form.validations.length', {
nonceLength: registrationConstants.NONCE_LENGTH,
}),
},
},
disabled: true,

View File

@ -1,5 +1,5 @@
<template>
<div id="footer" class="ds-footer">
<div id="footer" class="ds-footer" :class="{ 'is-sticky': isSticky }">
<!-- links to internal or external pages -->
<span v-for="pageParams in links.FOOTER_LINK_LIST" :key="pageParams.name">
<page-params-link :pageParams="pageParams">
@ -26,6 +26,12 @@ export default {
components: {
PageParamsLink,
},
props: {
isSticky: {
type: Boolean,
default: true,
},
},
data() {
return { links, version: `v${this.$env.VERSION}` }
},
@ -35,13 +41,16 @@ export default {
<style lang="scss" scoped>
.ds-footer {
text-align: center;
position: fixed;
bottom: 0px;
z-index: 10;
background-color: $color-footer-background;
width: 100%;
padding: 10px 10px;
box-shadow: 0px -6px 12px -4px rgba(0, 0, 0, 0.1);
&.is-sticky {
position: fixed;
bottom: 0px;
}
}
.ds-footer a {
color: $color-footer-link;

View File

@ -24,9 +24,9 @@
:disabled="disabled"
:loading="$apollo.loading"
filled
padding
name="submit"
type="submit"
icon="envelope"
>
{{ $t('components.password-reset.request.form.submit') }}
</base-button>

View File

@ -1,6 +1,5 @@
<template>
<ds-text>
{{ $t('components.registration.email-display.yourEmail') }}
<b v-if="emailAsString.length > 0">
{{ emailAsString }}
<b v-if="!isEmailFormat" class="email-warning">

View File

@ -35,19 +35,19 @@
<!-- leave this here in case the scoped variable is needed in the future nobody would remember this -->
<!-- <template v-slot="{ errors }"> -->
<template>
<email-display-and-verify :email="sliderData.collectedInputData.email" />
<div v-if="askForRealName" class="full-name">
<!-- <p>{{ $t('settings.data.realNamePlease') }}</p>-->
<ds-input
id="givenName"
model="givenName"
icon="user"
:label="$t('settings.data.givenName')"
:placeholder="$t('settings.data.givenNamePlaceholder')"
/>
<ds-input
id="surName"
model="surName"
icon="user"
:label="$t('settings.data.surName')"
:placeholder="$t('settings.data.surNamePlaceholder')"
/>
@ -56,90 +56,99 @@
v-else
id="name"
model="name"
icon="user"
:label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.namePlaceholder')"
/>
<label for="password">
{{ $t('settings.security.change-password.label-new-password') }}
</label>
<div class="password-wrapper">
<ds-input
id="password"
model="password"
:type="showPassword ? 'text' : 'password'"
:label="$t('settings.security.change-password.label-new-password')"
autocomplete="off"
class="password-field"
ref="password"
/>
<show-password
class="show-password-toggle with-label"
@show-password="toggleShowPassword('password')"
:iconName="iconNamePassword"
/>
</div>
<label for="passwordConfirmation">
{{ $t('settings.security.change-password.label-new-password-confirm') }}
</label>
<div class="password-wrapper">
<ds-input
id="passwordConfirmation"
model="passwordConfirmation"
:type="showPasswordConfirm ? 'text' : 'password'"
:label="$t('settings.security.change-password.label-new-password-confirm')"
autocomplete="off"
class="password-field"
ref="confirmPassword"
/>
<show-password
class="show-password-toggle with-label"
@show-password="toggleShowPassword('confirmPassword')"
:iconName="iconNamePasswordConfirm"
/>
</div>
<password-strength class="password-strength" :password="formData.password" />
<password-strength
class="password-strength"
:password="formData.password"
style="margin: 30px 0 20px 0"
/>
<!-- location -->
<location-select
v-if="locationRequired"
class="location-select"
v-model="locationName"
:canBeCleared="false"
:showPreviousLocation="false"
/>
<email-display-and-verify :email="sliderData.collectedInputData.email" />
<ds-text>
<div class="checkbox-group">
<div class="checkbox-item">
<input
id="checkbox0"
type="checkbox"
v-model="termsAndConditionsConfirmed"
:checked="termsAndConditionsConfirmed"
class="checkbox-input"
/>
<label for="checkbox0">
<label for="checkbox0" class="checkbox-label">
<span class="checkbox-text">
{{ $t('components.registration.create-user-account.termsAndCondsEtcConfirmed') }}
<br />
</span>
<div class="checkbox-links">
<page-params-link :pageParams="links.TERMS_AND_CONDITIONS" forceTargetBlank>
{{ $t('site.termsAndConditions') }}
</page-params-link>
<br />
<span class="separator"></span>
<page-params-link :pageParams="links.DATA_PRIVACY" forceTargetBlank>
{{ $t('site.data-privacy') }}
</page-params-link>
</div>
</label>
</ds-text>
<ds-text>
</div>
<div class="checkbox-item">
<input
id="checkbox1"
type="checkbox"
v-model="receiveCommunicationAsEmailsEtcConfirmed"
:checked="receiveCommunicationAsEmailsEtcConfirmed"
class="checkbox-input"
/>
<label for="checkbox1">
<label for="checkbox1" class="checkbox-label">
<span class="checkbox-text">
{{
$t(
'components.registration.create-user-account.receiveCommunicationAsEmailsEtcConfirmed',
)
}}
</span>
</label>
</ds-text>
</div>
</div>
</template>
<ds-space margin="xxx-small" />
</ds-form>
@ -395,39 +404,19 @@ export default {
display: flex;
width: 100%;
align-items: center;
padding: $input-padding-vertical $space-x-small;
padding-left: 0;
padding-right: 0;
padding: 0;
height: $input-height;
margin-bottom: 10px;
margin-bottom: $space-small;
color: $text-color-base;
background: $background-color-disabled;
border: $input-border-size solid $border-color-softer;
border-left: none;
border-radius: $border-radius-base;
outline: none;
transition: all $duration-short $ease-out;
&:focus-within {
background-color: $background-color-base;
border: $input-border-size solid $border-color-active;
.toggle-icon {
color: $text-color-base;
}
}
margin-top: 40px;
margin-bottom: 16px;
.password-field {
position: relative;
padding-top: $space-small;
border: none;
border-style: none;
appearance: none;
margin-left: 0;
width: 100%;
padding: 0;
}
::v-deep .ds-input-wrap {
bottom: 4px;
left: -1px;
}
}
@ -435,7 +424,60 @@ export default {
padding-bottom: $space-small;
}
.location-select {
padding-bottom: $space-base;
.checkbox-group {
margin: 20px 0;
}
.checkbox-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
padding: 16px;
background-color: #f8f9fa; // Light gray background
border-radius: 8px;
transition: background-color 0.2s ease;
&:hover {
background-color: #f0f2f5; // Slightly darker on hover
}
&:last-child {
margin-bottom: 0;
}
}
.checkbox-input {
flex-shrink: 0;
width: 18px;
height: 18px;
margin-right: 12px;
margin-top: 2px; // Align with first line of text
cursor: pointer;
}
.checkbox-label {
flex: 1;
cursor: pointer;
line-height: 1.5;
color: #374151; // Dark gray text
}
.checkbox-text {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.checkbox-links {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 0.875rem; // Slightly smaller text for links
.separator {
color: #9ca3af; // Light gray separator
font-size: 0.75rem;
}
}
</style>

View File

@ -9,14 +9,7 @@
<ds-text>
{{ $t('components.registration.signup.form.description') }}
</ds-text>
<ds-input
:placeholder="$t('login.email')"
type="email"
id="email"
model="email"
name="email"
icon="envelope"
/>
<ds-input :placeholder="$t('login.email')" type="email" id="email" model="email" name="email" />
<slot></slot>
<ds-text v-if="sliderData.collectedInputData.emailSend">
<input id="checkbox" type="checkbox" v-model="sendEmailAgain" :checked="sendEmailAgain" />
@ -111,11 +104,7 @@ export default {
? 'components.registration.email.buttonTitle.resend'
: 'components.registration.email.buttonTitle.skipResend'
: 'components.registration.email.buttonTitle.send',
buttonIcon: this.sliderData.collectedInputData.emailSend
? this.sendEmailAgain
? 'envelope'
: 'arrow-right'
: 'envelope',
buttonIcon: null,
},
}
},

View File

@ -7,11 +7,12 @@
@input-valid="handleInputValid"
>
<ds-input
:placeholder="$t('components.registration.invite-code.form.invite-code')"
:placeholder="formSchema.inviteCode.placeholder"
:minlength="formSchema.inviteCode.minLength"
:maxlength="formSchema.inviteCode.maxLength"
model="inviteCode"
name="inviteCode"
id="inviteCode"
icon="question-circle"
/>
<ds-text v-if="!validInput">
{{ $t('components.registration.invite-code.form.description') }}
@ -43,7 +44,7 @@
</template>
<script>
import registrationConstants from '~/constants/registration'
import registrationConstants from '~/constants/registrationBranded.js'
import { validateInviteCode } from '~/graphql/InviteCode'
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
@ -69,6 +70,7 @@ export default {
message: this.$t('components.registration.invite-code.form.validations.length', {
inviteCodeLength: registrationConstants.INVITE_CODE_LENGTH,
}),
placeholder: this.$t('components.registration.invite-code.form.invite-code'),
},
},
dbRequestInProgress: false,
@ -141,11 +143,16 @@ export default {
const validationResult = response.data.validateInviteCode
if (validationResult && validationResult.isValid) {
this.$toast.success(
this.$t('components.registration.invite-code.form.validations.success', {
inviteCode,
}),
)
// Auto-advance to next slide
const currentIndex = this.sliderData.sliderIndex
const nextIndex = currentIndex + 1
if (
this.sliderData.sliderSelectorCallback &&
nextIndex < this.sliderData.sliders.length
) {
this.sliderData.sliderSelectorCallback(nextIndex)
}
return true
} else {
this.$toast.error(

View File

@ -13,14 +13,10 @@
model="nonce"
name="nonce"
id="nonce"
icon="question-circle"
/>
<ds-text>
{{ $t('components.registration.email-nonce.form.description') }}
</ds-text>
<ds-text>
{{ $t('components.registration.email-nonce.form.click-next') }}
</ds-text>
<slot></slot>
<ds-space margin="xxx-small" />
</ds-form>
@ -120,12 +116,16 @@ export default {
if (this.sliderData.sliders[this.sliderIndex].data.response) {
if (this.sliderData.sliders[this.sliderIndex].data.response.VerifyNonce) {
this.$toast.success(
this.$t('components.registration.email-nonce.form.validations.success', {
email,
nonce,
}),
)
// Auto-advance to next slide
const currentIndex = this.sliderData.sliderIndex
const nextIndex = currentIndex + 1
if (
this.sliderData.sliderSelectorCallback &&
nextIndex < this.sliderData.sliders.length
) {
this.sliderData.sliderSelectorCallback(nextIndex)
}
} else {
this.$toast.error(
this.$t('components.registration.email-nonce.form.validations.error', {

View File

@ -1,5 +1,8 @@
<template>
<section class="registration-slider">
<div v-if="registrationType !== 'no-public-registration'" class="back-link" left>
<nuxt-link :to="loginLink">{{ $t('site.back-to-login') }}</nuxt-link>
</div>
<base-card>
<template #imageColumn>
<page-params-link :pageParams="links.ORGANIZATION" :title="$t('login.moreInfo', metadata)">
@ -27,12 +30,6 @@
<template #create-user-account>
<registration-slide-create :sliderData="sliderData" />
</template>
<template v-if="registrationType !== 'no-public-registration'" #footer>
<ds-space margin-bottom="xxx-small" margin-top="small" centered>
<nuxt-link :to="loginLink">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</template>
</component-slider>
<template #topMenu>
@ -94,7 +91,7 @@ export default {
data: { request: null, response: { isValidInviteCode: false } },
button: {
titleIdent: 'components.registration.invite-code.buttonTitle',
icon: 'arrow-right',
icon: null,
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
@ -106,7 +103,7 @@ export default {
data: { request: null, response: null },
button: {
titleIdent: 'components.registration.email.buttonTitle.send', // changed by slider component
icon: 'envelope', // changed by slider component
icon: null,
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
@ -118,7 +115,7 @@ export default {
data: { request: null, response: { VerifyNonce: false } },
button: {
titleIdent: 'components.registration.email-nonce.buttonTitle',
icon: 'arrow-right',
icon: null,
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
@ -130,7 +127,7 @@ export default {
data: { request: null, response: null },
button: {
titleIdent: 'components.registration.create-user-account.buttonTitle',
icon: 'check',
icon: null,
loading: false,
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
@ -266,4 +263,7 @@ export default {
max-width: 620px;
margin: auto;
}
.back-link {
height: 35px;
}
</style>

View File

@ -29,7 +29,6 @@
id="email"
model="email"
name="email"
icon="envelope"
/>
<base-button
:disabled="disabled"
@ -37,7 +36,6 @@
filled
name="submit"
type="submit"
icon="envelope"
>
{{ $t('components.registration.signup.form.submit') }}
</base-button>

View File

@ -19,6 +19,10 @@ export default {
LoadingSpinner,
},
props: {
bullet: {
type: Boolean,
default: false,
},
circle: {
type: Boolean,
default: false,
@ -46,9 +50,13 @@ export default {
type: String,
default: 'regular',
validator(value) {
return value.match(/(small|regular|large)/)
return value.match(/(tiny|small|regular|large)/)
},
},
padding: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'button',
@ -66,11 +74,14 @@ export default {
let buttonClass = 'base-button'
if (this.$slots.default === undefined) buttonClass += ' --icon-only'
if (this.bullet) buttonClass += ' --bullet'
if (this.circle) buttonClass += ' --circle'
if (this.danger) buttonClass += ' --danger'
if (this.loading) buttonClass += ' --loading'
if (this.size === 'tiny') buttonClass += ' --tiny'
if (this.size === 'small') buttonClass += ' --small'
if (this.size === 'large') buttonClass += ' --large'
if (this.padding) buttonClass += ' --padding'
if (this.filled) buttonClass += ' --filled'
else if (this.ghost) buttonClass += ' --ghost'
@ -98,6 +109,7 @@ export default {
font-weight: $font-weight-bold;
letter-spacing: $letter-spacing-large;
cursor: pointer;
white-space: nowrap;
&.--danger {
@include buttonStates($color-scheme: danger);
@ -117,14 +129,33 @@ export default {
border-radius: 50%;
}
&.--bullet {
width: 18px;
height: 18px;
border-radius: 50%;
&[disabled] {
background-color: transparent;
border: 1px solid $color-neutral-80;
}
}
&.--ghost {
border: none;
}
&.--tiny {
height: $size-button-tiny;
&.--bullet,
&.--circle {
width: $size-button-tiny;
}
}
&.--small {
height: $size-button-small;
font-size: $font-size-small;
&.--bullet,
&.--circle {
width: $size-button-small;
}
@ -134,11 +165,21 @@ export default {
height: $size-button-large;
font-size: $font-size-large;
&.--bullet,
&.--circle {
width: $size-button-large;
}
}
&.--padding {
padding: 0 20px;
}
@media screen and (max-width: 400px) {
&.--padding {
padding: 0 15px;
}
}
&:not(.--icon-only) > .base-icon {
margin-right: $space-xx-small;
}

View File

@ -1,11 +1,13 @@
<template>
<div class="layout-blank">
<div class="layout-content">
<ds-container>
<div style="padding: 5rem 2rem">
<div>
<nuxt />
</div>
</ds-container>
<page-footer />
</div>
<page-footer :is-sticky="false" />
<div id="overlay" />
</div>
</template>
@ -26,3 +28,25 @@ export default {
},
}
</script>
<style lang="scss">
.layout-blank {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.layout-content {
flex: 1;
}
.layout-blank > .layout-content > .ds-container > div {
padding: 5rem 2rem;
}
@media only screen and (max-width: 500px) {
.layout-blank > .layout-content > .ds-container > div {
padding: 3rem 0;
}
}
</style>

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": "⚠️ E-Mail hat ein ungültiges Format!",
"warningUndef": "⚠️ Keine E-Mail definiert!",
"yourEmail": "Deine E-Mail-Adresse:"
"warningUndef": "⚠️ Keine E-Mail definiert!"
},
"email-nonce": {
"buttonTitle": "Weiter",

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": "⚠️ E-mail has wrong format!",
"warningUndef": "⚠️ No e-mail defined!",
"yourEmail": "Your e-mail address:"
"warningUndef": "⚠️ No e-mail defined!"
},
"email-nonce": {
"buttonTitle": "Continue",

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": null,
"warningUndef": null,
"yourEmail": null
"warningUndef": null
},
"email-nonce": {
"buttonTitle": null,

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": null,
"warningUndef": null,
"yourEmail": null
"warningUndef": null
},
"email-nonce": {
"buttonTitle": null,

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": null,
"warningUndef": null,
"yourEmail": null
"warningUndef": null
},
"email-nonce": {
"buttonTitle": null,

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": null,
"warningUndef": null,
"yourEmail": null
"warningUndef": null
},
"email-nonce": {
"buttonTitle": null,

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": null,
"warningUndef": null,
"yourEmail": null
"warningUndef": null
},
"email-nonce": {
"buttonTitle": null,

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": null,
"warningUndef": null,
"yourEmail": null
"warningUndef": null
},
"email-nonce": {
"buttonTitle": null,

View File

@ -227,8 +227,7 @@
},
"email-display": {
"warningFormat": null,
"warningUndef": null,
"yourEmail": null
"warningUndef": null
},
"email-nonce": {
"buttonTitle": null,

View File

@ -1,5 +1,9 @@
<template>
<ds-container width="small" class="password-reset">
<div class="back-link">
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</div>
<base-card>
<template #imageColumn>
<page-params-link :pageParams="links.ORGANIZATION" :title="$t('login.moreInfo', metadata)">
@ -42,3 +46,9 @@ export default {
},
}
</script>
<style lang="scss">
.back-link {
height: 35px;
}
</style>

View File

@ -1,9 +1,5 @@
<template>
<request @handleSubmitted="handlePasswordResetRequested">
<ds-space margin-bottom="xxx-small" margin-top="large" centered>
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</request>
<request @handleSubmitted="handlePasswordResetRequested"></request>
</template>
<script>