7e2ca4c9e2
Mass rename of CSS classes, tokens and animations from the hoard- namespace to ui- (classes, variables like --ui-*, and keyframes). Introduces new UI components: EmptyState, StatusPill, and UserAvatar and updates admin views to import and use them. Updates many route/layout components and global.css to use the new ui- patterns and responsive variables. Also updates Impressum contact emails and adds .claude/settings.local.json to allow running npm scripts in the Claude local settings.
174 lines
4.2 KiB
Vue
174 lines
4.2 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { fetchCurrentUser } from '@/services/authSession'
|
||
|
||
const router = useRouter()
|
||
const isLoading = ref(true)
|
||
const isAuthenticated = ref(false)
|
||
|
||
const primaryActionLabel = computed(() => (isAuthenticated.value ? 'Zum Dashboard' : 'Zum Login'))
|
||
const primaryActionIcon = computed(() =>
|
||
isAuthenticated.value ? 'mdi-view-dashboard-outline' : 'mdi-login',
|
||
)
|
||
|
||
async function resolveAuthState() {
|
||
try {
|
||
const user = await fetchCurrentUser({ force: true })
|
||
isAuthenticated.value = user !== null
|
||
} catch {
|
||
isAuthenticated.value = false
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function navigatePrimaryAction() {
|
||
if (isAuthenticated.value) {
|
||
await router.replace({ name: 'Dashboard' })
|
||
return
|
||
}
|
||
|
||
await router.replace({ name: 'Login' })
|
||
}
|
||
|
||
async function navigateBack() {
|
||
if (window.history.length > 1) {
|
||
router.back()
|
||
return
|
||
}
|
||
|
||
await router.replace({ name: isAuthenticated.value ? 'Dashboard' : 'Home' })
|
||
}
|
||
|
||
onMounted(() => {
|
||
void resolveAuthState()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<v-container fluid class="forbidden-page ui-page ui-page--centered">
|
||
<section class="forbidden-shell ui-panel ui-panel-gradient ui-spotlight">
|
||
<div class="forbidden-icon">
|
||
<span class="forbidden-icon__halo" aria-hidden="true" />
|
||
<v-icon icon="mdi-shield-alert-outline" size="40" />
|
||
</div>
|
||
|
||
<header class="forbidden-head">
|
||
<p class="ui-kicker ui-kicker--wide">Fehlende Berechtigung</p>
|
||
<h1>Kein Zugriff.</h1>
|
||
<p>
|
||
Dein Konto hat aktuell keine ausreichende Rolle, um diese Seite zu sehen. Falls das ein Fehler ist,
|
||
wende dich an einen Admin – oder wechsle zurück in deinen freigegebenen Bereich.
|
||
</p>
|
||
</header>
|
||
|
||
<div class="forbidden-actions ui-action-row">
|
||
<v-btn
|
||
variant="elevated"
|
||
:prepend-icon="primaryActionIcon"
|
||
:loading="isLoading"
|
||
:disabled="isLoading"
|
||
@click="navigatePrimaryAction"
|
||
>
|
||
{{ primaryActionLabel }}
|
||
</v-btn>
|
||
<v-btn variant="outlined" prepend-icon="mdi-arrow-left" @click="navigateBack">
|
||
Zurück
|
||
</v-btn>
|
||
</div>
|
||
</section>
|
||
</v-container>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.forbidden-page {
|
||
--ui-centered-offset: 200px;
|
||
}
|
||
|
||
.forbidden-shell {
|
||
--ui-gradient-angle: 130deg;
|
||
--ui-gradient-start: color-mix(in srgb, var(--color-warning) 14%, var(--color-surface) 86%);
|
||
--ui-gradient-end: var(--color-surface);
|
||
--ui-gradient-end-stop: 65%;
|
||
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-5);
|
||
align-items: center;
|
||
text-align: center;
|
||
width: min(640px, 100%);
|
||
padding: var(--space-10) var(--space-7);
|
||
border-radius: var(--radius-xl);
|
||
}
|
||
|
||
.forbidden-icon {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: var(--radius-full);
|
||
background:
|
||
linear-gradient(135deg, var(--color-warning), color-mix(in srgb, var(--color-warning) 60%, var(--color-danger) 40%));
|
||
color: #fff;
|
||
box-shadow:
|
||
var(--shadow-md),
|
||
inset 0 0 0 2px color-mix(in srgb, var(--color-accent-lime) 18%, transparent);
|
||
}
|
||
|
||
.forbidden-icon__halo {
|
||
position: absolute;
|
||
inset: -10px;
|
||
border-radius: var(--radius-full);
|
||
background:
|
||
radial-gradient(
|
||
closest-side,
|
||
color-mix(in srgb, var(--color-warning) 38%, transparent),
|
||
transparent 70%
|
||
);
|
||
filter: blur(12px);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.forbidden-head h1 {
|
||
margin: 0 0 var(--space-2);
|
||
font-size: clamp(1.8rem, 1.4rem + 1vw, 2.4rem);
|
||
font-weight: 700;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.forbidden-head p {
|
||
margin: 0 auto;
|
||
max-width: 50ch;
|
||
color: var(--color-text-secondary);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.forbidden-actions {
|
||
justify-content: center;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
.forbidden-shell {
|
||
animation: ui-soft-enter 280ms both;
|
||
}
|
||
}
|
||
|
||
@media (width <= 600px) {
|
||
.forbidden-shell {
|
||
padding: var(--space-7) var(--space-5);
|
||
}
|
||
|
||
.forbidden-actions {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.forbidden-actions .v-btn) {
|
||
width: 100%;
|
||
min-height: 44px;
|
||
}
|
||
}
|
||
</style>
|