feat: restyle unauthenticated screens (#1103)

I wanted to give the no-auth screens a new look. Here's what I ended up with:

![image](/attachments/d272f36b-03c1-40ca-91f6-30f34e03e5fd)

The image is something we could change with every release.

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1103
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
This commit is contained in:
konrad 2021-12-12 16:40:13 +00:00 committed by Dominik Pschenitschni
parent 14f1ee1885
commit 32353e3b76
10 changed files with 394 additions and 323 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 KiB

View file

@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<div class="api-url-info" v-else> <div class="api-url-info" v-else>
<i18n-t keypath="apiConfig.signInOn"> <i18n-t keypath="apiConfig.use">
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span> <span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
</i18n-t> </i18n-t>
<br/> <br/>
@ -101,7 +101,7 @@ export default {
// Set it + save it to local storage to save us the hoops // Set it + save it to local storage to save us the hoops
this.errorMsg = '' this.errorMsg = ''
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain}) this.$message.success({message: this.$t('apiConfig.success', {domain: this.apiDomain})})
this.configureApi = false this.configureApi = false
this.apiUrl = url this.apiUrl = url
this.$emit('foundApi', this.apiUrl) this.$emit('foundApi', this.apiUrl)

View file

@ -1,7 +1,9 @@
<template> <template>
<div class="message-wrapper">
<div class="message" :class="variant"> <div class="message" :class="variant">
<slot/> <slot/>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -14,6 +16,11 @@ defineProps({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.message-wrapper {
border-radius: $radius;
background: var(--white);
}
.message { .message {
padding: .75rem 1rem; padding: .75rem 1rem;
border-radius: $radius; border-radius: $radius;

View file

@ -1,40 +1,134 @@
<template> <template>
<div class="no-auth-wrapper"> <div class="no-auth-wrapper">
<Logo class="logo" width="200" height="58"/>
<div class="noauth-container"> <div class="noauth-container">
<Logo class="logo" width="400" height="117" /> <section class="image" :class="{'has-message': motd !== ''}">
<Message v-if="motd !== ''" class="my-2"> <Message v-if="motd !== ''">
{{ motd }} {{ motd }}
</Message> </Message>
<h2 class="image-title">
{{ $t('misc.welcomeBack') }}
</h2>
</section>
<section class="content">
<div>
<h2 class="title" v-if="title">{{ title }}</h2>
<api-config @foundApi="hasApiUrl = true"/>
<slot/> <slot/>
</div> </div>
<legal/>
</section>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message'
import Legal from '@/components/misc/legal'
import ApiConfig from '@/components/misc/api-config.vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {computed} from 'vue' import {computed} from 'vue'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@/composables/useTitle'
const route = useRoute()
const store = useStore() const store = useStore()
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 ?? ''))
useTitle(() => title.value)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.no-auth-wrapper { .no-auth-wrapper {
background: url('@/assets/llama.svg?url') no-repeat bottom left fixed var(--site-background); background: var(--site-background) url('@/assets/llama.svg?url') no-repeat fixed bottom left;
min-height: 100vh; min-height: 100vh;
display: flex;
flex-direction: column;
place-items: center;
@media screen and (max-width: $fullhd) {
padding-bottom: 15rem;
}
} }
.noauth-container { .noauth-container {
max-width: 450px; max-width: $desktop;
width: 100%; width: 100%;
margin: 0 auto; min-height: 60vh;
display: flex;
background-color: var(--white);
box-shadow: var(--shadow-md);
overflow: hidden;
@media screen and (min-width: $desktop) {
border-radius: $radius;
}
}
.image {
width: 50%;
padding: 1rem; padding: 1rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
@media screen and (max-width: $tablet) {
display: none;
}
@media screen and (min-width: $tablet) {
background: url('@/assets/no-auth-image.jpg') no-repeat bottom/cover;
position: relative;
&.has-message {
justify-content: space-between;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .2);
}
> * {
position: relative;
}
}
}
.content {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 2rem 2rem 1.5rem;
@media screen and (max-width: $desktop) {
width: 100%;
max-width: 450px;
margin-inline: auto;
}
@media screen and (min-width: $desktop) {
width: 50%;
}
} }
.logo { .logo {
color: var(--logo-text-color);
max-width: 100%; max-width: 100%;
margin: 1rem 0;
}
.image-title {
color: var(--white);
font-size: 2.5rem;
} }
</style> </style>

View file

@ -36,6 +36,7 @@
"password": "Password", "password": "Password",
"passwordRepeat": "Retype your password", "passwordRepeat": "Retype your password",
"passwordPlaceholder": "e.g. •••••••••••", "passwordPlaceholder": "e.g. •••••••••••",
"forgotPassword": "Forgot your password?",
"resetPassword": "Reset your password", "resetPassword": "Reset your password",
"resetPasswordAction": "Send me a password reset link", "resetPasswordAction": "Send me a password reset link",
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.", "resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
@ -473,7 +474,8 @@
"download": "Download", "download": "Download",
"showMenu": "Show the menu", "showMenu": "Show the menu",
"hideMenu": "Hide the menu", "hideMenu": "Hide the menu",
"forExample": "For example:" "forExample": "For example:",
"welcomeBack": "Welcome Back!"
}, },
"input": { "input": {
"resetColor": "Reset Color", "resetColor": "Reset Color",
@ -811,7 +813,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "eg. https://localhost:3456", "urlPlaceholder": "eg. https://localhost:3456",
"change": "change", "change": "change",
"signInOn": "Sign in to your Vikunja account on {0}", "use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.", "error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".", "success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required." "urlRequired": "A url is required."

View file

@ -105,21 +105,33 @@ const router = createRouter({
path: '/login', path: '/login',
name: 'user.login', name: 'user.login',
component: LoginComponent, component: LoginComponent,
meta: {
title: 'user.auth.login',
},
}, },
{ {
path: '/get-password-reset', path: '/get-password-reset',
name: 'user.password-reset.request', name: 'user.password-reset.request',
component: GetPasswordResetComponent, component: GetPasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
}, },
{ {
path: '/password-reset', path: '/password-reset',
name: 'user.password-reset.reset', name: 'user.password-reset.reset',
component: PasswordResetComponent, component: PasswordResetComponent,
meta: {
title: 'user.auth.resetPassword',
},
}, },
{ {
path: '/register', path: '/register',
name: 'user.register', name: 'user.register',
component: RegisterComponent, component: RegisterComponent,
meta: {
title: 'user.auth.register',
},
}, },
{ {
path: '/user/settings', path: '/user/settings',

View file

@ -1,12 +1,12 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">Login</h2>
<div class="box">
<message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess"> <message variant="success" class="has-text-centered" v-if="confirmedEmailSuccess">
{{ $t('user.auth.confirmEmailSuccess') }} {{ $t('user.auth.confirmEmailSuccess') }}
</message> </message>
<api-config @foundApi="hasApiUrl = true"/> <message variant="danger" v-if="errorMessage">
<form @submit.prevent="submit" id="loginform" v-if="hasApiUrl && localAuthEnabled"> {{ errorMessage }}
</message>
<form @submit.prevent="submit" id="loginform" v-if="localAuthEnabled">
<div class="field"> <div class="field">
<label class="label" for="username">{{ $t('user.auth.usernameEmail') }}</label> <label class="label" for="username">{{ $t('user.auth.usernameEmail') }}</label>
<div class="control"> <div class="control">
@ -74,13 +74,10 @@
</div> </div>
<div class="control"> <div class="control">
<router-link :to="{ name: 'user.password-reset.request' }" class="reset-password-link"> <router-link :to="{ name: 'user.password-reset.request' }" class="reset-password-link">
{{ $t('user.auth.resetPassword') }} {{ $t('user.auth.forgotPassword') }}
</router-link> </router-link>
</div> </div>
</div> </div>
<message variant="danger" v-if="errorMessage">
{{ errorMessage }}
</message>
</form> </form>
<div <div
@ -96,9 +93,6 @@
{{ $t('user.auth.loginWith', {provider: p.name}) }} {{ $t('user.auth.loginWith', {provider: p.name}) }}
</x-button> </x-button>
</div> </div>
<legal/>
</div>
</div> </div>
</template> </template>
@ -107,8 +101,6 @@ import {mapState} from 'vuex'
import {HTTPFactory} from '@/http-common' import {HTTPFactory} from '@/http-common'
import {LOADING} from '@/store/mutation-types' import {LOADING} from '@/store/mutation-types'
import legal from '../../components/misc/legal'
import ApiConfig from '@/components/misc/api-config.vue'
import {getErrorText} from '@/message' 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'
@ -117,13 +109,10 @@ import {getLastVisited, clearLastVisited} from '../../helpers/saveLastVisited'
export default { export default {
components: { components: {
Message, Message,
ApiConfig,
legal,
}, },
data() { data() {
return { return {
confirmedEmailSuccess: false, confirmedEmailSuccess: false,
hasApiUrl: false,
errorMessage: '', errorMessage: '',
} }
}, },
@ -161,13 +150,11 @@ export default {
} }
}, },
created() { created() {
this.hasApiUrl = window.API_URL !== ''
this.setTitle(this.$t('user.auth.login')) this.setTitle(this.$t('user.auth.login'))
}, },
computed: { computed: {
hasOpenIdProviders() { hasOpenIdProviders() {
return this.hasApiUrl && return this.openidConnect.enabled &&
this.openidConnect.enabled &&
this.openidConnect.providers && this.openidConnect.providers &&
this.openidConnect.providers.length > 0 this.openidConnect.providers.length > 0
}, },

View file

@ -1,7 +1,16 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">{{ $t('user.auth.resetPassword') }}</h2> <message v-if="errorMsg">
<div class="box"> {{ errorMsg }}
</message>
<div class="has-text-centered" v-if="successMessage">
<message variant="success">
{{ successMessage }}
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<form @submit.prevent="submit" id="form" v-if="!successMessage"> <form @submit.prevent="submit" id="form" v-if="!successMessage">
<div class="field"> <div class="field">
<label class="label" for="password1">{{ $t('user.auth.password') }}</label> <label class="label" for="password1">{{ $t('user.auth.password') }}</label>
@ -45,23 +54,7 @@
</x-button> </x-button>
</div> </div>
</div> </div>
<message v-if="this.passwordResetService.loading">
{{ $t('misc.loading') }}
</message>
<message v-if="errorMsg">
{{ errorMsg }}
</message>
</form> </form>
<div class="has-text-centered" v-if="successMessage">
<message variant="success">
{{ successMessage }}
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<Legal/>
</div>
</div> </div>
</template> </template>
@ -69,14 +62,11 @@
import {ref, reactive} from 'vue' import {ref, reactive} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset' import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset' import PasswordResetService from '@/services/passwordReset'
import {useTitle} from '@/composables/useTitle'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
const {t} = useI18n() const {t} = useI18n()
useTitle(() => t('user.auth.resetPassword'))
const credentials = reactive({ const credentials = reactive({
password: '', password: '',

View file

@ -1,7 +1,8 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">{{ $t('user.auth.register') }}</h2> <message variant="danger" v-if="errorMessage !== ''">
<div class="box"> {{ errorMessage }}
</message>
<form @submit.prevent="submit" id="registerform"> <form @submit.prevent="submit" id="registerform">
<div class="field"> <div class="field">
<label class="label" for="username">{{ $t('user.auth.username') }}</label> <label class="label" for="username">{{ $t('user.auth.username') }}</label>
@ -83,15 +84,7 @@
</x-button> </x-button>
</div> </div>
</div> </div>
<message v-if="loading">
{{ $t('misc.loading') }}
</message>
<message variant="danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</message>
</form> </form>
<legal/>
</div>
</div> </div>
</template> </template>
@ -101,8 +94,6 @@ import {useI18n} from 'vue-i18n'
import router from '@/router' import router from '@/router'
import {store} from '@/store' import {store} from '@/store'
import {useTitle} from '@/composables/useTitle'
import Legal from '@/components/misc/legal'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
// FIXME: use the `beforeEnter` hook of vue-router // FIXME: use the `beforeEnter` hook of vue-router
@ -114,7 +105,6 @@ onBeforeMount(() => {
}) })
const {t} = useI18n() const {t} = useI18n()
useTitle(() => t('user.auth.register'))
const credentials = reactive({ const credentials = reactive({
username: '', username: '',

View file

@ -1,7 +1,16 @@
<template> <template>
<div> <div>
<h2 class="title has-text-centered">{{ $t('user.auth.resetPassword') }}</h2> <message variant="danger" v-if="errorMsg">
<div class="box"> {{ errorMsg }}
</message>
<div class="has-text-centered" v-if="isSuccess">
<message variant="success">
{{ $t('user.auth.resetPasswordSuccess') }}
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<form @submit.prevent="submit" v-if="!isSuccess"> <form @submit.prevent="submit" v-if="!isSuccess">
<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>
@ -31,37 +40,17 @@
</x-button> </x-button>
</div> </div>
</div> </div>
<message variant="danger" v-if="errorMsg">
{{ errorMsg }}
</message>
</form> </form>
<div class="has-text-centered" v-if="isSuccess">
<message variant="success">
{{ $t('user.auth.resetPasswordSuccess') }}
</message>
<x-button :to="{ name: 'user.login' }">
{{ $t('user.auth.login') }}
</x-button>
</div>
<Legal />
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {ref, reactive} from 'vue' import {ref, reactive} from 'vue'
import { useI18n } from 'vue-i18n'
import Legal from '@/components/misc/legal'
import PasswordResetModel from '@/models/passwordReset' import PasswordResetModel from '@/models/passwordReset'
import PasswordResetService from '@/services/passwordReset' import PasswordResetService from '@/services/passwordReset'
import { useTitle } from '@/composables/useTitle'
import Message from '@/components/misc/message' import Message from '@/components/misc/message'
const { t } = useI18n()
useTitle(() => t('user.auth.resetPassword'))
// Not sure if this instance needs a shalloRef at all // Not sure if this instance needs a shalloRef at all
const passwordResetService = reactive(new PasswordResetService()) const passwordResetService = reactive(new PasswordResetService())
const passwordReset = ref(new PasswordResetModel()) const passwordReset = ref(new PasswordResetModel())
@ -73,7 +62,7 @@ async function submit() {
try { try {
await passwordResetService.requestResetPassword(passwordReset.value) await passwordResetService.requestResetPassword(passwordReset.value)
isSuccess.value = true isSuccess.value = true
} catch(e) { } catch (e) {
errorMsg.value = e.response.data.message errorMsg.value = e.response.data.message
} }
} }