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
+47 -8
View File
@@ -8,9 +8,12 @@
"name": "-", "name": "-",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.2.10",
"@mdi/font": "^7.4.47",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.31", "vue": "^3.5.31",
"vue-router": "^5.0.4" "vue-router": "^5.0.4",
"vuetify": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node24": "^24.0.4", "@tsconfig/node24": "^24.0.4",
@@ -59,7 +62,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -515,10 +517,20 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@fontsource/roboto": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.10.tgz",
"integrity": "sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -564,6 +576,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mdi/font": {
"version": "7.4.47",
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
"license": "Apache-2.0"
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
@@ -886,7 +904,6 @@
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -1342,7 +1359,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.12", "baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782", "caniuse-lite": "^1.0.30001782",
@@ -2304,7 +2320,6 @@
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/devtools-api": "^7.7.7" "@vue/devtools-api": "^7.7.7"
}, },
@@ -2616,7 +2631,6 @@
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -2705,7 +2719,6 @@
"integrity": "sha512-baBr4jUVSLJ0RPyZ2nK0zS2+W8hNHbM4hEzfvllukmRPVS3xDG5ATTNtbRXrKIOE2b8/FsPWJAOnuIxcs7g3cw==", "integrity": "sha512-baBr4jUVSLJ0RPyZ2nK0zS2+W8hNHbM4hEzfvllukmRPVS3xDG5ATTNtbRXrKIOE2b8/FsPWJAOnuIxcs7g3cw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
@@ -2921,7 +2934,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.32", "@vue/compiler-dom": "3.5.32",
"@vue/compiler-sfc": "3.5.32", "@vue/compiler-sfc": "3.5.32",
@@ -3033,6 +3045,33 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/vuetify": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.5.tgz",
"integrity": "sha512-pFysKOHuY3dROTVh9PdlhVz50ZR0E5/goY5ecTXc8F8tajUA2ee3xZ8Lqs1WtEw/X3w93wx/LogyjgaQCAL/Ig==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/johnleider"
},
"peerDependencies": {
"typescript": ">=4.7",
"vite-plugin-vuetify": ">=2.1.0",
"vue": "^3.5.0",
"webpack-plugin-vuetify": ">=3.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"vite-plugin-vuetify": {
"optional": true
},
"webpack-plugin-vuetify": {
"optional": true
}
}
},
"node_modules/webpack-virtual-modules": { "node_modules/webpack-virtual-modules": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+4 -1
View File
@@ -12,9 +12,12 @@
"format": "prettier --write --experimental-cli src/" "format": "prettier --write --experimental-cli src/"
}, },
"dependencies": { "dependencies": {
"@fontsource/roboto": "^5.2.10",
"@mdi/font": "^7.4.47",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.31", "vue": "^3.5.31",
"vue-router": "^5.0.4" "vue-router": "^5.0.4",
"vuetify": "^4.0.5"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node24": "^24.0.4", "@tsconfig/node24": "^24.0.4",
-11
View File
@@ -1,11 +0,0 @@
<script setup lang="ts"></script>
<template>
<h1>You did it!</h1>
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
</template>
<style scoped></style>
+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>
+3 -1
View File
@@ -1,12 +1,14 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './Layout.vue'
import router from './router' import router from './router'
import vuetify from './plugins/vuetify'
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(router) app.use(router)
app.use(vuetify)
app.mount('#app') app.mount('#app')
+72
View File
@@ -0,0 +1,72 @@
import type { RouteRecordRaw } from 'vue-router'
import Home from '@/routes/Home.vue'
import NotFound from '@/routes/404NotFound.vue'
import Login from '@/routes/authentication/Login.vue'
import Impressum from '@/routes/Impressum.vue'
export enum Visibility {
Hidden,
Authenticated,
Unauthenticated,
Authorized,
Public,
Footer,
}
export interface LayoutRoute {
path: string
name: string
description: string
icon: string
disableFooter?: boolean
visible: Visibility
meta?: RouteRecordRaw
}
export const routes: LayoutRoute[] = [
{
path: '/',
name: 'Startseite',
description: 'Uebersicht der Anwendung',
icon: 'mdi-home',
visible: Visibility.Public,
meta: {
name: 'Home',
path: '/',
component: Home,
},
},
{
path: '/login',
name: 'Login',
description: 'Logge dich ein',
icon: 'mdi-login',
visible: Visibility.Hidden,
meta: {
path: '/login',
name: 'Login',
component: Login,
},
},
{
path: '/impressum',
name: 'Impressum',
description: 'Impressum der Anwendung',
icon: 'mdi-file-document',
visible: Visibility.Footer,
meta: {
path: '/impressum',
name: 'Impressum',
component: Impressum,
},
},
{
path: '/notFound',
name: 'Nicht gefunden',
description: 'Diese Seite wurde nicht gefunden',
icon: 'mdi-information-outline',
visible: Visibility.Hidden,
meta: { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
},
]
+27
View File
@@ -0,0 +1,27 @@
import 'vuetify/styles'
import '@fontsource/roboto/100.css'
import '@fontsource/roboto/300.css'
import '@fontsource/roboto/400.css'
import '@fontsource/roboto/500.css'
import '@fontsource/roboto/700.css'
import '@fontsource/roboto/900.css'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import '@mdi/font/css/materialdesignicons.css'
import { aliases, mdi } from 'vuetify/iconsets/mdi'
export default createVuetify({
components,
directives,
theme: {
defaultTheme: 'dark',
},
icons: {
defaultSet: 'mdi',
aliases,
sets: {
mdi,
},
},
})
+3 -2
View File
@@ -1,8 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { routes } from '@/plugins/routesLayout'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [], routes: routes.filter((x) => x.meta !== undefined).map((x) => x.meta) as RouteRecordRaw[],
}) })
export default router export default router
-1
View File
@@ -1 +0,0 @@
export const
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts"></script>
<template>
<v-container fluid class="fill-height d-flex flex-column justify-center align-center">
<h1 class="error-title text-h1 font-weight-bold">404</h1>
<p class="error-message text-h5">Seite nicht gefunden</p>
<router-link to="/" class="text-primary text-decoration-none font-weight-medium mt-5">
Zurueck zur Startseite
</router-link>
</v-container>
</template>
<style scoped>
.error-title {
font-size: 3rem;
font-weight: bold;
margin-bottom: 1rem;
letter-spacing: 2px;
}
.error-message {
font-size: 1.25rem;
opacity: 0.85;
}
a:hover {
text-decoration: underline !important;
}
</style>
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts"></script>
<template>
<v-container class="py-10">
<v-row justify="center">
<v-col cols="12" md="10" lg="8">
<v-card rounded="lg" elevation="2">
<v-card-title class="text-h4">Willkommen bei Hoard</v-card-title>
<v-card-text class="text-body-1">
Dein Vuetify-Setup ist aktiv. Ueber das Menue links kannst du zu den weiteren Seiten
navigieren.
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped></style>
+19
View File
@@ -0,0 +1,19 @@
<script setup lang="ts"></script>
<template>
<v-container class="py-10">
<v-row justify="center">
<v-col cols="12" md="10" lg="8">
<v-card rounded="lg" elevation="2">
<v-card-title class="text-h5">Impressum</v-card-title>
<v-card-text>
Diese Seite ist als Platzhalter angelegt. Trage hier deine Firmen- oder Vereinsdaten
ein.
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped></style>
+24
View File
@@ -0,0 +1,24 @@
<script setup lang="ts"></script>
<template>
<v-container class="py-10">
<v-row justify="center">
<v-col cols="12" sm="10" md="6" lg="4">
<v-card rounded="lg" elevation="2">
<v-card-title class="text-h5">Login</v-card-title>
<v-card-text>
<v-text-field label="E-Mail" prepend-inner-icon="mdi-email-outline" />
<v-text-field
label="Passwort"
type="password"
prepend-inner-icon="mdi-lock-outline"
/>
<v-btn color="primary" block prepend-icon="mdi-login">Anmelden</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped></style>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["env.d.ts", "src/**/*.vue", "src/**/*.ts"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
// Extra safety for array and object lookups, but may have false positives. // Extra safety for array and object lookups, but may have false positives.