Files
Hoard/GUI/src/routes/authentication/Login.vue
T
Jonas 6c2a149f96 Add global app banners and integrate into layout
Introduce a global app banners store (GUI/src/stores/appBanners.ts) and switch authentication error/notification flows to use it. GUI/src/Layout.vue now consumes the banner store, replaces the single snackbar with a stacked, dismissible banner UI (styles, transitions and z-index included), and adds navigateToBrandTarget() to route the brand button based on auth state. GUI/src/routes/authentication/Login.vue was updated to push errors to the new banner store and remove the local alert. Minor route adjustments in GUI/src/plugins/routesLayout.ts: rename 'Dash' to 'Dashboard' and change Login visibility to Unauthenticated. codexInfo.md updated to document these UI/behavior changes.
2026-04-18 23:02:01 +02:00

257 lines
5.9 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { AuthRequestError, login } from '@/services/authSession'
import { useAppBannersStore } from '@/stores/appBanners'
const showPassword = ref(false)
const userName = ref('')
const password = ref('')
const isSubmitting = ref(false)
const appBannersStore = useAppBannersStore()
const route = useRoute()
const router = useRouter()
const submitDisabled = computed(
() => isSubmitting.value || userName.value.trim().length === 0 || password.value.length === 0,
)
const redirectPath = computed(() => {
const redirectQuery = route.query.redirect
if (typeof redirectQuery === 'string' && redirectQuery.startsWith('/')) {
return redirectQuery
}
return '/'
})
async function handleSubmit() {
if (submitDisabled.value) {
appBannersStore.pushError('Bitte Benutzername und Passwort eingeben.', 'Anmeldung fehlgeschlagen')
return
}
isSubmitting.value = true
try {
await login({
userName: userName.value.trim(),
password: password.value,
})
await router.replace(redirectPath.value)
} catch (error) {
if (error instanceof AuthRequestError) {
appBannersStore.pushError(error.message, 'Anmeldung fehlgeschlagen')
} else {
appBannersStore.pushError(
'Anmeldung fehlgeschlagen. Bitte versuche es erneut.',
'Anmeldung fehlgeschlagen',
)
}
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<v-container fluid class="login-page hoard-page hoard-page--centered">
<section class="login-shell hoard-panel hoard-shell-grid hoard-panel-gradient">
<aside class="login-brand">
<p class="login-kicker hoard-kicker hoard-kicker--wide">Willkommen bei Hoard</p>
<h1>Anmelden und weiterarbeiten</h1>
<p class="login-intro">
Deine Dateiablage bleibt aufgeräumt, schnell und direkt im Browser bedienbar.
</p>
<ul class="login-points">
<li>
<v-icon icon="mdi-folder-outline" size="18" />
Ordner und Dateien zentral verwalten
</li>
<li>
<v-icon icon="mdi-file-document-edit-outline" size="18" />
Markdown-Dateien sofort bearbeiten
</li>
<li>
<v-icon icon="mdi-image-outline" size="18" />
Bilder und PDFs direkt als Vorschau ansehen
</li>
</ul>
</aside>
<v-form class="login-form hoard-panel" @submit.prevent="handleSubmit">
<div class="form-head">
<h2>Login</h2>
<p>Melde dich mit deinem bestehenden Konto an.</p>
</div>
<v-text-field
v-model="userName"
label="Benutzername"
type="text"
variant="outlined"
prepend-inner-icon="mdi-account-outline"
autocomplete="username"
required
:disabled="isSubmitting"
/>
<v-text-field
v-model="password"
label="Passwort"
:type="showPassword ? 'text' : 'password'"
variant="outlined"
prepend-inner-icon="mdi-lock-outline"
:append-inner-icon="showPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
autocomplete="current-password"
required
:disabled="isSubmitting"
@click:append-inner="showPassword = !showPassword"
/>
<div class="form-meta">
<p>Anmeldung erfolgt per sicherem Session-Cookie.</p>
</div>
<v-btn
type="submit"
color="primary"
block
size="large"
prepend-icon="mdi-login"
:loading="isSubmitting"
:disabled="submitDisabled"
>
Anmelden
</v-btn>
<v-btn variant="outlined" block to="/welcome" prepend-icon="mdi-home">Zur Startseite</v-btn>
</v-form>
</section>
</v-container>
</template>
<style scoped>
.login-shell {
--hoard-shell-width: 1040px;
--hoard-gradient-angle: 115deg;
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 45%, var(--color-surface) 55%);
--hoard-gradient-end: var(--color-surface);
--hoard-gradient-end-stop: 52%;
grid-template-columns: minmax(280px, 1fr) minmax(320px, 430px);
}
h1 {
margin-bottom: var(--space-3);
max-width: 18ch;
font-size: clamp(1.9rem, 2vw + 1rem, 2.6rem);
font-weight: 700;
}
.login-intro {
margin-bottom: var(--space-5);
max-width: 44ch;
color: var(--color-text-secondary);
}
.login-points {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: 0;
margin: 0;
list-style: none;
}
.login-points li {
display: flex;
align-items: center;
gap: var(--space-3);
color: var(--color-text-secondary);
font-weight: 500;
}
.login-form {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-6);
border-radius: var(--radius-lg);
}
.form-head h2 {
margin-bottom: var(--space-1);
font-size: 1.45rem;
}
.form-head p {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--font-size-md);
}
.form-meta {
display: flex;
align-items: center;
gap: var(--space-2);
}
.form-meta p {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
@media (width <= 960px) {
.login-shell {
grid-template-columns: 1fr;
}
.login-form {
padding: var(--space-5);
}
.form-meta {
flex-wrap: wrap;
}
}
@media (width <= 600px) {
h1 {
max-width: none;
font-size: clamp(1.55rem, 7vw, 1.95rem);
}
.login-intro {
margin-bottom: var(--space-4);
}
.login-points {
gap: var(--space-2);
}
.login-points li {
align-items: flex-start;
}
.login-form {
gap: var(--space-3);
padding: var(--space-4);
}
.form-meta {
flex-direction: column;
align-items: stretch;
gap: var(--space-2);
}
:deep(.login-form .v-btn) {
min-height: 44px;
}
}
</style>