Integrate Vuetify layout and routing

Add a Vuetify-powered application shell (Layout.vue) replacing the previous App.vue, including app bar, navigation drawer, theme toggle, footer and localStorage persistence for theme/drawer. Introduce a routesLayout plugin with a Visibility enum and centralized LayoutRoute definitions; add route components (Home, Impressum, Login, 404NotFound) and update the router to build routes from the new layout definitions. Register Vuetify in main.ts and add dependencies (vuetify, @fontsource/roboto, @mdi/font) in package.json; update tsconfig.app.json to include .ts files. Package-lock.json updated accordingly.
This commit is contained in:
Jonas
2026-04-15 20:56:47 +02:00
parent 58744e46b6
commit b9101a4582
14 changed files with 376 additions and 25 deletions
+127
View File
@@ -0,0 +1,127 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import { useTheme } from 'vuetify'
import { useRoute, useRouter } from 'vue-router'
import { Visibility, routes } from '@/plugins/routesLayout'
const theme = useTheme()
const route = useRoute()
const router = useRouter()
const showDrawer = ref(true)
function changeTheme() {
const nextTheme = theme.global.name.value === 'dark' ? 'light' : 'dark'
theme.global.name.value = nextTheme
localStorage.setItem('theme', nextTheme)
}
function toggleDrawer() {
showDrawer.value = !showDrawer.value
localStorage.setItem('drawer', showDrawer.value ? 'Y' : 'N')
}
function changeWebsiteTitle(path: string) {
const currentPageInfo = routes.find((x) => x.path === path)
if (currentPageInfo) {
document.title = `Hoard | ${currentPageInfo.name}`
return
}
document.title = 'Hoard'
}
onMounted(() => {
const storedTheme = localStorage.getItem('theme')
const fallbackTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
theme.global.name.value = storedTheme === 'dark' || storedTheme === 'light' ? storedTheme : fallbackTheme
const storedDrawer = localStorage.getItem('drawer')
showDrawer.value = storedDrawer ? storedDrawer.startsWith('Y') : true
})
watch(
() => route.path,
(newPath) => {
changeWebsiteTitle(newPath)
},
{ immediate: true },
)
</script>
<template>
<v-app>
<v-app-bar elevation="1">
<template #prepend>
<v-app-bar-nav-icon
v-tooltip="!showDrawer ? 'Menue oeffnen' : 'Menue schliessen'"
@click="toggleDrawer()"
/>
</template>
<v-app-bar-title class="title" @click="router.push({ name: 'Home' })">
<span class="pointer">Hoard</span>
</v-app-bar-title>
<v-tooltip v-if="!$vuetify.display.mobile">
<template #activator="{ props }">
<v-btn icon v-bind="props" to="/login">
<v-icon>mdi-account</v-icon>
</v-btn>
</template>
Account
</v-tooltip>
<v-tooltip>
<template #activator="{ props }">
<v-btn icon @click="changeTheme()" v-bind="props">
<v-icon>mdi-brightness-6</v-icon>
</v-btn>
</template>
{{ theme.global.name.value === 'dark' ? 'Hellen Modus aktivieren' : 'Dunklen Modus aktivieren' }}
</v-tooltip>
</v-app-bar>
<v-navigation-drawer
v-model="showDrawer"
:location="$vuetify.display.mobile ? 'bottom' : undefined"
:permanent="!$vuetify.display.mobile"
>
<v-list>
<v-list-item
v-for="item in routes.filter((x) => x.visible === Visibility.Public)"
:key="item.path"
:to="item.path"
:active="route.path === item.path"
link
:prepend-icon="item.icon"
:title="item.name"
class="rounded-lg mr-1 ml-1"
/>
</v-list>
</v-navigation-drawer>
<v-main>
<router-view />
<v-footer class="d-flex align-center justify-center ga-2 flex-wrap flex-grow-1 py-3">
<v-btn
v-for="link in routes.filter((x) => x.visible === Visibility.Footer)"
:key="link.path"
:to="link.path"
:text="link.name"
variant="text"
rounded
/>
<div class="flex-1-0-100 text-center mt-2">
{{ new Date().getFullYear() }} - <strong>Hoard</strong>
</div>
</v-footer>
</v-main>
</v-app>
</template>
<style scoped>
.pointer {
cursor: pointer;
}
</style>